2026-07-19 01:12:16 +08:00
import { redirect } from "next/navigation" ;
import { getSession } from "@/lib/auth" ;
import { db } from "@/lib/db" ;
import { PublishForm } from "./publish-form" ;
import { WebhookControl } from "./webhook-control" ;
type Source = { id : number ; name : string ; base_url : string ; sync_status : string ; last_synced_at : string | null ; last_error : string | null ; webhook_secret_hash : string | null ; last_webhook_at : string | null ; owner_id : number ; is_enabled : number ; disabled_at : string | null };
type Job = { id : number ; kind : string ; trigger : string | null ; status : string ; attempts : number ; last_error : string | null ; created_at : string ; finished_at : string | null };
export const dynamic = "force-dynamic" ;
export default async function Dashboard ({ searchParams } : { searchParams : Promise < { error? : string ; source? : string ; sync? : string } > }) {
const query = await searchParams ; const user = await getSession (); if ( ! user ) redirect ( "/login" );
const sourceRows = db . prepare ( "SELECT s.id,s.name,s.base_url,s.sync_status,s.last_synced_at,s.last_error,s.webhook_secret_hash,s.last_webhook_at,s.user_id AS owner_id,s.is_enabled,s.disabled_at FROM sources s JOIN source_members sm ON sm.source_id=s.id WHERE sm.user_id=? ORDER BY s.id DESC" ). all ( user . id ) as Source [];
const sources = sourceRows . map (( source ) => ({ ... source , members : db.prepare ( "SELECT u.username,u.id,sm.role FROM source_members sm JOIN users u ON u.id=sm.user_id WHERE sm.source_id=? ORDER BY sm.role DESC,u.username" ). all ( source . id ) as { username : string ; id : number ; role : string }[], jobs : db.prepare ( "SELECT id,kind,trigger,status,attempts,last_error,created_at,finished_at FROM sync_jobs WHERE source_id=? ORDER BY id DESC LIMIT 5" ). all ( source . id ) as Job [] }));
const publishSources = sources . filter (( source ) => source . is_enabled );
return <>
< h1 > 控制台 </ h1 >
{ query . error && < p className = "error" >{ query . error }</ p >}
{ query . source === "shared" ? < p > 你已加入既有的共享 Memos 來源,不會重複同步貼文。 </ p > : query . source === "updated" ? < p > 來源設定已更新。 </ p > : query . source && < p > 來源已連接,首次同步已排入佇列。 </ p >}
{ query . sync === "queued" && < p > 同步已排入佇列。 </ p >}{ query . sync === "already-queued" && < p className = "muted" > 此來源已有同步工作處理中,不重複排入。 </ p >}
< section className = "card" >< h2 > 發佈到自己的 Memos </ h2 >{ publishSources . length ? < PublishForm sources = { publishSources } /> : < p className = "muted" > 請先連接並啟用一個 Memos 來源。 </ p >}</ section >
< section className = "card" >< h2 > 連接 Memos </ h2 >< form action = "/api/sources" method = "post" >< label > 顯示名稱 < input name = "name" required placeholder = "我的 Memos" /></ label >< label > Memos 網址 < input name = "baseUrl" type = "url" required placeholder = "https://memos.example.com" /></ label >< label > Personal Access Token < input name = "token" type = "password" required /></ label >< button > 驗證並連接 </ button ></ form >< p className = "muted" > Token 會使用伺服器金鑰加密保存。同一個 Memos 帳號與網址會自動共用來源,不會建立重複貼文。 </ p ></ section >
< section >< h2 > 已連接來源 </ h2 >{ sources . map (( source ) => < article className = "card" key = { source . id }>
< div className = "space" >< strong >{ source . name }</ strong >< span className = "tag" >{ source . is_enabled ? source . sync_status : "disabled" }</ span ></ div >
< p className = "meta" > 來源 ID : { source . id }< br />{ source . base_url }< br /> 成員: { source . members . map (( member ) => ` ${ member . username }${ member . role === "owner" ? "(建立者)" : "" } ` ). join ( "、" )}< br /> 上次同步: { source . last_synced_at || "尚未完成" }< br /> Webhook : { source . webhook_secret_hash ? ( source . last_webhook_at ? `最近收到: ${ new Date ( source . last_webhook_at + "Z" ). toLocaleString ( "zh-TW" ) } ` : "已建立 URL,尚未收到呼叫" ) : "尚未建立 URL" }{ ! source . is_enabled && <>< br /> 已停用: { source . disabled_at ? new Date ( source . disabled_at + "Z" ). toLocaleString ( "zh-TW" ) : "是" }</>}{ source . last_error && <>< br />< span className = "error" >{ source . last_error }</ span ></>}</ p >
{ source . owner_id === user . id ? <>
< WebhookControl sourceId = { source . id } configured = { Boolean ( source . webhook_secret_hash )} />
< details >< summary > 來源管理 </ summary >< form action = { `/api/sources/ ${ source . id } /manage` } method = "post" >< input type = "hidden" name = "action" value = "rename" />< label > 顯示名稱 < input name = "name" defaultValue = { source . name } required maxLength = { 80 } /></ label >< button > 儲存名稱 </ button ></ form >< form action = { `/api/sources/ ${ source . id } /manage` } method = "post" >< input type = "hidden" name = "action" value = "set-enabled" />< input type = "hidden" name = "enabled" value = { source . is_enabled ? "0" : "1" } />< button className = { source . is_enabled ? "danger" : "" }>{ source . is_enabled ? "停用來源" : "啟用來源" }</ button ></ form >{ source . members . length > 1 && < form action = { `/api/sources/ ${ source . id } /manage` } method = "post" >< input type = "hidden" name = "action" value = "transfer" />< label > 轉移建立者 < select name = "username" required defaultValue = "" > < option value = "" disabled > 選擇成員 </ option >{ source . members . filter (( member ) => member . id !== user . id ). map (( member ) => < option key = { member . id } value = { member . username }>{ member . username }</ option >)}</ select ></ label >< button > 轉移所有權 </ button ></ form >}< form action = { `/api/sources/ ${ source . id } /manage` } method = "post" >< input type = "hidden" name = "action" value = "delete" />< button className = "danger" > 刪除來源與遠端鏡像貼文 </ button ></ form ></ details >
</> : < form action = { `/api/sources/ ${ source . id } /manage` } method = "post" >< input type = "hidden" name = "action" value = "leave" />< button className = "danger" > 離開共享來源 </ button ></ form >}
< form action = "/api/sync" method = "post" >< input type = "hidden" name = "sourceId" value = { source . id } />< button disabled = { ! source . is_enabled }>{ source . is_enabled ? "立即同步" : "來源已停用" }</ button ></ form >
< details >< summary > 最近同步工作 </ summary >{ source . jobs . length ? < ul className = "job-list" >{ source . jobs . map (( job ) => < li key = { job . id }>< strong >{ job . kind }</ strong > · { job . trigger || "legacy" } · < span className = "tag" >{ job . status }</ span > · 嘗試 { job . attempts } 次 < br />< span className = "meta" > 建立: { new Date ( job . created_at + "Z" ). toLocaleString ( "zh-TW" )}{ job . finished_at && `;完成: ${ new Date ( job . finished_at + "Z" ). toLocaleString ( "zh-TW" ) } ` }</ span >{ job . last_error && <>< br />< span className = "error" >{ job . last_error }</ span ></>}</ li >)}</ ul > : < p className = "muted" > 尚無同步工作。 </ p >}</ details >
</ article >)}</ section >
</>;
}