Punch list — Trung tâm điều hành

Phiên rà soát giao diện hub ngày 13/06/2026 · 29 mục · DRAFT — INTERNAL · không phân phối

29/29 xong · còn lại 0 (deferred/ready/clarify) · cập nhật 14/06/2026

Mỗi mục là một nhận xét khi xem hub sống, kèm chẩn đoán (file/dòng), cách sửa đề xuất và trạng thái. Tên học sinh và mã hồ sơ đã được ẩn (trang này công khai).

Tổng hợp

ViệcTrạng thái
P1Wording: "Quân số" / "Phủ rộng 8 cánh" on the Phân tích pageXONG
P2Charts should be interactive (hover/touch tooltips)XONG
P3Chart needs axis labels (years / amounts)XONG
P4Roster table: right-size columns (one line each) + scrollXONG
P5Remove the "Google Doc / bản 2, bản 3" helper line on ReportsXONG
P6Student report (Báo cáo cá nhân) fixesXONG
P7Reports page: top line not left-aligned with content belowXONG
P8"Danh sách em" → "Danh sách học sinh"XONG
P9"Báo cáo cá nhân" → "Báo cáo học sinh"XONG
P10Survey / selection page (Khảo sát & xét chọn) restructureXONG
P11Remove mock candidate profiles (Drive folder)XONG
P12"Ảnh" photo counts read 0 for the year-end activity foldersXONG
P13Top-right avatar shows "QT", should be the logged-in user's nameXONG
P14Overview "Toàn cảnh" should cover other areas, not just events/mediaXONG
P15"Nâng bước tương lai" programme shows 0 students ("no details yet")XONG
P16Benefits table "Mô tả" shows the raw benefit_id codeXONG
P17"Nguồn gốc" (provenance) is "—" for all entries → fill source-of-truthXONG
P18Score cards (Học bạ) should expand/collapse per year/semesterXONG
P19Banner grade frozen at cohort-entry year (real bug — wrong loop order)XONG
P20Bulk score-card intake (no batch path today)XONG
P21Remove "(trọn đời)" from the Tổng phúc lợi statXONG
P22Account drawers (view / add) render blurredXONG
P23"Người dùng" / "Quản trị người dùng" → "Quản lý tài khoản"XONG
P24Login: allow the browser to remember the passwordXONG
P25Login heading: title-case + remove the sub-lineXONG
P26Overview tiles show raw event slugs instead of readable titlesXONG
P27Remove the "Cần bạn xử lý hôm nay" block on the overviewXONG
P28Status taxonomy: is "Đã hoàn tất" the same as "Tốt nghiệp"?XONG
P29Email account-setup notifications; let users set their own passwordXONG

Chi tiết

P1 — Wording: "Quân số" / "Phủ rộng 8 cánh" on the Phân tích page XONG

Page: /analytics (Phân tích) · screenshot 2026-06-13.

User comment (verbatim):

Not quân số:
Should be: Hoạt động 8 cánh

Read: "quân số" (troop-strength / headcount, a military register) is the

wrong word for a charity dashboard. The replacement label offered is

"Hoạt động 8 cánh" (8-petal activities).

Affected code (live hub):

Also uses "Quân số" (decide if the rename should be workspace-wide):

Decision (user, 2026-06-13): option (a).

  1. Rename the 8-petal card heading Phủ rộng 8 cánh (theo ghi chú)

Hoạt động 8 cánh (analytics.html:73).

  1. Headcount card: Quân số theo trườngSố học sinh theo trường

(user, 2026-06-13) (analytics.html:53), and the page sub-head

…quân số, phủ 8 cánh, phúc lợi.…số học sinh, hoạt động 8 cánh, phúc lợi.

(analytics.html:38).

  1. Care reports + report-builder: "Quân số" → học-sinh wording everywhere

(user confirmed 2026-06-13, "Quân số to học sinh"). Closes the earlier open

question — rename, don't keep.

Status: done — shipped + live (rev 00082). Quân số→Số học sinh everywhere; Phủ rộng 8 cánh→Hoạt động 8 cánh; (trọn đời) left in reports as a flagged consistency follow-on.

P2 — Charts should be interactive (hover/touch tooltips) XONG

Page: /analytics (Phân tích) · screenshot 2026-06-13 (Phúc lợi theo tháng).

User comment (verbatim):

chart should be interactive, showing numbers or details as mouse/touch moving in

Read: the charts are currently static (a flat SVG line / CSS bars), so the

two near-overlapping lines read as a single straight line with no values. On

