jevhooks.git / web / src / content.rs

What the page says and shows, as data: the words a link preview carries, the four pictures, and the addresses it links to. No markup here; view draws it.

4use aldebaran_assets::{Asset, Bundle};
5use jev_ui::components::Icon;
7pub const NAME: &str = "jevhooks";
8pub const TITLE: &str = "jevhooks: Jev judges what Claude Code is about to do";
9pub const DESCRIPTION: &str = "A Claude Code plugin. Before a shell command runs, Jev says what kind of act it is, how hard it \
10     would be to undo and how much memory it needs. Ordinary work runs without a prompt, risky work is put to you, and \
11     heavy commands wait their turn.";

Where the site is when nothing says otherwise (wrangler dev). The deploy passes the real one.

14pub const SITE_ORIGIN: &str = "http://localhost:8790";

The guide: every folder of the repository as a chapter, on the code host.

17pub const GUIDE: &str = "https://code.lmjtfy.fun/jevhooks.git/";
18pub const CHAPTER_HOOKS: &str = "https://code.lmjtfy.fun/jevhooks.git/plugin/hooks/";
19pub const CHAPTER_QUESTIONS: &str = "https://code.lmjtfy.fun/jevhooks.git/crates/jevhooks-rules/src/";
20pub const CHAPTER_DAEMON: &str = "https://code.lmjtfy.fun/jevhooks.git/crates/jevhooks-daemon/";
21pub const JEV_DOCS: &str = "https://docs.typesafe.ai";
22pub const LMJTFY: &str = "https://lmjtfy.fun";

What a stranger types to see it work, with no Rust, no key and no account.

25pub const TRY_IT: &str = "git clone --recurse-submodules https://lmjtfy.fun/jevhooks.git && cd jevhooks && claude plugin test plugin";

The release the page tells people to install: the workspace's version (tools/check-versions.sh holds this to the root Cargo.toml).

29pub const VERSION: &str = "0.1.0";

The public half of the key releases are signed with: the second line of tools/release.pub, the one copy of it. Whoever installs gives it to mise, which then accepts only what it signed.

33pub fn release_key() -> &'static str {
34    include_str!("../../tools/release.pub").lines().nth(1).unwrap_or_default().trim()
35}

The name releases are signed under, which is where they are served.

38pub const PROJECT: &str = "code.lmjtfy.fun/jevhooks";

Installs the plugin, daemon built in, with mise checking the signature.

41pub fn install() -> String {
42    format!("mise use -g 'packslip:{PROJECT}[pubkey={},allow_unlogged=true]@{VERSION}'", release_key())
43}

Starts a session with the installed plugin loaded.

46pub const RUN_IT: &str = "claude --plugin-dir \"$(mise where packslip:code.lmjtfy.fun/jevhooks)\"";

The same files for someone without mise: the archive for this machine and the digests to check it by.

49pub const BY_HAND: &str = "https://code.lmjtfy.fun/jevhooks/latest/";

The link preview's card: drawn by src/bin/card.rs, made a PNG by tools/build-web.sh.

52pub const CARD_PATH: &str = "/preview/card.png";
53pub const CARD_WIDTH: u32 = 1200;
54pub const CARD_HEIGHT: u32 = 630;
55pub const CARD_ALT: &str = "A window on a dotted background that says: Jev judges what Claude Code is about to do";

A hook, 12 by 12, # for ink: the title bars' and the tab's icon.

58pub const HOOK: Icon = [
59    "............", "....####....", "...##..##...", "...##..##...", ".......##...", ".......##...", ".......##...", "..##...##...",
60    "..##...##...", "...#####....", "............", "............",
61];

One picture of the real terminal, and what it shows.

64#[derive(Clone, Copy)]
65pub struct Shot {
66    pub asset: Asset,

The window's title.

68    pub title: &'static str,

For someone who cannot see the picture: what is on the screen.

70    pub alt: &'static str,

One or two plain sentences beside it.

72    pub says: &'static str,

The picture's own size, so the page does not jump as it loads.

74    pub width: u32,
75    pub height: u32,
76}
78const SVG: &str = "image/svg+xml";

The pictures are the guide's own: mise run screenshots takes them from a real session and writes them beside the chapters that show them. They are embedded from there, so the site cannot show a different picture from the guide.

