3 Commits
Author SHA1 Message Date
fen d3e5842ff7 Fix #215-api: reject malformed multipart burn_after_reads with 400
handleCreatePasteMultipart silently dropped non-numeric
burn_after_reads values (err == nil guard), so burn_after_reads=abc
created a 201 paste with NO burn budget. Now returns 400 with the
error+code shape ({'code':'burn_invalid'}) matching the JSON path,
where an invalid value fails unmarshal into the int field. An empty
value stays 'unset' like the JSON path's omitempty. Verified live:
burn_after_reads=abc -> 400 burn_invalid; =2 -> 201.
2026-10-16 16:51:36 -05:00
fen ce9bf45010 Fix #214-ui: add page <title> to every rendered page
layout.html now emits <title>{{ pageTitle . }} · Palette</title>; the
pageTitle template func maps the Page field to a label (New paste,
Public pastes, Saved, Settings, paste title or 'Paste <id>', Unlock,
Can, Not found).
2026-10-16 16:51:36 -05:00
fen 01f177a5ee Fix #213-ui: /new horizontal overflow at ~800px viewports
The .deck grid used '1fr 300px'; the 1fr editor column could not shrink
below its min-content, expanding the grid to ~1000px at 800px viewports
and pushing .pane-r to right=980 (document.scrollWidth 980 vs
innerWidth 800). Use minmax(0, 1fr) so the editor column can shrink;
the editor itself already clips/scrolls its own content.

Verified with a CDP overflow probe at 800x600, 375x812, 1400x900 and
700x600: scrollWidth == innerWidth at all four.
2026-10-16 16:51:36 -05:00
4 changed files with 46 additions and 3 deletions

No files matched your search

+11 -2
View File
@@ -186,9 +186,18 @@ func (a *apiServer) handleCreatePasteMultipart(w http.ResponseWriter, r *http.Re
case "burn_after_read": case "burn_after_read":
p.BurnAfterRead = v == "true" || v == "1" || v == "on" p.BurnAfterRead = v == "true" || v == "1" || v == "on"
case "burn_after_reads": case "burn_after_reads":
if n, err := strconv.Atoi(v); err == nil { if v == "" { // absent value: unset, like the JSON path's omitempty
p.BurnAfterReads = &n continue
} }
n, err := strconv.Atoi(v)
if err != nil {
// malformed burn_after_reads is a 400, not a silently-dropped
// budget: same 400-with-code shape the JSON path uses for
// invalid values (which fail unmarshal into the int field).
writeErrCode(w, 400, "burn_invalid", "burn_after_reads must be a positive number")
return
}
p.BurnAfterReads = &n
} }
} }
+1 -1
View File
@@ -129,7 +129,7 @@ body {
/* new paste page */ /* new paste page */
.deck { .deck {
display: grid; grid-template-columns: 1fr 300px; gap: 16px; display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 16px;
padding: 16px 20px 20px; height: calc(100vh - 76px); padding: 16px 20px 20px; height: calc(100vh - 76px);
max-width: 1400px; margin: 0 auto; max-width: 1400px; margin: 0 auto;
} }
+1
View File
@@ -1,5 +1,6 @@
{{define "head"}} {{define "head"}}
<meta charset="utf-8"> <meta charset="utf-8">
<title>{{ pageTitle . }} · Palette</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/app.css"> <link rel="stylesheet" href="/static/app.css">
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"> <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
+33
View File
@@ -74,6 +74,7 @@ func New() (*UI, error) {
"humanSize": humanSize, "humanSize": humanSize,
"version": func() string { return Version }, // #93: topbar version label "version": func() string { return Version }, // #93: topbar version label
"defaultDark": func() bool { return DefaultDark }, // #127: server-configured default "defaultDark": func() bool { return DefaultDark }, // #127: server-configured default
"pageTitle": pageTitle, // page <title> labels
} }
t, err := template.New("").Funcs(funcs).ParseFS(tmplFS, "templates/*.html") t, err := template.New("").Funcs(funcs).ParseFS(tmplFS, "templates/*.html")
if err != nil { if err != nil {
@@ -82,6 +83,38 @@ func New() (*UI, error) {
return &UI{tmpl: t}, nil return &UI{tmpl: t}, nil
} }
// pageTitle returns the <title> label for the rendered page. Page data is
// always a map with a "Page" key set by the handlers.
func pageTitle(data any) string {
m, _ := data.(map[string]any)
page, _ := m["Page"].(string)
id, _ := m["ID"].(string)
title, _ := m["Title"].(string)
switch page {
case "new":
return "New paste"
case "public":
return "Public pastes"
case "saved":
return "Saved"
case "settings":
return "Settings"
case "paste":
if title != "" {
return title
}
return "Paste " + id
case "unlock":
return "Unlock " + id
case "can":
return "Can " + id
case "notfound":
return "Not found"
default:
return "Palette"
}
}
func humanSize(n int) string { func humanSize(n int) string {
if n < 1024 { if n < 1024 {
return fmt.Sprintf("%d B", n) return fmt.Sprintf("%d B", n)