Files
Artifacto/internal/server/templates/dashboard.html
dtoro c7d7ee287c initial commit: Artifacto v0.1
Self-hosted HTML artifact publisher. Single Go binary, SQLite metadata,
HTML on disk. Features: password-protected artifacts, per-artifact expiration,
admin dashboard with view metrics (privacy-preserving daily-salt visitor hash,
sparklines, 30-day SVG chart, top referrers), rate-limited unlock endpoint.

Packaged as a Docker image (two-stage, CGO_ENABLED=0, pure-Go SQLite).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:45:57 +02:00

122 lines
6.4 KiB
HTML

{{ define "dashboard" }}
{{ template "head" "Dashboard" }}
{{ template "header" true }}
<main class="max-w-6xl mx-auto px-6 py-8">
<!-- Stat tiles -->
<section class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-8">
<div class="rounded border border-slate-800 bg-slate-900/50 px-4 py-3">
<div class="text-xs uppercase tracking-wide text-slate-400">Artifacts</div>
<div class="text-2xl font-semibold mt-1">{{ .Stats.TotalArtifacts }}</div>
</div>
<div class="rounded border border-slate-800 bg-slate-900/50 px-4 py-3">
<div class="text-xs uppercase tracking-wide text-slate-400">Views (7d)</div>
<div class="text-2xl font-semibold mt-1">{{ .Stats.Views7d }}</div>
</div>
<div class="rounded border border-slate-800 bg-slate-900/50 px-4 py-3">
<div class="text-xs uppercase tracking-wide text-slate-400">Views (30d)</div>
<div class="text-2xl font-semibold mt-1">{{ .Stats.Views30d }}</div>
</div>
<div class="rounded border border-slate-800 bg-slate-900/50 px-4 py-3">
<div class="text-xs uppercase tracking-wide text-slate-400">Storage</div>
<div class="text-2xl font-semibold mt-1">{{ humanSize .Stats.StorageBytes }}</div>
</div>
</section>
<!-- Publish form -->
<section class="rounded border border-slate-800 bg-slate-900/50 p-4 mb-8">
<h2 class="text-lg font-semibold mb-3">Publish</h2>
<form method="post" action="/api/publish" enctype="multipart/form-data" class="space-y-3">
<textarea name="html" rows="8" placeholder="Paste HTML here, or use the file picker below"
class="w-full bg-slate-950 border border-slate-800 rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500"></textarea>
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
<label class="block">
<span class="block text-xs text-slate-400 mb-1">Custom slug (optional)</span>
<input type="text" name="slug" pattern="[a-z0-9][a-z0-9-]{2,31}" placeholder="auto"
class="w-full bg-slate-950 border border-slate-800 rounded px-3 py-2 text-sm">
</label>
<label class="block">
<span class="block text-xs text-slate-400 mb-1">Title (optional)</span>
<input type="text" name="title" placeholder="from &lt;title&gt; if blank"
class="w-full bg-slate-950 border border-slate-800 rounded px-3 py-2 text-sm">
</label>
<label class="block">
<span class="block text-xs text-slate-400 mb-1">Password (optional)</span>
<input type="password" name="password" autocomplete="new-password"
class="w-full bg-slate-950 border border-slate-800 rounded px-3 py-2 text-sm">
</label>
<label class="block">
<span class="block text-xs text-slate-400 mb-1">Expires</span>
<select name="expires_in" class="w-full bg-slate-950 border border-slate-800 rounded px-3 py-2 text-sm">
<option value="never">never</option>
<option value="1h">1 hour</option>
<option value="24h">1 day</option>
<option value="7d">7 days</option>
<option value="30d">30 days</option>
</select>
</label>
</div>
<div class="flex items-center justify-between">
<label class="text-xs text-slate-400">
Or upload: <input type="file" name="file" accept=".html,text/html" class="ml-2 text-xs">
</label>
<button class="bg-indigo-600 hover:bg-indigo-500 text-white rounded px-4 py-2 font-medium text-sm">Publish</button>
</div>
</form>
</section>
{{ if .FlashSlug }}
<div class="rounded border border-emerald-800 bg-emerald-900/30 text-emerald-100 px-4 py-3 mb-6 text-sm flex items-center justify-between">
<div>
Published: <code class="bg-slate-900 px-2 py-0.5 rounded">{{ .BaseURL }}/p/{{ .FlashSlug }}</code>
</div>
<button onclick="navigator.clipboard.writeText('{{ .BaseURL }}/p/{{ .FlashSlug }}')"
class="bg-emerald-700 hover:bg-emerald-600 rounded px-3 py-1 text-xs">Copy link</button>
</div>
{{ end }}
<!-- Artifact list -->
<section class="rounded border border-slate-800 bg-slate-900/30 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-slate-900 text-slate-400 text-xs uppercase tracking-wide">
<tr>
<th class="text-left px-4 py-2">Slug</th>
<th class="text-left px-4 py-2">Title</th>
<th class="text-right px-4 py-2">Views</th>
<th class="text-left px-4 py-2">Last viewed</th>
<th class="text-left px-4 py-2">7-day</th>
<th class="text-left px-4 py-2">Size</th>
<th class="text-right px-4 py-2">Actions</th>
</tr>
</thead>
<tbody>
{{ range .Items }}
<tr id="row-{{ .Slug }}" class="border-t border-slate-800 hover:bg-slate-900/50">
<td class="px-4 py-2">
<a class="font-mono text-indigo-300 hover:text-indigo-200" href="/a/{{ .Slug }}">{{ .Slug }}</a>
{{ if .HasPassword }}<span class="ml-1 text-xs text-amber-400" title="Password-protected">🔒</span>{{ end }}
</td>
<td class="px-4 py-2 text-slate-300">{{ if .Title }}{{ .Title }}{{ else }}<span class="text-slate-500"></span>{{ end }}</td>
<td class="px-4 py-2 text-right tabular-nums">{{ .ViewCount }}</td>
<td class="px-4 py-2 text-slate-400">{{ humanTime .LastViewedAt }}</td>
<td class="px-4 py-2 spark text-indigo-300">{{ sparkline .Spark }}</td>
<td class="px-4 py-2 text-slate-400">{{ humanSize .SizeBytes }}</td>
<td class="px-4 py-2 text-right">
<button onclick="navigator.clipboard.writeText('{{ $.BaseURL }}/p/{{ .Slug }}')"
class="text-xs px-2 py-1 rounded bg-slate-800 hover:bg-slate-700">Copy</button>
<a href="/p/{{ .Slug }}" target="_blank" class="text-xs px-2 py-1 rounded bg-slate-800 hover:bg-slate-700">Open</a>
<button hx-delete="/a/{{ .Slug }}" hx-target="#row-{{ .Slug }}" hx-swap="outerHTML"
hx-confirm="Delete {{ .Slug }}? This cannot be undone."
class="text-xs px-2 py-1 rounded bg-red-900/60 hover:bg-red-800">Delete</button>
</td>
</tr>
{{ else }}
<tr><td colspan="7" class="px-4 py-10 text-center text-slate-500">No artifacts yet. Paste some HTML above to publish your first one.</td></tr>
{{ end }}
</tbody>
</table>
</section>
</main>
{{ template "footer" }}
{{ end }}