hover (desktop) or touch-drag (mobile) the chart should surface the underlying

number/detail at that point — month, monthly amount, cumulative amount.

Affected code (all charts on the page are static today):

Scope note: today the hub ships no chart JS library — charts are hand-rolled

SVG/CSS. Interactivity means either (a) add lightweight tooltip JS over the

existing SVG/divs (vertices + <title>/JS hover layer; no dependency, CSP-safe),

or (b) adopt a small charting lib. (a) fits the "resist abstraction / no new dep"

discipline; decide at build time.

Status: done — live. SVG hover/touch tooltip (no charting dependency) maps the pointer to the nearest month and shows exact monthly + cumulative VND; bar charts get title tooltips; native <title> is the no-JS fallback.

P3 — Chart needs axis labels (years / amounts) XONG

Page: /analytics (Phân tích) · Phúc lợi theo tháng line chart.

User comment (verbatim):

chart mentions years/amounts etc

Read: the line chart has no axis ticks or labels at all — the SVG draws only

two polylines plus a baseline (analytics.html:99-104), so the reader can't tell *when* (which month/year along the x-axis) or

*how much* (VND amount along the y-axis) any point represents. Add axis

labelling: month/year ticks on the x-axis and amount ticks (VND, abbreviated)

on the y-axis. Pairs with P2 (hover detail) — P3 is the always-visible scale,

P2 is the on-demand exact value.

Affected code:

Status: done — live. y-axis VND gridlines (cumulative scale) + x-axis month labels.

P4 — Roster table: right-size columns (one line each) + scroll XONG

Page: student list (cohort/roster), grouped by school · screenshot 2026-06-13

(THCS Tam Thanh · 38 em).

User comment (verbatim):

Can we right size the column based on texts in each? ideally have one line if possible. Can we have scroll within this?

Read: cells wrap to two lines (THCS Tam Thanh, Cùng em tiến bước each

break across lines), making rows tall and the table loose. Want columns sized to

their content so each value sits on one line where it fits, and the table to

scroll horizontally within its own container instead of forcing wraps /

pushing page width.

Affected code:

Likely fix (decide at build): wrap each table in a div.table-scroll

(overflow-x:auto), set table.data { width:max-content; } or table-layout:auto,

and white-space:nowrap on the cells that wrap today (Trường, Chương trình,

Họ tên). Keep it responsive — on a narrow viewport the scroll container is what

prevents overflow. Pure CSS change; no view-code change expected.

Status: done — live. .table-scroll wrapper + .data--roster (content-sized, one-line, horizontal scroll).

P5 — Remove the "Google Doc / bản 2, bản 3" helper line on Reports XONG

Page: /reports (Báo cáo) · screenshot 2026-06-13.

User comment (verbatim):

remove this line

Target line (delete):

Báo cáo được tạo thành Google Doc trong thư mục Báo cáo trên Drive; tạo lại cùng ngày sẽ thành "bản 2", "bản 3"… không ghi đè.

Affected code:

Status: done — live. Helper line deleted.

P6 — Student report (Báo cáo cá nhân) fixes XONG

Artefact: per-student report, tools/report_child.py → the Google Doc /

markdown in Bao-cao/. Screenshot 2026-06-13 ([học sinh A], journey-0019).

Note: reports are cached files — after the fix, regenerate the affected

report(s) (memory care-report-fixes-2026-06-10). Same patterns likely recur in

report_school.py / report_overall.py / report_cohort.py — apply there too

for consistency (flagged per sub-item).

User comment (verbatim):

this is a student report:
1. change "active" to Vietnamese
2. Update student code through out.
3. Trạng thái is being duplicated.
4. GPA to Điểm trung bình

P6.1 — "active" → Vietnamese. The identity header prints the raw status

active; it should use the Vietnamese label (Đang hỗ trợ) like §1 already

does via rc.status_label(...).

P6.2 — Show student code throughout (not journey_id). The report displays

the internal journey_id: journey-0019; it should show the minted student code

(HS20230xxx). journey_id stays the internal key, but every *displayed*

identity becomes student_code (fall back to journey_id only if a code is

missing).

P6.3 — "Trạng thái" is duplicated. It appears twice: in the identity header

and in §1 Tổng quan tham gia.

complete). (Confirm at build which to keep.) — note this overlaps P6.1: if 224

is removed, the "active"→VN fix only needs to ensure 268 stays the source.

P6.4 — "GPA" → "Điểm trung bình". Rename the column header and the trend

section/heading. (English GPA in the LLM-prompt/narrative strings at lines 35,

461, 484 is internal, not displayed — leave those.)

