diff --git a/CLAUDE.md b/CLAUDE.md index a7c7704..2ed968b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ Fredy is a self-hosted real estate finder for Germany. It scrapes German real es - Node.js >= 22, ESM-only (`"type": "module"`) - Default port: 9998, default login: admin / admin -- SQLite via `better-sqlite3` (synchronous — all DB ops are sync; only network I/O is async) +- SQLite via `better-sqlite3` (synchronous - all DB ops are sync; only network I/O is async) ## Commands @@ -66,13 +66,13 @@ scheduler (every N minutes) or manual trigger via POST /api/jobs/:id/run ### Plugin systems -**Providers** (`lib/provider/*.js`) — each module exports: -- `metaInformation` — `{ id, name, baseUrl }` -- `config` — `ProviderConfig` with `requiredFieldNames`, `crawlContainer`, `crawlFields`, `sortByDateParam`, `normalize()`, `filter()`, optional `getListings()`, `fetchDetails()`, `activeTester()` -- `init(sourceConfig, blacklist)` — called before each job run; providers are **stateful modules** holding mutable `url` and `appliedBlackList` at module scope +**Providers** (`lib/provider/*.js`) - each module exports: +- `metaInformation` - `{ id, name, baseUrl }` +- `config` - `ProviderConfig` with `requiredFieldNames`, `crawlContainer`, `crawlFields`, `sortByDateParam`, `normalize()`, `filter()`, optional `getListings()`, `fetchDetails()`, `activeTester()` +- `init(sourceConfig, blacklist)` - called before each job run; providers are **stateful modules** holding mutable `url` and `appliedBlackList` at module scope -**Notification adapters** (`lib/notification/adapter/*.js`) — each exports: -- `config` — `{ id, name, description, fields }` (drives the UI form) +**Notification adapters** (`lib/notification/adapter/*.js`) - each exports: +- `config` - `{ id, name, description, fields }` (drives the UI form) - `send({ serviceName, newListings, notificationConfig, jobKey, baseUrl })` - Loaded dynamically at startup via `fs.readdirSync` @@ -98,18 +98,18 @@ scheduler (every N minutes) or manual trigger via POST /api/jobs/:id/run ### MCP server Two transports: -1. **stdio** (`lib/mcp/stdio.js`) — for Claude Desktop/LM Studio; opens its own DB connection (main process need not be running) -2. **HTTP** (`/api/mcp`) — authenticated via Bearer token (`mcp_token` column in `users` table) +1. **stdio** (`lib/mcp/stdio.js`) - for Claude Desktop/LM Studio; opens its own DB connection (main process need not be running) +2. **HTTP** (`/api/mcp`) - authenticated via Bearer token (`mcp_token` column in `users` table) Tools: `list_jobs`, `get_job`, `list_listings`, `get_listing`, `get_current_date_time`. Responses are Markdown via `lib/mcp/mcpNormalizer.js`. ## Key Conventions -- **ESM only** — `import`/`export` everywhere, no CommonJS -- **JSDoc typedefs** (no TypeScript) in `lib/types/` — `listing.js`, `job.js`, `filter.js`, `providerConfig.js` -- **Copyright header** required on all `.js` files — enforced by `lint-staged` pre-commit hook via `copyright.js` +- **ESM only** - `import`/`export` everywhere, no CommonJS +- **JSDoc typedefs** (no TypeScript) in `lib/types/` - `listing.js`, `job.js`, `filter.js`, `providerConfig.js` +- **Copyright header** required on all `.js` files - enforced by `lint-staged` pre-commit hook via `copyright.js` - **`NoNewListingsWarning`** (`lib/errors.js`) is used as control flow to short-circuit the pipeline (not an error) -- **Test fixtures** in `test/testFixtures/` — HTML/JSON snapshots per provider; `TEST_MODE=offline` mocks `puppeteerExtractor` and global `fetch` via `test/offlineFixtures.js` +- **Test fixtures** in `test/testFixtures/` - HTML/JSON snapshots per provider; `TEST_MODE=offline` mocks `puppeteerExtractor` and global `fetch` via `test/offlineFixtures.js` - **`conf/config.json`** is the only runtime config file; created with defaults if missing ## Coding diff --git a/docker-test.sh b/docker-test.sh index 35d6184..af7b36b 100755 --- a/docker-test.sh +++ b/docker-test.sh @@ -43,13 +43,13 @@ for i in $(seq 1 30); do done # Verify the DB is readable/writable via the API. -# /api/demo is unauthenticated and reads the settings table — if SQLite is broken this returns an error. +# /api/demo is unauthenticated and reads the settings table - if SQLite is broken this returns an error. echo "Testing DB via API (/api/demo)..." DEMO_RESPONSE=$(docker exec fredy curl -sf http://localhost:9998/api/demo 2>&1) if echo "$DEMO_RESPONSE" | grep -q "demoMode"; then echo "DB is readable (got demoMode from /api/demo)" else - echo "DB check failed — unexpected response from /api/demo: $DEMO_RESPONSE" + echo "DB check failed - unexpected response from /api/demo: $DEMO_RESPONSE" docker logs fredy exit 1 fi diff --git a/docs/superpowers/plans/2026-04-22-fredy-ui-redesign.md b/docs/superpowers/plans/2026-04-22-fredy-ui-redesign.md deleted file mode 100644 index c5dcb12..0000000 --- a/docs/superpowers/plans/2026-04-22-fredy-ui-redesign.md +++ /dev/null @@ -1,2371 +0,0 @@ -# Fredy UI Redesign Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Apply the cronpilot visual identity (dark design system with red accent) to Fredy's React frontend screen-by-screen using Semi UI component overrides and LESS tokens. - -**Architecture:** Add a `tokens.less` file as the single source of truth for all design variables; override Semi UI's CSS custom properties globally in `Index.less`; apply the new CI to every view and shared component. No state management changes. No routing changes. - -**Tech Stack:** React 18, `@douyinfe/semi-ui-19` (v2.95), LESS (BEM), Zustand, React Router, MapLibre GL. - ---- - -## File Map - -| File | Action | Purpose | -|---|---|---| -| `index.html` | Modify | Add Google Fonts link | -| `ui/src/tokens.less` | Create | All LESS/CSS design tokens | -| `ui/src/Index.less` | Modify | Semi UI CSS variable overrides, body bg, scrollbar | -| `ui/src/App.less` | Modify | App shell layout | -| `ui/src/components/navigation/Navigation.jsx` | Modify | New sidebar with footer bar, collapse logic | -| `ui/src/components/navigation/Navigate.less` | Modify | Sidebar LESS | -| `ui/src/components/footer/FredyFooter.jsx` | Modify | "Made with heart by Christian Kellner" link | -| `ui/src/components/footer/FredyFooter.less` | Modify | Footer bar styles | -| `ui/src/components/headline/Headline.jsx` | Modify | Become `PageHeading` with gradient line + optional action slot | -| `ui/src/views/login/Login.jsx` | Modify | New centered glass-card login | -| `ui/src/views/login/login.less` | Modify | New login styles | -| `ui/src/components/cards/KpiCard.jsx` | Modify | Compact 112px card, glow hover only | -| `ui/src/components/cards/DashboardCard.less` | Modify | New KPI card LESS | -| `ui/src/views/dashboard/Dashboard.jsx` | Modify | Add PageHeading, remove section-label typography | -| `ui/src/views/dashboard/Dashboard.less` | Modify | Dashboard layout LESS | -| `ui/src/components/grid/jobs/JobGrid.jsx` | Modify | New topbar layout + table layout (replace Card grid) | -| `ui/src/components/grid/jobs/JobGrid.less` | Modify | Table + topbar LESS | -| `ui/src/views/jobs/Jobs.jsx` | Modify | Add PageHeading | -| `ui/src/components/segment/SegmentPart.jsx` | Modify | New section-card design | -| `ui/src/components/segment/SegmentParts.less` | Modify | Section card LESS | -| `ui/src/views/jobs/mutation/JobMutation.jsx` | Modify | Add PageHeading + back button | -| `ui/src/views/jobs/mutation/JobMutation.less` | Modify | Minor layout updates | -| `ui/src/components/grid/listings/ListingsGrid.jsx` | Modify | New card design with action bar, watermark | -| `ui/src/components/grid/listings/ListingsGrid.less` | Modify | New listing card LESS | -| `ui/src/views/listings/Map.jsx` | Modify | Add PageHeading, styled filter panel | -| `ui/src/views/listings/Map.less` | Modify | Filter panel overlay styles | -| `ui/src/views/user/Users.jsx` | Modify | Add PageHeading, styled table | -| `ui/src/views/user/Users.less` | Modify | Users table LESS | -| `ui/src/components/table/UserTable.jsx` | Modify | Avatar initials, admin badge, styled rows | -| `ui/src/views/user/mutation/UserMutator.jsx` | Modify | Add PageHeading + back button | -| `ui/src/views/user/mutation/UserMutator.less` | Modify | Section card layout | -| `ui/src/views/generalSettings/GeneralSettings.jsx` | Modify | Tab styles, section cards, Save button with icon | -| `ui/src/views/generalSettings/GeneralSettings.less` | Modify | Tab + section card LESS | - ---- - -## Task 1: Design Tokens + Global Styles - -**Files:** -- Create: `ui/src/tokens.less` -- Modify: `index.html` -- Modify: `ui/src/Index.less` - -This is the foundation. Every subsequent task imports `tokens.less`. - -- [ ] **Step 1: Create `ui/src/tokens.less`** - -```less -// ── Backgrounds ────────────────────────────────────────── -@color-base: #0d0d0d; -@color-surface: #161616; -@color-elevated: #1e1e1e; -@color-border: #2a2a2a; -@color-border-bright: #383838; - -// ── Accent ─────────────────────────────────────────────── -@color-accent: #e04a38; -@color-accent-dim: #c13827; -@color-accent-glow: rgba(224, 74, 56, 0.13); - -// ── Text ───────────────────────────────────────────────── -@color-text: #efefef; -@color-muted: #909090; -@color-faint: #505050; - -// ── Semantic ───────────────────────────────────────────── -@color-success: #34d399; -@color-success-dim: #065f46; -@color-error: #fb7185; -@color-error-dim: #881337; -@color-warning: #fbbf24; -@color-info: #60a5fa; - -// ── KPI card accents ───────────────────────────────────── -@color-blue-text: #60a5fa; @color-blue-border: #3b6ea8; @color-blue-bg: rgba(96,165,250,0.10); -@color-orange-text: #fb923c; @color-orange-border: #c2622a; @color-orange-bg: rgba(251,146,60,0.10); -@color-green-text: #34d399; @color-green-border: #2a8a61; @color-green-bg: rgba(52,211,153,0.10); -@color-purple-text: #a78bfa; @color-purple-border: #6d4fc2; @color-purple-bg: rgba(167,139,250,0.10); -@color-gray-text: #94a3b8; @color-gray-border: #323a47; @color-gray-bg: rgba(148,163,184,0.10); - -// ── Typography ─────────────────────────────────────────── -@font-ui: 'Outfit', system-ui, sans-serif; -@font-mono: 'JetBrains Mono', monospace; - -@text-xs: 11px; -@text-sm: 12px; -@text-base: 14px; -@text-md: 16px; -@text-lg: 20px; -@text-xl: 24px; - -// ── Spacing ────────────────────────────────────────────── -@space-1: 4px; -@space-2: 8px; -@space-3: 12px; -@space-4: 16px; -@space-5: 20px; -@space-6: 24px; -@space-8: 32px; -@space-12: 48px; - -// ── Radius ─────────────────────────────────────────────── -@radius-input: 10px; -@radius-card: 10px; -@radius-btn: 6px; -@radius-pill: 9999px; -@radius-chip: 4px; - -// ── Transitions ────────────────────────────────────────── -@transition-fast: 0.15s ease-in-out; -@transition-card: 0.18s ease-in-out; -@transition-sidebar: width 0.25s ease-in-out; -``` - -- [ ] **Step 2: Add Google Fonts to `index.html`** - -Open `index.html` and add inside `` before the closing `` tag: - -```html - - - -``` - -- [ ] **Step 3: Replace `ui/src/Index.less` with new global styles** - -```less -@import './tokens.less'; - -body, -html { - margin: 0; - height: 100%; - width: 100%; - font-family: @font-ui; - background-color: @color-base; - background-image: radial-gradient(ellipse at 60% 0%, rgba(224,74,56,0.05) 0%, transparent 55%); - background-attachment: fixed; -} - -body { - // Semi UI theme overrides - --semi-color-bg-0: #0d0d0d; - --semi-color-bg-1: #161616; - --semi-color-bg-2: #1e1e1e; - --semi-color-bg-3: #2a2a2a; - --semi-color-border: #2a2a2a; - --semi-color-primary: #e04a38; - --semi-color-primary-hover: #c13827; - --semi-color-primary-active: #c13827; - --semi-color-primary-light-default: rgba(224,74,56,0.12); - --semi-color-primary-light-hover: rgba(224,74,56,0.18); - --semi-color-primary-light-active: rgba(224,74,56,0.22); - --semi-color-text-0: #efefef; - --semi-color-text-1: #efefef; - --semi-color-text-2: #909090; - --semi-color-text-3: #505050; - --semi-color-fill-0: rgba(255,255,255,0.04); - --semi-color-fill-1: rgba(255,255,255,0.06); - --semi-color-fill-2: rgba(255,255,255,0.08); - --semi-font-family: 'Outfit', system-ui, sans-serif; -} - -// Semi table row overrides -.semi-table-row-head { - background-color: rgba(255,255,255,0.06) !important; -} -.semi-table-row-head .semi-table-row-cell { - background-color: rgba(255,255,255,0.06) !important; - color: @color-muted !important; - font-size: @text-xs; - text-transform: uppercase; - letter-spacing: 0.04em; -} -.semi-table-row-cell { - background-color: @color-surface !important; -} -.semi-table-tbody .semi-table-row:nth-child(even) .semi-table-row-cell { - background-color: @color-base !important; -} -.semi-table-tbody .semi-table-row:hover .semi-table-row-cell { - background-color: @color-elevated !important; -} - -// Scrollbar -::-webkit-scrollbar { width: 6px; } -::-webkit-scrollbar-track { background: @color-surface; } -::-webkit-scrollbar-thumb { background: @color-border-bright; border-radius: 3px; } -::-webkit-scrollbar-thumb:hover { background: @color-muted; } - -.semi-icon:not(.semi-tabs-bar .semi-tabs-tab .semi-icon) { - vertical-align: middle; -} -``` - -- [ ] **Step 4: Update `ui/src/App.less` to import tokens and fix layout** - -```less -@import './tokens.less'; - -.app { - height: 100vh; - width: 100vw; - - &__main { - height: 100vh; - display: flex; - flex-direction: column; - overflow: hidden; - } - - &__content { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - position: relative; - padding: @space-6; - background-color: transparent; - box-sizing: border-box; - display: flex; - flex-direction: column; - - @media (max-width: 768px) { - padding: @space-3; - } - } -} -``` - -- [ ] **Step 5: Replace `ui/src/components/cards/DashboardCardColors.less`** - -The file currently defines all KPI color variables. Replace its entire content with just the import so there is one source of truth: - -```less -@import '../../tokens.less'; -``` - -All `@color-blue-*`, `@color-orange-*`, etc. now come from `tokens.less`. The import chain `DashboardCard.less → DashboardCardColors.less → tokens.less` makes every token available in DashboardCard.less. - -- [ ] **Step 6: Add Semi Modal overrides to `ui/src/Index.less`** - -Append to the end of `Index.less` (inside or after the body rule): - -```less -// Semi Modal dark theme overrides -.semi-modal-content { - background: #161616 !important; - border: 1px solid @color-border !important; - border-radius: 14px !important; -} - -.semi-modal-header { - background: linear-gradient(135deg, #1e1e1e 0%, #161616 100%) !important; - border-bottom: 1px solid @color-border !important; - border-left: 3px solid @color-accent !important; - border-radius: 14px 14px 0 0 !important; -} - -.semi-modal-mask { - background: rgba(0,0,0,0.7) !important; - backdrop-filter: blur(4px); -} -``` - -- [ ] **Step 7: Verify the app compiles** - -```bash -yarn dev -``` - -Expected: Vite dev server starts without errors. The page should be mostly dark with the Outfit font visible. - -- [ ] **Step 8: Commit** - -```bash -git add index.html ui/src/tokens.less ui/src/Index.less ui/src/App.less ui/src/components/cards/DashboardCardColors.less -git commit -m "feat: add design tokens and Semi UI theme overrides" -``` - ---- - -## Task 2: App Shell — Sidebar Navigation - -**Files:** -- Modify: `ui/src/components/navigation/Navigation.jsx` -- Modify: `ui/src/components/navigation/Navigate.less` - -The sidebar expands to 220px / collapses to 60px. Auto-collapses at <=850px. Active item has red accent. Footer has logout + collapse toggle. - -- [ ] **Step 1: Replace `ui/src/components/navigation/Navigate.less`** - -```less -@import '../../tokens.less'; - -.navigate { - display: flex; - flex-direction: column; - height: 100%; - width: 100%; - background: @color-surface; - border-right: 1px solid @color-border; - transition: @transition-sidebar; - overflow: hidden; - - &__header { - display: flex; - align-items: center; - justify-content: center; - padding: 20px 16px 16px; - min-height: 64px; - flex-shrink: 0; - - img { - transition: width @transition-fast, opacity @transition-fast; - } - } - - &__footer { - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - padding: 12px 8px; - margin-top: auto; - flex-shrink: 0; - } -} - -// Semi Nav overrides -.semi-navigation { - background: @color-surface !important; - border-right: none !important; -} - -.semi-navigation-item { - border-radius: @radius-btn !important; - color: @color-muted !important; - transition: background @transition-fast, color @transition-fast !important; - margin: 2px 8px !important; - - &:hover { - background: #23242a !important; - color: @color-text !important; - } - - &.semi-navigation-item-selected, - &[aria-selected="true"] { - background: rgba(224,74,56,0.12) !important; - border: 1px solid rgba(224,74,56,0.25) !important; - color: @color-text !important; - - .semi-navigation-item-icon { - color: @color-accent !important; - } - } -} - -.semi-navigation-sub-title { - color: @color-muted !important; -} -``` - -- [ ] **Step 2: Update `ui/src/components/navigation/Navigation.jsx`** - -No structural changes needed in JSX — the Semi `Nav` component is kept. Just update the `style` prop and add the `navigate` class: - -```jsx -/* - * Copyright (c) 2026 by Christian Kellner. - * Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause - */ - -import { useEffect, useState } from 'react'; -import { Button, Nav } from '@douyinfe/semi-ui-19'; -import { IconStar, IconSetting, IconTerminal, IconHistogram, IconSidebar } from '@douyinfe/semi-icons'; -import logoWhite from '../../assets/logo_white.png'; -import heart from '../../assets/heart.png'; -import Logout from '../logout/Logout.jsx'; -import { useLocation, useNavigate } from 'react-router-dom'; - -import './Navigate.less'; -import { useScreenWidth } from '../../hooks/screenWidth.js'; - -export default function Navigation({ isAdmin }) { - const navigate = useNavigate(); - const location = useLocation(); - - const width = useScreenWidth(); - const [collapsed, setCollapsed] = useState(width <= 850); - - useEffect(() => { - if (width <= 850) { - setCollapsed(true); - } - }, [width]); - - const items = [ - { itemKey: '/dashboard', text: 'Dashboard', icon: }, - { itemKey: '/jobs', text: 'Jobs', icon: }, - { - itemKey: 'listings', - text: 'Listings', - icon: , - items: [ - { itemKey: '/listings', text: 'Overview' }, - { itemKey: '/map', text: 'Map View' }, - ], - }, - ]; - - if (isAdmin) { - items.push({ - itemKey: 'settings', - text: 'Settings', - icon: , - items: [ - { itemKey: '/users', text: 'User Management' }, - { itemKey: '/generalSettings', text: 'Settings' }, - ], - }); - } else { - items.push({ - itemKey: 'settings', - text: 'Settings', - icon: , - items: [{ itemKey: '/generalSettings', text: 'Settings' }], - }); - } - - function parsePathName(name) { - const split = name.split('/').filter((s) => s.length !== 0); - return '/' + split[0]; - } - - const sidebarWidth = collapsed ? '60px' : '220px'; - - return ( -