import { useEffect, useState } from "react"; import type { ImportJobSummary, IntegrationSummary, SessionUser, WebhookDeliverySummary, WebhookSummary } from "@ledger/shared"; import { api } from "../lib/api"; import { resolveDisplayedMcpEndpoint } from "../lib/mcp"; import { PageEditor } from "./PageEditor"; type Space = { id: string; name: string; key: string; visibility: string; }; type AdminFeedback = Array<{ id: string; page_title: string; helpful: boolean; comment: string | null }>; export function DashboardView({ user, spaces }: { user: SessionUser; spaces: Space[] }) { const canAdmin = user.role === "admin" || user.role === "owner"; const canEdit = canAdmin || user.role === "editor"; const [analytics, setAnalytics] = useState<{ topSearches: Array<{ query: string; count: number }>; noResults: Array<{ query: string; count: number }>; } | null>(null); const [feedback, setFeedback] = useState(null); const [integrations, setIntegrations] = useState([]); const [importJobs, setImportJobs] = useState([]); const [webhooks, setWebhooks] = useState([]); const [deliveries, setDeliveries] = useState>({}); const [aiSettings, setAiSettings] = useState({ provider: "none", model: "", isEnabled: false, hasApiKey: false, apiKey: "" }); const [brandingForm, setBrandingForm] = useState({ siteName: "Ledger", logoUrl: "", brandColor: "#245cff", publicKnowledgeBaseEnabled: true }); const [status, setStatus] = useState(null); const [webhookForm, setWebhookForm] = useState({ name: "", targetUrl: "", signingSecret: "", isActive: true, events: "page.created, page.updated" }); const [integrationForms, setIntegrationForms] = useState>({ github: { name: "GitHub", isEnabled: false, token: "", accessToken: "" }, google_docs: { name: "Google Docs", isEnabled: false, token: "", accessToken: "" }, markdown_import: { name: "Markdown import", isEnabled: true, token: "", accessToken: "" } }); const [markdownFiles, setMarkdownFiles] = useState>([]); const [markdownPreview, setMarkdownPreview] = useState | null>(null); const [githubImport, setGithubImport] = useState({ repo: "", branch: "main", path: "" }); const [githubPreview, setGithubPreview] = useState<{ title: string; excerpt?: string } | null>(null); const [googleImport, setGoogleImport] = useState({ documentId: "" }); const [googlePreview, setGooglePreview] = useState<{ title: string; excerpt?: string } | null>(null); const [importTarget, setImportTarget] = useState({ spaceId: spaces[0]?.id ?? "", visibility: "internal", state: "draft" }); const displayedMcpEndpoint = resolveDisplayedMcpEndpoint(); async function loadAdminData() { if (!canAdmin) { return; } const [analyticsResponse, feedbackResponse, settingsResponse, integrationsResponse, jobsResponse, aiResponse, webhookResponse] = await Promise.all([ api.get<{ topSearches: Array<{ query: string; count: number }>; noResults: Array<{ query: string; count: number }> }>("/api/admin/search-analytics"), api.get<{ feedback: AdminFeedback }>("/api/admin/feedback"), api.get<{ branding: { site_name: string; logo_url: string | null; brand_color: string; footer_text: string | null; public_knowledge_base_enabled: boolean } }>("/api/settings/admin"), api.get<{ integrations: IntegrationSummary[] }>("/api/integrations"), api.get<{ jobs: ImportJobSummary[] }>("/api/integrations/import-jobs"), api.get<{ settings: { provider: string; model: string; isEnabled: boolean; hasApiKey: boolean } }>("/api/ai/settings"), api.get<{ webhooks: WebhookSummary[] }>("/api/webhooks") ]); setAnalytics(analyticsResponse); setFeedback(feedbackResponse.feedback); setIntegrations(integrationsResponse.integrations); setImportJobs(jobsResponse.jobs); setAiSettings((current) => ({ ...current, ...aiResponse.settings, apiKey: "" })); setWebhooks(webhookResponse.webhooks); setBrandingForm({ siteName: settingsResponse.branding.site_name, logoUrl: settingsResponse.branding.logo_url ?? "", brandColor: settingsResponse.branding.brand_color, publicKnowledgeBaseEnabled: settingsResponse.branding.public_knowledge_base_enabled }); } useEffect(() => { void loadAdminData(); }, [canAdmin]); async function saveBranding() { await api.put("/api/settings/branding", { siteName: brandingForm.siteName, logoUrl: brandingForm.logoUrl || null, brandColor: brandingForm.brandColor, publicKnowledgeBaseEnabled: brandingForm.publicKnowledgeBaseEnabled }); setStatus("Workspace settings saved."); } async function saveIntegration(provider: "github" | "google_docs" | "markdown_import") { const form = integrationForms[provider]; const config = provider === "github" ? { token: form.token } : provider === "google_docs" ? { accessToken: form.accessToken } : {}; await api.put(`/api/integrations/${provider}`, { name: form.name, isEnabled: form.isEnabled, config }); setStatus(`${provider} settings saved.`); await loadAdminData(); } async function loadDeliveries(webhookId: string) { const response = await api.get<{ deliveries: WebhookDeliverySummary[] }>(`/api/webhooks/${webhookId}/deliveries`); setDeliveries((current) => ({ ...current, [webhookId]: response.deliveries })); } async function createWebhook() { await api.post("/api/webhooks", { ...webhookForm, events: webhookForm.events.split(",").map((value) => value.trim()).filter(Boolean) }); setStatus("Webhook created."); await loadAdminData(); } async function deleteWebhook(webhookId: string) { if (!window.confirm("Delete this webhook and its recent deliveries?")) return; await api.delete(`/api/webhooks/${webhookId}`); setStatus("Webhook deleted."); await loadAdminData(); } async function saveAi() { await api.put("/api/ai/settings", { provider: aiSettings.provider, model: aiSettings.model, apiKey: aiSettings.apiKey || null, isEnabled: aiSettings.isEnabled }); setStatus("AI settings saved."); await loadAdminData(); } async function handleMarkdownFiles(fileList: FileList | null) { if (!fileList) return; const files = await Promise.all( Array.from(fileList).map( (file) => new Promise<{ fileName: string; content: string }>((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve({ fileName: file.name, content: String(reader.result ?? "") }); reader.onerror = () => reject(reader.error); reader.readAsText(file); }) ) ); setMarkdownFiles(files); const response = await api.post<{ documents: Array<{ title: string; excerpt?: string }> }>("/api/integrations/markdown/preview", { files }); setMarkdownPreview(response.documents); } async function runMarkdownImport() { await api.post("/api/integrations/markdown/import", { files: markdownFiles, target: importTarget }); setStatus("Markdown import completed."); await loadAdminData(); } async function previewGithubImport() { const response = await api.post<{ document: { title: string; excerpt?: string } }>("/api/integrations/github/preview", githubImport); setGithubPreview(response.document); } async function runGithubImport() { await api.post("/api/integrations/github/import", { ...githubImport, target: importTarget }); setStatus("GitHub import completed."); await loadAdminData(); } async function previewGoogleImport() { const response = await api.post<{ document: { title: string; excerpt?: string } }>("/api/integrations/google-docs/preview", googleImport); setGooglePreview(response.document); } async function runGoogleImport() { await api.post("/api/integrations/google-docs/import", { ...googleImport, target: importTarget }); setStatus("Google Docs import completed."); await loadAdminData(); } return (

Manage workspace

{user.displayName}

Publish docs, import trusted sources, configure AI, and control integrations with an audit-friendly admin surface.

{user.role} {spaces.length} collections
{status ?

{status}

: null}
{canEdit ? : null}

Imports

Bring documentation into Ledger

Browser note

Ledger supports multiple Markdown files in the browser. Folder upload support depends on the browser, so multiple-file import is the reliable default.

Markdown

void handleMarkdownFiles(event.target.files)} /> {markdownPreview ? (
{markdownPreview.map((document) => (
{document.title}

{document.excerpt ?? "No excerpt available."}

))}
) : null}

GitHub

{githubPreview ?
{githubPreview.title}

{githubPreview.excerpt ?? "No excerpt available."}

: null}

Google Docs

{googlePreview ?
{googlePreview.title}

{googlePreview.excerpt ?? "No excerpt available."}

: null}
{canAdmin ? ( <>

Branding

Workspace settings

Footer Powered by Ledger made by ANord.cc

Integrations

Provider configuration

{(["github", "google_docs", "markdown_import"] as const).map((provider) => { const currentIntegration = integrations.find((integration) => integration.provider === provider); const form = integrationForms[provider]; return (
{provider.replace("_", " ")} {currentIntegration?.statusMessage ?? "Not configured"}
{provider === "github" ? ( ) : null} {provider === "google_docs" ? ( ) : null}
); })}

Webhooks

Signed event delivery

Ledger signs `timestamp.body` with HMAC SHA-256 and sends `X-Ledger-Event`, `X-Ledger-Timestamp`, and `X-Ledger-Signature` headers.

{webhooks.map((webhook) => (
{webhook.name} {webhook.isActive ? "Active" : "Disabled"}

{webhook.targetUrl}

{webhook.events.join(", ")}

{deliveries[webhook.id]?.length ? (
{deliveries[webhook.id].map((delivery) => (
{delivery.eventName} {delivery.responseStatus ?? "pending"} {delivery.success ? "Success" : delivery.errorMessage ?? "Failed"}
))}
) : null}
))}

AI settings

Permissions-aware answers

AI uses the same page visibility rules as search and document reads, so restricted content is never exposed to the wrong user.

MCP

Ledger MCP server

Endpoint: {displayedMcpEndpoint}

Tools: `search_knowledge_base`, `read_page`, `list_spaces`, `get_page_metadata`, `create_draft_page`

Auth: same session context as the web app.

Import history

Recent jobs

{importJobs.map((job) => (
{job.sourceLabel} {job.status}

{job.provider} • {job.importedCount} pages

{job.errorMessage ?

{job.errorMessage}

: null}
))}
{analytics ? (

Analytics

Search performance

Top searches {analytics.topSearches.map((item) => (

{item.query} ({item.count})

))}
No results {analytics.noResults.map((item) => (

{item.query} ({item.count})

))}
) : null} {feedback ? (

Feedback queue

Reader responses

{feedback.map((item) => (
{item.page_title} {item.helpful ? "Helpful" : "Not helpful"}

{item.comment ?? "No comment provided."}

))}
) : null} ) : null}
); }