htlin222/unfit-coach-tracker
GitHub: htlin222/unfit-coach-tracker
基于 Cloudflare 全家桶构建的不适任教练信息公开追踪平台,每日自动同步官方数据并提供搜索与 API 查询服务。
Stars: 0 | Forks: 0
# 不適任教練資訊追蹤平台 · Unfit Coach Tracker



## 線上版本
| | |
|---|---|
| **網站** | |
| **教練名單 API** | |
| **統計 API** | |
| **同步 Worker** | |
## 架構
```
┌─────────────────────────────────────────────────────────────┐
│ Cloudflare Edge │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Pages (CDN) │ │ Worker │ │ D1 │ │
│ │ │ │ (cron 06:00)│ │ (SQLite) │ │
│ │ React + │───▶│ │───▶│ │ │
│ │ Primer.style│ │ Scrape │ │ coaches │ │
│ │ │ │ sports.gov │ │ sync_log │ │
│ │ functions/ │───▶│ │ │ meta │ │
│ │ api/*.ts │ │ Deterministic│ │ │ │
│ │ │ │ Upsert │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ ▲ │
│ ▼ ▼ │ │
│ /api/coaches POST /sync read/write │
│ /api/stats GET /health │
└─────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────────────┐
│ User │ │ sports.gov.tw │
│ Browser │ │ /News/6295 │
└──────────┘ └──────────────────┘
```
## 快速開始
需要 **Node ≥ 20** 和 **pnpm**(本專案透過 `packageManager` 鎖定版本)。
```
# 安装 deps
pnpm install
# 创建 D1 database
pnpm db:create
# → 将 database_id 复制到 wrangler.toml 和 wrangler.worker.toml 中
# 运行 schema + seed (远程)
pnpm db:migrate
pnpm db:seed
# 部署 cron Worker
pnpm deploy:worker
# 构建并部署 frontend + Functions
pnpm deploy:pages
# 或者:一键部署(执行上述所有操作,幂等)
./deploy.sh
```
### 本地開發
```
pnpm dev # Vite dev server (expects `wrangler pages dev` on :8788 for the API)
pnpm dev:full # build + wrangler pages dev with a local D1
pnpm typecheck # tsc --noEmit
```
## 專案結構
```
unfit-coach-tracker/
├── wrangler.toml # Pages config (dist/ + D1 binding for Functions)
├── wrangler.worker.toml # Worker config (cron + D1 binding)
├── schema.sql # D1 schema (coaches, sync_log, meta)
├── seed.sql # Initial 147 records from OSINT scrape
├── index.html # Vite entry + SEO meta + JSON-LD
├── vite.config.ts
├── tsconfig.json
│
├── src/ # Frontend (React + @primer/react)
│ ├── main.tsx # Entry — ThemeProvider + BaseStyles
│ ├── App.tsx # Main app — search, filter, table
│ ├── components/
│ │ ├── Header.tsx # Sticky header + sync badge + stat cards
│ │ ├── SearchBar.tsx # TextInput + Select (sport filter)
│ │ ├── CategoryTabs.tsx# 球類運動 / 格鬥類 / 水上運動類 ...
│ │ ├── CoachTable.tsx # Primer Table with judgment links
│ │ └── States.tsx # Loading / Error states
│ ├── lib/
│ │ ├── types.ts # TypeScript interfaces + category map
│ │ └── api.ts # Fetch wrappers for /api/*
│ └── styles/
│ └── app.css # Global overrides + category label colors
│
├── worker/
│ └── index.ts # Cron Worker — scrape → deterministic upsert
│
├── functions/api/ # Pages Functions (API layer)
│ ├── coaches.ts # GET /api/coaches?q=&category=&sport=&page=
│ └── stats.ts # GET /api/stats
│
├── public/ # Copied verbatim into dist/
│ ├── favicon.svg
│ ├── apple-touch-icon.png
│ ├── og.png # 1200×630 social card
│ ├── robots.txt
│ ├── sitemap.xml
│ ├── site.webmanifest
│ └── _headers # Pages response headers (security + caching)
└── deploy.sh # One-shot deployment script
```
## D1 Schema
```
coaches (
id TEXT PK, -- SHA-256(name|sport|judgment_url)
name TEXT,
sport TEXT,
category TEXT, -- 球類運動 | 格鬥類 | ...
judgment_url TEXT, -- Link to judicial.gov.tw
judgment_type TEXT, -- 裁判書 / 判決書
first_seen_at TEXT, -- When first discovered
last_updated_at TEXT, -- Last sync that confirmed this record
is_active INTEGER -- 1 = in latest sync, 0 = removed
)
sync_log (
id, started_at, completed_at, total_fetched,
new_records, updated_records, removed_records,
status, error, source_url
)
```
## 確定性同步邏輯
Worker 的 `syncToD1()` 函數:
1. **爬取** 透過基於正規表示式的 HTML 表格解析器,從 `sports.gov.tw/News/6295` 爬取所有頁面
2. **去重** 根據 `(name, sport, judgment_url)` 元組進行去重
3. **計算確定性 ID**:`SHA-256(name + "|" + sport + "|" + judgment_url)`
4. **Upsert**:對於每筆爬取到的記錄:
- 若 ID 已存在 → 執行 `UPDATE`(設定 `is_active=1`,更新 `last_updated_at`)
- 若為新 ID → 執行 `INSERT`(設定 `first_seen_at` 與 `last_updated_at`)
5. **停用**:存在於 D1 但未在爬取結果中的記錄 → `SET is_active=0`
6. **日誌**:寫入包含計數與狀態的 `sync_log` 紀錄
## Cron 排程
`wrangler.worker.toml`:
```
[triggers]
crons = ["0 6 * * *"] # Daily at 06:00 UTC = 14:00 Taiwan
```
透過 HTTP 進行手動同步:
```
curl -X POST https://unfit-coach-sync..workers.dev/sync
```
## API 端點
| 端點 | 方法 | 說明 |
|----------|--------|-------------|
| `/api/coaches` | GET | 列出教練,支援 `q`、`category`、`sport`、`page`、`per_page` |
| `/api/stats` | GET | 匯總統計數據 + 上次同步資訊 |
| `/sync` | POST | 手動觸發 (Worker) |
| `/health` | GET | 健康檢查 (Worker) |
## SEO
前端是一個客戶端渲染的 SPA,因此可發現性是在文件層級處理的:
| 資產 | 用途 |
|-------|---------|
| `index.html` `` | 標題/說明、canonical、`robots`、Open Graph、Twitter card、`theme-color` |
| JSON-LD `@graph` | `WebSite`(含 `SearchAction`)、`Dataset`(來源、授權條款、JSON 分發)、`WebPage` |
| `
标签:Cloudflare, MITRE ATT&CK, React, Serverless, SQLite, Syscalls, URL抓取, 信息追踪, 数据抓取, 程序员工具, 自动化攻击