Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/migration/status.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ _Last updated: 2026-06-13_

## Where we are

**🚀 READY TO CUT OVER (2026-06-13).** Steps 1–8 are complete — every Step 8 slice (A–G) landed, incl. the full-history dividend line chart; the slice-F comparison gate was spot-checked rather than run formally (Francesc's call — "all looks fine, fix whatever feels wrong later"). Beta runs real data, the final-import path is rehearsed end-to-end on the VPS, and **production email is live** (Resend, `quantic.es` domain verified — magic links deliver). All that remains is executing **Step 9** in a maintenance window: the cutover is a **kamal-proxy host re-route, not a DNS change** (legacy + monolith share one IP), per the runbook in [`docs/deploy.md`](../deploy.md#production-cutover-step-9). Founder to schedule the window. Carry-overs are explicitly deferrable post-cutover: per-user date/decimal format (needs a centralized formatting layer), native review of pt/de/fr/it, and the legacy `source=ibkr` dividend mislabel (fix by re-uploading statements through `/dividends`, which detects source correctly).

**Step 8 slice G landed — the admin dashboard** (← legacy AdminDashboardPage). New `users.admin` boolean (never cast from user input; set by the legacy import — now carried — or `mix quantic.set_admin EMAIL [--revoke]` / `Accounts.set_admin/2`). Gated by `UserAuth.on_mount(:ensure_admin)` in a `live_session :admin`; non-admins (and signed-out visitors) are bounced to `/` with no hint the area exists. An Admin nav link shows only for admins. `Quantic.Admin.dashboard_stats/0` aggregates across every context — users (total/signups/admins), catalog (stocks/enriched/radars/tracked/buy-plan items), portfolios, dividends (adoption + imported/manual split), community adoption, activity (active 7d/30d from session tokens + holding-change counts + an 8-week active-users trend), AI (calls today/7d/30d, by-feature, top users, at-quota today) and Telegram (linked, notifications, bot questions, top bot users) — demo users excluded so the numbers reflect real usage. Plus `Admin.users/0` (the users table with per-user radar/holdings counts), `delete_user/2` (can't delete self; cascades), and `refresh_all_stocks/0` (enqueues the RefreshWorker with a new `force` arg that bypasses the market-hours gate). The display improves on legacy's dense grid: sectioned gradient stat cards (the community/pulse design language), a CSS bar trend chart, and top-user/by-feature panels. ~45 strings translated ×6.

**Step 8 slice G complete — content drafts landed** (← legacy `AdminContentDraftsPage`): the founder social-post drafting tool at `/admin/content-drafts` (admin-gated via the same `live_session :admin` on_mount; linked from the dashboard's header). New `Quantic.Content` context (`content_drafts` table, binary-id, `payload`/`inputs` jsonb) with a three-stage generation pipeline ported from legacy's `ContentDrafts::*`: **`TopicSelector`** picks `{category, key}` candidates — weighted auto-pick across `stock_of_the_day`/`dividend_calendar`/`pulse_aggregates` with per-category repeat windows (14/21/60d) + a 1-day cross-category cooldown, plus `feature_announcement` deduped forever by name-slug; **`TopicDataBuilder`** assembles each topic's `inputs` from the monolith's own contexts (MarketData quote+rating, the new `MarketData.stocks_with_ex_dividend_between/2`, `Community.dashboard_stats`), returning `nil` when a topic has no data today so the caller falls through to the next candidate; **`ContentGenerator`** turns inputs into X (≤280) + LinkedIn (≤3000) drafts via `AI.generate_json/3` (system-initiated, no user quota), carrying legacy's voice rules + per-category framing, char-limit truncation (flagged), and a **privacy guard** that rejects any draft leaking an email / `portfolio_slug` / public profile URL. The LiveView generates async (`start_async`, spinner, error surfacing), lists drafts newest-first, and per-draft offers copy (colocated clipboard hook), mark-posted/unmark, and discard. ~16 strings translated ×6. Tests cover the context (slug dedup, privacy-guard rejection, truncation, mark/discard) + the LiveView (admin gate, generate, discard). **Step 8 slice G is now fully complete.**
Expand Down Expand Up @@ -163,7 +165,7 @@ The legacy stack had four cache layers (gem 5-min in-memory, app 1-h shared, FX

## What's next

**Step 8 is in progress** (slice roadmap below); Step 9 (cutover) follows once the comparison gate passes.
**Step 8 is complete (slice roadmap below); the project is ready to cut over.** Step 9 is the only remaining work — an ops maintenance window, not code. See the READY TO CUT OVER summary at the top and the [cutover runbook](../deploy.md#production-cutover-step-9).

| Option | Size | Description |
|---|---|---|
Expand Down
Loading