_system / plans · Option B (đã chốt)
Status: draft for Editor review · 2026-05-31 · author: Claude session Companion to: _system/notes/management-hub-mockup-handoff-2026-05-31.md, memory project_management-hub-v2-design. The live mockup is v2.1 at https://hmt-media-review.pages.dev/management-hub/.
This doc answers two asks:
panel it maps to.
(Option B: one app, one login, mounted sub-apps), in de-risked phases.
Phase A is untouched throughout: the hub reads and routes; every sensitive action (approve-with-dignity, watchlist escalation, thank-you send) stays in its own surface behind the login. No auto-publish; no channel-publisher cron.
| # | Area | Field → output | Data store (bucket) | Cloud Run | Maturity |
|---|---|---|---|---|---|
| 2 | Media & Story | Drive upload → ingest → sanitise → triage → retouch → caption → QC → review → approve → Stage-1 pack | index/registry.sqlite + Sandbox/posts/ (bundles bucket) + Dashboard Sheet | event-folder, ingest-watch, pipeline, decisions, status-reconcile, review | Deployed, live (Đợt 1–4) |
| 1 | Care / Beneficiary | intake drop → promote → profile + benefits ledger + watchlist; school-doc extract | so-dang-ky.sqlite + Ho-so/*.md (care-db bucket) | care-dashboard, care-jobs | Deployed (Đợt 3) |
| 3+4 | Donor / Finance | donor intake → contribution / in-kind → thank-you + receipt → monthly transparency report | so-tai-chinh.sqlite (care-db bucket) | finance-dashboard (factory ready), donor-input (built) | Built, not deployed (Đợt 5) |
| Op1 | Community | comments/messages → approval-gated replies → R1 hide+escalate → monthly engagement report | none yet | none yet | Not built (placeholder) |
Three SQLites, split by privacy boundary, never ATTACH-joined — any cross-area read (e.g. a benefit funded by a specific donor) happens at the application layer.
FIELD EDITORIAL AUDIENCE RESOURCES
┌────────────────────┐ ┌──────────────────┐ ┌──────────────┐ ┌──────────────────┐
│ brief-form submit │ │ content plan │ │ approved post│ │ donor gives │
│ → event folder │ │ (12mo + 3mo) │ │ → FB + web │ │ cash / in-kind │
│ photos to Drive │ │ ↑ pulls from │ │ │ │ → thank-you + │
│ → ingest-watch │ │ trips, holidays,│ │ presence: │ │ receipt │
│ → registry.sqlite │ │ milestones, │ │ cadence, │ │ → transparency │
│ │ │ journey miles │ │ 80/20, │ │ report │
│ pipeline: │ │ │ │ petal cover │ │ │
│ sanitise→triage→ │ │ orphan-trip flag │ │ │ │ community: │
│ retouch→caption→ │──┼─▶ feeds plan │ │ comments → │◀─┤ "where it went" │
│ QC (gate 1) │ │ │ │ replies │ │ follow-ups │
│ → awaiting-approval│ │ queue: approve │ │ (Op1) │ │ │
└─────────┬──────────┘ │ with dignity_ok │ └──────┬───────┘ └────────┬─────────┘
│ └────────┬─────────┘ │ │
CARE (parallel, field-driven): │ │ │
intake→promote→profile+benefits │ │ │
+watchlist ──── journey miles ──┘ │ │
▼ ▼
everything closes the transparency loop
| Hub panel (sidebar) | Backed by | Kind |
|---|---|---|
| 🌻 Tổng quan | summary counts from all four areas + today-strip | new (native hub) |
| 🧭 Hoạt động & Chuyến đi | Dashboard Sheet rows + brief.yaml + registry.sqlite photo counts | new read-view |
| 🌱 Thụ hưởng | app/main.py (care-dashboard) | mount existing |
| 🗓️ Kế hoạch nội dung | new content-plan artifact + content-calendar agent + VN observances | new surface + new data store |
| 📝 Hàng đợi duyệt | app/review_main.py (review) | mount existing |
| 📣 Hiện diện xã hội | registry.sqlite post table + monthly recaps | new read-view |
| 💬 Cộng đồng | — | placeholder |
| 🤝 Tài trợ & Tài chính | app/finance_dashboard.py (finance) | mount existing |
| ℹ️ Cách hoạt động | static | static |
So the hub is 3 mounts (care, review, finance) + 3 new read-views (trips, presence, overview) + 1 new surface with its own data (content plan) + 1 placeholder (community).
One Cloud Run Service management-hub, one login, this route layout:
app/hub.py (new parent — owns login + session middleware)
/ → Overview (native)
/trips → Trip ledger (native read-view)
/plan → Content plan (native; new data store)
/presence → Social presence (native read-view)
/community → placeholder (native static)
/login /logout → parent-owned (care_auth session)
/care/* → mount app.main:create_app(...) auth OFF (parent gates)
/review/* → mount app.review_main:create_app(...) auth OFF (parent gates)
/finance/* → mount app.finance_dashboard:create_finance_app(...)
Single login. All three apps already import tools/care_auth.py and already expose factories (create_app, create_app, create_finance_app). The parent owns /login + the @app.middleware("http") session check; Starlette runs that middleware around mounted sub-apps too, so each sub-app is built with password_hash=None (auth_off) and trusts the parent gate. One secret pair (care-password-hash, care-session-key) already shared by review + care.
Two buckets, one service. This is the part the "50 lines" estimate missed:
registry.sqlite,Sandbox/posts/, _system/runs).
so-dang-ky, so-tai-chinh).gcsfuse can mount both in one container at different paths (e.g. /data for bundles, /care-data for the care-db bucket); each sub-app's env points at its own path. This is a Cloud Run deploy-config change (two volume mounts), not app code. The single service account already has object-admin on both buckets.
templates use literal href="/bundle/...", /children/..., /donors/.... Mounted at /review etc., those point back at the parent root. Fix: switch the handful of cross-page links to request.url_for(...) (Starlette includes the mount prefix) or root-relative-to-mount. This is the main effort in the mount step — a template audit per app, not a rewrite.
/static + /login + /healthz across sub-apps. Harmless oncemounted (each lives under its prefix, e.g. /review/static), but the parent must own the canonical /login; sub-app logins become dead routes (leave or strip).
/plan is the only panel whose data doesnot exist yet — no plan artifact on disk, content-calendar has no board, Knowledge/Brand/facts.md is missing. Needs a small artifact (content-plan.yaml: 12-month themes + 3-month slots + source palette) and a thin reader. The self-flagging "editorial intelligence" (orphan trips, petal gaps, 80/20 drift, observance countdown) is computed, not stored.
static/. The parent gets its own template dir; sub-apps keep theirs. Shared CSS can stay one file served by the parent.
Sheet.** The Sheet read needs the service account creds the pipeline already uses; cache it (it changes slowly).
Honest effort: not 50 lines. Realistically 3–4 focused sessions: (a) parent shell + login + finance mount, (b) trip + presence read-views, (c) content-plan artifact + plan surface, (d) review + care mount with template-link audit + the two-bucket deploy. The "~50 lines" was just the finance mount+auth.
Phase 0 — decide & freeze the contract (no code). Confirm the route layout in §3 and the panel map in §2. Confirm Editor is fine with one combined public URL replacing the separate app URLs.
Phase 1 — parent shell + login + finance mount. Lowest risk: finance is the newest, simplest templates, not yet deployed, and the mount path is already designed for. Build app/hub.py (login + middleware + a static Overview reading mock numbers first), mount finance at /finance, audit finance templates for absolute links. Deploy locally; verify one login reaches finance.
Phase 2 — trip ledger + presence read-views (native, real data). No mount risk (new code). Read Dashboard Sheet + registry.sqlite for the trip ledger and the presence aggregation. Wire the Overview today-strip + tiles to these real numbers. Orphan-trip + petal-gap computed here.
Phase 3 — content-plan artifact + /plan surface. Define content-plan.yaml, a thin reader, and the editorial-intelligence computed flags. This is the genuinely new product surface; can lag if the Editor wants to ship the mounts first.
Phase 4 — mount review + care; two-bucket deploy. The heaviest: template-link audit for both, build the management-hub Dockerfile, mount both GCS buckets, move the env to per-app paths, deploy as one Cloud Run Service, retire (or alias) the separate review + care-dashboard services. Verify Phase-A approve flow end to end through the mounted review app.
Phase 5 — community. Only when Op1 is actually built; the placeholder holds the slot meanwhile.
De-risking option for Phase 4: if the template audit proves fiddly, an interim is to keep review + care as their own services but share the session cookie + secret so the single login works across them and the hub deep-links (closer to A for those two, full-B for finance + the native views). Ship that, finish the true mount later. Stated here so it is a deliberate choice, not drift.
dignity_ok, watchlist escalation, reject, thank-you send stay intheir own surface behind the login (gate-8 unchanged; safeguarding.md §3a).
channel-publisher cron; nothing posts to Facebook from the hub.still private-by-login (the public Pages mockup stays mock-only, noindex).
read-views. Route layout per §3.
read-views → Content-plan → review/care mount. Each phase ships usable; the two-bucket mount + template audit comes last.
content-calendar generates it. Not ahand-edited YAML — Phase 3 wires the existing content-calendar agent to produce/refresh the 12-month themes + 3-month board from VN observances + trips (orphan-trip feed) + journey milestones. The agent writes the plan artifact; the hub reads it and computes the editorial-intelligence flags.
management-hub Service replaces the standalone review + care-dashboard (and absorbs the not-yet-deployed finance-dashboard) in Phase 4. One service, one URL, one login. (Cut-over is a single switch — mitigate by verifying the full Phase-A approve flow on the new service before retiring the old ones.)
app/hub.py: login + session middleware + Overview (mock numbers) +mount finance at /finance + finance template-link audit.
registry.sqlite);wire Overview today-strip/tiles to real numbers.
content-calendar to emit the plan artifact; build /plan surface+ computed flags (orphan trips, petal gaps, 80/20 drift, observance countdown).
review + care (template-link audit each); management-hubDockerfile; two-bucket gcsfuse deploy; replace the old services after the approve-flow smoke test passes.