Render Capture through Layout; document CSRF; fix utopia note
- Layout: accept a title and deferred scripts, emitting page scripts
before Alpine so /capture.js registers captureApp() in time. Render
the body via the content block.
- Capture: render inside Layout instead of duplicating the html/head/
body shell. Layout was previously dead code.
- web.rb: document why POST /files has no CSRF check (trusted-proxy
header auth, no cookie sessions) and what to do if that changes.
- utopia skill: correct the stale note — app.css and domus-tokens.css
are already in sync; only the unused one-up pairs differ.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JQTgGstuG7U9yzrtK6emxy
diff --git a/.claude/skills/utopia/SKILL.md b/.claude/skills/utopia/SKILL.md
index a7c65ab..1e67521 100644
--- a/.claude/skills/utopia/SKILL.md
+++ b/.claude/skills/utopia/SKILL.md
@@ -37,9 +37,11 @@ The design-system scale uses:
Space multiples off the base: 0.25 / 0.5 / 0.75 / 1 / 1.5 / 2 / 3 / 4 / 6
(→ `3xs` … `3xl`). Utopia can also emit one-up pairs (`--space-s-m`, …).
-> The shipped `public/app.css` currently carries an older scale (viewport
-> 320→1280, ratio 1.25). When restyling toward the design system, regenerate
-> it from the config above so it matches `domus-tokens.css`.
+> `public/app.css` and `docs/design/domus-tokens.css` are in sync — same
+> config, identical `--step-*` / `--space-*` clamps. The only difference is
+> the one-up pairs (`--space-s-m`, etc.), which live in `domus-tokens.css`
+> but aren't shipped in `app.css` because no rule uses them yet. When you
+> change the scale, regenerate from the config above and update both files.
## How a step is computed
diff --git a/lib/views/home.rb b/lib/views/home.rb
index 5237e97..2a0dd13 100644
--- a/lib/views/home.rb
+++ b/lib/views/home.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "phlex"
+require_relative "layout"
require_relative "icons"
require_relative "capture_form"
require_relative "../relative_time"
@@ -20,23 +21,12 @@ module Domus
end
def view_template
- doctype
- html(lang: "en") do
- head do
- meta(charset: "utf-8")
- meta(name: "viewport", content: "width=device-width, initial-scale=1")
- title { "Domus" }
- link(rel: "stylesheet", href: "/app.css")
- script(defer: true, src: "/capture.js")
- script(defer: true, src: "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js")
- end
- body do
- div(class: "page", "x-data": "captureApp()") do
- topbar
- recent_assets
- save_panel
- dock
- end
+ render Layout.new(scripts: ["/capture.js"]) do
+ div(class: "page", "x-data": "captureApp()") do
+ topbar
+ recent_assets
+ save_panel
+ dock
end
end
end
diff --git a/lib/views/layout.rb b/lib/views/layout.rb
index b773a4b..144e4fa 100644
--- a/lib/views/layout.rb
+++ b/lib/views/layout.rb
@@ -5,12 +5,17 @@ require "phlex"
module Domus
module Views
class Layout < Phlex::HTML
- def initialize(title: "Domus", &content)
+ ALPINE = "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
+
+ # `scripts` are emitted (deferred) before Alpine so page scripts that
+ # register Alpine components — e.g. /capture.js defining captureApp() —
+ # run first. Order matters: deferred scripts execute in document order.
+ def initialize(title: "Domus", scripts: [])
@title = title
- @content = content
+ @scripts = scripts
end
- def view_template
+ def view_template(&block)
doctype
html(lang: "en") do
@@ -19,12 +24,11 @@ module Domus
meta(name: "viewport", content: "width=device-width, initial-scale=1")
title { @title }
link(rel: "stylesheet", href: "/app.css")
- script(defer: true, src: "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js")
+ @scripts.each { |src| script(defer: true, src:) }
+ script(defer: true, src: ALPINE)
end
- body do
- yield
- end
+ body(&block)
end
end
end
diff --git a/lib/web.rb b/lib/web.rb
index b93282b..b2a580e 100644
--- a/lib/web.rb
+++ b/lib/web.rb
@@ -40,6 +40,12 @@ module Domus
end
end
+ # POST /files has no CSRF token check. Domus authenticates via the
+ # reverse proxy's trusted X-Forwarded-User header (see
+ # lib/middleware/auth.rb), not cookie sessions, so there's no ambient
+ # credential a forged cross-site POST could ride on. If this app ever
+ # adopts cookie-based sessions, load the Roda :route_csrf plugin and
+ # verify the token here before accepting the upload.
r.on "files" do
r.post do
save_file(r.params)