Nội bộ — kế hoạch. Tài liệu thiết kế để duyệt, không phải bản phân phối. Bản nháp, có thể thay đổi. Một số định danh hạ tầng đã được lược bỏ. noindex.

_system / plans · Option B (đã chốt)

Management Hub — sơ đồ vận hành & kế hoạch tích hợp

Một app · một đăng nhập · gắn các bảng con. Bản nháp 2026-05-31.

Status: design-locked (Option B) · architecture 2026-05-31, build backlog reconciled with the per-flow walkthrough 2026-06-02 · author: Claude session (decisions in §7 still hold; the real-data mount P1–P5 is not yet built). This revision folds in everything the per-flow walkthrough surfaced so §8 is a start-ready build list. Companion to: _system/notes/management-hub-workflows-walkthrough-2026-05-31.md (the end-to-end operating detail + per-flow Gaps — the source for §8), _system/notes/management-hub-mockup-handoff-2026-05-31.md, _system/plans/donor-and-finance-subplan.md, _system/plans/story-archive-subplan.md, memory project_management-hub-integration-plan. The live mockup is v2.1+ at https://hmt-media-review.pages.dev/management-hub/ (Quản lý học sinh tab expanded 2026-06-02) and the walkthrough at …/management-hub-workflows/.

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.

**Update 2026-06-02 (workflow reshape, administrator-confirmed — reconciled

with the per-flow walkthrough).** The mockup + walkthrough (`management-hub-

workflows`) evolved past the v2.1 panel set captured below. The Option-B

architecture and phase ordering (§3, §7) are unaffected; what changed is the

per-workflow build backlog, now catalogued in §8. Net changes:

- Media is on-demand, not auto-drafted. Bringing an activity into the Hub

(folder + photos + watch-job row) is its own workflow that ends at the Hub;

the daily pipeline narrows to ingest + sanitise only. Story drafting

fires on a per-activity "Yêu cầu viết bài" button, taking a **direction

box + an optional user-selected photo set. Not yet decoupled** — today

the daily pipeline still runs straight through cull→retouch→caption. (§8-A.)

- **Finals archive to the EXISTING 10_Cau-chuyen/ story band — the

09_Mang-xa-hoi/ band is NOT provisioned** (decision revised the same day,

2026-06-02). The social-final vs story-record distinction is carried by

registry.post tags, not a separate Drive band. (Supersedes the earlier

"new 09_ band" note. story-archive-subplan.md, walkthrough §2/§4.B.)

- The registry.post writer is the single most important unbuilt gap. There

is no INSERT INTO post anywhere; the manual-publish loop ends at

publish.yaml + posted_id and never back-fills registry.sqlite, so the

Presence read-view + content-plan suggestions read zeros until it exists.

Added to P2. (§8-B, walkthrough §7/§10.)

- A human-gated self-improve summary of the admin's edits is sent for review

before anything is applied to prompts/few-shot (not auto-optimize's own

cadence). (§8-A.)

- New Khảo sát (survey & selection) workflow — a #survey hub panel + a

candidate survey-Form → restricted 11_Khao-sat/ band → a **candidate

register (own store until admission) → human "admit"** → mints

journey-NNNN → hands to Care. Internal-only; never a published post. Entirely

unbuilt. (§8-D, walkthrough §1b.) Care panel renamed Quản lý học sinh.

- Email-link-back (the new-folder link emailed to the form submitter) is a

new unbuilt step shared by both the activity intake (§1) and the survey intake

(§1b). (§8-A/D.)

- Donor/Finance split into two panels — **#donors (Tài trợ — engagement &

communications) and #finance (Tài chính — transparency & distribution)**;

plus a standalone donor-engagement mockup. A scoped relaxation lets the

transactional thank-you + receipt auto-send (receipts only; newsletters +

appreciation stay human-approved) — recorded in .claude/rules/autonomy-phase.md.

The bank-credit trigger is unbuilt (today: manual NCB-CSV); candidate under

review is the SePay Open-Banking webhook (§8-E). (workflows-doc §4 / §5;

donor-and-finance-subplan.md 2026-06-02 block.)

- The old presence/community/about hub panels from v2.1 were folded/dropped in

the current mockup; the operating-loop framing is unchanged.


1. The four areas (what is actually built)

