NobGit
public anord read

Ledger

Why work hard when you can work easier?

Languages

Repository composition by tracked source files.

TypeScript
TypeScript 86% CSS 10% SQL 3% Shell 1% HTML 0%
Create file Wiki Documentation
Clone
https://nobgit.com/orgs/anord/ledger.git
ssh://[email protected]:2222/orgs/anord/ledger.git
DashboardView.tsx
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<AdminFeedback | null>(null);
  const [integrations, setIntegrations] = useState<IntegrationSummary[]>([]);
  const [importJobs, setImportJobs] = useState<ImportJobSummary[]>([]);
  const [webhooks, setWebhooks] = useState<WebhookSummary[]>([]);
  const [deliveries, setDeliveries] = useState<Record<string, WebhookDeliverySummary[]>>({});
  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<string | null>(null);
  const [webhookForm, setWebhookForm] = useState({
    name: "",
    targetUrl: "",
    signingSecret: "",
    isActive: true,
    events: "page.created, page.updated"
  });
  const [integrationForms, setIntegrationForms] = useState<Record<string, { name: string; isEnabled: boolean; token: string; accessToken: string }>>({
    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<Array<{ fileName: string; content: string }>>([]);
  const [markdownPreview, setMarkdownPreview] = useState<Array<{ title: string; excerpt?: string }> | 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 (
    <div className="manage-layout">
      <section className="panel manage-hero">
        <div>
          <p className="eyebrow">Manage workspace</p>
          <h1>{user.displayName}</h1>
          <p className="lede">Publish docs, import trusted sources, configure AI, and control integrations with an audit-friendly admin surface.</p>
        </div>
        <div className="manage-hero__meta">
          <span className="pill">{user.role}</span>
          <span className="pill">{spaces.length} collections</span>
        </div>
      </section>

      {status ? <p className="muted">{status}</p> : null}

      <div className="manage-grid">
        {canEdit ? <PageEditor spaces={spaces} /> : null}

        <section className="panel" id="imports">
          <div className="panel__header">
            <div>
              <p className="eyebrow">Imports</p>
              <h3>Bring documentation into Ledger</h3>
            </div>
          </div>
          <div className="field-grid">
            <label className="field">
              Target collection
              <select value={importTarget.spaceId} onChange={(event) => setImportTarget((current) => ({ ...current, spaceId: event.target.value }))}>
                {spaces.map((space) => (
                  <option key={space.id} value={space.id}>
                    {space.name}
                  </option>
                ))}
              </select>
            </label>
            <label className="field">
              State
              <select value={importTarget.state} onChange={(event) => setImportTarget((current) => ({ ...current, state: event.target.value }))}>
                <option value="draft">Draft</option>
                <option value="published">Published</option>
              </select>
            </label>
          </div>
          <div className="field-grid">
            <label className="field">
              Visibility
              <select value={importTarget.visibility} onChange={(event) => setImportTarget((current) => ({ ...current, visibility: event.target.value }))}>
                <option value="internal">Internal</option>
                <option value="public">Public</option>
                <option value="restricted">Restricted</option>
              </select>
            </label>
            <div className="field field-note">
              Browser note
              <p className="muted">Ledger supports multiple Markdown files in the browser. Folder upload support depends on the browser, so multiple-file import is the reliable default.</p>
            </div>
          </div>

          <div className="import-grid">
            <section className="subpanel">
              <h4>Markdown</h4>
              <input type="file" accept=".md,.markdown,text/markdown" multiple onChange={(event) => void handleMarkdownFiles(event.target.files)} />
              {markdownPreview ? (
                <div className="preview-list">
                  {markdownPreview.map((document) => (
                    <div key={document.title} className="preview-item">
                      <strong>{document.title}</strong>
                      <p>{document.excerpt ?? "No excerpt available."}</p>
                    </div>
                  ))}
                  <button onClick={runMarkdownImport}>Import Markdown</button>
                </div>
              ) : null}
            </section>

            <section className="subpanel">
              <h4>GitHub</h4>
              <label className="field">
                Repository
                <input value={githubImport.repo} onChange={(event) => setGithubImport((current) => ({ ...current, repo: event.target.value }))} placeholder="org/repo" />
              </label>
              <div className="field-grid">
                <label className="field">
                  Branch
                  <input value={githubImport.branch} onChange={(event) => setGithubImport((current) => ({ ...current, branch: event.target.value }))} />
                </label>
                <label className="field">
                  Path
                  <input value={githubImport.path} onChange={(event) => setGithubImport((current) => ({ ...current, path: event.target.value }))} placeholder="docs/getting-started.md" />
                </label>
              </div>
              <div className="panel__footer">
                <button className="button-secondary" onClick={previewGithubImport}>Preview</button>
                <button onClick={runGithubImport}>Import</button>
              </div>
              {githubPreview ? <div className="preview-item"><strong>{githubPreview.title}</strong><p>{githubPreview.excerpt ?? "No excerpt available."}</p></div> : null}
            </section>

            <section className="subpanel">
              <h4>Google Docs</h4>
              <label className="field">
                Document ID
                <input value={googleImport.documentId} onChange={(event) => setGoogleImport({ documentId: event.target.value })} placeholder="Google Doc ID" />
              </label>
              <div className="panel__footer">
                <button className="button-secondary" onClick={previewGoogleImport}>Preview</button>
                <button onClick={runGoogleImport}>Import</button>
              </div>
              {googlePreview ? <div className="preview-item"><strong>{googlePreview.title}</strong><p>{googlePreview.excerpt ?? "No excerpt available."}</p></div> : null}
            </section>
          </div>
        </section>

        {canAdmin ? (
          <>
            <section className="panel" id="settings">
              <div className="panel__header">
                <div>
                  <p className="eyebrow">Branding</p>
                  <h3>Workspace settings</h3>
                </div>
              </div>
              <label className="field">
                Site name
                <input value={brandingForm.siteName} onChange={(event) => setBrandingForm((current) => ({ ...current, siteName: event.target.value }))} />
              </label>
              <div className="field-grid">
                <label className="field">
                  Brand color
                  <input value={brandingForm.brandColor} onChange={(event) => setBrandingForm((current) => ({ ...current, brandColor: event.target.value }))} />
                </label>
                <label className="field">
                  Logo URL
                  <input value={brandingForm.logoUrl} onChange={(event) => setBrandingForm((current) => ({ ...current, logoUrl: event.target.value }))} />
                </label>
              </div>
              <div className="list-item">
                <strong>Footer</strong>
                <span>Powered by Ledger made by ANord.cc</span>
              </div>
              <label className="checkbox-row checkbox-card">
                <input type="checkbox" checked={brandingForm.publicKnowledgeBaseEnabled} onChange={(event) => setBrandingForm((current) => ({ ...current, publicKnowledgeBaseEnabled: event.target.checked }))} />
                <span>Public knowledge base enabled</span>
              </label>
              <div className="panel__footer">
                <button onClick={saveBranding}>Save settings</button>
              </div>
            </section>

            <section className="panel" id="integrations">
              <div className="panel__header">
                <div>
                  <p className="eyebrow">Integrations</p>
                  <h3>Provider configuration</h3>
                </div>
              </div>
              <div className="integration-list">
                {(["github", "google_docs", "markdown_import"] as const).map((provider) => {
                  const currentIntegration = integrations.find((integration) => integration.provider === provider);
                  const form = integrationForms[provider];
                  return (
                    <div key={provider} className="integration-card">
                      <div className="feedback-item__header">
                        <strong>{provider.replace("_", " ")}</strong>
                        <span className={`badge badge-${currentIntegration?.status === "configured" ? "public" : "restricted"}`}>
                          {currentIntegration?.statusMessage ?? "Not configured"}
                        </span>
                      </div>
                      <label className="field">
                        Name
                        <input value={form.name} onChange={(event) => setIntegrationForms((current) => ({ ...current, [provider]: { ...current[provider], name: event.target.value } }))} />
                      </label>
                      {provider === "github" ? (
                        <label className="field">
                          GitHub token
                          <input type="password" value={form.token} onChange={(event) => setIntegrationForms((current) => ({ ...current, [provider]: { ...current[provider], token: event.target.value } }))} />
                        </label>
                      ) : null}
                      {provider === "google_docs" ? (
                        <label className="field">
                          Google access token
                          <input type="password" value={form.accessToken} onChange={(event) => setIntegrationForms((current) => ({ ...current, [provider]: { ...current[provider], accessToken: event.target.value } }))} />
                        </label>
                      ) : null}
                      <label className="checkbox-row checkbox-card">
                        <input type="checkbox" checked={form.isEnabled} onChange={(event) => setIntegrationForms((current) => ({ ...current, [provider]: { ...current[provider], isEnabled: event.target.checked } }))} />
                        <span>Enable {provider.replace("_", " ")}</span>
                      </label>
                      <div className="panel__footer">
                        <button onClick={() => saveIntegration(provider)}>Save integration</button>
                      </div>
                    </div>
                  );
                })}
              </div>
            </section>

            <section className="panel" id="webhooks">
              <div className="panel__header">
                <div>
                  <p className="eyebrow">Webhooks</p>
                  <h3>Signed event delivery</h3>
                </div>
              </div>
              <div className="field-grid">
                <label className="field">
                  Name
                  <input value={webhookForm.name} onChange={(event) => setWebhookForm((current) => ({ ...current, name: event.target.value }))} />
                </label>
                <label className="field">
                  Target URL
                  <input value={webhookForm.targetUrl} onChange={(event) => setWebhookForm((current) => ({ ...current, targetUrl: event.target.value }))} />
                </label>
              </div>
              <label className="field">
                Signing secret
                <input value={webhookForm.signingSecret} onChange={(event) => setWebhookForm((current) => ({ ...current, signingSecret: event.target.value }))} />
              </label>
              <label className="field">
                Events
                <input value={webhookForm.events} onChange={(event) => setWebhookForm((current) => ({ ...current, events: event.target.value }))} />
              </label>
              <label className="checkbox-row checkbox-card">
                <input type="checkbox" checked={webhookForm.isActive} onChange={(event) => setWebhookForm((current) => ({ ...current, isActive: event.target.checked }))} />
                <span>Active</span>
              </label>
              <p className="muted">Ledger signs `timestamp.body` with HMAC SHA-256 and sends `X-Ledger-Event`, `X-Ledger-Timestamp`, and `X-Ledger-Signature` headers.</p>
              <div className="panel__footer">
                <button onClick={createWebhook}>Create webhook</button>
              </div>
              <div className="feedback-list">
                {webhooks.map((webhook) => (
                  <div key={webhook.id} className="feedback-item">
                    <div className="feedback-item__header">
                      <strong>{webhook.name}</strong>
                      <span className={`badge badge-${webhook.isActive ? "public" : "restricted"}`}>{webhook.isActive ? "Active" : "Disabled"}</span>
                    </div>
                    <p>{webhook.targetUrl}</p>
                    <p className="muted">{webhook.events.join(", ")}</p>
                    <div className="panel__footer">
                      <button className="button-secondary" onClick={() => loadDeliveries(webhook.id)}>Load deliveries</button>
                      <button className="button-secondary" onClick={() => deleteWebhook(webhook.id)}>Delete</button>
                    </div>
                    {deliveries[webhook.id]?.length ? (
                      <div className="delivery-list">
                        {deliveries[webhook.id].map((delivery) => (
                          <div key={delivery.id} className="delivery-item">
                            <strong>{delivery.eventName}</strong>
                            <span>{delivery.responseStatus ?? "pending"}</span>
                            <span>{delivery.success ? "Success" : delivery.errorMessage ?? "Failed"}</span>
                          </div>
                        ))}
                      </div>
                    ) : null}
                  </div>
                ))}
              </div>
            </section>

            <section className="panel" id="ai">
              <div className="panel__header">
                <div>
                  <p className="eyebrow">AI settings</p>
                  <h3>Permissions-aware answers</h3>
                </div>
              </div>
              <div className="field-grid">
                <label className="field">
                  Provider
                  <select value={aiSettings.provider} onChange={(event) => setAiSettings((current) => ({ ...current, provider: event.target.value }))}>
                    <option value="none">Disabled</option>
                    <option value="openai_compatible">OpenAI-compatible</option>
                    <option value="anthropic_compatible">Anthropic-compatible</option>
                  </select>
                </label>
                <label className="field">
                  Model
                  <input value={aiSettings.model} onChange={(event) => setAiSettings((current) => ({ ...current, model: event.target.value }))} />
                </label>
              </div>
              <label className="field">
                API key
                <input type="password" value={aiSettings.apiKey} onChange={(event) => setAiSettings((current) => ({ ...current, apiKey: event.target.value }))} placeholder={aiSettings.hasApiKey ? "Configured. Enter a new key to rotate it." : "Paste provider API key"} />
              </label>
              <label className="checkbox-row checkbox-card">
                <input type="checkbox" checked={aiSettings.isEnabled} onChange={(event) => setAiSettings((current) => ({ ...current, isEnabled: event.target.checked }))} />
                <span>Enable AI answers</span>
              </label>
              <p className="muted">AI uses the same page visibility rules as search and document reads, so restricted content is never exposed to the wrong user.</p>
              <div className="panel__footer">
                <button onClick={saveAi}>Save AI settings</button>
              </div>
            </section>

            <section className="panel" id="mcp">
              <div className="panel__header">
                <div>
                  <p className="eyebrow">MCP</p>
                  <h3>Ledger MCP server</h3>
                </div>
              </div>
              <p className="muted">Endpoint: {displayedMcpEndpoint}</p>
              <p className="muted">Tools: `search_knowledge_base`, `read_page`, `list_spaces`, `get_page_metadata`, `create_draft_page`</p>
              <p className="muted">Auth: same session context as the web app.</p>
            </section>

            <section className="panel">
              <div className="panel__header">
                <div>
                  <p className="eyebrow">Import history</p>
                  <h3>Recent jobs</h3>
                </div>
              </div>
              <div className="feedback-list">
                {importJobs.map((job) => (
                  <div key={job.id} className="feedback-item">
                    <div className="feedback-item__header">
                      <strong>{job.sourceLabel}</strong>
                      <span className={`badge badge-${job.status === "completed" ? "public" : "restricted"}`}>{job.status}</span>
                    </div>
                    <p>{job.provider} • {job.importedCount} pages</p>
                    {job.errorMessage ? <p className="muted">{job.errorMessage}</p> : null}
                  </div>
                ))}
              </div>
            </section>

            {analytics ? (
              <section className="panel">
                <div className="panel__header">
                  <div>
                    <p className="eyebrow">Analytics</p>
                    <h3>Search performance</h3>
                  </div>
                </div>
                <div className="analytics-grid">
                  <div>
                    <strong>Top searches</strong>
                    {analytics.topSearches.map((item) => (
                      <p key={item.query}>{item.query} <span className="muted">({item.count})</span></p>
                    ))}
                  </div>
                  <div>
                    <strong>No results</strong>
                    {analytics.noResults.map((item) => (
                      <p key={item.query}>{item.query} <span className="muted">({item.count})</span></p>
                    ))}
                  </div>
                </div>
              </section>
            ) : null}

            {feedback ? (
              <section className="panel">
                <div className="panel__header">
                  <div>
                    <p className="eyebrow">Feedback queue</p>
                    <h3>Reader responses</h3>
                  </div>
                </div>
                <div className="feedback-list">
                  {feedback.map((item) => (
                    <div key={item.id} className="feedback-item">
                      <div className="feedback-item__header">
                        <strong>{item.page_title}</strong>
                        <span className={`badge ${item.helpful ? "badge-public" : "badge-restricted"}`}>
                          {item.helpful ? "Helpful" : "Not helpful"}
                        </span>
                      </div>
                      <p>{item.comment ?? "No comment provided."}</p>
                    </div>
                  ))}
                </div>
              </section>
            ) : null}
          </>
        ) : null}
      </div>
    </div>
  );
}