Platform Codex — Full Build & Remediation Log ================================================ Date: 2026-03-29 PLATFORM OVERVIEW ================= A premium SaaS-style personal dashboard integrating self-hosted services: - Budget (Actual Budget) - Inventory (NocoDB) - Fitness (SparkyFitness) - Trips - Reader (Miniflux) - Media (Shelfmark/Spotizerr/Booklore) Stack: SvelteKit (Svelte 5 runes) + Python gateway + Express.js services Orchestration: Docker Compose, 6 containers Reverse proxy: Pangolin at dash.quadjourney.com Gitea repo: gate.quadjourney.com/yusiboyz/platform WHAT WAS BUILT ============== 1. Design System - Unified CSS token system: spacing (--sp-*), radius (--radius-*), shadows (--shadow-*), colors, typography (--text-*) - Migrated 235 raw values across 31 files - Documented in frontend-v2/DESIGN_SYSTEM.md 2. Gateway Modular Refactor - Split 1878-line server.py into 15 modular files: server.py (thin router), config.py, database.py, auth.py, proxy.py, dashboard.py, plus integrations/ directory (booklore, kindle, image_proxy, qbittorrent, etc.) - ThreadingHTTPServer for concurrent requests - 30-second per-user dashboard cache 3. Fitness API Integration - Replaced all mock data with real SparkyFitness backend API calls - AI food input with multi-item splitting (/api/fitness/foods/split) - Canonical food storage with dedup (naive singularize + pre-creation check) - Entry editing (quantity) and deletion - Confirmation modal for resolved items with quantity +/- - Local timezone date fix (was showing UTC = next day after 7pm CDT) - Per-user goals editing in Settings - Shared food database across users, independent fitness goals 4. Media App (Built from Scratch) - Books tab: search via Shelfmark/Anna's Archive, download + auto-import to Booklore, per-book library dropdown - Music tab: Spotify search via Spotizerr, track/album/artist/playlist search types, Spotify embed player, download progress polling - Library tab: Booklore library browser (237 books), cover resolution via ISBN -> Open Library with localStorage cache, book detail modal, format badges (EPUB/PDF), library filter pills - Send to Kindle: SMTP2GO API integration, per-book Kindle label selector ("Madiha"/"Hafsa"), sends from bookdrop or booklore-books volumes - "After download, also send to" Kindle option on book search 5. Multi-User Support - Created Madiha's account - Per-user nav visibility (cosmetic only, documented as such) - hiddenByUser map in +layout.server.ts - Shared food database, independent fitness goals 6. Frontend Architecture - SvelteKit with Svelte 5 runes ($state, $props, $derived, $effect, $bindable) - SvelteKit hooks.server.ts for auth on Immich/Karakeep proxies - Settings page: real API data, fitness goals editing, disconnect confirmation dialog, theme toggle, sign out SECURITY REMEDIATION (Gitea Issues #1-#10) ========================================== All 10 issues completed. Re-audited and verified in code on 2026-03-29. #2 Auth Boundary - /api/auth/register disabled (403) - Gateway admin seeded from ADMIN_USERNAME/ADMIN_PASSWORD env vars only - Trips USERNAME/PASSWORD have no default fallback - Fitness user seed requires env vars (no "changeme" default) - All passwords use bcrypt #3 Trips Sharing Security - handle_share_api enforces password via X-Share-Password header + bcrypt - share_password stored as bcrypt hash - All plaintext password logging removed - Existing plaintext passwords invalidated by migration - Dead hash_password function removed #4 Fitness Authorization - All user_id query params enforced to authenticated user's own ID - /api/users returns only current user - Wildcard CORS removed #5 Gateway Trust Model - Inventory and budget require API keys (X-API-Key middleware) - Token validation uses protected endpoints per service type - /debug-nocodb removed from inventory - /test removed from inventory - NocoDB search filter sanitized (strips operator injection chars) - SERVICE_LEVEL_AUTH renamed to GATEWAY_KEY_SERVICES - Trust model documented in docs/trust-model.md - Per-user vs gateway-key services clearly distinguished - Known limitations documented (no per-user isolation on shared services) #6 Repository Hygiene - No .env or .db files tracked in git - .gitignore covers: .env*, *.db*, services/**/.env, data/, test-results/ - .env.example updated with all current env vars (no secrets) #7 Transport Security - Gateway: _internal_ssl_ctx removed entirely (internal services use plain HTTP) - Gateway: ssl import removed from config.py - Gateway: proxy.py uses urlopen() without context parameter - Gateway: logout cookie includes HttpOnly, Secure, SameSite=Lax - Gateway: image proxy uses default TLS + domain allowlist + content-type validation - Trips: all 5 CERT_NONE sites removed (OpenAI, Gemini, Google Places, Geocode) - Inventory: permissive cors() removed AND dead cors import removed - Budget: permissive cors() removed AND dead cors import removed #8 Dependency Security - Budget path-to-regexp vulnerability fixed - .gitea/workflows/security.yml committed with 3 jobs + workflow_dispatch trigger - Gitea Actions enabled ([actions] ENABLED = true in app.ini) - Runner (gitea/act_runner) added to Gitea docker-compose - Runner registered as platform-runner on gitea_gitea network - Config sets container.network = gitea_gitea so job containers can git clone - Runner token stored in /media/yusiboyz/Media/Scripts/gitea/.env - All 3 jobs verified passing: - dependency-audit: SUCCESS (npm audit on budget + frontend) - secret-scanning: SUCCESS (no tracked .env/.db, no hardcoded secrets) - dockerfile-lint: SUCCESS (all Dockerfiles have USER + HEALTHCHECK) - Runner setup documented in .gitea/README.md #9 Performance Hardening - Inventory /issues: server-side NocoDB WHERE filter (no full scan) - Inventory /needs-review-count: server-side filter + pageInfo.totalRows - Budget /summary: 1-minute cache - Budget /transactions/recent: 30-second cache - Budget /uncategorized-count: 2-minute cache - Budget buildLookups: 2-minute cache - Gateway /api/dashboard: 30-second per-user cache - Actual Budget per-account API constraint documented #10 Deployment Hardening - All 6 containers run as non-root (appuser/node) - Health checks on gateway, trips, fitness, inventory, budget, frontend - PYTHONUNBUFFERED=1 on all Python services - Trips Dockerfile only copies server.py (not whole context) - Frontend uses multi-stage build RE-AUDIT FINDINGS (2026-03-29) ============================== 1 inaccuracy found in prior report: CORS dead imports (const cors = require('cors')) remained in inventory/server.js and budget/server.js after app.use(cors()) was removed. Fixed by removing the dead imports. All other claims verified accurate in code: - Trips TLS: zero CERT_NONE or check_hostname = False - Settings disconnect: confirm() dialog present - /test cleanup: no references remain - Cosmetic nav: documented as cosmetic-only, no false authz claims GITEA ACTIONS RUNNER SETUP (2026-03-29) ======================================= Problem: Workflow existed in repo but no runner was configured to execute it. What was done: 1. Added [actions] ENABLED = true to Gitea app.ini File: /media/yusiboyz/Media/Scripts/gitea/gitea/gitea/conf/app.ini 2. Restarted Gitea to pick up config change 3. Generated runner token: docker exec -u git gitea gitea actions generate-runner-token 4. Added runner service to Gitea docker-compose: File: /media/yusiboyz/Media/Scripts/gitea/docker-compose.yml Image: gitea/act_runner:latest Container: gitea-runner 5. Saved token in /media/yusiboyz/Media/Scripts/gitea/.env as RUNNER_TOKEN 6. First attempt: job containers created on auto-generated network, could not reach server:3000 for git clone (hung on git fetch) 7. Fix: created /data/config.yaml inside runner with container.network = gitea_gitea and set CONFIG_FILE=/data/config.yaml env var 8. Recreated runner container (docker compose up -d runner) to pick up env change 9. Triggered workflow via API: POST /api/v1/repos/yusiboyz/platform/actions/workflows/security.yml/dispatches 10. All 3 jobs ran to completion: dependency-audit, secret-scanning, dockerfile-lint = SUCCESS 11. Added workflow_dispatch trigger to security.yml for manual runs 12. Updated .gitea/README.md with setup documentation Key detail: job containers must be on gitea_gitea network to resolve "server:3000" for git operations. Without this, git fetch hangs indefinitely. BUGS FIXED DURING BUILD ======================== - SERVICE_MAP import bug: captured empty dict at import time, fixed with module reference - Gateway Dockerfile missing modules: only copied server.py, fixed to copy all .py + integrations/ - Non-root container permission denied: fixed with COPY --chown=appuser - Fitness date timezone: toISOString() returns UTC, fixed with local date construction - Dashboard fitness widget not updating: plain let vs $state() in Svelte 5 - Food library empty: /api/fitness/foods/recent returns entry-shaped data, fixed mapFood - Book covers from search: double-wrapped image proxy URLs, fixed to proxy directly - Booklore cover API returns HTML: switched to Open Library + Google Books fallback - Booklore books API too slow (14s): moved to lazy client-side cover resolution - Fitness entries orphaned after DB reset: reassigned to new user IDs - Madiha accidentally disconnected fitness: added confirm() dialog - Double Kindle sends: actually processed + delivered SMTP2GO events, added debounce - Kindle email typo: lowercase L vs uppercase I in address MANUAL OPS ACTIONS ================== 1. Store admin password securely (set via ADMIN_PASSWORD env var) 2. Clean up local untracked .env files with real credentials if needed 3. Monitor @sveltejs/kit for a non-breaking cookie fix in future releases ARCHITECTURE REFERENCE ====================== - Trust model: docs/trust-model.md - CI workflows: .gitea/workflows/security.yml - Runner setup: .gitea/README.md - Design system: frontend-v2/DESIGN_SYSTEM.md - Env var reference: .env.example - Gitea instance: localhost:3300 (gate.quadjourney.com) - Gitea compose: /media/yusiboyz/Media/Scripts/gitea/docker-compose.yml - Platform compose: /media/yusiboyz/Media/Scripts/platform/docker-compose.yml