Status: done — code live (cached reports refresh on next generation). Status→VN, Mã học sinh shown (not journey_id), duplicate Trạng thái dropped, GPA→Điểm trung bình.

P7 — Reports page: top line not left-aligned with content below XONG

Page: /reports (Báo cáo) · screenshot 2026-06-13.

User comment (verbatim):

Top line not aligned left with below.

Read: the tab strip (Danh sách em · Phân tích · Báo cáo · Cảnh báo) sits at

a different left edge from the page content (Tạo báo cáo tổng hợp and the cards

below it) — the first tab's text is indented by the tab's own padding while the

content starts at the container edge. They should share one left margin. (Tabs

are shared chrome, so this affects every care tab, not just Reports.)

Affected code:

Status: done — live. First care-tab left-aligned with the page content.

P8 — "Danh sách em" → "Danh sách học sinh" XONG

User comment (verbatim):

Danh sách học sinh (not em)

Affected code:

Open (decide at build): whether the body count text `Hiển thị 211 em

(209 đang hỗ trợ) (cohort.html:60) and the group counts · 38 em`

(cohort.html:88) also switch emhọc sinh. The explicit ask is the

label; flag the count text as a consistency follow-on.

Status: done — live. Danh sách học sinh (tab + h1 + title).

P9 — "Báo cáo cá nhân" → "Báo cáo học sinh" XONG

User comment (verbatim):

It should be called Báo cáo học sinh

Read: the per-student report is titled/labelled "Báo cáo cá nhân" (both in

its own heading and in the report index list on the Reports page). Rename to

"Báo cáo học sinh".

Affected code:

Status: done — code live. Báo cáo cá nhân→Báo cáo học sinh (report H1 + /reports index title).

P10 — Survey / selection page (Khảo sát & xét chọn) restructure XONG

Page: /survey · screenshot 2026-06-13. Template

survey.html; data

hub_reads.survey_candidates.

User comment (verbatim):

1. We would have two parts: Chờ xét duyệt (include all candidates to be selected) and Được chọn (those selected, i.e. would have or have student profiles/folders)
2. Update to student code. Show full name.
3. Remove: Danh sách ứng viên (xét chọn ở bước riêng) and 1 ứng viên đang chờ xét chọn. Không hiển thị thông tin nhận dạng.

P10.1 — Two sections. Replace the current "Chờ xét chọn" + "Đã xử lý gần

đây" with:

state=cho-xet-chon set).

student profile/folder (journey_id set). Replaces "Đã xử lý gần đây";

filter on journey_id present rather than the current "decided & after

go-live date" rule (hub_reads.py:416-420) — decide at build whether rejected-but-decided

candidates show anywhere.

P10.2 — Show student code + full name. Currently the list shows only

case_id (ung-vien-NNNN) and hides identity. Now: show the full name, and

the student code for selected candidates (those with a student profile).

identifying columns and the docstring promises "non-identifying columns only"

(hub_reads.py:368-373). This is a deliberate de-anonymisation of an

internal, login-gated page — surface real_name (the candidate table has it,

see hub_reads.py:494) and the student code for selected rows. Update the

docstring + the "non-identifying" contract so code and intent agree.

(resolve from the linked student profile / journey_idstudent_code),

not ung-vien-NNNN.

is the access control, not anonymisation). Keep it off any public surface.

P10.3 — Remove two text lines.

Status: done — live. Two sections (Chờ xét duyệt / Được chọn); Họ tên + Mã học sinh surfaced (internal, login-gated); sub-head + footer removed.

P11 — Remove mock candidate profiles (Drive folder) XONG

User comment (verbatim):

[Drive folder] https://drive.google.com/drive/u/1/folders/[Drive-folder-id]
Remove all profiles of candidates here. They are just mock up

Read: the candidate survey folder holds mock-up candidate profiles (e.g.

ung-vien-0026, the survey-subsystem test cases) that should be removed so the

selection page isn't seeded with fake data.

Scope / what "remove" touches (confirm before acting):

back the /survey list — otherwise the page still shows ung-vien-0026.

delete a real student by accident).

⚠ Destructive + outward (Drive) — not auto-run. Per workspace-ethics.md

§2 and the "look before deleting" rule: this needs an explicit go-ahead and a

look at the folder first (the survey test cases ung-vien-9006 / ung-vien-0026

were the proven end-to-end fixtures — confirm they're disposable mock-ups, not a

real submitted survey). Held for confirmation; see chat.

