fix(web): harden CSP and headers, ignore htmx in untrusted HTML, cache health checks
- CSP adds base-uri 'none', form-action 'self', object-src 'none' - HSTS when the public URL is https - hx-ignore on README and code view containers - htmx-config meta disables unused history and indicator CSS, limits extensions - health check result cached for 5s - raw route logs the redacted path
- commit
- 49d699271ce1a6ee74b47d7ac87a170a85a77eb4
- author
- brice · · 2026-10-11 18:31 UTC
- parent
- bfab294
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
45 files · +220 -52
M cmd/ermite/forge.go +1 -0
| @@ -247,6 +247,7 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e | |||
| 247 | 247 | routes = append(routes, webRoutes(site)...) | |
| 248 | 248 | f.server = httpserver.New(httpserver.Config{ | |
| 249 | 249 | Addr: cfg.Addr, | |
| 250 | + | PublicURL: cfg.PublicURL, | |
| 250 | 251 | HealthChecks: []httpserver.Check{store.Check, httpserver.DirWritable(cfg.DataDir)}, | |
| 251 | 252 | Routes: routes, | |
| 252 | 253 | API: f.API.Handler(), | |
| @@ -247,6 +247,7 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e | |||||
| 247 | routes = append(routes, webRoutes(site)...) | 247 | routes = append(routes, webRoutes(site)...) | ||
| 248 | f.server = httpserver.New(httpserver.Config{ | 248 | f.server = httpserver.New(httpserver.Config{ | ||
| 249 | Addr: cfg.Addr, | 249 | Addr: cfg.Addr, | ||
| 250 | + | PublicURL: cfg.PublicURL, | |||
| 250 | HealthChecks: []httpserver.Check{store.Check, httpserver.DirWritable(cfg.DataDir)}, | 251 | HealthChecks: []httpserver.Check{store.Check, httpserver.DirWritable(cfg.DataDir)}, | ||
| 251 | Routes: routes, | 252 | Routes: routes, | ||
| 252 | API: f.API.Handler(), | 253 | API: f.API.Handler(), | ||
M cmd/ermite/testdata/web/blob-binary.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/blob-code.html +2 -1
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -100,7 +101,7 @@ | |||
| 100 | 101 | q('.line in this').arr().forEach(l => { let k = +l.id.slice(1); l.classList.toggle('sel', k >= lo && k <= hi) }); | |
| 101 | 102 | document.body.dataset.sel = lo ? 'L' + lo + (hi > lo ? '-L' + hi + ' · ' + (hi - lo + 1) + ' lines' : '') : ''; | |
| 102 | 103 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" data-ready=""> | |
| 103 | - | <code> | |
| 104 | + | <code hx-ignore> | |
| 104 | 105 | <span class="line" id="L1"> | |
| 105 | 106 | <a class="ln" href="#L1">1</a> | |
| 106 | 107 | <span class="kn">package</span> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
| @@ -100,7 +101,7 @@ | |||||
| 100 | q('.line in this').arr().forEach(l => { let k = +l.id.slice(1); l.classList.toggle('sel', k >= lo && k <= hi) }); | 101 | q('.line in this').arr().forEach(l => { let k = +l.id.slice(1); l.classList.toggle('sel', k >= lo && k <= hi) }); | ||
| 101 | document.body.dataset.sel = lo ? 'L' + lo + (hi > lo ? '-L' + hi + ' · ' + (hi - lo + 1) + ' lines' : '') : ''; | 102 | document.body.dataset.sel = lo ? 'L' + lo + (hi > lo ? '-L' + hi + ' · ' + (hi - lo + 1) + ' lines' : '') : ''; | ||
| 102 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" data-ready=""> | 103 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" data-ready=""> | ||
| 103 | - | <code> | 104 | + | <code hx-ignore> |
| 104 | <span class="line" id="L1"> | 105 | <span class="line" id="L1"> | ||
| 105 | <a class="ln" href="#L1">1</a> | 106 | <a class="ln" href="#L1">1</a> | ||
| 106 | <span class="kn">package</span> | 107 | <span class="kn">package</span> | ||
M cmd/ermite/testdata/web/blob-empty.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/blob-image.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/blob-markdown.html +3 -2
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -96,7 +97,7 @@ | |||
| 96 | 97 | <a class="btn" href="/files/tree/main/docs">Folder</a> | |
| 97 | 98 | </span> | |
| 98 | 99 | </h1> | |
| 99 | - | <article class="prose" aria-label="guide.md"> | |
| 100 | + | <article class="prose" aria-label="guide.md" hx-ignore> | |
| 100 | 101 | <h1 id="md-guide">Guide</h1> | |
| 101 | 102 | <h2 id="md-steps">Steps</h2> | |
| 102 | 103 | <p>Run it.</p> | |
| @@ -114,7 +115,7 @@ | |||
| 114 | 115 | q('.line in this').arr().forEach(l => { let k = +l.id.slice(1); l.classList.toggle('sel', k >= lo && k <= hi) }); | |
| 115 | 116 | document.body.dataset.sel = lo ? 'L' + lo + (hi > lo ? '-L' + hi + ' · ' + (hi - lo + 1) + ' lines' : '') : ''; | |
| 116 | 117 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" data-ready=""> | |
| 117 | - | <code> | |
| 118 | + | <code hx-ignore> | |
| 118 | 119 | <span class="line" id="L1"> | |
| 119 | 120 | <a class="ln" href="#L1">1</a> | |
| 120 | 121 | <span class="gh"># Guide</span> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
| @@ -96,7 +97,7 @@ | |||||
| 96 | <a class="btn" href="/files/tree/main/docs">Folder</a> | 97 | <a class="btn" href="/files/tree/main/docs">Folder</a> | ||
| 97 | </span> | 98 | </span> | ||
| 98 | </h1> | 99 | </h1> | ||
| 99 | - | <article class="prose" aria-label="guide.md"> | 100 | + | <article class="prose" aria-label="guide.md" hx-ignore> |
| 100 | <h1 id="md-guide">Guide</h1> | 101 | <h1 id="md-guide">Guide</h1> | ||
| 101 | <h2 id="md-steps">Steps</h2> | 102 | <h2 id="md-steps">Steps</h2> | ||
| 102 | <p>Run it.</p> | 103 | <p>Run it.</p> | ||
| @@ -114,7 +115,7 @@ | |||||
| 114 | q('.line in this').arr().forEach(l => { let k = +l.id.slice(1); l.classList.toggle('sel', k >= lo && k <= hi) }); | 115 | q('.line in this').arr().forEach(l => { let k = +l.id.slice(1); l.classList.toggle('sel', k >= lo && k <= hi) }); | ||
| 115 | document.body.dataset.sel = lo ? 'L' + lo + (hi > lo ? '-L' + hi + ' · ' + (hi - lo + 1) + ' lines' : '') : ''; | 116 | document.body.dataset.sel = lo ? 'L' + lo + (hi > lo ? '-L' + hi + ' · ' + (hi - lo + 1) + ' lines' : '') : ''; | ||
| 116 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" data-ready=""> | 117 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" data-ready=""> | ||
| 117 | - | <code> | 118 | + | <code hx-ignore> |
| 118 | <span class="line" id="L1"> | 119 | <span class="line" id="L1"> | ||
| 119 | <a class="ln" href="#L1">1</a> | 120 | <a class="ln" href="#L1">1</a> | ||
| 120 | <span class="gh"># Guide</span> | 121 | <span class="gh"># Guide</span> | ||
M cmd/ermite/testdata/web/blob-svg.html +2 -1
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -100,7 +101,7 @@ | |||
| 100 | 101 | q('.line in this').arr().forEach(l => { let k = +l.id.slice(1); l.classList.toggle('sel', k >= lo && k <= hi) }); | |
| 101 | 102 | document.body.dataset.sel = lo ? 'L' + lo + (hi > lo ? '-L' + hi + ' · ' + (hi - lo + 1) + ' lines' : '') : ''; | |
| 102 | 103 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" data-ready=""> | |
| 103 | - | <code> | |
| 104 | + | <code hx-ignore> | |
| 104 | 105 | <span class="line" id="L1"> | |
| 105 | 106 | <a class="ln" href="#L1">1</a> | |
| 106 | 107 | <span class="nt"><svg</span> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
| @@ -100,7 +101,7 @@ | |||||
| 100 | q('.line in this').arr().forEach(l => { let k = +l.id.slice(1); l.classList.toggle('sel', k >= lo && k <= hi) }); | 101 | q('.line in this').arr().forEach(l => { let k = +l.id.slice(1); l.classList.toggle('sel', k >= lo && k <= hi) }); | ||
| 101 | document.body.dataset.sel = lo ? 'L' + lo + (hi > lo ? '-L' + hi + ' · ' + (hi - lo + 1) + ' lines' : '') : ''; | 102 | document.body.dataset.sel = lo ? 'L' + lo + (hi > lo ? '-L' + hi + ' · ' + (hi - lo + 1) + ' lines' : '') : ''; | ||
| 102 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" data-ready=""> | 103 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" data-ready=""> | ||
| 103 | - | <code> | 104 | + | <code hx-ignore> |
| 104 | <span class="line" id="L1"> | 105 | <span class="line" id="L1"> | ||
| 105 | <a class="ln" href="#L1">1</a> | 106 | <a class="ln" href="#L1">1</a> | ||
| 106 | <span class="nt"><svg</span> | 107 | <span class="nt"><svg</span> | ||
M cmd/ermite/testdata/web/blob-text.html +2 -1
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -100,7 +101,7 @@ | |||
| 100 | 101 | q('.line in this').arr().forEach(l => { let k = +l.id.slice(1); l.classList.toggle('sel', k >= lo && k <= hi) }); | |
| 101 | 102 | document.body.dataset.sel = lo ? 'L' + lo + (hi > lo ? '-L' + hi + ' · ' + (hi - lo + 1) + ' lines' : '') : ''; | |
| 102 | 103 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" data-ready=""> | |
| 103 | - | <code> | |
| 104 | + | <code hx-ignore> | |
| 104 | 105 | <span class="line" id="L1"> | |
| 105 | 106 | <a class="ln" href="#L1">1</a>plain notes</span> | |
| 106 | 107 | <span class="line" id="L2"> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
| @@ -100,7 +101,7 @@ | |||||
| 100 | q('.line in this').arr().forEach(l => { let k = +l.id.slice(1); l.classList.toggle('sel', k >= lo && k <= hi) }); | 101 | q('.line in this').arr().forEach(l => { let k = +l.id.slice(1); l.classList.toggle('sel', k >= lo && k <= hi) }); | ||
| 101 | document.body.dataset.sel = lo ? 'L' + lo + (hi > lo ? '-L' + hi + ' · ' + (hi - lo + 1) + ' lines' : '') : ''; | 102 | document.body.dataset.sel = lo ? 'L' + lo + (hi > lo ? '-L' + hi + ' · ' + (hi - lo + 1) + ' lines' : '') : ''; | ||
| 102 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" data-ready=""> | 103 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" data-ready=""> | ||
| 103 | - | <code> | 104 | + | <code hx-ignore> |
| 104 | <span class="line" id="L1"> | 105 | <span class="line" id="L1"> | ||
| 105 | <a class="ln" href="#L1">1</a>plain notes</span> | 106 | <a class="ln" href="#L1">1</a>plain notes</span> | ||
| 106 | <span class="line" id="L2"> | 107 | <span class="line" id="L2"> | ||
M cmd/ermite/testdata/web/blob-too-large.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/blob-trojan.html +2 -1
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -100,7 +101,7 @@ | |||
| 100 | 101 | q('.line in this').arr().forEach(l => { let k = +l.id.slice(1); l.classList.toggle('sel', k >= lo && k <= hi) }); | |
| 101 | 102 | document.body.dataset.sel = lo ? 'L' + lo + (hi > lo ? '-L' + hi + ' · ' + (hi - lo + 1) + ' lines' : '') : ''; | |
| 102 | 103 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" data-ready=""> | |
| 103 | - | <code> | |
| 104 | + | <code hx-ignore> | |
| 104 | 105 | <span class="line" id="L1"> | |
| 105 | 106 | <a class="ln" href="#L1">1</a> | |
| 106 | 107 | <span class="kn">package</span> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
| @@ -100,7 +101,7 @@ | |||||
| 100 | q('.line in this').arr().forEach(l => { let k = +l.id.slice(1); l.classList.toggle('sel', k >= lo && k <= hi) }); | 101 | q('.line in this').arr().forEach(l => { let k = +l.id.slice(1); l.classList.toggle('sel', k >= lo && k <= hi) }); | ||
| 101 | document.body.dataset.sel = lo ? 'L' + lo + (hi > lo ? '-L' + hi + ' · ' + (hi - lo + 1) + ' lines' : '') : ''; | 102 | document.body.dataset.sel = lo ? 'L' + lo + (hi > lo ? '-L' + hi + ' · ' + (hi - lo + 1) + ' lines' : '') : ''; | ||
| 102 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" data-ready=""> | 103 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" data-ready=""> | ||
| 103 | - | <code> | 104 | + | <code hx-ignore> |
| 104 | <span class="line" id="L1"> | 105 | <span class="line" id="L1"> | ||
| 105 | <a class="ln" href="#L1">1</a> | 106 | <a class="ln" href="#L1">1</a> | ||
| 106 | <span class="kn">package</span> | 107 | <span class="kn">package</span> | ||
M cmd/ermite/testdata/web/commit-big.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/commit-binary.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/commit-merge.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/commit-modify-split.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/commit-modify.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/commit-rename.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/commit-root.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/commit-trojan.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/commits-page2.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/commits-path.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/commits-root.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/commits-small.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/empty.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/home.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/not-found-page.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/not-found.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/refs.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/summary.html +2 -1
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -83,7 +84,7 @@ | |||
| 83 | 84 | <h2 class="card-h" id="readme-h"> | |
| 84 | 85 | <span class="ico i-file" aria-hidden="true"> | |
| 85 | 86 | </span> README.md</h2> | |
| 86 | - | <article class="prose" aria-label="README.md"> | |
| 87 | + | <article class="prose" aria-label="README.md" hx-ignore> | |
| 87 | 88 | <h1 id="md-demo">Demo</h1> | |
| 88 | 89 | <p>A small <em>demo</em> with a <a href="/demo/blob/main/docs/guide.md">guide</a> and <a href="https://example.org/x" rel="nofollow noopener noreferrer">the site</a>.</p> | |
| 89 | 90 | <h2 id="md-install">Install</h2> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
| @@ -83,7 +84,7 @@ | |||||
| 83 | <h2 class="card-h" id="readme-h"> | 84 | <h2 class="card-h" id="readme-h"> | ||
| 84 | <span class="ico i-file" aria-hidden="true"> | 85 | <span class="ico i-file" aria-hidden="true"> | ||
| 85 | </span> README.md</h2> | 86 | </span> README.md</h2> | ||
| 86 | - | <article class="prose" aria-label="README.md"> | 87 | + | <article class="prose" aria-label="README.md" hx-ignore> |
| 87 | <h1 id="md-demo">Demo</h1> | 88 | <h1 id="md-demo">Demo</h1> | ||
| 88 | <p>A small <em>demo</em> with a <a href="/demo/blob/main/docs/guide.md">guide</a> and <a href="https://example.org/x" rel="nofollow noopener noreferrer">the site</a>.</p> | 89 | <p>A small <em>demo</em> with a <a href="/demo/blob/main/docs/guide.md">guide</a> and <a href="https://example.org/x" rel="nofollow noopener noreferrer">the site</a>.</p> | ||
| 89 | <h2 id="md-install">Install</h2> | 90 | <h2 id="md-install">Install</h2> | ||
M cmd/ermite/testdata/web/tree-nested.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/tree-root.html +2 -1
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -347,7 +348,7 @@ | |||
| 347 | 348 | <h2 class="card-h" id="readme-h"> | |
| 348 | 349 | <span class="ico i-file" aria-hidden="true"> | |
| 349 | 350 | </span> README.md</h2> | |
| 350 | - | <article class="prose" aria-label="README.md"> | |
| 351 | + | <article class="prose" aria-label="README.md" hx-ignore> | |
| 351 | 352 | <h1 id="md-files">Files</h1> | |
| 352 | 353 | <p>See <a href="/files/blob/main/docs/guide.md">the guide</a>.</p> | |
| 353 | 354 | </article> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
| @@ -347,7 +348,7 @@ | |||||
| 347 | <h2 class="card-h" id="readme-h"> | 348 | <h2 class="card-h" id="readme-h"> | ||
| 348 | <span class="ico i-file" aria-hidden="true"> | 349 | <span class="ico i-file" aria-hidden="true"> | ||
| 349 | </span> README.md</h2> | 350 | </span> README.md</h2> | ||
| 350 | - | <article class="prose" aria-label="README.md"> | 351 | + | <article class="prose" aria-label="README.md" hx-ignore> |
| 351 | <h1 id="md-files">Files</h1> | 352 | <h1 id="md-files">Files</h1> | ||
| 352 | <p>See <a href="/files/blob/main/docs/guide.md">the guide</a>.</p> | 353 | <p>See <a href="/files/blob/main/docs/guide.md">the guide</a>.</p> | ||
| 353 | </article> | 354 | </article> | ||
M cmd/ermite/testdata/web/tree-slash-ref.html +1 -0
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
M cmd/ermite/testdata/web/tree-subdir.html +2 -1
| @@ -8,6 +8,7 @@ | |||
| 8 | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | |
| 9 | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | |
| 10 | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | |
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |
| 11 | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | |
| 12 | 13 | </script> | |
| 13 | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | |
| @@ -142,7 +143,7 @@ | |||
| 142 | 143 | <h2 class="card-h" id="readme-h"> | |
| 143 | 144 | <span class="ico i-file" aria-hidden="true"> | |
| 144 | 145 | </span> README.md</h2> | |
| 145 | - | <article class="prose" aria-label="README.md"> | |
| 146 | + | <article class="prose" aria-label="README.md" hx-ignore> | |
| 146 | 147 | <h1 id="md-docs">Docs</h1> | |
| 147 | 148 | <p>Back to <a href="/files/blob/main/docs/guide.md">the guide</a> and <a href="/files/blob/main/main.go">root</a>.</p> | |
| 148 | 149 | </article> | |
| @@ -8,6 +8,7 @@ | |||||
| 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | 8 | <link rel="icon" type="image/svg+xml" href="/-/static/img/icon.HASH.svg"> | ||
| 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | 9 | <link rel="preload" href="/-/static/fonts/atkinson-next.HASH.woff2" as="font" type="font/woff2" crossorigin> | ||
| 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | 10 | <link rel="stylesheet" href="/-/static/css/ermite.HASH.css"> | ||
| 11 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'> | |||
| 11 | <script src="/-/static/js/htmx.min.HASH.js" defer> | 12 | <script src="/-/static/js/htmx.min.HASH.js" defer> | ||
| 12 | </script> | 13 | </script> | ||
| 13 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | 14 | <script src="/-/static/js/hx-live.min.HASH.js" defer> | ||
| @@ -142,7 +143,7 @@ | |||||
| 142 | <h2 class="card-h" id="readme-h"> | 143 | <h2 class="card-h" id="readme-h"> | ||
| 143 | <span class="ico i-file" aria-hidden="true"> | 144 | <span class="ico i-file" aria-hidden="true"> | ||
| 144 | </span> README.md</h2> | 145 | </span> README.md</h2> | ||
| 145 | - | <article class="prose" aria-label="README.md"> | 146 | + | <article class="prose" aria-label="README.md" hx-ignore> |
| 146 | <h1 id="md-docs">Docs</h1> | 147 | <h1 id="md-docs">Docs</h1> | ||
| 147 | <p>Back to <a href="/files/blob/main/docs/guide.md">the guide</a> and <a href="/files/blob/main/main.go">root</a>.</p> | 148 | <p>Back to <a href="/files/blob/main/docs/guide.md">the guide</a> and <a href="/files/blob/main/main.go">root</a>.</p> | ||
| 148 | </article> | 149 | </article> | ||
M cmd/ermite/web_files_test.go +1 -1
| @@ -305,7 +305,7 @@ func TestWebFilesContentSecurity(t *testing.T) { | |||
| 305 | 305 | h := newHarness(t, harnessOpts{}) | |
| 306 | 306 | h.webFilesRepo("files") | |
| 307 | 307 | ||
| 308 | - | const csp = "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'" | |
| 308 | + | const csp = "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; object-src 'none'" | |
| 309 | 309 | for _, p := range []string{ | |
| 310 | 310 | "/files/tree/main", "/files/tree/main/docs", "/files/blob/main/main.go", "/files/blob/main/docs/guide.md", | |
| 311 | 311 | "/files/blob/main/pic.png", "/files/blob/main/logo.svg", "/files/blob/main/evil.go", "/files/blob/main/big.txt", | |
| @@ -305,7 +305,7 @@ func TestWebFilesContentSecurity(t *testing.T) { | |||||
| 305 | h := newHarness(t, harnessOpts{}) | 305 | h := newHarness(t, harnessOpts{}) | ||
| 306 | h.webFilesRepo("files") | 306 | h.webFilesRepo("files") | ||
| 307 | 307 | ||||
| 308 | - | const csp = "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'" | 308 | + | const csp = "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; object-src 'none'" |
| 309 | for _, p := range []string{ | 309 | for _, p := range []string{ | ||
| 310 | "/files/tree/main", "/files/tree/main/docs", "/files/blob/main/main.go", "/files/blob/main/docs/guide.md", | 310 | "/files/tree/main", "/files/tree/main/docs", "/files/blob/main/main.go", "/files/blob/main/docs/guide.md", | ||
| 311 | "/files/blob/main/pic.png", "/files/blob/main/logo.svg", "/files/blob/main/evil.go", "/files/blob/main/big.txt", | 311 | "/files/blob/main/pic.png", "/files/blob/main/logo.svg", "/files/blob/main/evil.go", "/files/blob/main/big.txt", | ||
M cmd/ermite/web_test.go +1 -1
| @@ -207,7 +207,7 @@ func TestWebContentSecurity(t *testing.T) { | |||
| 207 | 207 | h.webRepo("demo") | |
| 208 | 208 | h.seed("quiet", domain.VisibilityPublic) | |
| 209 | 209 | ||
| 210 | - | const csp = "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'" | |
| 210 | + | const csp = "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; object-src 'none'" | |
| 211 | 211 | for _, p := range []string{"/", "/demo", "/demo/refs", "/quiet", "/nope", "/demo/commits/main"} { | |
| 212 | 212 | status, hdr, body := h.get(p) | |
| 213 | 213 | assert.Contains(t, []int{200, 404}, status, p) | |
| @@ -207,7 +207,7 @@ func TestWebContentSecurity(t *testing.T) { | |||||
| 207 | h.webRepo("demo") | 207 | h.webRepo("demo") | ||
| 208 | h.seed("quiet", domain.VisibilityPublic) | 208 | h.seed("quiet", domain.VisibilityPublic) | ||
| 209 | 209 | ||||
| 210 | - | const csp = "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'" | 210 | + | const csp = "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; object-src 'none'" |
| 211 | for _, p := range []string{"/", "/demo", "/demo/refs", "/quiet", "/nope", "/demo/commits/main"} { | 211 | for _, p := range []string{"/", "/demo", "/demo/refs", "/quiet", "/nope", "/demo/commits/main"} { | ||
| 212 | status, hdr, body := h.get(p) | 212 | status, hdr, body := h.get(p) | ||
| 213 | assert.Contains(t, []int{200, 404}, status, p) | 213 | assert.Contains(t, []int{200, 404}, status, p) | ||
M internal/adapters/httpserver/health.go +47 -8
| @@ -3,26 +3,65 @@ | |||
| 3 | 3 | package httpserver | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | + | "context" | |
| 6 | 7 | "net/http" | |
| 8 | + | "sync" | |
| 9 | + | "time" | |
| 7 | 10 | ||
| 8 | 11 | "github.com/bnema/zerowrap" | |
| 9 | 12 | ) | |
| 10 | 13 | ||
| 14 | + | // healthCacheTTL is how long the result of the checks is reused. The endpoint | |
| 15 | + | // is unauthenticated and the checks touch the disk and the store: a burst of | |
| 16 | + | // requests must not turn into a burst of work. | |
| 17 | + | const healthCacheTTL = 5 * time.Second | |
| 18 | + | ||
| 19 | + | // healthCheckTimeout bounds one run of the checks. | |
| 20 | + | const healthCheckTimeout = 5 * time.Second | |
| 21 | + | ||
| 11 | 22 | // healthHandler answers 200 "ok" when every check passes and 503 otherwise. | |
| 12 | 23 | // The failure is logged, never sent: the body stays the same for any cause. | |
| 24 | + | // The result, failure included, is kept for healthCacheTTL; concurrent | |
| 25 | + | // requests wait for the run in progress rather than starting their own. | |
| 13 | 26 | func healthHandler(checks []Check) http.HandlerFunc { | |
| 27 | + | var ( | |
| 28 | + | mu sync.Mutex | |
| 29 | + | until time.Time // the cached result is valid before this instant | |
| 30 | + | healthy bool | |
| 31 | + | ) | |
| 14 | 32 | return func(w http.ResponseWriter, r *http.Request) { | |
| 33 | + | mu.Lock() | |
| 34 | + | if !time.Now().Before(until) { | |
| 35 | + | // The run belongs to every waiting request, not to this one: a | |
| 36 | + | // client that goes away must not cache a failure. | |
| 37 | + | ctx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), healthCheckTimeout) | |
| 38 | + | healthy = runChecks(ctx, checks) | |
| 39 | + | cancel() | |
| 40 | + | until = time.Now().Add(healthCacheTTL) | |
| 41 | + | } | |
| 42 | + | ok := healthy | |
| 43 | + | mu.Unlock() | |
| 44 | + | ||
| 15 | 45 | w.Header().Set("Cache-Control", "no-store") | |
| 16 | 46 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") | |
| 17 | - | for _, check := range checks { | |
| 18 | - | if err := check(r.Context()); err != nil { | |
| 19 | - | log := zerowrap.FromCtx(r.Context()) | |
| 20 | - | log.Warn().Err(err).Msg("health check failed") | |
| 21 | - | w.WriteHeader(http.StatusServiceUnavailable) | |
| 22 | - | _, _ = w.Write([]byte("unavailable\n")) | |
| 23 | - | return | |
| 24 | - | } | |
| 47 | + | if !ok { | |
| 48 | + | w.WriteHeader(http.StatusServiceUnavailable) | |
| 49 | + | _, _ = w.Write([]byte("unavailable\n")) | |
| 50 | + | return | |
| 25 | 51 | } | |
| 26 | 52 | _, _ = w.Write([]byte("ok\n")) | |
| 27 | 53 | } | |
| 28 | 54 | } | |
| 55 | + | ||
| 56 | + | // runChecks runs the checks in order and stops at the first failure, which it | |
| 57 | + | // logs. | |
| 58 | + | func runChecks(ctx context.Context, checks []Check) bool { | |
| 59 | + | for _, check := range checks { | |
| 60 | + | if err := check(ctx); err != nil { | |
| 61 | + | log := zerowrap.FromCtx(ctx) | |
| 62 | + | log.Warn().Err(err).Msg("health check failed") | |
| 63 | + | return false | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | return true | |
| 67 | + | } | |
| @@ -3,26 +3,65 @@ | |||||
| 3 | package httpserver | 3 | package httpserver | ||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | + | "context" | |||
| 6 | "net/http" | 7 | "net/http" | ||
| 8 | + | "sync" | |||
| 9 | + | "time" | |||
| 7 | 10 | ||||
| 8 | "github.com/bnema/zerowrap" | 11 | "github.com/bnema/zerowrap" | ||
| 9 | ) | 12 | ) | ||
| 10 | 13 | ||||
| 14 | + | // healthCacheTTL is how long the result of the checks is reused. The endpoint | |||
| 15 | + | // is unauthenticated and the checks touch the disk and the store: a burst of | |||
| 16 | + | // requests must not turn into a burst of work. | |||
| 17 | + | const healthCacheTTL = 5 * time.Second | |||
| 18 | + | ||||
| 19 | + | // healthCheckTimeout bounds one run of the checks. | |||
| 20 | + | const healthCheckTimeout = 5 * time.Second | |||
| 21 | + | ||||
| 11 | // healthHandler answers 200 "ok" when every check passes and 503 otherwise. | 22 | // healthHandler answers 200 "ok" when every check passes and 503 otherwise. | ||
| 12 | // The failure is logged, never sent: the body stays the same for any cause. | 23 | // The failure is logged, never sent: the body stays the same for any cause. | ||
| 24 | + | // The result, failure included, is kept for healthCacheTTL; concurrent | |||
| 25 | + | // requests wait for the run in progress rather than starting their own. | |||
| 13 | func healthHandler(checks []Check) http.HandlerFunc { | 26 | func healthHandler(checks []Check) http.HandlerFunc { | ||
| 27 | + | var ( | |||
| 28 | + | mu sync.Mutex | |||
| 29 | + | until time.Time // the cached result is valid before this instant | |||
| 30 | + | healthy bool | |||
| 31 | + | ) | |||
| 14 | return func(w http.ResponseWriter, r *http.Request) { | 32 | return func(w http.ResponseWriter, r *http.Request) { | ||
| 33 | + | mu.Lock() | |||
| 34 | + | if !time.Now().Before(until) { | |||
| 35 | + | // The run belongs to every waiting request, not to this one: a | |||
| 36 | + | // client that goes away must not cache a failure. | |||
| 37 | + | ctx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), healthCheckTimeout) | |||
| 38 | + | healthy = runChecks(ctx, checks) | |||
| 39 | + | cancel() | |||
| 40 | + | until = time.Now().Add(healthCacheTTL) | |||
| 41 | + | } | |||
| 42 | + | ok := healthy | |||
| 43 | + | mu.Unlock() | |||
| 44 | + | ||||
| 15 | w.Header().Set("Cache-Control", "no-store") | 45 | w.Header().Set("Cache-Control", "no-store") | ||
| 16 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") | 46 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") | ||
| 17 | - | for _, check := range checks { | 47 | + | if !ok { |
| 18 | - | if err := check(r.Context()); err != nil { | 48 | + | w.WriteHeader(http.StatusServiceUnavailable) |
| 19 | - | log := zerowrap.FromCtx(r.Context()) | 49 | + | _, _ = w.Write([]byte("unavailable\n")) |
| 20 | - | log.Warn().Err(err).Msg("health check failed") | 50 | + | return |
| 21 | - | w.WriteHeader(http.StatusServiceUnavailable) | |||
| 22 | - | _, _ = w.Write([]byte("unavailable\n")) | |||
| 23 | - | return | |||
| 24 | - | } | |||
| 25 | } | 51 | } | ||
| 26 | _, _ = w.Write([]byte("ok\n")) | 52 | _, _ = w.Write([]byte("ok\n")) | ||
| 27 | } | 53 | } | ||
| 28 | } | 54 | } | ||
| 55 | + | ||||
| 56 | + | // runChecks runs the checks in order and stops at the first failure, which it | |||
| 57 | + | // logs. | |||
| 58 | + | func runChecks(ctx context.Context, checks []Check) bool { | |||
| 59 | + | for _, check := range checks { | |||
| 60 | + | if err := check(ctx); err != nil { | |||
| 61 | + | log := zerowrap.FromCtx(ctx) | |||
| 62 | + | log.Warn().Err(err).Msg("health check failed") | |||
| 63 | + | return false | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | return true | |||
| 67 | + | } | |||
M internal/adapters/httpserver/middleware.go +20 -4
| @@ -4,6 +4,7 @@ package httpserver | |||
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | 6 | "net/http" | |
| 7 | + | "net/url" | |
| 7 | 8 | "path" | |
| 8 | 9 | "strings" | |
| 9 | 10 | "time" | |
| @@ -12,18 +13,33 @@ import ( | |||
| 12 | 13 | ) | |
| 13 | 14 | ||
| 14 | 15 | // contentSecurityPolicy allows scripts from the origin plus 'unsafe-eval' | |
| 15 | - | // for htmx attribute expressions; inline scripts stay forbidden. | |
| 16 | - | const contentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'" | |
| 16 | + | // for htmx attribute expressions; inline scripts stay forbidden. Forms post | |
| 17 | + | // to the origin only, <base> and plugins are off. | |
| 18 | + | const contentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; object-src 'none'" | |
| 19 | + | ||
| 20 | + | // hstsValue is one year. No includeSubDomains or preload: the server cannot | |
| 21 | + | // speak for the rest of the domain. | |
| 22 | + | const hstsValue = "max-age=31536000" | |
| 23 | + | ||
| 24 | + | // isHTTPS reports whether the public URL is an https:// URL. | |
| 25 | + | func isHTTPS(publicURL string) bool { | |
| 26 | + | u, err := url.Parse(publicURL) | |
| 27 | + | return err == nil && strings.EqualFold(u.Scheme, "https") | |
| 28 | + | } | |
| 17 | 29 | ||
| 18 | 30 | // withSecurityHeaders sets the security headers on every response, before | |
| 19 | - | // the handler runs, so that errors and 404s carry them too. | |
| 20 | - | func withSecurityHeaders(next http.Handler) http.Handler { | |
| 31 | + | // the handler runs, so that errors and 404s carry them too. With hsts, the | |
| 32 | + | // browser is told to stay on https. | |
| 33 | + | func withSecurityHeaders(next http.Handler, hsts bool) http.Handler { | |
| 21 | 34 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 22 | 35 | h := w.Header() | |
| 23 | 36 | h.Set("Content-Security-Policy", contentSecurityPolicy) | |
| 24 | 37 | h.Set("X-Frame-Options", "DENY") | |
| 25 | 38 | h.Set("X-Content-Type-Options", "nosniff") | |
| 26 | 39 | h.Set("Referrer-Policy", "same-origin") | |
| 40 | + | if hsts { | |
| 41 | + | h.Set("Strict-Transport-Security", hstsValue) | |
| 42 | + | } | |
| 27 | 43 | next.ServeHTTP(w, r) | |
| 28 | 44 | }) | |
| 29 | 45 | } | |
| @@ -4,6 +4,7 @@ package httpserver | |||||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | "net/http" | 6 | "net/http" | ||
| 7 | + | "net/url" | |||
| 7 | "path" | 8 | "path" | ||
| 8 | "strings" | 9 | "strings" | ||
| 9 | "time" | 10 | "time" | ||
| @@ -12,18 +13,33 @@ import ( | |||||
| 12 | ) | 13 | ) | ||
| 13 | 14 | ||||
| 14 | // contentSecurityPolicy allows scripts from the origin plus 'unsafe-eval' | 15 | // contentSecurityPolicy allows scripts from the origin plus 'unsafe-eval' | ||
| 15 | - | // for htmx attribute expressions; inline scripts stay forbidden. | 16 | + | // for htmx attribute expressions; inline scripts stay forbidden. Forms post |
| 16 | - | const contentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'" | 17 | + | // to the origin only, <base> and plugins are off. |
| 18 | + | const contentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; object-src 'none'" | |||
| 19 | + | ||||
| 20 | + | // hstsValue is one year. No includeSubDomains or preload: the server cannot | |||
| 21 | + | // speak for the rest of the domain. | |||
| 22 | + | const hstsValue = "max-age=31536000" | |||
| 23 | + | ||||
| 24 | + | // isHTTPS reports whether the public URL is an https:// URL. | |||
| 25 | + | func isHTTPS(publicURL string) bool { | |||
| 26 | + | u, err := url.Parse(publicURL) | |||
| 27 | + | return err == nil && strings.EqualFold(u.Scheme, "https") | |||
| 28 | + | } | |||
| 17 | 29 | ||||
| 18 | // withSecurityHeaders sets the security headers on every response, before | 30 | // withSecurityHeaders sets the security headers on every response, before | ||
| 19 | - | // the handler runs, so that errors and 404s carry them too. | 31 | + | // the handler runs, so that errors and 404s carry them too. With hsts, the |
| 20 | - | func withSecurityHeaders(next http.Handler) http.Handler { | 32 | + | // browser is told to stay on https. |
| 33 | + | func withSecurityHeaders(next http.Handler, hsts bool) http.Handler { | |||
| 21 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 34 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 22 | h := w.Header() | 35 | h := w.Header() | ||
| 23 | h.Set("Content-Security-Policy", contentSecurityPolicy) | 36 | h.Set("Content-Security-Policy", contentSecurityPolicy) | ||
| 24 | h.Set("X-Frame-Options", "DENY") | 37 | h.Set("X-Frame-Options", "DENY") | ||
| 25 | h.Set("X-Content-Type-Options", "nosniff") | 38 | h.Set("X-Content-Type-Options", "nosniff") | ||
| 26 | h.Set("Referrer-Policy", "same-origin") | 39 | h.Set("Referrer-Policy", "same-origin") | ||
| 40 | + | if hsts { | |||
| 41 | + | h.Set("Strict-Transport-Security", hstsValue) | |||
| 42 | + | } | |||
| 27 | next.ServeHTTP(w, r) | 43 | next.ServeHTTP(w, r) | ||
| 28 | }) | 44 | }) | ||
| 29 | } | 45 | } | ||
M internal/adapters/httpserver/server.go +7 -2
| @@ -36,7 +36,12 @@ type Route struct { | |||
| 36 | 36 | type Config struct { | |
| 37 | 37 | // Addr is the listen address, "host:port". | |
| 38 | 38 | Addr string | |
| 39 | - | // HealthChecks run on every GET /-/health; one failure answers 503. | |
| 39 | + | // PublicURL is the public base URL of the server. When it is https://, | |
| 40 | + | // every response carries Strict-Transport-Security. | |
| 41 | + | PublicURL string | |
| 42 | + | // HealthChecks run on GET /-/health, at most once per healthCacheTTL | |
| 43 | + | // (the answer is cached, the endpoint being unauthenticated); one | |
| 44 | + | // failure answers 503. | |
| 40 | 45 | HealthChecks []Check | |
| 41 | 46 | // Routes are added to the router next to /-/health. | |
| 42 | 47 | Routes []Route | |
| @@ -71,7 +76,7 @@ func New(cfg Config) *Server { | |||
| 71 | 76 | for _, r := range cfg.Routes { | |
| 72 | 77 | mux.Handle(r.Pattern, r.Handler) | |
| 73 | 78 | } | |
| 74 | - | return &Server{addr: cfg.Addr, handler: withRequestLog(withSecurityHeaders(mux)), onShutdown: cfg.OnShutdown} | |
| 79 | + | return &Server{addr: cfg.Addr, handler: withRequestLog(withSecurityHeaders(mux, isHTTPS(cfg.PublicURL))), onShutdown: cfg.OnShutdown} | |
| 75 | 80 | } | |
| 76 | 81 | ||
| 77 | 82 | // Handler returns the root handler, for tests. | |
| @@ -36,7 +36,12 @@ type Route struct { | |||||
| 36 | type Config struct { | 36 | type Config struct { | ||
| 37 | // Addr is the listen address, "host:port". | 37 | // Addr is the listen address, "host:port". | ||
| 38 | Addr string | 38 | Addr string | ||
| 39 | - | // HealthChecks run on every GET /-/health; one failure answers 503. | 39 | + | // PublicURL is the public base URL of the server. When it is https://, |
| 40 | + | // every response carries Strict-Transport-Security. | |||
| 41 | + | PublicURL string | |||
| 42 | + | // HealthChecks run on GET /-/health, at most once per healthCacheTTL | |||
| 43 | + | // (the answer is cached, the endpoint being unauthenticated); one | |||
| 44 | + | // failure answers 503. | |||
| 40 | HealthChecks []Check | 45 | HealthChecks []Check | ||
| 41 | // Routes are added to the router next to /-/health. | 46 | // Routes are added to the router next to /-/health. | ||
| 42 | Routes []Route | 47 | Routes []Route | ||
| @@ -71,7 +76,7 @@ func New(cfg Config) *Server { | |||||
| 71 | for _, r := range cfg.Routes { | 76 | for _, r := range cfg.Routes { | ||
| 72 | mux.Handle(r.Pattern, r.Handler) | 77 | mux.Handle(r.Pattern, r.Handler) | ||
| 73 | } | 78 | } | ||
| 74 | - | return &Server{addr: cfg.Addr, handler: withRequestLog(withSecurityHeaders(mux)), onShutdown: cfg.OnShutdown} | 79 | + | return &Server{addr: cfg.Addr, handler: withRequestLog(withSecurityHeaders(mux, isHTTPS(cfg.PublicURL))), onShutdown: cfg.OnShutdown} |
| 75 | } | 80 | } | ||
| 76 | 81 | ||||
| 77 | // Handler returns the root handler, for tests. | 82 | // Handler returns the root handler, for tests. | ||
M internal/adapters/httpserver/server_test.go +76 -1
| @@ -14,7 +14,9 @@ import ( | |||
| 14 | 14 | "os" | |
| 15 | 15 | "strings" | |
| 16 | 16 | "sync" | |
| 17 | + | "sync/atomic" | |
| 17 | 18 | "testing" | |
| 19 | + | "testing/synctest" | |
| 18 | 20 | "time" | |
| 19 | 21 | ||
| 20 | 22 | "github.com/bnema/zerowrap" | |
| @@ -25,7 +27,7 @@ import ( | |||
| 25 | 27 | ) | |
| 26 | 28 | ||
| 27 | 29 | var securityHeaders = map[string]string{ | |
| 28 | - | "Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'", | |
| 30 | + | "Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; object-src 'none'", | |
| 29 | 31 | "X-Frame-Options": "DENY", | |
| 30 | 32 | "X-Content-Type-Options": "nosniff", | |
| 31 | 33 | "Referrer-Policy": "same-origin", | |
| @@ -76,6 +78,79 @@ func TestHealth(t *testing.T) { | |||
| 76 | 78 | } | |
| 77 | 79 | } | |
| 78 | 80 | ||
| 81 | + | func TestHealthIsCached(t *testing.T) { | |
| 82 | + | synctest.Test(t, func(t *testing.T) { | |
| 83 | + | var calls atomic.Int32 | |
| 84 | + | var failing atomic.Bool | |
| 85 | + | check := httpserver.Check(func(context.Context) error { | |
| 86 | + | calls.Add(1) | |
| 87 | + | if failing.Load() { | |
| 88 | + | return errors.New("down") | |
| 89 | + | } | |
| 90 | + | return nil | |
| 91 | + | }) | |
| 92 | + | h := httpserver.New(httpserver.Config{HealthChecks: []httpserver.Check{check}}).Handler() | |
| 93 | + | ||
| 94 | + | steps := []struct { | |
| 95 | + | name string | |
| 96 | + | sleep time.Duration | |
| 97 | + | fail bool | |
| 98 | + | wantStatus int | |
| 99 | + | wantCalls int32 | |
| 100 | + | }{ | |
| 101 | + | {"first request runs the checks", 0, false, 200, 1}, | |
| 102 | + | {"a burst reuses the result", 4 * time.Second, true, 200, 1}, | |
| 103 | + | {"after the TTL the checks run again", time.Second, true, 503, 2}, | |
| 104 | + | {"a failure is cached too", time.Second, false, 503, 2}, | |
| 105 | + | {"and recovers after the TTL", 4 * time.Second, false, 200, 3}, | |
| 106 | + | } | |
| 107 | + | for _, st := range steps { | |
| 108 | + | time.Sleep(st.sleep) | |
| 109 | + | failing.Store(st.fail) | |
| 110 | + | rec := do(t, h, "GET", "/-/health") | |
| 111 | + | assert.Equal(t, st.wantStatus, rec.Code, st.name) | |
| 112 | + | assert.Equal(t, st.wantCalls, calls.Load(), st.name) | |
| 113 | + | } | |
| 114 | + | }) | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | func TestHealthConcurrentRequestsShareOneRun(t *testing.T) { | |
| 118 | + | var calls atomic.Int32 | |
| 119 | + | check := httpserver.Check(func(context.Context) error { calls.Add(1); return nil }) | |
| 120 | + | h := httpserver.New(httpserver.Config{HealthChecks: []httpserver.Check{check}}).Handler() | |
| 121 | + | ||
| 122 | + | var wg sync.WaitGroup | |
| 123 | + | for range 20 { | |
| 124 | + | wg.Go(func() { assert.Equal(t, 200, do(t, h, "GET", "/-/health").Code) }) | |
| 125 | + | } | |
| 126 | + | wg.Wait() | |
| 127 | + | assert.Equal(t, int32(1), calls.Load()) | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | func TestStrictTransportSecurity(t *testing.T) { | |
| 131 | + | tests := []struct { | |
| 132 | + | name string | |
| 133 | + | publicURL string | |
| 134 | + | want string | |
| 135 | + | }{ | |
| 136 | + | {"https", "https://forge.example.com", "max-age=31536000"}, | |
| 137 | + | {"https uppercase scheme", "HTTPS://forge.example.com", "max-age=31536000"}, | |
| 138 | + | {"http", "http://forge.example.com", ""}, | |
| 139 | + | {"http loopback", "http://127.0.0.1:7341", ""}, | |
| 140 | + | {"unset", "", ""}, | |
| 141 | + | {"garbage", "://%", ""}, | |
| 142 | + | } | |
| 143 | + | for _, tt := range tests { | |
| 144 | + | t.Run(tt.name, func(t *testing.T) { | |
| 145 | + | h := httpserver.New(httpserver.Config{PublicURL: tt.publicURL}).Handler() | |
| 146 | + | for _, target := range []string{"/-/health", "/nope"} { | |
| 147 | + | rec := do(t, h, "GET", target) | |
| 148 | + | assert.Equal(t, tt.want, rec.Header().Get("Strict-Transport-Security"), target) | |
| 149 | + | } | |
| 150 | + | }) | |
| 151 | + | } | |
| 152 | + | } | |
| 153 | + | ||
| 79 | 154 | func TestHeadOverTheWire(t *testing.T) { | |
| 80 | 155 | ts := httptest.NewServer(httpserver.New(httpserver.Config{}).Handler()) | |
| 81 | 156 | defer ts.Close() | |
| @@ -14,7 +14,9 @@ import ( | |||||
| 14 | "os" | 14 | "os" | ||
| 15 | "strings" | 15 | "strings" | ||
| 16 | "sync" | 16 | "sync" | ||
| 17 | + | "sync/atomic" | |||
| 17 | "testing" | 18 | "testing" | ||
| 19 | + | "testing/synctest" | |||
| 18 | "time" | 20 | "time" | ||
| 19 | 21 | ||||
| 20 | "github.com/bnema/zerowrap" | 22 | "github.com/bnema/zerowrap" | ||
| @@ -25,7 +27,7 @@ import ( | |||||
| 25 | ) | 27 | ) | ||
| 26 | 28 | ||||
| 27 | var securityHeaders = map[string]string{ | 29 | var securityHeaders = map[string]string{ | ||
| 28 | - | "Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'", | 30 | + | "Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; object-src 'none'", |
| 29 | "X-Frame-Options": "DENY", | 31 | "X-Frame-Options": "DENY", | ||
| 30 | "X-Content-Type-Options": "nosniff", | 32 | "X-Content-Type-Options": "nosniff", | ||
| 31 | "Referrer-Policy": "same-origin", | 33 | "Referrer-Policy": "same-origin", | ||
| @@ -76,6 +78,79 @@ func TestHealth(t *testing.T) { | |||||
| 76 | } | 78 | } | ||
| 77 | } | 79 | } | ||
| 78 | 80 | ||||
| 81 | + | func TestHealthIsCached(t *testing.T) { | |||
| 82 | + | synctest.Test(t, func(t *testing.T) { | |||
| 83 | + | var calls atomic.Int32 | |||
| 84 | + | var failing atomic.Bool | |||
| 85 | + | check := httpserver.Check(func(context.Context) error { | |||
| 86 | + | calls.Add(1) | |||
| 87 | + | if failing.Load() { | |||
| 88 | + | return errors.New("down") | |||
| 89 | + | } | |||
| 90 | + | return nil | |||
| 91 | + | }) | |||
| 92 | + | h := httpserver.New(httpserver.Config{HealthChecks: []httpserver.Check{check}}).Handler() | |||
| 93 | + | ||||
| 94 | + | steps := []struct { | |||
| 95 | + | name string | |||
| 96 | + | sleep time.Duration | |||
| 97 | + | fail bool | |||
| 98 | + | wantStatus int | |||
| 99 | + | wantCalls int32 | |||
| 100 | + | }{ | |||
| 101 | + | {"first request runs the checks", 0, false, 200, 1}, | |||
| 102 | + | {"a burst reuses the result", 4 * time.Second, true, 200, 1}, | |||
| 103 | + | {"after the TTL the checks run again", time.Second, true, 503, 2}, | |||
| 104 | + | {"a failure is cached too", time.Second, false, 503, 2}, | |||
| 105 | + | {"and recovers after the TTL", 4 * time.Second, false, 200, 3}, | |||
| 106 | + | } | |||
| 107 | + | for _, st := range steps { | |||
| 108 | + | time.Sleep(st.sleep) | |||
| 109 | + | failing.Store(st.fail) | |||
| 110 | + | rec := do(t, h, "GET", "/-/health") | |||
| 111 | + | assert.Equal(t, st.wantStatus, rec.Code, st.name) | |||
| 112 | + | assert.Equal(t, st.wantCalls, calls.Load(), st.name) | |||
| 113 | + | } | |||
| 114 | + | }) | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | func TestHealthConcurrentRequestsShareOneRun(t *testing.T) { | |||
| 118 | + | var calls atomic.Int32 | |||
| 119 | + | check := httpserver.Check(func(context.Context) error { calls.Add(1); return nil }) | |||
| 120 | + | h := httpserver.New(httpserver.Config{HealthChecks: []httpserver.Check{check}}).Handler() | |||
| 121 | + | ||||
| 122 | + | var wg sync.WaitGroup | |||
| 123 | + | for range 20 { | |||
| 124 | + | wg.Go(func() { assert.Equal(t, 200, do(t, h, "GET", "/-/health").Code) }) | |||
| 125 | + | } | |||
| 126 | + | wg.Wait() | |||
| 127 | + | assert.Equal(t, int32(1), calls.Load()) | |||
| 128 | + | } | |||
| 129 | + | ||||
| 130 | + | func TestStrictTransportSecurity(t *testing.T) { | |||
| 131 | + | tests := []struct { | |||
| 132 | + | name string | |||
| 133 | + | publicURL string | |||
| 134 | + | want string | |||
| 135 | + | }{ | |||
| 136 | + | {"https", "https://forge.example.com", "max-age=31536000"}, | |||
| 137 | + | {"https uppercase scheme", "HTTPS://forge.example.com", "max-age=31536000"}, | |||
| 138 | + | {"http", "http://forge.example.com", ""}, | |||
| 139 | + | {"http loopback", "http://127.0.0.1:7341", ""}, | |||
| 140 | + | {"unset", "", ""}, | |||
| 141 | + | {"garbage", "://%", ""}, | |||
| 142 | + | } | |||
| 143 | + | for _, tt := range tests { | |||
| 144 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 145 | + | h := httpserver.New(httpserver.Config{PublicURL: tt.publicURL}).Handler() | |||
| 146 | + | for _, target := range []string{"/-/health", "/nope"} { | |||
| 147 | + | rec := do(t, h, "GET", target) | |||
| 148 | + | assert.Equal(t, tt.want, rec.Header().Get("Strict-Transport-Security"), target) | |||
| 149 | + | } | |||
| 150 | + | }) | |||
| 151 | + | } | |||
| 152 | + | } | |||
| 153 | + | ||||
| 79 | func TestHeadOverTheWire(t *testing.T) { | 154 | func TestHeadOverTheWire(t *testing.T) { | ||
| 80 | ts := httptest.NewServer(httpserver.New(httpserver.Config{}).Handler()) | 155 | ts := httptest.NewServer(httpserver.New(httpserver.Config{}).Handler()) | ||
| 81 | defer ts.Close() | 156 | defer ts.Close() | ||
M internal/adapters/web/blob.templ +1 -1
| @@ -54,7 +54,7 @@ templ CodeView(h template.HTML) { | |||
| 54 | 54 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" | |
| 55 | 55 | hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" | |
| 56 | 56 | data-ready="" | |
| 57 | - | ><code>@templ.Raw(string(h))</code></pre> | |
| 57 | + | ><code hx-ignore>@templ.Raw(string(h))</code></pre> | |
| 58 | 58 | } | |
| 59 | 59 | ||
| 60 | 60 | templ blobRail(p BlobPage) { | |
| @@ -54,7 +54,7 @@ templ CodeView(h template.HTML) { | |||||
| 54 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" | 54 | if (lo && event.detail?.scroll) document.getElementById('L' + lo)?.scrollIntoView({ block: 'center' })" | ||
| 55 | hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" | 55 | hx-live="if (!data.ready) { data.ready = '1'; trigger('sel', { scroll: true }) }" | ||
| 56 | data-ready="" | 56 | data-ready="" | ||
| 57 | - | ><code>@templ.Raw(string(h))</code></pre> | 57 | + | ><code hx-ignore>@templ.Raw(string(h))</code></pre> |
| 58 | } | 58 | } | ||
| 59 | 59 | ||||
| 60 | templ blobRail(p BlobPage) { | 60 | templ blobRail(p BlobPage) { | ||
M internal/adapters/web/blob_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/components.templ +1 -1
| @@ -76,7 +76,7 @@ templ ReadmeCard(r Readme) { | |||
| 76 | 76 | // ReadmeView is a rendered README. Its HTML comes from the Markdown | |
| 77 | 77 | // renderer, which drops raw HTML and filters URLs. | |
| 78 | 78 | templ ReadmeView(r Readme) { | |
| 79 | - | <article class="prose" aria-label={ r.Name }> | |
| 79 | + | <article class="prose" aria-label={ r.Name } hx-ignore> | |
| 80 | 80 | if r.Note != "" { | |
| 81 | 81 | <p class="dim">{ r.Note }</p> | |
| 82 | 82 | } else { | |
| @@ -76,7 +76,7 @@ templ ReadmeCard(r Readme) { | |||||
| 76 | // ReadmeView is a rendered README. Its HTML comes from the Markdown | 76 | // ReadmeView is a rendered README. Its HTML comes from the Markdown | ||
| 77 | // renderer, which drops raw HTML and filters URLs. | 77 | // renderer, which drops raw HTML and filters URLs. | ||
| 78 | templ ReadmeView(r Readme) { | 78 | templ ReadmeView(r Readme) { | ||
| 79 | - | <article class="prose" aria-label={ r.Name }> | 79 | + | <article class="prose" aria-label={ r.Name } hx-ignore> |
| 80 | if r.Note != "" { | 80 | if r.Note != "" { | ||
| 81 | <p class="dim">{ r.Note }</p> | 81 | <p class="dim">{ r.Note }</p> | ||
| 82 | } else { | 82 | } else { | ||
M internal/adapters/web/components_templ.go +1 -1 generated file: collapsed
M internal/adapters/web/raw.go +1 -1
| @@ -81,7 +81,7 @@ func (s *Server) raw(w http.ResponseWriter, r *http.Request) { | |||
| 81 | 81 | // Headers are gone: all that is left is to cut the answer short, so | |
| 82 | 82 | // that the client sees it is incomplete. | |
| 83 | 83 | log := zerowrap.FromCtx(ctx) | |
| 84 | - | log.Error().Err(err).Str("repo", repo).Str("path", r.URL.Path).Msg("raw stream failed") | |
| 84 | + | log.Error().Err(err).Str("repo", repo).Str("path", logPath(r.URL.Path)).Msg("raw stream failed") | |
| 85 | 85 | panic(http.ErrAbortHandler) | |
| 86 | 86 | } | |
| 87 | 87 | } | |
| @@ -81,7 +81,7 @@ func (s *Server) raw(w http.ResponseWriter, r *http.Request) { | |||||
| 81 | // Headers are gone: all that is left is to cut the answer short, so | 81 | // Headers are gone: all that is left is to cut the answer short, so | ||
| 82 | // that the client sees it is incomplete. | 82 | // that the client sees it is incomplete. | ||
| 83 | log := zerowrap.FromCtx(ctx) | 83 | log := zerowrap.FromCtx(ctx) | ||
| 84 | - | log.Error().Err(err).Str("repo", repo).Str("path", r.URL.Path).Msg("raw stream failed") | 84 | + | log.Error().Err(err).Str("repo", repo).Str("path", logPath(r.URL.Path)).Msg("raw stream failed") |
| 85 | panic(http.ErrAbortHandler) | 85 | panic(http.ErrAbortHandler) | ||
| 86 | } | 86 | } | ||
| 87 | } | 87 | } | ||
M internal/adapters/web/shell.templ +1 -0
| @@ -16,6 +16,7 @@ templ Shell(l Layout, rail templ.Component) { | |||
| 16 | 16 | <link rel="icon" type="image/svg+xml" href={ templ.SafeURL(l.Assets.Icon) }/> | |
| 17 | 17 | <link rel="preload" href={ templ.SafeURL(l.Assets.Font) } as="font" type="font/woff2" crossorigin/> | |
| 18 | 18 | <link rel="stylesheet" href={ templ.SafeURL(l.Assets.CSS) }/> | |
| 19 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'/> | |
| 19 | 20 | <script src={ l.Assets.HTMX } defer></script> | |
| 20 | 21 | <script src={ l.Assets.Live } defer></script> | |
| 21 | 22 | <script src={ l.Assets.SSE } defer></script> | |
| @@ -16,6 +16,7 @@ templ Shell(l Layout, rail templ.Component) { | |||||
| 16 | <link rel="icon" type="image/svg+xml" href={ templ.SafeURL(l.Assets.Icon) }/> | 16 | <link rel="icon" type="image/svg+xml" href={ templ.SafeURL(l.Assets.Icon) }/> | ||
| 17 | <link rel="preload" href={ templ.SafeURL(l.Assets.Font) } as="font" type="font/woff2" crossorigin/> | 17 | <link rel="preload" href={ templ.SafeURL(l.Assets.Font) } as="font" type="font/woff2" crossorigin/> | ||
| 18 | <link rel="stylesheet" href={ templ.SafeURL(l.Assets.CSS) }/> | 18 | <link rel="stylesheet" href={ templ.SafeURL(l.Assets.CSS) }/> | ||
| 19 | + | <meta name="htmx-config" content='{"history":false,"includeIndicatorCSS":false,"extensions":"sse, hx-live"}'/> | |||
| 19 | <script src={ l.Assets.HTMX } defer></script> | 20 | <script src={ l.Assets.HTMX } defer></script> | ||
| 20 | <script src={ l.Assets.Live } defer></script> | 21 | <script src={ l.Assets.Live } defer></script> | ||
| 21 | <script src={ l.Assets.SSE } defer></script> | 22 | <script src={ l.Assets.SSE } defer></script> | ||
M internal/adapters/web/shell_templ.go +21 -21
| @@ -86,14 +86,14 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||
| 86 | 86 | if templ_7745c5c3_Err != nil { | |
| 87 | 87 | return templ_7745c5c3_Err | |
| 88 | 88 | } | |
| 89 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"><script src=\"") | |
| 89 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"><meta name=\"htmx-config\" content='{\"history\":false,\"includeIndicatorCSS\":false,\"extensions\":\"sse, hx-live\"}'><script src=\"") | |
| 90 | 90 | if templ_7745c5c3_Err != nil { | |
| 91 | 91 | return templ_7745c5c3_Err | |
| 92 | 92 | } | |
| 93 | 93 | var templ_7745c5c3_Var6 string | |
| 94 | 94 | templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.HTMX) | |
| 95 | 95 | if templ_7745c5c3_Err != nil { | |
| 96 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 19, Col: 30} | |
| 96 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 20, Col: 30} | |
| 97 | 97 | } | |
| 98 | 98 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6) | |
| 99 | 99 | if templ_7745c5c3_Err != nil { | |
| @@ -106,7 +106,7 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||
| 106 | 106 | var templ_7745c5c3_Var7 string | |
| 107 | 107 | templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Live) | |
| 108 | 108 | if templ_7745c5c3_Err != nil { | |
| 109 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 20, Col: 30} | |
| 109 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 21, Col: 30} | |
| 110 | 110 | } | |
| 111 | 111 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7) | |
| 112 | 112 | if templ_7745c5c3_Err != nil { | |
| @@ -119,7 +119,7 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||
| 119 | 119 | var templ_7745c5c3_Var8 string | |
| 120 | 120 | templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.SSE) | |
| 121 | 121 | if templ_7745c5c3_Err != nil { | |
| 122 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 21, Col: 29} | |
| 122 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 22, Col: 29} | |
| 123 | 123 | } | |
| 124 | 124 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8) | |
| 125 | 125 | if templ_7745c5c3_Err != nil { | |
| @@ -132,7 +132,7 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||
| 132 | 132 | var templ_7745c5c3_Var9 string | |
| 133 | 133 | templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.App) | |
| 134 | 134 | if templ_7745c5c3_Err != nil { | |
| 135 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 22, Col: 29} | |
| 135 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 23, Col: 29} | |
| 136 | 136 | } | |
| 137 | 137 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9) | |
| 138 | 138 | if templ_7745c5c3_Err != nil { | |
| @@ -145,7 +145,7 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||
| 145 | 145 | var templ_7745c5c3_Var10 string | |
| 146 | 146 | templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath) | |
| 147 | 147 | if templ_7745c5c3_Err != nil { | |
| 148 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 24, Col: 28} | |
| 148 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 25, Col: 28} | |
| 149 | 149 | } | |
| 150 | 150 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10) | |
| 151 | 151 | if templ_7745c5c3_Err != nil { | |
| @@ -158,7 +158,7 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||
| 158 | 158 | var templ_7745c5c3_Var11 string | |
| 159 | 159 | templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA) | |
| 160 | 160 | if templ_7745c5c3_Err != nil { | |
| 161 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 24, Col: 47} | |
| 161 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 25, Col: 47} | |
| 162 | 162 | } | |
| 163 | 163 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11) | |
| 164 | 164 | if templ_7745c5c3_Err != nil { | |
| @@ -202,7 +202,7 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||
| 202 | 202 | var templ_7745c5c3_Var14 string | |
| 203 | 203 | templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede) | |
| 204 | 204 | if templ_7745c5c3_Err != nil { | |
| 205 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 29, Col: 81} | |
| 205 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 30, Col: 81} | |
| 206 | 206 | } | |
| 207 | 207 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) | |
| 208 | 208 | if templ_7745c5c3_Err != nil { | |
| @@ -305,7 +305,7 @@ func Header(l Layout) templ.Component { | |||
| 305 | 305 | var templ_7745c5c3_Var16 string | |
| 306 | 306 | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(l.Instance) | |
| 307 | 307 | if templ_7745c5c3_Err != nil { | |
| 308 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 57, Col: 41} | |
| 308 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 58, Col: 41} | |
| 309 | 309 | } | |
| 310 | 310 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) | |
| 311 | 311 | if templ_7745c5c3_Err != nil { | |
| @@ -333,7 +333,7 @@ func Header(l Layout) templ.Component { | |||
| 333 | 333 | var templ_7745c5c3_Var17 templ.SafeURL | |
| 334 | 334 | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) | |
| 335 | 335 | if templ_7745c5c3_Err != nil { | |
| 336 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 66, Col: 38} | |
| 336 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 67, Col: 38} | |
| 337 | 337 | } | |
| 338 | 338 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) | |
| 339 | 339 | if templ_7745c5c3_Err != nil { | |
| @@ -356,7 +356,7 @@ func Header(l Layout) templ.Component { | |||
| 356 | 356 | var templ_7745c5c3_Var18 string | |
| 357 | 357 | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) | |
| 358 | 358 | if templ_7745c5c3_Err != nil { | |
| 359 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 70, Col: 19} | |
| 359 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 71, Col: 19} | |
| 360 | 360 | } | |
| 361 | 361 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) | |
| 362 | 362 | if templ_7745c5c3_Err != nil { | |
| @@ -384,7 +384,7 @@ func Header(l Layout) templ.Component { | |||
| 384 | 384 | var templ_7745c5c3_Var19 string | |
| 385 | 385 | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) | |
| 386 | 386 | if templ_7745c5c3_Err != nil { | |
| 387 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 76, Col: 19} | |
| 387 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 77, Col: 19} | |
| 388 | 388 | } | |
| 389 | 389 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) | |
| 390 | 390 | if templ_7745c5c3_Err != nil { | |
| @@ -417,7 +417,7 @@ func Header(l Layout) templ.Component { | |||
| 417 | 417 | var templ_7745c5c3_Var20 string | |
| 418 | 418 | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref) | |
| 419 | 419 | if templ_7745c5c3_Err != nil { | |
| 420 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 95, Col: 36} | |
| 420 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 96, Col: 36} | |
| 421 | 421 | } | |
| 422 | 422 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) | |
| 423 | 423 | if templ_7745c5c3_Err != nil { | |
| @@ -430,7 +430,7 @@ func Header(l Layout) templ.Component { | |||
| 430 | 430 | var templ_7745c5c3_Var21 string | |
| 431 | 431 | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short) | |
| 432 | 432 | if templ_7745c5c3_Err != nil { | |
| 433 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 96, Col: 33} | |
| 433 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 97, Col: 33} | |
| 434 | 434 | } | |
| 435 | 435 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) | |
| 436 | 436 | if templ_7745c5c3_Err != nil { | |
| @@ -468,7 +468,7 @@ func Header(l Layout) templ.Component { | |||
| 468 | 468 | var templ_7745c5c3_Var22 templ.SafeURL | |
| 469 | 469 | templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href)) | |
| 470 | 470 | if templ_7745c5c3_Err != nil { | |
| 471 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 127, Col: 36} | |
| 471 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 128, Col: 36} | |
| 472 | 472 | } | |
| 473 | 473 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) | |
| 474 | 474 | if templ_7745c5c3_Err != nil { | |
| @@ -481,7 +481,7 @@ func Header(l Layout) templ.Component { | |||
| 481 | 481 | var templ_7745c5c3_Var23 string | |
| 482 | 482 | templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | |
| 483 | 483 | if templ_7745c5c3_Err != nil { | |
| 484 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 128, Col: 24} | |
| 484 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 129, Col: 24} | |
| 485 | 485 | } | |
| 486 | 486 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) | |
| 487 | 487 | if templ_7745c5c3_Err != nil { | |
| @@ -494,7 +494,7 @@ func Header(l Layout) templ.Component { | |||
| 494 | 494 | var templ_7745c5c3_Var24 string | |
| 495 | 495 | templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | |
| 496 | 496 | if templ_7745c5c3_Err != nil { | |
| 497 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 129, Col: 33} | |
| 497 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 130, Col: 33} | |
| 498 | 498 | } | |
| 499 | 499 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24) | |
| 500 | 500 | if templ_7745c5c3_Err != nil { | |
| @@ -517,7 +517,7 @@ func Header(l Layout) templ.Component { | |||
| 517 | 517 | var templ_7745c5c3_Var25 string | |
| 518 | 518 | templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label) | |
| 519 | 519 | if templ_7745c5c3_Err != nil { | |
| 520 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 133, Col: 17} | |
| 520 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 134, Col: 17} | |
| 521 | 521 | } | |
| 522 | 522 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) | |
| 523 | 523 | if templ_7745c5c3_Err != nil { | |
| @@ -530,7 +530,7 @@ func Header(l Layout) templ.Component { | |||
| 530 | 530 | var templ_7745c5c3_Var26 string | |
| 531 | 531 | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key) | |
| 532 | 532 | if templ_7745c5c3_Err != nil { | |
| 533 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 133, Col: 50} | |
| 533 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 134, Col: 50} | |
| 534 | 534 | } | |
| 535 | 535 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) | |
| 536 | 536 | if templ_7745c5c3_Err != nil { | |
| @@ -584,7 +584,7 @@ func Footer(l Layout) templ.Component { | |||
| 584 | 584 | var templ_7745c5c3_Var28 string | |
| 585 | 585 | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version) | |
| 586 | 586 | if templ_7745c5c3_Err != nil { | |
| 587 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 146, Col: 26} | |
| 587 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 147, Col: 26} | |
| 588 | 588 | } | |
| 589 | 589 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) | |
| 590 | 590 | if templ_7745c5c3_Err != nil { | |
| @@ -597,7 +597,7 @@ func Footer(l Layout) templ.Component { | |||
| 597 | 597 | var templ_7745c5c3_Var29 templ.SafeURL | |
| 598 | 598 | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL)) | |
| 599 | 599 | if templ_7745c5c3_Err != nil { | |
| 600 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 147, Col: 38} | |
| 600 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 148, Col: 38} | |
| 601 | 601 | } | |
| 602 | 602 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | |
| 603 | 603 | if templ_7745c5c3_Err != nil { | |
| @@ -86,14 +86,14 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||||
| 86 | if templ_7745c5c3_Err != nil { | 86 | if templ_7745c5c3_Err != nil { | ||
| 87 | return templ_7745c5c3_Err | 87 | return templ_7745c5c3_Err | ||
| 88 | } | 88 | } | ||
| 89 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"><script src=\"") | 89 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"><meta name=\"htmx-config\" content='{\"history\":false,\"includeIndicatorCSS\":false,\"extensions\":\"sse, hx-live\"}'><script src=\"") |
| 90 | if templ_7745c5c3_Err != nil { | 90 | if templ_7745c5c3_Err != nil { | ||
| 91 | return templ_7745c5c3_Err | 91 | return templ_7745c5c3_Err | ||
| 92 | } | 92 | } | ||
| 93 | var templ_7745c5c3_Var6 string | 93 | var templ_7745c5c3_Var6 string | ||
| 94 | templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.HTMX) | 94 | templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.HTMX) | ||
| 95 | if templ_7745c5c3_Err != nil { | 95 | if templ_7745c5c3_Err != nil { | ||
| 96 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 19, Col: 30} | 96 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 20, Col: 30} |
| 97 | } | 97 | } | ||
| 98 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6) | 98 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6) | ||
| 99 | if templ_7745c5c3_Err != nil { | 99 | if templ_7745c5c3_Err != nil { | ||
| @@ -106,7 +106,7 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||||
| 106 | var templ_7745c5c3_Var7 string | 106 | var templ_7745c5c3_Var7 string | ||
| 107 | templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Live) | 107 | templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.Live) | ||
| 108 | if templ_7745c5c3_Err != nil { | 108 | if templ_7745c5c3_Err != nil { | ||
| 109 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 20, Col: 30} | 109 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 21, Col: 30} |
| 110 | } | 110 | } | ||
| 111 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7) | 111 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7) | ||
| 112 | if templ_7745c5c3_Err != nil { | 112 | if templ_7745c5c3_Err != nil { | ||
| @@ -119,7 +119,7 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||||
| 119 | var templ_7745c5c3_Var8 string | 119 | var templ_7745c5c3_Var8 string | ||
| 120 | templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.SSE) | 120 | templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.SSE) | ||
| 121 | if templ_7745c5c3_Err != nil { | 121 | if templ_7745c5c3_Err != nil { | ||
| 122 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 21, Col: 29} | 122 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 22, Col: 29} |
| 123 | } | 123 | } | ||
| 124 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8) | 124 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8) | ||
| 125 | if templ_7745c5c3_Err != nil { | 125 | if templ_7745c5c3_Err != nil { | ||
| @@ -132,7 +132,7 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||||
| 132 | var templ_7745c5c3_Var9 string | 132 | var templ_7745c5c3_Var9 string | ||
| 133 | templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.App) | 133 | templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.Assets.App) | ||
| 134 | if templ_7745c5c3_Err != nil { | 134 | if templ_7745c5c3_Err != nil { | ||
| 135 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 22, Col: 29} | 135 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 23, Col: 29} |
| 136 | } | 136 | } | ||
| 137 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9) | 137 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9) | ||
| 138 | if templ_7745c5c3_Err != nil { | 138 | if templ_7745c5c3_Err != nil { | ||
| @@ -145,7 +145,7 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||||
| 145 | var templ_7745c5c3_Var10 string | 145 | var templ_7745c5c3_Var10 string | ||
| 146 | templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath) | 146 | templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.RefPath) | ||
| 147 | if templ_7745c5c3_Err != nil { | 147 | if templ_7745c5c3_Err != nil { | ||
| 148 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 24, Col: 28} | 148 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 25, Col: 28} |
| 149 | } | 149 | } | ||
| 150 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10) | 150 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10) | ||
| 151 | if templ_7745c5c3_Err != nil { | 151 | if templ_7745c5c3_Err != nil { | ||
| @@ -158,7 +158,7 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||||
| 158 | var templ_7745c5c3_Var11 string | 158 | var templ_7745c5c3_Var11 string | ||
| 159 | templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA) | 159 | templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(l.SHA) | ||
| 160 | if templ_7745c5c3_Err != nil { | 160 | if templ_7745c5c3_Err != nil { | ||
| 161 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 24, Col: 47} | 161 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 25, Col: 47} |
| 162 | } | 162 | } | ||
| 163 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11) | 163 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11) | ||
| 164 | if templ_7745c5c3_Err != nil { | 164 | if templ_7745c5c3_Err != nil { | ||
| @@ -202,7 +202,7 @@ func Shell(l Layout, rail templ.Component) templ.Component { | |||||
| 202 | var templ_7745c5c3_Var14 string | 202 | var templ_7745c5c3_Var14 string | ||
| 203 | templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede) | 203 | templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(l.Lede) | ||
| 204 | if templ_7745c5c3_Err != nil { | 204 | if templ_7745c5c3_Err != nil { | ||
| 205 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 29, Col: 81} | 205 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 30, Col: 81} |
| 206 | } | 206 | } | ||
| 207 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) | 207 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) | ||
| 208 | if templ_7745c5c3_Err != nil { | 208 | if templ_7745c5c3_Err != nil { | ||
| @@ -305,7 +305,7 @@ func Header(l Layout) templ.Component { | |||||
| 305 | var templ_7745c5c3_Var16 string | 305 | var templ_7745c5c3_Var16 string | ||
| 306 | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(l.Instance) | 306 | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(l.Instance) | ||
| 307 | if templ_7745c5c3_Err != nil { | 307 | if templ_7745c5c3_Err != nil { | ||
| 308 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 57, Col: 41} | 308 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 58, Col: 41} |
| 309 | } | 309 | } | ||
| 310 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) | 310 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) | ||
| 311 | if templ_7745c5c3_Err != nil { | 311 | if templ_7745c5c3_Err != nil { | ||
| @@ -333,7 +333,7 @@ func Header(l Layout) templ.Component { | |||||
| 333 | var templ_7745c5c3_Var17 templ.SafeURL | 333 | var templ_7745c5c3_Var17 templ.SafeURL | ||
| 334 | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) | 334 | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) | ||
| 335 | if templ_7745c5c3_Err != nil { | 335 | if templ_7745c5c3_Err != nil { | ||
| 336 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 66, Col: 38} | 336 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 67, Col: 38} |
| 337 | } | 337 | } | ||
| 338 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) | 338 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) | ||
| 339 | if templ_7745c5c3_Err != nil { | 339 | if templ_7745c5c3_Err != nil { | ||
| @@ -356,7 +356,7 @@ func Header(l Layout) templ.Component { | |||||
| 356 | var templ_7745c5c3_Var18 string | 356 | var templ_7745c5c3_Var18 string | ||
| 357 | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) | 357 | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) | ||
| 358 | if templ_7745c5c3_Err != nil { | 358 | if templ_7745c5c3_Err != nil { | ||
| 359 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 70, Col: 19} | 359 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 71, Col: 19} |
| 360 | } | 360 | } | ||
| 361 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) | 361 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) | ||
| 362 | if templ_7745c5c3_Err != nil { | 362 | if templ_7745c5c3_Err != nil { | ||
| @@ -384,7 +384,7 @@ func Header(l Layout) templ.Component { | |||||
| 384 | var templ_7745c5c3_Var19 string | 384 | var templ_7745c5c3_Var19 string | ||
| 385 | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) | 385 | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label) | ||
| 386 | if templ_7745c5c3_Err != nil { | 386 | if templ_7745c5c3_Err != nil { | ||
| 387 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 76, Col: 19} | 387 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 77, Col: 19} |
| 388 | } | 388 | } | ||
| 389 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) | 389 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) | ||
| 390 | if templ_7745c5c3_Err != nil { | 390 | if templ_7745c5c3_Err != nil { | ||
| @@ -417,7 +417,7 @@ func Header(l Layout) templ.Component { | |||||
| 417 | var templ_7745c5c3_Var20 string | 417 | var templ_7745c5c3_Var20 string | ||
| 418 | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref) | 418 | templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(l.Ref) | ||
| 419 | if templ_7745c5c3_Err != nil { | 419 | if templ_7745c5c3_Err != nil { | ||
| 420 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 95, Col: 36} | 420 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 96, Col: 36} |
| 421 | } | 421 | } | ||
| 422 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) | 422 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) | ||
| 423 | if templ_7745c5c3_Err != nil { | 423 | if templ_7745c5c3_Err != nil { | ||
| @@ -430,7 +430,7 @@ func Header(l Layout) templ.Component { | |||||
| 430 | var templ_7745c5c3_Var21 string | 430 | var templ_7745c5c3_Var21 string | ||
| 431 | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short) | 431 | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(l.Short) | ||
| 432 | if templ_7745c5c3_Err != nil { | 432 | if templ_7745c5c3_Err != nil { | ||
| 433 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 96, Col: 33} | 433 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 97, Col: 33} |
| 434 | } | 434 | } | ||
| 435 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) | 435 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) | ||
| 436 | if templ_7745c5c3_Err != nil { | 436 | if templ_7745c5c3_Err != nil { | ||
| @@ -468,7 +468,7 @@ func Header(l Layout) templ.Component { | |||||
| 468 | var templ_7745c5c3_Var22 templ.SafeURL | 468 | var templ_7745c5c3_Var22 templ.SafeURL | ||
| 469 | templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href)) | 469 | templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(s.Href)) | ||
| 470 | if templ_7745c5c3_Err != nil { | 470 | if templ_7745c5c3_Err != nil { | ||
| 471 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 127, Col: 36} | 471 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 128, Col: 36} |
| 472 | } | 472 | } | ||
| 473 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) | 473 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) | ||
| 474 | if templ_7745c5c3_Err != nil { | 474 | if templ_7745c5c3_Err != nil { | ||
| @@ -481,7 +481,7 @@ func Header(l Layout) templ.Component { | |||||
| 481 | var templ_7745c5c3_Var23 string | 481 | var templ_7745c5c3_Var23 string | ||
| 482 | templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | 482 | templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | ||
| 483 | if templ_7745c5c3_Err != nil { | 483 | if templ_7745c5c3_Err != nil { | ||
| 484 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 128, Col: 24} | 484 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 129, Col: 24} |
| 485 | } | 485 | } | ||
| 486 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) | 486 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) | ||
| 487 | if templ_7745c5c3_Err != nil { | 487 | if templ_7745c5c3_Err != nil { | ||
| @@ -494,7 +494,7 @@ func Header(l Layout) templ.Component { | |||||
| 494 | var templ_7745c5c3_Var24 string | 494 | var templ_7745c5c3_Var24 string | ||
| 495 | templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | 495 | templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(s.Key) | ||
| 496 | if templ_7745c5c3_Err != nil { | 496 | if templ_7745c5c3_Err != nil { | ||
| 497 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 129, Col: 33} | 497 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 130, Col: 33} |
| 498 | } | 498 | } | ||
| 499 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24) | 499 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24) | ||
| 500 | if templ_7745c5c3_Err != nil { | 500 | if templ_7745c5c3_Err != nil { | ||
| @@ -517,7 +517,7 @@ func Header(l Layout) templ.Component { | |||||
| 517 | var templ_7745c5c3_Var25 string | 517 | var templ_7745c5c3_Var25 string | ||
| 518 | templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label) | 518 | templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label) | ||
| 519 | if templ_7745c5c3_Err != nil { | 519 | if templ_7745c5c3_Err != nil { | ||
| 520 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 133, Col: 17} | 520 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 134, Col: 17} |
| 521 | } | 521 | } | ||
| 522 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) | 522 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) | ||
| 523 | if templ_7745c5c3_Err != nil { | 523 | if templ_7745c5c3_Err != nil { | ||
| @@ -530,7 +530,7 @@ func Header(l Layout) templ.Component { | |||||
| 530 | var templ_7745c5c3_Var26 string | 530 | var templ_7745c5c3_Var26 string | ||
| 531 | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key) | 531 | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(s.Key) | ||
| 532 | if templ_7745c5c3_Err != nil { | 532 | if templ_7745c5c3_Err != nil { | ||
| 533 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 133, Col: 50} | 533 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 134, Col: 50} |
| 534 | } | 534 | } | ||
| 535 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) | 535 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) | ||
| 536 | if templ_7745c5c3_Err != nil { | 536 | if templ_7745c5c3_Err != nil { | ||
| @@ -584,7 +584,7 @@ func Footer(l Layout) templ.Component { | |||||
| 584 | var templ_7745c5c3_Var28 string | 584 | var templ_7745c5c3_Var28 string | ||
| 585 | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version) | 585 | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version) | ||
| 586 | if templ_7745c5c3_Err != nil { | 586 | if templ_7745c5c3_Err != nil { | ||
| 587 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 146, Col: 26} | 587 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 147, Col: 26} |
| 588 | } | 588 | } | ||
| 589 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) | 589 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) | ||
| 590 | if templ_7745c5c3_Err != nil { | 590 | if templ_7745c5c3_Err != nil { | ||
| @@ -597,7 +597,7 @@ func Footer(l Layout) templ.Component { | |||||
| 597 | var templ_7745c5c3_Var29 templ.SafeURL | 597 | var templ_7745c5c3_Var29 templ.SafeURL | ||
| 598 | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL)) | 598 | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL)) | ||
| 599 | if templ_7745c5c3_Err != nil { | 599 | if templ_7745c5c3_Err != nil { | ||
| 600 | - | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 147, Col: 38} | 600 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 148, Col: 38} |
| 601 | } | 601 | } | ||
| 602 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | 602 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | ||
| 603 | if templ_7745c5c3_Err != nil { | 603 | if templ_7745c5c3_Err != nil { | ||