<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/<hash></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>
⭐
Log in to mark this repository.
10.3 KiB · text
History
c20d4f7