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>
53 lines
2.5 KiB
HTML
53 lines
2.5 KiB
HTML
{{ define "detail" }}
|
|
{{ template "head" (print "Artifact " .Artifact.Slug) }}
|
|
{{ template "header" true }}
|
|
<main class="max-w-6xl mx-auto px-6 py-8">
|
|
<nav class="text-sm text-slate-400 mb-4"><a href="/" class="hover:text-slate-100">← back to dashboard</a></nav>
|
|
|
|
<div class="flex items-start justify-between mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-semibold font-mono">{{ .Artifact.Slug }} {{ if .Artifact.HasPassword }}<span class="text-base text-amber-400 align-middle">🔒</span>{{ end }}</h1>
|
|
{{ if .Artifact.Title }}<p class="text-slate-400 mt-1">{{ .Artifact.Title }}</p>{{ end }}
|
|
<p class="text-xs text-slate-500 mt-2">
|
|
Created {{ humanTime .Artifact.CreatedAt }} ·
|
|
{{ humanSize .Artifact.SizeBytes }} ·
|
|
{{ .Artifact.ViewCount }} views
|
|
{{ if .Artifact.ExpiresAt }} · expires {{ humanTime .Artifact.ExpiresAt }}{{ end }}
|
|
</p>
|
|
<p class="mt-2 text-sm"><a class="text-indigo-300 hover:text-indigo-200" href="{{ .BaseURL }}/p/{{ .Artifact.Slug }}" target="_blank">{{ .BaseURL }}/p/{{ .Artifact.Slug }}</a></p>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button onclick="navigator.clipboard.writeText('{{ .BaseURL }}/p/{{ .Artifact.Slug }}')"
|
|
class="text-sm px-3 py-1.5 rounded bg-slate-800 hover:bg-slate-700">Copy link</button>
|
|
<button hx-delete="/a/{{ .Artifact.Slug }}" hx-confirm="Delete {{ .Artifact.Slug }}? This cannot be undone."
|
|
hx-on::after-request="window.location='/'"
|
|
class="text-sm px-3 py-1.5 rounded bg-red-900/60 hover:bg-red-800">Delete</button>
|
|
</div>
|
|
</div>
|
|
|
|
<section class="rounded border border-slate-800 bg-slate-900/30 p-4 mb-6">
|
|
<h2 class="text-sm uppercase tracking-wide text-slate-400 mb-3">Views — last 30 days</h2>
|
|
{{ .ChartSVG }}
|
|
</section>
|
|
|
|
<section class="rounded border border-slate-800 bg-slate-900/30 p-4">
|
|
<h2 class="text-sm uppercase tracking-wide text-slate-400 mb-3">Top referrers</h2>
|
|
{{ if .Referrers }}
|
|
<table class="w-full text-sm">
|
|
<tbody>
|
|
{{ range .Referrers }}
|
|
<tr class="border-t border-slate-800 first:border-t-0">
|
|
<td class="py-1.5 text-slate-300">{{ if .Host }}{{ .Host }}{{ else }}<span class="text-slate-500">(direct)</span>{{ end }}</td>
|
|
<td class="py-1.5 text-right tabular-nums">{{ .Count }}</td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
{{ else }}
|
|
<p class="text-sm text-slate-500">No referrer data yet.</p>
|
|
{{ end }}
|
|
</section>
|
|
</main>
|
|
{{ template "footer" }}
|
|
{{ end }}
|