Cans UI + parity: unlock-cookie flow, /can page, listings badge, custom slug, delete, sweeper; #32 perf notes (#4, #32)

This commit is contained in:
2026-09-09 16:34:51 -05:00
parent 91568c0598
commit 19804d47a3
13 changed files with 950 additions and 43 deletions
+40 -6
View File
@@ -34,6 +34,7 @@ type UI struct {
func New() (*UI, error) {
funcs := template.FuncMap{
"humanSize": humanSize,
"version": func() string { return Version }, // #93: topbar version label
}
t, err := template.New("").Funcs(funcs).ParseFS(tmplFS, "templates/*.html")
if err != nil {
@@ -64,6 +65,29 @@ func (h *Handlers) renderPage(w http.ResponseWriter, name string, data any) {
}
}
// RenderPage is the exported wrapper used by the api package (#4 can pages).
func (h *Handlers) RenderPage(w http.ResponseWriter, name string, data any) {
h.renderPage(w, name, data)
}
// WriteRateLimited is the exported rate-limit response used by the api package (#4).
func (h *Handlers) WriteRateLimited(w http.ResponseWriter, retryAfterSecs int) {
h.writeRateLimited(w, retryAfterSecs)
}
// UnlockToken returns the per-id HMAC unlock token (cookie value, #34).
// Exported for the api package so can pages share paste cookie semantics (#4).
func UnlockToken(id string) string { return unlockToken(id) }
// AgoString formats a relative "N ago" string (exported for the api package, #4).
func AgoString(ts int64) string { return agoString(ts) }
// HumanSize formats a byte count as a human string (exported for the api package, #4).
func HumanSize(n int) string { return humanSize(n) }
// ExpiryString formats remaining time until an epoch seconds expiry (#4).
func ExpiryString(expiresAt int64) string { return expiryString(expiresAt) }
// #34: per-paste unlock tokens. unlockSecret is generated once at startup
// (also derivable from PALETTE_UNLOCK_SECRET for multi-instance deploys) and
// used to HMAC paste ids, so a client can only hold a valid pw_<id> cookie by
@@ -245,12 +269,13 @@ func (h *Handlers) HandlePasteView(w http.ResponseWriter, r *http.Request) {
// one-time display of the deletion token via the created banner
http.SetCookie(w, &http.Cookie{Name: "tok_" + row.ID, Value: token, Path: "/", MaxAge: 60, HttpOnly: true, SameSite: http.SameSiteLaxMode})
}
// Count the view for every real page render. Raw views increment in
// handleRaw; the HTML path was missing its increment, so view_count only
// ever moved via /raw and the API-stored count stayed at 0 (#33).
// The just-created banner render does not count as a view.
// Count the view for real page renders, deduped per-viewer within the
// burn window (#95): a reload by the same vwr cookie doesn't inflate
// view_count. Raw views increment unconditionally in handleRaw (#49); the
// HTML path was missing its increment originally (#33). The just-created
// banner render does not count as a view.
if !justCreated {
h.Store.IncrementViews(row.ID)
h.Store.IncrementViews(row.ID, h.ViewerID(r), h.BurnWindowMin())
}
// #49: burn-after-N-reads budget (per-viewer dedupe window).
// Just-created first render does not count as a read for the creator.
@@ -279,7 +304,16 @@ func (h *Handlers) HandleHistoryPage(w http.ResponseWriter, r *http.Request) {
// HandleSettingsPage serves /settings.
func (h *Handlers) HandleSettingsPage(w http.ResponseWriter, r *http.Request) {
h.renderPage(w, "settings.html", map[string]any{"Page": "settings"})
// #100: theme presets. Midnight is the default (no data-preset attribute),
// so its swatches are hardcoded here; the CSS defines the token values.
themes := []map[string]any{
{"Name": "midnight", "Label": "Midnight", "Swatches": []string{"#241B30", "#2D2340", "#3A2D52", "#C4A8F0", "#F2EDF8"}},
{"Name": "smooth", "Label": "Smooth", "Swatches": []string{"#F6F5FA", "#FFFFFF", "#DAD7E6", "#7A7796", "#2A2A36"}},
{"Name": "pastel-lavender", "Label": "Pastel Lavender", "Swatches": []string{"#e6e0f5", "#f1edfa", "#cbb8e7", "#806bb8", "#3E3059"}},
{"Name": "pastel-peach", "Label": "Pastel Peach", "Swatches": []string{"#ffe0d6", "#fff0ea", "#ffc4a8", "#f9826c", "#4F2318"}},
{"Name": "pastel-cloud", "Label": "Pastel Cloud", "Swatches": []string{"#fff0f6", "#fff7fb", "#ffc8dd", "#a2d2ff", "#4A3355"}},
}
h.renderPage(w, "settings.html", map[string]any{"Page": "settings", "Themes": themes})
}
// HandleMinePage serves /mine.