hush/internal/web/templates/base.html
jx12n d7cd57f330
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
serve the MCP install instructions at /mcp
Using hush from an agent needed a clone and docs/MCP.md. It now needs one
command, and the instructions are served by the deployment itself.

`go install github.com/orchard9/hush/cmd/hush-mcp@latest` is the whole
install: cmd/hush-mcp imports only the standard library, so module graph
pruning never reaches the private go-chassis dependency cmd/hushd needs.
Verified against an empty module cache and the public proxy, then create ->
reveal end to end against production with the resulting binary.

The page carries the per-client configuration for Claude Code, Codex CLI,
Gemini CLI, VS Code, Claude Desktop, Cursor and omp. Each command was run
against the installed client rather than copied from documentation, which is
how the differences on it are there at all: VS Code's wrapper key is
`servers`, not `mcpServers`; gemini defaults to project scope, not user;
Claude Code rejects `--env` immediately before the server name.

The shared browser crypto moves from base.html into templates/crypto.html,
which the two pages that encrypt parse and this one does not. An empty
`{{define}}` cannot replace a non-empty one — text/template reads an empty
body as no definition — so the shell holds the call and the partial holds the
code, and the docs page ships no script at all.

Three things this exposed, fixed here:

- The public Ingress enumerates paths, so a handler without one 404s at the
  edge while working in `make dev`. The Ingress is now its own manifest:
  hush.yaml pins a `:bootstrap` image that does not exist, so re-applying it
  to publish a path would roll the workload onto an unpullable image.
  `make deploy-ingress` applies the route alone.
- release.sh guarded HEAD against `@{upstream}`, which is the GitHub mirror
  here, while Kaniko clones Gitea. A commit pushed to one and not the other
  would have built the previous commit silently. It now fetches and compares
  the branch that actually gets built.
- smoke.sh checks that /mcp serves the install command, so a stale rollout or
  an unexecutable template fails the release instead of being found later.
  Confirmed it fails: against production before this deploy it reported 404.
2026-09-05 14:03:34 -06:00

107 lines
4.5 KiB
HTML

{{define "base.html"}}<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>hush</title>
<meta name="color-scheme" content="dark">
<meta name="referrer" content="no-referrer">
<!-- The policy is a HEADER, set in internal/web/web.go, and every inline block
below carries that response's nonce. There is deliberately no CSP <meta>
here: two policies on one response intersect, so a <meta> cannot loosen the
header, and it silently made this page's own script look permitted while
the browser blocked it. No external origins either — no CDN, no font host,
no analytics: a third-party script on this page could read the key out of
the fragment. -->
<style nonce="{{.Nonce}}">
:root{
color-scheme:dark;
--bg:#08090a; /* page */
--panel:#0e1012; /* the one card */
--inset:#0a0b0c; /* fields and output, recessed from the card */
--line:#1c1e22; /* hairline */
--line-lit:#2b2f35; /* hairline, hovered or focused */
--fg:#f1f2f4;
--dim:#8d939d; /* supporting copy */
--faint:#5d626b; /* placeholder, footer */
--accent:#8ce0b8; /* used sparingly: the mark, focus, success */
--warn:#f2c078;
--bad:#ff8189;
--r:12px;
--mono:ui-monospace,SFMono-Regular,Menlo,monospace;
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
margin:0;background:var(--bg);color:var(--fg);
font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;
font-feature-settings:"kern" 1;-webkit-font-smoothing:antialiased;
display:flex;min-height:100vh;align-items:center;justify-content:center;padding:24px;
}
main{width:100%;max-width:440px}
.card{
background:var(--panel);border:1px solid var(--line);border-radius:var(--r);
padding:28px 26px;box-shadow:0 30px 60px -40px #000,0 1px 0 #ffffff05 inset;
}
h1{font-size:21px;font-weight:600;letter-spacing:-.02em;margin:0 0 6px}
h1 span{color:var(--accent)}
p.lede{color:var(--dim);margin:0 0 22px;font-size:13.5px}
textarea,button{font:inherit;width:100%;border-radius:10px}
textarea{
color:var(--fg);background:var(--inset);border:1px solid var(--line);
font:13px/1.55 var(--mono);padding:13px 14px;min-height:132px;resize:vertical;
transition:border-color .12s,box-shadow .12s;
}
textarea::placeholder{color:var(--faint)}
textarea:hover{border-color:var(--line-lit)}
textarea:focus,button:focus-visible{outline:0;border-color:var(--line-lit);box-shadow:0 0 0 3px #8ce0b81f}
button{
margin-top:14px;padding:11px 16px;border:1px solid transparent;
background:var(--fg);color:#08090a;font-weight:600;font-size:14px;
cursor:pointer;transition:background .12s,border-color .12s,color .12s;
}
button:hover:not(:disabled){background:#fff}
button:active:not(:disabled){transform:translateY(.5px)}
/* A disabled solid button is a grey slab that still dominates the card and
reads as "loading" rather than "not now". It recedes instead. */
button:disabled{background:none;color:var(--faint);border-color:var(--line);cursor:not-allowed}
button.ghost{background:none;color:var(--dim);border-color:var(--line);font-weight:500}
button.ghost:hover{background:none;color:var(--fg);border-color:var(--line-lit)}
.row{display:flex;gap:10px}
.row>*{flex:1}
.out{
background:var(--inset);border:1px solid var(--line);border-radius:10px;
padding:13px 14px;font:13px/1.6 var(--mono);word-break:break-all;
white-space:pre-wrap;user-select:all;
}
.note{color:var(--dim);font-size:12.5px;line-height:1.5;margin:12px 0 0}
.note.err{color:var(--bad)}
.note.warn{color:var(--warn)}
.hide{display:none}
footer{margin:14px 2px 0;color:var(--faint);font-size:11.5px;line-height:1.5}
footer code{font:11px var(--mono);color:var(--dim)}
footer a{color:var(--dim);text-decoration:none;border-bottom:1px solid var(--line)}
footer a:hover{color:var(--fg);border-color:var(--line-lit)}
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
</style>
<!-- Page-specific CSS, nonced by the page that defines it. Empty for the two
product pages, which need nothing beyond the shell above. -->
{{block "styles" .}}{{end}}
</head>
<body>
<main>
<div class="card">
{{template "content" .}}
</div>
<footer>
Encrypted in your browser. The key travels in the link's <code>#fragment</code>,
which browsers never send to a server.{{block "nav" .}}{{end}}
</footer>
</main>
<!-- The pages that encrypt pull in templates/crypto.html, which defines this.
The MCP page defines it empty: it runs no script at all. -->
{{template "crypto" .}}
{{template "script" .}}
</body>
</html>{{end}}