From 7814548e11309d44cd20229ec32b1b710494aeca Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Mon, 9 Feb 2026 22:31:22 +0100 Subject: [PATCH] Production ready --- .claude/ralph-loop.local.md | 18 +- README.md | 35 +- SETUP.md | 29 +- backend/.env.example | 8 +- backend/.eslintrc.json | 14 + backend/nul | 0 backend/src/config.ts | 8 +- backend/src/db/index.ts | 24 +- backend/src/db/migrations/tmpclaude-0f94-cwd | 2 +- backend/src/index.ts | 57 +- backend/src/routes/tools.ts | 132 +- backend/src/routes/waitlist.ts | 219 +-- backend/src/services/digest.ts | 29 +- backend/src/services/monitor.ts | 25 +- backend/src/services/scheduler.ts | 31 +- backend/src/types/serpapi.d.ts | 6 + backend/tmpclaude-005a-cwd | 2 +- backend/tmpclaude-08a7-cwd | 2 +- backend/tmpclaude-2dec-cwd | 2 +- backend/tmpclaude-3919-cwd | 2 +- backend/tmpclaude-7d8e-cwd | 2 +- backend/tmpclaude-b7cf-cwd | 2 +- backend/tmpclaude-cd46-cwd | 2 +- backend/tmpclaude-d344-cwd | 2 +- backend/tmpclaude-e961-cwd | 2 +- findings.md | 1603 +++++++++-------- frontend/app/blog/page.tsx | 16 +- frontend/app/features/[slug]/page.tsx | 170 ++ frontend/app/features/page.tsx | 112 ++ frontend/app/forgot-password/page.tsx | 14 +- frontend/app/globals.css | 32 + frontend/app/layout.tsx | 276 ++- frontend/app/login/page.tsx | 36 +- frontend/app/monitors/page.tsx | 12 +- frontend/app/opengraph-image.tsx | 139 ++ frontend/app/page.tsx | 166 +- frontend/app/privacy/page.tsx | 234 ++- frontend/app/register/page.tsx | 14 +- frontend/app/sitemap.ts | 34 + frontend/app/use-cases/[slug]/page.tsx | 226 +++ frontend/app/use-cases/page.tsx | 92 + .../components/analytics/PostHogProvider.tsx | 9 +- .../components/compliance/CookieBanner.tsx | 10 +- .../components/landing/BackgroundEffects.tsx | 112 ++ .../landing/CompetitorDemoVisual.tsx | 24 +- .../components/landing/LandingSections.tsx | 533 ++---- .../components/landing/LiveSerpPreview.tsx | 342 ++-- .../components/landing/PolicyDemoVisual.tsx | 32 +- frontend/components/landing/SEODemoVisual.tsx | 29 +- frontend/components/landing/WaitlistForm.tsx | 55 +- frontend/components/layout/Footer.tsx | 130 +- frontend/components/ui/ThemeToggle.tsx | 140 +- frontend/components/ui/input.tsx | 13 +- frontend/components/ui/select.tsx | 13 +- frontend/lint_output.txt | Bin 0 -> 6508 bytes frontend/middleware.ts | 57 + frontend/next.config.js | 1 + frontend/package-lock.json | 30 +- frontend/public/robots.txt | 9 + frontend/tmpclaude-7897-cwd | 2 +- frontend/tsconfig.tsbuildinfo | 2 +- tmpclaude-0117-cwd | 2 +- tmpclaude-0273-cwd | 2 +- tmpclaude-04e9-cwd | 2 +- tmpclaude-2343-cwd | 2 +- tmpclaude-2cc8-cwd | 2 +- tmpclaude-45d7-cwd | 2 +- tmpclaude-46ce-cwd | 2 +- tmpclaude-4845-cwd | 2 +- tmpclaude-4f97-cwd | 2 +- tmpclaude-514e-cwd | 2 +- tmpclaude-7810-cwd | 2 +- tmpclaude-8493-cwd | 2 +- tmpclaude-a54e-cwd | 2 +- tmpclaude-e08c-cwd | 2 +- tmpclaude-e1bd-cwd | 2 +- tmpclaude-e545-cwd | 2 +- tmpclaude-e657-cwd | 2 +- tmpclaude-eb35-cwd | 2 +- tmpclaude-f1e3-cwd | 2 +- tmpclaude-fbbf-cwd | 2 +- tmpclaude-fbec-cwd | 2 +- 82 files changed, 3390 insertions(+), 2026 deletions(-) create mode 100644 backend/.eslintrc.json create mode 100644 backend/nul create mode 100644 backend/src/types/serpapi.d.ts create mode 100644 frontend/app/features/[slug]/page.tsx create mode 100644 frontend/app/features/page.tsx create mode 100644 frontend/app/opengraph-image.tsx create mode 100644 frontend/app/sitemap.ts create mode 100644 frontend/app/use-cases/[slug]/page.tsx create mode 100644 frontend/app/use-cases/page.tsx create mode 100644 frontend/components/landing/BackgroundEffects.tsx create mode 100644 frontend/lint_output.txt create mode 100644 frontend/middleware.ts create mode 100644 frontend/public/robots.txt diff --git a/.claude/ralph-loop.local.md b/.claude/ralph-loop.local.md index eb3c783..7e3b900 100644 --- a/.claude/ralph-loop.local.md +++ b/.claude/ralph-loop.local.md @@ -1,9 +1,9 @@ ---- -active: true -iteration: 1 -max_iterations: 0 -completion_promise: null -started_at: "2026-01-17T14:40:37Z" ---- - -Implement website monitor features in priority order: +--- +active: true +iteration: 1 +max_iterations: 0 +completion_promise: null +started_at: "2026-01-17T14:40:37Z" +--- + +Implement website monitor features in priority order: diff --git a/README.md b/README.md index 480a197..47a4fa6 100644 --- a/README.md +++ b/README.md @@ -96,20 +96,35 @@ npm run migrate ### Backend (.env) ```env -PORT=3001 -DATABASE_URL=postgresql://admin:admin123@localhost:5432/website_monitor -REDIS_URL=redis://localhost:6379 -JWT_SECRET=your-secret-key -SMTP_HOST=smtp.sendgrid.net -SMTP_PORT=587 -SMTP_USER=apikey -SMTP_PASS=your-api-key +PORT=3001 +DATABASE_URL=postgresql://admin:admin123@localhost:5432/website_monitor +REDIS_URL=redis://localhost:6379 +JWT_SECRET=your-secret-key +SMTP_HOST=smtp.sendgrid.net +SMTP_PORT=587 +SMTP_USER=apikey +SMTP_PASS=your-api-key +LANDING_ONLY_MODE=false +ADMIN_PASSWORD=change-me-for-admin-waitlist ``` ### Frontend (.env.local) ```env -NEXT_PUBLIC_API_URL=http://localhost:3001 -``` +NEXT_PUBLIC_API_URL=http://localhost:3001 +NEXT_PUBLIC_LANDING_ONLY_MODE=false +``` + +### Landing-only mode (Waitlist rollout) + +To expose only landing pages and waitlist APIs, enable both flags: + +- Backend `.env`: `LANDING_ONLY_MODE=true` +- Frontend `.env.local`: `NEXT_PUBLIC_LANDING_ONLY_MODE=true` + +When enabled: +- Public pages: `/`, `/blog`, `/privacy`, `/admin` +- All other frontend routes redirect to `/` with HTTP `307` +- Backend only allows `/api/waitlist/*`, `POST /api/tools/meta-preview`, and `/health` ## πŸ“– Usage diff --git a/SETUP.md b/SETUP.md index 1101a48..abd50c4 100644 --- a/SETUP.md +++ b/SETUP.md @@ -154,15 +154,34 @@ EMAIL_FROM=alerts@yourdomain.com For development, use [Mailtrap.io](https://mailtrap.io) (free). -### Adjust Plan Limits -Edit `backend/.env`: +### Adjust Plan Limits +Edit `backend/.env`: ```env MAX_MONITORS_FREE=5 MAX_MONITORS_PRO=50 -MIN_FREQUENCY_FREE=60 # minutes -MIN_FREQUENCY_PRO=5 # minutes -``` +MIN_FREQUENCY_FREE=60 # minutes +MIN_FREQUENCY_PRO=5 # minutes +``` + +### Landing-only Mode +For a waitlist-only launch: + +```env +# backend/.env +LANDING_ONLY_MODE=true +ADMIN_PASSWORD=your-secure-admin-password +``` + +```env +# frontend/.env.local +NEXT_PUBLIC_LANDING_ONLY_MODE=true +``` + +With both flags enabled: +- only `/`, `/blog`, `/privacy`, `/admin` stay public +- all other frontend URLs redirect to `/` (HTTP 307) +- backend only permits `/api/waitlist/*`, `POST /api/tools/meta-preview`, and `/health` ## πŸ“– Learn More diff --git a/backend/.env.example b/backend/.env.example index 1cc1ca9..a7f86d2 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -19,9 +19,11 @@ SMTP_PORT=587 SMTP_USER=apikey SMTP_PASS=your-sendgrid-api-key -# App -APP_URL=http://localhost:3000 -API_URL=http://localhost:3002 +# App +APP_URL=http://localhost:3000 +API_URL=http://localhost:3002 +LANDING_ONLY_MODE=false +ADMIN_PASSWORD=change-me-for-admin-waitlist # Rate Limiting MAX_MONITORS_FREE=5 diff --git a/backend/.eslintrc.json b/backend/.eslintrc.json new file mode 100644 index 0000000..f4fe7ac --- /dev/null +++ b/backend/.eslintrc.json @@ -0,0 +1,14 @@ +{ + "root": true, + "env": { + "node": true, + "es2022": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "ignorePatterns": ["dist", "node_modules"] +} diff --git a/backend/nul b/backend/nul new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/config.ts b/backend/src/config.ts index e9f2918..2dc7514 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -47,10 +47,10 @@ export function getMaxMonitors(plan: UserPlan): number { /** * Check if a plan has a specific feature */ -export function hasFeature(plan: UserPlan, feature: string): boolean { - const planConfig = PLAN_LIMITS[plan] || PLAN_LIMITS.free; - return planConfig.features.includes(feature as any); -} +export function hasFeature(plan: UserPlan, feature: string): boolean { + const planConfig = PLAN_LIMITS[plan] || PLAN_LIMITS.free; + return (planConfig.features as readonly string[]).includes(feature); +} /** * Webhook retry configuration diff --git a/backend/src/db/index.ts b/backend/src/db/index.ts index f6c728a..9a7464b 100644 --- a/backend/src/db/index.ts +++ b/backend/src/db/index.ts @@ -53,11 +53,13 @@ export const query = async ( return result; }; -export const getClient = () => pool.connect(); - -// User queries -export const db = { - users: { +export const getClient = () => pool.connect(); + +// User queries +export const db = { + query, + + users: { async create(email: string, passwordHash: string): Promise { const result = await query( 'INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING *', @@ -455,12 +457,12 @@ export const db = { return toCamelCase(result.rows[0]); }, - async findLatestByMonitorId(monitorId: string, limit = 50): Promise { - // Gets the latest check per keyword for this monitor - // Using DISTINCT ON is efficient in Postgres - const result = await query( - `SELECT DISTINCT ON (keyword) * - FROM monitor_rankings + async findLatestByMonitorId(monitorId: string): Promise { + // Gets the latest check per keyword for this monitor + // Using DISTINCT ON is efficient in Postgres + const result = await query( + `SELECT DISTINCT ON (keyword) * + FROM monitor_rankings WHERE monitor_id = $1 ORDER BY keyword, created_at DESC`, [monitorId] diff --git a/backend/src/db/migrations/tmpclaude-0f94-cwd b/backend/src/db/migrations/tmpclaude-0f94-cwd index d0038b9..3e81033 100644 --- a/backend/src/db/migrations/tmpclaude-0f94-cwd +++ b/backend/src/db/migrations/tmpclaude-0f94-cwd @@ -1 +1 @@ -/c/Users/timo/Documents/Websites/website-monitor/backend/src/db/migrations +/c/Users/timo/Documents/Websites/website-monitor/backend/src/db/migrations diff --git a/backend/src/index.ts b/backend/src/index.ts index de68d26..dcf8de3 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -8,8 +8,28 @@ import { authMiddleware } from './middleware/auth'; import { apiLimiter, authLimiter } from './middleware/rateLimiter'; import { startWorker, shutdownScheduler, getSchedulerStats } from './services/scheduler'; -const app = express(); -const PORT = process.env.PORT || 3002; +const app = express(); +const PORT = process.env.PORT || 3002; +const isLandingOnlyMode = process.env.LANDING_ONLY_MODE === 'true'; + +const isAllowedInLandingOnlyMode = (req: express.Request): boolean => { + if ((req.method === 'GET' || req.method === 'HEAD') && req.path === '/health') { + return true; + } + + if ( + (req.path === '/api/tools/meta-preview' || req.path === '/api/tools/meta-preview/') && + (req.method === 'POST' || req.method === 'OPTIONS') + ) { + return true; + } + + if (req.path === '/api/waitlist' || req.path.startsWith('/api/waitlist/')) { + return true; + } + + return false; +}; // Middleware app.use(cors({ @@ -24,13 +44,27 @@ app.use(express.urlencoded({ extended: true })); app.use('/api/', apiLimiter); // Request logging -app.use((req, _res, next) => { - console.log(`${new Date().toISOString()} ${req.method} ${req.path}`); - next(); -}); - -// Health check -app.get('/health', async (_req, res) => { +app.use((req, _res, next) => { + console.log(`${new Date().toISOString()} ${req.method} ${req.path}`); + next(); +}); + +if (isLandingOnlyMode) { + app.use((req, res, next) => { + if (isAllowedInLandingOnlyMode(req)) { + return next(); + } + + return res.status(403).json({ + error: 'landing_only_mode', + message: 'This endpoint is disabled while landing-only mode is active.', + path: req.path, + }); + }); +} + +// Health check +app.get('/health', async (_req, res) => { const schedulerStats = await getSchedulerStats(); res.json({ status: 'ok', @@ -62,8 +96,9 @@ app.use((req, res) => { }); // Error handler -app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => { - console.error('Unhandled error:', err); +app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => { + void _next; + console.error('Unhandled error:', err); res.status(500).json({ error: 'server_error', diff --git a/backend/src/routes/tools.ts b/backend/src/routes/tools.ts index 3cac7b5..8e417bd 100644 --- a/backend/src/routes/tools.ts +++ b/backend/src/routes/tools.ts @@ -2,78 +2,82 @@ import { Router } from 'express'; import axios from 'axios'; import * as cheerio from 'cheerio'; import { z } from 'zod'; - -const router = Router(); - -const previewSchema = z.object({ - url: z.string().min(1) -}); - -router.post('/meta-preview', async (req, res) => { - try { - let { url } = previewSchema.parse(req.body); - - // Add protocol if missing - if (!url.startsWith('http://') && !url.startsWith('https://')) { - url = `https://${url}`; - } - - const response = await axios.get(url, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - 'Accept-Language': 'en-US,en;q=0.9', - 'Accept-Encoding': 'gzip, deflate, br', - 'Upgrade-Insecure-Requests': '1', - 'Sec-Fetch-Dest': 'document', - 'Sec-Fetch-Mode': 'navigate', - 'Sec-Fetch-Site': 'none', - 'Sec-Fetch-User': '?1', - 'Cache-Control': 'max-age=0' +import { Agent as HttpAgent } from 'http'; +import { Agent as HttpsAgent } from 'https'; + +const router = Router(); + +const previewSchema = z.object({ + url: z.string().min(1) +}); + +router.post('/meta-preview', async (req, res) => { + try { + let { url } = previewSchema.parse(req.body); + + // Add protocol if missing + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = `https://${url}`; + } + + const response = await axios.get(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + 'Upgrade-Insecure-Requests': '1', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-User': '?1', + 'Cache-Control': 'max-age=0' }, timeout: 30000, - httpAgent: new (require('http').Agent)({ family: 4, keepAlive: true }), - httpsAgent: new (require('https').Agent)({ family: 4, rejectUnauthorized: false, keepAlive: true }), + httpAgent: new HttpAgent({ family: 4, keepAlive: true }), + httpsAgent: new HttpsAgent({ family: 4, rejectUnauthorized: false, keepAlive: true }), validateStatus: (status) => status < 500 }); - - const html = response.data; - const $ = cheerio.load(html); - - const title = $('title').text() || $('meta[property="og:title"]').attr('content') || ''; - const description = $('meta[name="description"]').attr('content') || $('meta[property="og:description"]').attr('content') || ''; - - // Attempt to find favicon - let favicon = ''; - const linkIcon = $('link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]').attr('href'); - if (linkIcon) { - if (linkIcon.startsWith('http')) { - favicon = linkIcon; - } else if (linkIcon.startsWith('//')) { - favicon = `https:${linkIcon}`; - } else { - const urlObj = new URL(url); - favicon = `${urlObj.protocol}//${urlObj.host}${linkIcon.startsWith('/') ? '' : '/'}${linkIcon}`; - } - } else { - const urlObj = new URL(url); - favicon = `${urlObj.protocol}//${urlObj.host}/favicon.ico`; - } - - res.json({ - title: title.trim(), - description: description.trim(), - favicon, - url: url - }); - + + const html = response.data; + const $ = cheerio.load(html); + + const title = $('title').text() || $('meta[property="og:title"]').attr('content') || ''; + const description = $('meta[name="description"]').attr('content') || $('meta[property="og:description"]').attr('content') || ''; + + // Attempt to find favicon + let favicon = ''; + const linkIcon = $('link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]').attr('href'); + if (linkIcon) { + if (linkIcon.startsWith('http')) { + favicon = linkIcon; + } else if (linkIcon.startsWith('//')) { + favicon = `https:${linkIcon}`; + } else { + const urlObj = new URL(url); + favicon = `${urlObj.protocol}//${urlObj.host}${linkIcon.startsWith('/') ? '' : '/'}${linkIcon}`; + } + } else { + const urlObj = new URL(url); + favicon = `${urlObj.protocol}//${urlObj.host}/favicon.ico`; + } + + res.json({ + title: title.trim(), + description: description.trim(), + favicon, + url: url + }); + } catch (error) { console.error('Meta preview error:', error); if (error instanceof z.ZodError) { - return res.status(400).json({ error: 'Invalid URL provided' }); + res.status(400).json({ error: 'Invalid URL provided' }); + return; } res.status(500).json({ error: 'Failed to fetch page metadata' }); + return; } }); - -export const toolsRouter = router; + +export const toolsRouter = router; diff --git a/backend/src/routes/waitlist.ts b/backend/src/routes/waitlist.ts index 881d92c..69f22c1 100644 --- a/backend/src/routes/waitlist.ts +++ b/backend/src/routes/waitlist.ts @@ -1,131 +1,134 @@ -import { Router } from 'express'; -import { query } from '../db'; -import { z } from 'zod'; - -const router = Router(); - -// Validation schema -const waitlistSchema = z.object({ - email: z.string().email('Invalid email address'), - source: z.string().optional().default('landing_page'), - referrer: z.string().optional(), -}); - -// POST /api/waitlist - Add email to waitlist -router.post('/', async (req, res) => { - try { - const data = waitlistSchema.parse(req.body); - - // Check if email already exists - const existing = await query( - 'SELECT id FROM waitlist_leads WHERE email = $1', - [data.email.toLowerCase()] - ); - +import { Router } from 'express'; +import { query } from '../db'; +import { z } from 'zod'; + +const router = Router(); + +// Validation schema +const waitlistSchema = z.object({ + email: z.string().email('Invalid email address'), + source: z.string().optional().default('landing_page'), + referrer: z.string().optional(), +}); + +// POST /api/waitlist - Add email to waitlist +router.post('/', async (req, res) => { + try { + const data = waitlistSchema.parse(req.body); + + // Check if email already exists + const existing = await query( + 'SELECT id FROM waitlist_leads WHERE email = $1', + [data.email.toLowerCase()] + ); + if (existing.rows.length > 0) { // Already on waitlist - return success anyway (don't reveal they're already signed up) const countResult = await query('SELECT COUNT(*) FROM waitlist_leads'); const position = parseInt(countResult.rows[0].count, 10); - return res.json({ + res.json({ success: true, message: 'You\'re on the list!', position, alreadySignedUp: true, }); + return; } - - // Insert new lead - await query( - 'INSERT INTO waitlist_leads (email, source, referrer) VALUES ($1, $2, $3)', - [data.email.toLowerCase(), data.source, data.referrer || null] - ); - - // Get current position (total count) - const countResult = await query('SELECT COUNT(*) FROM waitlist_leads'); - const position = parseInt(countResult.rows[0].count, 10); - - console.log(`βœ… Waitlist signup: ${data.email} (Position #${position})`); - - res.json({ - success: true, - message: 'You\'re on the list!', - position, - }); + + // Insert new lead + await query( + 'INSERT INTO waitlist_leads (email, source, referrer) VALUES ($1, $2, $3)', + [data.email.toLowerCase(), data.source, data.referrer || null] + ); + + // Get current position (total count) + const countResult = await query('SELECT COUNT(*) FROM waitlist_leads'); + const position = parseInt(countResult.rows[0].count, 10); + + console.log(`βœ… Waitlist signup: ${data.email} (Position #${position})`); + + res.json({ + success: true, + message: 'You\'re on the list!', + position, + }); } catch (error) { if (error instanceof z.ZodError) { - return res.status(400).json({ + res.status(400).json({ success: false, error: 'validation_error', message: error.errors[0].message, }); + return; } - - console.error('Waitlist signup error:', error); - res.status(500).json({ - success: false, - error: 'server_error', - message: 'Failed to join waitlist. Please try again.', - }); - } -}); - -// GET /api/waitlist/count - Get current waitlist count (public) -router.get('/count', async (_req, res) => { - try { - const result = await query('SELECT COUNT(*) FROM waitlist_leads'); - const count = parseInt(result.rows[0].count, 10); - - // Add a base number to make it look more impressive at launch - const displayCount = count + 430; // Starting with "430+ waiting" - - res.json({ - success: true, - count: displayCount, - }); - } catch (error) { - console.error('Waitlist count error:', error); - res.status(500).json({ - success: false, - count: 430, // Fallback to base number - }); - } -}); - -// GET /api/waitlist/admin - Get waitlist leads (Admin only) -router.get('/admin', async (req, res) => { - try { - const adminPassword = process.env.ADMIN_PASSWORD; - const providedPassword = req.headers['x-admin-password']; - + + console.error('Waitlist signup error:', error); + res.status(500).json({ + success: false, + error: 'server_error', + message: 'Failed to join waitlist. Please try again.', + }); + } +}); + +// GET /api/waitlist/count - Get current waitlist count (public) +router.get('/count', async (_req, res) => { + try { + const result = await query('SELECT COUNT(*) FROM waitlist_leads'); + const count = parseInt(result.rows[0].count, 10); + + // Add a base number to make it look more impressive at launch + const displayCount = count + 430; // Starting with "430+ waiting" + + res.json({ + success: true, + count: displayCount, + }); + } catch (error) { + console.error('Waitlist count error:', error); + res.status(500).json({ + success: false, + count: 430, // Fallback to base number + }); + } +}); + +// GET /api/waitlist/admin - Get waitlist leads (Admin only) +router.get('/admin', async (req, res) => { + try { + const adminPassword = process.env.ADMIN_PASSWORD; + const providedPassword = req.headers['x-admin-password']; + if (!adminPassword || providedPassword !== adminPassword) { - return res.status(401).json({ + res.status(401).json({ success: false, message: 'Unauthorized', }); + return; } - - // Get stats - const countResult = await query('SELECT COUNT(*) FROM waitlist_leads'); - const total = parseInt(countResult.rows[0].count, 10); - - // Get leads - const leadsResult = await query( - 'SELECT * FROM waitlist_leads ORDER BY created_at DESC LIMIT 100' - ); - - res.json({ - success: true, - total, - leads: leadsResult.rows, - }); - } catch (error) { - console.error('Waitlist admin error:', error); - res.status(500).json({ - success: false, - message: 'Server error', - }); - } -}); - -export default router; + + // Get stats + const countResult = await query('SELECT COUNT(*) FROM waitlist_leads'); + const total = parseInt(countResult.rows[0].count, 10); + + // Get leads + const leadsResult = await query( + 'SELECT * FROM waitlist_leads ORDER BY created_at DESC LIMIT 100' + ); + + res.json({ + success: true, + total, + leads: leadsResult.rows, + }); + } catch (error) { + console.error('Waitlist admin error:', error); + res.status(500).json({ + success: false, + message: 'Server error', + }); + } +}); + +export default router; diff --git a/backend/src/services/digest.ts b/backend/src/services/digest.ts index 299f761..ef336e9 100644 --- a/backend/src/services/digest.ts +++ b/backend/src/services/digest.ts @@ -1,16 +1,17 @@ -import { Queue, Worker } from 'bullmq'; -import Redis from 'ioredis'; -import nodemailer from 'nodemailer'; -import db from '../db'; +import { ConnectionOptions, Queue, Worker } from 'bullmq'; +import Redis from 'ioredis'; +import nodemailer from 'nodemailer'; +import db from '../db'; // Redis connection (reuse from main scheduler) -const redisConnection = new Redis(process.env.REDIS_URL || 'redis://localhost:6380', { - maxRetriesPerRequest: null, -}); +const redisConnection = new Redis(process.env.REDIS_URL || 'redis://localhost:6380', { + maxRetriesPerRequest: null, +}); +const queueConnection = redisConnection as unknown as ConnectionOptions; // Digest queue -export const digestQueue = new Queue('change-digests', { - connection: redisConnection, +export const digestQueue = new Queue('change-digests', { + connection: queueConnection, defaultJobOptions: { removeOnComplete: 10, removeOnFail: 10, @@ -263,11 +264,11 @@ export function startDigestWorker(): Worker { const { interval } = job.data; await processDigests(interval); }, - { - connection: redisConnection, - concurrency: 1, - } - ); + { + connection: queueConnection, + concurrency: 1, + } + ); worker.on('completed', (job) => { console.log(`[Digest] Job ${job.id} completed`); diff --git a/backend/src/services/monitor.ts b/backend/src/services/monitor.ts index 20a2a7d..ecef7f1 100644 --- a/backend/src/services/monitor.ts +++ b/backend/src/services/monitor.ts @@ -1,5 +1,5 @@ import db from '../db'; -import { Monitor, Snapshot } from '../types'; +import { Snapshot } from '../types'; import { fetchPage } from './fetcher'; import { applyIgnoreRules, @@ -11,6 +11,20 @@ import { calculateChangeImportance } from './importance'; import { sendChangeAlert, sendErrorAlert, sendKeywordAlert } from './alerter'; import { generateSimpleSummary, generateAISummary } from './summarizer'; import { processSeoChecks } from './seo'; +import Redis from 'ioredis'; + +const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6380', { + maxRetriesPerRequest: null, +}); + +async function acquireLock(key: string, ttlMs = 120000): Promise { + const result = await redis.set(key, '1', 'PX', ttlMs, 'NX'); + return result === 'OK'; +} + +async function releaseLock(key: string): Promise { + await redis.del(key); +} export interface CheckResult { snapshot: Snapshot; @@ -24,6 +38,13 @@ export async function checkMonitor( ): Promise<{ snapshot?: Snapshot; alertSent: boolean } | void> { console.log(`[Monitor] Starting check: ${monitorId} | Type: ${checkType} | ForceSEO: ${forceSeo}`); + const lockKey = `lock:monitor-check:${monitorId}`; + const acquired = await acquireLock(lockKey); + if (!acquired) { + console.log(`[Monitor] Skipping ${monitorId} - another check is already running`); + return; + } + try { const monitor = await db.monitors.findById(monitorId); @@ -247,6 +268,8 @@ export async function checkMonitor( } catch (error) { console.error(`[Monitor] Error checking monitor ${monitorId}:`, error); await db.monitors.incrementErrors(monitorId); + } finally { + await releaseLock(lockKey); } } diff --git a/backend/src/services/scheduler.ts b/backend/src/services/scheduler.ts index ca3c97c..07f4851 100644 --- a/backend/src/services/scheduler.ts +++ b/backend/src/services/scheduler.ts @@ -1,16 +1,17 @@ -import { Queue, Worker, QueueEvents } from 'bullmq'; -import Redis from 'ioredis'; -import { checkMonitor } from './monitor'; -import { Monitor } from '../db'; +import { ConnectionOptions, Queue, Worker, QueueEvents } from 'bullmq'; +import Redis from 'ioredis'; +import { checkMonitor } from './monitor'; +import { Monitor } from '../types'; // Redis connection -const redisConnection = new Redis(process.env.REDIS_URL || 'redis://localhost:6380', { - maxRetriesPerRequest: null, -}); +const redisConnection = new Redis(process.env.REDIS_URL || 'redis://localhost:6380', { + maxRetriesPerRequest: null, +}); +const queueConnection = redisConnection as unknown as ConnectionOptions; // Monitor check queue -export const monitorQueue = new Queue('monitor-checks', { - connection: redisConnection, +export const monitorQueue = new Queue('monitor-checks', { + connection: queueConnection, defaultJobOptions: { removeOnComplete: 100, // Keep last 100 completed jobs removeOnFail: 50, // Keep last 50 failed jobs @@ -23,7 +24,7 @@ export const monitorQueue = new Queue('monitor-checks', { }); // Queue events for monitoring -const queueEvents = new QueueEvents('monitor-checks', { connection: redisConnection }); +const queueEvents = new QueueEvents('monitor-checks', { connection: queueConnection }); queueEvents.on('completed', ({ jobId }) => { console.log(`[Scheduler] Job ${jobId} completed`); @@ -130,11 +131,11 @@ export function startWorker(): Worker { throw error; // Re-throw to mark job as failed } }, - { - connection: redisConnection, - concurrency: 5, // Process up to 5 monitors concurrently - } - ); + { + connection: queueConnection, + concurrency: 5, // Process up to 5 monitors concurrently + } + ); worker.on('completed', (job) => { console.log(`[Worker] Job ${job.id} completed`); diff --git a/backend/src/types/serpapi.d.ts b/backend/src/types/serpapi.d.ts new file mode 100644 index 0000000..c19cabc --- /dev/null +++ b/backend/src/types/serpapi.d.ts @@ -0,0 +1,6 @@ +declare module 'serpapi' { + export function getJson( + params: Record, + callback: (json: any) => void + ): void; +} diff --git a/backend/tmpclaude-005a-cwd b/backend/tmpclaude-005a-cwd index 6b73fa7..afb3ca3 100644 --- a/backend/tmpclaude-005a-cwd +++ b/backend/tmpclaude-005a-cwd @@ -1 +1 @@ -/c/Users/timo/Documents/Websites/website-monitor/backend +/c/Users/timo/Documents/Websites/website-monitor/backend diff --git a/backend/tmpclaude-08a7-cwd b/backend/tmpclaude-08a7-cwd index 6b73fa7..afb3ca3 100644 --- a/backend/tmpclaude-08a7-cwd +++ b/backend/tmpclaude-08a7-cwd @@ -1 +1 @@ -/c/Users/timo/Documents/Websites/website-monitor/backend +/c/Users/timo/Documents/Websites/website-monitor/backend diff --git a/backend/tmpclaude-2dec-cwd b/backend/tmpclaude-2dec-cwd index 6b73fa7..afb3ca3 100644 --- a/backend/tmpclaude-2dec-cwd +++ b/backend/tmpclaude-2dec-cwd @@ -1 +1 @@ -/c/Users/timo/Documents/Websites/website-monitor/backend +/c/Users/timo/Documents/Websites/website-monitor/backend diff --git a/backend/tmpclaude-3919-cwd b/backend/tmpclaude-3919-cwd index 6b73fa7..afb3ca3 100644 --- a/backend/tmpclaude-3919-cwd +++ b/backend/tmpclaude-3919-cwd @@ -1 +1 @@ -/c/Users/timo/Documents/Websites/website-monitor/backend +/c/Users/timo/Documents/Websites/website-monitor/backend diff --git a/backend/tmpclaude-7d8e-cwd b/backend/tmpclaude-7d8e-cwd index 6b73fa7..afb3ca3 100644 --- a/backend/tmpclaude-7d8e-cwd +++ b/backend/tmpclaude-7d8e-cwd @@ -1 +1 @@ -/c/Users/timo/Documents/Websites/website-monitor/backend +/c/Users/timo/Documents/Websites/website-monitor/backend diff --git a/backend/tmpclaude-b7cf-cwd b/backend/tmpclaude-b7cf-cwd index 6b73fa7..afb3ca3 100644 --- a/backend/tmpclaude-b7cf-cwd +++ b/backend/tmpclaude-b7cf-cwd @@ -1 +1 @@ -/c/Users/timo/Documents/Websites/website-monitor/backend +/c/Users/timo/Documents/Websites/website-monitor/backend diff --git a/backend/tmpclaude-cd46-cwd b/backend/tmpclaude-cd46-cwd index 6b73fa7..afb3ca3 100644 --- a/backend/tmpclaude-cd46-cwd +++ b/backend/tmpclaude-cd46-cwd @@ -1 +1 @@ -/c/Users/timo/Documents/Websites/website-monitor/backend +/c/Users/timo/Documents/Websites/website-monitor/backend diff --git a/backend/tmpclaude-d344-cwd b/backend/tmpclaude-d344-cwd index 6b73fa7..afb3ca3 100644 --- a/backend/tmpclaude-d344-cwd +++ b/backend/tmpclaude-d344-cwd @@ -1 +1 @@ -/c/Users/timo/Documents/Websites/website-monitor/backend +/c/Users/timo/Documents/Websites/website-monitor/backend diff --git a/backend/tmpclaude-e961-cwd b/backend/tmpclaude-e961-cwd index 6b73fa7..afb3ca3 100644 --- a/backend/tmpclaude-e961-cwd +++ b/backend/tmpclaude-e961-cwd @@ -1 +1 @@ -/c/Users/timo/Documents/Websites/website-monitor/backend +/c/Users/timo/Documents/Websites/website-monitor/backend diff --git a/findings.md b/findings.md index 21f81d3..4a9fdf5 100644 --- a/findings.md +++ b/findings.md @@ -1,801 +1,802 @@ -Website Monitor - Umfassende Analyse & Verbesserungsplan - - πŸ“Š Projekt-Status Übersicht - - Implementierungsstatus nach Bereich - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Bereich β”‚ Status β”‚ QualitΓ€t β”‚ Kritische Issues β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Landing Page β”‚ βœ… 100% β”‚ Exzellent β”‚ Keine β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Authentication β”‚ βœ… 100% β”‚ Gut β”‚ Password Reset fehlt β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Dashboard β”‚ βœ… 100% β”‚ Gut β”‚ Keine β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Monitors Management β”‚ βœ… 100% β”‚ Exzellent β”‚ Keyword UI fehlt β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Monitor History β”‚ βœ… 100% β”‚ Gut β”‚ Keine β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Snapshot Details β”‚ βœ… 100% β”‚ Exzellent β”‚ Keine β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Analytics β”‚ ⚠️ 60% β”‚ Basic β”‚ Keine Trends/Zeitbereiche β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Incidents β”‚ ⚠️ 60% β”‚ Basic β”‚ Kein Resolution Tracking β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Settings β”‚ ❌ 20% β”‚ Stub β”‚ Komplett nicht funktional β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Backend Core β”‚ βœ… 95% β”‚ Exzellent β”‚ Job Scheduling fehlt β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Change Detection β”‚ βœ… 100% β”‚ Exzellent β”‚ Funktioniert! β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - --- - 🚨 KRITISCHER BLOCKER (Muss vor Launch behoben werden) - - Problem: Keine automatische Überwachung - - Dateien: - - backend/src/services/monitor.ts (Zeile 168-171) - scheduleMonitor() ist nur ein Stub - - backend/src/index.ts - Queue-Initialisierung fehlt - - backend/src/routes/monitors.ts - Ruft scheduleMonitor() auf, aber die tut nichts - - Aktueller Stand: - export async function scheduleMonitor(monitor: Monitor): Promise { - // This will be implemented when we add the job queue - console.log(`[Monitor] Scheduling monitor ${monitor.id}...`); - } - - Auswirkung: - - ❌ Monitors prΓΌfen NICHT automatisch im Hintergrund - - ❌ Nutzer mΓΌssen manuell "Check Now" klicken - - ❌ Das komplette Wertversprechen ("I watch pages so you don't have to") funktioniert nicht - - βœ… Manuelle Checks ΓΌber API funktionieren (aber das ist nicht das Produkt) - - Was fehlt: - 1. Bull Queue Worker implementieren - 2. Redis-Verbindung initialisieren - 3. Recurring Jobs fΓΌr jeden Monitor erstellen - 4. Job-Processor der checkMonitor() aufruft - 5. Job-Cleanup bei Monitor-LΓΆschung/Pause - 6. Job-Aktualisierung bei Frequency-Γ„nderung - - --- - βœ… Was tatsΓ€chlich FUNKTIONIERT (Change Detection Analyse) - - Der Change Detection Algorithmus ist exzellent implementiert: - - 1. Multi-Layer Noise Filtering: - - βœ… 20+ Timestamp-Regex-Patterns (ISO, Unix, relative Zeiten) - - βœ… Cookie Banner via CSS Selektoren (20+ Patterns) - - βœ… Script/Style Tag Entfernung - - βœ… Custom Ignore Rules (CSS, Regex, Text) - - 2. Diff-Vergleich: - - βœ… Verwendet diff Library mit diffLines() - - βœ… Berechnet Change Percentage korrekt - - βœ… ZΓ€hlt Additions/Deletions - - βœ… Severity Classification (major > 50%, medium > 10%, minor) - - 3. Keyword Detection: - - βœ… 3 Rule Types: appears, disappears, count - - βœ… Case-sensitive Support - - βœ… Threshold-basierte Triggering - - βœ… Detaillierte Match-Info - - 4. Error Handling: - - βœ… 3-Retry Logic mit Backoff - - βœ… Consecutive Error Tracking - - βœ… Automatische Error Alerts (ab 2 Fehlern) - - Fazit: Der Core-Algorithmus ist produktionsreif und arbeitet zuverlΓ€ssig. - - --- - πŸ“ Detaillierte Feature-Analyse - - 1. Landing Page (frontend/app/page.tsx) - - Status: βœ… VollstΓ€ndig implementiert - - Vorhandene Features: - - Hero Section mit CTAs - - Feature-Highlights (3 Differenzierungsmerkmale) - - "How it works" Steps - - 3-Tier Pricing (Starter/Pro/Enterprise) - - FAQ Accordion - - Responsive Design - - Kleinere Issues: - - Demo-Video Link ist Platzhalter - - Pricing Buttons fΓΌhren nicht zur Checkout-Flow - - "10,000+ developers" ist Hardcoded - - --- - 2. Authentication (frontend/app/login, frontend/app/register) - - Status: βœ… VollstΓ€ndig, ⚠️ Features fehlen - - Vorhanden: - - Login/Register mit Validation - - JWT Token Management - - Auto-Redirect bei Authentication - - Error Handling - - Loading States - - Fehlt: - - ❌ Password Reset/Recovery Flow - - ❌ Email Verification - - ❌ "Remember Me" FunktionalitΓ€t - - ❌ 2FA Support - - ❌ Social Auth (Google, GitHub) - - --- - 3. Dashboard (frontend/app/dashboard/page.tsx) - - Status: βœ… Gut implementiert - - Features: - - 4 Stat Cards (Total, Active, Errors, Recent Changes) - - Recent Monitors List (Top 5) - - Quick Action Buttons - - Status Indicators - - Verbesserungspotenzial: - - Keine Pagination (nur 5 Monitors) - - Keine Charts/Visualisierungen - - Keine Echtzeit-Updates - - Keine historischen Trends - - --- - 4. Monitors Management (frontend/app/monitors/page.tsx) - - Status: βœ… Exzellent implementiert - - Starke Features: - - Grid/List View Toggle - - Filter Tabs (All, Active, Error) - - Inline Create/Edit Form - - Frequency Presets (5min bis 24h) - - Ignore Content Presets (Timestamps, Cookies, etc.) - - Custom CSS Selector Support - - Check Now, Edit, Delete Actions - - Konfirmations-Dialoge - - Fehlt: - - ❌ Keyword Rules UI (Backend unterstΓΌtzt es, aber kein UI!) - - ❌ Visual Element Picker - - ❌ Bulk Actions (mehrere Monitors gleichzeitig) - - ❌ Tags/Gruppierung - - ❌ Keyboard Shortcuts - - --- - 5. Monitor History & Snapshots - - Status: βœ… Sehr gut implementiert - - Features: - - Timeline mit allen Checks - - Change/No Change/Error Badges - - HTTP Status + Response Time - - Change Percentage - - Error Messages - - Diff Viewer mit Split-View (react-diff-viewer-continued) - - Verbesserungspotenzial: - - Keine Pagination (lΓ€dt alle 50 Snapshots auf einmal) - - Kein Zeitbereich-Filter - - Kein Export (PDF, CSV) - - Keine Screenshot-Vergleiche - - --- - 6. Analytics (frontend/app/analytics/page.tsx) - - Status: ⚠️ 60% - Basic Stats nur - - Vorhanden: - - Total Monitors, Uptime Rate, Error Rate - - Monitor Status Distribution (Donut Chart) - - Check Frequency Distribution (Bar Charts) - - Kritisch fehlend: - - ❌ Zeitbereich-Auswahl (7d, 30d, 90d) - - ❌ Trend-Charts (Change Frequency ΓΌber Zeit) - - ❌ Response Time Trends - - ❌ Historische Vergleiche - - ❌ Per-Monitor Analytics - - ❌ Export-FunktionalitΓ€t - - --- - 7. Incidents (frontend/app/incidents/page.tsx) - - Status: ⚠️ 60% - Sehr basic - - Vorhanden: - - Liste von Errors + Changes - - Type Badges - - View Details Links - - Fehlt: - - ❌ Incident Grouping (gleicher Monitor, gleicher Tag) - - ❌ Resolution Tracking (Mark as resolved) - - ❌ Severity Levels - - ❌ Incident Timeline - - ❌ Filter nach Datum/Typ - - ❌ Alert Delivery Status - - --- - 8. Settings (frontend/app/settings/page.tsx) - - Status: ❌ 20% - Nur UI Mockup - - Problem: Alle Buttons sind nicht-funktional, keine Backend-Integration! - - Was fehlt: - - ❌ Change Password Flow - - ❌ Email Notification Preferences - - ❌ Slack Integration Setup - - ❌ Webhook Configuration - - ❌ Billing Management (Stripe Portal Link) - - ❌ Account Deletion mit Confirmation - - ❌ Plan Management - - --- - πŸ—οΈ Backend Architektur-Analys - - Datenbankschema (backend/src/db/schema.sql) - - Status: βœ… Gut durchdacht - - Tabellen: - - users - Email, password_hash, plan, stripe_customer_id - - monitors - URL, frequency, rules (JSONB), status tracking - - snapshots - HTML, text, hash, diff results, HTTP info - - alerts - Type, title, channels (JSONB), delivery tracking - - Indexes: Gut gesetzt fΓΌr Performance - - Snapshot Retention: Automatisches Cleanup (letzte 50 behalten) - - --- - API Endpoints (backend/src/routes/monitors.ts) - - Status: βœ… RESTful und vollstΓ€ndig - - Endpoints: - - GET /api/monitors - Liste aller Monitors - - POST /api/monitors - Neuer Monitor (mit Plan Limits Check) - - GET /api/monitors/:id - Einzelner Monitor - - PUT /api/monitors/:id - Update Monitor - - DELETE /api/monitors/:id - LΓΆschen - - POST /api/monitors/:id/check - Manueller Check (synchron!) - - GET /api/monitors/:id/history - Snapshot History (max 100) - - GET /api/monitors/:id/history/:snapshotId - Einzelner Snapshot - - Plan Limits Enforcement: - - FREE: 5 Monitors, 60min Frequency - - PRO: 50 Monitors, 5min Frequency - - BUSINESS: 200 Monitors, 1min Frequency - - ENTERPRISE: Unlimited, 1min Frequency - - Issue: /check Endpoint ist synchron (wartet bis Check fertig) - kΓΆnnte bei langsamen Seiten timeouten - - --- - Alert System (backend/src/services/alerter.ts) - - Status: βœ… Funktioniert - - 3 Alert-Typen: - 1. Change Alert - bei erkannten Γ„nderungen - 2. Error Alert - nach 2 konsekutiven Fehlern - 3. Keyword Alert - bei Keyword-Match - - Email-Versand: - - Nodemailer mit SMTP (SendGrid konfiguriert) - - BenΓΆtigt SMTP_USER und SMTP_PASS in .env - - --- - 🎨 Design System & Components - - UI Components (frontend/components/ui/) - - Status: βœ… Grundlagen vorhanden, ⚠️ Fortgeschrittene fehlen - - Vorhanden (7 Components): - - Button (variants, sizes, loading state) - - Input (mit label, error, hint) - - Card (Header, Title, Content, Footer) - - Badge (status variants) - - Select (Dropdown) - - Fehlt: - - Modal/Dialog (fΓΌr Confirmations, Forms) - - Dropdown Menu (fΓΌr Action Menus) - - Tabs (fΓΌr Settings Sections) - - Pagination - - Data Table mit Sorting - - Toggle/Switch - - Progress Bar - - Tooltip - - Design System (frontend/app/globals.css) - - Status: βœ… Exzellent - Premium Look - - Highlights: - - Warme Farbpalette (Tan/Sand Primary: #C4B29C) - - Dark Mode Support - - Custom Animations (fadeIn, slideIn, pulseGlow) - - Glass Panel Effects - - Status Dots (animated) - - Scrollbar Styling - - QualitΓ€t: Sehr professionell, hebt sich von Generic Material Design ab - - --- - πŸ”’ Sicherheit & Authentication - - JWT Implementation (backend/src/middleware/auth.ts) - - Status: βœ… Sicher implementiert - - Features: - - JWT mit 7 Tage Expiry - - Bcrypt Password Hashing - - Password Requirements (8+ Zeichen, Upper/Lower/Number) - - Auto-Redirect bei 401 - - ⚠️ Kritisch: - - Default JWT_SECRET ist 'your-secret-key' (Zeile 5 in utils/auth.ts) - - MUSS in Production geΓ€ndert werden! - - --- - πŸ’‘ VerbesserungsvorschlΓ€ge (Priorisiert) - - PRIORITΓ„T 1: MVP Blocker (Muss vor Launch) - - 1. Bull Queue Job Scheduling implementieren ⚠️ KRITISCH - - Dateien: - - backend/src/services/scheduler.ts (neu erstellen) - - backend/src/index.ts (Queue initialisieren) - - backend/src/routes/monitors.ts (scheduleMonitor() aufrufen) - - Aufwand: 3-4 Stunden - - Implementierung: - // scheduler.ts - import { Queue, Worker } from 'bullmq'; - import Redis from 'ioredis'; - import { checkMonitor } from './monitor'; - - const redis = new Redis(process.env.REDIS_URL); - const monitorQueue = new Queue('monitor-checks', { connection: redis }); - - export async function scheduleMonitor(monitor: Monitor) { - await monitorQueue.add( - 'check', - { monitorId: monitor.id }, - { - repeat: { every: monitor.frequency * 60 * 1000 }, - jobId: `monitor-${monitor.id}`, - removeOnComplete: 100, - removeOnFail: false - } - ); - } - - export async function unscheduleMonitor(monitorId: string) { - await monitorQueue.remove(`monitor-${monitorId}`); - } - - // Worker - const worker = new Worker( - 'monitor-checks', - async (job) => { - await checkMonitor(job.data.monitorId); - }, - { connection: redis } - ); - - --- - 2. Settings Page Backend implementieren - - Dateien: - - backend/src/routes/settings.ts (neu) - - frontend/app/settings/page.tsx (API-Integration) - - Fehlende Features: - - Change Password Endpoint - - Update Notification Preferences - - Webhook CRUD Endpoints - - Slack OAuth Integration - - Billing Portal Link (Stripe) - - Account Deletion Endpoint - - Aufwand: 4-5 Stunden - - --- - 3. Password Reset Flow - - Dateien: - - frontend/app/forgot-password/page.tsx (neu) - - frontend/app/reset-password/[token]/page.tsx (neu) - - backend/src/routes/auth.ts (Endpoints hinzufΓΌgen) - - Flow: - 1. User gibt Email ein - 2. Backend generiert Reset Token (JWT, 1h Expiry) - 3. Email mit Reset-Link - 4. User setzt neues Passwort - 5. Token wird invalidiert - - Aufwand: 2-3 Stunden - - --- - 4. Email Verification - - Dateien: - - frontend/app/verify-email/[token]/page.tsx (neu) - - backend/src/routes/auth.ts (Endpoints) - - backend/src/services/alerter.ts (Check vor Alert-Versand) - - Wichtig: Verhindert Spam-Accounts und verbessert Email-Deliverability - - Aufwand: 2 Stunden - - --- - PRIORITΓ„T 2: Kern-Features komplettieren - - 5. Keyword Alerts UI implementieren πŸ”₯ WICHTIG - - Dateien: - - frontend/app/monitors/page.tsx (Form erweitern) - - Backend funktioniert bereits perfekt! Nur UI fehlt. - - Was hinzufΓΌgen: - - Keyword Rules Section im Monitor Form - - Add/Remove Keyword Rules - - Optionen: keyword, type (appears/disappears/count), threshold, case-sensitive - - Preview der Keyword-Matches in Snapshot Details - - Keyword Alert Badges in History - - Aufwand: 3-4 Stunden - - --- - 6. Advanced Noise Filtering UI - - Aktuell: Nur Presets (Timestamps, Cookies, etc.) - - Erweiterungen: - - Visual Element Selector (Click-to-Ignore) - - Regex-basierte Filter mit Preview - - Custom Filter Templates speichern - - Sensitivity Slider (Schwellenwert) - - Aufwand: 3-4 Stunden - - --- - 7. Mobile Responsiveness - - Issues: - - Sidebar klappt nicht ein auf Mobile - - Monitor Cards zu breit auf kleinen Screens - - Form Inputs stacken nicht richtig - - Aufwand: 2 Stunden - - --- - 8. Analytics Enhancements - - Fehlende Features: - - Zeitbereich-Selector (7d, 30d, 90d, all time) - - Change Frequency Trend Chart - - Response Time Graph - - Error Rate Trends - - Export als CSV - - Aufwand: 3-4 Stunden - - --- - 9. Incidents Improvements - - Erweiterungen: - - Incident Grouping (gleicher Monitor, gleicher Tag) - - Mark as Resolved/Acknowledged - - Severity Indicators - - Filter nach Type/Date - - Incident Details Modal - - Aufwand: 3 Stunden - - --- - PRIORITΓ„T 3: Competitive Advantages - - 10. AI-Powered Change Importance Scoring πŸš€ - - Das wΓ€re ein KILLER-Feature! - - Konzept: Nicht alle Changes sind gleich wichtig. Score 0-100 basierend auf: - - Change Percentage - - Important Keywords enthalten? - - Main Content vs. Sidebar? - - Recurring Pattern (immer gleiche Zeit)? - - Optional: GPT-4o-mini fΓΌr semantische Analyse - - User Benefit: Nur bei wirklich wichtigen Changes benachrichtigen - - Aufwand: 8-10 Stunden - - --- - 11. Visual Element Selector - - Problem: Aktuell muss User CSS Selector kennen - - LΓΆsung: - - Page in iframe rendern - - Overlay mit Click-Handler - - Element highlighten beim Hover - - Auto-generiere optimalen CSS Selector - - Test-Button um zu prΓΌfen ob Selector funktioniert - - Libraries: optimal-select, element-inspector - - Aufwand: 6-8 Stunden - - --- - 12. Smart Diff Visualization - - Aktuell: Basic Side-by-Side - - Verbesserungen: - - Inline Diff mit Highlighting - - Collapsible unchanged sections - - Visual Diff (Screenshot Comparison) - - Synchronized Scrolling - - Search within Diff - - Export as PDF - - Aufwand: 4-5 Stunden - - --- - 13. Monitor Templates Marketplace πŸ’‘ - - Konzept: Pre-configured Monitors fΓΌr populΓ€re Sites - - Beispiele: - - "Amazon Product Price Tracker" - - "Reddit Job Postings" - - "GitHub Releases Watcher" - - "Competitor Pricing Page" - - User installiert Template in 1-Click, ersetzt nur URL - - Aufwand: 10+ Stunden - - --- - 14. Change Digest Mode - - Problem: Notification Fatigue - - LΓΆsung: Batch Changes in tΓ€gliche/wΓΆchentliche Digests - - Per-Monitor oder Account-wide Setting - - Smart Grouping - - Beautiful Email Template - - "Top Changes This Week" Ranking - - Aufwand: 4 Stunden - - --- - PRIORITΓ„T 4: Performance & Scale - - 15. Optimize Diff Calculation - - Aktuelle Performance: Funktioniert, aber kΓΆnnte schneller sein - - Optimierungen: - - Stream large HTML (nicht in Memory laden) - - xxHash statt SHA-256 (schneller) - - Diff nur visible Text (strip HTML vorher) - - Cache filtered HTML - - Incremental Diffing - - Aufwand: 3-4 Stunden - - --- - 16. Add Pagination - - Wo fehlt: - - Monitor History (lΓ€dt alle 50 auf einmal) - - Monitors List - - Incidents List - - Aufwand: 2 Stunden - - --- - 17. Implement Caching - - Strategie: - - Redis Cache fΓΌr Monitor List (1min TTL) - - Latest Snapshot per Monitor (1min TTL) - - User Plan Limits (10min TTL) - - Aufwand: 3 Stunden - - --- - PRIORITΓ„T 5: Zukunft & Integrations - - 18. Webhook Integration - - Status: Settings UI existiert, Backend fehlt - - Implementation: - - Store Webhook URL per User - - POST JSON auf Change - - Retry Logic (3 Versuche) - - Webhook Logs - - HMAC Signature fΓΌr Security - - Aufwand: 2 Stunden - - --- - 19. Slack Integration - - Implementation: - - Slack OAuth Flow - - Post to Channel on Change - - Rich Message Formatting mit Buttons - - Configure per-Monitor oder Global - - Aufwand: 4 Stunden - - --- - 20. Browser Extension - - Features: - - Right-Click β†’ "Monitor this page" - - Auto-fill Form - - Visual Element Picker - - Quick Status View in Popup - - Aufwand: 20+ Stunden - - --- - 🎯 Empfohlene Implementierungs-Reihenfolge - - Woche 1: Critical Blockers beheben - - 1. Tag 1-2: Bull Queue Job Scheduling ⚠️ - 2. Tag 2: Password Reset Flow - 3. Tag 3: Email Verification - 4. Tag 4-5: Settings Page Backend - - Deliverable: Voll funktionales MVP mit automatischem Monitoring - - --- - Woche 2: Core Features komplettieren - - 1. Tag 1-2: Keyword Alerts UI - 2. Tag 2: Mobile Responsiveness - 3. Tag 3-4: Fehlende UI Components (Modal, Dropdown, etc.) - 4. Tag 4-5: Analytics & Incidents Enhancements - - Deliverable: Feature-complete Product fΓΌr Beta Users - - --- - Woche 3: Competitive Advantages - - 1. Tag 1-3: AI Change Importance Scoring - 2. Tag 3-5: Visual Element Selector - - Deliverable: Unique Features die Konkurrenz nicht hat - - --- - Woche 4: Polish & Performance - - 1. Tag 1-2: Advanced Noise Filtering UI - 2. Tag 2-3: Smart Diff Visualization - 3. Tag 4: Caching & Pagination - 4. Tag 5: Database Optimization & Testing - - Deliverable: Production-ready, scalable Product - - --- - πŸ“Š Impact Matrix - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Feature β”‚ User Value β”‚ Tech Risk β”‚ Aufwand β”‚ PrioritΓ€t β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Bull Queue Scheduling β”‚ πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯ β”‚ Niedrig β”‚ Mittel β”‚ P1 β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Settings Backend β”‚ πŸ”₯πŸ”₯πŸ”₯πŸ”₯ β”‚ Niedrig β”‚ Mittel β”‚ P1 β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Password Reset β”‚ πŸ”₯πŸ”₯πŸ”₯πŸ”₯ β”‚ Niedrig β”‚ Klein β”‚ P1 β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Email Verification β”‚ πŸ”₯πŸ”₯πŸ”₯πŸ”₯ β”‚ Niedrig β”‚ Klein β”‚ P1 β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Keyword Alerts UI β”‚ πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯ β”‚ Niedrig β”‚ Mittel β”‚ P2 β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ AI Importance Scoring β”‚ πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯ β”‚ Mittel β”‚ Groß β”‚ P3 β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Visual Element Selector β”‚ πŸ”₯πŸ”₯πŸ”₯πŸ”₯ β”‚ Mittel β”‚ Groß β”‚ P3 β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Mobile Responsive β”‚ πŸ”₯πŸ”₯πŸ”₯ β”‚ Niedrig β”‚ Klein β”‚ P2 β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Analytics Enhancements β”‚ πŸ”₯πŸ”₯πŸ”₯ β”‚ Niedrig β”‚ Mittel β”‚ P2 β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Monitor Templates β”‚ πŸ”₯πŸ”₯πŸ”₯πŸ”₯ β”‚ Niedrig β”‚ Groß β”‚ P3 β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - --- - πŸ”§ Kritische Dateien fΓΌr Implementation - - FΓΌr Bull Queue (P1 - CRITICAL): - - - backend/src/services/scheduler.ts - Komplett neu erstellen - - backend/src/index.ts - Queue initialisieren beim Server Start - - backend/src/routes/monitors.ts - scheduleMonitor() nach create/update aufrufen - - FΓΌr Settings (P1): - - - backend/src/routes/settings.ts - Neu erstellen - - frontend/app/settings/page.tsx - API Integration hinzufΓΌgen - - FΓΌr Keyword Alerts UI (P2): - - - frontend/app/monitors/page.tsx - Form um Keyword Rules Section erweitern - - frontend/app/monitors/[id]/snapshot/[snapshotId]/page.tsx - Keyword Matches anzeigen - - FΓΌr Auth Erweiterungen (P1): - - - frontend/app/forgot-password/page.tsx - Neu - - frontend/app/reset-password/[token]/page.tsx - Neu - - frontend/app/verify-email/[token]/page.tsx - Neu - - backend/src/routes/auth.ts - Endpoints hinzufΓΌgen - - --- - 🎨 Kreative Differenzierungs-Ideen - - 1. Change Prediction - - Historical Data nutzen um zu predicten wann Changes typischerweise auftreten. Check Frequency automatisch erhΓΆhen um vorhergesagte Zeiten. - - 2. Natural Language Monitoring - - "Alert me when this page mentions hiring OR job openings" - β†’ System ΓΌbersetzt automatisch in Keyword Rules - - 3. Collaborative Change Annotations - - Users kΓΆnnen Notes auf Changes hinterlassen: "Expected change" oder "False alarm". Im Team teilen. - - 4. Change Feed (RSS-like) - - Public/Authenticated Feed aller Changes. Power Users kΓΆnnen via RSS Reader konsumieren. - - 5. Monitor Health Score - - Track Reliability jedes Monitors (success rate, false positive rate). Auto-suggest Improvements. - - --- - βœ… Zusammenfassung - - Was EXZELLENT funktioniert: - - - βœ… Change Detection Algorithmus (Multi-Layer Filtering, Diff, Keywords) - - βœ… Frontend Design & Core Features (Landing, Auth, Dashboard, Monitors) - - βœ… Database Schema & API Endpoints - - βœ… Security (JWT, Password Hashing, Authorization) - - KRITISCHER Blocker: - - - ❌ Keine automatische Überwachung - Bull Queue nicht implementiert - - Fehlende Features fΓΌr MVP: - - - ❌ Settings Page FunktionalitΓ€t - - ❌ Password Reset & Email Verification - - ❌ Keyword Alerts UI - - ❌ Mobile Responsiveness - - Empfehlung: - - Fokus auf Woche 1 Plan - Die 4 P1 Blocker beheben macht das Produkt voll funktional und launchbar. Dann iterativ weitere Features hinzufΓΌgen basierend auf User Feedback. - - Das Produkt ist 85% fertig und hat eine exzellente technische Basis. Mit 1-2 Wochen fokussierter Arbeit kann es production-ready sein! \ No newline at end of file +Von Ihnen eingegebene Begriffe +monitor website for changes +1000 – 10000 +0 % +-90 % +Gering +β€” +1,38 € +11,44 € +monitor web page for changes +100 – 1000 +0 % +0 % +Gering +β€” +1,87 € +16,08 € +monitor webpage for changes +100 – 1000 +0 % +0 % +Gering +β€” +1,87 € +16,08 € +webpage change monitor +1000 – 10000 +0 % +-90 % +Gering +β€” +1,38 € +11,44 € +page change monitor +100 – 1000 +0 % +-90 % +Gering +β€” +1,29 € +13,25 € +site change monitor +1000 – 10000 +0 % +-90 % +Gering +β€” +1,38 € +11,44 € +Keyword-Ideen +track website changes +100 – 1000 +0 % +0 % +Mittel +β€” +1,25 € +5,65 € +monitor website changes free +100 – 1000 +0 % +-90 % +Mittel +β€” +1,60 € +5,81 € +alert when website changes +100 – 1000 +0 % +0 % +Gering +β€” +1,01 € +5,94 € +website watcher +100 – 1000 +0 % +0 % +Gering +β€” +0,94 € +3,39 € +notify when website changes free +10 – 100 +0 % +0 % +Hoch +β€” +β€” +β€” +website change detection free +10 – 100 +0 % +0 % +Hoch +β€” +1,29 € +5,15 € +get notification when website changes +100 – 1000 +0 % +-90 % +Mittel +β€” +0,87 € +5,92 € +website change monitoring tools +100 – 1000 ++900 % +0 % +Gering +β€” +5,25 € +16,86 € +get notified when a website is updated +10 – 100 +0 % +-90 % +Mittel +β€” +1,02 € +3,21 € +notify when website changes +100 – 1000 +-90 % +-90 % +Gering +β€” +1,00 € +5,69 € +get notified when website changes +100 – 1000 +0 % +-90 % +Mittel +β€” +0,87 € +5,92 € +webpage change alert +100 – 1000 +0 % +0 % +Gering +β€” +1,01 € +5,94 € +track website changes free +10 – 100 +0 % +0 % +Hoch +β€” +1,51 € +3,62 € +track price changes on websites +10 – 100 +0 % +0 % +Gering +β€” +β€” +β€” +monitor website changes every minute free +10 – 100 +-100 % +-100 % +β€” +β€” +β€” +β€” +web page change alert +100 – 1000 +0 % +0 % +Gering +β€” +1,01 € +5,94 € +notification when website changes +100 – 1000 +0 % +-90 % +Mittel +β€” +0,94 € +5,33 € +notify me when website changes +10 – 100 +0 % +0 % +Gering +β€” +1,29 € +7,52 € +monitor competitor website changes +10 – 100 +0 % +0 % +Gering +β€” +β€” +β€” +monitor website for changes free +10 – 100 +0 % +0 % +Mittel +β€” +1,63 € +8,23 € +website change monitor app +10 – 100 +0 % +0 % +Gering +β€” +β€” +β€” +free website change detection +10 – 100 +0 % +0 % +Hoch +β€” +1,94 € +6,07 € +visualping free +10 – 100 +0 % +0 % +Mittel +β€” +2,33 € +9,47 € +bot to monitor website changes +10 – 100 +0 % +0 % +Mittel +β€” +β€” +β€” +monitor webpage for changes free +10 – 100 +0 % +0 % +Mittel +β€” +β€” +β€” +free monitor website changes +10 – 100 +0 % +0 % +Hoch +β€” +β€” +β€” +watch website for changes +1000 – 10000 +0 % +-90 % +Gering +β€” +1,38 € +11,44 € +get notified if a web page changes +10 – 100 +0 % +0 % +Mittel +β€” +1,69 € +9,04 € +get alert when website changes +10 – 100 +0 % +0 % +Mittel +β€” +1,13 € +6,42 € +get alerted when a website changes +10 – 100 +0 % +0 % +Mittel +β€” +1,13 € +6,42 € +get an alert when a website changes +10 – 100 +0 % +0 % +Gering +β€” +β€” +β€” +detect website changes +100 – 1000 ++900 % +0 % +Mittel +β€” +1,41 € +6,57 € +monitor web pages for changes free +10 – 100 +-100 % +0 % +β€” +β€” +β€” +β€” +alert if website changes +100 – 1000 +0 % +0 % +Gering +β€” +1,01 € +5,94 € +website change checker +10 – 100 +0 % +0 % +Gering +β€” +1,19 € +4,24 € +website content change monitor +10 – 100 +-100 % +-100 % +β€” +β€” +β€” +β€” +free web page change monitoring +10 – 100 +-100 % +-100 % +β€” +β€” +β€” +β€” +versionista +100 – 1000 +0 % +0 % +Gering +β€” +2,10 € +12,02 € +alert me when website changes +10 – 100 +0 % +0 % +Mittel +β€” +1,46 € +11,16 € +notify me if a website changes +10 – 100 +0 % +0 % +Mittel +β€” +0,74 € +2,13 € +monitor website content changes +10 – 100 +0 % +0 % +Gering +β€” +β€” +β€” +get notified when a webpage changes +10 – 100 +0 % +0 % +Mittel +β€” +β€” +β€” +website change alert free +10 – 100 +0 % +0 % +Mittel +β€” +β€” +β€” +watch webpage for changes +10 – 100 +0 % +0 % +Gering +β€” +β€” +β€” +alert when page changes +100 – 1000 +0 % +0 % +Gering +β€” +1,01 € +5,94 € +tool to monitor website changes +10 – 100 +0 % +0 % +Hoch +β€” +2,30 € +14,14 € +web page change notification +10 – 100 +0 % +0 % +Gering +β€” +1,25 € +3,98 € +detect web page changes +100 – 1000 +0 % +0 % +Gering +β€” +2,56 € +11,29 € +get notified when a website updates +10 – 100 +-100 % +-100 % +β€” +β€” +β€” +β€” +track web page changes +100 – 1000 +0 % +0 % +Mittel +β€” +1,25 € +5,65 € +monitor a webpage for changes +10 – 100 +0 % +0 % +Mittel +β€” +3,77 € +19,32 € +get updates when a web page changes +10 – 100 +0 % +0 % +Hoch +β€” +β€” +β€” +notify me if website changes +10 – 100 +0 % +0 % +Mittel +β€” +0,74 € +2,13 € +notify if website changes +10 – 100 +0 % +0 % +Mittel +β€” +1,58 € +11,30 € +get alerts when a web page changes +10 – 100 +0 % +0 % +Gering +β€” +β€” +β€” +notify when web page changes +100 – 1000 +-90 % +-90 % +Gering +β€” +1,00 € +5,69 € +track page changes +100 – 1000 +0 % +0 % +Mittel +β€” +1,25 € +5,65 € +create alert when webpage changes +10 – 100 +0 % +0 % +Hoch +β€” +β€” +β€” +app to track website changes +10 – 100 +0 % +0 % +Mittel +β€” +β€” +β€” +alert when web page changes +100 – 1000 +0 % +0 % +Gering +β€” +1,01 € +5,94 € +monitor for website changes +10 – 100 +0 % +0 % +Hoch +β€” +β€” +β€” +web page change alert chrome +10 – 100 +0 % +-100 % +β€” +β€” +β€” +β€” +alert on website change +10 – 100 +0 % +0 % +Gering +β€” +1,09 € +4,12 € +notify on website change +100 – 1000 +-90 % +-90 % +Gering +β€” +1,00 € +5,69 € +get notification when a website changes +100 – 1000 +0 % +-90 % +Mittel +β€” +0,87 € +5,92 € +get notified of website changes +10 – 100 +-100 % +-100 % +β€” +β€” +β€” +β€” +tools to monitor website changes +10 – 100 +0 % +0 % +Hoch +β€” +2,30 € +14,14 € +track website for changes +100 – 1000 +0 % +0 % +Mittel +β€” +1,25 € +5,65 € +free website monitoring for changes +10 – 100 +-100 % +-100 % +β€” +β€” +β€” +β€” +alert when a webpage changes +10 – 100 ++∞ +0 % +Gering +β€” +β€” +β€” +get notified when website updates +10 – 100 +0 % +-90 % +Mittel +β€” +1,02 € +3,21 € +google alert when website changes +10 – 100 +0 % +0 % +Gering +β€” +0,87 € +2,92 € +track when a website changes +10 – 100 +0 % +0 % +Mittel +β€” +β€” +β€” +get notification if website changes +10 – 100 +0 % +0 % +Gering +β€” +β€” +β€” +best website change monitor +10 – 100 +0 % +0 % +Gering +β€” +β€” +β€” +web page change alert free +10 – 100 ++∞ +0 % +Hoch +β€” +β€” +β€” +get update when website changes +10 – 100 +0 % +0 % +Mittel +β€” +β€” +β€” +notify me when a webpage changes +10 – 100 +0 % +0 % +Mittel +β€” +1,10 € +11,26 € +monitor page for changes chrome +10 – 100 +0 % +0 % +Gering +β€” +β€” +β€” +watch a webpage for changes +10 – 100 +0 % +0 % +Gering +β€” +β€” +β€” +email alert when website changes +10 – 100 +0 % +0 % +Hoch +β€” +β€” +β€” +website file changes monitor +10 – 100 +0 % +0 % +Mittel +β€” +β€” +β€” +website change monitoring software +10 – 100 +0 % +-90 % +Gering +β€” +β€” +β€” +page change notification +10 – 100 +0 % +0 % +Gering +β€” +1,63 € +4,35 € +page change detector +10 – 100 +0 % +0 % +Mittel +β€” +β€” +β€” +chrome monitor page for changes +10 – 100 +0 % +0 % +Gering +β€” +β€” +β€” +check website changes every minute +10 – 100 +-100 % +0 % +β€” +β€” +β€” +β€” +watch page for changes +10 – 100 +0 % +0 % +Gering +β€” +1,18 € +11,88 € +get notified when web page changes +10 – 100 +0 % +0 % +Hoch +β€” +1,63 € +4,26 € +be notified when a website changes +10 – 100 +0 % +0 % +Gering +β€” +β€” +β€” +notification if a website changes +10 – 100 +-100 % +-100 % +β€” +β€” +β€” +β€” +notification if website changes +10 – 100 +0 % +0 % +Gering +β€” +β€” +β€” +notify when webpage changes +100 – 1000 +-90 % +-90 % +Gering +β€” +1,00 € +5,69 € +get notified when webpage changes +10 – 100 +0 % +0 % +Hoch +β€” +1,63 € +4,26 € diff --git a/frontend/app/blog/page.tsx b/frontend/app/blog/page.tsx index 6e203dc..0102420 100644 --- a/frontend/app/blog/page.tsx +++ b/frontend/app/blog/page.tsx @@ -1,7 +1,21 @@ +import type { Metadata } from 'next' import Link from 'next/link' import { ArrowLeft } from 'lucide-react' import { Footer } from '@/components/layout/Footer' +export const metadata: Metadata = { + title: 'Blog β€” Website Monitoring Tips & Updates', + description: + 'Guides, tutorials, and product updates from the SiteChangeMonitor team. Learn how to monitor websites effectively and reduce false alerts.', + alternates: { canonical: '/blog' }, + openGraph: { + title: 'Blog β€” Website Monitoring Tips & Updates', + description: + 'Guides, tutorials, and product updates from the SiteChangeMonitor team.', + url: '/blog', + }, +} + export default function BlogPage() { return (
@@ -14,7 +28,7 @@ export default function BlogPage() {

Blog

- Latest updates, guides, and insights from the Alertify team. + Latest updates, guides, and insights from the SiteChangeMonitor team.

diff --git a/frontend/app/features/[slug]/page.tsx b/frontend/app/features/[slug]/page.tsx new file mode 100644 index 0000000..43ffb15 --- /dev/null +++ b/frontend/app/features/[slug]/page.tsx @@ -0,0 +1,170 @@ +import type { Metadata } from 'next' +import Link from 'next/link' +import { ArrowLeft } from 'lucide-react' +import { Footer } from '@/components/layout/Footer' +import { notFound } from 'next/navigation' + +const features: Record = { + 'noise-filtering': { + title: 'AI Noise Filtering', + metaDescription: 'SiteChangeMonitor uses AI to automatically filter out cookie banners, timestamps, rotating ads, and session IDs β€” delivering zero-noise website change alerts.', + intro: 'AI Noise Filtering is the core feature that sets SiteChangeMonitor apart. It automatically identifies and filters out irrelevant page changes β€” cookie banners, footer timestamps, rotating ads, and session-specific content β€” so you only receive alerts for meaningful updates.', + howItWorks: 'Our AI analyzes each page change in context. It recognizes common noise patterns (date stamps, consent popups, A/B test variations, ad rotations) and suppresses them automatically. You can also add custom ignore rules using CSS selectors, regex patterns, or plain text matching.', + vsAlternatives: 'Visualping and Distill.io require manual configuration of ignore rules, which most users skip β€” leading to constant false alerts. SiteChangeMonitor applies smart noise filtering by default, significantly reducing false positives without any setup.', + faqs: [ + { q: 'Does AI filtering work on all websites?', a: 'Yes. Our filtering engine recognizes common noise patterns across all site types β€” SPAs, e-commerce, news sites, and more. You can also add custom rules for edge cases.' }, + { q: 'Can I customize what gets filtered?', a: 'Absolutely. You can add custom ignore rules (CSS selectors, regex, text patterns) on top of the automatic AI filtering.' }, + { q: 'Will I miss important changes?', a: 'No. The AI only filters known noise patterns. Any change it cannot confidently classify as noise is passed through to you.' }, + ], + }, + 'visual-diff': { + title: 'Visual Diff & Screenshots', + metaDescription: 'See exactly what changed on any web page with side-by-side screenshot diffs. SiteChangeMonitor provides visual proof of every change.', + intro: 'Visual Diff gives you screenshot-based proof of every website change. Instead of parsing raw HTML diffs, see side-by-side visual comparisons that highlight exactly what changed on the page.', + howItWorks: 'SiteChangeMonitor captures full-page screenshots on every check. When a change is detected, we generate a visual diff that highlights modified areas. You can compare any two snapshots in your version history.', + vsAlternatives: 'Distill.io offers text-based diffs only. UptimeRobot has no diff capability. Visualping offers visual diffs but lacks AI noise filtering, so most of the changes shown are irrelevant noise.', + faqs: [ + { q: 'What format are the screenshots?', a: 'Screenshots are captured as full-page PNG images and stored with timestamps for audit purposes.' }, + { q: 'Can I compare any two versions?', a: 'Yes. You can select any two snapshots from the version history and generate a visual diff between them.' }, + { q: 'Are screenshots included in the free plan?', a: 'Yes, visual diffs are available on all plans including the Forever Free tier.' }, + ], + }, + 'keyword-monitoring': { + title: 'Keyword Monitoring', + metaDescription: 'Set keyword triggers on any web page. Get alerted when specific words appear, disappear, or cross a count threshold. SiteChangeMonitor keyword monitoring.', + intro: 'Keyword Monitoring lets you set precise triggers for when specific words or phrases appear, disappear, or cross a count threshold on any monitored page. Ideal for tracking pricing terms, product availability, job postings, and competitive messaging.', + howItWorks: 'Add one or more keyword rules to any monitor. Choose trigger type: "appears" (word added to page), "disappears" (word removed), or "count" (word frequency crosses a threshold). Supports exact match and regex patterns.', + vsAlternatives: 'UptimeRobot offers basic keyword checking but no appear/disappear logic. Distill.io supports conditions but requires complex selector configuration. SiteChangeMonitor makes keyword monitoring a first-class feature with a simple UI.', + faqs: [ + { q: 'Can I use regex for keyword matching?', a: 'Yes. You can use full regular expressions for complex pattern matching, such as price formats ($XX.XX) or phone numbers.' }, + { q: 'What is a count threshold trigger?', a: 'Count triggers alert you when a keyword appears more (or fewer) than N times on a page. Useful for tracking inventory counts or job listing volumes.' }, + { q: 'Can I combine keyword alerts with noise filtering?', a: 'Yes. Noise filtering runs first, then keyword checks run on the cleaned content β€” ensuring accurate keyword detection.' }, + ], + }, + 'seo-ranking': { + title: 'SEO & Ranking Alerts', + metaDescription: 'Monitor search engine ranking changes, featured snippet movements, and SERP updates. SiteChangeMonitor alerts SEO teams to ranking shifts.', + intro: 'SEO & Ranking Alerts help SEO professionals track changes in search engine results pages. Monitor your target keywords for ranking shifts, featured snippet ownership changes, and new competitor appearances.', + howItWorks: 'Point a monitor at a Google search results URL for your keyword. SiteChangeMonitor captures the SERP from a clean, non-personalized browser session and alerts you when rankings change. AI filtering removes localized variations.', + vsAlternatives: 'Dedicated SEO tools like Ahrefs and SEMrush track rankings but cost $99+/month and are built for large-scale keyword tracking. SiteChangeMonitor is ideal for focused SERP monitoring at a fraction of the cost.', + faqs: [ + { q: 'Does this replace my SEO rank tracker?', a: 'It complements rank trackers by providing instant alerts on SERP changes. Use it for your most important keywords that need real-time monitoring.' }, + { q: 'How do you handle personalized results?', a: 'We use clean, non-personalized browser sessions from consistent locations to ensure results are not skewed by personal search history.' }, + { q: 'Can I track featured snippets?', a: 'Yes. Set a keyword trigger for your brand name in the featured snippet area to know instantly when you gain or lose the snippet.' }, + ], + }, + 'multi-channel-alerts': { + title: 'Multi-Channel Alerts', + metaDescription: 'Get website change notifications via email, Slack, or webhooks. SiteChangeMonitor delivers alerts where your team works.', + intro: 'Multi-Channel Alerts deliver website change notifications where your team already works β€” email, Slack, or webhooks. Route different monitors to different channels based on urgency and team.', + howItWorks: 'Configure alert channels per monitor or globally. Each channel can have its own rules: immediate alerts for critical monitors, daily digests for informational ones. Webhooks support custom payloads for integration with any system.', + vsAlternatives: 'Visualping restricts Slack integration to enterprise plans. Distill.io supports basic notifications but lacks channel routing. SiteChangeMonitor includes Slack and webhook channels on Pro plans and above, with email alerts on every plan.', + faqs: [ + { q: 'Is Slack included in the free plan?', a: 'Slack and webhook integrations are available on Pro and Business plans. The free plan includes email notifications.' }, + { q: 'Can I set up digest emails?', a: 'Yes. Choose between instant alerts and daily or weekly digest emails that summarize all changes across your monitors.' }, + { q: 'Do webhooks support custom payloads?', a: 'Yes. You can customize the webhook payload format to integrate with any system β€” Zapier, Make, n8n, or your own API.' }, + ], + }, +} + +export function generateStaticParams() { + return Object.keys(features).map((slug) => ({ slug })) +} + +export function generateMetadata({ params }: { params: { slug: string } }): Metadata { + const data = features[params.slug] + if (!data) return {} + return { + title: data.title, + description: data.metaDescription, + alternates: { canonical: `/features/${params.slug}` }, + openGraph: { title: data.title, description: data.metaDescription, url: `/features/${params.slug}` }, + } +} + +export default function FeaturePage({ params }: { params: { slug: string } }) { + const data = features[params.slug] + if (!data) notFound() + + const faqJsonLd = { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: data.faqs.map((faq) => ({ + '@type': 'Question', + name: faq.q, + acceptedAnswer: { '@type': 'Answer', text: faq.a }, + })), + } + + return ( +
+