District 67 Dashboard — 建置規格 · Build Spec
雙語文件:第一部分為繁體中文,第二部分為 English;兩版內容一致。 Bilingual document: Part 1 is Traditional Chinese, Part 2 is English — both parts are equivalent.
第一部分 · 繁體中文
我們在做什麼: 一個獨立、唯讀的網頁應用,為**教育卓越總監(PQD)**與地區領導者呈現 國際演講會第 67 地區(District 67)的營運表現,並補上官方儀表板所缺少的俱樂部中文名稱。
取代/整合: project-highlight/ai001.md(功能構想)與 project-highlight/ai002.md(中文名稱補充)。本檔為權威規格; 凡與 ai001/ai002 衝突之處,以本檔為準(尤其是比對鍵 — 見 §3.4)。
範圍: 僅限第 67 地區(id=67)。無地區切換器。唯讀鏡像,絕不回寫 TI。
設計理念 — 以完整性為先的預覽版。 我們尚未確定 PQD 想用哪些資料來決策,因此本應用的任務是 讓第 67 地區的所有資訊都可取得、可探索,而非預先替使用者決定什麼重要。原則:
- 忠實鏡像、毫不隱藏 — 官方報表對
id=67顯示的每一項指標與層級,全部重現。 - 可探索、不強加觀點 — 提供排序/篩選/搜尋/切換;帶觀點的視圖(如 watchlist)只是選用輔助, 而非應用的主張。
- 只補上缺少的 — 中文名稱、雙語搜尋、資料截止日可視化、行動裝置存取。
- 定義要透明 — 把 183 已授證/178 運作中/174 已繳費並列呈現;標示「繳費筆數 ≠ 不重複人數」。 我們讓區別清楚可讀,由 PQD 自行詮釋。
- 漸進式揭露 — 總覽 → 下鑽 → 完整表格 → 匯出,讓深度可得但不致一眼資訊過載。這是我們呈給 PQD、 用以蒐集後續指示的 v1。
第 67 地區基準(2026-06-15 驗證,年度 2025-2026,資料截至 2026-06-12)
| 指標 | 數值 |
|---|---|
| 區(Divisions) | 9(A–I) |
| 分會區(Areas) | 38 |
| 名冊上的俱樂部 | 183(178 運作中 · 174 已繳費 · 173 基準) |
| 暫停的俱樂部 | 5(FHK, NTUST, Critical Thinkers, MIRDC, Scien Tech) |
| 會員實際人數(目前 To Date) | 2,660(基準 2,838 · 淨 −178)— 真正的會員數 |
| 會籍繳費筆數(本年度累計) | 5,472(基準 5,377)— 繳費交易數,約為會員數 2 倍;非人數 |
各區(分會區數/俱樂部數/會員目前人數/淨): A 4/20/225/−11 · B 4/20/280/−21 · C 4/21/365/+10 · D 4/23/324/−3 · E 4/17/275/−8 · F 5/23/291/−81 · G 5/23/367/+9 · H 4/18/277/−17 · I 4/18/256/−56。
會員數=實際人數,非繳費筆數。 District Performance 報表只揭露會籍繳費數(5,472)— 每位會員每年可繳費兩次(十月與四月續會),故繳費 ≈ 2× 會員數,絕不可標為會員數。真正的人數來自 Club Performance 報表(
Club.aspx?id=67):每間俱樂部的 基準/目前/淨成長(例如 Taipei 31 → 26,淨 −5)。 地區總計 目前 To Date = 2,660。本應用以 目前(To Date)作為「會員數」;繳費筆數另行呈現並明確標為交易數。
俱樂部計數詞彙(雙語 — 必須顯示於應用內)
這些區別是「定義要透明」的核心。請在應用內(如資訊面板/各計數的提示)以中英雙語顯示:
| 名稱 | 意義 |
|---|---|
| 名冊上的俱樂部(Clubs on Roster) | 名冊上的俱樂部總數(含暫停) |
| 已繳費俱樂部(Paid Clubs) | 已完成本期會籍繳費的俱樂部 |
| 良好標準俱樂部(Good Standing Clubs) | 符合 TI 規定、會員數達標且完成繳費的俱樂部 |
| 運作中俱樂部(Active Clubs) | 仍在運作中的俱樂部 |
| 暫停的俱樂部(Suspended Clubs) | 暫停或未達要求的俱樂部 |
階段劃分(範圍紀律)
第一階段 — 立即建置並呈現(在 PQD 回饋前唯一要做的): 俱樂部計數+會員數 — 整份摘要的基礎。 一個總覽畫面:D67 一覽 (區 9/分會區 38/名冊俱樂部 183/運作中 178/已繳費 174/暫停 5),加上各區的 分會區/俱樂部/**會員(目前實際人數)表。會員數=目前 To Date = 2,660(取自 Club Performance 的真實人數), 含基準+淨成長;會籍繳費(5,472)**另行呈現並標為交易數(≈2×)。中英雙語並顯示俱樂部計數詞彙。 每間俱樂部連結至其 TI Club Report(
ClubReport.aspx?id=<補零>)。第二階段以後 — 在此規劃備妥,但僅在需求者(PQD)回應後才建置/發布: 完整雙語俱樂部表、 區/分會區與 Club-Report 區段各自深連到官方 TI 報表(以區段呈現,非分頁)、中文名稱補充 (
clubId比對,§3.4)、DCP 目標與傑出俱樂部狀態、趨勢、輔助/watchlist 視圖、匯出。本規格其餘部分定義這些, 以便一經要求即可快速推進。
1. 主要資料來源(2026-06-15 驗證)
基底: https://dashboards.toastmasters.org/2025-2026/district.aspx?id=67&hideclub=1
此為 District Performance 報表,列出 D67 每一間俱樂部,依 區 → 分會區 → 俱樂部 分組。
亦提供 Export → CSV 控制項與 資料截止日/年度/月份 選擇器(約近一週可取每日快照;年度可回溯至 2008-2009)。
1.1 地區 KPI 摘要(頁面頂端)
三個標題區塊,各含 基準(Base)、目前(To Date),與四個表彰目標門檻:
- 已繳費俱樂部 — 基準 173/目前 174 · 目標:Distinguished 175/Select 179/President's 182/Smedley 187
- 繳費筆數 — 基準 5,377/目前 5,472 · 目標:5,431/5,539/5,646/5,808
- 傑出俱樂部 — 基準 173/目前 65 · 目標:78/87/96/104
- 會籍繳費細項: Late、Oct、Apr、Total(續會)、New、Charter、Total、運作中俱樂部(178)
1.2 各俱樂部資料列(全俱樂部表)
每列提供:
| 欄位 | 範例 | 說明 |
|---|---|---|
| 俱樂部編號 | 00001890、28679758 |
8 位、前置補零。TI 俱樂部編號=主鍵。 |
| 俱樂部名稱(英文) | Taipei Club、DALI Bilingual Toastmasters Club |
|
| 狀態標記 | Susp 03/31/26、Charter 02/02/26 |
選用;僅暫停/新授證俱樂部才有 |
| Late/Oct/Apr | 0 / 24 / 20 |
各時段續會繳費 |
| Total(續會) | 44 |
Late+Oct+Apr |
| New | 8 |
新會員繳費 |
| Charter | 0 |
授證繳費 |
| Total(繳費) | 52 |
續會+新會+授證(本年度累計) |
區與分會區的小計列另帶 俱樂部數 欄。
1.3 Club Performance 報表 — Club.aspx?id=67(已納入)
真實會員人數 在此,而非 District Performance。每間俱樂部:Mem. 基準/目前/淨、達成目標數(滿 10),
以及 10 項 DCP 個別目標。第一階段納入 基準/目前/淨+達成目標數
(data/raw/club-performance-67.json,以 clubId 與俱樂部比對);完整 10 目標格與傑出狀態屬後續階段。
目前人數亦可於 Division.aspx?id=67(各俱樂部「Mem. To Date」)核對。
2. 補充來源 — 中文名稱(2026-06-15 驗證)
- 清單:
https://toastmasters.org.tw/new/page.php?ver=en&page_type=club_list各列:AreaCode(如F3)· 合併的中文名 English Name· 語言(英語/雙語/國語/台語/日語/客語)· 城市。 - 明細:
https://toastmasters.org.tw/new/page.php?page_type=club&id=<CMS_ID>&ver=en另含:區/分會區、例會形式/時間、場地、會費、聯絡人、授證日、創會人/輔導人/創會會長。
3. 資料管線
3.1 擷取基底(TI)— 每週快照
- 頻率:每週。 每次執行擷取
id=67的 TI 原始資料,附加俱樂部中文名稱(§3.4),並寫出一份不可變的 快照,標記dataAsOf+capturedAt。v1 以每週單一已提交的 JSON 檔保存(簡單、可 diff、無資料庫); 待需要趨勢時再升級為資料庫。 - 首選來源: 報表的 CSV 匯出(較 HTML 解析乾淨)。
- 後備: 解析 HTML 表格(結構見 §1.2)。
3.1a UI 模式 — 關鍵資訊卡 → 明細
儀表板以關鍵資訊卡呈現;每張卡連到該報表的明細區段(以區段呈現、非分頁 — §5),明細再深連至官方 TI 頁面。 總覽 → 卡片 → 明細 → 官方來源。
3.2 擷取補充資料(台灣)
- ⚠️ 台灣官網對純伺服器請求回傳 HTTP 403(已驗證)。需用無頭/瀏覽器風格請求並帶真實標頭,
不可用裸
fetch()。 - 解析清單列 →
{ cmsId, areaCode, chineseName, englishName, language, city, detailUrl }。 (cmsId取自各列明細頁連結的id。) - (選用,每週) 爬明細頁取例會時間/場地/授證日/聯絡人。
3.3 名稱解析
- 拆分合併字串:開頭連續 CJK 字元=
chineseName;其餘=englishName。修剪空白。 - 純英文俱樂部(名稱重複,如
Global Family Global Family、FHK Toastmasters Club …)→ 無中文名,僅顯示英文。
3.4 基底 × 補充比對 — 以 clubId(長整數)為主,名稱+分會區為後備
更正(取代先前 ai002 的說法)。 台灣官網的
id就是 TI 俱樂部編號,只是未補零 — 故雙方轉為整數後即可比對。 已用清單實際連結id對 12 間俱樂部驗證:376=新竹(TI00000376)、1890=台北(00001890)、1904=高雄、2304=YMIC … 全部相符。DALI 為唯一例外 — 全新授證,儀表板顯示 TI 新會編號(28679758),本地則為其 永久編號(1460763)。故 id 比對為通則,新授證為後備案例。
標準鍵: clubId = parseInt(tiClubNumber, 10)(見 §4 — 同時保存補零顯示形式)。
比對演算法:
- 主要:
dashboard.clubId === taiwan.id→ 整數精確比對。涵蓋絕大多數。 - 後備(新授證/未連結俱樂部): 正規化英文名(轉小寫;去標點;正規化
Toastmasters/Club/Int'l/&; 去除尾端括號),並在同一 區+分會區 內比對(儀表板Division X / Area NN↔ 清單AreaCode如F3)。 - 未比中 → 中文名留空並列入對帳報告。(TW 連結 160 間 vs TI 名冊 183 間 → 初期約 23 間落在此。)
overrides.json(clubId → chineseName)處理人工殘餘 — 永久保存,可在重新匯入後存活。
TI 俱樂部編號仍為應用主鍵;chineseName 為經比對解析而附加的屬性。
4. 資料模型
ClubSnapshot {
asOf: date
district: {
paidClubs:{base,toDate,goals{distinguished,select,presidents,smedley}}
payments:{base,toDate,goals{...}, breakdown{late,oct,apr,renewTotal,new,charter,total}}
distinguishedClubs:{base,toDate,goals{...}}
activeClubs: number
}
divisions: [{ code, clubCount, payments{...},
areas: [{ code, clubCount, payments{...},
clubs: [Club] }] }]
}
Club {
clubNumber // TI, PRIMARY KEY (e.g. "28679758")
englishName
division, area // e.g. "F","03"
status // active | suspended | chartering ; statusDate
payments {late, oct, apr, renewTotal, new, charter, total}
membership {base, toDate, net, goalsMet} // 真實人數,取自 Club Performance
// --- 補充(以 clubId 比對)---
chineseName | null
language | null // 英語/雙語/...
city | null
detailUrl | null
meeting | null // {format,time,venue,fee,contact,charterDate} (第二階段)
match {method: clubId | exact | fuzzy | override | unmatched, confidence}
// --- 第二階段:DCP ---
dcp | null // {goalsMet, goals[10], recognition}
}
5. 功能(v1)
- F1 — 地區健康總覽。 已繳費俱樂部/繳費筆數/傑出俱樂部的 KPI 卡,各顯示 基準 → 目前 與距下一表彰層級 (Distinguished/Select/President's/Smedley)的差距,並有年度進度指示(「剩餘天數、達標/落後」)。完全對應 §1.1。
- F2 — 區與分會區下鑽。 可摺疊的 區 → 分會區 → 俱樂部 樹,含繳費小計與達標上色。
- F3 — 全俱樂部表。 可排序/篩選:俱樂部編號、雙語名稱、區/分會區、會員/繳費、狀態。可依區、分會區、狀態、語言、城市篩選。
- F4 — 輔助視圖(選用)。 非應用主張(見理念)— 中性、衍生的便利功能,PQD 可忽略:例如「需要關注」篩選,
列出暫停(
Susp …)、低繳費俱樂部與新授證(Charter …),各顯示區/分會區供權責總監查看。建於同一份完整資料之上, 絕不取代之。 - F5 — 全面雙語。 每間俱樂部顯示
中文名 (English Name);搜尋可比對任一語言;中文/EN 切換主導語言。無中文名時僅顯示英文。
第二階段
- 每間俱樂部的 DCP 目標格+傑出狀態(納入 Club Performance 報表)。
- 以日期化快照的趨勢圖(繳費曲線、傑出俱樂部累積 vs 進度)。
- 「假設情境」傑出地區規劃器;供理事會與名人堂使用的 CSV/PDF 匯出。
- 明細頁補充資料(例會時間/場地/聯絡人)作為篩選/欄位。
6. 頁面/路由
| 路由 | 內容 | 狀態 |
|---|---|---|
/ |
F1 總覽+各區摘要表+俱樂部計數卡 | 已建置 |
/divisions |
F2 區 → 分會區 → 俱樂部 下鑽樹(每間俱樂部含 ClubReport 連結+狀態) | 已建置 |
/spec |
本規格(雙語) | 已建置 |
/clubs |
F3 全俱樂部可排序/篩選表(雙語) | 第二階段 |
/clubs/[clubNumber] |
單一俱樂部明細(繳費、狀態、中文名、例會資訊) | 第二階段 |
/watchlist |
F4 完整關注清單 | 第二階段 |
/admin/reconcile |
未比中俱樂部視圖+override 編輯器(§3.4) | 第二階段 |
7. 技術堆疊
- Next.js(App Router);目前以 Azure Static Web Apps 靜態匯出部署(
output: "export"),自訂網域dashboard.toastmasters.org.tw,CNAME + Azure 自動簽發 HTTPS。Vercel 仍為可選替代。 - 擷取: 排程(Cron)→ TI CSV 抓取+每週台灣補充爬取(以瀏覽器風格請求繞過 403)。
- 儲存: v1 為每週已提交的快照 JSON;待趨勢需求時升級為資料庫(Postgres/Blob)+快取最新合併資料集。
- UI: Tailwind;伺服器元件+快取資料。
- i18n: zh-Hant/en;雙語俱樂部呈現內建於資料層。
- 行動優先(分會區/區總監多以手機查看)。
8. 非功能需求
- 首屏快速(資料於伺服器端預取/快取;無前端爬取)。
- 補充資料具韌性:台灣擷取失敗時,沿用上次良好的中文名快照。
- 唯讀;除公開台灣官網已顯示者(明細頁俱樂部聯絡人)外,不含 PII。
9. 範圍外
- 其他地區/區域彙總/跨地區比較。
- 編輯或回寫 TI 資料。
- 會員個人層級紀錄。
10. 風險與待決問題
- 中文名稱比對 — 已改為以 ID 為準(已解決)(見 §3.4)。早期草稿稱比對為「以名稱為準」;現已改為
clubId整數比對(TI 俱樂部編號=台灣官網id,去除前置零後相同),並已驗證。名稱+分會區的模糊比對 現在僅作為新成立俱樂部的後備方案(例如 DALI:在 TI 顯示臨時新會編號、本地則為永久編號);其餘以overrides.json手動補齊。183 間俱樂部的名冊對帳會列出任何仍未比中的俱樂部。 - TI CSV 匯出端點格式 — 仍需確認
id=67的匯出網址/參數;HTML 解析為後備方案。(待確認) - 台灣官網 403 反爬蟲 — 仍需以瀏覽器風格請求(含真實標頭)擷取並積極快取。
- Q(教育卓越總監 PQD): 是否有官方的 D67 名冊可對應 TI 俱樂部編號 ↔ 中文名稱?若有,即可讓比對完全精確、淘汰模糊比對器。
- Q(教育卓越總監 PQD): v1 優先順序 — 中文名稱+俱樂部/會員計數+實際人數是否足以先行上線,DCP/傑出俱樂部留待第二階段?
11. 里程碑
- M1 — 擷取+模型: TI 基底(CSV/HTML)→ 快照;驗證所有俱樂部可解析。
- M2 — 核心 UI: F1 總覽、F3 全俱樂部表、F2 下鑽。
- M3 — 補充: 台灣爬取 → 名稱+分會區比對 → 雙語呈現+
/admin/reconcile。 - M4 — F4 watchlist +行動裝置打磨 → 發布 v1。
- 第二階段: DCP 報表、趨勢、匯出、明細頁資料。
Part 2 · English
What we're building: a standalone, read-only web app that presents Toastmasters District 67 performance for the Program Quality Director (PQD) and district leaders, with Chinese club names that the official dashboard lacks.
Supersedes/consolidates: project-highlight/ai001.md (feature ideas) and project-highlight/ai002.md (Chinese-name enrichment). This file is the authoritative spec; where it disagrees with ai001/ai002, this wins (notably the join key — see §3.4).
Scope: District 67 only (id=67). No district picker. Read-only mirror; we never write back to TI.
Design philosophy — completeness-first preview. We don't yet know which decisions the PQD wants to drive, so the app's job is to make all of District 67's information available and explorable, not to pre-decide what matters. Principles:
- Mirror faithfully, hide nothing — every metric and level the official report shows for
id=67, reproduced. - Explorable, not prescriptive — give sort/filter/search/toggles; opinionated views (e.g. a watchlist) are optional helpers, never the app's thesis.
- Add only what's missing — Chinese names, bilingual search, as-of-date visibility, mobile access.
- Be transparent about definitions — show 183 chartered / 178 active / 174 paid side by side; label "payments ≠ unique headcount." We make distinctions legible; the PQD interprets.
- Progressive disclosure — overview → drilldown → full table → export, so depth is available but not overwhelming on first glance. This is the v1 we present to the PQD to gather further instructions.
District 67 baseline (verified 2026-06-15, PY 2025-2026, as-of 12-Jun-2026)
| Metric | Value |
|---|---|
| Divisions | 9 (A–I) |
| Areas | 38 |
| Clubs on roster | 183 (178 active · 174 paid · 173 base) |
| Suspended clubs | 5 (FHK, NTUST, Critical Thinkers, MIRDC, Scien Tech) |
| Member head count (To Date) | 2,660 (base 2,838 · net −178) — the real member count |
| Membership payments YTD | 5,472 (base 5,377) — payment transactions, ≈2× members; NOT a head count |
Per division (Areas / Clubs / members To Date / net): A 4/20/225/−11 · B 4/20/280/−21 · C 4/21/365/+10 · D 4/23/324/−3 · E 4/17/275/−8 · F 5/23/291/−81 · G 5/23/367/+9 · H 4/18/277/−17 · I 4/18/256/−56.
Member count = head count, not payments. The District Performance report only exposes membership payments (5,472) — each member can pay twice a year (Oct + Apr renewals), so payments ≈ 2× members and must never be labelled as a member count. The real head count comes from the Club Performance report (
Club.aspx?id=67): per club Base / To Date / Net Growth (e.g. Taipei 31 → 26, net −5). District total To Date = 2,660. The app uses To Date as 會員數 (member count); payments are shown separately and clearly labelled as transactions.
Club-count glossary (bilingual — must render in the app)
These distinctions are central to "be transparent about definitions." Show this glossary in-app (e.g. an info panel / tooltips on each count), in both English and 繁體中文:
| Name 名稱 | Meaning 意義 |
|---|---|
| Clubs on Roster | 名冊上的俱樂部總數 (all clubs on the roster, incl. suspended) |
| Paid Clubs | 已完成本期會籍繳費的俱樂部 (clubs that have completed this period's membership dues) |
| Good Standing Clubs | 符合 TI 規定、會員數達標且完成繳費的俱樂部 (meet TI rules — membership minimum met and dues paid) |
| Active Clubs | 仍在運作中的俱樂部 (clubs still operating) |
| Suspended Clubs | 暫停或未達要求的俱樂部 (suspended / not meeting requirements) |
Phasing (scope discipline)
Phase 1 — build & present NOW (this is the only thing we build before PQD feedback): Club count + member count — the base for the whole summary. One overview screen: the D67 at-a-glance (Divisions 9 / Areas 38 / Clubs-on-roster 183 / Active 178 / Paid 174 / Suspended 5) plus the per-division Areas / Clubs / Members (To Date head count) table. Member count = To Date = 2,660 (real head count from Club Performance), with Base + Net Growth; membership payments (5,472) shown separately and labelled as transactions (≈2×). Bilingual (EN / 繁中) with the club-count glossary visible. Each club links to its TI Club Report (
ClubReport.aspx?id=<padded>).Phase 2+ — planned & prepared here, but built/released only on the requestor's (PQD's) response: full bilingual club table, Division/Area & Club-Report sections each deep-linking to the official TI report (sections, not tabs), Chinese-name enrichment (
clubIdjoin, §3.4), DCP goals & Distinguished status, trends, helper/watchlist views, exports. The rest of this spec defines these so we can move fast once asked.
1. Primary data source (verified 2026-06-15)
Base: https://dashboards.toastmasters.org/2025-2026/district.aspx?id=67&hideclub=1
This is the District Performance report and it lists every club in D67, grouped Division → Area → Club.
It also exposes an Export → CSV control and as-of date / program-year / month selectors (daily snapshots
available for ~the last week; program years back to 2008-2009).
1.1 District KPI summary (top of page)
Three headline blocks, each with Base, To Date, and the four recognition goal thresholds:
- Paid Clubs — Base 173 / To Date 174 · goals: Distinguished 175 / Select 179 / President's 182 / Smedley 187
- Payments — Base 5,377 / To Date 5,472 · goals: 5,431 / 5,539 / 5,646 / 5,808
- Distinguished Clubs — Base 173 / To Date 65 · goals: 78 / 87 / 96 / 104
- Membership Payments breakdown: Late, Oct, Apr, Total(renewals), New, Charter, Total, Active Clubs (178)
1.2 Per-club row (the all-clubs table)
Each club row provides:
| Field | Example | Notes |
|---|---|---|
| Club Number | 00001890, 28679758 |
8-digit, zero-padded. TI club number = our primary key. |
| Club Name (English) | Taipei Club, DALI Bilingual Toastmasters Club |
|
| Status flag | Susp 03/31/26, Charter 02/02/26 |
Optional; present only for suspended / newly chartered clubs |
| Late / Oct / Apr | 0 / 24 / 20 |
Renewal payments by window |
| Total (renewals) | 44 |
Late+Oct+Apr |
| New | 8 |
New-member payments |
| Charter | 0 |
Charter payments |
| Total (payments) | 52 |
renewals + new + charter (YTD) |
Division and Area subtotal rows additionally carry a Club Count column.
1.3 Club Performance report — Club.aspx?id=67 (now ingested)
The real member head count lives here, not on District Performance. Per club: Mem. Base / To Date / Net,
Goals Met (of 10), and the 10 individual DCP goals. Phase 1 ingests Base / To Date / Net + Goals Met
(data/raw/club-performance-67.json, joined to clubs by clubId); the full 10-goal grid + Distinguished status
are a later phase. To Date is also confirmable on Division.aspx?id=67 (per-club "Mem. To Date").
2. Enrichment source — Chinese names (verified 2026-06-15)
- List:
https://toastmasters.org.tw/new/page.php?ver=en&page_type=club_listRows of:AreaCode(e.g.F3) · combined中文名 English Name· language (英語/雙語/國語/台語/日語/客語) · city. - Detail:
https://toastmasters.org.tw/new/page.php?page_type=club&id=<CMS_ID>&ver=enAdds: Division/Area, meeting format/time, venue, fee, contact, charter date, founders/mentors/charter president.
3. Data pipeline
3.1 Ingest base (TI) — weekly snapshot
- Cadence: weekly. Each run gathers raw TI data for
id=67, appends Chinese club names (§3.4), and writes one immutable snapshot stamped withdataAsOf+capturedAt. v1 stores it as a single committed JSON file per week (simple, diffable, no DB); graduate to a database once trends matter. - Preferred source: the report's CSV export for
id=67(cleaner than HTML parsing). - Fallback: parse the HTML table (structure documented in §1.2).
3.1a UI pattern — key-info cards → detail
The dashboard surfaces key info as cards; each card links to that report's detail section (sections, not tabs — §5), which in turn deep-links to the official TI page. Overview → card → detail → official source.
3.2 Ingest enrichment (Taiwan)
- ⚠️ The Taiwan site returns HTTP 403 to plain server fetches (verified). Use a headless/browser-style
fetch with realistic headers, not naive
fetch(). - Parse the list rows →
{ cmsId, areaCode, chineseName, englishName, language, city, detailUrl }. (cmsIdcomes from each row's detail-page linkid.) - (Optional, weekly) crawl detail pages for meeting time / venue / charter date / contacts.
3.3 Name parsing
- Split combined string: leading run of CJK chars =
chineseName; remainder =englishName. Trim. - English-only clubs (name duplicated, e.g.
Global Family Global Family,FHK Toastmasters Club …) → no Chinese name; render English only.
3.4 Join base × enrichment — by clubId (long int), with name+area fallback
Correction (supersedes the earlier ai002 claim). The Taiwan site's
idis the TI club number, just unpadded — so the join works once both sides are converted to integer. Verified across 12 clubs from the listing's actual linkids:376=Hsinchu (TI00000376),1890=Taipei (00001890),1904=Kaohsiung,2304=YMIC, … all match. DALI is the lone exception — a brand-new charter showing TI's new-club number (28679758) on the dashboard but its permanent number (1460763) locally. So the id join is the rule; new charters are the fallback case.
Canonical key: clubId = parseInt(tiClubNumber, 10) (see §4 — store padded display form too).
Join algorithm:
- Primary:
dashboard.clubId === taiwan.id→ exact integer match. Handles the large majority. - Fallback (new charters / unlinked clubs): normalize English names (lowercase; strip punctuation;
normalize
Toastmasters/Club/Int'l/&; drop trailing parens) and match within the same Division+Area (dashboardDivision X / Area NN↔ listAreaCodelikeF3). - Unmatched → Chinese name blank + added to a reconciliation report. (160 clubs linked on TW vs 183 on TI roster → expect ~23 here initially.)
overrides.json(clubId → chineseName) for the manual residue — permanent, survives re-imports.
The TI club number remains the app's primary key; chineseName is an appended attribute resolved via the join.
4. Data model
ClubSnapshot {
asOf: date
district: {
paidClubs:{base,toDate,goals{distinguished,select,presidents,smedley}}
payments:{base,toDate,goals{...}, breakdown{late,oct,apr,renewTotal,new,charter,total}}
distinguishedClubs:{base,toDate,goals{...}}
activeClubs: number
}
divisions: [{ code, clubCount, payments{...},
areas: [{ code, clubCount, payments{...},
clubs: [Club] }] }]
}
Club {
clubNumber // TI, PRIMARY KEY (e.g. "28679758")
englishName
division, area // e.g. "F","03"
status // active | suspended | chartering ; statusDate
payments {late, oct, apr, renewTotal, new, charter, total}
membership {base, toDate, net, goalsMet} // real head count from Club Performance
// --- enrichment (joined by clubId) ---
chineseName | null
language | null // 英語/雙語/...
city | null
detailUrl | null
meeting | null // {format,time,venue,fee,contact,charterDate} (Phase 2)
match {method: clubId | exact | fuzzy | override | unmatched, confidence}
// --- Phase 2: DCP ---
dcp | null // {goalsMet, goals[10], recognition}
}
5. Features (v1)
- F1 — District Health Overview. KPI cards for Paid Clubs / Payments / Distinguished Clubs, each showing Base → To Date and distance to the next recognition tier (Distinguished/Select/President's/Smedley), plus a year-pacing indicator ("days left, on/off track"). Mirrors §1.1 exactly.
- F2 — Division & Area drilldown. Collapsible Division → Area → Club tree with payment subtotals and on-track coloring.
- F3 — All-Clubs table. Sortable/filterable: club #, bilingual name, division/area, members/payments, status. Filters by division, area, status, language, city.
- F4 — Helper views (optional). Not the app's thesis (see philosophy) — neutral, derived conveniences the PQD
can ignore: e.g. a "needs attention" filter surfacing Suspended (
Susp …), low-payment clubs, and new charters (Charter …), each showing Division/Area for the responsible director. Built on the same complete data, never a substitute for it. - F5 — Bilingual everywhere. Every club shows
中文名 (English Name); search matches either language; 中文/EN toggle for which leads. English-only when no Chinese name.
Phase 2
- DCP goal grid + Distinguished status per club (ingest Club Performance report).
- Trend charts from dated snapshots (payments curve, distinguished-club accumulation vs pace).
- "What-if" Distinguished-District planner; CSV/PDF export for council & Hall of Fame.
- Detail-page enrichment (meeting time/venue/contact) as filters/columns.
6. Pages / routes
| Route | Content | Status |
|---|---|---|
/ |
F1 overview + per-division summary table + count cards | Built |
/divisions |
F2 Division → Area → Club drilldown tree (per-club ClubReport links + status) | Built |
/spec |
This spec (bilingual) | Built |
/clubs |
F3 all-clubs sortable/filterable table (bilingual) | Phase 2 |
/clubs/[clubNumber] |
Single-club detail (payments, status, Chinese name, meeting info) | Phase 2 |
/watchlist |
F4 full at-risk lists | Phase 2 |
/admin/reconcile |
Unmatched-clubs view + override editor (§3.4) | Phase 2 |
7. Tech stack
- Next.js (App Router); currently deployed as a static export on Azure Static Web Apps (
output: "export"), custom domaindashboard.toastmasters.org.twvia CNAME + Azure auto-issued HTTPS. Vercel remains an option. - Ingestion: scheduled (Cron) → TI CSV pull + weekly Taiwan enrichment crawl (browser-style fetch for the 403).
- Storage: v1 is a committed weekly snapshot JSON; graduate to a database (Postgres / Blob) for dated snapshots
- cache the latest merged dataset once trends matter.
- UI: Tailwind; server components with cached data.
- i18n: zh-Hant / en; bilingual club rendering baked into the data layer.
- Mobile-first (area/division directors check clubs on phones).
8. Non-functional
- Fast first paint (data pre-fetched/cached server-side; no client-side scraping).
- Resilient enrichment: on Taiwan-fetch failure, reuse last good Chinese-name snapshot.
- Read-only; no PII beyond what the public Taiwan site already shows (club contacts on detail pages).
9. Out of scope
- Other districts / region rollups / cross-district comparison.
- Editing or writing back to TI data.
- Member-level records.
10. Risks & open questions
- Chinese-name join — RESOLVED, now id-based (§3.4). Earlier drafts called the join "name-based, not id-based";
it is now a
clubIdinteger match (TI club number = the Taiwan site'sid, unpadded), and verified. Name+area fuzzy matching is now only a fallback for new charters (e.g. DALI, which carries a temporary new-club number on TI vs its permanent number locally);overrides.jsoncovers the residue. The 183-club roster reconciliation surfaces any clubs still unmatched. - TI CSV endpoint shape — still to confirm the export URL/params for
id=67; HTML parse remains the fallback. (open) - Taiwan 403 anti-scraping — still needs a browser-style fetch with realistic headers; cache aggressively.
- Q (PQD): Is there an official D67 roster mapping TI club number ↔ Chinese name? It would make the join exact and retire the fuzzy matcher entirely.
- Q (PQD): v1 priority — is the Chinese-name + club/member counts + head-count set enough to ship, with DCP/Distinguished in Phase 2?
11. Milestones
- M1 — Ingest + model: TI base (CSV/HTML) → snapshot; verify all clubs parse.
- M2 — Core UI: F1 overview, F3 all-clubs table, F2 drilldown.
- M3 — Enrichment: Taiwan crawl → name+area join → bilingual rendering +
/admin/reconcile. - M4 — F4 watchlist + mobile polish → ship v1.
- Phase 2: DCP report, trends, exports, detail-page data.
Data structures verified live 2026-06-15 against district.aspx (id=67) and toastmasters.org.tw. Base report is program year 2025-2026, as-of 12-Jun-2026.