#AreaField → outputData store (bucket)Cloud RunMaturity
2Media & Storyactivity intake (folder+photos, ends at Hub) ‖ on-demand draft → QC → review → approve → Stage-1 pack → 10_Cau-chuyen/ archiveindex/registry.sqlite + Sandbox/posts/ (bundles bucket) + Dashboard Sheetevent-folder, ingest-watch, pipeline, decisions, status-reconcile, reviewDeployed, live (Đợt 1–4); on-demand decoupling + registry.post writer unbuilt
1Care / Beneficiary (Quản lý học sinh)intake drop → promote → profile + benefits ledger + watchlist; school-doc extractso-dang-ky.sqlite + Ho-so/*.md (care-db bucket)care-dashboard, care-jobsDeployed (Đợt 3)
1bKhảo sát (survey & selection)survey-Form → 11_Khao-sat/ candidate folder → candidate register → human admit → mint journey-NNNN → Carecandidate register (own store until admission) + 11_Khao-sat/* (restricted)survey-folder Service + survey watch Job (both new)Not built (mockup #survey panel only)
3Donor (Tài trợ)donate link → form → personalised VietQR → bank-matched contribution → thank-you + receipt → lists / newsletter / appreciationso-tai-chinh.sqlite (donor + contribution + update_recipient)donor-input Service built (deploy-ready)Code-complete; donor-input deploy-ready. Bank-credit trigger + SMTP auto-send unbuilt
4Finance (Tài chính)initial position → periodic statement upload + extract → internal report → public transparency (via media QC) → in-kind ledgerso-tai-chinh.sqlite (07_Quan-tri/Tai-chinh/)finance-dashboard factory ready (lift parked)Code-complete; not deployed. Initial-position input + general-statement extractor unbuilt
Op1Communitycomments/messages → approval-gated replies → R1 hide+escalate → monthly engagement reportnone yetnone yetNot 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. Areas 3 + 4 share so-tai-chinh.sqlite (the contribution table) but are split into two hub panels. The candidate register (1b) stays separate from so-dang-ky.sqlite until admission, then the admitted candidate becomes a journey-NNNN in the care store.


2. Workflow sketch — the operating loop and where each surface lives

            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

Area → hub panel map

Hub panel (sidebar)Backed byKind
🌻 Tổng quansummary counts from all areas + "Cần bạn xử lý hôm nay" action strip (3 independent DB reads, stitched in Python)new (native hub)
🧭 Hoạt độngDashboard Sheet rows + brief.yaml + registry.sqlite photo counts; orphan-trip flagnew read-view
🏡 Khảo sátcandidate register + 11_Khao-sat/; read + light-write (admit/revisit/reject)new surface + new data store
🎒 Quản lý học sinh (Thụ hưởng)app/main.py (care-dashboard)mount existing
🗓️ Kế hoạch nội dungnew content-plan.yaml + content-strategist (plans) + content-calendar (drafts) + VN observances + facts.mdnew surface + new data store
📝 Dự thảo nội dung / Hàng đợi duyệtapp/review_main.py (review)mount existing
📣 Hiện diện xã hộiregistry.sqlite post table + monthly recaps (needs the post-row writer)new read-view
💛 Tài trợ (Area 3)app/finance_dashboard.py donor surface (donor CRM + comms)mount existing
📊 Tài chính (Area 4)app/finance_dashboard.py finance surface (transparency + distribution)mount existing
💬 Cộng đồngplaceholder

So the hub is 3 mounts (care, review, finance — finance serving both the #donors and #finance panels) + 3 new read-views (trips, presence, overview) + 2 new surfaces with their own data (content plan, survey/candidate register) + 1 placeholder (community).


3. Target architecture (Option B)

One Cloud Run Service management-hub, one login, this route layout:

app/hub.py  (new parent — owns login + session middleware)
  /                     → Overview        (native; action strip = 3 DB reads)
  /trips                → Trip ledger     (native read-view; orphan-trip flag)
  /survey               → Candidate survey & selection (native; new register store)
  /plan                 → Content plan    (native; new content-plan.yaml store)
  /presence             → Social presence (native read-view; needs post-row writer)
  /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(...)  → serves both
               the #donors (Tài trợ) and #finance (Tài chính) panels

/survey is a native light read/write surface over the care bucket (the candidate register), not a mount — it shares the care DB connection but writes to a register store kept separate from so-dang-ky.sqlite until a candidate is admitted. #donors and #finance are two views of the one mounted finance app, not two services.

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:

Sandbox/posts/, _system/runs).

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.


4. The real wrinkles (honest scoping)

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.

mounted (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 panel whose data does not exist yet — no content-plan.yaml on disk, no board, Knowledge/Brand/facts.md missing (a prerequisite). The walkthrough §6 splits the role: a new content-strategist agent _plans_ (proposes/refreshes content-plan.yaml slots as status: idea on a monthly anchor + weekly flag-refresh cadence), while the existing content-calendar _drafts_ one Editor-confirmed slot. The Editor promotes idea → planned slot-by-slot (same human-tick discipline as dignity_ok); an unconfirmed slot cannot be drafted. The self-flagging "editorial intelligence" (orphan trips, petal gaps, 80/20 drift, observance countdown) is computed at read time, not stored — and most flags read zero until the post-row writer (#6) exists.

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). The Overview action strip stitches three independent DB reads in Python (bundles by iterating Sandbox/posts/*/STATUS.md; care + finance each via a separate read-only connection) — never** an ATTACH-join across the files.

