This commit is contained in:
nadja
2026-11-01 20:14:25 +01:00
parent 828c38c779
commit c4685c2152
1686 changed files with 570761 additions and 560 deletions

145
backend/dist/analytics-db.js vendored Normal file
View File

@@ -0,0 +1,145 @@
import Database from 'better-sqlite3';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const DB_FILE = process.env.ANALYTICS_PATH || join(__dirname, 'analytics.db');
const db = new Database(DB_FILE);
db.pragma('journal_mode = WAL');
// Tabelle erstellen falls nicht vorhanden
db.exec(`
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT,
user_id INTEGER,
timestamp TEXT,
metadata TEXT
);
`);
function now() {
return new Date().toISOString().replace('T', ' ').slice(0, 19);
}
export function trackEvent(type, userId, metadata = {}) {
const timestamp = now();
const metaStr = JSON.stringify(metadata);
const stmt = db.prepare('INSERT INTO events (type, user_id, timestamp, metadata) VALUES (?, ?, ?, ?)');
const info = stmt.run(type, userId || null, timestamp, metaStr);
return {
id: info.lastInsertRowid,
type,
user_id: userId || null,
timestamp,
...metadata,
};
}
export function getEvents({ from, to, type, user_id, limit = 100 } = {}) {
let query = 'SELECT * FROM events WHERE 1=1';
const params = [];
if (from) {
query += ' AND timestamp >= ?';
params.push(from);
}
if (to) {
query += ' AND timestamp <= ?';
params.push(to);
}
if (type) {
query += ' AND type = ?';
params.push(type);
}
if (user_id) {
query += ' AND user_id = ?';
params.push(Number(user_id));
}
query += ' ORDER BY timestamp DESC LIMIT ?';
params.push(limit);
const rows = db.prepare(query).all(...params);
return rows.map(r => {
let meta = {};
try {
meta = JSON.parse(r.metadata || '{}');
}
catch { }
return {
id: r.id,
type: r.type,
user_id: r.user_id,
timestamp: r.timestamp,
...meta,
};
});
}
export function getOverview() {
const today = new Date().toISOString().slice(0, 10);
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
const allPageviews = db.prepare("SELECT timestamp FROM events WHERE type = 'page_view'").all();
const pageviewsToday = allPageviews.filter(e => e.timestamp.startsWith(today)).length;
const pageviewsYesterday = allPageviews.filter(e => e.timestamp.startsWith(yesterday)).length;
const pageviewsWeek = allPageviews.filter(e => e.timestamp.slice(0, 10) >= weekAgo).length;
const pageviewsTotal = allPageviews.length;
const activeUsersTotal = db.prepare("SELECT COUNT(DISTINCT user_id) as count FROM events WHERE user_id IS NOT NULL").get().count;
const activeUsersToday = db.prepare("SELECT COUNT(DISTINCT user_id) as count FROM events WHERE user_id IS NOT NULL AND timestamp LIKE ?").get(`${today}%`).count;
const recipesCreated = db.prepare("SELECT COUNT(*) as count FROM events WHERE type = 'recipe_created'").get().count;
const recipesUpdated = db.prepare("SELECT COUNT(*) as count FROM events WHERE type = 'recipe_updated'").get().count;
const recipesDeleted = db.prepare("SELECT COUNT(*) as count FROM events WHERE type = 'recipe_deleted'").get().count;
const logins = db.prepare("SELECT COUNT(*) as count FROM events WHERE type = 'user_login'").get().count;
return {
pageviews: { today: pageviewsToday, yesterday: pageviewsYesterday, week: pageviewsWeek, total: pageviewsTotal },
activeUsers: { total: activeUsersTotal, today: activeUsersToday },
recipes: { created: recipesCreated, updated: recipesUpdated, deleted: recipesDeleted },
logins,
};
}
export function getPageviewsByDay(from, to) {
let query = "SELECT SUBSTR(timestamp, 1, 10) as day, COUNT(*) as count FROM events WHERE type = 'page_view'";
const params = [];
if (from) {
query += " AND SUBSTR(timestamp, 1, 10) >= ?";
params.push(from);
}
if (to) {
query += " AND SUBSTR(timestamp, 1, 10) <= ?";
params.push(to);
}
query += " GROUP BY day ORDER BY day ASC";
const rows = db.prepare(query).all(...params);
return rows.map(r => ({ date: r.day, count: r.count }));
}
export function getTopUsers(limit = 10) {
const rows = db.prepare(`
SELECT user_id,
SUM(CASE WHEN type = 'user_login' THEN 1 ELSE 0 END) as logins,
SUM(CASE WHEN type = 'recipe_created' THEN 1 ELSE 0 END) as recipes_created,
SUM(CASE WHEN type = 'recipe_updated' THEN 1 ELSE 0 END) as recipes_updated,
COUNT(*) as total_events
FROM events
WHERE user_id IS NOT NULL
GROUP BY user_id
ORDER BY total_events DESC
LIMIT ?
`).all(limit);
return rows;
}
export function getTopRecipes(limit = 10) {
const pageviews = db.prepare("SELECT metadata FROM events WHERE type = 'page_view'").all();
const recipeViews = {};
for (const row of pageviews) {
try {
const meta = JSON.parse(row.metadata || '{}');
if (meta.path && meta.path.startsWith('/recipe/')) {
const id = meta.path.split('/')[2];
if (id) {
recipeViews[id] = (recipeViews[id] || 0) + 1;
}
}
}
catch { }
}
return Object.entries(recipeViews)
.map(([recipe_id, views]) => ({ recipe_id: Number(recipe_id), views }))
.sort((a, b) => b.views - a.views)
.slice(0, limit);
}
export function getRecentActivity(limit = 50) {
return getEvents({ limit });
}