Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions keel/web/static/css/keel.css
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,26 @@ code,
.stopped p { margin: 0 0 0.7rem; }
.stopped .detail { color: var(--muted); font-size: 0.88rem; }

/* How to get in (#634 follow-up): the command that RECOVERS the address, set apart from the
* detail prose rather than appended to it. The paragraph beside it runs five sentences, and a
* command added as a sixth is a command nobody sees -- which is how two people who knew this
* codebase were locked out and recovered by other means. Boxed, monospaced and selectable,
* because its whole purpose is to be copied into a terminal. */
.how-to-get-in { margin: 0 0 1rem; }
.how-to-get-in code {
display: block;
margin: 0.35rem 0 0.6rem;
padding: 0.5rem 0.7rem;
border: 1px solid var(--control-line);
border-radius: 4px;
background: var(--code-bg, rgba(127, 127, 127, 0.12));
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.95rem;
/* The one string on this page meant to leave it. */
user-select: all;
overflow-wrap: anywhere;
}

/* The reconnect field (#634): the one control on a refused page, so it is laid out as a row that
* wraps rather than a form, and its border is `--control-line` -- the variable that exists
* because WCAG 1.4.11 wants 3:1 on a control boundary and `--line` is decorative only. */
Expand Down
52 changes: 51 additions & 1 deletion keel/web/static/js/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,27 @@ export function stoppedView(reading, setupHref) {
return fragment;
}

/**
* The `keel open` invocation that recovers THIS console's address.
*
* It returns a NODE and joins nothing, because this file may use neither template literals nor
* `+` -- `test_render_contains_no_arithmetic` bans the operator outright rather than try to tell
* string concatenation from money, and that bluntness is the point of the gate. `append` takes
* several strings, so the port is placed beside the command instead of being joined to it.
*
* A port the browser cannot report -- the default 80/443, where `location.port` is the empty
* string -- yields the bare command, which is still correct: `keel open` has its own default.
*
* @param {string} port `window.location.port`, empty when the URL carries no explicit port.
* @returns {HTMLElement}
*/
function openCommandNode(port) {
const code = el("code");
if (port) code.append("keel open --port ", port);
else code.append("keel open");
return code;
}

