diff --git a/README.md b/README.md index d723d003..e283a477 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ https://wiki.kinic.xyz The official Kinic Wiki database is: -https://wiki.kinic.xyz/db_kva4v2twg6jv/Wiki?read=anonymous +https://wiki.kinic.xyz/db_kva4v2twg6jv/Wiki Database ID: diff --git a/extensions/wiki-clipper/scripts/check-candid-drift.mjs b/extensions/wiki-clipper/scripts/check-candid-drift.mjs index 797600a9..de52b589 100644 --- a/extensions/wiki-clipper/scripts/check-candid-drift.mjs +++ b/extensions/wiki-clipper/scripts/check-candid-drift.mjs @@ -76,11 +76,6 @@ const expectedTypes = { WriteNodeResult: { kind: "record", fields: { created: "bool", node: "NodeMutationAck" } }, WriteSourceForGenerationResult: { kind: "record", fields: { write: "WriteNodeResult", session_nonce: "text" } } }; -const actorExpectedTypes = { - ...expectedTypes, - DatabaseStatus: { kind: "variant", fields: { Hot: "null", Pending: "null", Active: "null", Restoring: "null", Archiving: "null", Archived: "null" } } -}; - const expectedMethods = { authorize_url_ingest_trigger_session: { input: ["UrlIngestTriggerSessionRequest"], output: "ResultUnit", mode: "update" }, get_cycles_billing_config: { input: [], output: "ResultCyclesBillingConfig", mode: "query" }, @@ -99,7 +94,7 @@ const actorMethods = parseActorMethods(actor); for (const [name, shape] of Object.entries(expectedTypes)) { assert.deepEqual(canonicalTypeShape(didTypes[name]), shape, `vfs.did type drift: ${name}`); - assert.deepEqual(actorTypes[name], actorExpectedTypes[name], `extension IDL type drift: ${name}`); + assert.deepEqual(actorTypes[name], shape, `extension IDL type drift: ${name}`); } for (const [name, shape] of Object.entries(expectedMethods)) { @@ -148,7 +143,7 @@ function parseDidMethods(source) { function parseActorTypes(source) { const result = {}; - for (const [name, shape] of Object.entries(actorExpectedTypes)) { + for (const [name, shape] of Object.entries(expectedTypes)) { const initializer = source.match(new RegExp(`const\\s+${name}\\s*=\\s*idl\\.(Record|Variant)\\(\\{([^]*?)\\}\\);`, "m")); assert.ok(initializer, `extension IDL type missing: ${name}`); const kind = initializer[1] === "Record" ? "record" : "variant"; diff --git a/extensions/wiki-clipper/src/vfs-actor.js b/extensions/wiki-clipper/src/vfs-actor.js index 0a64b7f5..664668e9 100644 --- a/extensions/wiki-clipper/src/vfs-actor.js +++ b/extensions/wiki-clipper/src/vfs-actor.js @@ -17,7 +17,6 @@ export async function createVfsActor({ canisterId, host, identity }) { function idlFactory({ IDL: idl }) { const DatabaseRole = idl.Variant({ Reader: idl.Null, Writer: idl.Null, Owner: idl.Null }); const DatabaseStatus = idl.Variant({ - Hot: idl.Null, Pending: idl.Null, Active: idl.Null, Restoring: idl.Null, @@ -170,8 +169,7 @@ function normalizeDatabaseSummary(raw) { } function normalizeDatabaseStatus(status) { - const key = variantKey(status); - return key === "Hot" ? "Active" : key; + return variantKey(status); } export async function getCyclesBillingConfigOrNull(actor) { diff --git a/wikibrowser/app/app-header.tsx b/wikibrowser/app/app-header.tsx index 5344f611..936aee32 100644 --- a/wikibrowser/app/app-header.tsx +++ b/wikibrowser/app/app-header.tsx @@ -3,7 +3,6 @@ // Where: root wikibrowser layout. // What: renders the shared dashboard/cycles header with wallet and II controls. // Why: funding pages should keep the same wallet session and management shell. -import Link from "next/link"; import { usePathname } from "next/navigation"; import { AdminHeader } from "@/components/admin-header"; import { formatTokenAmountFromE8s } from "@/lib/kinic-amount"; @@ -21,7 +20,6 @@ export function AppHeader() { login, logout, principal, - refreshAuth, wallet, walletBalance, walletBalanceLoading, @@ -29,7 +27,7 @@ export function AppHeader() { walletControlsLocked } = useAppSession(); - if (pathname !== "/" && pathname !== "/cycles") return null; + if (pathname !== "/dashboard" && pathname !== "/cycles") return null; const title = pathname === "/cycles" ? "Database cycles purchase" : "Database dashboard"; const connectedWalletLabel = wallet ? `${walletLabel(wallet.provider)} ${shortPrincipal(connectedWalletPrincipal(wallet))}` : null; @@ -40,13 +38,6 @@ export function AppHeader() {
- Database dashboard - - ) : null - } actions={ <> { void logout(); }} - onRefresh={() => { - void refreshAuth(); - }} /> } diff --git a/wikibrowser/app/app-session-provider.tsx b/wikibrowser/app/app-session-provider.tsx index b0582a8e..e48879b2 100644 --- a/wikibrowser/app/app-session-provider.tsx +++ b/wikibrowser/app/app-session-provider.tsx @@ -14,7 +14,6 @@ type AppSessionContext = { authError: string | null; authLoading: boolean; authReady: boolean; - authRefreshSeq: number; principal: string | null; wallet: ConnectedKinicWallet | null; walletBalance: string | null; @@ -26,7 +25,6 @@ type AppSessionContext = { disconnectWallet: (provider: HeaderWalletProvider) => void; logout: () => Promise; login: () => Promise; - refreshAuth: () => Promise; refreshWalletBalance: (wallet: ConnectedKinicWallet) => Promise; setWalletControlsLocked: (locked: boolean) => void; }; @@ -41,7 +39,6 @@ export function AppSessionProvider({ children }: { children: ReactNode }) { const [authError, setAuthError] = useState(null); const [authLoading, setAuthLoading] = useState(true); const [authReady, setAuthReady] = useState(false); - const [authRefreshSeq, setAuthRefreshSeq] = useState(0); const [principal, setPrincipal] = useState(null); const [wallet, setWallet] = useState(() => readStoredWallet()); const [walletBalance, setWalletBalance] = useState(null); @@ -134,22 +131,8 @@ export function AppSessionProvider({ children }: { children: ReactNode }) { const syncAuth = useCallback(async (client: AuthClient) => { const authenticated = await client.isAuthenticated(); setPrincipal(authenticated ? client.getIdentity().getPrincipal().toText() : null); - setAuthRefreshSeq((current) => current + 1); }, []); - const refreshAuth = useCallback(async () => { - if (!authClient) return; - setAuthLoading(true); - setAuthError(null); - try { - await syncAuth(authClient); - } catch (cause) { - setAuthError(errorMessage(cause)); - } finally { - setAuthLoading(false); - } - }, [authClient, syncAuth]); - const login = useCallback(async () => { if (!authClient) return; setAuthLoading(true); @@ -173,7 +156,6 @@ export function AppSessionProvider({ children }: { children: ReactNode }) { try { await authClient.logout(); setPrincipal(null); - setAuthRefreshSeq((current) => current + 1); clearWallet(); } catch (cause) { setAuthError(errorMessage(cause)); @@ -225,7 +207,6 @@ export function AppSessionProvider({ children }: { children: ReactNode }) { authError, authLoading, authReady, - authRefreshSeq, principal, wallet, walletBalance, @@ -237,7 +218,6 @@ export function AppSessionProvider({ children }: { children: ReactNode }) { disconnectWallet, login, logout, - refreshAuth, refreshWalletBalance, setWalletControlsLocked }} diff --git a/wikibrowser/app/cli/page.tsx b/wikibrowser/app/cli/page.tsx index 3c03a616..bc28959c 100644 --- a/wikibrowser/app/cli/page.tsx +++ b/wikibrowser/app/cli/page.tsx @@ -1,7 +1,6 @@ import type { Metadata } from "next"; import Image from "next/image"; -import Link from "next/link"; -import { ArrowLeft, CheckCircle2, Database, Search, ShieldCheck, TerminalSquare, Wrench } from "lucide-react"; +import { CheckCircle2, Database, Search, ShieldCheck, TerminalSquare, Wrench } from "lucide-react"; import { CliGuideBlock } from "./cli-guide-block"; export const metadata: Metadata = { @@ -56,11 +55,7 @@ export default function CliPage() {
- - - Database dashboard - -
+
diff --git a/wikibrowser/app/cycles/cycles-client.tsx b/wikibrowser/app/cycles/cycles-client.tsx index 5346f52b..18c2ab11 100644 --- a/wikibrowser/app/cycles/cycles-client.tsx +++ b/wikibrowser/app/cycles/cycles-client.tsx @@ -144,7 +144,7 @@ function cyclesPurchaseSuccessHref({ params.set("provider", provider); params.set("kinic", kinic); params.set("cycles", cycles); - return `/?${params.toString()}`; + return `/dashboard?${params.toString()}`; } function Field({ label, value }: { label: string; value: string }) { diff --git a/wikibrowser/app/dashboard/dashboard-client.tsx b/wikibrowser/app/dashboard/dashboard-client.tsx index 79264cba..47877c6b 100644 --- a/wikibrowser/app/dashboard/dashboard-client.tsx +++ b/wikibrowser/app/dashboard/dashboard-client.tsx @@ -1,7 +1,6 @@ "use client"; import { AuthClient } from "@icp-sdk/auth/client"; -import Link from "next/link"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Pencil } from "lucide-react"; @@ -382,18 +381,6 @@ export function DashboardDatabaseClient({ databaseId }: { databaseId: string }) ) : null } - nav={ - <> - - Database dashboard - - {databaseId && isActiveDatabase ? ( - - Skill Registry - - ) : null} - - } actions={ <> {canisterId ? : null} @@ -456,14 +443,6 @@ export function DashboardDatabaseClient({ databaseId }: { databaseId: string }) ) : ( ) - ) : !databaseId ? ( -
-

Select a database to manage

-

Open the Database dashboard, then choose Manage on a database row.

- - Open Database dashboard - -
) : principal ? ( ) : ( diff --git a/wikibrowser/app/home-page-client.tsx b/wikibrowser/app/dashboard/dashboard-home-client.tsx similarity index 87% rename from wikibrowser/app/home-page-client.tsx rename to wikibrowser/app/dashboard/dashboard-home-client.tsx index d342777a..661b445f 100644 --- a/wikibrowser/app/home-page-client.tsx +++ b/wikibrowser/app/dashboard/dashboard-home-client.tsx @@ -4,22 +4,22 @@ import type { AuthClient } from "@icp-sdk/auth/client"; import { useCallback, useEffect, useRef, useState } from "react"; import { Plus } from "lucide-react"; import { useSearchParams } from "next/navigation"; -import { useAppSession } from "./app-session-provider"; -import { CreateDatabaseDialog } from "./create-database-dialog"; -import { DatabaseBody, OfficialKinicWikiPanel, StatusPanel } from "./home-ui"; +import { useAppSession } from "../app-session-provider"; +import { CreateDatabaseDialog } from "../create-database-dialog"; +import { DatabaseBody, OfficialKinicWikiPanel, StatusPanel } from "../home-ui"; import { KINIC_LEDGER_FEE_E8S } from "@/lib/cycles"; import { parseKinicAmountE8sInput } from "@/lib/cycles-url"; import { purchaseCyclesWithOisy, purchaseCyclesWithPlug } from "@/lib/cycles-wallet"; import { formatTokenAmountFromE8s } from "@/lib/kinic-amount"; import type { CyclesBillingConfig, DatabaseSummary } from "@/lib/types"; import { createDatabaseAuthenticated, getCyclesBillingConfig, listDatabasesAuthenticated, listDatabasesPublic } from "@/lib/vfs-client"; -import type { DatabaseRow } from "./home-ui"; +import type { DatabaseRow } from "../home-ui"; type LoadState = "idle" | "loading" | "ready" | "error"; const CREATE_DATABASE_PURCHASE_KINIC = "1"; -export function HomePageClient() { +export function DashboardHomeClient() { const canisterId = process.env.NEXT_PUBLIC_KINIC_WIKI_CANISTER_ID ?? ""; const searchParams = useSearchParams(); const refreshSeqRef = useRef(0); @@ -27,7 +27,6 @@ export function HomePageClient() { authClient, authError, authReady, - authRefreshSeq, principal, refreshWalletBalance, setWalletControlsLocked, @@ -94,12 +93,13 @@ export function HomePageClient() { let cancelled = false; queueMicrotask(() => { if (cancelled) return; - void refreshDatabases(authClient); + const databaseRefreshClient = principal && authClient ? authClient : null; + void refreshDatabases(databaseRefreshClient); }); return () => { cancelled = true; }; - }, [authClient, authReady, authRefreshSeq, refreshDatabases]); + }, [authClient, authReady, principal, refreshDatabases]); useEffect(() => { setWalletControlsLocked(creating); @@ -179,10 +179,27 @@ export function HomePageClient() { const trimmedDatabaseName = newDatabaseName.trim(); const databaseNameValidationError = databaseNameError(trimmedDatabaseName); const walletReadyToFundCreate = walletCanFundCreate(walletBalance); - const createUnavailable = loadState === "loading" || walletBusyProvider !== null || walletBalanceLoading || !walletReadyToFundCreate; + const createUnavailable = !principal || loadState === "loading" || walletBusyProvider !== null || walletBalanceLoading || !walletReadyToFundCreate; const createDisabled = creating || createUnavailable || databaseNameValidationError !== null; - const createButtonLabel = databaseCreateButtonLabel({ creating, walletConnected: Boolean(wallet), walletBalanceLoading, walletReadyToFundCreate }); + const createButtonLabel = databaseCreateButtonLabel({ + creating, + iiConnected: Boolean(principal), + walletConnected: Boolean(wallet), + walletBalanceLoading, + walletReadyToFundCreate + }); const fundingSuccessMessage = dashboardFundingSuccessMessage(searchParams); + const createDatabaseAction = ( + + ); return (
@@ -213,17 +230,7 @@ export function HomePageClient() { {principal ? ( setCreateDialogOpen(true)} - > - - {createButtonLabel} - - } + createDatabaseAction={createDatabaseAction} cyclesConfig={cyclesConfig} loading={loadState === "loading"} myDatabases={myDatabases} @@ -238,8 +245,9 @@ export function HomePageClient() {

Public databases

Public databases open without login. Login with Internet Identity to show My databases linked to your principal.

+ {createDatabaseAction}
- +
)}
@@ -285,17 +293,20 @@ function walletCanFundCreate(balanceE8s: string | null): boolean { function databaseCreateButtonLabel({ creating, + iiConnected, walletConnected, walletBalanceLoading, walletReadyToFundCreate }: { creating: boolean; + iiConnected: boolean; walletConnected: boolean; walletBalanceLoading: boolean; walletReadyToFundCreate: boolean; }): string { if (creating) return "Creating..."; - if (!walletConnected) return "Connect wallet first"; + if (!iiConnected) return "Connect Internet Identity"; + if (!walletConnected) return "Connect OISY or Plug"; if (walletBalanceLoading) return "Checking balance..."; if (!walletReadyToFundCreate) return "Insufficient KINIC"; return "Create and fund database"; diff --git a/wikibrowser/app/dashboard/page.tsx b/wikibrowser/app/dashboard/page.tsx index 0d6607f5..6d9dbbb3 100644 --- a/wikibrowser/app/dashboard/page.tsx +++ b/wikibrowser/app/dashboard/page.tsx @@ -1,5 +1,34 @@ -import { DashboardDatabaseClient } from "./dashboard-client"; +import type { Metadata } from "next"; +import { Suspense } from "react"; +import { DashboardHomeClient } from "./dashboard-home-client"; + +export const metadata: Metadata = { + title: "Kinic Wiki Database Dashboard", + description: "Browse, create, fund, and manage Kinic Wiki canister databases.", + openGraph: { + title: "Kinic Wiki Database Dashboard", + description: "Browse, create, fund, and manage Kinic Wiki canister databases." + }, + twitter: { + title: "Kinic Wiki Database Dashboard", + description: "Browse, create, fund, and manage Kinic Wiki canister databases." + } +}; export default function DashboardPage() { - return ; + return ( + }> + + + ); +} + +function DashboardHomeFallback() { + return ( +
+
+
Loading databases...
+
+
+ ); } diff --git a/wikibrowser/app/home-hero.png b/wikibrowser/app/home-hero.png new file mode 100644 index 00000000..8520dc3f Binary files /dev/null and b/wikibrowser/app/home-hero.png differ diff --git a/wikibrowser/app/home-ui.tsx b/wikibrowser/app/home-ui.tsx index 913ace4d..9f1151c0 100644 --- a/wikibrowser/app/home-ui.tsx +++ b/wikibrowser/app/home-ui.tsx @@ -128,15 +128,13 @@ export function AuthControls({ principal, loading, onLogin, - onLogout, - onRefresh + onLogout }: { authReady: boolean; principal: string | null; loading: boolean; onLogin: () => void; onLogout: () => void; - onRefresh: () => void; }) { if (!principal) { return ( @@ -154,10 +152,7 @@ export function AuthControls({ return (
- -
@@ -183,12 +178,12 @@ export function DatabaseBody({ }) { if (loading) return
Loading databases...
; if (!principal) { - return ; + return ; } return (
- +
); } @@ -473,6 +468,5 @@ function isActiveRoutableDatabase(database: DatabaseRow): boolean { } function openDatabaseHref(database: DatabaseRow): string { - const base = `/${encodeURIComponent(database.databaseId)}/Wiki`; - return !database.member && database.publicReadable ? `${base}?read=anonymous` : base; + return `/${encodeURIComponent(database.databaseId)}/Wiki`; } diff --git a/wikibrowser/app/layout.tsx b/wikibrowser/app/layout.tsx index b668b42a..b52a1654 100644 --- a/wikibrowser/app/layout.tsx +++ b/wikibrowser/app/layout.tsx @@ -5,18 +5,18 @@ import { AppSessionProvider } from "./app-session-provider"; export const metadata: Metadata = { metadataBase: new URL("https://wiki.kinic.xyz"), - title: "Kinic Wiki Database Dashboard", - description: "Browse, search, edit, and manage Kinic Wiki canister databases.", + title: "Kinic Wiki AI Memory", + description: "Use Kinic Wiki as canister-backed AI memory through kinic-vfs-cli, with browser tools for browsing and management.", openGraph: { - title: "Kinic Wiki Database Dashboard", - description: "Browse, search, edit, and manage Kinic Wiki canister databases.", + title: "Kinic Wiki AI Memory", + description: "Use Kinic Wiki as canister-backed AI memory through kinic-vfs-cli, with browser tools for browsing and management.", siteName: "Kinic Wiki", type: "website" }, twitter: { card: "summary_large_image", - title: "Kinic Wiki Database Dashboard", - description: "Browse, search, edit, and manage Kinic Wiki canister databases." + title: "Kinic Wiki AI Memory", + description: "Use Kinic Wiki as canister-backed AI memory through kinic-vfs-cli, with browser tools for browsing and management." } }; diff --git a/wikibrowser/app/page.tsx b/wikibrowser/app/page.tsx index 054a03e9..3914b2cd 100644 --- a/wikibrowser/app/page.tsx +++ b/wikibrowser/app/page.tsx @@ -1,19 +1,224 @@ -import { Suspense } from "react"; -import { HomePageClient } from "./home-page-client"; +import type { Metadata } from "next"; +import Image from "next/image"; +import Link from "next/link"; +import { BookOpen, Database, Search, ShieldCheck, TerminalSquare, Wrench } from "lucide-react"; +import heroImage from "./home-hero.png"; + +export const metadata: Metadata = { + title: "Kinic Wiki AI Memory", + description: "Use Kinic Wiki as canister-backed AI memory through kinic-vfs-cli, with the browser UI as a companion surface.", + openGraph: { + title: "Kinic Wiki AI Memory", + description: "Use Kinic Wiki as canister-backed AI memory through kinic-vfs-cli, with the browser UI as a companion surface." + }, + twitter: { + title: "Kinic Wiki AI Memory", + description: "Use Kinic Wiki as canister-backed AI memory through kinic-vfs-cli, with the browser UI as a companion surface." + } +}; + +const workflowSteps = [ + { + title: "Create databases", + body: "Create a writable Kinic Wiki database and open the same database from browser management views." + }, + { + title: "Manage access and cycles", + body: "Grant writers, inspect members, and fund cycles without leaving the browser companion." + }, + { + title: "Browse and edit", + body: "Inspect public databases, browse wiki paths, and make manual Markdown edits when operators need a visual surface." + } +]; + +const companionSurfaces = [ + { + title: "Dashboard", + body: "Create databases, inspect public entries, manage access, and fund cycles from the browser companion.", + icon: Database + }, + { + title: "Official wiki", + body: "Open the public Kinic Wiki example in the wiki browser to inspect how /Wiki and /Sources are organized.", + href: "/db_kva4v2twg6jv/Wiki", + label: "Open Official Wiki", + icon: BookOpen + }, + { + title: "Capture tools", + body: "Save ChatGPT/Claude conversations as raw sources and queue active web pages as URL ingest requests. The extension requires Internet Identity writer access; use the CLI to turn raw chats into organized /Wiki pages.", + details: ["Web pages -> /Sources/ingest-requests/...", "AI chats -> /Sources/raw/..."], + href: "https://chromewebstore.google.com/detail/moebdnadaffhlddnhifmmdoecifhcbdi", + label: "Chrome Extension", + icon: Wrench + } +]; + +const commandLines = [ + "npm install -g kinic-vfs-cli", + "kinic-vfs-cli database link ", + 'kinic-vfs-cli search-remote "query" --prefix /Wiki --json' +]; export default function HomePage() { return ( - }> - - - ); -} +
+
+ +
+
-function HomePageFallback() { - return ( -
-
-
Loading databases...
+
+ + +
+

CLI-first AI memory

+

Kinic Wiki is AI memory for agents

+

+ kinic-vfs-cli is the primary interface. The browser UI is a companion for inspection, editing, and database management. +

+ +
+
+
+ +
+
+
+

Meet Kinic Wiki

+

Use the CLI first. Manage the same memory in the browser.

+
+ +
+
+
+
+ +

Agent CLI workflow

+
+ For agents +
+

+ Connect your agent to a Kinic Wiki database, then search, read, and update durable memory from the CLI. +

+
+                {commandLines.join("\n")}
+              
+
+ +
+
+ +

Browser companion

+
+

+ Use the Dashboard and wiki browser for database creation, public browsing, manual edits, cycles funding, and access management. +

+
+ {workflowSteps.map((step, index) => ( +
+

0{index + 1}

+
+

{step.title}

+

{step.body}

+
+
+ ))} +
+ + + Open Dashboard + +
+
+
+
+ +
+
+
+

How it stays useful

+

A durable memory surface for agents and operators.

+
+ +
+ {companionSurfaces.map((surface) => { + const Icon = surface.icon; + const href = surface.href; + const label = surface.label ?? surface.title; + const linkClassName = "mt-6 inline-flex min-h-11 items-center justify-center gap-2 rounded-2xl border border-line bg-white px-4 py-3 text-sm font-bold text-ink no-underline shadow-[0_4px_10px_#14142b0a] transition-[transform,background-color,border-color,color,box-shadow] hover:-translate-y-[3px] hover:border-accent hover:bg-accent hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent motion-reduce:transition-none motion-reduce:hover:translate-y-0"; + return ( +
+
+ +
+

{surface.title}

+

{surface.body}

+ {surface.details ? ( +
    + {surface.details.map((detail) => ( +
  • + {detail} +
  • + ))} +
+ ) : null} + {href?.startsWith("http") ? ( + + {label} + opens in new tab + + ) : href ? ( + + {label} + + ) : null} +
+ ); + })} +
+ +
+
+
+ +

Memory shape

+
+

+ Structured notes live under /Wiki/.... Raw evidence lives under /Sources/raw/.... Agents can search, follow paths and links, and update named knowledge nodes. +

+
+
+
+ +

Safe edits

+
+

+ Agents read current etags before mutation, so wiki changes remain explicit when operators and automated workflows touch the same memory. +

+
+
+
); diff --git a/wikibrowser/app/skills/skill-registry-client.tsx b/wikibrowser/app/skills/skill-registry-client.tsx index 6554c148..5a86d462 100644 --- a/wikibrowser/app/skills/skill-registry-client.tsx +++ b/wikibrowser/app/skills/skill-registry-client.tsx @@ -215,14 +215,9 @@ export function SkillRegistryClient({ databaseId }: { databaseId: string }) { - - Database dashboard - - - Wiki - - + + Wiki + } actions={ <> diff --git a/wikibrowser/components/admin-header.tsx b/wikibrowser/components/admin-header.tsx index 981fd205..0e2452bb 100644 --- a/wikibrowser/components/admin-header.tsx +++ b/wikibrowser/components/admin-header.tsx @@ -2,6 +2,7 @@ // What: renders the common Kinic Wiki admin header shell. // Why: dashboard, database management, and Skill Registry should present one management UI shape. import Image from "next/image"; +import Link from "next/link"; import type { ReactNode } from "react"; export function AdminHeader({ actions, nav, title, titleAction }: { actions?: ReactNode; nav?: ReactNode; title: string; titleAction?: ReactNode }) { @@ -10,7 +11,9 @@ export function AdminHeader({ actions, nav, title, titleAction }: { actions?: Re
{nav ? : null}
- + + +

Kinic Wiki

diff --git a/wikibrowser/components/document-pane.tsx b/wikibrowser/components/document-pane.tsx index 3dc5e835..c23e12a0 100644 --- a/wikibrowser/components/document-pane.tsx +++ b/wikibrowser/components/document-pane.tsx @@ -37,8 +37,7 @@ export function DocumentHeader({ isDirectory, canEditDirectory, editState, - rawContent, - readMode = null + rawContent }: { canisterId: string; databaseId: string; @@ -49,7 +48,6 @@ export function DocumentHeader({ canEditDirectory: boolean; editState: DocumentEditState; rawContent: string | null; - readMode?: "anonymous" | null; }) { const [copyStatus, setCopyStatus] = useState(null); async function copyText(label: string, value: string) { @@ -63,7 +61,7 @@ export function DocumentHeader({ return (
- +
); @@ -693,8 +660,7 @@ function FolderIndexSection({ isLargeContent, contentBytes, canisterId, - databaseId, - readMode + databaseId }: { folderPath: string; folderIndexNode: PathLoadState; @@ -703,7 +669,6 @@ function FolderIndexSection({ contentBytes: number; canisterId: string; databaseId: string; - readMode: "anonymous" | null; }) { if (folderIndexNode.loading) { return

Loading folder note...

; @@ -722,10 +687,10 @@ function FolderIndexSection({ {view === "raw" ? ( ) : isLargeContent ? ( - + ) : (
- +
)}
@@ -737,18 +702,16 @@ function DirectoryDocument({ childrenState, canisterId, databaseId, - readMode, parentPath }: { childrenState: LoadState; canisterId: string; databaseId: string; - readMode: "anonymous" | null; parentPath: string; }) { return (
- +
); } @@ -757,13 +720,11 @@ function DirectoryChildrenCard({ childrenState, canisterId, databaseId, - readMode, parentPath }: { childrenState: LoadState; canisterId: string; databaseId: string; - readMode: "anonymous" | null; parentPath: string; }) { const children = childrenState.data ? visibleChildren(childrenState.data, parentPath) : null; @@ -777,7 +738,7 @@ function DirectoryChildrenCard({ {children?.map((child) => ( diff --git a/wikibrowser/components/explorer-tree.tsx b/wikibrowser/components/explorer-tree.tsx index e5fed82a..6a057e51 100644 --- a/wikibrowser/components/explorer-tree.tsx +++ b/wikibrowser/components/explorer-tree.tsx @@ -19,7 +19,6 @@ export function ExplorerTree({ selectedPath, autoExpandSelected = true, readIdentity, - readMode = null, childNodesCache, onSelectedNode }: { @@ -28,15 +27,14 @@ export function ExplorerTree({ selectedPath: string; autoExpandSelected?: boolean; readIdentity: Identity | null; - readMode?: "anonymous" | null; childNodesCache: { current: Map }; onSelectedNode: (node: ChildNode) => void; }) { const readPrincipal = readIdentity?.getPrincipal().toText() ?? null; return (
- - + +
); } @@ -49,7 +47,6 @@ function TreeNode({ depth, autoExpandSelected, readIdentity, - readMode, childNodesCache, onSelectedNode }: { @@ -60,7 +57,6 @@ function TreeNode({ depth: number; autoExpandSelected: boolean; readIdentity: Identity | null; - readMode: "anonymous" | null; childNodesCache: { current: Map }; onSelectedNode: (node: ChildNode) => void; }) { @@ -74,7 +70,7 @@ function TreeNode({ isVirtual: nodeIsVirtual, hasChildren: nodeHasChildren } = node; - const readPrincipal = readMode === "anonymous" ? null : readIdentity?.getPrincipal().toText() ?? null; + const readPrincipal = readIdentity?.getPrincipal().toText() ?? null; const requestKey = nodeRequestKey(canisterId, databaseId, nodePath, readPrincipal); const [expanded, setExpanded] = useState(autoExpandSelected && (nodePath === selectedPath || selectedPath.startsWith(`${nodePath}/`))); const [children, setChildren] = useState>(() => { @@ -167,7 +163,7 @@ function TreeNode({ {directoryIcon(canExpand, expanded)} {node.name} @@ -182,7 +178,6 @@ function TreeNode({ selectedPath={selectedPath} autoExpandSelected={autoExpandSelected} readIdentity={readIdentity} - readMode={readMode} childNodesCache={childNodesCache} onSelectedNode={onSelectedNode} /> @@ -212,7 +207,6 @@ function ChildrenList({ selectedPath, autoExpandSelected, readIdentity, - readMode, childNodesCache, onSelectedNode }: { @@ -223,7 +217,6 @@ function ChildrenList({ selectedPath: string; autoExpandSelected: boolean; readIdentity: Identity | null; - readMode: "anonymous" | null; childNodesCache: { current: Map }; onSelectedNode: (node: ChildNode) => void; }) { @@ -241,7 +234,6 @@ function ChildrenList({ depth={depth + 1} autoExpandSelected={autoExpandSelected} readIdentity={readIdentity} - readMode={readMode} childNodesCache={childNodesCache} onSelectedNode={onSelectedNode} /> diff --git a/wikibrowser/components/graph-panel.tsx b/wikibrowser/components/graph-panel.tsx index 907e2213..bf9207c3 100644 --- a/wikibrowser/components/graph-panel.tsx +++ b/wikibrowser/components/graph-panel.tsx @@ -25,7 +25,7 @@ type GraphLoadState = LoadState & { requestKey: string | null; }; -export function GraphPanel({ canisterId, databaseId, centerPath, depth, readIdentity, readMode = null }: { canisterId: string; databaseId: string; centerPath: string | null; depth: 1 | 2; readIdentity: Identity | null; readMode?: "anonymous" | null }) { +export function GraphPanel({ canisterId, databaseId, centerPath, depth, readIdentity }: { canisterId: string; databaseId: string; centerPath: string | null; depth: 1 | 2; readIdentity: Identity | null }) { const readPrincipal = readIdentity?.getPrincipal().toText() ?? null; const currentRequestKey = graphRequestKey(canisterId, databaseId, centerPath, depth, readPrincipal); const [links, setLinks] = useState({ centerPath: null, requestKey: null, data: null, error: null, loading: false }); @@ -73,10 +73,10 @@ export function GraphPanel({ canisterId, databaseId, centerPath, depth, readIden {centerPath} {nodeCount} nodes {edgeCount} edges - + depth 1 - + depth 2
@@ -97,7 +97,7 @@ export function GraphPanel({ canisterId, databaseId, centerPath, depth, readIden return ; })} {graph.nodes.map((node) => ( - + {shortName(node.path)} diff --git a/wikibrowser/components/inspector.tsx b/wikibrowser/components/inspector.tsx index faeca45d..c31e93a8 100644 --- a/wikibrowser/components/inspector.tsx +++ b/wikibrowser/components/inspector.tsx @@ -24,8 +24,7 @@ export function Inspector({ incomingLinks, incomingError, outgoingLinks, - readIdentity, - readMode = null + readIdentity }: { canisterId: string; databaseId: string; @@ -38,7 +37,6 @@ export function Inspector({ incomingError?: string | null; outgoingLinks: LinkEdge[]; readIdentity: Identity | null; - readMode?: "anonymous" | null; }) { const kind = node?.kind ?? "directory"; const size = node ? `${new TextEncoder().encode(node.content).length}` : null; @@ -112,7 +110,7 @@ export function Inspector({
    {outgoingLinks.map((edge) => (
  • - + {edge.targetPath}

    {edge.linkText || edge.rawHref}

    @@ -134,7 +132,7 @@ export function Inspector({
      {incomingLinks.map((edge) => (
    • - + {edge.sourcePath}

      {edge.linkText || edge.rawHref}

      @@ -150,7 +148,7 @@ export function Inspector({
        {rawSourceLinks.map((link) => (
      • - + {link}
      • diff --git a/wikibrowser/components/lint-panel.tsx b/wikibrowser/components/lint-panel.tsx index 348715ba..995f35ec 100644 --- a/wikibrowser/components/lint-panel.tsx +++ b/wikibrowser/components/lint-panel.tsx @@ -5,7 +5,7 @@ import { hrefForPath } from "@/lib/paths"; import { collectLintHints } from "@/lib/lint-hints"; import type { WikiNode } from "@/lib/types"; -export function LintPanel({ path, node, canisterId, databaseId, readMode = null }: { path: string; node: WikiNode | null; canisterId: string; databaseId: string; readMode?: "anonymous" | null }) { +export function LintPanel({ path, node, canisterId, databaseId }: { path: string; node: WikiNode | null; canisterId: string; databaseId: string }) { if (!node) { return

        Select a markdown node to inspect lightweight hints.

        ; } @@ -29,7 +29,7 @@ export function LintPanel({ path, node, canisterId, databaseId, readMode = null

        {hint.detail}

        {hint.preview ?

        {hint.preview}

        : null} {hint.line ?

        line {hint.line}

        : null} - + Open raw view
diff --git a/wikibrowser/components/markdown-preview.tsx b/wikibrowser/components/markdown-preview.tsx index 99bbe253..52859346 100644 --- a/wikibrowser/components/markdown-preview.tsx +++ b/wikibrowser/components/markdown-preview.tsx @@ -11,14 +11,12 @@ export function MarkdownPreview({ canisterId, databaseId, nodePath, - content, - readMode = null + content }: { canisterId: string; databaseId: string; nodePath: string; content: string; - readMode?: "anonymous" | null; }) { const frontmatter = splitMarkdownFrontmatter(content); const markdown = renderWikilinksAsMarkdown(frontmatter ? frontmatter.body : content); @@ -29,7 +27,7 @@ export function MarkdownPreview({ remarkPlugins={[remarkGfm]} components={{ a({ href, children, ...props }) { - const wikiHref = hrefForMarkdownLink(canisterId, databaseId, nodePath, href, readMode); + const wikiHref = hrefForMarkdownLink(canisterId, databaseId, nodePath, href); if (!wikiHref) { return {children}; } diff --git a/wikibrowser/components/query-panel.tsx b/wikibrowser/components/query-panel.tsx index c0e5cf0e..731347fc 100644 --- a/wikibrowser/components/query-panel.tsx +++ b/wikibrowser/components/query-panel.tsx @@ -27,7 +27,6 @@ export function QueryPanel({ currentNode, readIdentity, writeIdentity, - readMode, readIdentityMode, databaseCyclesError }: { @@ -37,7 +36,6 @@ export function QueryPanel({ currentNode: WikiNode | null; readIdentity: Identity | null; writeIdentity: Identity | null; - readMode: "anonymous" | null; readIdentityMode: ReadIdentityMode; databaseCyclesError: string | null; }) { @@ -188,7 +186,7 @@ export function QueryPanel({
{previewAction ? void confirmQueueUrl(pendingAction) : null} /> : null} - +
); } @@ -232,7 +230,7 @@ function MetaBadge({ children }: { children: ReactNode }) { return {children}; } -function QueryResultView({ canisterId, databaseId, readMode, result }: { canisterId: string; databaseId: string; readMode: "anonymous" | null; result: QueryResult | null }) { +function QueryResultView({ canisterId, databaseId, result }: { canisterId: string; databaseId: string; result: QueryResult | null }) { if (!result) return null; if (result.kind === "message") { return
{result.text}
; @@ -246,7 +244,7 @@ function QueryResultView({ canisterId, databaseId, readMode, result }: { caniste

Citations

{result.citations.map((path) => ( - + {path} ))} @@ -263,7 +261,7 @@ function QueryResultView({ canisterId, databaseId, readMode, result }: { caniste {result.hits.map((hit) => { const excerpt = resultExcerpt(hit); return ( - + {hit.path} {excerpt ? {excerpt} : null} @@ -274,7 +272,7 @@ function QueryResultView({ canisterId, databaseId, readMode, result }: { caniste } return (
- + {result.targetPath} {result.hints.length === 0 ?

No lightweight warnings.

: null} diff --git a/wikibrowser/components/search-panel.tsx b/wikibrowser/components/search-panel.tsx index d48765dd..b51b25b5 100644 --- a/wikibrowser/components/search-panel.tsx +++ b/wikibrowser/components/search-panel.tsx @@ -29,8 +29,7 @@ export function SearchPanel({ prefix = "/Wiki", emptyMessage = "Use the header search.", eyebrow = "Search", - title = "Wiki search", - readMode = null + title = "Wiki search" }: { canisterId: string; databaseId: string; @@ -41,7 +40,6 @@ export function SearchPanel({ emptyMessage?: string; eyebrow?: string; title?: string; - readMode?: "anonymous" | null; }) { const latestRequest = useRef(0); const lastRequestedKey = useRef(null); @@ -119,7 +117,7 @@ export function SearchPanel({ return (
{hit.path}
diff --git a/wikibrowser/components/wiki-browser.tsx b/wikibrowser/components/wiki-browser.tsx index 40a905d3..6ba7cbc2 100644 --- a/wikibrowser/components/wiki-browser.tsx +++ b/wikibrowser/components/wiki-browser.tsx @@ -43,6 +43,8 @@ const SIDEBAR_TABS: ModeTab[] = ["explorer", "query", "ingest"]; const HEADER_ICON_LINK_CLASS = "inline-flex h-9 items-center justify-center gap-1 rounded-lg border px-3 text-sm no-underline"; const EMPTY_EDIT_STATE: DocumentEditState = { dirty: false, saveState: "idle" }; const UNSAVED_MARKDOWN_MESSAGE = "You have unsaved Markdown changes. Leave edit mode?"; +const EMPTY_DATABASE_SUMMARIES: DatabaseSummary[] = []; +const EMPTY_PUBLIC_DATABASE_IDS: ReadonlySet = new Set(); const GraphPanel = dynamic(() => import("@/components/graph-panel").then((module) => module.GraphPanel), { ssr: false, loading: () =>

Loading graph view...

@@ -56,6 +58,16 @@ type BrowserLoadState = PathLoadState & { requestKey: string; }; +type DatabaseDirectoryState = { + requestKey: string; + databases: DatabaseSummary[]; + memberDatabases: DatabaseSummary[]; + cyclesConfig: CyclesBillingConfig | null; + publicDatabaseIds: ReadonlySet; + memberDatabasesLoaded: boolean; + databaseListError: string | null; +}; + export function WikiBrowser() { const pathname = usePathname(); const router = useRouter(); @@ -68,7 +80,6 @@ export function WikiBrowser() { const isHelpPage = useMemo(() => isBrowserHelpPathname(canisterId, databaseId, pathname), [canisterId, databaseId, pathname]); const graphCenter = isGraphPage ? searchParams.get("center") : null; const graphDepth = parseGraphDepth(searchParams.get("depth")); - const readMode = parseReadMode(searchParams.get("read")); const selectedPath = useMemo( () => isSearchPage || isHelpPage ? "/Wiki" : isGraphPage ? graphCenter ?? "/Wiki" : routeState.nodePath, [graphCenter, isGraphPage, isHelpPage, isSearchPage, routeState.nodePath] @@ -80,20 +91,25 @@ export function WikiBrowser() { const [authClient, setAuthClient] = useState(null); const [readIdentity, setReadIdentity] = useState(null); const [authError, setAuthError] = useState(null); - const [databases, setDatabases] = useState([]); - const [memberDatabases, setMemberDatabases] = useState([]); - const [cyclesConfig, setCyclesBillingConfig] = useState(null); - const [publicDatabaseIds, setPublicDatabaseIds] = useState>(() => new Set()); - const [memberDatabasesLoaded, setMemberDatabasesLoaded] = useState(false); - const [databaseListError, setDatabaseListError] = useState(null); + const [databaseDirectory, setDatabaseDirectory] = useState(() => emptyDatabaseDirectoryState("")); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); + const authPrincipal = readIdentity?.getPrincipal().toText() ?? null; + const databaseDirectoryRequestKey = useMemo(() => `${canisterId}\n${authPrincipal ?? ""}`, [authPrincipal, canisterId]); + const emptyCurrentDatabaseDirectory = useMemo(() => emptyDatabaseDirectoryState(databaseDirectoryRequestKey), [databaseDirectoryRequestKey]); + const { + databases, + memberDatabases, + cyclesConfig, + publicDatabaseIds, + memberDatabasesLoaded, + databaseListError + } = databaseDirectory.requestKey === databaseDirectoryRequestKey ? databaseDirectory : emptyCurrentDatabaseDirectory; const currentDatabaseRole = useMemo( () => readIdentity ? memberDatabases.find((database) => database.databaseId === databaseId)?.role ?? null : null, [databaseId, memberDatabases, readIdentity] ); - const currentReadIdentityMode = resolveReadIdentityMode(readMode, Boolean(readIdentity), Boolean(currentDatabaseRole), memberDatabasesLoaded, publicDatabaseIds.has(databaseId)); + const currentReadIdentityMode = resolveReadIdentityMode(Boolean(readIdentity), Boolean(currentDatabaseRole), memberDatabasesLoaded, publicDatabaseIds.has(databaseId)); const effectiveReadIdentity = currentReadIdentityMode === "user" ? readIdentity : null; - const authPrincipal = readIdentity?.getPrincipal().toText() ?? null; const readPrincipal = effectiveReadIdentity?.getPrincipal().toText() ?? null; const currentRequestKey = nodeRequestKey(canisterId, databaseId, selectedPath, readPrincipal); const folderIndexRequestKey = nodeRequestKey(canisterId, databaseId, folderIndexPath(selectedPath), readPrincipal); @@ -137,52 +153,73 @@ export function WikiBrowser() { useEffect(() => { let cancelled = false; if (!canisterId) return; - Promise.resolve() - .then(() => { - if (cancelled) return null; - setMemberDatabasesLoaded(false); - setDatabaseListError(null); - return Promise.allSettled([ - listDatabasesPublic(canisterId), - readIdentity ? listDatabasesAuthenticated(canisterId, readIdentity) : Promise.resolve([]), - getCyclesBillingConfig(canisterId) - ]); + const requestKey = databaseDirectoryRequestKey; + let publicDatabases: DatabaseSummary[] = []; + let authenticatedDatabases: DatabaseSummary[] = []; + let nextCyclesConfig: CyclesBillingConfig | null = null; + let nextMemberDatabasesLoaded = false; + let cyclesConfigError: string | null = null; + let publicListError: string | null = null; + let memberListError: string | null = null; + const updateDatabaseRows = () => { + setDatabaseDirectory({ + requestKey, + databases: mergeDatabaseSummaries(authenticatedDatabases, publicDatabases), + memberDatabases: authenticatedDatabases, + cyclesConfig: nextCyclesConfig, + publicDatabaseIds: new Set(publicDatabases.map((database) => database.databaseId)), + memberDatabasesLoaded: nextMemberDatabasesLoaded, + databaseListError: databaseListWarning(cyclesConfigError, publicListError, memberListError) + }); + }; + + void listDatabasesPublic(canisterId) + .then((nextPublicDatabases) => { + if (cancelled) return; + publicDatabases = nextPublicDatabases; + publicListError = null; + updateDatabaseRows(); }) - .then((results) => { - if (cancelled || !results) return; - const [publicResult, memberResult, cyclesConfigResult] = results; - if (publicResult.status === "rejected" && memberResult.status === "rejected") { - setDatabases([]); - setMemberDatabases([]); - setCyclesBillingConfig(null); - setPublicDatabaseIds(new Set()); - setMemberDatabasesLoaded(false); - setDatabaseListError(`${errorMessage(publicResult.reason)}; ${errorMessage(memberResult.reason)}`); - return; - } - const publicDatabases = publicResult.status === "fulfilled" ? publicResult.value : []; - const authenticatedDatabases = memberResult.status === "fulfilled" ? memberResult.value : []; - setDatabases(mergeDatabaseSummaries(authenticatedDatabases, publicDatabases)); - setMemberDatabases(authenticatedDatabases); - setCyclesBillingConfig(cyclesConfigResult.status === "fulfilled" ? cyclesConfigResult.value : null); - setPublicDatabaseIds(new Set(publicDatabases.map((database) => database.databaseId))); - setMemberDatabasesLoaded(memberResult.status === "fulfilled"); - setDatabaseListError(databaseListWarning(publicResult, memberResult, cyclesConfigResult)); + .catch((cause) => { + if (cancelled) return; + publicDatabases = []; + publicListError = errorMessage(cause); + updateDatabaseRows(); + }); + + void (readIdentity ? listDatabasesAuthenticated(canisterId, readIdentity) : Promise.resolve([])) + .then((nextMemberDatabases) => { + if (cancelled) return; + authenticatedDatabases = nextMemberDatabases; + memberListError = null; + nextMemberDatabasesLoaded = true; + updateDatabaseRows(); }) .catch((cause) => { - if (!cancelled) { - setDatabases([]); - setMemberDatabases([]); - setCyclesBillingConfig(null); - setPublicDatabaseIds(new Set()); - setMemberDatabasesLoaded(false); - setDatabaseListError(errorMessage(cause)); - } + if (cancelled) return; + authenticatedDatabases = []; + memberListError = errorMessage(cause); + nextMemberDatabasesLoaded = false; + updateDatabaseRows(); + }); + + void getCyclesBillingConfig(canisterId) + .then((loadedCyclesConfig) => { + if (cancelled) return; + cyclesConfigError = null; + nextCyclesConfig = loadedCyclesConfig; + updateDatabaseRows(); + }) + .catch((cause) => { + if (cancelled) return; + cyclesConfigError = errorMessage(cause); + nextCyclesConfig = null; + updateDatabaseRows(); }); return () => { cancelled = true; }; - }, [canisterId, readIdentity, authPrincipal]); + }, [canisterId, databaseDirectoryRequestKey, readIdentity]); useEffect(() => { let cancelled = false; @@ -368,13 +405,7 @@ export function WikiBrowser() { const selectedExplorerNode = selectedExplorerState?.key === explorerSelectionKey ? selectedExplorerState.node : explorerNodeFromSelection(selectedPath, currentNode, currentChildren); - const explorerWriteDisabledReason = writeDisabledReason( - readMode, - readIdentity, - currentDatabaseRole, - readIdentity && !currentDatabaseRole ? databaseListError : null, - currentDatabaseCycleReason - ); + const explorerWriteDisabledReason = writeDisabledReason(readIdentity, currentDatabaseRole, readIdentity && !currentDatabaseRole ? databaseListError : null, currentDatabaseCycleReason); const explorerCreateDirectory = createDirectoryForExplorerNode(selectedExplorerNode); const explorerMutationTarget = selectedExplorerNode && isMutableWikiExplorerNode(selectedExplorerNode) ? selectedExplorerNode : null; const selectedExplorerChildren = selectedExplorerNode?.kind === "folder" @@ -403,7 +434,6 @@ export function WikiBrowser() { }, [canisterId, databaseId, readPrincipal, setSelectedExplorerState]); const createMarkdownFile = useCallback(async (directoryPath: string, fileName: string) => { if (!canLeaveDirtyEdit()) return false; - if (readMode === "anonymous") throw new Error("Authenticated mode is required."); if (!readIdentity) throw new Error("Login with Internet Identity to create Markdown files."); if (currentDatabaseRole !== "writer" && currentDatabaseRole !== "owner") throw new Error("Writer or owner access required."); if (currentDatabaseCycleReason) throw new Error(currentDatabaseCycleReason); @@ -419,12 +449,11 @@ export function WikiBrowser() { }); invalidateBrowserCaches(); setEditState(EMPTY_EDIT_STATE); - router.replace(hrefForPath(canisterId, databaseId, nextPath, "edit", tab, undefined, undefined, readMode)); + router.replace(hrefForPath(canisterId, databaseId, nextPath, "edit", tab)); return true; - }, [canLeaveDirtyEdit, canisterId, currentDatabaseCycleReason, currentDatabaseRole, databaseId, invalidateBrowserCaches, readIdentity, readMode, router, setEditState, tab]); + }, [canLeaveDirtyEdit, canisterId, currentDatabaseCycleReason, currentDatabaseRole, databaseId, invalidateBrowserCaches, readIdentity, router, setEditState, tab]); const createFolderNode = useCallback(async (directoryPath: string, folderName: string) => { if (!canLeaveDirtyEdit()) return false; - if (readMode === "anonymous") throw new Error("Authenticated mode is required."); if (!readIdentity) throw new Error("Login with Internet Identity to create folders."); if (currentDatabaseRole !== "writer" && currentDatabaseRole !== "owner") throw new Error("Writer or owner access required."); if (currentDatabaseCycleReason) throw new Error(currentDatabaseCycleReason); @@ -436,12 +465,11 @@ export function WikiBrowser() { }); invalidateBrowserCaches(); setEditState(EMPTY_EDIT_STATE); - router.replace(hrefForPath(canisterId, databaseId, nextPath, undefined, tab, undefined, undefined, readMode)); + router.replace(hrefForPath(canisterId, databaseId, nextPath, undefined, tab)); return true; - }, [canLeaveDirtyEdit, canisterId, currentDatabaseCycleReason, currentDatabaseRole, databaseId, invalidateBrowserCaches, readIdentity, readMode, router, setEditState, tab]); + }, [canLeaveDirtyEdit, canisterId, currentDatabaseCycleReason, currentDatabaseRole, databaseId, invalidateBrowserCaches, readIdentity, router, setEditState, tab]); const renameExplorerNode = useCallback(async (target: ChildNode, nextName: string) => { if (!canLeaveDirtyEdit()) return false; - if (readMode === "anonymous") throw new Error("Authenticated mode is required."); if (!readIdentity) throw new Error("Login with Internet Identity to rename nodes."); if (currentDatabaseRole !== "writer" && currentDatabaseRole !== "owner") throw new Error("Writer or owner access required."); if (currentDatabaseCycleReason) throw new Error(currentDatabaseCycleReason); @@ -461,12 +489,11 @@ export function WikiBrowser() { }); invalidateBrowserCaches(); setEditState(EMPTY_EDIT_STATE); - router.replace(hrefForPath(canisterId, databaseId, nextPath, target.kind === "file" ? view : undefined, tab, undefined, undefined, readMode)); + router.replace(hrefForPath(canisterId, databaseId, nextPath, target.kind === "file" ? view : undefined, tab)); return true; - }, [canLeaveDirtyEdit, canisterId, currentDatabaseCycleReason, currentDatabaseRole, databaseId, invalidateBrowserCaches, readIdentity, readMode, router, setEditState, tab, view]); + }, [canLeaveDirtyEdit, canisterId, currentDatabaseCycleReason, currentDatabaseRole, databaseId, invalidateBrowserCaches, readIdentity, router, setEditState, tab, view]); const moveExplorerNode = useCallback(async (target: ChildNode, targetDirectory: string) => { if (!canLeaveDirtyEdit()) return false; - if (readMode === "anonymous") throw new Error("Authenticated mode is required."); if (!readIdentity) throw new Error("Login with Internet Identity to move nodes."); if (currentDatabaseRole !== "writer" && currentDatabaseRole !== "owner") throw new Error("Writer or owner access required."); if (currentDatabaseCycleReason) throw new Error(currentDatabaseCycleReason); @@ -485,12 +512,11 @@ export function WikiBrowser() { }); invalidateBrowserCaches(); setEditState(EMPTY_EDIT_STATE); - router.replace(hrefForPath(canisterId, databaseId, nextPath, target.kind === "file" ? view : undefined, tab, undefined, undefined, readMode)); + router.replace(hrefForPath(canisterId, databaseId, nextPath, target.kind === "file" ? view : undefined, tab)); return true; - }, [canLeaveDirtyEdit, canisterId, currentDatabaseCycleReason, currentDatabaseRole, databaseId, invalidateBrowserCaches, readIdentity, readMode, router, setEditState, tab, view]); + }, [canLeaveDirtyEdit, canisterId, currentDatabaseCycleReason, currentDatabaseRole, databaseId, invalidateBrowserCaches, readIdentity, router, setEditState, tab, view]); const deleteExplorerNode = useCallback(async (target: ChildNode) => { if (!canLeaveDirtyEdit()) return false; - if (readMode === "anonymous") throw new Error("Authenticated mode is required."); if (!readIdentity) throw new Error("Login with Internet Identity to delete nodes."); if (currentDatabaseRole !== "writer" && currentDatabaseRole !== "owner") throw new Error("Writer or owner access required."); if (currentDatabaseCycleReason) throw new Error(currentDatabaseCycleReason); @@ -513,10 +539,10 @@ export function WikiBrowser() { invalidateBrowserCaches(); setEditState(EMPTY_EDIT_STATE); if (selectedPath === target.path) { - router.replace(hrefForPath(canisterId, databaseId, parentPath(target.path) ?? "/Wiki", undefined, tab, undefined, undefined, readMode)); + router.replace(hrefForPath(canisterId, databaseId, parentPath(target.path) ?? "/Wiki", undefined, tab)); } return true; - }, [canLeaveDirtyEdit, canisterId, currentDatabaseCycleReason, currentDatabaseRole, databaseId, invalidateBrowserCaches, readIdentity, readMode, readPrincipal, router, selectedPath, setEditState, tab]); + }, [canLeaveDirtyEdit, canisterId, currentDatabaseCycleReason, currentDatabaseRole, databaseId, invalidateBrowserCaches, readIdentity, readPrincipal, router, selectedPath, setEditState, tab]); async function submitExplorerCreate(event: FormEvent) { event.preventDefault(); @@ -579,26 +605,6 @@ export function WikiBrowser() { } } - useEffect(() => { - const loadError = currentNode.error || currentChildren.error; - if (readMode === "anonymous" || !isPermissionError(loadError)) return; - const anonymousHref = hrefForCurrentReadRoute(canisterId, databaseId, { - graphCenter, - graphDepth, - isHelpPage, - isGraphPage, - isSearchPage, - query, - searchKind, - selectedPath, - tab, - view - }); - if (anonymousHref) { - router.replace(anonymousHref); - } - }, [canisterId, currentChildren.error, currentNode.error, databaseId, graphCenter, graphDepth, isGraphPage, isHelpPage, isSearchPage, query, readMode, router, searchKind, selectedPath, tab, view]); - return (
) : undefined} /> - + {tab === "explorer" && explorerActionMode ? ( ) : isGraphPage ? ( - + ) : isSearchPage ? ( - + ) : ( <> { if (nextView !== "edit" && !canLeaveDirtyEdit()) { return; } - router.replace(hrefForPath(canisterId, databaseId, selectedPath, nextView, tab, undefined, undefined, readMode)); + router.replace(hrefForPath(canisterId, databaseId, selectedPath, nextView, tab)); }} isDirectory={currentNode.data?.kind === "folder" || (!currentNode.data && Boolean(currentChildren.data))} canEditDirectory={currentNode.data?.kind === "folder" && isWikiPath(selectedPath)} @@ -773,7 +776,6 @@ export function WikiBrowser() { onFolderIndexSaved={refreshSelectedFolderIndex} onEditStateChange={setEditState} tab={tab} - readMode={readMode} /> )} @@ -793,7 +795,6 @@ export function WikiBrowser() { incomingError={currentNodeContext.error} outgoingLinks={currentNodeContext.data?.outgoingLinks ?? []} readIdentity={effectiveReadIdentity} - readMode={readMode} /> ) : null} @@ -813,7 +814,6 @@ function LeftPane({ effectiveReadIdentity, currentNode, readIdentityMode, - readMode, databaseCyclesError, explorerRevision, onSelectedExplorerNode @@ -828,7 +828,6 @@ function LeftPane({ effectiveReadIdentity: Identity | null; currentNode: WikiNode | null; readIdentityMode: "anonymous" | "user"; - readMode: "anonymous" | null; databaseCyclesError: string | null; explorerRevision: number; onSelectedExplorerNode: (node: ChildNode) => void; @@ -842,7 +841,6 @@ function LeftPane({ currentNode={currentNode} readIdentity={effectiveReadIdentity} writeIdentity={readIdentity} - readMode={readMode} readIdentityMode={readIdentityMode} databaseCyclesError={databaseCyclesError} /> @@ -866,7 +864,6 @@ function LeftPane({ selectedPath={selectedPath} autoExpandSelected={autoExpandExplorer} readIdentity={effectiveReadIdentity} - readMode={readMode} childNodesCache={childNodesCache} onSelectedNode={onSelectedExplorerNode} /> @@ -1177,13 +1174,11 @@ function isProtectedRootFolder(path: string): boolean { } function writeDisabledReason( - readMode: "anonymous" | null, writeIdentity: Identity | null, currentDatabaseRole: DatabaseRole | null, databaseRoleError: string | null, databaseCyclesError: string | null ): string | null { - if (readMode === "anonymous") return "Switch to authenticated mode to change files."; if (!writeIdentity) return "Login with Internet Identity to change files."; if (databaseRoleError) return databaseRoleError; if (!currentDatabaseRole) return "Database role unavailable."; @@ -1240,7 +1235,6 @@ function TopBar({ isGraphPage, isSearchPage, graphCenter, - readMode, databaseOptions, currentDatabase, currentDatabaseName, @@ -1266,7 +1260,6 @@ function TopBar({ isGraphPage: boolean; isSearchPage: boolean; graphCenter: string | null; - readMode: "anonymous" | null; databaseOptions: DatabaseSummary[]; currentDatabase: DatabaseSummary | null; currentDatabaseName: string; @@ -1284,8 +1277,8 @@ function TopBar({ const router = useRouter(); const graphLinkCenter = isGraphPage ? graphCenter : selectedPath; const graphHref = isGraphPage - ? hrefForPath(canisterId, databaseId, graphLinkCenter ?? "/Wiki", undefined, undefined, undefined, undefined, readMode) - : hrefForGraph(canisterId, databaseId, graphLinkCenter, undefined, readMode); + ? hrefForPath(canisterId, databaseId, graphLinkCenter ?? "/Wiki") + : hrefForGraph(canisterId, databaseId, graphLinkCenter); const visibleError = authError ?? databaseListError; const cycles = databaseCyclesView(currentDatabase, cyclesConfig); @@ -1300,8 +1293,7 @@ function TopBar({ isHelpPage, query, searchKind, - graphDepth, - readMode + graphDepth }) ); } @@ -1322,7 +1314,7 @@ function TopBar({ @@ -1348,7 +1340,7 @@ function TopBar({
- +
{visibleError ? {visibleError} : null} @@ -1367,7 +1359,7 @@ function TopBar({ ) : null} @@ -1467,13 +1459,26 @@ function withCurrentDatabase(databases: DatabaseSummary[], databaseId: string): ]; } -function databaseListWarning(publicResult: PromiseSettledResult, memberResult: PromiseSettledResult, cyclesConfigResult: PromiseSettledResult): string | null { - if (cyclesConfigResult.status === "rejected") return `Cycles config unavailable: ${errorMessage(cyclesConfigResult.reason)}`; - if (publicResult.status === "rejected") return `Public database list unavailable: ${errorMessage(publicResult.reason)}`; - if (memberResult.status === "rejected") return `Member database list unavailable: ${errorMessage(memberResult.reason)}`; +function databaseListWarning(cyclesConfigError: string | null, publicListError: string | null, memberListError: string | null): string | null { + if (cyclesConfigError) return `Cycles config unavailable: ${cyclesConfigError}`; + if (publicListError && memberListError) return `Public database list unavailable: ${publicListError}; Member database list unavailable: ${memberListError}`; + if (publicListError) return `Public database list unavailable: ${publicListError}`; + if (memberListError) return `Member database list unavailable: ${memberListError}`; return null; } +function emptyDatabaseDirectoryState(requestKey: string): DatabaseDirectoryState { + return { + requestKey, + databases: EMPTY_DATABASE_SUMMARIES, + memberDatabases: EMPTY_DATABASE_SUMMARIES, + cyclesConfig: null, + publicDatabaseIds: EMPTY_PUBLIC_DATABASE_IDS, + memberDatabasesLoaded: false, + databaseListError: null + }; +} + export function isPermissionError(message: string | null): boolean { return Boolean(message && /access|auth|permission|principal|unauthorized|not allowed|forbidden/i.test(message)); } @@ -1483,14 +1488,12 @@ function HeaderSearch({ databaseId, query, searchKind, - readMode, canLeaveDirtyEdit }: { canisterId: string; databaseId: string; query: string; searchKind: "path" | "full"; - readMode: "anonymous" | null; canLeaveDirtyEdit: () => boolean; }) { const router = useRouter(); @@ -1502,7 +1505,7 @@ function HeaderSearch({ function submitSearch(event: FormEvent) { event.preventDefault(); if (!canLeaveDirtyEdit()) return; - router.replace(hrefForSearch(canisterId, databaseId, text.trim(), kind, readMode)); + router.replace(hrefForSearch(canisterId, databaseId, text.trim(), kind)); } return ( @@ -1543,14 +1546,12 @@ function ModeTabs({ canisterId, databaseId, selectedPath, - tab, - readMode + tab }: { canisterId: string; databaseId: string; selectedPath: string; tab: ModeTab; - readMode: "anonymous" | null; }) { return (
".length); +const dashboardHomeClient = readFileSync(new URL("../app/dashboard/dashboard-home-client.tsx", import.meta.url), "utf8"); const cyclesState = readFileSync(new URL("../lib/cycles-state.ts", import.meta.url), "utf8"); const apiErrors = readFileSync(new URL("../lib/api-errors.ts", import.meta.url), "utf8"); const wikiLayout = readFileSync(new URL("../app/[databaseId]/layout.tsx", import.meta.url), "utf8"); @@ -40,25 +41,56 @@ assert.match(homeUi, /href=\{`\/dashboard\/\$\{encodeURIComponent\(database\.dat assert.match(adminHeader, /export function AdminHeader/); assert.match(adminHeader, /titleAction\?: ReactNode/); assert.match(adminHeader, /titleAction \?
/); +assert.match(adminHeader, /import Link from "next\/link";/); +assert.match(adminHeader, /href="\/dashboard" aria-label="Back to dashboard"/); assert.match(adminHeader, /Kinic Wiki/); assert.match(rootLayout, //); assert.match(rootLayout, //); assert.match(appHeader, /usePathname/); -assert.match(appHeader, /pathname !== "\/" && pathname !== "\/cycles"/); +assert.match(appHeader, /pathname !== "\/dashboard" && pathname !== "\/cycles"/); assert.match(appHeader, /title = pathname === "\/cycles" \? "Database cycles purchase" : "Database dashboard"/); assert.match(appHeader, /Database dashboard/); +assert.doesNotMatch(appHeader, /\}>/); -assert.match(homePage, //); +assert.match(homePage, /Kinic Wiki is AI memory for agents/); +assert.match(homePage, /]*>kinic-vfs-cli<\/code> is the primary interface/); +assert.match(homePage, /import heroImage from "\.\/home-hero\.png";/); +assert.equal(existsSync(new URL("../app/home-hero.png", import.meta.url)), true); +assert.match(homePage, /Dashboard/); +assert.match(homePage, /Open Dashboard/); +assert.equal([...homePage.matchAll(/href="\/dashboard"/g)].length, 2); +assert.match(homeHeroSection, /Install CLI/); +assert.doesNotMatch(homeHeroSection, /Open Dashboard/); +assert.doesNotMatch(homeHeroSection, /Open Official Wiki/); +assert.match(homePage, /Chrome Extension/); +assert.match(homePage, /ChatGPT\/Claude conversations/); +assert.match(homePage, /raw sources/); +assert.match(homePage, /URL ingest requests/); +assert.match(homePage, /writer access/); +assert.match(homePage, /use the CLI to turn raw chats into organized \/Wiki pages/); +assert.match(homePage, /\/Sources\/ingest-requests\/\.\.\./); +assert.match(homePage, /\/Sources\/raw\/\.\.\./); +assert.match(homePage, /Open Official Wiki/); +assert.match(homePage, /Meet Kinic Wiki/); +assert.match(homePage, /Agent CLI workflow/); +assert.match(homePage, /Create databases/); +assert.match(homePage, /Manage access and cycles/); +assert.match(homePage, /Browse and edit/); +assert.doesNotMatch(homePage, /Companion UI/); +assert.doesNotMatch(homePage, /title: "Connect an agent"/); +assert.doesNotMatch(homePage, /title: "Search and read"/); +assert.doesNotMatch(homePage, /title: "Write with guards"/); +assert.doesNotMatch(homePage, /--expected-etag/); assert.doesNotMatch(homePage, /useSearchParams/); -assert.doesNotMatch(homePageClient, /\}>/); +assert.match(dashboardIndex, //); +assert.doesNotMatch(dashboardHomeClient, //); assert.match(cliPage, /skill find/); assert.match(cliPage, /--identity-mode anonymous/); assert.match(cliPage, /const installCommands = \["npm install -g kinic-vfs-cli"\]/); +assert.doesNotMatch(cliPage, /Database dashboard/); +assert.doesNotMatch(cliPage, /ArrowLeft/); assert.match(cliPage, /const checkCommands = \["kinic-vfs-cli --version", "kinic-vfs-cli --help"\]/); assert.match(cliPage, /title="First Check"/); assert.match(cliGuideBlock, /navigator\.clipboard\.writeText\(copyValue \?\? commandText\)/); assert.match(cliGuideBlock, /Copy .* commands/); assert.match(cliGuideBlock, /absolute right-2 top-2/); -assert.match(dashboardIndex, //); +assert.doesNotMatch(dashboardIndex, //); assert.match(dashboardRoute, /params: Promise<\{ databaseId: string \}>/); assert.match(dashboardRoute, //); assert.match(dashboardClient, /export function DashboardDatabaseClient\(\{ databaseId \}/); assert.match(dashboardClient, / database\.member\)/); -assert.match(homePageClient, /publicDatabases = databases\.filter\(\(database\) => !database\.member && database\.publicReadable\)/); -assert.doesNotMatch(homePageClient, /Database dashboard/); -assert.match(homePageClient, //); -assert.match(homePageClient, /const \[createDialogOpen, setCreateDialogOpen\] = useState\(false\);/); -assert.match(homePageClient, /const \[newDatabaseName, setNewDatabaseName\] = useState\(""\);/); -assert.match(homePageClient, /const databaseNameInput = newDatabaseName\.trim\(\);/); -assert.match(homePageClient, /createDatabaseAuthenticated\(canisterId, authClient\.getIdentity\(\), databaseNameInput\)/); -assert.match(homePageClient, /useAppSession/); -assert.match(homePageClient, /authRefreshSeq/); -assert.match(homePageClient, /setWalletControlsLocked\(creating\)/); +assert.match(dashboardHomeClient, /myDatabases = databases\.filter\(\(database\) => database\.member\)/); +assert.match(dashboardHomeClient, /publicDatabases = databases\.filter\(\(database\) => !database\.member && database\.publicReadable\)/); +assert.doesNotMatch(dashboardHomeClient, /Database dashboard/); +assert.match(dashboardHomeClient, //); +assert.match(dashboardHomeClient, /const \[createDialogOpen, setCreateDialogOpen\] = useState\(false\);/); +assert.match(dashboardHomeClient, /const \[newDatabaseName, setNewDatabaseName\] = useState\(""\);/); +assert.match(dashboardHomeClient, /const databaseNameInput = newDatabaseName\.trim\(\);/); +assert.match(dashboardHomeClient, /createDatabaseAuthenticated\(canisterId, authClient\.getIdentity\(\), databaseNameInput\)/); +assert.match(dashboardHomeClient, /useAppSession/); +assert.doesNotMatch(dashboardHomeClient, /authRefreshSeq/); +assert.match(dashboardHomeClient, /setWalletControlsLocked\(creating\)/); assert.match(appSession, /connectOisyWallet/); assert.match(appSession, /connectPlugWallet/); assert.match(appSession, /getConnectedWalletKinicBalance/); @@ -183,34 +223,36 @@ assert.match(appSession, /safeSessionStorageRemove\(WALLET_SESSION_KEY\)/); assert.match(appSession, /provider: nextWallet\.provider/); assert.match(appSession, /principal: connectedWalletPrincipal\(nextWallet\)/); assert.match(appSession, /useState\(\(\) => readStoredWallet\(\)\)/); -assert.match(homePageClient, /await refreshWalletBalance\(wallet\);/); -assert.match(homePageClient, /purchaseCyclesWithOisy/); -assert.match(homePageClient, /purchaseCyclesWithPlug/); -assert.match(homePageClient, /CREATE_DATABASE_PURCHASE_KINIC = "1"/); -assert.match(homePageClient, /import \{ KINIC_LEDGER_FEE_E8S \} from "@\/lib\/cycles";/); -assert.match(homePageClient, /const paymentAmountE8s = createDatabasePurchaseAmountE8s\(\);/); -assert.match(homePageClient, /function createDatabaseRequiredBalanceE8s\(\): bigint/); -assert.match(homePageClient, /return createDatabasePurchaseAmountE8s\(\) \+ KINIC_LEDGER_FEE_E8S \* 2n;/); -assert.match(homePageClient, /Database created pending\. Requesting/); -assert.match(homePageClient, /Database created pending, but initial cycles purchase failed/); -assert.doesNotMatch(homePageClient, /purchaseQueryString\(\{ databaseId: result\.database_id \}\)/); -assert.doesNotMatch(homePageClient, /useRouter|router\.push/); -assert.match(homePageClient, /Connect OISY or Plug with at least \$\{formatTokenAmountFromE8s\(createDatabaseRequiredBalanceE8s\(\)\)\} before creating a database\./); -assert.match(homePageClient, /Create database requires at least \$\{formatTokenAmountFromE8s\(createDatabaseRequiredBalanceE8s\(\)\)\} in the connected wallet\./); -assert.doesNotMatch(homePageClient, /Checking KINIC balance|Create database will request the first cycles purchase|walletFundingMessage/); -assert.match(homePageClient, /await purchaseCyclesWithOisy\(\{ canisterId, databaseId: result\.database_id, paymentAmountE8s \}, wallet\.connection\)/); -assert.match(homePageClient, /await purchaseCyclesWithPlug\(\{ canisterId, databaseId: result\.database_id, paymentAmountE8s \}, wallet\.connection\)/); -assert.match(homePageClient, /const walletReadyToFundCreate = walletCanFundCreate\(walletBalance\);/); -assert.match(homePageClient, /const createUnavailable = loadState === "loading" \|\| walletBusyProvider !== null \|\| walletBalanceLoading \|\| !walletReadyToFundCreate;/); -assert.match(homePageClient, /function walletCanFundCreate\(balanceE8s: string \| null\): boolean/); -assert.match(homePageClient, /return BigInt\(balanceE8s\) >= createDatabaseRequiredBalanceE8s\(\);/); -assert.match(homePageClient, /function databaseCreateButtonLabel/); -assert.match(homePageClient, /return "Connect wallet first"/); -assert.match(homePageClient, /return "Checking balance\.\.\."/); -assert.match(homePageClient, /return "Insufficient KINIC"/); -assert.match(homePageClient, /return "Create and fund database"/); -assert.match(homePageClient, /disabled=\{creating \|\| createUnavailable\}/); -assert.doesNotMatch(homePageClient, /= createDatabaseRequiredBalanceE8s\(\);/); +assert.match(dashboardHomeClient, /function databaseCreateButtonLabel/); +assert.match(dashboardHomeClient, /iiConnected: boolean;/); +assert.match(dashboardHomeClient, /return "Connect Internet Identity"/); +assert.match(dashboardHomeClient, /return "Connect OISY or Plug"/); +assert.match(dashboardHomeClient, /return "Checking balance\.\.\."/); +assert.match(dashboardHomeClient, /return "Insufficient KINIC"/); +assert.match(dashboardHomeClient, /return "Create and fund database"/); +assert.match(dashboardHomeClient, /disabled=\{creating \|\| createUnavailable\}/); +assert.doesNotMatch(dashboardHomeClient, /\n]*label="Cycles"/); assert.match(homeUi, /href=\{`\/dashboard\/\$\{encodeURIComponent\(databaseId\)\}`\}/); assert.match(homeUi, /Manage reservation/); -assert.doesNotMatch(homeUi, /href=\{`\/\$\{encodeURIComponent\(databaseId\)\}\/Wiki`\}/); -assert.match(homeUi, /!database\.member && database\.publicReadable \? `\$\{base\}\?read=anonymous` : base/); -assert.match(homeUi, /read=anonymous/); +assert.doesNotMatch(homeUi, /read=anonymous/); +assert.match(homeUi, /return `\/\$\{encodeURIComponent\(database\.databaseId\)\}\/Wiki`;/); assert.match(wikiBrowser, /"ingest"/); assert.match(wikiBrowser, / database\.databaseId\)\)/); +assert.match(wikiBrowser, /void \(readIdentity \? listDatabasesAuthenticated\(canisterId, readIdentity\) : Promise\.resolve\(\[\]\)\)/); +assert.doesNotMatch(wikiBrowser, /hrefForCurrentReadRoute/); +assert.doesNotMatch(wikiBrowser, /anonymousHref/); assert.match(ingestPanel, /createUrlIngestRequest/); assert.match(ingestPanel, /databaseCyclesError/); assert.match(ingestPanel, /const submitDisabled = busy \|\| !url\.trim\(\) \|\| Boolean\(databaseCyclesError\)/); @@ -354,8 +400,8 @@ assert.match(dashboardClient, /mergeDatabaseRows/); assert.match(dashboardClient, /Promise\.allSettled/); assert.match(dashboardClient, /Public database list unavailable/); assert.match(dashboardClient, /await refresh\(null, databaseId\);/); -assert.match(dashboardClient, /Select a database to manage/); -assert.match(dashboardClient, /Open Database dashboard/); +assert.doesNotMatch(dashboardClient, /Select a database to manage/); +assert.doesNotMatch(dashboardClient, /Open Database dashboard/); assert.doesNotMatch(dashboardClient, /Database id is missing\./); assert.match(dashboardClient, /databaseId && \(database \|\| principal\) \? /); assert.match(dashboardClient, /deleteDatabaseAuthenticated/); @@ -393,7 +439,7 @@ assert.match(dashboardDangerZone, /const deleteDisabled = props\.busy/); assert.match(dashboardDangerZone, /disabled=\{props\.busy \|\| !deleteConfirmed\}/); assert.match(vfsIdl, /const DeleteDatabaseRequest = idl\.Record/); assert.match(vfsIdl, /delete_database: idl\.Func\(\[DeleteDatabaseRequest\], \[ResultUnit\], \[\]\)/); -assert.doesNotMatch(homePageClient, /process\.env\.KINIC_WIKI_CANISTER_ID/); +assert.doesNotMatch(dashboardHomeClient, /process\.env\.KINIC_WIKI_CANISTER_ID/); assert.doesNotMatch(dashboardClient, /process\.env\.KINIC_WIKI_CANISTER_ID/); assert.match(dashboardUi, /type PendingAclAction/); @@ -437,7 +483,7 @@ assert.match(dashboardUi, /BookOpen/); assert.doesNotMatch(dashboardUi, /Minimum update/); assert.match(homeUi, /Cycles/); assert.match(homeUi, /Cycles/); -assert.match(homePageClient, /getCyclesBillingConfig/); +assert.match(dashboardHomeClient, /getCyclesBillingConfig/); assert.match(vfsClient, /export async function listDatabaseCycleEntries/); assert.match(vfsClient, /export async function listDatabaseCyclesPendingPurchasesAuthenticated/); assert.match(vfsClient, /normalizeDatabaseCycleEntryPage/); @@ -461,17 +507,19 @@ assert.match(dashboardActionButton, /hover:-translate-y-\[3px\]/); assert.match(dashboardClient, /busyAction/); assert.match(dashboardClient, /Access updated\./); -assert.match(homePageClient, /refreshSeqRef/); -assert.match(homePageClient, /isCurrentRefresh/); +assert.match(dashboardHomeClient, /refreshSeqRef/); +assert.match(dashboardHomeClient, /isCurrentRefresh/); assert.match(dashboardClient, /refreshSeqRef/); assert.match(dashboardClient, /isCurrentRefresh/); assert.match(appSession, /await authClient\.logout\(\);/); assert.match(appSession, /setPrincipal\(null\);/); assert.match(appSession, /clearWallet\(\);/); -assert.match(homePageClient, /createDatabaseAction=\{/); +assert.match(dashboardHomeClient, /const createDatabaseAction = \(/); +assert.match(dashboardHomeClient, /\s*/); assert.match(client, /SkillRegistryClient/); assert.match(adminHeader, /export function AdminHeader/); assert.match(client, / publicDatabasePath("cli"), /reserved database route slug: cli/); assert.equal( xShareDatabaseHref({ databaseId: "alpha db", databaseName: "Research DB", origin: "https://wiki.kinic.xyz" }), - "https://twitter.com/intent/tweet?text=Kinic+Wiki%3A+Research+DB&url=https%3A%2F%2Fwiki.kinic.xyz%2Falpha%2520db%2FWiki%3Fread%3Danonymous" + "https://twitter.com/intent/tweet?text=Kinic+Wiki%3A+Research+DB&url=https%3A%2F%2Fwiki.kinic.xyz%2Falpha%2520db%2FWiki" ); assert.equal( renderWikilinksAsMarkdown("[[/Sources/raw/a/a.md|opencode.ai/DESIGN.md]]"), diff --git a/wikibrowser/scripts/generate-vfs-idl.mjs b/wikibrowser/scripts/generate-vfs-idl.mjs index 9f808679..c3e91699 100644 --- a/wikibrowser/scripts/generate-vfs-idl.mjs +++ b/wikibrowser/scripts/generate-vfs-idl.mjs @@ -8,21 +8,6 @@ const root = join(here, "..", ".."); const didPath = join(root, "crates", "vfs_canister", "vfs.did"); const idlPath = join(here, "..", "lib", "vfs-idl.ts"); -const browserExpectedTypes = { - ...expectedTypes, - DatabaseStatus: { - kind: "variant", - cases: { - Hot: "null", - Pending: "null", - Active: "null", - Restoring: "null", - Archiving: "null", - Archived: "null" - } - } -}; - const typeOrder = [ "CanisterHealth", "DatabaseRole", @@ -197,7 +182,7 @@ function renderVfsIdl() { ]; for (const name of typeOrder) { - lines.push(...renderTypeConst(name, browserExpectedTypes[name])); + lines.push(...renderTypeConst(name, expectedTypes[name])); } lines.push(""); @@ -287,9 +272,9 @@ function validateDidSubset(source) { } function validateRenderOrder() { - const missingTypes = Object.keys(browserExpectedTypes).filter((name) => !typeOrder.includes(name)); + const missingTypes = Object.keys(expectedTypes).filter((name) => !typeOrder.includes(name)); const missingMethods = Object.keys(expectedMethods).filter((name) => !methodOrder.includes(name)); - const unknownTypes = typeOrder.filter((name) => !(name in browserExpectedTypes)); + const unknownTypes = typeOrder.filter((name) => !(name in expectedTypes)); const unknownMethods = methodOrder.filter((name) => !(name in expectedMethods)); const failures = [ ...missingTypes.map((name) => `typeOrder missing ${name}`), diff --git a/wikibrowser/scripts/smoke-mobile-layout.mjs b/wikibrowser/scripts/smoke-mobile-layout.mjs index 2c7f0ce1..ffe477c9 100644 --- a/wikibrowser/scripts/smoke-mobile-layout.mjs +++ b/wikibrowser/scripts/smoke-mobile-layout.mjs @@ -8,26 +8,26 @@ const viewports = [ [390, 844] ]; const browserRoutes = [ - "/Wiki?read=anonymous", - "/Wiki?read=anonymous&view=raw", - "/Wiki?read=anonymous&view=edit", - "/Wiki?read=anonymous&tab=query", - "/Wiki?read=anonymous&tab=ingest", - "/search?q=Wiki&kind=path&read=anonymous", - "/help?read=anonymous", - "/graph?center=%2FWiki&depth=1&read=anonymous", - "/graph?read=anonymous" + "/Wiki", + "/Wiki?view=raw", + "/Wiki?view=edit", + "/Wiki?tab=query", + "/Wiki?tab=ingest", + "/search?q=Wiki&kind=path", + "/help", + "/graph?center=%2FWiki&depth=1", + "/graph" ]; runOptional("close", []); -run("open", [`${baseUrl}/${encodeURIComponent(databaseId)}/Wiki?read=anonymous`]); +run("open", [`${baseUrl}/${encodeURIComponent(databaseId)}/Wiki`]); for (const [width, height] of viewports) { run("resize", [String(width), String(height)]); for (const route of browserRoutes) { run("goto", [`${baseUrl}/${encodeURIComponent(databaseId)}${route}`]); run("eval", [wikiLayoutProbe(route)]); } - run("goto", [baseUrl]); + run("goto", [`${baseUrl}/dashboard`]); run("eval", [dashboardLayoutProbe()]); run("goto", [`${baseUrl}/dashboard/${encodeURIComponent(databaseId)}`]); run("eval", [genericLayoutProbe()]); diff --git a/wikibrowser/tsconfig.json b/wikibrowser/tsconfig.json index 0291d909..3099c541 100644 --- a/wikibrowser/tsconfig.json +++ b/wikibrowser/tsconfig.json @@ -32,7 +32,8 @@ "next-env.d.ts", "**/*.ts", "**/*.tsx", - ".next/types/**/*.ts" + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" ], "exclude": [ "node_modules"