defter 0.1.0

Self-hosted Zotero sync and file storage server, with metadata in SQLite and attachments in S3.
<script>
  import { formatBytes, formatCount } from './lib/format.js'

  let status = $state(null)
  let error = $state(null)

  async function load() {
    try {
      const resp = await fetch('/ui/status')
      if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
      status = await resp.json()
      error = null
    } catch (e) {
      error = e.message
    }
  }

  $effect(() => {
    load()
    const timer = setInterval(load, 30000)
    return () => clearInterval(timer)
  })

  const stats = $derived(
    status
      ? [
          { label: 'Items', value: formatCount(status.library.items) },
          { label: 'Attachments', value: formatCount(status.library.attachments) },
          { label: 'Notes', value: formatCount(status.library.notes) },
          { label: 'Collections', value: formatCount(status.library.collections) },
          { label: 'Tags', value: formatCount(status.library.tags) },
          { label: 'In trash', value: formatCount(status.library.trashed) },
          { label: 'Files', value: formatCount(status.library.files) },
          { label: 'Stored', value: formatBytes(status.library.fileBytes) },
        ]
      : [],
  )
</script>

{#if error}
  <p class="rounded-md border border-destructive/50 p-4 text-destructive">
    Could not load the server status: {error}
  </p>
{:else if !status}
  <p class="text-muted-foreground">Loading…</p>
{:else}
  <section class="space-y-2">
    <h1 class="text-2xl font-semibold tracking-tight">Library</h1>
    <p class="text-sm text-muted-foreground">
      Library version {formatCount(status.library.version)} · user {status.userId} ·
      defter {status.version} · commonmeta {status.commonmetaVersion}
    </p>
  </section>

  <section class="mt-6 grid grid-cols-2 gap-3 sm:grid-cols-4">
    {#each stats as stat (stat.label)}
      <div class="rounded-lg border bg-card p-4">
        <div class="text-xs uppercase tracking-wide text-muted-foreground">{stat.label}</div>
        <div class="mt-1 text-2xl font-semibold tabular-nums">{stat.value}</div>
      </div>
    {/each}
  </section>

  <section class="mt-8 rounded-lg border bg-card p-5 space-y-2">
    <h2 class="font-semibold">Attachment storage</h2>
    <p class="text-sm">
      <code class="rounded bg-muted px-1.5 py-0.5">{status.storage.location}</code>
    </p>
    <p class="text-sm text-muted-foreground">
      {status.storage.signedDownloads
        ? 'Clients download files directly from the bucket through pre-signed URLs.'
        : 'Downloads are streamed through this server.'}
      Files up to {formatBytes(status.storage.maxUpload)} are accepted.
    </p>
  </section>

  <section class="mt-4 rounded-lg border bg-card p-5 space-y-3">
    <h2 class="font-semibold">Connect Zotero</h2>
    <p class="text-sm text-muted-foreground">
      In Zotero, open Settings → Advanced → Config Editor and set:
    </p>
    <pre class="overflow-x-auto rounded-md bg-muted p-3 text-sm"><code
        >extensions.zotero.api.url = {status.apiUrl}/
extensions.zotero.streaming.enabled = false</code
      ></pre>
    <p class="text-sm text-muted-foreground">
      Then log in under Settings → Sync and approve the request on this server.
    </p>
  </section>

  <section class="mt-4 rounded-lg border bg-card p-5 space-y-3">
    <h2 class="font-semibold">Enrich with commonmeta</h2>
    <p class="text-sm text-muted-foreground">
      commonmeta sync talks to the same API (see <code>docs/commonmeta-rs-changes.md</code> for
      the commonmeta release that supports a remote server):
    </p>
    <pre class="overflow-x-auto rounded-md bg-muted p-3 text-sm"><code
        >commonmeta sync --zotero-url {status.apiUrl}</code
      ></pre>
  </section>
{/if}