feat(kez-chat/web): dashboard shows verified claims

The dashboard's Claims section used to be a "here's what claims are"
teaser with a link to /claims. Now it shows the actual verified rows:

  • Empty state → "+ Add your first claim" CTA
  • Claims exist, none verified → amber callout pointing at Re-verify
  • Verified claims → green rows, one per verified claim:
        [GitHub]  github:tudisco
        ✓ Verified via public gist                          proof ↗
  • Trailing summary if relevant: "1 failed verification. 2 not yet
    checked. See the claims page for details."

A "Re-verify all" button at the section header re-runs every verifier
in place, so the dashboard stays fresh without a round-trip to /claims.

Uses $derived to bucket claims by verification status. $state.snapshot
before passing each claim to the verifier (same proxy/structuredClone
gotcha as the Claims page).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Jason Tudisco 2026-05-25 15:34:52 -06:00
parent a8036cc392
commit 109852ed75

View File

@ -4,16 +4,37 @@
import { bytesToHex } from "@noble/hashes/utils"; import { bytesToHex } from "@noble/hashes/utils";
import { lookup, ApiError } from "../lib/api.js"; import { lookup, ApiError } from "../lib/api.js";
import { session } from "../lib/store.svelte.js"; import { session } from "../lib/store.svelte.js";
import {
listClaims,
setVerifyResult,
type StoredClaim,
} from "../lib/claims-store.js";
import { verifyClaim } from "../lib/verify.js";
let registryRecord = $state<any | null>(null); let registryRecord = $state<any | null>(null);
let loading = $state(true); let loading = $state(true);
let error = $state<string | null>(null); let error = $state<string | null>(null);
let claims = $state<StoredClaim[]>([]);
let verifyingAll = $state(false);
// Derived buckets for the verified-claims section.
const verifiedClaims = $derived(
claims.filter((c) => c.last_verify?.status === "ok"),
);
const failedClaims = $derived(
claims.filter((c) => c.last_verify?.status === "fail"),
);
const unverifiedClaims = $derived(
claims.filter((c) => !c.last_verify),
);
onMount(async () => { onMount(async () => {
if (!session.unlocked) { if (!session.unlocked) {
push("/unlock"); push("/unlock");
return; return;
} }
claims = await listClaims();
try { try {
registryRecord = await lookup(session.unlocked.handle); registryRecord = await lookup(session.unlocked.handle);
} catch (e) { } catch (e) {
@ -27,6 +48,33 @@
} }
}); });
async function reverifyAll() {
verifyingAll = true;
try {
for (const c of claims) {
const result = await verifyClaim($state.snapshot(c) as StoredClaim);
await setVerifyResult(c.id, result);
}
claims = await listClaims();
} finally {
verifyingAll = false;
}
}
/** Friendly label per channel — used in the verified-claims row. */
function channelLabel(ch: string): string {
return (
{
github: "GitHub",
dns: "DNS",
web: "Website",
nostr: "Nostr",
bluesky: "Bluesky",
ap: "ActivityPub",
} as Record<string, string>
)[ch] ?? ch;
}
function showSeed() { function showSeed() {
if (!session.unlocked) return; if (!session.unlocked) return;
const hex = bytesToHex(session.unlocked.seed); const hex = bytesToHex(session.unlocked.seed);
@ -64,21 +112,95 @@
</section> </section>
<section class="border border-gray-200 rounded-lg p-6 bg-white"> <section class="border border-gray-200 rounded-lg p-6 bg-white">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between gap-4 mb-4">
<div> <div>
<p class="text-xs text-gray-500 uppercase tracking-wide">Claims</p> <p class="text-xs text-gray-500 uppercase tracking-wide">
Verified claims
</p>
<p class="text-sm text-gray-700 mt-1"> <p class="text-sm text-gray-700 mt-1">
Link other accounts (GitHub, your domain, nostr, Bluesky, ActivityPub) Accounts cryptographically linked to
to your KEZ identity by signing claims and publishing the proofs. <span class="font-mono">{session.unlocked.handle}@{session.unlocked.server}</span>.
Anyone can re-verify these without trusting this server.
</p> </p>
</div> </div>
<a <div class="flex flex-col gap-2 shrink-0">
href="#/claims" {#if claims.length > 0}
class="px-3 py-2 text-sm bg-gray-900 text-white rounded-md hover:bg-gray-700 no-underline" <button
> class="px-3 py-1.5 text-sm border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 disabled:opacity-50"
Manage claims → onclick={reverifyAll}
</a> disabled={verifyingAll}
>
{verifyingAll ? "Verifying…" : "Re-verify all"}
</button>
{/if}
<a
href="#/claims"
class="px-3 py-1.5 text-sm bg-gray-900 text-white rounded-md hover:bg-gray-700 no-underline text-center"
>
Manage →
</a>
</div>
</div> </div>
{#if claims.length === 0}
<p class="text-sm text-gray-500 italic">
No claims yet. Link a GitHub account, a domain, your nostr identity,
or anywhere else you'd like to prove you control.
</p>
<a
href="#/claims/add"
class="mt-3 inline-block px-3 py-2 text-sm border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 no-underline"
>
+ Add your first claim
</a>
{:else if verifiedClaims.length === 0}
<p class="text-sm text-amber-800 bg-amber-50 border border-amber-200 rounded p-3">
You have {claims.length} claim{claims.length === 1 ? "" : "s"} but
none are verified yet. Publish the proofs on each channel and hit
<strong>Re-verify all</strong>.
</p>
{:else}
<ul class="space-y-2">
{#each verifiedClaims as c (c.id)}
<li class="flex items-center justify-between gap-3 p-3 bg-green-50 border border-green-200 rounded">
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 flex-wrap">
<span class="text-xs font-semibold text-green-800 bg-green-200 px-1.5 py-0.5 rounded">
{channelLabel(c.channel)}
</span>
<span class="font-mono text-sm text-gray-900 truncate">
{c.envelope.payload.subject}
</span>
</div>
<p class="mt-1 text-xs text-green-900">
{c.last_verify?.summary}
</p>
</div>
{#if c.last_verify?.evidence_url}
<a
href={c.last_verify.evidence_url}
target="_blank"
rel="noopener noreferrer"
class="text-xs text-green-700 hover:text-green-900 underline shrink-0"
>
proof ↗
</a>
{/if}
</li>
{/each}
</ul>
{#if failedClaims.length > 0 || unverifiedClaims.length > 0}
<p class="mt-3 text-xs text-gray-500">
{#if failedClaims.length > 0}
{failedClaims.length} failed verification.
{/if}
{#if unverifiedClaims.length > 0}
{unverifiedClaims.length} not yet checked.
{/if}
See the claims page for details.
</p>
{/if}
{/if}
</section> </section>
<section class="border border-gray-200 rounded-lg p-6 bg-white"> <section class="border border-gray-200 rounded-lg p-6 bg-white">