posts_published: 0; there is no INSERT INTO post anywhere. The manual-publish loop ends at publish.yaml + a typed posted_id and never back-fills registry.sqlite, so Presence and the content-plan suggestions read zeros indefinitely until a post-row writer is built (hooked on posted_id recording, alongside the 10_Cau-chuyen/ archive write, carrying tags activity_type · petal · program · school · journey_id · theme · observance, with petal_anchor validated against the 8 canonical petals). This is not strictly "hub plumbing," but it gates a hub read-view, so P2 owns it.

finding — agent gates 3–7 (brand-voice, dignity, claims, platform-fit) are not invoked in the cloud path, so the human approval is the real substantive QC**. Fine under Phase A (nothing posts without /approve), but worth stating: the hub surfaces the bundle for human review precisely because automated QC upstream is thin.

Honest effort: not 50 lines. Realistically 3–4 focused sessions for the hub shell itself: (a) parent shell + login + finance mount, (b) trip + presence read-views + the post-row writer, (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. The per-workflow build items (media decoupling, survey subsystem, email-link-back, bank-credit trigger, finance lift) are separate tracks catalogued in §8 — several are independent of the hub shell and can start in parallel.


5. Phased plan (de-risked order — each phase ships something usable)

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) + the post-row writer. 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 real numbers; orphan-trip + petal-gap computed here. Prerequisite for Presence to show anything: build the registry.post writer (§4 wrinkle 6 / §8-B) — without it Presence reads zeros. This is the single highest-value gap and is not in the original v1 phase list.

Phase 3 — content-plan: content-strategist (plans) + content-calendar (drafts) + /plan surface. Create Knowledge/Brand/facts.md (missing prerequisite); add the content-strategist agent that emits/refreshes content-plan.yaml (idea slots only); build the /plan reader + per-slot promote (idea → planned) + computed flags; add the external-photo lane (a sanitised, provenance-tagged source/ drop for Stream-B reuse). content-calendar still drafts a picked slot. 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.


6. Phase-A invariants (non-negotiable, carried through every phase)

their own surface behind the login (gate-8 unchanged; safeguarding.md §3a).

still private-by-login (the public Pages mockup stays mock-only, noindex).


7. Decisions — LOCKED (Editor, 2026-05-31)

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.

2026-06-02 (walkthrough §6): the role splits into a new content-strategist agent that _plans_ (proposes/refreshes content-plan.yaml — 12-month themes + 3-month board from VN observances + the orphan-trip feed + journey milestones + facts.md — as idea slots), and the existing content-calendar that _drafts_ one Editor-confirmed slot. Splitting keeps synthesis (no vision) apart from drafting (Opus vision). The Editor promotes slots idea → planned one at a time; the hub reads the artifact and computes flags. (Decision intent unchanged from the lock; the agent split is an implementation refinement.)

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.)

Resulting build backlog — hub shell (the Option-B mount track)

mount finance at /finance (serving both #donors + #finance) + finance template-link audit + finance auth-param wiring (the "~50 lines").

wire Overview today-strip/tiles to real numbers. Add: the registry.post writer (§8-B) — without it Presence/AUDIENCE has no data.

slots; content-calendar still drafts a picked slot); build /plan + per-slot promote + computed flags + the external-photo lane; create the missing Knowledge/Brand/facts.md.

Dockerfile; two-bucket gcsfuse deploy; replace the old services after the approve-flow smoke test passes.

The per-workflow build tracks (media on-demand decoupling, the survey/selection subsystem, email-link-back, the bank-credit trigger + SMTP auto-send, the finance lift) are catalogued in §8 with maturity flags + dependencies. Several are independent of the hub shell and can be picked up in parallel.


8. Per-workflow build backlog (reconciled with the walkthrough)

Concrete, start-ready items distilled from the Gaps lines of each flow in management-hub-workflows. The hub-shell track (P1–P5 above) builds the parent app + mounts; these tracks build the workflow gaps underneath it. Tracks A–F are largely independent of the shell except where noted, so they can run in parallel.

Maturity legend (from the walkthrough): 🟢 deployed-live · 🟡 deployed · 🟠 built-not-deployed · 🔵 new read-view (unbuilt) · 🟣 new surface + new data (unbuilt) · ⚪ placeholder.

Phase-A holds across all of §8: the only auto-send is the donor transactional thank-you+receipt (scoped relaxation; never a post/newsletter/appreciation); nothing posts without /approve; gate-8 dignity is a human action; no channel-publisher cron.

