htlin222/unfit-coach-tracker

GitHub: htlin222/unfit-coach-tracker

基于 Cloudflare 全家桶构建的不适任教练信息公开追踪平台,每日自动同步官方数据并提供搜索与 API 查询服务。

Stars: 0 | Forks: 0

# 不適任教練資訊追蹤平台 · Unfit Coach Tracker ![Architecture](https://img.shields.io/badge/Cloudflare-Pages%20%2B%20Worker%20%2B%20D1-F38020) ![Primer](https://img.shields.io/badge/UI-Primer.style-0969da) ![License](https://img.shields.io/badge/License-MIT-green) ## 線上版本 | | | |---|---| | **網站** | | | **教練名單 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抓取, 信息追踪, 数据抓取, 程序员工具, 自动化攻击