/**
* The "keel refused this browser" view (#634).
*
Expand Down Expand Up @@ -548,8 +569,37 @@ export function refusedView(reading, onReconnect) {
card.append(el("p", "detail", reading.error.detail || reading.error.title));
}

// ── HOW TO GET THE VALUE THIS FIELD WANTS ──────────────────────────────────────────────
// Added because the view told an operator what to paste and never where to get it. The
// refusal's own prose says the token "is never written to disk", which since #756 is only
// true of a server attached to a TERMINAL: run detached -- launchd, a pipe, a container --
// `runtime.py` records the address in a 0600 file precisely so `keel open` can hand it back.
// A reader who believes the sentence concludes the token is unrecoverable and restarts the
// server, which mints a NEW one and is the single worst move available. The command is the
// remedy, and it was named in `server.py`, in the launchd plist and in `runtime.py`'s own
// docstring -- everywhere except the screen the locked-out operator is looking at.
//
// The PORT is taken from `window.location`, the same source and the same reason as
// `timelineExportUrl` and the deployment card: it is a fact about where this page is, not a
// claim from a payload that just refused us. Nothing else about the deployment is named --
// the path in particular -- because the port is the one thing the reader demonstrably
// already knows (they reached this page on it), so naming it discloses nothing to a request
// that has NOT been admitted. That matters more once #648 lets this console answer beyond
// loopback.
const how = el("div", "detail how-to-get-in");
how.append(el("p", undefined, "To get this run's address, from your keel deployment directory:"));
how.append(openCommandNode(window.location.port));
const note = el("p");
note.append(
"It opens the console, and prints the address so you can paste it below. ",
"Attached to a terminal keel prints the address and keeps nothing; run as a service it ",
"records the address for that command and deletes it on shutdown.",
);
how.append(note);
card.append(how);

const form = el("form");
const label = el("label", undefined, "Paste the address keel printed, or just its token");
const label = el("label", undefined, "Paste the address from that command, or just its token");
label.setAttribute("for", "reconnect-address");
const input = el("input");
input.setAttribute("id", "reconnect-address");
Expand Down
95 changes: 95 additions & 0 deletions tests/web/test_refused_view_instructions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""The refusal must say how to GET the value it asks you to paste (#634 follow-up).

The evidence this exists for: on 2026-09-16 two people who know this codebase were locked out of
the console by a daemon restart, and neither recovered from the view. One went to `keel open
--help`; the other asked a colleague. The view named the cause well and the remedy not at all.

Worse than an omission, the refusal's prose says the token "is never written to disk". Since #756
that is true only of a server attached to a TERMINAL -- run detached, `keel/web/runtime.py`
records the address in a `0600` file precisely so `keel open` can hand it back. A reader who
believes the sentence concludes the token is unrecoverable and restarts the server, which mints a
NEW token: the single worst move available, and the one `refusedView`'s own docstring already
warns about ("sends them to restart the thing that is working").

`keel open` was named in `server.py`, in the launchd plist, and in `runtime.py`'s own module
docstring -- everywhere except the screen the locked-out operator is looking at.
"""

from __future__ import annotations

from tests.web.test_client_assets import _comments_only
from tests.web.test_view_sections import _JS, _decl

RENDER = (_JS / "render.js").read_text(encoding="utf-8")


def _refused() -> str:
"""`refusedView`'s body with comments stripped -- prose must not satisfy these assertions."""
return _comments_only(_decl(RENDER, "refusedView"))


def test_the_refusal_names_the_command_that_recovers_the_address() -> None:
"""The remedy, on the screen that needs it.

Rejects the mutation that started this: a view that tells an operator what to paste and never
where to get it. Asserted on the CODE with comments stripped, so the prose above the function
explaining why the command belongs here cannot stand in for the command.
"""
body = _refused()
assert "openCommandNode" in body, (
"refusedView no longer builds the `keel open` command -- the view is back to asking for "
"a token it never says how to obtain"
)
assert body.count("openCommandNode") == 1


def test_the_command_carries_this_console_s_own_port() -> None:
"""A deployment runs several consoles (four, on the machine this was found on), so a command
without a port sends the operator to whichever one `keel open` defaults to -- a DIFFERENT
console, answering happily, which reads as the command having failed.

`window.location` is the source deliberately: the port is a fact about where this page is,
not a claim from a payload that just refused us. Rejects the mutation that hardcodes a port
or drops the argument.
"""
body = _refused()
assert "openCommandNode(window.location.port)" in body


def test_the_command_is_a_code_element_not_a_sentence() -> None:
"""Placed to be copied, not read past.

The detail paragraph it sits beside is five sentences long; a command appended as a sixth is
a command nobody sees. Rejects the mutation that inlines the text into the surrounding `<p>`.
"""
command = _comments_only(_decl(RENDER, "openCommandNode"))
assert 'el("code")' in command
assert "keel open --port " in command
assert "keel open" in command


def test_the_command_survives_a_console_on_a_default_port() -> None:
"""`location.port` is the EMPTY STRING on 80/443, so a naive build emits `keel open --port `
with a dangling flag that the CLI rejects. The bare command is correct there -- `keel open`
carries its own default -- and the branch is what keeps it that way."""
command = _comments_only(_decl(RENDER, "openCommandNode"))
assert "if (port)" in command
assert "else" in command


def test_the_paste_field_points_at_the_command_not_at_a_past_printing() -> None:
""" "Paste the address keel printed" is past tense about something that may be long gone --
a daemon printed it to a log the plist says explicitly not to grep. The label must point at
something the operator can make happen NOW."""
body = _refused()
assert "Paste the address from that command" in body
assert "Paste the address keel printed" not in body, (
"the label is back to naming a printing the operator may have no way to reach"
)


def test_the_instructions_precede_the_field_they_serve() -> None:
"""Order is the whole affordance: the command PRODUCES the value the field consumes, so a
reader who meets the field first has already been asked for something they do not have."""
body = _refused()
assert body.index("openCommandNode") < body.index('el("form")')
Loading