Cans UI + parity: unlock-cookie flow, /can page, listings badge, custom slug, delete, sweeper; #32 perf notes (#4, #32)
This commit is contained in:
+40
-6
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user