From f71d0ab0adcbe4211159b7bb4afa99e90198106a Mon Sep 17 00:00:00 2001 From: Francesc Leveque Date: Sat, 13 Jun 2026 18:26:18 +0200 Subject: [PATCH] docs(status): mark the project ready to cut over MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Steps 1-8 complete; beta runs real data with the import path rehearsed and production email live (Resend, quantic.es verified). Step 9 is the only remaining work — an ops window, per the cutover runbook. Adds a READY TO CUT OVER summary at the top of status and updates What's next. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/migration/status.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/migration/status.md b/docs/migration/status.md index 464029e..581d493 100644 --- a/docs/migration/status.md +++ b/docs/migration/status.md @@ -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.** @@ -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 | |---|---|---|