A. Media — on-demand drafting reshape 🔵🟣 (walkthrough §1/§2)

Today the daily 09:00 pipeline Job auto-drafts every bundle with photos. Target: the daily Job stops at sanitise; drafting is on-demand.

automatic cull→retouch→caption chain). 🔵

planned content-plan slot) that runs cull→retouch→caption on demand. 🔵

steer) + an optional user-selected photo set (overrides auto-cull). 🔵

onFormSubmit.gs, which already holds the submitter email) emails the new folder link to the submitter so they know where to upload. 🟣

(reuse the existing story archive; no 09_ band). 🟣

edits sent for review before anything is applied to prompts/few-shot.yaml (distinct from auto-optimize's own cadence). 🟣

writer fires on the same approval/posted_id hook).

B. The registry.post writer — #1 gap 🟣 (walkthrough §7/§10; = P2 add)

decisions_core, alongside the 10_Cau-chuyen/ archive write). 🟣

journey_id · theme · observance; validate petal_anchor against the 8 canonical petals (also fixes the free-text petal_anchor in brief.yaml`). 🟣

draft time, plan suggestions for under-covered petals/activities, and the reminders/follow-up countdown on the Overview.

value; do early.**

C. Content plan 🟣 (walkthrough §6; = P3)

prerequisite**). 🟣

(directions block + roadmap_12mo + board_3mo slots as status: idea), monthly anchor + weekly flag refresh. 🟣

computed flags (orphan trips, petal gaps, 80/20 drift, observance countdown). 🟣

drops files into source/, mandatorily runs media_sanitise (gate 2) + writes a provenance sidecar (origin · supplied_by · license_basis · attribution_required); claim-checker already blocks un-sourced third-party media. 🟣

read-views.

D. Khảo sát — survey & selection subsystem 🟣 (walkthrough §1b)

Entirely unbuilt; the mockup #survey panel exists but nothing backs it.

activity brief-Form) + onSurveySubmit.gs. 🟣

(decided 2026-06-02: a new band, kept separate from the beneficiary store until admission). 🟣

11_Khao-sat/): make <date>__ung-vien-<NNNN>/{anh}, write phieu-khao-sat.yaml, append a candidate register row state=cho-xet-chon. Slug is a non-identifying case id (ung-vien-NNNN), reserved alongside journey-NNNN so the two never collide. 🟣

never enters the media pipeline) → Hub/register upsert. 🟣

journey-NNNN, creates 10_Ho-so-thu-huong/Ho-so/journey-NNNN/, seeds the profile from phieu-khao-sat.yaml (a beneficiary_promote-style first Block-3 note), stamps the candidate register state=da-chon with the journey-id link. No candidate→care promote exists today. 🟣

Overview "khảo sát chờ xét chọn" chip. Internal-only — does not feed media.

E. Donor + Finance 🟠 (walkthrough §4/§5; donor-and-finance-subplan.md)

review (2026-06-02, not decided): SePay Open-Banking webhook — a contribution_webhook endpoint verifies HMAC-SHA256**, adapts the payload to the shape contribution_import_statement already emits, then runs donor_matchcontribution_promote (idempotent on bank_txn_ref; SePay id = dedup key). Confirm: Foundation owns the SePay account + bank link (third-party processor → external legal/data handling), NCB supported, webhook pricing. 🔓

(receipts only; arms the relaxation in autonomy-phase.md). Currently inert — nothing auto-sends without it. 🔓

opt-in metadata only today). 🟠

params** (the literal "~50 lines"); management-hub Dockerfile + the two-bucket mount (= P4); so-tai-chinh.sqlite provisioned. 🟠

extractor** (today extract = NCB-CSV only). 🟣

(donor/finance tool code still emits the pre-decision 02_… string — a tracked follow-up). Receipt PDFs need weasyprint + GTK; receipt issuer block (reg number + signature image) still placeholder. F-zero/D-zero hand pilots before scale. 🟠

F. Cross-cutting hygiene

on 10_Ho-so-thu-huong/ and retire stale 01_ references. (naming-convention.md.)

(parked) or 10_Ho-so-thu-huong/ (trial reads the GCS bucket mirror); add to TREE when the finance lift ships / Care moves to the Shared Drive.

care /reports page only lists pre-generated markdown) — wire to a Job if on-demand report generation is wanted from the hub.

separately) — fold into the on-demand path (A2) if videos should draft too.

Suggested start order

registry.sqlite work, no shell dependency.

with B on the same approval hook; includes the shared email-link-back.

single login and the finance panels.

start using candidate intake independently of the rest.

an alternative) and the bank/account decision.