Signed by hanna SSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
assets/base.css +33 −0 assets/themes/nord.css +46 −0 crates/git/src/signature.rs +6 −2 crates/web/src/layout.rs +2 −2 crates/web/src/lib.rs +4 −0 crates/web/src/repo.rs +6 −3 docs/decisions.md +18 −0 assets/base.css +33 −0 585 . page { 585 . page { 586 padding : 0.75rem ; 586 padding : 0.75rem ; 587 } 587 } 588 589 body { 590 overflow-x : hidden ; 591 } 592 593 . topbar . tabs a , 594 . repo-tabs a , 595 . drawer nav a , 596 . drawer . drawer-item , 597 . btn , 598 . icon-btn { 599 min-height : 44px ; 600 display : inline-flex ; 601 align-items : center ; 602 } 603 . icon-btn { 604 justify-content : center ; 605 min-width : 44px ; 606 } 607 . repo-tabs , 608 . clone-row { 609 overflow-x : auto ; 610 -webkit-overflow-scrolling : touch ; 611 } 612 . clone-row input { 613 min-width : 0 ; 614 } 615 616 table . blob , 617 table . diff { 618 display : block ; 619 overflow-x : auto ; 620 } 588 } 621 }
assets/themes/nord.css +46 −0 1 2 3 4 5 6 7 8 9 : root { 10 --fb-color-scheme : dark ; 11 12 13 --fb-bg : #2e3440 ; 14 --fb-bg-raised : #3b4252 ; 15 --fb-bg-inset : #434c5e ; 16 --fb-border : #4c566a ; 17 --fb-fg : #eceff4 ; 18 --fb-fg-muted : #99a3b3 ; 19 20 21 --fb-accent : #88c0d0 ; 22 --fb-accent-fg : #2e3440 ; 23 --fb-success : #a3be8c ; 24 --fb-warning : #ebcb8b ; 25 --fb-danger : #bf616a ; 26 27 28 --fb-diff-add-bg : rgba ( 163 , 190 , 140 , 0.16 ) ; 29 --fb-diff-del-bg : rgba ( 191 , 97 , 106 , 0.16 ) ; 30 --fb-diff-add-fg : #a3be8c ; 31 --fb-diff-del-fg : #bf616a ; 32 33 34 --fb-hl-keyword : #81a1c1 ; 35 --fb-hl-function : #88c0d0 ; 36 --fb-hl-type : #8fbcbb ; 37 --fb-hl-string : #a3be8c ; 38 --fb-hl-number : #b48ead ; 39 --fb-hl-comment : #616e88 ; 40 --fb-hl-constant : #b48ead ; 41 --fb-hl-variable : #d8dee9 ; 42 --fb-hl-operator : #81a1c1 ; 43 --fb-hl-punctuation : #eceff4 ; 44 --fb-hl-attribute : #8fbcbb ; 45 --fb-hl-tag : #81a1c1 ; 46 }
crates/git/src/signature.rs +6 −2 255 } 255 } 256 256 257 impl SignatureCache { 257 impl SignatureCache { 258 258 259 259 # [ must_use ] 260 # [ must_use ] 260 pub fn new ( capacity : u64 ) -> Self { 261 pub fn new ( capacity : u64 ) -> Self { 261 Self { 262 Self { 262 inner: moka:: sync:: Cache :: new ( capacity) , 263 inner: moka:: sync:: Cache :: builder ( ) 264 . max_capacity ( capacity) 265 . time_to_live ( std:: time:: Duration :: from_secs ( 600 ) ) 266 . build ( ) , 263 } 267 } 264 } 268 } 265 269
crates/web/src/layout.rs +2 −2 70 html ! { 70 html ! { 71 header class="topbar" { 71 header class="topbar" { 72 button class="icon-btn" type ="button" data-drawer-toggle aria-label="Menu" 72 button class="icon-btn" type ="button" data-drawer-toggle aria-label="Menu" 73 aria-expanded="false" { "☰" } 73 aria-controls="drawer" aria-expanded="false" { "☰" } 74 span class="slug" { a href="/" { ( chrome. instance_name) } } 74 span class="slug" { a href="/" { ( chrome. instance_name) } } 75 nav class="tabs" aria-label="Primary" { } 75 nav class="tabs" aria-label="Primary" { } 76 a class="icon-btn" href="/" aria-label="Home" { "⌂" } 76 a class="icon-btn" href="/" aria-label="Home" { "⌂" } 81 81 82 fn drawer ( chrome : & Chrome ) -> Markup { 82 fn drawer ( chrome : & Chrome ) -> Markup { 83 html ! { 83 html ! { 84 aside class="drawer" aria-label="Navigation" { 84 aside id="drawer" class="drawer" aria-label="Navigation" { 85 h2 { ( chrome. instance_name) } 85 h2 { ( chrome. instance_name) } 86 nav { 86 nav { 87 a href="/" { "Home" } 87 a href="/" { "Home" }
crates/web/src/lib.rs +4 −0 66 pub mailer: Arc < Mailer > , 66 pub mailer: Arc < Mailer > , 67 67 68 pub secret: Arc < String > , 68 pub secret: Arc < String > , 69 70 71 pub signatures: git:: SignatureCache , 69 72 70 assets: Arc < RwLock < Assets > > , 73 assets: Arc < RwLock < Assets > > , 71 } 74 } 84 store, 87 store, 85 mailer: Arc :: new ( mailer) , 88 mailer: Arc :: new ( mailer) , 86 secret: Arc :: new ( secret) , 89 secret: Arc :: new ( secret) , 90 signatures: git:: SignatureCache :: new ( 4096 ) , 87 assets: Arc :: new ( RwLock :: new ( assets) ) , 91 assets: Arc :: new ( RwLock :: new ( assets) ) , 88 } ) 92 } ) 89 } 93 }
crates/web/src/repo.rs +6 −3 618 } ) 618 } ) 619 . await ?; 619 . await ?; 620 620 621 621 622 622 let keys = signing_keys ( state. store. all_keys ( ) . await ?) ; 623 let keys = signing_keys ( state. store. all_keys ( ) . await ?) ; 624 let cache = state. signatures. clone ( ) ; 623 let states = git_read ( state, & ctx. repo. id, { 625 let states = git_read ( state, & ctx. repo. id, { 624 let oids: Vec < git:: Oid > = commits. iter ( ) . map ( |c| c. oid. clone ( ) ) . collect ( ) ; 626 let oids: Vec < git:: Oid > = commits. iter ( ) . map ( |c| c. oid. clone ( ) ) . collect ( ) ; 625 move |repo| { 627 move |repo| { 626 Ok ( oids 628 Ok ( oids 627 . iter ( ) 629 . iter ( ) 628 . map ( |o| repo. signature_state ( o, & keys) ) 630 . map ( |o| ( * cache. get ( repo, o, & keys) ) . clone ( ) ) 629 . collect :: < Vec < _ > > ( ) ) 631 . collect :: < Vec < _ > > ( ) ) 630 } 632 } 631 } ) 633 } ) 751 let oid = git:: Oid :: new ( sha) ; 753 let oid = git:: Oid :: new ( sha) ; 752 let context_lines = state. config. ui. diff_context_lines; 754 let context_lines = state. config. ui. diff_context_lines; 753 let sha_owned = sha. to_string ( ) ; 755 let sha_owned = sha. to_string ( ) ; 756 let cache = state. signatures. clone ( ) ; 754 git_read ( state, repo_id, move |repo| { 757 git_read ( state, repo_id, move |repo| { 755 let detail = repo. commit ( & oid) ?; 758 let detail = repo. commit ( & oid) ?; 756 let base = detail. parents. first ( ) . cloned ( ) ; 759 let base = detail. parents. first ( ) . cloned ( ) ; 772 . into_iter ( ) 775 . into_iter ( ) 773 . map ( |file| render_one_file ( repo, & head_rev, base_rev. as_ref ( ) , file) ) 776 . map ( |file| render_one_file ( repo, & head_rev, base_rev. as_ref ( ) , file) ) 774 . collect ( ) ; 777 . collect ( ) ; 775 let sig = repo. signature_state ( & oid, & keys) ; 778 let sig = ( * cache. get ( repo, & oid, & keys) ) . clone ( ) ; 776 Ok ( ( detail, rendered, sig) ) 779 Ok ( ( detail, rendered, sig) ) 777 } ) 780 } ) 778 . await 781 . await
docs/decisions.md +18 −0 65 **Rationale:** Foreground is correct for systemd and Docker, which own process 65 **Rationale:** Foreground is correct for systemd and Docker, which own process 66 lifecycle. This deviates from an earlier `server start`/`server stop` sketch. 66 lifecycle. This deviates from an earlier `server start`/`server stop` sketch. 67 67 68 ## 2026-07-25 — Polish: signature cache wired; `theme list`/`gc` CLI deferred 69 70 **Decision:** The `moka` `SignatureCache` (bounded 4096, 10-minute TTL) is wired 71 into `AppState` and used by the commit list and commit page. The 72 `fabrica theme list` and `fabrica gc` CLI subcommands are **not** shipped. 73 74 **Alternatives:** Also cache attributes/highlight; implement `theme list`/`gc`. 75 76 **Rationale:** Signature verification is the measured hot path (list views verify 77 every row), so it is the one place caching earns its keep (§18 "moka where 78 measured"); the TTL bounds staleness after a key change without explicit 79 invalidation. `theme list` would need the embedded theme registry, which lives in 80 `web` and cannot be reached from `cli` (nothing may depend on `web`); the web theme 81 picker already lists every theme, so the CLI command is low value. `gc` 82 (trash reaping) is a maintenance convenience with no correctness impact; both are 83 tracked gaps, not blockers. The per-file highlight **timeout** guard (§8) also 84 remains deferred as noted earlier. 85 68 ## 2026-07-25 — API nested into web via `nest_service`; web may depend on api 86 ## 2026-07-25 — API nested into web via `nest_service`; web may depend on api 69 87 70 **Decision:** The `/api/v1` JSON surface is a self-contained `Router` built by 88 **Decision:** The `/api/v1` JSON surface is a self-contained `Router` built by