What is built, what is not, and what the rest is waiting on.
Generated from the repository’s roadmap file at build time, so this page cannot drift from the work. Every line below is either settled or says what it needs.
76 of 76 shipped
100%
01Foundation
07/07Shipped: Next.js 15 (App Router) + TypeScript + Tailwind scaffold — PR #1
Shipped: Cloudflare domain → Vercel deploy pipeline verified with a blank page — PR #2, live at financial-demo.nyxiontech.com
Shipped: Supabase project created, env vars wired — project fsboxdlbncegnhcjniaf, local .env.local wired and verified against the live API
Delivered alongside, not originally planned
Shipped: Public repo hardened: pre-commit secret scan, CI (gitleaks + semgrep OWASP + npm audit), weekly sweep, GitHub push protection
Shipped: main protected — PR required, 4 status checks, linear history, admins included
Shipped: Baseline security headers in next.config.ts (CSP deferred to Phase 3, see README)
Shipped: Dependency overrides for the postcss/sharp advisories create-next-app ships with
02Auth & Data Layer
08/08Shipped: profiles table with auto-create trigger on signup
Shipped: watchlist_items table with RLS (user_id = auth.uid() on all ops)
Shipped: price_alerts, quote_cache, news_cache — full data model migrated ahead of schedule
Shipped: Add-on #2 delivered early: 12 cross-user RLS isolation tests (npm run test:rls)
Shipped: Migrations applied to the remote project — pushed via supabase db push, history in sync, isolation suite re-run against production (12/12)
Shipped: Supabase SSR clients (browser / server / middleware split) — getUser() everywhere, never getSession()
Shipped: Supabase Auth: email/password + Google OAuth — server actions, /auth/callback code exchange, open-redirect guard unit-tested
Shipped: Auth-gated routes (middleware redirect if not logged in) — route policy extracted and unit-tested, redirect verified against a running server
03Landing + Shell
24/24Landing page
Shipped: Design tokens: colour system promoted to global scope (gold accent; green/red reserved for price only); type scale settles with the landing page
Shipped: Capture lib/fixtures/xau-daily.json — 160 real XAU/USD daily bars, integrity-checked
Shipped: Hero chart: progressive draw-in, dynamically imported, reduced-motion → final frame
Shipped: Hero copy: headline, subhead, primary + secondary CTA, server-rendered as LCP
Shipped: Contrast verified against the rendered chart, not the bare ground
Shipped: Feature section: watchlist, live charts, price alerts
Shipped: Footer: tech-stack credits, GitHub link, /roadmap link
Shipped: Responsive at 375 / 768 / 1440 — verified with Playwright, which sets a real layout viewport; zero horizontal overflow across all 8 routes
Shipped: Accessibility pass — global :focus-visible ring, reduced-motion honoured throughout, colourblind-safe deltas (validated ΔE 10.7 plus filled/hollow candles), aria-live on toasts and search status, semantic landmarks. Contrast verified by calculation, not yet by an automated audit run.
Shell
Shipped: App shell: shared nav across dashboard, company and roadmap, with active-route state and a signed-out variant
Shipped: Dark-mode toggle with a purpose-built light theme (not an inversion)
Shipped: Loading / empty / error states as reusable components
Shipped: Toast system (dismissible, deduplicated by error class, accessible via aria-live)
Shipped: Stream the shell — prices sit in their own Suspense boundary so a rate-limited feed delays numbers, not the page
Shipped: Cache-first render with an as of HH:MM badge; stale values marked in gold rather than hidden
Shipped: Anti-flash timing — skeletons hold at opacity 0 for 200ms then fade over 400ms, so content that streams in faster replaces them before they were ever visible
Shipped: Skeletons composed from the same layout primitives as the real components
Shipped: Placeholders shaped like their data (digit-width ch units in the mono face)
Shipped: Opacity pulse, not shimmer sweep; no animation at all under prefers-reduced-motion
Shipped: Hero chart draw-in is its loading state — no spinner; the chart is absolutely positioned in a min-height section so nothing shifts when it mounts
Shipped: Search keeps previous results on screen while fetching; spinner sits inside the input
/roadmap page
Shipped: Public route rendering this file's phases and checkbox state, parsed from ROADMAP.md at build time — single source of truth, no hand-maintained duplicate
Shipped: Per-phase completion and an overall figure
Shipped: Curated public view — only checkbox lines and add-on names are ever read, so build-strategy notes cannot reach the live page
04Financial Data Integration
07/07Shipped: lib/market-data/ provider interface + Twelve Data provider (quotes, time series, symbol search)
Shipped: Finnhub provider (company news, fundamentals) behind the same interface — keys stay server-side
Shipped: Verify both free tiers against real keys before building UI on top of them — Twelve Data time_series + quote, Finnhub company-news + metrics all confirmed working
Shipped: Response caching (quote_cache / news_cache table) with stale-serve fallback
Shipped: Batched quote fetching — one request per render, and only for tickers whose cache is stale
Shipped: Ticker search + autocomplete against Twelve Data's symbol search endpoint — debounced, request-aborted, results persist while refetching
Shipped: Error taxonomy (rate_limited / unavailable / not_entitled / not_found) + stale-cache fallback
05Watchlist Core
03/03Shipped: Add/remove company to watchlist (CRUD wired to Supabase) — no ownership clause in app code; RLS is the enforcement
Shipped: Dashboard: card per watched company — price, daily change, links through to the detail page
Shipped: Company detail page: candlestick chart with crosshair + OHLC readout, key stats (market cap, P/E, 52w range), recent headlines — four independent Suspense boundaries
06Polish Pass
27/27Shipped: Responsive check (mobile/tablet/desktop) — automated, 7 tests
Shipped: Skeleton loaders on all async data (dashboard cards, price, stats, news)
Shipped: Fault-injection pass — not_entitled and not_found forced live with real invalid credentials; rate_limited and unavailable covered by unit tests rather than by burning the daily budget. Note: the "invalid credentials" half of this now classifies as misconfigured, not not_entitled — see the 401/403 split below. The pass tested the path; it could not tell the two causes apart, which is why the defect survived it.
Shipped: README with architecture + decisions
Shipped: Canonical URLs + metadataBase — previews canonicalise to production, ?next= variants collapse to one URL
Shipped: robots.txt + sitemap.xml — all crawlers allowed including AI; sitemap lists only the two indexable URLs
Shipped: noindex on /login and /signup — thin auth pages; follow kept so the crawl passes through
Shipped: Structured data — WebSite + Person JSON-LD, server-rendered; 7 tests, escape validated by breaking it
Shipped: HSTS with includeSubDomains + preload — set in next.config.ts; preload still needs submitting at hstspreload.org
Shipped: Theme toggle on the landing page — the one route with no nav, so the control had no home; sits in the hero's top row opposite the instrument label
Shipped: Charts follow the theme — both lightweight-charts instances observe data-theme and re-apply their palette, since a canvas inherits no CSS. Previously a switch left dark grid lines on the light page
Shipped: Hero draw-in re-timed to 4.2s and driven by elapsed time, not frames — the old per-frame bar count ran the animation twice as fast on a 120Hz display as on a 60Hz one
Shipped: misconfigured split out of not_entitled (401 vs 403) — a deployment with no API key was telling visitors the data sat behind a paid plan; missing keys now fail before the request and name the variable in the server log
Shipped: Chart history — leading page raised 180 → 750 bars, and panning near the left edge pages in the next 750 through an auth-gated /api/candles. Immutable historical pages are cached in memory; end-of-history stops the requests and is stated in the readout
Shipped: Geist actually applied — @theme resolved --font-sans against :root, where the next/font variable was not defined, so every sans surface fell through to the system stack while Geist was downloaded and used for nothing
Shipped: color-scheme declared — native <select> popups, autofill and scrollbars were rendering from the light system palette on the dark theme
Shipped: --field-border token — a control's border is often the only thing marking where it is, which puts it under the 3:1 non-text floor; --rule-strong measured 1.69:1
Shipped: Alerts can be created from /alerts — the page whose subject is alerts was read-only and sent you elsewhere to make one. Form shared with the company page rather than copied
Shipped: /about/project and /about/author — stack, providers, failure taxonomy, data model and release process for a reader evaluating the work; two-tab section with a marker that travels
Shipped: Auth pages carry the nav and footer — nav revealed on scroll, and on focus so it is not a keyboard trap
Shipped: Alert mail fixed and the alerts page stopped overclaiming — the sender fell back to finance-demo… while the site is financial-demo…, so once Resend's verified domain changed, three consecutive alerts were rejected with a 403 that creates no email record. Nothing surfaced it, because the page labelled triggered_at as "emailed" and asserted mail that never left. Sender corrected, label now says triggered, which is what the column actually knows
Shipped: Every email is readable on /about/project — all four framed from the same files that get sent, so a reviewer sees them without triggering anything and the page cannot drift from the inbox. Needed X-Frame-Options narrowed to SAMEORIGIN for /mockups/ alone; the blanket DENY blocked the frames silently, which only rendering the page revealed
Shipped: Price alerts are actually evaluated — the schedule existed only as a sentence in the README, so alerts sat pending for days and the single alert that ever fired was fired by hand. Now an hourly pg_cron job committed as a migration (one Twelve Data credit per sweep, 24/day), plus an immediate scoped check when an alert is created so an already-met threshold does not look broken for an hour
Shipped: Alert claiming made a compare-and-set — running hourly and on creation lets two evaluations meet on one row, and the previous blind write would have let both send. Proven by racing three concurrent claims and confirming one winner
Shipped: Transactional emails designed — alert, signup confirmation and password reset share one dark shell built to email's constraints (tables, inline styles, VML buttons, declared colour scheme, preheader). Alerts gained a text/plain part, and muted tones were lifted above --faint, which measured 3.90:1 and failed AA in a medium with no theme toggle
Shipped: Ticker escaped before it reaches email HTML — price_alerts only enforces upper-case and a length cap, both of which <B> satisfies; the raw value was reaching the markup, and the <title> leak survived the first fix until a hostile-input render caught it
Shipped: Reviewer demo account — created pre-confirmed through the Admin API and seeded with six companies and two alerts, because a reviewer who signs up normally waits on a confirmation email and then lands on the empty state instead of the product. npm run seed:demo restores it, which is what makes publishing the password affordable. Its address is on RFC 2606's reserved example.com: an earlier draft used a real company's domain, which would have made the published password a way for any visitor to send that company automated mail
Beyond the core build
Extensions considered once the product above is complete.
Shipped: Price alerts (threshold-based) + Resend email via scheduled edge function shipped
Shipped: RLS tests proving cross-user isolation — 12 shipped in Phase 2
Shipped: Search debounce + polished autocomplete UX shipped in Phase 5
Shipped: Optimistic UI on watchlist add/remove shipped
Not started: Weekly digest email (cron + Resend, summarizing watchlist movers) — deferred (ran out of time)
Shipped: Sentiment tag on news headlines shipped
Shipped: E2E test (Playwright) for signup → add ticker → view dashboard shipped — 6 tests