10.3 KiB · text History f5b246f
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
  <section class="py-20 sm:py-28 text-center">
    <p class="font-mono text-xs uppercase tracking-[0.25em] text-accent mb-4">DMS</p>
    <h1 class="text-4xl sm:text-6xl font-bold tracking-tight">
      Stash images. Find them with a phrase.
    </h1>
    <p class="text-base-content/70 text-lg sm:text-xl mt-6 max-w-2xl mx-auto leading-relaxed">
      Self-hosted Dank Macro Stash for screenshots and photos. Every upload
      fans out to a queue of categorizers — thumbnails, OCR, image description,
      tag extraction, similarity. Anything any pipeline notices becomes a tag,
      so a year from now "McMaster catalog cover" still finds it.
    </p>
    <div class="flex justify-center gap-3 mt-8 flex-wrap">
      {% if user %}
        <a href="/uploads" class="btn btn-primary">
          <i class="fas fa-fw fa-images"></i> Open uploads
        </a>
        <a href="/search" class="btn btn-ghost">
          <i class="fas fa-fw fa-magnifying-glass"></i> Search
        </a>
        <a href="/api-tokens" class="btn btn-ghost">
          <i class="fas fa-fw fa-mobile-screen"></i> Pair a device
        </a>
      {% else %}
        {% if registration_open %}
          <a href="/register" class="btn btn-primary">Get started</a>
        {% else %}
          <span class="btn btn-disabled" title="Registration is invite-only">
            Get started · invite-only
          </span>
        {% endif %}
        <a href="/login" class="btn btn-ghost">Log in</a>
      {% endif %}
    </div>
  </section>

  <section class="py-12 sm:py-16">
    <h2 class="text-2xl sm:text-3xl font-bold mb-2">What you get</h2>
    <p class="text-base-content/70 mb-8">
      A document-management system that does the boring parts — categorizing,
      tagging, indexing — so a year from now you can still find that one
      screenshot.
    </p>

    <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
      <div class="border border-base-300 rounded-box p-5 bg-base-200">
        <div class="flex items-center gap-2 mb-2">
          <i class="fas fa-fw fa-bolt size-5 text-accent"></i>
          <h3 class="font-semibold">Five auto-categorizers</h3>
        </div>
        <p class="text-sm text-base-content/80">
          Every upload fans out to thumbnail, OCR (<a href="https://github.com/robertknight/ocrs" class="link">ocrs</a>),
          image describe (VLM), tag extraction, and similarity-search. Each
          pipeline can be re-run on demand from the detail page.
        </p>
      </div>

      <div class="border border-base-300 rounded-box p-5 bg-base-200">
        <div class="flex items-center gap-2 mb-2">
          <i class="fas fa-fw fa-tags size-5 text-accent"></i>
          <h3 class="font-semibold">Namespaced tags</h3>
        </div>
        <p class="text-sm text-base-content/80">
          <code>desc:dog</code>, <code>Series:Friends</code>, <code>ocr:mcmaster</code>.
          Tags carry their origin so categorizer output never collides with
          your own labels — and you can filter by namespace.
        </p>
      </div>

      <div class="border border-base-300 rounded-box p-5 bg-base-200">
        <div class="flex items-center gap-2 mb-2">
          <i class="fas fa-fw fa-magnifying-glass size-5 text-accent"></i>
          <h3 class="font-semibold">Phrase search</h3>
        </div>
        <p class="text-sm text-base-content/80">
          Search by file name, item description, or any tag from any pipeline.
          OCR text becomes <code>ocr:*</code> tags, descriptions become
          <code>desc:*</code> — toggle sources per query, or use <code>ns:*</code>
          to enumerate every value in a namespace.
        </p>
      </div>

      <div class="border border-base-300 rounded-box p-5 bg-base-200">
        <div class="flex items-center gap-2 mb-2">
          <i class="fas fa-fw fa-mobile-screen size-5 text-accent"></i>
          <h3 class="font-semibold">Android companion</h3>
        </div>
        <p class="text-sm text-base-content/80">
          QR-pair from <a href="/api-tokens" class="link">/api-tokens</a> and
          you're in. Share images straight from the gallery into DMS, search and
          retag from your pocket, share back out via the system sheet. Uploads
          run in a foreground service — fire and forget.
        </p>
      </div>

      <div class="border border-base-300 rounded-box p-5 bg-base-200">
        <div class="flex items-center gap-2 mb-2">
          <i class="fas fa-fw fa-envelope-open-text size-5 text-accent"></i>
          <h3 class="font-semibold">Invite-only by default</h3>
        </div>
        <p class="text-sm text-base-content/80">
          Public registration is off until you flip <code>REGISTRATION_ENABLED</code>.
          Generate scoped invite links from <a href="/invites" class="link">/invites</a>
          — optional email pin, optional expiry, one-click revoke.
        </p>
      </div>

      <div class="border border-base-300 rounded-box p-5 bg-base-200">
        <div class="flex items-center gap-2 mb-2">
          <i class="fas fa-fw fa-layer-group size-5 text-accent"></i>
          <h3 class="font-semibold">Workers scale horizontally</h3>
        </div>
        <p class="text-sm text-base-content/80">
          RabbitMQ competing-consumer queues per categorizer. Run <code>just worker
          ocr</code> N times across N machines — work is distributed; failures
          dead-letter; idempotency is per-job.
        </p>
      </div>

      <div class="border border-base-300 rounded-box p-5 bg-base-200">
        <div class="flex items-center gap-2 mb-2">
          <i class="fas fa-fw fa-gear fa-spin size-5 text-accent"></i>
          <h3 class="font-semibold">Operable</h3>
        </div>
        <p class="text-sm text-base-content/80">
          The <a href="/failed" class="link">Work</a> page shows live queue
          depth, consumer counts, recent completions per kind, and every failed
          job. One click retries a job, drains a DLQ back into the main queue,
          or replays everything at once.
        </p>
      </div>

      <div class="border border-base-300 rounded-box p-5 bg-base-200">
        <div class="flex items-center gap-2 mb-2">
          <i class="fas fa-fw fa-fingerprint size-5 text-accent"></i>
          <h3 class="font-semibold">Content-addressed storage</h3>
        </div>
        <p class="text-sm text-base-content/80">
          Files land at <code>sha256/aa/bb/&lt;hash&gt;</code>. Re-uploading the
          same image short-circuits to the existing record — no duplicate
          bytes, no duplicate categorizer runs.
        </p>
      </div>

      <div class="border border-base-300 rounded-box p-5 bg-base-200">
        <div class="flex items-center gap-2 mb-2">
          <i class="fas fa-fw fa-code size-5 text-accent"></i>
          <h3 class="font-semibold">JSON API parity</h3>
        </div>
        <p class="text-sm text-base-content/80">
          Every HTML surface — uploads, search, tags, retries, DLQ requeue —
          has a JSON twin under <code>/api</code>, authenticated with the same
          bearer token the Android app uses. Build whatever client you want.
        </p>
      </div>
    </div>
  </section>

  <section class="py-12 sm:py-16">
    <h2 class="text-2xl sm:text-3xl font-bold mb-2">Three steps to searchable</h2>
    <p class="text-base-content/70 mb-8">
      From "I have an image" to "I can find that image again" — the only
      manual step is the upload.
    </p>

    <ol class="grid grid-cols-1 sm:grid-cols-3 gap-4">
      <li class="border border-base-300 rounded-box p-5 bg-base-200 flex flex-col gap-2">
        <span class="font-mono text-xs text-accent">01</span>
        <h3 class="font-semibold">Get in</h3>
        <p class="text-sm text-base-content/80">
          {% if registration_open %}
            Sign up directly — registration is open.
          {% else %}
            DMS is invite-only. Ask an existing user to generate a link from <a href="/invites" class="link">/invites</a>, or run <code>just invite</code> on the server.
          {% endif %}
        </p>
      </li>
      <li class="border border-base-300 rounded-box p-5 bg-base-200 flex flex-col gap-2">
        <span class="font-mono text-xs text-accent">02</span>
        <h3 class="font-semibold">Drop something in</h3>
        <p class="text-sm text-base-content/80">
          Upload from <a href="/uploads" class="link">the web</a>, or pair the
          Android app from <a href="/api-tokens" class="link">/api-tokens</a>
          and share images straight from your gallery. Categorizer jobs fire off
          in the background — watch the status grid on the detail page.
        </p>
      </li>
      <li class="border border-base-300 rounded-box p-5 bg-base-200 flex flex-col gap-2">
        <span class="font-mono text-xs text-accent">03</span>
        <h3 class="font-semibold">Find it again</h3>
        <p class="text-sm text-base-content/80">
          Filter by free-text, tag, or namespace. Click any tag on a detail
          page to pivot to other uploads sharing it. Re-run a pipeline anytime
          if you swap in a smarter model.
        </p>
      </li>
    </ol>
  </section>

  <section class="py-12 sm:py-16 text-center">
    <h2 class="text-2xl sm:text-3xl font-bold mb-2">Built on Rust</h2>
    <p class="text-base-content/70 max-w-2xl mx-auto">
      axum + sea-orm on Postgres, RabbitMQ for the categorizer queues,
      <a href="https://github.com/robertknight/ocrs" class="link">ocrs</a>
      for OCR, minijinja + Tailwind + DaisyUI on the front, a Kotlin/Compose
      Android client. Three binaries: webservice, queue-worker,
      registration-token.
    </p>
  </section>

  <footer class="py-10 sm:py-12 mt-8 border-t border-base-300 text-center">
    <p class="font-mono text-xs uppercase tracking-[0.25em] text-base-content/70 flex items-center justify-center gap-2">
      <span class="font-bold tracking-[0.16em]">DMS</span>
      <span class="opacity-50">·</span>
      <span>a</span>
      {# Hardcoded oklch from the neiam-co accent (the value every
         ported theme assigns to --color-accent). Inline so the
         NEIAM mark stays yellow regardless of which theme the
         viewer has picked. #}
      <span class="font-bold tracking-[0.16em]" style="color: oklch(96% 0.058 96);">
        <a href="https://neiam.org"> NEIAM </a>
      </span>
      <span>production</span>
    </p>
  </footer>
</div>