Compare commits

..

5 Commits

Author SHA1 Message Date
orangecoding
6ae0c9749b update dependencies 2026-02-16 12:30:59 +01:00
orangecoding
10e40e038e adding check if fredy is running in docker 2026-02-16 12:29:02 +01:00
orangecoding
4ba6828939 adding release tool 2026-02-05 12:02:18 +01:00
orangecoding
d09770dae2 fancy, almost impossible to see animation on dashboard 2026-02-05 09:54:42 +01:00
orangecoding
248e4d2562 improve tracking 2026-02-04 14:41:55 +01:00
7 changed files with 760 additions and 444 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ npm-debug.log
.DS_Store
.idea
.vscode
tools/release/config.json

View File

@@ -35,6 +35,7 @@ WORKDIR /fredy
RUN apk add --no-cache chromium curl
ENV NODE_ENV=production \
IS_DOCKER=true \
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

View File

@@ -14,56 +14,70 @@ import { getSettings } from '../storage/settingsStorage.js';
const deviceId = getUniqueId() || 'N/A';
const version = await getPackageVersion();
const FREDY_TRACKING_URL = 'https://fredy.orange-coding.net/tracking';
const isDocker = process.env.IS_DOCKER != null;
export const trackMainEvent = async () => {
const staticTrackingData = {
operatingSystem: os.platform(),
osVersion: os.release(),
isDocker,
arch: process.arch,
language: process.env.LANG || 'en',
nodeVersion: process.version || 'N/A',
deviceId,
version,
};
const shouldTrack = async () => {
const settings = await getSettings();
return settings.analyticsEnabled && !inDevMode();
};
const sendTrackingData = async (endpoint, payload) => {
try {
const settings = await getSettings();
if (settings.analyticsEnabled && !inDevMode()) {
const activeProvider = new Set();
const activeAdapter = new Set();
const jobs = getJobs();
if (jobs != null && jobs.length > 0) {
jobs.forEach((job) => {
job.provider.forEach((provider) => activeProvider.add(provider.id));
job.notificationAdapter.forEach((adapter) => activeAdapter.add(adapter.id));
});
const trackingObj = enrichTrackingObject({
adapter: Array.from(activeAdapter),
provider: Array.from(activeProvider),
});
await fetch(`${FREDY_TRACKING_URL}/main`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(trackingObj),
});
}
const response = await fetch(`${FREDY_TRACKING_URL}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: payload ? JSON.stringify(payload) : undefined,
});
if (!response.ok) {
logger.warn(`Error sending tracking data to ${endpoint}. Status: ${response.status}`);
}
} catch (error) {
logger.warn('Error sending tracking data', error);
logger.warn(`Error sending tracking data to ${endpoint}`, error);
}
};
export const trackMainEvent = async () => {
if (!(await shouldTrack())) return;
const activeProvider = new Set();
const activeAdapter = new Set();
const jobs = getJobs();
if (jobs != null && jobs.length > 0) {
jobs.forEach((job) => {
job.provider.forEach((provider) => activeProvider.add(provider.id));
job.notificationAdapter.forEach((adapter) => activeAdapter.add(adapter.id));
});
const trackingObj = await enrichTrackingObject({
adapter: Array.from(activeAdapter),
provider: Array.from(activeProvider),
});
await sendTrackingData('/main', trackingObj);
}
};
export const trackFeature = async (feature) => {
try {
const settings = await getSettings();
if (settings.analyticsEnabled && !inDevMode()) {
const trackingObj = await enrichTrackingObject({
feature,
});
if (!(await shouldTrack())) return;
await fetch(`${FREDY_TRACKING_URL}/feature`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(trackingObj),
});
}
} catch (error) {
logger.warn('Error tracking feature', error);
}
const trackingObj = await enrichTrackingObject({
feature,
});
await sendTrackingData('/feature', trackingObj);
};
/**
@@ -72,34 +86,17 @@ export const trackFeature = async (feature) => {
export async function trackDemoAccessed() {
const settings = await getSettings();
if (settings.analyticsEnabled && !inDevMode() && settings.demoMode) {
try {
await fetch(`${FREDY_TRACKING_URL}/demo/accessed`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
logger.warn('Error sending tracking data', error);
}
const trackingObj = await enrichTrackingObject({});
await sendTrackingData('/demo/accessed', trackingObj);
}
}
async function enrichTrackingObject(trackingObject) {
const settings = await getSettings();
const operatingSystem = os.platform();
const osVersion = os.release();
const arch = process.arch;
const language = process.env.LANG || 'en';
const nodeVersion = process.version || 'N/A';
return {
...trackingObject,
...staticTrackingData,
isDemo: settings.demoMode,
operatingSystem,
osVersion,
arch,
nodeVersion,
language,
deviceId,
version,
};
}

View File

@@ -1,6 +1,6 @@
{
"name": "fredy",
"version": "19.3.5",
"version": "19.3.8",
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
"scripts": {
"prepare": "husky",
@@ -17,7 +17,8 @@
"lint:fix": "yarn lint --fix",
"migratedb": "node lib/services/storage/migrations/migrate.js",
"migratedb:overwrite": "x-var MIGRATION_ALLOW_CHECKSUM_UPDATE=true node lib/services/storage/migrations/migrate.js",
"copyright": "node ./copyright.js"
"copyright": "node ./copyright.js",
"release": "node ./tools/release/release.js"
},
"type": "module",
"lint-staged": {
@@ -59,11 +60,11 @@
"Firefox ESR"
],
"dependencies": {
"@douyinfe/semi-icons": "^2.90.13",
"@douyinfe/semi-ui": "2.90.13",
"@douyinfe/semi-ui-19": "^2.90.13",
"@douyinfe/semi-icons": "^2.91.0",
"@douyinfe/semi-ui": "2.91.0",
"@douyinfe/semi-ui-19": "^2.91.0",
"@sendgrid/mail": "8.1.6",
"@vitejs/plugin-react": "5.1.3",
"@vitejs/plugin-react": "5.1.4",
"adm-zip": "^0.5.16",
"better-sqlite3": "^12.6.2",
"body-parser": "2.2.2",
@@ -72,14 +73,14 @@
"cookie-session": "2.1.1",
"handlebars": "4.7.8",
"lodash": "4.17.23",
"maplibre-gl": "^5.17.0",
"maplibre-gl": "^5.18.0",
"nanoid": "5.1.6",
"node-cron": "^4.2.1",
"node-fetch": "3.3.2",
"node-mailjet": "6.0.11",
"p-throttle": "^8.1.0",
"package-up": "^5.0.0",
"puppeteer": "^24.36.1",
"puppeteer": "^24.37.2",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",
"query-string": "9.3.1",
@@ -90,7 +91,7 @@
"react-router": "7.13.0",
"react-router-dom": "7.13.0",
"restana": "5.1.0",
"semver": "^7.7.3",
"semver": "^7.7.4",
"serve-static": "2.2.1",
"slack": "11.0.2",
"vite": "7.3.1",
@@ -103,7 +104,8 @@
"@babel/preset-env": "7.29.0",
"@babel/preset-react": "7.28.5",
"chai": "6.2.2",
"eslint": "9.39.2",
"chalk": "^5.6.2",
"eslint": "10.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-react": "7.37.5",
"esmock": "2.7.3",

196
tools/release/release.js Normal file
View File

@@ -0,0 +1,196 @@
/*
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
import fs from 'fs';
import path from 'path';
import { execSync, spawn } from 'child_process';
import fetch from 'node-fetch';
import chalk from 'chalk';
import { fileURLToPath } from 'url';
/**
* Release Tool for Fredy
*
* This tool automates the process of creating a GitHub release.
* It fetches the latest release, compares it with the current master branch,
* allows manual editing of commit messages, and creates a new release on GitHub.
*/
// Define __dirname for ESM
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Configuration and Paths
const CONFIG_PATH = path.join(__dirname, 'config.json');
const PACKAGE_JSON_PATH = path.join(__dirname, '../../package.json');
const REPO = 'orangecoding/fredy';
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
const GITHUB_TOKEN = config.github_token;
/**
* Main function to execute the release process
*/
async function createRelease() {
/* eslint-disable no-console */
try {
console.log(chalk.cyan('🚀 Starting release process...'));
// 1. Load Configuration
if (!fs.existsSync(CONFIG_PATH)) {
console.error(chalk.red('❌ Error: config.json not found in tools/release/'));
process.exit(1);
}
if (!GITHUB_TOKEN) {
console.error(chalk.red('❌ Error: GitHub token not configured.'));
process.exit(1);
}
// 2. Get current version from package.json
const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8'));
const version = packageJson.version;
const tag = version; // Using version as tag
console.log(chalk.blue(`📦 Target version: ${version}`));
// 3. Check if release already exists
console.log(chalk.yellow('🔍 Checking if release already exists...'));
const existingReleaseResponse = await fetch(`https://api.github.com/repos/${REPO}/releases/tags/${tag}`, {
headers: {
Authorization: `token ${GITHUB_TOKEN}`,
Accept: 'application/vnd.github.v3+json',
},
});
if (existingReleaseResponse.status === 200) {
console.error(chalk.red(`❌ Error: A release with tag ${tag} already exists.`));
process.exit(1);
}
// 4. Fetch latest release to find the starting point for the diff
console.log(chalk.yellow('📡 Fetching latest release from GitHub...'));
const latestReleaseResponse = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, {
headers: {
Authorization: `token ${GITHUB_TOKEN}`,
Accept: 'application/vnd.github.v3+json',
},
});
if (!latestReleaseResponse.ok) {
console.error(chalk.red('❌ Error fetching latest release.'));
const errorData = await latestReleaseResponse.json();
console.error(chalk.red(JSON.stringify(errorData)));
process.exit(1);
}
const latestRelease = await latestReleaseResponse.json();
const latestTag = latestRelease.tag_name;
console.log(chalk.green(`✅ Latest release found: ${latestTag}`));
// 5. Ensure the latest tag is available locally
console.log(chalk.yellow(`📡 Fetching tag ${latestTag} from remote...`));
try {
execSync(`git fetch origin tag ${latestTag} --no-tags`);
} catch (error) {
console.error(chalk.red(`❌ Error fetching tag ${latestTag} from origin.`));
console.error(error.message);
// We don't exit here, maybe it's already there but fetch failed for some reason
}
// 6. Get commit messages between latest tag and current HEAD
console.log(chalk.yellow(`Git diff: ${latestTag} .. HEAD`));
let commitMessages;
try {
commitMessages = execSync(`git log ${latestTag}..HEAD --pretty=format:"- %s"`).toString().trim();
} catch (error) {
console.error(chalk.red('❌ Error running git log. Make sure the latest tag is available locally.'), error);
process.exit(1);
}
if (!commitMessages) {
console.log(chalk.magenta('⚠️ No new commits found since last release.'));
commitMessages = '- No changes recorded';
}
// 7. Open commit messages in editor for manual adjustment
const tempFilePath = path.join(__dirname, 'CHANGELOG_EDIT.tmp');
const initialContent = `# Release Notes for ${version}\n# Edit the messages below. Lines starting with # will be ignored.\n\n${commitMessages}`;
fs.writeFileSync(tempFilePath, initialContent);
console.log(chalk.blue('📝 Opening editor for release notes (using nano or $EDITOR)...'));
await openInEditor(tempFilePath);
// 8. Read edited content
let editedContent = fs
.readFileSync(tempFilePath, 'utf8')
.split('\n')
.filter((line) => !line.startsWith('#'))
.join('\n')
.trim();
fs.unlinkSync(tempFilePath); // Clean up temp file
if (!editedContent) {
console.error(chalk.red('❌ Release notes are empty. Aborting release.'));
process.exit(1);
}
// 9. Create the new release
console.log(chalk.cyan(`🚀 Creating release ${version} on GitHub...`));
const createResponse = await fetch(`https://api.github.com/repos/${REPO}/releases`, {
method: 'POST',
headers: {
Authorization: `token ${GITHUB_TOKEN}`,
Accept: 'application/vnd.github.v3+json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
tag_name: tag,
name: version,
body: editedContent,
draft: false,
prerelease: false,
}),
});
if (createResponse.status === 201) {
const data = await createResponse.json();
console.log(chalk.green('🎉 Release successfully created!'));
console.log(chalk.green(`🔗 URL: ${data.html_url}`));
} else {
const errorData = await createResponse.json();
console.error(chalk.red('❌ Failed to create release.'));
console.error(chalk.red(JSON.stringify(errorData, null, 2)));
process.exit(1);
}
} catch (error) {
console.error(chalk.red('💥 An unexpected error occurred:'));
console.error(error);
process.exit(1);
}
}
/**
* Helper to open a file in a terminal editor
* @param {string} filePath
*/
function openInEditor(filePath) {
return new Promise((resolve, reject) => {
const editor = process.env.EDITOR || 'nano';
const child = spawn(editor, [filePath], {
stdio: 'inherit',
});
child.on('exit', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Editor exited with code ${code}`));
}
});
});
}
await createRelease();
/* eslint-enable no-console */

View File

@@ -2,30 +2,29 @@
width: 100%;
height: 140px;
margin-bottom: 16px;
transition: transform 0.2s, box-shadow 0.2s;
transition: transform 0.2s;
background-color: rgba(36, 36, 36, 0.9);
backdrop-filter: blur(8px);
border: 1px solid var(--semi-color-border);
--pulse-color: rgba(255, 255, 255, 0.1);
position: relative;
z-index: 1;
overflow: visible;
&:hover {
transform: translateY(-4px);
background-color: rgba(36, 36, 36, 1);
&.blue {
box-shadow: 0 8px 24px -5px var(--semi-color-primary);
}
&.orange {
box-shadow: 0 8px 24px -5px var(--semi-color-warning);
}
&.green {
box-shadow: 0 8px 24px -5px var(--semi-color-success);
}
&.purple {
box-shadow: 0 8px 24px -5px var(--semi-color-info);
}
&.gray {
box-shadow: 0 8px 24px -5px rgba(255, 255, 255, 0.4);
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: inherit;
box-shadow: 0 4px 25px -2px var(--pulse-color);
opacity: 0;
animation: pulse 5s infinite ease-in-out;
pointer-events: none;
z-index: -1;
will-change: opacity;
}
&__icon {
@@ -46,22 +45,36 @@
}
&.blue {
box-shadow: 0 4px 20px -5px var(--semi-color-primary);
--pulse-color: var(--semi-color-primary);
box-shadow: 0 4px 20px -5px var(--pulse-color);
}
&.orange {
box-shadow: 0 4px 20px -5px var(--semi-color-warning);
--pulse-color: var(--semi-color-warning);
box-shadow: 0 4px 20px -5px var(--pulse-color);
}
&.green {
box-shadow: 0 4px 20px -5px var(--semi-color-success);
--pulse-color: var(--semi-color-success);
box-shadow: 0 4px 20px -5px var(--pulse-color);
}
&.purple {
box-shadow: 0 4px 20px -5px var(--semi-color-info);
--pulse-color: var(--semi-color-info);
box-shadow: 0 4px 20px -5px var(--pulse-color);
}
&.gray {
box-shadow: 0 4px 20px -5px rgba(255, 255, 255, 0.2);
--pulse-color: rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 20px -5px var(--pulse-color);
}
}
@keyframes pulse {
0%, 100% {
opacity: 0.1;
}
50% {
opacity: 0.5;
}
}

802
yarn.lock

File diff suppressed because it is too large Load Diff