83pub const BAND: Shot = Shot {
84    asset: Asset { path: "/shots/band.svg", content_type: SVG, body: include_bytes!("../../plugin/hooks/band.svg") },
85    title: "Ordinary work just runs",
86    alt: "Claude Code's terminal after a command ran with no prompt. Above the prompt, a line reads: command: Jev: builds or tests; undone by one command; allowed.",
87    says: "Reading, building and editing run without a prompt. A band above the prompt says what Jev made of the last \
88           command, how long it took and what the session's questions have cost.",
89    width: 1048,
90    height: 268,
91};
93pub const ASK: Shot = Shot {
94    asset: Asset { path: "/shots/ask.svg", content_type: SVG, body: include_bytes!("../../plugin/hooks/ask.svg") },
95    title: "Anything hard to undo is put to you",
96    alt: "Claude Code's permission dialog for git reset --hard and git clean. It carries Jev's reason: discards or rewrites version-control state; hard to undo.",
97    says: "A command that deletes, rewrites history, changes the machine, sends something elsewhere or runs code nobody \
98           has read stops at a prompt, with Jev's reason in it. You decide.",
99    width: 1048,
100    height: 436,
101};
102
103pub const WAITING: Shot = Shot {
104    asset: Asset { path: "/shots/waiting.svg", content_type: SVG, body: include_bytes!("../../crates/jevhooks-daemon/src/waiting.svg") },
105    title: "Heavy commands wait their turn",
106    alt: "Claude Code's terminal while cargo build --release waits. The band reads: waiting for memory: needs 3.0 GB, 1.0 GB to spare.",
107    says: "Every session on the machine shares one ledger of the memory already promised to commands. Five agents \
108           starting five builds in the same second get two builds now and three in a queue, where they used to get a \
109           dead machine.",
110    width: 1048,
111    height: 268,
112};
113
114pub const MODEL: Shot = Shot {
115    asset: Asset { path: "/shots/model.svg", content_type: SVG, body: include_bytes!("../../crates/jevhooks-daemon/src/model.svg") },
116    title: "A prompt goes to the cheapest model that will do",
117    alt: "Claude Code's terminal after the prompt: What branch am I on? The band reads: prompt: Jev: a lookup; haiku.",
118    says: "Switch it on and Jev scores what each prompt takes, on four levels from a lookup to open-ended work. The turn \
119           goes to the cheapest of haiku, sonnet, opus and fable that is 75% likely to be enough.",
120    width: 1048,
121    height: 218,
122};

In the order the page shows them.

125pub const SHOTS: [Shot; 4] = [BAND, ASK, WAITING, MODEL];

Every file the site serves from itself besides the shared look's.

128pub static FILES: Bundle = Bundle::new(&[BAND.asset, ASK.asset, WAITING.asset, MODEL.asset]);
130#[cfg(test)]
131mod tests {
132    use super::*;

A picture's declared size is what keeps the page from jumping as it loads, so it has to be the picture's real one: read back from the SVG the screenshots task wrote.

136    #[test]
137    fn each_picture_declares_its_real_size() {
138        for shot in SHOTS {
139            let svg = std::str::from_utf8(shot.asset.body).unwrap();
140            let number = |name: &str| -> f64 {
141                let at = svg.find(&format!("{name}=\"")).unwrap_or_else(|| panic!("{}: no {name}", shot.asset.path)) + name.len() + 2;
142                svg[at..].split('"').next().unwrap().parse().unwrap()
143            };
144            assert_eq!((number("width").round() as u32, number("height").round() as u32), (shot.width, shot.height), "{}", shot.asset.path);
145        }
146    }

The screenshots task drops the rows Claude Code draws about the account of whoever ran it. This is the last place one could get out, so it is checked here too.

150    #[test]
151    fn no_picture_shows_the_account_of_whoever_took_it() {
152        for shot in SHOTS {
153            let svg = std::str::from_utf8(shot.asset.body).unwrap();
154            for private in ["weekly limit", "usage-credits", "/home/", "/Users/"] {
155                assert!(!svg.contains(private), "{} contains {private:?}", shot.asset.path);
156            }
157        }
158    }

Link previews cut long text; ours must arrive whole (jev_ui::preview refuses longer).

161    #[test]
162    fn the_preview_words_fit() {
163        assert!(TITLE.chars().count() <= 70, "{}", TITLE.chars().count());
164        assert!(DESCRIPTION.chars().count() <= 300, "{}", DESCRIPTION.chars().count());
165    }
166}