Status: done (2026-06-14). Only ung-vien-0026 was mock — its candidate row was deleted (130→129) and its Drive folder trashed (reversible 30 days). The 129 real back-loaded students were left untouched.

P12 — "Ảnh" photo counts read 0 for the year-end activity folders XONG

Page: /trips (Hoạt động) · screenshot 2026-06-13. The "Ảnh" column shows

18 for Tong ket nam hoc tam thanh and 0 for the other 5 year-end folders.

User comment (verbatim):

Deploy the built tool to reflect photo details based on HMT Workspace

Investigation (2026-06-13):

(commit 7965cbf, "workspace photo count 2a/2b"); the trips list reads it via

hub_reads._count_photosmedia_count.event_photo_count. Tam Thanh shows 18

precisely because the code is deployed and a .media-count sidecar exists for

that folder. So the gap is not a missing deploy.

Drive folder (ingest_watch.py:188, media_count.write_count). The other 5

year-end folders read 0 because no sidecar was generated for them — the

year-end corpus load copied their media into Drive 1_Hoat-dong/vao but the

ingest/count step never ran over them (no local bundle either: Sandbox/posts/

holds only the bana trial + _trial, zero .media-count files).

Actual fix (a data backfill, not a deploy): generate the workspace photo

count for the 5 year-end folders (Tân Khánh, Vĩnh Hào, Đại An, Lương Thế Vinh,

Nguyễn Bính) from their Drive 1_Hoat-dong/vao/<slug>/ photo sets — i.e. run

media_count.write_count over each, writing the .media-count sidecar where the

deployed hub reads it (confirm the live hub's posts_root — likely the bundles

bucket, not local). Redeploy only if a check shows the live rev predates

