defter 0.8.5

Self-hosted Zotero sync and file storage server, with metadata in SQLite and attachments in S3.
{% extends "base.html" %}

{% block title %}{{ t.library }}{% endblock %}

{% block content %}
<p class="mb-4 flex flex-wrap items-baseline justify-between gap-2 text-sm text-muted-foreground">
  <span>{{ t.signed_in_as }} <span class="font-medium text-foreground">{{ name }}</span></span>
</p>
{#- Rendered alone for htmx's poll (see ui::dashboard), which swaps it in
    place of itself every 30 seconds. -#}
{% block status %}
<div id="status" hx-get="/ui/settings" hx-trigger="every 30s" hx-swap="outerHTML">
  <section class="space-y-2">
    <h1 class="text-2xl font-semibold tracking-tight">{{ t.library }}</h1>
    <p class="text-sm text-muted-foreground">
      {{ t.library_version }} {{ status.version }} · {{ t.user }} {{ status.user_id }} ·
      defter {{ status.defter_version }} · commonmeta {{ status.commonmeta_version }}
    </p>
  </section>

  <section class="mt-6 grid grid-cols-2 gap-3 sm:grid-cols-4">
    {%- for stat in status.stats %}
    <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>
    {%- endfor %}
  </section>

  <section class="mt-8 rounded-lg border bg-card p-5 space-y-2">
    <h2 class="font-semibold">{{ t.attachment_storage }}</h2>
    <p class="text-sm">{{ t.storage_used }}: <span class="tabular-nums">{{ status.usage }}</span></p>
    {%- if status.over_quota %}
    <p role="alert" class="text-sm text-destructive">{{ t.over_quota }}</p>
    {%- endif %}
    <p class="text-sm text-muted-foreground">
      {%- if status.admin %}
      {%- if status.signed_downloads %}
      {{ t.signed_downloads }}
      {%- else %}
      {{ t.streamed_downloads }}
      {%- endif %}
      {%- endif %}
      {{ t.files_up_to }} {{ status.max_upload }} {{ t.are_accepted }}
    </p>
  </section>

  {%- if status.admin %}
  <section class="mt-4 rounded-lg border bg-card p-5 space-y-2">
    <h2 class="font-semibold">{{ t.backups }}</h2>
    <p class="text-sm {% if status.backup_failed %}text-destructive{% else %}text-muted-foreground{% endif %}">{{ status.backup }}</p>
  </section>
  {%- endif %}
</div>
{% endblock %}

{%- if let Some(profile) = profile %}
<section class="mt-4 rounded-lg border bg-card p-5 space-y-3">
  <h2 class="font-semibold">{{ t.public_profile }}</h2>
  <p class="text-sm"><a href="{{ profile.url }}" class="underline">{{ profile.url }}</a></p>
  <p class="text-sm text-muted-foreground">
    {%- if profile.hidden %}
    {{ t.profile_hidden }}
    {%- else %}
    {{ t.profile_visible }}
    {%- endif %}
    {{ t.it_lists }} {{ profile.public }}{{ t.all_of_my_publications }}
  </p>
  {%- if can_write %}
  <form method="post" action="/ui/profile">
    <input type="hidden" name="hidden" value="{% if profile.hidden %}false{% else %}true{% endif %}">
    <button type="submit" class="rounded-md border px-3 py-1.5 text-sm hover:bg-muted">
      {%- if profile.hidden %}{{ t.show_profile }}{% else %}{{ t.hide_profile }}{% endif -%}
    </button>
  </form>
  {%- endif %}
</section>
{%- endif %}

<section class="mt-4 rounded-lg border bg-card p-5 space-y-3">
  <h2 class="font-semibold">{{ t.groups }}</h2>
  <p class="text-sm text-muted-foreground">{{ t.groups_intro }} <a href="https://www.zotero.org/groups" target="_blank" rel="noreferrer" class="underline">zotero.org</a>. <a href="/ui/groups" class="underline">{{ t.all_groups }}</a></p>
  {%- if groups.is_empty() %}
  <p class="text-sm text-muted-foreground">{{ t.no_groups }}</p>
  {%- else %}
  <ul class="divide-y text-sm">
    {%- for group in groups %}
    <li class="flex items-baseline justify-between gap-2 py-2">
      <a href="/ui/groups/{{ group.id }}" class="font-medium underline">{{ group.name }}</a>
      <span class="text-muted-foreground">{{ group.role }}</span>
    </li>
    {%- endfor %}
  </ul>
  {%- endif %}
</section>

<section class="mt-4 rounded-lg border bg-card p-5 space-y-3">
  <h2 class="font-semibold">{{ t.devices }}</h2>
  {%- if devices.is_empty() %}
  <p class="text-sm text-muted-foreground">{{ t.no_devices }}</p>
  {%- else %}
  <p class="text-sm text-muted-foreground">{{ t.devices_intro }}</p>
  <ul class="divide-y text-sm">
    {%- for device in devices %}
    <li class="flex flex-wrap items-center justify-between gap-2 py-2">
      <span>
        <span class="font-medium">{{ device.name }}</span>
        <code class="ml-1 rounded bg-muted px-1.5 py-0.5 text-xs">{{ device.prefix }}…</code>
        <span class="block text-muted-foreground">
          {{ t.added }} {{ device.added }} ·
          {%- if let Some(last_used) = device.last_used %} {{ t.last_used }} {{ last_used }}{% else %} {{ t.never_used }}{% endif %}
        </span>
      </span>
      {%- if can_write %}
      <form method="post" action="/ui/keys/revoke">
        <input type="hidden" name="id" value="{{ device.id }}">
        <button type="submit" class="rounded-md border px-3 py-1.5 text-sm hover:bg-muted">{{ t.revoke }}</button>
      </form>
      {%- endif %}
    </li>
    {%- endfor %}
  </ul>
  {%- endif %}
</section>

<section class="mt-4 rounded-lg border bg-card p-5 space-y-3">
  <h2 class="font-semibold">{{ t.connect_zotero }}</h2>
  <p class="text-sm text-muted-foreground">{{ t.connect_intro }}</p>
  <ol class="list-decimal space-y-1 pl-5 text-sm">
    <li>{{ t.connect_download }} <a href="{{ status.plugin_url }}" class="underline">Defter</a> for Zotero{{ t.connect_install }}</li>
    <li>{{ t.connect_server }} <code class="rounded bg-muted px-1.5 py-0.5">{{ status.server_url }}</code></li>
    <li>{{ t.connect_login }}</li>
  </ol>
</section>

{%- if let Some(sync) = sync %}
<section class="mt-4 rounded-lg border bg-card p-5 space-y-3">
  <h2 class="font-semibold">{{ t.sync_heading }}</h2>
  <p class="text-sm text-muted-foreground">{{ t.sync_intro }}</p>
  {%- if can_write %}
  <form method="post" action="/ui/sync" class="space-y-2 text-sm">
    <label class="flex items-start gap-2">
      <input type="checkbox" name="publications" class="mt-1"
        {%- if sync.setting.publications %} checked{% endif %}{% if !sync.has_orcid %} disabled{% endif %}>
      <span>{{ t.sync_publications }}
        {%- if !sync.has_orcid %} <span class="block text-muted-foreground">{{ t.sync_needs_orcid }}</span>{% endif %}</span>
    </label>
    <label class="flex items-start gap-2">
      <input type="checkbox" name="metadata" class="mt-1"{% if sync.setting.metadata %} checked{% endif %}>
      <span>{{ t.sync_metadata }}</span>
    </label>
    <button type="submit" class="rounded-md border px-3 py-1.5 text-sm hover:bg-muted">{{ t.save }}</button>
  </form>
  <div class="flex flex-wrap gap-2">
    <form method="post" action="/ui/sync/run">
      <input type="hidden" name="dry_run" value="true">
      <button type="submit" class="rounded-md border px-3 py-1.5 text-sm hover:bg-muted">{{ t.sync_preview }}</button>
    </form>
    {%- if sync.setting.any() %}
    <form method="post" action="/ui/sync/run">
      <input type="hidden" name="dry_run" value="false">
      <button type="submit" class="rounded-md border px-3 py-1.5 text-sm hover:bg-muted">{{ t.sync_now }}</button>
    </form>
    {%- endif %}
  </div>
  {%- endif %}
  {%- if sync.busy %}
  <p role="alert" class="text-sm text-destructive">{{ t.sync_busy }}</p>
  {%- endif %}
  {%- if sync.running %}
  <p class="text-sm">{{ t.sync_running }}</p>
  {%- endif %}
  {%- if let Some(run) = sync.last %}
  <p class="text-sm">
    <span class="{% if run.failed %}text-destructive{% else %}text-muted-foreground{% endif %}">{{ run.label }} {{ run.at }}, {{ run.status }}.</span>
    {{ run.counts }}.
  </p>
  {%- if let Some(error) = run.error %}
  <p role="alert" class="text-sm text-destructive">{{ error }}</p>
  {%- endif %}
  {%- if !run.results.is_empty() %}
  <p class="text-sm"><a href="/ui/sync/log.json" class="underline">{{ t.sync_log }}</a></p>
  <details class="text-sm">
    <summary class="cursor-pointer">{{ t.sync_details }}</summary>
    <ul class="mt-2 divide-y">
      {%- for result in run.results %}
      <li class="py-1.5">
        {%- if !result.key.is_empty() %}<code class="rounded bg-muted px-1.5 py-0.5 text-xs">{{ result.key }}</code> {% endif %}
        {%- if !result.doi.is_empty() %}<a href="https://doi.org/{{ result.doi }}" class="underline">{{ result.doi }}</a> {% endif %}
        <span class="text-muted-foreground">{{ result.status }}</span>
        {%- if !result.detail.is_empty() %}<span class="block text-muted-foreground">{{ result.detail }}</span>{% endif %}
      </li>
      {%- endfor %}
    </ul>
    {%- if run.more > 0 %}
    <p class="mt-1 text-muted-foreground">{{ run.more }} {{ t.sync_more }}</p>
    {%- endif %}
  </details>
  {%- endif %}
  {%- else %}
  <p class="text-sm text-muted-foreground">{{ t.sync_never }}</p>
  {%- endif %}
</section>
{%- endif %}
{% endblock %}