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).
| Mã | Việc | Trạng thái |
|---|---|---|
| P1 | Wording: "Quân số" / "Phủ rộng 8 cánh" on the Phân tích page | XONG |
| P2 | Charts should be interactive (hover/touch tooltips) | XONG |
| P3 | Chart needs axis labels (years / amounts) | XONG |
| P4 | Roster table: right-size columns (one line each) + scroll | XONG |
| P5 | Remove the "Google Doc / bản 2, bản 3" helper line on Reports | XONG |
| P6 | Student report (Báo cáo cá nhân) fixes | XONG |
| P7 | Reports page: top line not left-aligned with content below | XONG |
| 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 |
| P10 | Survey / selection page (Khảo sát & xét chọn) restructure | XONG |
| P11 | Remove mock candidate profiles (Drive folder) | XONG |
| P12 | "Ảnh" photo counts read 0 for the year-end activity folders | XONG |
| P13 | Top-right avatar shows "QT", should be the logged-in user's name | XONG |
| P14 | Overview "Toàn cảnh" should cover other areas, not just events/media | XONG |
| P15 | "Nâng bước tương lai" programme shows 0 students ("no details yet") | XONG |
| P16 | Benefits table "Mô tả" shows the raw benefit_id code | XONG |
| P17 | "Nguồn gốc" (provenance) is "—" for all entries → fill source-of-truth | XONG |
| P18 | Score cards (Học bạ) should expand/collapse per year/semester | XONG |
| P19 | Banner grade frozen at cohort-entry year (real bug — wrong loop order) | XONG |
| P20 | Bulk score-card intake (no batch path today) | XONG |
| P21 | Remove "(trọn đời)" from the Tổng phúc lợi stat | XONG |
| P22 | Account drawers (view / add) render blurred | XONG |
| P23 | "Người dùng" / "Quản trị người dùng" → "Quản lý tài khoản" | XONG |
| P24 | Login: allow the browser to remember the password | XONG |
| P25 | Login heading: title-case + remove the sub-line | XONG |
| P26 | Overview tiles show raw event slugs instead of readable titles | XONG |
| P27 | Remove the "Cần bạn xử lý hôm nay" block on the overview | XONG |
| P28 | Status taxonomy: is "Đã hoàn tất" the same as "Tốt nghiệp"? | XONG |
| P29 | Email account-setup notifications; let users set their own password | 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):
analytics.html:53 — <h2>Quân số theo trường</h2>analytics.html:38 — sub-head …quân số, phủ 8 cánh, phúc lợi.analytics.html:73 — <h2>Phủ rộng 8 cánh (theo ghi chú)</h2>Also uses "Quân số" (decide if the rename should be workspace-wide):
report_school.py:166, report_overall.py:153, report_cohort.py:230 — ## Quân số section headers in the generated care reports.Decision (user, 2026-06-13): option (a).
Phủ rộng 8 cánh (theo ghi chú) →Hoạt động 8 cánh (analytics.html:73).
Quân số theo trường → Số 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).
(user confirmed 2026-06-13, "Quân số to học sinh"). Closes the earlier open
question — rename, don't keep.
report_school.py:166, report_overall.py:153, report_cohort.py:230 — ## Quân số section headers (and the "quân số & hỗ trợ … trọn đời" window captions at report_school.py:164 / report_overall.py:151 / report_cohort.py:228).reports.html:15 — Quân số theo năm, hỗ trợ trọn đời… builder description.reports.html:27 — Quân số theo năm & trường… builder description.Số học sinh theo năm… — settle at build.)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.
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):
analytics.html:97-104 — Phúc lợi theo tháng: static <svg> with two <polyline>s, no points/tooltips. Exact numbers only in the table below (:110-116).analytics.html:55-64 — Quân số theo trường: CSS bar-fill divs, value shown as static text only.analytics.html:74-91 — Phủ rộng 8 cánh: same static CSS bars.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.
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:
analytics.html:97-104 — the <svg> viewBox has margins but no tick/label elements; month_line / cum_line are bare point strings built server-side./analytics view builds months_rows, month_line, cum_line. (Locate in app/ view code when worked.)Status: done — live. y-axis VND gridlines (cumulative scale) + x-axis month labels.
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:
cohort.html:62-83 — roster_table macro, <table class="data">. Same macro renders both the grouped (<details>) and flat views.table.data lives in app/static/hmt.css (the hub stylesheet) — locate and adjust there; the markup likely needs only a wrapping scroll <div>.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).
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:
reports.html:72 — the <p class="muted">…</p> helper line. Delete the whole <p>.Status: done — live. Helper line deleted.
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(...).
report_child.py:224 — f"- Trạng thái: {child['status']}" → wrap in rc.status_label(child['status']).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).
report_child.py:220 — - journey_id: \journey-0019\` → - Mã học sinh: \<student_code>\`.report_child.py:216 — anonymised-name fallback uses journey_id; switch to student_code so the sponsor view shows the code, not journey-NNNN._fetch_child query (report_child.py:122-128) exposes student_code; the children-list page already shows c.student_code or c.journey_id, mirror that.P6.3 — "Trạng thái" is duplicated. It appears twice: in the identity header
and in §1 Tổng quan tham gia.
report_child.py:224 — header bullet Trạng thái.report_child.py:268 — §1 bullet Trạng thái (richer: adds exit reason).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.)
report_child.py:405 — học bạ table header "GPA" → "Điểm trung bình".report_child.py:411 — section title ### Diễn biến GPA (cũ → mới) → Điểm trung bình.report_child.py:412 — trend table header ["Mốc", "GPA"] → "Điểm trung bình".GPA too (e.g. assessment tables) — apply the same rename for consistency.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.
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:
base.html:125-130 — nav.tabbar.care-tabs and its .tab links; sibling of {% block content %} at base.html:132..tabbar / .tab in app/static/hmt.css — likely the first .tab needs its left padding pulled (negative margin or :first-child padding-left:0) so its text aligns with the content edge. Render-and-eyeball.Status: done — live. First care-tab left-aligned with the page content.
User comment (verbatim):
Danh sách học sinh (not em)
Affected code:
base.html:126 — nav tab label Danh sách em.cohort.html:16 — page <h1>Danh sách em</h1>.cohort.html:3 — {% block title %}Danh sách em · HMT.Open (decide at build): whether the body count text `Hiển thị 211 em
(209 đang hỗ trợ) (· 38 em`cohort.html:60) and the group counts
(cohort.html:88) also switch em → họ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).
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:
report_index.py:162 — f"Báo cáo cá nhân — {label}{d}", the title shown in the /reports index list.report_child.py:218 — f"# Báo cáo cá nhân · {name}", the report's own H1 (regenerate cached reports after, as in P6).Báo cáo cá nhân once more at build for any other occurrence (selftest strings, etc.).Status: done — code live. Báo cáo cá nhân→Báo cáo học sinh (report H1 + /reports index title).
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:
Chờ xét duyệt — all candidates still to be selected (the awaiting /state=cho-xet-chon set).
Được chọn — candidates who were selected, i.e. who have (or will have) astudent 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.
survey.html:24 (Chờ xét chọn → Chờ xét duyệt) and survey.html:46-47 (Đã xử lý gần đây → Được chọn).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).
survey_candidates._view (hub_reads.py:399-409) deliberately dropsidentifying 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.
Họ tên column; for Được chọn show the student code(resolve from the linked student profile / journey_id → student_code),
not ung-vien-NNNN.
is the access control, not anonymisation). Keep it off any public surface.
P10.3 — Remove two text lines.
survey.html:11-13 — sub-head {{ n_awaiting }} ứng viên đang chờ xét chọn. Không hiển thị thông tin nhận dạng. → delete (the "không hiển thị nhận dạng" claim is now false anyway, per P10.2).survey.html:65-67 — footer Danh sách ứng viên (xét chọn ở bước riêng). → delete.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.
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):
[Drive-folder-id]).so-khao-sat.sqlite (candidate table) thatback the /survey list — otherwise the page still shows ung-vien-0026.
journey_id / student profile a mock candidate was promoted into (don'tdelete 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.
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):
media_count.py(commit 7965cbf, "workspace photo count 2a/2b"); the trips list reads it via
hub_reads._count_photos → media_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.
.media-count sidecar written at ingest from theDrive 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).
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):
base.html:114, hub/base.html:85, finance/base.html:102, review/base.html:75 — <span class="avatar" title="Quản trị viên">QT</span>.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).
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:
hub.py:511-548 — the live tiles list (all media/event); and the MOCK_OVERVIEW fallback at hub.py:103.hub_reads.overview_counts — the counts source; would need care-DB + finance-DB counts added (it currently takes registry_db + posts_root + dashboard rows only).hub.py:552-561) already hides a tile whose href module the user can't reach — give each new tile a care/finance href so a content-only user won't see care/finance tiles. No new gating needed.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.
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.
benefit_id code XONGPage: 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:
child.html:52 — <td><code>{{ b.benefit_id }}</code></td> under the Mô tả header (child.html:40).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).
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):
source_ref → source_doc → original for existing facts across all~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):
2_Hoc-sinh/Truong/<school>/<năm-học>/): 14 funding BBs + 24 year-end docs + 9 KQ sheets.source_doc + 167 school_report rows).profile_entry.source_ref 151 → 209 (SE entries → their SE source_doc), then the extended pass added 123 precise links (102 academic "Điểm thi" → KQ doc, 21 "Lễ tổng kết" → year-end báo cáo) → 332/666 facts sourced. Remaining unsourced rows are legitimately blank (schools using rosters not a catalogued KQ doc, enrollment/admin notes, essays).report_common.drive_view_url(), child view + child.html link relabelled "Tài liệu gốc ↗"; committed (70c5298 on main) and deployed live — management-hub rev 00093-p4k. The column now populates instead of showing "—" everywhere. Live counts: child 311, source_doc 324, benefit 512.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.
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:
child.html:90-122 — the {% for r in reports %} loop; each <section class="hocba"> becomes a <details> (header = child.html:92-104 summary, body = the grade table child.html:105-121). Pure template/CSS; reports already arrive ordered (latest first).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.
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.
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:981 → school_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.)
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:
analytics.html:49 — <div class="stat__l">Tổng phúc lợi (trọn đời)</div> → Tổng phúc lợi.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.
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.
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:
users_admin.html:7 — <h1>Người dùng</h1> → Quản lý tài khoản.users_admin.html:5 — {% block page_title %}Quản trị người dùng → Quản lý tài khoản.users_admin.html:4 — {% block title %}Người dùng · … → Quản lý tài khoản · ….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).
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.)
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:
login.html:30 — <h1>Trung tâm điều hành</h1> → <h1>Trung Tâm Điều Hành</h1> (title-case each word).login.html:31 — <p class="sub">Khu vực nội bộ của Quỹ Hoa Mặt Trời.</p> → delete the whole <p>.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.
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:
hub.py:529 — "preview": counts.get("orphan_examples").hub.py:537 — "preview": counts.get("recent_examples").hub_reads.py:551-552 — orphan_examples = [t["event_slug"] …].hub_reads.py:554 — recent_examples = [r["event_slug"] …].overview.html:74-79 — the template prints each preview p verbatim.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.
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:
overview.html:35-62 — the whole "Action launchpad" section (<h2>Cần bạn xử lý hôm nay</h2> + the <div class="card"> with the action list / empty state). Delete the block.overview.html:13-16 — the page-head sub-line reads "<b>N</b> việc đang chờ xử lý" off the same actions; once the block is gone, simplify the sub-line (drop the actions count, keep the calm "Mọi việc đang trong tầm kiểm soát." text).actions / today keys (hub.py:549-550) become unused by the template; leave the data builder or trim it (decide at build — MOCK_OVERVIEW at hub.py:103 also carries actions).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.
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):
active → Đang hỗ trợpaused → Tạm dừnggraduated → Đã hoàn tấtgraduated-exited → Tốt nghiệp · kết thúc hỗ trợgraduated-continuing → Tốt nghiệp · tiếp tục hỗ trợwithdrawn → Đã rời chương trìnhThe 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.
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-135 → create_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:
(and reuse the same channel for "other information later" — a general
transactional-email lane for the hub).
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):
user table has no email column(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.