7965cbf (Tam Thanh's 18 suggests it does not).

Open: confirm where the deployed hub reads bundle sidecars from (gcsfuse

bundles bucket vs local), so the backfilled sidecars land somewhere live.

Status: done (2026-06-14). Investigation: only 2 of the 5 folders actually have Drive photos — wrote .media-count sidecars vinh-hao=239, tan-khanh=235. The other 3 (dai-an, nguyen-binh, luong-the-vinh) genuinely have 0 photos, so their 0 was already correct. No deploy needed (tool already live).

P13 — Top-right avatar shows "QT", should be the logged-in user's name XONG

User comment (verbatim):

This should be the user's name

Read: the avatar badge is hardcoded QT (Quản trị viên) with a fixed

title="Quản trị viên". It should reflect the logged-in user — their

initials in the badge, their name on hover.

Affected code (hardcoded in 4 layout bases):

care_auth.session_user(token) (hub.py:290), set at login (hub.py:342).

Surface the username (+ display name if we have one) into template context

(a context processor / per-response var), derive initials for the badge, set

the title to the full name. Fall back to QT/Quản trị viên only if no

session user.

Status: done — live. Avatar shows the logged-in user's initials + name (hub middleware sets it; rides the shared scope into all sub-apps).

P14 — Overview "Toàn cảnh" should cover other areas, not just events/media XONG

Page: / overview (Tổng quan) · screenshot 2026-06-13.

User comment (verbatim):

Consider this to cover other areas, not just events and media (can trim down)

Read: all five KPI tiles are media/event only (Bài chờ duyệt, Hoạt động

đang mở, Hoạt động chưa lên bài, Tổng bài đã đăng, Cánh hoa chưa phủ). The

overview should give a whole-Foundation glance — add Area 1 care (e.g. số

học sinh đang hỗ trợ, tín hiệu cảnh báo đang mở) and Areas 3&4 donor/finance

(e.g. đóng góp trong tháng, thư cảm ơn chờ gửi) — and trim the media tiles so

the row isn't five-media-then-others (merge/drop one or two).

Affected code:

Open (decide at build): the final tile set + which media tiles to drop, and

whether the trimmed media tiles move into their own section vs one mixed row.

Status: done — live (rev 00082). Overview now adds care (active students, open signals) + finance (month contributions, unsent thank-yous) tiles, media trimmed to three; care/finance tiles hidden by the existing module filter.

P15 — "Nâng bước tương lai" programme shows 0 students ("no details yet") XONG

Page: student list filtered by Chương trình = Nâng bước tương lai ·

Hiển thị 0 / 211 em · (chưa có em nào trong sổ đăng ký).

User comment (verbatim):

no details yet

Read: filtering to the Nâng bước tương lai (NBTL) programme returns zero

— that programme's students aren't in the care registry yet. This matches a

known held data load: the Hoa Sữa ~81 NBTL trainees were parked pending a

dedicated parser (split-name columns, Lào Cai / Sa Pa provenance, HVK<k> codes

8xxx) — memory care-data-migration-plan ("HELD: (1) Hoa Sữa ~81 NBTL

trainees"). So the 0 is expected, not a filter bug — but verify the

programme slug filter (nang-buoc-tuong-lai) actually matches before assuming

data-only, so we don't mask a slug mismatch.

Fix path: this is a data backfill, not a UI change — load the held Hoa

Sữa NBTL cohort (needs the dedicated parser) so the programme populates. Tracks

to the existing care-migration backlog, not the hub redesign.

Status: done — verified already-loaded (2026-06-14 session 2). The held cohort was loaded by the Track-D care session after this screenshot was taken: the live care DB now has 82 students under nang-buoc-tuong-lai (school hoa-sua, codes HVK1001+, cohort 2023; total children 311). The programme-filter value nang-buoc-tuong-lai matches exactly, so the hub returns all 82 — the 0 was a stale snapshot, not a slug bug. No load done (re-loading would duplicate live beneficiary data); verified read-only against the live bucket DB, scratch copy deleted.

P16 — Benefits table "Mô tả" shows the raw benefit_id code XONG

Page: student detail (/children/<id>) · Sổ phúc lợi table. The "Mô tả"

column shows chips like ben-2026-0008-001.

User comment (verbatim):

what is the code at the end? Should we remove?

Answer + read: that code is the internal benefit_id (the primary key of

the benefit row), not a description. The "Mô tả" header is mislabelled — it

renders <code>{{ b.benefit_id }}</code> with no actual description, and the

real context (Quỹ Hoa Mặt Trời (giải thưởng tổng kết …)) is already in the

adjacent Nguồn column. So the chip is internal noise.

Affected code:

Fix (decide at build): either (a) drop the Mô tả column entirely (the

benefit_id is internal and the Nguồn column carries the meaning), or (b) if the

benefit row has a real note/description field, show that instead of the id.

Lean (a) unless a description field exists.

Status: done — live. benefit_id chip dropped; column relabelled Biên nhận (keeps the receipt link).

P17 — "Nguồn gốc" (provenance) is "—" for all entries → fill source-of-truth XONG

Page: student detail · Nhật ký tiến độ table · the Nguồn gốc column is

on every row (and same story across students).

User comment (verbatim):

Update with sources (of truth) for all students

Read: the column already knows how to render provenance — it shows a `Nguồn

link / source_ref when present, else (child.html:73-76). Every —`

means that profile_entry has no source_ref populated. The fix is a

data backfill, not a template change: link each fact back to the source

document it came from, per care-data.md v2 §8 ("provenance is non-optional;

every new profile_entry / school_report / benefit carries source_ref").

Scope (large, care-side, ties to the migration backlog):

~211 students (most predate the provenance rule).

into Tai-lieu-goc/ and cataloging them as source_doc kind=student

(memory three-place-stocktake-2026-06-12 — "172 per-student original PDFs

unfiled"; only ~30 Đại An OCR-Docs filed so far). A fact can only point at a

source once the source is filed.

it there; the hub column is already correct and will populate as source_ref

fills in.

Update 2026-06-14 — done (live). The source-of-truth migration ran live (memory lnquang-sourcetruth-migration-2026-06-14):

Status: done — source-of-truth migration + link re-point + provenance backfill complete; hub render fix live (rev 00093-p4k). Remaining unsourced facts are legitimately blank.

P18 — Score cards (Học bạ) should expand/collapse per year/semester XONG

Page: student detail · "Học bạ & chuyên cần" section.

User comment (verbatim):

score cards should be expand/collapse for all years/semesters

Read: each year/semester học-bạ card renders its full subject grade table

inline, so a multi-year student is a long scroll. Make each card collapsible

(expand/collapse per year·semester), defaulting probably to collapsed (or only

the latest expanded), with the summary line (Năm học · kỳ · lớp · ĐTB) always

visible as the toggle header.

Affected code:

Status: done — live. Each year/kỳ học-bạ card is a collapsible <details>; the latest opens by default; summary carries năm · kỳ · lớp · ĐTB.

P19 — Banner grade frozen at cohort-entry year (real bug — wrong loop order) XONG

Page: student detail banner. Shows Lớp 7B (2023-2024) while the latest

học bạ is 9B (2025-2026). ([học sinh B] / [mã HS].)

User comment (verbatim):

Grade is not updated with the latest year and still the year of cohort entry

Root cause (confirmed — this is a genuine bug, not styling): the banner

*does* try to derive the current class from the latest transcript

(main.py:595-601), but the loop takes the first report with a

class_label and breaks, on the assumption (per its own comment,

main.py:591) that "reports are sorted latest-first". They are not

reports comes from order_transcript, which sorts oldest-first

(chronological, care-data.md rule 3; same order the cards render in). So the

loop grabs the oldest transcript's class (7B / 2023-2024) instead of the

newest. A prior session added this derivation (memory

query-live-bucket-not-snapshot) but with the ordering inverted, so it never

took effect.

Fix (one line): iterate newest-first — for r in reversed(reports): (or

pick the max by chrono key). Then current_class = 9B, current_class_year =

2025-2026. Also correct the wrong "sorted latest-first" comment.

Note: template-only output bug; no data change. Could be fixed immediately —

flagged for the user as the first concrete bug in this list.

Status: done — live (real bug fixed). Banner grade now iterates reversed(reports) → the latest học bạ (was grabbing the oldest); stale 'latest-first' comment corrected.

P20 — Bulk score-card intake (no batch path today) XONG

Context: logged at the user's request (2026-06-13) after confirming the

per-student + Nạp học bạ / sổ điểm (PDF) upload is fully wired

(main.py:981school_doc_extract → side-by-side review → school_report).

Gap: that path is one PDF, one student at a time. There is no in-hub

bulk score-card intake — drop many PDFs (or sweep a Drive folder) → classify

per student → extract → queue for review. Today bulk loading is done out-of-band

via care_migrate / care_reocr (developer tooling, not a coordinator UI).

Possible shape (decide at build): a multi-file upload / a watched Drive

"sổ điểm vào" folder that fans out to the existing school_doc_extract +

review queue, so a coordinator can process a whole school's term sheets without

20 single uploads. Reuse the existing extractor + review page; the new part is

the batch fan-out + a review queue.

Status: done — live (rev 00091-87p, 2026-06-14; commit 4b77da2). New tools/score_card_batch.py + routes under /score-cards: upload many PDFs → name-match each to a student (reusing care_reocr.match_scan_to_student, school-scoped + 1-char fuzzy; unmatched held, never guessed) → background extraction via the existing school_doc_extract engine → a per-item review queue → commit. "Committed" is derived from school_report.source_pdf (no manifest write-back). The single + bulk commit share one _persist_school_doc write path and one _school_doc_fields template partial so they can't drift. Entry point: a "+ Nạp học bạ hàng loạt" button on the student list. (Tests: tests/test_score_card_batch.py + P20 cases in tests/test_app.py.)

P21 — Remove "(trọn đời)" from the Tổng phúc lợi stat XONG

Page: /analytics (Phân tích) · KPI stat Tổng phúc lợi (trọn đời).

User comment (verbatim):

Remove trọn đời

Affected code:

Open (consistency): trọn đời also appears in the two report-builder

descriptions (reports.html:15, reports.html:27, "hỗ trợ trọn đời") and in

the report window captions (report_school/overall/cohort). The explicit ask is

the analytics stat; flag whether to strip it from those too. (Note: the figure

genuinely is lifetime — removing the word is a label-tidy, the meaning is

unchanged.)

Status: done — live. (trọn đời) removed from the Tổng phúc lợi stat.

P22 — Account drawers (view / add) render blurred XONG

Page: /quan-tri/nguoi-dung (Quản lý tài khoản) · screenshots 2026-06-13 (the

"Quản trị viên" view-account drawer and the "Thêm tài khoản" add drawer).

User comment (verbatim):

got blurred when creating new accounts or view an account

Root cause (confirmed — real CSS bug, not a blurry screenshot): the slide-in

drawer and the dimming overlay carry the same z-index: 100

(hmt.css:503, hmt.css:521), and the overlay <div id="user-overlay">

is rendered after the drawers in source order (users_admin.html:160, drawers at

:56-158). With equal z-index the later sibling paints on top, so the

overlay sits over the drawer, and its backdrop-filter: blur(2px)

(hmt.css:504) blurs the drawer content. (The .modal variant escapes this

because the modal is a *child* of .overlay (hmt.css:509) and so paints above the

blur; the drawer is a *sibling*.)

Fix (one line): raise the drawer above the overlay — .drawer { z-index: 101; }

(hmt.css:520-525), or move <div class="overlay"> before the drawers in the

template. Pure CSS; no view-code change. Affects both drawer kinds (per-user

manage + add-account) since they share the class.

Status: done — live (real CSS bug fixed). .drawer z-index 101 paints above the blurring overlay.

P23 — "Người dùng" / "Quản trị người dùng" → "Quản lý tài khoản" XONG

Page: /quan-tri/nguoi-dung · screenshot 2026-06-13 (page heading "Người dùng").

User comment (verbatim):

người dùng is no longer the term. Quản lý tài khoản.

Read: the nav label was already renamed to "Quản lý tài khoản" (memory

hub-cleanup-punchlist-2026-06-11), but the page itself still says "Người

dùng" / "Quản trị người dùng". Bring the page in line with the term.

Affected code:

Open (decide at build): the "Đã thêm người dùng" success banner

(users_admin.html:11) also says "người dùng"; switch to "tài khoản" for

consistency. The sub-head (users_admin.html:8) doesn't use the word — leave it.

Status: done — live. Quản lý tài khoản (page h1 + block titles).

P24 — Login: allow the browser to remember the password XONG

Page: /login (hub login) · screenshot 2026-06-13.

User comment (verbatim):

allow remember password

Read: the login form already sets autocomplete="username" /

autocomplete="current-password" (login.html:39, login.html:43) and the username is

pre-filling in the screenshot, so browser autofill is partly working. Two

readings of "remember password" — settle which the user means at build:

Chrome isn't offering to save, it is usually the 303 redirect heuristic, not

a missing attribute. Verify on the live login (submit once, watch for the

save prompt) before changing anything.

logged in. Today the session cookie always uses `max_age =

DEFAULT_SESSION_TTL_SECONDS` (hub.py:354); a remember-me option would set a

long max-age when ticked and a session cookie (no max-age) when not.

Status: done — live (rev 00091-87p, 2026-06-14; commit 8c1dab5). Added a "Ghi nhớ đăng nhập" checkbox on the login form. login_submit now lengthens both lifetimes in lockstep when ticked — the cookie max_age and the token's own signed exp (a long cookie holding a 12h token would silently log the user out) — to REMEMBER_ME_TTL_SECONDS (30 days). Unticked → a true session cookie (max_age=None, dropped on browser close) with the default working-day token TTL. (Test: test_remember_me_long_lived_cookie_and_token.)

P25 — Login heading: title-case + remove the sub-line XONG

Page: /login (hub login) · screenshot 2026-06-13.

User comment (verbatim):

Make it: Trung Tâm Điều Hành
remove: Khu vực nội bộ của Quỹ Hoa Mặt Trời.

Affected code:

Open (consistency): "Trung tâm điều hành" is the hub name and appears

sentence-cased in every page <title> / brand line (login <title> at

login.html:7; the base templates). The explicit ask is the login h1;

flag whether to title-case the name workspace-wide or only on the login card.

Status: done — live. Login h1 title-cased (Trung Tâm Điều Hành); sub-line removed.

P26 — Overview tiles show raw event slugs instead of readable titles XONG

Page: / overview (Tổng quan) · screenshot 2026-06-13. The "Hoạt động chưa

lên bài" and "Tổng bài đã đăng" tile previews list 2026-05-19__tong-ket-n…,

2026-06-04__tong-ket-n… etc.

User comment (verbatim):

slugs still shown

Read: the tile preview items render the raw event_slug string. The

overview tiles pass preview straight from the slug examples:

Fix (data, where the example lists are built): convert the slug to a

readable title before it reaches the tile — strip the <YYYY-MM-DD>__ date

prefix and de-kebab (tong-ket-nam-hoc-tam-thanh → `Tổng kết năm học Tam

Thanh`), or surface the event's stored title if the trip/presence row carries

one. Apply at hub_reads.py:551-554 so both tiles fix together; keep the slug as the

link target. (Same readable-title helper would serve other slug-facing

surfaces — check /trips / /presence lists for the same raw-slug display.)

Status: done — live. Overview tiles show the humanised event title, not the raw <date>__slug.

P27 — Remove the "Cần bạn xử lý hôm nay" block on the overview XONG

Page: / overview (Tổng quan) · screenshot 2026-06-13 ("Cần bạn xử lý hôm

nay" → "1 hoạt động đã có ảnh nhưng chưa lên bài / Cân nhắc Yêu cầu viết bài").

User comment (verbatim):

remove this

Affected code:

Note: overlaps P14 (overview should cover other areas, trim media). P27

removes the action launchpad; P14 reshapes the KPI tiles below it. Do them in

one overview pass.

Status: done — live. 'Cần bạn xử lý hôm nay' launchpad removed; sub-line simplified.

P28 — Status taxonomy: is "Đã hoàn tất" the same as "Tốt nghiệp"? XONG

Page: student list (Quản lý tài khoản → Danh sách học sinh) · Trạng thái

filter dropdown · screenshot 2026-06-13.

User comment (verbatim):

đã hoàn thành is same with tốt nghiệp?

Read: the status filter offers six lifecycle states (report_common.py:73-78):

The user is right that these overlap: "Đã hoàn tất" (graduated) and the two

"Tốt nghiệp · …" variants (graduated-exited / graduated-continuing) all mean

*finished the programme*. The roll-up bucket map (report_common.py:207-209)

already collapses all three graduated* into one "Đã hoàn tất" bucket —

confirming they are sub-cases of the same end-state, distinguished only by

whether support continued after graduation.

Decision needed (user): keep all three (graduation + a support-disposition

sub-state) or consolidate to one "Tốt nghiệp/Đã hoàn tất"? If kept, the labels

should make the relationship obvious (e.g. graduated → "Tốt nghiệp" as the

parent, the two variants as its dispositions) so they don't read as three

unrelated states. Changing the enum touches report_common.STATUS_LABELS, the

bucket map, the child-edit <option>s, and any stored status values.

Status: done (2026-06-14). User decision: remove 'Đã hoàn tất'. Dropped the bare graduated from CHILD_STATUSES + kept a render-fallback label; live migration collapsed the 23 HVK rows graduated→graduated-exited (0 graduated left; active=261). The aggregate roll-up bucket label is a flagged follow-on.

P29 — Email account-setup notifications; let users set their own password XONG

Page: /quan-tri/nguoi-dung (Quản lý tài khoản) · add-account drawer.

User comment (verbatim):

think about sending account setup notifications (and other information later)
to the account's email address. Allow them to set their own password

Read: today an admin creates an account by typing the new user's password

directly in the add drawer (users_admin.html:134-135create_user,

care_auth.py:227); nothing is emailed and the admin knows the password. The

request is to flip this to a proper invite flow:

  1. Email a setup notification to the account's email when it's created

(and reuse the same channel for "other information later" — a general

transactional-email lane for the hub).

  1. Let the user set their own password via a tokenised setup link, instead

of the admin choosing it. The admin's add-account form drops the password

field (or makes it optional); the account starts password-unset until the

user completes setup.

What this touches (design item — several pieces, none built today):

(care_auth.py:190-199); username happens to be email-shaped for the seeds

(admin@hoamattroi.org) but that's convention, not a field. Decide: treat

username as the email, or add an explicit email column.

(care_auth.make_session builds signed <exp>.<user>.<sig> tokens,

care_auth.py:92-108) for a scoped, expiring "set your password" link; add a

GET/POST /dat-mat-khau/<token> route that verifies it and writes the hash.

sender** yet; the donor thank-you email is explicitly inert "until the SMTP

send path exists" (autonomy-phase.md, document-chrome.md). This account

lane needs that same path. Build it once, share it (account setup, donor

thank-you, later notifications).

transactional admin mail**, not a published post and not the donor receipt —

so it sits outside both the Phase-A publication gate and the scoped

receipts-only auto-send exception (autonomy-phase.md §2a). Decide whether

account-setup mail auto-sends on creation or is admin-triggered; it does not

touch the publication gate either way.

Status: done — live (rev 00091-87p, 2026-06-14; commit 682251b). The shared SMTP path now exists (tools/mailer.py, the one outbound-email lane, **inert until HMT_SMTP_* is configured — the donor thank-you can ride it later). care_auth gained an email column (idempotent migration), password-unset invite-pending accounts (cannot log in), and purpose-scoped set-password tokens (never interchangeable with a session token). Admin add-account now offers an invite: leave the password blank → the user sets their own via /dat-mat-khau/<token> (the one token-authed public route), emailed when SMTP is live and always shown to the admin to copy when it is not; a per-user "gửi lại lời mời" re-issues the link without wiping a password. Send posture: internal transactional admin mail, outside the Phase-A publication gate and the receipts-only auto-send exception. Remaining to fully activate email:** set HMT_SMTP_HOST/_FROM (+ _PORT/_USERNAME/_PASSWORD/_TLS) on the hub service. (Tests: tests/test_mailer.py, P29 cases in tests/test_care_auth.py + tests/test_hub.py.)

Nguồn: _system/notes/hub-punchlist-2026-06-13.md · sinh từ build_punchlist_page.py · đã ẩn PII trước khi xuất bản.