Files
Mebbling/app/dashboard/page.tsx
T

36 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
</>;
}