[{"data":1,"prerenderedAt":1173},["ShallowReactive",2],{"works-zh-TW":3},[4,201,335,470,587,726,877,1015],{"id":5,"title":6,"body":7,"cover":165,"description":158,"draft":166,"extension":167,"featured":168,"gallery":169,"highlights":170,"kind":174,"links":175,"meta":179,"navigation":168,"order":180,"path":181,"period":182,"role":184,"seo":187,"stack":188,"status":194,"stem":195,"summary":196,"team":197,"visibility":198,"__hash__":199,"slug":200,"isFallback":166},"works\u002Fworks\u002Fzh-TW\u002Fui-sanring.md","Sanring UI",{"type":8,"value":9,"toc":157},"minimark",[10,14,18,21,76,79,130,133],[11,12,13],"h2",{"id":13},"背景與目標",[15,16,17],"p",{},"傳統元件庫以 npm 套件發布，客製化時常卡在覆寫樣式。Sanring UI 改用「複製貼上」模式：CLI 把元件原始碼直接放進使用者的專案，程式碼歸使用者所有、可自由修改。",[11,19,20],{"id":20},"核心功能",[22,23,24,37,43,61,70],"ul",{},[25,26,27,31,32,36],"li",{},[28,29,30],"strong",{},"安裝元件","：",[33,34,35],"code",{},"sanring add"," 自動解析元件之間的相依、peer 套件與共用工具",[25,38,39,42],{},[28,40,41],{},"安全更新","：記錄每個檔案的內容雜湊，未修改的檔案靜默更新，改過的檔案先顯示 diff 再決定是否覆寫",[25,44,45,48,49,52,53,56,57,60],{},[28,46,47],{},"多來源 registry","：可設定多個元件來源（",[33,50,51],{},"alias:component","、",[33,54,55],{},"github:owner\u002Frepo","），也能用 ",[33,58,59],{},"sanring build"," 產生自己的 registry",[25,62,63,31,66,69],{},[28,64,65],{},"AI 整合",[33,67,68],{},"sanring mcp"," 提供 MCP server，AI 助手可查詢元件規格與用法",[25,71,72,75],{},[28,73,74],{},"文件站","：互動式主題產生器、一鍵開到 StackBlitz、深色模式",[11,77,78],{"id":78},"技術架構",[22,80,81,107,113,118],{},[25,82,83,86,87,90,91,94,95,98,99,102,103,106],{},[28,84,85],{},"專案結構","：pnpm monorepo，含 ",[33,88,89],{},"apps\u002Fdocs","（文件站）、",[33,92,93],{},"packages\u002Fui","（元件原始碼）、",[33,96,97],{},"packages\u002Fcli","（發布到 npm 的 ",[33,100,101],{},"@sanring\u002Fcli","）、",[33,104,105],{},"registry\u002F","（CLI 下載的元件範本）",[25,108,109,112],{},[28,110,111],{},"發布","：以 Changesets 管理版本，GitHub Actions 透過 OIDC trusted publishing 發布到 npm",[25,114,115,117],{},[28,116,74],{},"：部署於 GitHub Pages",[25,119,120,123,124,129],{},[28,121,122],{},"相依","：日期元件依賴同系列的 ",[125,126,128],"a",{"href":127},"\u002Fworks\u002Fdate-picker","Date Picker"," 引擎",[11,131,132],{"id":132},"挑戰與解法",[22,134,135,141,147],{},[25,136,137,140],{},[28,138,139],{},"組織圖排版","：需要雙主管、跨層虛線、避開卡片的直角連線。實際打包比較 d3-hierarchy、dagre 等排版引擎後，選用 elkjs。",[25,142,143,146],{},[28,144,145],{},"富文字編輯器","：考量 SSR 安全與既有 popover 的焦點行為，不用現成的 Angular 包裝套件，改以 Tiptap v3 自寫薄包裝。",[25,148,149,152,153,156],{},[28,150,151],{},"多 registry 設計","：CLI 原本只支援單一來源，改為在設定檔記錄具名來源，並以 ",[33,154,155],{},"alias:"," 前綴指定元件出處。",{"title":158,"searchDepth":159,"depth":159,"links":160},"",2,[161,162,163,164],{"id":13,"depth":159,"text":13},{"id":20,"depth":159,"text":20},{"id":78,"depth":159,"text":78},{"id":132,"depth":159,"text":132},"\u002Fworks\u002Fui-sanring\u002Fcover.png",false,"md",true,[],[171,172,173],"50+ 個 headless 元件與 4 個頁面區塊（登入、後台框架、表格頁、組織圖）","CLI 內建 MCP server，讓 AI coding agent 直接查元件規格","CI 把打包後的 CLI 裝進全新 Angular 專案做 production build，並跑雙主題的無障礙與視覺回歸測試","side-project",{"demo":176,"repo":177,"docs":178},"https:\u002F\u002Fui.sanring.dev","https:\u002F\u002Fgithub.com\u002Fsanringtech\u002Fui","https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@sanring\u002Fcli",{},1,"\u002Fworks\u002Fzh-tw\u002Fui-sanring",{"start":183},"2026-06",[185,186],"前端","CLI 工具",{"title":6,"description":158},[189,190,191,192,193],"Angular","Tailwind CSS","TypeScript","pnpm monorepo","Playwright","live","works\u002Fzh-TW\u002Fui-sanring","給 Angular 開發者的複製貼上式 headless 元件庫與 CLI","個人","public","n3gI9734uZQkw_mDO00SgCTrd_mSD1UINNmBiLpWDuQ","ui-sanring",{"id":202,"title":203,"body":204,"cover":311,"description":158,"draft":166,"extension":167,"featured":168,"gallery":312,"highlights":313,"kind":174,"links":317,"meta":321,"navigation":168,"order":159,"path":322,"period":323,"role":325,"seo":326,"stack":327,"status":194,"stem":331,"summary":332,"team":197,"visibility":198,"__hash__":333,"slug":334,"isFallback":166},"works\u002Fworks\u002Fzh-TW\u002Fdate-picker.md","Sanring Date Picker",{"type":8,"value":205,"toc":305},[206,208,211,213,245,247,281,283],[11,207,13],{"id":13},[15,209,210],{},"Angular Material、PrimeNG 等現成日期選擇器把外觀和邏輯綁在一起，版面不合用時只能和 CSS 覆寫奮戰。這個套件只負責「大腦」（狀態與互動邏輯），markup 與樣式完全交給使用者。",[11,212,20],{"id":20},[22,214,215,221,227,233,239],{},[25,216,217,220],{},[28,218,219],{},"日曆引擎","：單選、區間、多選，可設定不可選日期",[25,222,223,226],{},[28,224,225],{},"粒度選擇","：月、季、年的選擇引擎",[25,228,229,232],{},[28,230,231],{},"時間調整","：時、分調整引擎",[25,234,235,238],{},[28,236,237],{},"組合式元件","：DatePicker、RangePicker、多選、粒度選擇、DateTime、DateTimeRange",[25,240,241,244],{},[28,242,243],{},"多語系","：語系需明確注入，文件站提供中／英文",[11,246,78],{"id":78},[22,248,249,258,267,276],{},[25,250,251,31,254,257],{},[28,252,253],{},"引擎",[33,255,256],{},"@sanring\u002Fdate-picker-core","，headless，不含任何 UI",[25,259,260,31,263,266],{},[28,261,262],{},"元件",[33,264,265],{},"@sanring\u002Fdate-picker","，以 Angular CDK Overlay 組合而成",[25,268,269,271,272,275],{},[28,270,74],{},"：部署於 GitHub Pages，本身用 ",[125,273,6],{"href":274},"\u002Fworks\u002Fui-sanring"," 的 CLI 安裝元件",[25,277,278,280],{},[28,279,111],{},"：以 Changesets 管理版本與發布",[11,282,132],{"id":132},[22,284,285,295],{},[25,286,287,290,291,294],{},[28,288,289],{},"粒度選擇的架構","：月／季／年選擇獨立成 ",[33,292,293],{},"GranularityPickerEngine","，並把與日曆共用的狀態機抽成參數化的純函式，避免兩套邏輯分歧。",[25,296,297,300,301,304],{},[28,298,299],{},"時間調整放在哪一層","：最初把時間調整放進日曆引擎，同日改為獨立的 ",[33,302,303],{},"TimeAdjustmentEngine","（輸入、輸出都是 Date），讓引擎層保持單一職責，跨層的串接交給元件層處理。",{"title":158,"searchDepth":159,"depth":159,"links":306},[307,308,309,310],{"id":13,"depth":159,"text":13},{"id":20,"depth":159,"text":20},{"id":78,"depth":159,"text":78},{"id":132,"depth":159,"text":132},"\u002Fworks\u002Fdate-picker\u002Fcover.png",[],[314,315,316],"拆成 headless 引擎與組合式元件兩個 npm 套件，元件也支援 shadcn 式複製模式","依 WAI-ARIA Date Picker Grid 實作完整鍵盤操作，支援日／月／季／年四種粒度","先寫規格（憲法、PRD、ADR）再實作，第一個 commit 隔天即發布 v0.1.0",{"demo":318,"repo":319,"docs":320},"https:\u002F\u002Fdate-picker.sanring.dev\u002F","https:\u002F\u002Fgithub.com\u002Fsanringtech\u002Fdate-picker","https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@sanring\u002Fdate-picker",{},"\u002Fworks\u002Fzh-tw\u002Fdate-picker",{"start":324,"end":324},"2026-07",[185],{"title":203,"description":158},[189,328,329,190,330],"date-fns","Angular CDK","Vitest","works\u002Fzh-TW\u002Fdate-picker","Angular 的 headless 日期選擇引擎與組合式元件套件","4XM4XTqPv4yHyzV8e6AErXOjyvy-fCzx8UGecVuwWmc","date-picker",{"id":336,"title":337,"body":338,"cover":443,"description":158,"draft":166,"extension":167,"featured":168,"gallery":444,"highlights":445,"kind":174,"links":449,"meta":452,"navigation":168,"order":453,"path":454,"period":455,"role":458,"seo":460,"stack":461,"status":194,"stem":466,"summary":467,"team":197,"visibility":198,"__hash__":468,"slug":469,"isFallback":166},"works\u002Fworks\u002Fzh-TW\u002Fhaul-sanring.md","Haul",{"type":8,"value":339,"toc":437},[340,342,345,347,379,381,407,409],[11,341,13],{"id":13},[15,343,344],{},"同時使用多個 AI 服務、每個服務又有多個帳號時，要逐一登入才看得到各自的用量。Haul 把每個 AI 底下每個帳號的用量集中到同一個畫面。",[11,346,20],{"id":20},[22,348,349,355,361,367,373],{},[25,350,351,354],{},[28,352,353],{},"多帳號用量卡片","：5 小時／7 天用量百分比、重置時間，以及 DeepSeek、Kimi 的餘額",[25,356,357,360],{},[28,358,359],{},"帳號切換","：擷取目前 CLI 的登入狀態，在多個帳號之間切換",[25,362,363,366],{},[28,364,365],{},"本機用量帳本","：掃描近 30 天的 Claude／Codex 紀錄，依月／週／日與模型彙整",[25,368,369,372],{},[28,370,371],{},"用量趨勢圖","：折線圖與環形儀表，手刻 SVG，不依賴圖表套件",[25,374,375,378],{},[28,376,377],{},"匯出與常駐","：匯出 xlsx；Windows 支援系統匣常駐",[11,380,78],{"id":78},[22,382,383,389,395,401],{},[25,384,385,388],{},[28,386,387],{},"桌面殼層","：Photino.NET（原生 WebView），後端為 C# \u002F .NET，介面為 Angular",[25,390,391,394],{},[28,392,393],{},"前後端通訊","：透過 WebView 訊息溝通；發布時 Angular 打包後內嵌進執行檔，由本機 loopback 伺服器提供給 WebView",[25,396,397,400],{},[28,398,399],{},"資料","：用量紀錄存在 SQLite，匯出用 ClosedXML",[25,402,403,406],{},[28,404,405],{},"介紹頁","：放在同一個 repo，部署於 GitHub Pages",[11,408,132],{"id":132},[22,410,411,425,431],{},[25,412,413,416,417,420,421,424],{},[28,414,415],{},"Windows 發布版黑畫面","：原因有二——WebView2 需要 STA 執行緒，以及 ",[33,418,419],{},"file:\u002F\u002F"," 的 CORS 擋掉 Angular 的 ES module。改為 ",[33,422,423],{},"[STAThread]"," 並以 loopback HTTP 伺服器提供前端檔案。",[25,426,427,430],{},[28,428,429],{},"Windows 憑證管理員容量限制","：單筆上限 2560 bytes，帳號快照改為分段寫入。",[25,432,433,436],{},[28,434,435],{},"從 Finder 開啟時找不到 CLI","：macOS 從 Finder 啟動的 App 只有精簡的 PATH，改用登入 shell 執行指令偵測。",{"title":158,"searchDepth":159,"depth":159,"links":438},[439,440,441,442],{"id":13,"depth":159,"text":13},{"id":20,"depth":159,"text":20},{"id":78,"depth":159,"text":78},{"id":132,"depth":159,"text":132},"\u002Fworks\u002Fhaul-sanring\u002Fcover.png",[],[446,447,448],"以單一介面接上 Claude、Codex、DeepSeek、Kimi、Grok、Cursor 等 7 種用量來源，單一來源失敗不影響其他","Claude、Codex 的用量數字與官方用量頁比對一致","發布 Windows 單一 .exe、macOS .dmg 與 Homebrew cask；API key 存在系統鑰匙圈",{"demo":450,"repo":451},"https:\u002F\u002Fhaul.sanring.dev","https:\u002F\u002Fgithub.com\u002Fsanringtech\u002Fhaul",{},3,"\u002Fworks\u002Fzh-tw\u002Fhaul-sanring",{"start":456,"end":457},"2026-08","2026-09",[459],"全端",{"title":337,"description":158},[462,463,464,189,465],"C#",".NET","Photino.NET","SQLite","works\u002Fzh-TW\u002Fhaul-sanring","跨平台桌面工具，替重度 AI CLI 使用者一次看完多帳號用量","n3__lV5nZlkK6KncWHsUi5soo5G0qb1ufiwpOJS7OGs","haul-sanring",{"id":471,"title":472,"body":473,"cover":551,"description":158,"draft":166,"extension":167,"featured":168,"gallery":552,"highlights":565,"kind":174,"links":569,"meta":572,"navigation":168,"order":573,"path":574,"period":575,"role":576,"seo":577,"stack":578,"status":582,"stem":583,"summary":584,"team":197,"visibility":198,"__hash__":585,"slug":586,"isFallback":166},"works\u002Fworks\u002Fzh-TW\u002Fworld-line.md","World Line",{"type":8,"value":474,"toc":546},[475,477,480,483,485,511,513],[11,476,13],{"id":13},[15,478,479],{},"以 GIS 與時間軸呈現世界歷史：在同一時間點比較多個政權的疆域、名稱與互動，並保留史觀差異、時間不確定性與史料修正歷程。",[15,481,482],{},"第一階段範圍為 1908-01-01～1945-09-30（一戰、戰間期、二戰），涵蓋全世界所有國家與殖民地。",[11,484,20],{"id":20},[22,486,487,493,499,505],{},[25,488,489,492],{},[28,490,491],{},"主權／實際控制雙圖層","：同一日期可切換「主權」與「實際控制」兩種視角",[25,494,495,498],{},[28,496,497],{},"衝突","：地圖上標示交戰關係，點選可看交戰雙方與停戰／投降資訊",[25,500,501,504],{},[28,502,503],{},"政權資訊面板","：首都、國界期間、起訖變動、領導人與生平、相關事件",[25,506,507,510],{},[28,508,509],{},"時間軸","：拖曳或播放跳年掃描動畫，觀察疆域隨時間變化",[11,512,78],{"id":78},[22,514,515,520,526,532],{},[25,516,517,519],{},[28,518,185],{},"：Angular",[25,521,522,525],{},[28,523,524],{},"後端","：ASP.NET API、PostgreSQL",[25,527,528,531],{},[28,529,530],{},"圖磚","：PMTiles，由 Martin 提供服務",[25,533,534,537,538,541,542,545],{},[28,535,536],{},"部署","：Docker Compose 部署於 VPS，經 Cloudflare Tunnel 對外，nginx 負責 SPA、",[33,539,540],{},"\u002Fapi"," 與 ",[33,543,544],{},"\u002Ftiles"," 的轉發",{"title":158,"searchDepth":159,"depth":159,"links":547},[548,549,550],{"id":13,"depth":159,"text":13},{"id":20,"depth":159,"text":20},{"id":78,"depth":159,"text":78},"\u002Fworks\u002Fworld-line\u002Fcover.png",[553,557,561],{"src":554,"alt":555,"caption":556},"\u002Fworks\u002Fworld-line\u002F01.png","主權圖層與政權資訊面板","地圖顯示 1941 年的主權疆域，右側面板列出政權的國界期間、首都與史料出處",{"src":558,"alt":559,"caption":560},"\u002Fworks\u002Fworld-line\u002F02.png","衝突彈窗","點選衝突圖示，查看交戰雙方、開戰與停戰日期及查證狀態",{"src":562,"alt":563,"caption":564},"\u002Fworks\u002Fworld-line\u002F03.png","政權事件時間軸","政權相關事件依時間排列，以目前日期為分界",[566,567,568],"1908–1945 全球主權疆域，可沿時間軸逐日切換","每筆疆域與事件標示史料出處與查證狀態","Angular 前端＋ASP.NET API＋自架 PMTiles 圖磚，Docker 部署於 VPS",{"demo":570,"repo":571},"https:\u002F\u002Fworldline.sanring.dev\u002F?axis=sovereignty","https:\u002F\u002Fgithub.com\u002Fjack755051\u002Fworld_line",{},4,"\u002Fworks\u002Fzh-tw\u002Fworld-line",{"start":456},[459],{"title":472,"description":158},[189,579,580,581],"ASP.NET","PostgreSQL","PMTiles","wip","works\u002Fzh-TW\u002Fworld-line","以 GIS 與時間軸呈現世界歷史，比較同一時間點各政權的疆域與互動","a3Pq66n4gfw74TCa98iB3u9aTIZ0M0LXQFcocNUvM2Y","world-line",{"id":588,"title":589,"body":590,"cover":704,"description":158,"draft":166,"extension":167,"featured":168,"gallery":705,"highlights":706,"kind":174,"links":710,"meta":712,"navigation":168,"order":713,"path":714,"period":715,"role":716,"seo":717,"stack":718,"status":194,"stem":722,"summary":723,"team":197,"visibility":198,"__hash__":724,"slug":725,"isFallback":166},"works\u002Fworks\u002Fzh-TW\u002Fsanring-tech-site.md","Sanring 官網",{"type":8,"value":591,"toc":698},[592,594,597,599,604,615,620,637,639,682,684],[11,593,13],{"id":13},[15,595,596],{},"監控工程品牌的官方網站，目標是建立潛在客戶的信任，並把流量導向「預約場勘／工程諮詢」表單。後台讓非工程人員自行維護產品、內容與活動。",[11,598,20],{"id":20},[15,600,601],{},[28,602,603],{},"官網",[22,605,606,609,612],{},[25,607,608],{},"產品、解決方案、最新消息，SSR 輸出利於 SEO",[25,610,611],{},"聯絡／詢價表單",[25,613,614],{},"即時影像展示頁，作為施工實績的佐證",[15,616,617],{},[28,618,619],{},"後台",[22,621,622,625,628,631,634],{},[25,623,624],{},"產品型錄：規格範本、匯出匯入",[25,626,627],{},"詢價收件匣：來源標記與分類",[25,629,630],{},"首頁季節活動排程，以時間軸檢視",[25,632,633],{},"提案審核流程：版本快照、刪除回收區、操作紀錄、系統備份",[25,635,636],{},"提案文件 PDF 產生",[11,638,78],{"id":78},[22,640,641,648,661,666,677],{},[25,642,643,645,646],{},[28,644,185],{},"：pnpm monorepo，含官網、後台、共用三個 Angular 專案；TanStack Query、Signals、Tailwind CSS、",[125,647,6],{"href":274},[25,649,650,652,653,656,657,660],{},[28,651,524],{},"：.NET Web API（Domain／Application／Infrastructure／Api 分層），",[33,654,655],{},"\u002Fapi\u002Fpublic"," 匿名存取、",[33,658,659],{},"\u002Fapi\u002Fadmin"," 需登入",[25,662,663,665],{},[28,664,399],{},"：PostgreSQL（EF Core）、Redis；檔案存物件儲存，郵件走 HTTPS API",[25,667,668,671,672,676],{},[28,669,670],{},"登入","：後台改接 ",[125,673,675],{"href":674},"\u002Fworks\u002Fsanring-sso","Sanring SSO","，寫入權限需 MFA，權限採動態 RBAC",[25,678,679,681],{},[28,680,536],{},"：Docker Compose 部署於單台 VPS，經 Cloudflare Tunnel 對外",[11,683,132],{"id":132},[22,685,686,692],{},[25,687,688,691],{},[28,689,690],{},"主機商封鎖 SMTP","：VPS 無法直接寄信，改以 HTTPS 寄信 API 發送通知。",[25,693,694,697],{},[28,695,696],{},"帳號系統外移","：把網站自有的帳號表遷移到獨立的 SSO，完成後移除舊表，網站只保留授權邏輯。",{"title":158,"searchDepth":159,"depth":159,"links":699},[700,701,702,703],{"id":13,"depth":159,"text":13},{"id":20,"depth":159,"text":20},{"id":78,"depth":159,"text":78},{"id":132,"depth":159,"text":132},"\u002Fworks\u002Fsanring-tech-site\u002Fcover.png",[],[707,708,709],"Angular SSR 官網與後台同一個 monorepo，一個 SSR 映像依網域服務兩個品牌",".NET Web API 採 Clean Architecture，公開 API 與後台 API 分流","CI 含單元測試、相依套件弱點檢查、OWASP ZAP 掃描與 Playwright E2E",{"demo":711},"https:\u002F\u002Fwww.sanring.com",{},5,"\u002Fworks\u002Fzh-tw\u002Fsanring-tech-site",{"start":457},[459],{"title":589,"description":158},[719,463,580,720,721],"Angular SSR","Redis","Docker","works\u002Fzh-TW\u002Fsanring-tech-site","監控工程品牌的 SSR 官網，搭配內容與詢價管理後台","4kbjXkF083wRl-1Nbhroti0FwRA5Yp-Bh2y3W5NzW0o","sanring-tech-site",{"id":727,"title":675,"body":728,"cover":836,"description":158,"draft":166,"extension":167,"featured":168,"gallery":837,"highlights":858,"kind":174,"links":862,"meta":863,"navigation":168,"order":864,"path":865,"period":866,"role":868,"seo":869,"stack":870,"status":194,"stem":872,"summary":873,"team":197,"visibility":874,"__hash__":875,"slug":876,"isFallback":166},"works\u002Fworks\u002Fzh-TW\u002Fsanring-sso.md",{"type":8,"value":729,"toc":830},[730,732,735,737,769,771,804,806],[11,731,13],{"id":13},[15,733,734],{},"登入、帳號與角色原本寫在官網裡，獨立成 IdP 後，多個系統共用同一套帳號；新系統接入只需註冊，不用改程式或資料表。",[11,736,20],{"id":20},[22,738,739,745,751,757,763],{},[25,740,741,744],{},[28,742,743],{},"OIDC 登入","：Authorization Code + PKCE、refresh token 輪替；服務帳號使用 client credentials",[25,746,747,750],{},[28,748,749],{},"即時跨系統登出","：登出只結束當前工作階段，停權則撤銷全部；通知失敗會排隊重送",[25,752,753,756],{},[28,754,755],{},"依權限要求 MFA","：有編輯權限的使用者必須綁定 TOTP 並取得備用碼，唯讀使用者免綁",[25,758,759,762],{},[28,760,761],{},"系統與權限註冊","：每個系統有自己的權限代碼與角色；使用者失去最後一個角色時自動停用",[25,764,765,768],{},[28,766,767],{},"登入頁與帳號中心","：中英雙語登入頁、Google 登入，以及 Angular 帳號中心",[11,770,78],{"id":78},[22,772,773,779,785,791],{},[25,774,775,778],{},[28,776,777],{},"IdP","：單一專案同時提供 Razor 登入頁與 Angular 帳號中心",[25,780,781,784],{},[28,782,783],{},"帳號模型","：OpenIddict 只負責協定層，帳號、角色與權限自行實作（ASP.NET Core Identity）",[25,786,787,790],{},[28,788,789],{},"資料與部署","：PostgreSQL、Redis，Docker Compose 部署",[25,792,793,31,796,52,800],{},[28,794,795],{},"已接入系統",[125,797,799],{"href":798},"\u002Fworks\u002Fsanring-middle-platform","監控中台",[125,801,803],{"href":802},"\u002Fworks\u002Fsanring-tech-site","官網後台",[11,805,132],{"id":132},[22,807,808,814,820],{},[25,809,810,813],{},[28,811,812],{},"自建還是採購 IdP","：評估後選擇自建，OpenIddict 處理協定細節，帳號與權限模型保留彈性。",[25,815,816,819],{},[28,817,818],{},"OpenIddict 沒有 Back-Channel Logout","：依規格自行實作簽章的 logout token、失敗重送佇列，接收端檢查 token 識別碼防止重放。",[25,821,822,825,826,829],{},[28,823,824],{},"CSP 擋住登入轉址","：加上 ",[33,827,828],{},"form-action"," 限制後，各系統的登入轉址被阻擋，改以中介頁轉址解決。",{"title":158,"searchDepth":159,"depth":159,"links":831},[832,833,834,835],{"id":13,"depth":159,"text":13},{"id":20,"depth":159,"text":20},{"id":78,"depth":159,"text":78},{"id":132,"depth":159,"text":132},"\u002Fworks\u002Fsanring-sso\u002Fcover.png",[838,842,846,850,854],{"src":839,"alt":840,"caption":841},"\u002Fworks\u002Fsanring-sso\u002F01.png","統一登入頁","中英雙語登入頁，支援帳號密碼與 Google 登入，並顯示即將前往的系統",{"src":843,"alt":844,"caption":845},"\u002Fworks\u002Fsanring-sso\u002F02.png","我的系統","帳號中心列出可進入的系統與在各系統的角色",{"src":847,"alt":848,"caption":849},"\u002Fworks\u002Fsanring-sso\u002F03.png","安全性設定","變更密碼、TOTP 雙重驗證與備用碼、Google 帳號綁定",{"src":851,"alt":852,"caption":853},"\u002Fworks\u002Fsanring-sso\u002F04.png","使用者管理","使用者列表，依狀態篩選並顯示在各系統的角色",{"src":855,"alt":856,"caption":857},"\u002Fworks\u002Fsanring-sso\u002F05.png","組織架構圖","部門、主管與成員的組織圖，可拖曳與縮放",[859,860,861],"以 OpenIddict 自建 IdP，並自行實作 OIDC Back-Channel Logout，登出與停權即時跨系統生效","135 個測試涵蓋 50 種安全與流程情境","CI 含 .NET 安全分析、相依弱點掃描、secret 掃描、Semgrep、Trivy 與 OWASP ZAP",{},{},6,"\u002Fworks\u002Fzh-tw\u002Fsanring-sso",{"start":867},"2026-10",[459],{"title":675,"description":158},[463,871,580,720,189],"OpenIddict","works\u002Fzh-TW\u002Fsanring-sso","各系統共用的自建 OIDC 統一登入與帳號權限服務","private","vmVffHH_fTEZlsk89MeBcYLVdBvfOCdrmXFaSxBMgRY","sanring-sso",{"id":878,"title":879,"body":880,"cover":984,"description":158,"draft":166,"extension":167,"featured":168,"gallery":985,"highlights":998,"kind":174,"links":1002,"meta":1003,"navigation":168,"order":1004,"path":1005,"period":1006,"role":1007,"seo":1008,"stack":1009,"status":194,"stem":1011,"summary":1012,"team":197,"visibility":874,"__hash__":1013,"slug":1014,"isFallback":166},"works\u002Fworks\u002Fzh-TW\u002Fsanring-middle-platform.md","Sanring 監控中台",{"type":8,"value":881,"toc":978},[882,884,887,889,921,923,956,958],[11,883,13],{"id":13},[15,885,886],{},"多個網站放在 Cloudflare 後面、Docker 服務分散在多台 VPS，沒有統一的地方看流量、健康度與告警。監控中台把這些集中起來：異常自動告警、自動產生月報，狀態紀錄以統一格式保存一年。",[11,888,20],{"id":20},[22,890,891,897,903,909,915],{},[25,892,893,896],{},[28,894,895],{},"主機與服務健康度","：依 agent 回報判定狀態等級；部署重啟有寬限期避免誤報，長時間未回報即標示離線",[25,898,899,902],{},[28,900,901],{},"Cloudflare 流量","：定期擷取流量數據，偵測流量異常與攻擊，並產生月報",[25,904,905,908],{},[28,906,907],{},"告警","：透過通知佇列寄送 email 並重試；維護時段與靜音設定最長 7 天",[25,910,911,914],{},[28,912,913],{},"外部可用性檢查","：定期檢查 HTTP 狀態、轉址與 SSL 到期日，與 Cloudflare 狀態合併判斷",[25,916,917,920],{},[28,918,919],{},"狀態紀錄","：單一格式、不可修改，依月分區保存",[11,922,78],{"id":78},[22,924,925,930,937,945,951],{},[25,926,927,929],{},[28,928,524],{},"：.NET，分為 API、Agent、共用 DTO 三個專案，API 依功能分資料夾",[25,931,932,934,935],{},[28,933,185],{},"：Angular、Tailwind CSS、ECharts、",[125,936,6],{"href":274},[25,938,939,941,942,944],{},[28,940,670],{},"：透過 ",[125,943,675],{"href":674}," 以 OIDC 登入，後端作為 BFF；所有 API 檢查權限，寫入需 MFA；程式存取使用唯讀 token",[25,946,947,950],{},[28,948,949],{},"測試","：Testcontainers 整合測試與 Playwright E2E",[25,952,953,955],{},[28,954,536],{},"：Docker Compose 部署於單台小型 VPS",[11,957,132],{"id":132},[22,959,960,966,972],{},[25,961,962,965],{},[28,963,964],{},"1 GB 記憶體的 VPS","：只記錄狀態變化、資料降採樣，並為每個容器設定記憶體上限。",[25,967,968,971],{},[28,969,970],{},"兩種狀態來源互相覆蓋","：Cloudflare 判定與外部檢查原本會互相覆寫，改為分開儲存、取較嚴重者顯示。",[25,973,974,977],{},[28,975,976],{},"部署後使用者全被登出","：登入金鑰沒有持久化，改存到 volume。",{"title":158,"searchDepth":159,"depth":159,"links":979},[980,981,982,983],{"id":13,"depth":159,"text":13},{"id":20,"depth":159,"text":20},{"id":78,"depth":159,"text":78},{"id":132,"depth":159,"text":132},"\u002Fworks\u002Fsanring-middle-platform\u002Fcover.png",[986,990,994],{"src":987,"alt":988,"caption":989},"\u002Fworks\u002Fsanring-middle-platform\u002F01.png","監控總覽","網站、主機、服務的狀態統計，近 24 小時請求數與基準線比較，以及最近的狀態變化",{"src":991,"alt":992,"caption":993},"\u002Fworks\u002Fsanring-middle-platform\u002F02.png","網站流量明細","單一網站的健康與攻擊判定、4xx\u002F5xx 次數、回源時間與訪客國家分布",{"src":995,"alt":996,"caption":997},"\u002Fworks\u002Fsanring-middle-platform\u002F03.png","網站列表","從 Cloudflare 收集流量的網站，依狀態篩選並顯示最後收集時間",[999,1000,1001],"每台 VPS 部署獨立的 C# agent 回報健康度，以唯讀方式讀取 Docker 狀態","整合 Cloudflare 流量、外部可用性檢查與告警，集中在單一平台","前端 API 型別由後端 OpenAPI 產生，契約測試防止前後端不一致",{},{},7,"\u002Fworks\u002Fzh-tw\u002Fsanring-middle-platform",{"start":867},[459],{"title":879,"description":158},[463,189,1010,580,720],"ECharts","works\u002Fzh-TW\u002Fsanring-middle-platform","網站流量、VPS 服務健康度與告警通知的監控平台","5rxMl4IBrRE8IN-duvEQOVFnp5Q8nr1lr28Dq1zKT44","sanring-middle-platform",{"id":1016,"title":1017,"body":1018,"cover":1142,"description":158,"draft":166,"extension":167,"featured":168,"gallery":1143,"highlights":1154,"kind":174,"links":1158,"meta":1159,"navigation":168,"order":1160,"path":1161,"period":1162,"role":1163,"seo":1164,"stack":1165,"status":582,"stem":1169,"summary":1170,"team":197,"visibility":874,"__hash__":1171,"slug":1172,"isFallback":166},"works\u002Fworks\u002Fzh-TW\u002Fstream-hub.md","Sanring Stream Hub",{"type":8,"value":1019,"toc":1136},[1020,1022,1025,1028,1030,1062,1064,1108,1110],[11,1021,13],{"id":13},[15,1023,1024],{},"官網要在網頁上播放案場監視器的即時畫面，原本影像收發跟網站綁在一起。Stream Hub 把它拆成獨立服務，部署在一台 Mac mini：案場只負責把畫面送進來，各網站認領自己要播的攝影機，播放權限集中在這裡控管。",[15,1026,1027],{},"一支攝影機只有一路畫面，多個網站認領同一支也不會重複拉流。",[11,1029,20],{"id":20},[22,1031,1032,1038,1044,1050,1056],{},[25,1033,1034,1037],{},[28,1035,1036],{},"接收畫面","：案場以 RTMP／RTSP 推流（需帳密）；NVR 不會推流時，由服務主動拉 RTSP，斷線自動重連",[25,1039,1040,1043],{},[28,1041,1042],{},"轉碼與播放","：H.264 原樣轉出，H.265 才轉成 H.264；瀏覽器以 WebRTC（WHEP）播放，影像不經 Cloudflare",[25,1045,1046,1049],{},[28,1047,1048],{},"認領與授權","：各網站認領可播的攝影機；網站後端用金鑰換短效播放憑證，瀏覽器帶憑證才播得到",[25,1051,1052,1055],{},[28,1053,1054],{},"管理畫面","：查看各攝影機推流狀態、哪個網站正在看哪一路，並提供測試推流與 tunnel 連線檢查",[25,1057,1058,1061],{},[28,1059,1060],{},"掉線通報","：每 30 秒把被認領攝影機的狀態回報給監控中台，由中台判定掉線並寄信",[11,1063,78],{"id":78},[22,1065,1066,1072,1078,1086,1092,1102],{},[25,1067,1068,1071],{},[28,1069,1070],{},"串流","：MediaMTX 收 RTMP／RTSP、以 WebRTC 播放；FFmpeg 負責主動拉流與 H.265 轉碼",[25,1073,1074,1077],{},[28,1075,1076],{},"認領服務","：.NET 10 minimal API 與 SQLite；MediaMTX 每次推流、播放都向它確認授權；管理、授權、網站 API 分開三個埠",[25,1079,1080,1082,1083,1085],{},[28,1081,1054],{},"：Angular、Tailwind CSS、",[125,1084,6],{"href":274},"，跟認領服務建在同一個映像",[25,1087,1088,1091],{},[28,1089,1090],{},"對外連線","：Cloudflare Tunnel 只開放 WebRTC 信令與網站 API，管理畫面只綁本機",[25,1093,1094,31,1097,1099,1100],{},[28,1095,1096],{},"系統整合",[125,1098,603],{"href":802},"的即時監控改由這裡供應畫面；掉線狀態回報",[125,1101,799],{"href":798},[25,1103,1104,1107],{},[28,1105,1106],{},"測試與部署","：xUnit 測試涵蓋授權、拉流、網站 API 與中台回報；arm64 映像推上 Docker Hub，Mac mini 以 Docker Compose 啟動",[11,1109,132],{"id":132},[22,1111,1112,1118,1124,1130],{},[25,1113,1114,1117],{},[28,1115,1116],{},"只靠網站代號擋不住盜播","：改為網站後端用金鑰換憑證，內容含網站、編號與期限並以 HMAC 簽章；MediaMTX 每次播放都回問認領服務，取消認領後新播放立即被拒，正在看的連線也會被中斷。",[25,1119,1120,1123],{},[28,1121,1122],{},"案場的 NVR 不會推流","：認領服務透過 MediaMTX API 動態新增拉流設定，在容器內以 FFmpeg 拉 RTSP；MediaMTX 重啟後每 30 秒自動補回設定。現場兩層 NAT 的轉發只對服務的 IP 開放。",[25,1125,1126,1129],{},[28,1127,1128],{},"瀏覽器不支援 H.265","：只在來源是 H.265 時轉碼，H.264 原樣轉出，降低 Mac mini 的負載。",[25,1131,1132,1135],{},[28,1133,1134],{},"掉線通報不想在 mini 上寄信","：mini 只回報攝影機狀態快照，掉線判定、寄信與靜音規則都交給監控中台，mini 不存寄信用的金鑰。",{"title":158,"searchDepth":159,"depth":159,"links":1137},[1138,1139,1140,1141],{"id":13,"depth":159,"text":13},{"id":20,"depth":159,"text":20},{"id":78,"depth":159,"text":78},{"id":132,"depth":159,"text":132},"\u002Fworks\u002Fstream-hub\u002F01.png",[1144,1147,1151],{"src":1142,"alt":1145,"caption":1146},"管理總覽","MediaMTX 連線、推流中與被拒的路數、正在看的連線數，以及目前播出中的攝影機",{"src":1148,"alt":1149,"caption":1150},"\u002Fworks\u002Fstream-hub\u002F02.png","攝影機列表","各編號的推流狀態與編碼、認領的網站與正在看的連線；拉流與內建測試畫面另有標示",{"src":1152,"alt":996,"caption":1153},"\u002Fworks\u002Fstream-hub\u002F03.png","各網站認領的路數、正在看的連線數與金鑰設定狀態",[1155,1156,1157],"播放憑證以 HMAC 簽章、兩分鐘內有效；網站取消認領後，正在看的連線立即中斷","案場可推流，不會推流的 NVR 改由服務主動拉流；H.265 自動轉成瀏覽器可播的 H.264","第一個案場已上線，掉線狀態回報監控中台統一告警",{},{},8,"\u002Fworks\u002Fzh-tw\u002Fstream-hub",{"start":867},[459],{"title":1017,"description":158},[463,189,1166,1167,1168,465],"MediaMTX","FFmpeg","WebRTC","works\u002Fzh-TW\u002Fstream-hub","收集案場監視器畫面、統一授權後供各網站即時播放的串流服務","NQRW7zWBf0noLTSmCMdUCivhtayaAoFrW8_rDJSckbk","stream-hub",1793972936354]