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
change
commit b57f8045daac870ab7b5419375a5326f78fbfb87
author Claude <noreply@anthropic.com>
date
parent cb858cf0
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)