{"allowContribute":false,"item":{"content":"## 修改範圍\n\n*   **分支**：`cubelv-cli-announcement-hardening`（從 `main` 開出）\n    \n*   **修改檔案**：`index.html` + `announcement-admin.html`\n    \n*   **未修改**：Supabase RLS、正式資料、GitHub Pages 設定\n    \n\n* * *\n\n## A. 兩處核心修改\n\n### index.html（前台）\n\n\u003ctable style=\"min-width: 50px;\"\u003e\u003ccolgroup\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003c/colgroup\u003e\u003ctbody\u003e\u003ctr\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e原來\u003c/p\u003e\u003c/th\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改成\u003c/p\u003e\u003c/th\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e從 \u003ccode\u003elocalStorage.getItem('supabase-url')\u003c/code\u003e 讀設定\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e硬編碼 \u003ccode\u003eSUPABASE_URL\u003c/code\u003e / \u003ccode\u003eSUPABASE_PUBLISHABLE_KEY\u003c/code\u003e 常數\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e若 localStorage 無設定 → 顯示「尚未設定連線」錯誤\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e頁面載入時直接建立 Supabase client 並讀取公告\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003eloadPublishedAnnouncements(url, key)\u003c/code\u003e 接受參數\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改為無參數版本，直接使用常數\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\n\n### announcement-admin.html（後台）\n\n\u003ctable style=\"min-width: 50px;\"\u003e\u003ccolgroup\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003c/colgroup\u003e\u003ctbody\u003e\u003ctr\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e原來\u003c/p\u003e\u003c/th\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改成\u003c/p\u003e\u003c/th\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e頁面載入不自動初始化，需手動按「初始化連線」\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e頁面載入時自動初始化（若常數有效）\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003econst adminEmail = 'mj29379898@gmail.com'\u003c/code\u003e 在前端做 email 比對\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e抽成 \u003ccode\u003eADMIN_EMAIL\u003c/code\u003e 常數，只用於 Magic Link 發送目標\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003everifyAdmin()\u003c/code\u003e 檢查 \u003ccode\u003esession.user.email === adminEmail\u003c/code\u003e，不符就踢出\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改為登入成功即顯示管理面板，\u003cstrong\u003e權限由 Supabase RLS 強制\u003c/strong\u003e\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e管理員信箱直接寫死在 HTML \u003ccode\u003e\u0026lt;strong\u0026gt;\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改用 \u003ccode\u003e\u0026lt;strong id=\"admin-email-display\"\u0026gt;\u003c/code\u003e + JS 動態填入\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\n\n* * *\n\n## B. Supabase RLS SQL 規則\n\n**執行位置**：Supabase Dashboard → SQL Editor → 貼上執行\n\n```sql\n-- 1. 啟用 RLS（如果尚未啟用）\nalter table announcements enable row level security;\n\n-- 2. 刪除所有舊 policy（避免殘留規則干擾）\ndrop policy if exists \"公開可讀已發布公告\" on announcements;\ndrop policy if exists \"管理員可寫公告\" on announcements;\n\n-- 3. 公開訪客只能 SELECT 已發布公告\ncreate policy \"公開可讀已發布公告\" on announcements\n  for select\n  using (status = 'published');\n\n-- 4. 已登入管理員可 INSERT（以 auth.uid() 記錄建立者）\ncreate policy \"管理員可新增公告\" on announcements\n  for insert\n  with check (auth.role() = 'authenticated');\n\n-- 5. 已登入管理員可 UPDATE\ncreate policy \"管理員可更新公告\" on announcements\n  for update\n  using (auth.role() = 'authenticated');\n\n-- 6. 已登入管理員可 DELETE\ncreate policy \"管理員可刪除公告\" on announcements\n  for delete\n  using (auth.role() = 'authenticated');\n```\n\n### announcements 應有欄位\n\n\u003ctable style=\"min-width: 100px;\"\u003e\u003ccolgroup\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003c/colgroup\u003e\u003ctbody\u003e\u003ctr\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e欄位\u003c/p\u003e\u003c/th\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e型別\u003c/p\u003e\u003c/th\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e必填\u003c/p\u003e\u003c/th\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e說明\u003c/p\u003e\u003c/th\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003eid\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003euuid\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e✅\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003ePK，\u003ccode\u003edefault gen_random_uuid()\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003etitle\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003etext\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e✅\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e公告標題\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003econtent\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003etext\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e✅\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e公告內容\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003estatus\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003etext\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e✅\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003edraft\u003c/code\u003e / \u003ccode\u003epublished\u003c/code\u003e，default \u003ccode\u003edraft\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003eis_pinned\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003eboolean\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e✅\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e是否置頂，default \u003ccode\u003efalse\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003epublished_at\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003etimestamptz\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e​\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e發布時間（下架時設 null）\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003ecreated_at\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003etimestamptz\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e✅\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e建立時間，\u003ccode\u003edefault now()\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003eupdated_at\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003etimestamptz\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e✅\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e更新時間，\u003ccode\u003edefault now()\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\n\n建表 SQL（如果尚無此表）：\n\n```sql\ncreate table if not exists announcements (\n  id uuid default gen_random_uuid() primary key,\n  title text not null,\n  content text not null,\n  status text not null default 'draft',\n  is_pinned boolean not null default false,\n  published_at timestamptz,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now()\n);\n```\n\n* * *\n\n## C. Magic Link Redirect URL 設定位置\n\n**步驟**：\n\n1.  登入 https://supabase.com/dashboard\n    \n2.  點專案 → 左側選單 **Authentication** → **URL Configuration**\n    \n3.  在 **Redirect URLs** 欄位新增一行：\n    \n    ```\n    https://muzha-shengmu.github.io/muzha-shengmu-test/announcement-admin.html\n    ```\n    \n4.  按 Save\n    \n\n**為什麼要設**：Magic Link 登入後 Supabase 會把使用者導回此 URL。若未設定此 redirect，登入後會顯示錯誤。\n\n* * *\n\n## D. 測試步驟\n\n### 測試 1：前台公告讀取（一般信眾）\n\n1.  用手機打開 https://muzha-shengmu.github.io/muzha-shengmu-test/\n    \n2.  **預期**：直接看到公告內容，不再顯示「尚未設定 Supabase 連線」\n    \n3.  若尚無公告 → 顯示「目前尚無已發布公告。」\n    \n4.  若有公告 → 依置頂優先、發布時間排列\n    \n\n### 測試 2：後台 Magic Link 登入\n\n1.  打開 https://muzha-shengmu.github.io/muzha-shengmu-test/announcement-admin.html\n    \n2.  **預期**：頁面自動初始化，直接顯示「管理員登入」區塊（含信箱 `mj29379898@gmail.com`）\n    \n3.  按「發送 Magic Link」\n    \n4.  **預期**：顯示「登入連結已寄出」\n    \n5.  至 `mj29379898@gmail.com` 信箱點擊連結\n    \n6.  **預期**：導回後台，自動顯示管理面板（公告新增／編輯／發布／置頂功能）\n    \n\n### 測試 3：RLS 權限驗證（未登入不可寫）\n\n1.  在不登入的狀態下，用瀏覽器開發者工具執行：\n    \n    ```js\n    // 貼到 console 測試 RLS 是否阻止寫入\n    const c = supabase.createClient(URL, KEY);\n    c.from('announcements').insert([{title:'x',content:'x'}]).then(r=\u003econsole.log(r));\n    ```\n    \n2.  **預期**：回傳 RLS 錯誤（無法新增）\n    \n\n### 測試 4：發布 → 前台同步\n\n1.  在後台新增一則公告 → 按「發布」\n    \n2.  打開前台首頁\n    \n3.  **預期**：新發布的公告出現在前台\n    \n\n* * *\n\n## E. GitHub 推送前的檔案差異摘要\n\n```\n分支：cubelv-cli-announcement-hardening（從 main 開出）\n\n index.html                  | 18 行變動 (+11 / -7)\n announcement-admin.html     | 52 行變動 (+43 / -9)\n ───────────────────────────\n 2 files changed, 54 insertions(+), 16 deletions(-)\n```\n\n### index.html 變更重點\n\n*   第 119–134 行：新增 `SUPABASE_URL` / `SUPABASE_PUBLISHABLE_KEY` 常數\n    \n*   移除 `localStorage.getItem()` 邏輯\n    \n*   `loadPublishedAnnouncements()` 改為無參數版本\n    \n\n### announcement-admin.html 變更重點\n\n*   第 166–170 行：新增三個常數定義\n    \n*   第 188–237 行：新增 `autoInitSupabase()`，頁面載入自動連線\n    \n*   第 266–275 行：Magic Link 改用 `ADMIN_EMAIL` 常數\n    \n*   第 316–333 行：`verifyAdmin()` 改為不做 email 比對，直接放行（靠 RLS）\n    \n*   第 138 行：信箱改為 `\u003cstrong id=\"admin-email-display\"\u003e` + JS 動態填入\n    \n\n### 未修改\n\n*   CSS 樣式：完全未更動\n    \n*   公告 CRUD 邏輯：完全未更動\n    \n*   前台 UI 結構：完全未更動\n    \n*   GitHub Pages 設定：未動\n    \n\n​","createdAt":1783228207298,"deletedAt":null,"id":"6a49e72f77c9634b5faff8a6","isNew":true,"isPublic":true,"itemType":"NOTE","name":"修復計畫：公告系統安全與可用性（A~E 完整報告） 的副本","parents":{"2a5bb84fc5a46bee9113f20d":1783228110867},"pinnedAt":null,"preParentID":null,"reminderTime":null,"updatedAt":1783228322379,"version":3,"websiteContent":"[{\"originUrl\":\"https://supabase.com/dashboard\",\"title\":null,\"content\":null,\"imageUrl\":null},{\"originUrl\":\"https://muzha-shengmu.github.io/muzha-shengmu-test/announcement-admin.html\",\"title\":null,\"content\":null,\"imageUrl\":null},{\"originUrl\":\"https://muzha-shengmu.github.io/muzha-shengmu-test/\",\"title\":null,\"content\":null,\"imageUrl\":null}]"},"ownerName":"Sun Lin","subtree":[{"content":"## 修改範圍\n\n*   **分支**：`cubelv-cli-announcement-hardening`（從 `main` 開出）\n    \n*   **修改檔案**：`index.html` + `announcement-admin.html`\n    \n*   **未修改**：Supabase RLS、正式資料、GitHub Pages 設定\n    \n\n* * *\n\n## A. 兩處核心修改\n\n### index.html（前台）\n\n\u003ctable style=\"min-width: 50px;\"\u003e\u003ccolgroup\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003c/colgroup\u003e\u003ctbody\u003e\u003ctr\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e原來\u003c/p\u003e\u003c/th\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改成\u003c/p\u003e\u003c/th\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e從 \u003ccode\u003elocalStorage.getItem('supabase-url')\u003c/code\u003e 讀設定\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e硬編碼 \u003ccode\u003eSUPABASE_URL\u003c/code\u003e / \u003ccode\u003eSUPABASE_PUBLISHABLE_KEY\u003c/code\u003e 常數\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e若 localStorage 無設定 → 顯示「尚未設定連線」錯誤\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e頁面載入時直接建立 Supabase client 並讀取公告\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003eloadPublishedAnnouncements(url, key)\u003c/code\u003e 接受參數\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改為無參數版本，直接使用常數\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\n\n### announcement-admin.html（後台）\n\n\u003ctable style=\"min-width: 50px;\"\u003e\u003ccolgroup\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003c/colgroup\u003e\u003ctbody\u003e\u003ctr\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e原來\u003c/p\u003e\u003c/th\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改成\u003c/p\u003e\u003c/th\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e頁面載入不自動初始化，需手動按「初始化連線」\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e頁面載入時自動初始化（若常數有效）\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003econst adminEmail = 'mj29379898@gmail.com'\u003c/code\u003e 在前端做 email 比對\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e抽成 \u003ccode\u003eADMIN_EMAIL\u003c/code\u003e 常數，只用於 Magic Link 發送目標\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003everifyAdmin()\u003c/code\u003e 檢查 \u003ccode\u003esession.user.email === adminEmail\u003c/code\u003e，不符就踢出\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改為登入成功即顯示管理面板，\u003cstrong\u003e權限由 Supabase RLS 強制\u003c/strong\u003e\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e管理員信箱直接寫死在 HTML \u003ccode\u003e\u0026lt;strong\u0026gt;\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e改用 \u003ccode\u003e\u0026lt;strong id=\"admin-email-display\"\u0026gt;\u003c/code\u003e + JS 動態填入\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\n\n* * *\n\n## B. Supabase RLS SQL 規則\n\n**執行位置**：Supabase Dashboard → SQL Editor → 貼上執行\n\n```sql\n-- 1. 啟用 RLS（如果尚未啟用）\nalter table announcements enable row level security;\n\n-- 2. 刪除所有舊 policy（避免殘留規則干擾）\ndrop policy if exists \"公開可讀已發布公告\" on announcements;\ndrop policy if exists \"管理員可寫公告\" on announcements;\n\n-- 3. 公開訪客只能 SELECT 已發布公告\ncreate policy \"公開可讀已發布公告\" on announcements\n  for select\n  using (status = 'published');\n\n-- 4. 已登入管理員可 INSERT（以 auth.uid() 記錄建立者）\ncreate policy \"管理員可新增公告\" on announcements\n  for insert\n  with check (auth.role() = 'authenticated');\n\n-- 5. 已登入管理員可 UPDATE\ncreate policy \"管理員可更新公告\" on announcements\n  for update\n  using (auth.role() = 'authenticated');\n\n-- 6. 已登入管理員可 DELETE\ncreate policy \"管理員可刪除公告\" on announcements\n  for delete\n  using (auth.role() = 'authenticated');\n```\n\n### announcements 應有欄位\n\n\u003ctable style=\"min-width: 100px;\"\u003e\u003ccolgroup\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003ccol style=\"min-width: 25px;\"\u003e\u003c/colgroup\u003e\u003ctbody\u003e\u003ctr\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e欄位\u003c/p\u003e\u003c/th\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e型別\u003c/p\u003e\u003c/th\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e必填\u003c/p\u003e\u003c/th\u003e\u003cth colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e說明\u003c/p\u003e\u003c/th\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003eid\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003euuid\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e✅\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003ePK，\u003ccode\u003edefault gen_random_uuid()\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003etitle\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003etext\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e✅\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e公告標題\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003econtent\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003etext\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e✅\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e公告內容\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003estatus\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003etext\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e✅\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003edraft\u003c/code\u003e / \u003ccode\u003epublished\u003c/code\u003e，default \u003ccode\u003edraft\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003eis_pinned\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003eboolean\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e✅\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e是否置頂，default \u003ccode\u003efalse\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003epublished_at\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003etimestamptz\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e​\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e發布時間（下架時設 null）\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003ecreated_at\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003etimestamptz\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e✅\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e建立時間，\u003ccode\u003edefault now()\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003eupdated_at\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e\u003ccode\u003etimestamptz\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e✅\u003c/p\u003e\u003c/td\u003e\u003ctd colspan=\"1\" rowspan=\"1\"\u003e\u003cp\u003e更新時間，\u003ccode\u003edefault now()\u003c/code\u003e\u003c/p\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/tbody\u003e\u003c/table\u003e\n\n建表 SQL（如果尚無此表）：\n\n```sql\ncreate table if not exists announcements (\n  id uuid default gen_random_uuid() primary key,\n  title text not null,\n  content text not null,\n  status text not null default 'draft',\n  is_pinned boolean not null default false,\n  published_at timestamptz,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now()\n);\n```\n\n* * *\n\n## C. Magic Link Redirect URL 設定位置\n\n**步驟**：\n\n1.  登入 https://supabase.com/dashboard\n    \n2.  點專案 → 左側選單 **Authentication** → **URL Configuration**\n    \n3.  在 **Redirect URLs** 欄位新增一行：\n    \n    ```\n    https://muzha-shengmu.github.io/muzha-shengmu-test/announcement-admin.html\n    ```\n    \n4.  按 Save\n    \n\n**為什麼要設**：Magic Link 登入後 Supabase 會把使用者導回此 URL。若未設定此 redirect，登入後會顯示錯誤。\n\n* * *\n\n## D. 測試步驟\n\n### 測試 1：前台公告讀取（一般信眾）\n\n1.  用手機打開 https://muzha-shengmu.github.io/muzha-shengmu-test/\n    \n2.  **預期**：直接看到公告內容，不再顯示「尚未設定 Supabase 連線」\n    \n3.  若尚無公告 → 顯示「目前尚無已發布公告。」\n    \n4.  若有公告 → 依置頂優先、發布時間排列\n    \n\n### 測試 2：後台 Magic Link 登入\n\n1.  打開 https://muzha-shengmu.github.io/muzha-shengmu-test/announcement-admin.html\n    \n2.  **預期**：頁面自動初始化，直接顯示「管理員登入」區塊（含信箱 `mj29379898@gmail.com`）\n    \n3.  按「發送 Magic Link」\n    \n4.  **預期**：顯示「登入連結已寄出」\n    \n5.  至 `mj29379898@gmail.com` 信箱點擊連結\n    \n6.  **預期**：導回後台，自動顯示管理面板（公告新增／編輯／發布／置頂功能）\n    \n\n### 測試 3：RLS 權限驗證（未登入不可寫）\n\n1.  在不登入的狀態下，用瀏覽器開發者工具執行：\n    \n    ```js\n    // 貼到 console 測試 RLS 是否阻止寫入\n    const c = supabase.createClient(URL, KEY);\n    c.from('announcements').insert([{title:'x',content:'x'}]).then(r=\u003econsole.log(r));\n    ```\n    \n2.  **預期**：回傳 RLS 錯誤（無法新增）\n    \n\n### 測試 4：發布 → 前台同步\n\n1.  在後台新增一則公告 → 按「發布」\n    \n2.  打開前台首頁\n    \n3.  **預期**：新發布的公告出現在前台\n    \n\n* * *\n\n## E. GitHub 推送前的檔案差異摘要\n\n```\n分支：cubelv-cli-announcement-hardening（從 main 開出）\n\n index.html                  | 18 行變動 (+11 / -7)\n announcement-admin.html     | 52 行變動 (+43 / -9)\n ───────────────────────────\n 2 files changed, 54 insertions(+), 16 deletions(-)\n```\n\n### index.html 變更重點\n\n*   第 119–134 行：新增 `SUPABASE_URL` / `SUPABASE_PUBLISHABLE_KEY` 常數\n    \n*   移除 `localStorage.getItem()` 邏輯\n    \n*   `loadPublishedAnnouncements()` 改為無參數版本\n    \n\n### announcement-admin.html 變更重點\n\n*   第 166–170 行：新增三個常數定義\n    \n*   第 188–237 行：新增 `autoInitSupabase()`，頁面載入自動連線\n    \n*   第 266–275 行：Magic Link 改用 `ADMIN_EMAIL` 常數\n    \n*   第 316–333 行：`verifyAdmin()` 改為不做 email 比對，直接放行（靠 RLS）\n    \n*   第 138 行：信箱改為 `\u003cstrong id=\"admin-email-display\"\u003e` + JS 動態填入\n    \n\n### 未修改\n\n*   CSS 樣式：完全未更動\n    \n*   公告 CRUD 邏輯：完全未更動\n    \n*   前台 UI 結構：完全未更動\n    \n*   GitHub Pages 設定：未動\n    \n\n​","createdAt":1783228207298,"deletedAt":null,"id":"6a49e72f77c9634b5faff8a6","isNew":true,"isPublic":true,"itemType":"NOTE","name":"修復計畫：公告系統安全與可用性（A~E 完整報告） 的副本","parents":{"2a5bb84fc5a46bee9113f20d":1783228110867},"pinnedAt":null,"preParentID":null,"reminderTime":null,"updatedAt":1783228322379,"version":3,"websiteContent":"[{\"originUrl\":\"https://supabase.com/dashboard\",\"title\":null,\"content\":null,\"imageUrl\":null},{\"originUrl\":\"https://muzha-shengmu.github.io/muzha-shengmu-test/announcement-admin.html\",\"title\":null,\"content\":null,\"imageUrl\":null},{\"originUrl\":\"https://muzha-shengmu.github.io/muzha-shengmu-test/\",\"title\":null,\"content\":null,\"imageUrl\":null}]"}]}