Files
skills/orchestrator/templates/page-sky.js
T
Kevin Veen-BirkenbachandClaude Opus 5.5 c035373518 feat(orchestrator)!: S0 English everywhere, Sécurité, NATO hashes, ICAO digits
Requirement 001 phase 6, S0:

- Speech: the security class shows as "Sécurité" and is spoken as in
  maritime radio, "say-cure-ee-tay" three times. A hex run of 7 or more
  characters with a letter and a digit (commit, session, message ids) is
  spelled in the NATO alphabet; every other digit is spoken on its own
  in ICAO pronunciation (42305: four two tree zero fife). The spelling
  runs inside clip() before the length cut, so a phrase still fits
  SPEECH_LIMIT; the displayed text keeps its digits and hashes.
- English: every orchestrator label, tooltip, message, error, script
  output, document and test string is English (Regeln -> Firewall,
  Hierarchie -> Hierarchy, Steuerung -> Controls, Ausführen -> Run, ...);
  the page declares lang="en" and formats dates as en-GB. Task filter
  ids are English. German stays only in the skill trigger phrases that
  quote the operator, and as identifiers: the view keys (view=) and the
  leitstand id.
- tests/test_english.py scans every tracked text file for umlauts and
  German words, with an allowlist of the quoted trigger phrases.

BREAKING CHANGE: the Stream search tokens art: and seit: are now kind:
and since:; an old art: or seit: token counts as a plain word.

Evidence: make test 657 OK, ruff and markdownlint clean
(/tmp/s0-make-test-1.log); e2e 17 OK, none skipped (/tmp/s0-e2e-1.log);
MEDUSA 86 files, 24 findings on reworded strings accepted after an
independent skeptic (21 path:line, 0 rejected, no behaviour change in
signing, approving, rules, denying, offices, events), rescan 0 open.

🎯 Fix: 93 %
💥 Break: 4 %
💸 Cost: 100 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 89
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:26:51 +00:00

207 lines
9.7 KiB
JavaScript

const skyMoment = () => (live() ? Date.now() : (ui.asOf === null ? state.until : ui.asOf) * 1000);
function applyTheme(sun) {
const custom = settings.theme === "custom";
const mode = custom ? Ambience.modeOf(settings.colors.top, settings.colors.bottom) : settings.theme === "sky" ? Ambience.modeAt(sun.alt) : settings.theme;
const glass = Boolean(window.CSS && CSS.supports("(backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))")) && !window.matchMedia("(prefers-reduced-transparency: reduce)").matches;
const key = [mode, custom ? settings.colors.accent : "", glass].join("|");
if (key === sky.mode) {
return;
}
sky.mode = key;
const root = document.documentElement;
const tokens = Ambience.THEMES[mode];
for (const [name, value] of Object.entries(tokens)) {
root.style.setProperty(`--${name}`, value);
}
if (custom) {
root.style.setProperty("--accent", settings.colors.accent);
root.style.setProperty("--accent-soft", Ambience.css(Ambience.mix(Ambience.hex(settings.colors.accent), Ambience.hex(tokens.panel), 0.84)));
}
root.style.setProperty("--veil", `${glass ? Ambience.GLASS * 100 : 100}%`);
root.style.setProperty("--card", `${glass ? Ambience.CARD * 100 : 100}%`);
root.style.colorScheme = mode;
root.dataset.mode = mode;
refresh3d();
}
function drawMoon(paint, at, radius, moon) {
const tilt = (moon.tilt * Math.PI) / 180;
paint.save();
paint.translate(at.x, at.y);
paint.rotate(Math.atan2(-Math.cos(tilt), -Math.sin(tilt)));
const halo = paint.createRadialGradient(0, 0, radius, 0, 0, radius * 4);
halo.addColorStop(0, `rgb(235 240 255 / ${(0.28 * moon.lit).toFixed(3)})`);
halo.addColorStop(1, "rgb(235 240 255 / 0)");
paint.fillStyle = halo;
paint.fillRect(-radius * 4, -radius * 4, radius * 8, radius * 8);
paint.fillStyle = "rgb(60 68 92 / 0.55)";
paint.beginPath();
paint.arc(0, 0, radius, 0, Math.PI * 2);
paint.fill();
paint.fillStyle = "#f3f0e4";
paint.beginPath();
paint.arc(0, 0, radius, -Math.PI / 2, Math.PI / 2, false);
paint.ellipse(0, 0, Math.abs(2 * moon.lit - 1) * radius, radius, 0, Math.PI / 2, -Math.PI / 2, moon.lit < 0.5);
paint.closePath();
paint.fill();
paint.restore();
}
function drawSky() {
const ms = skyMoment();
const sun = Ambience.sun(ms, settings.latitude, settings.longitude);
const moon = Ambience.moon(ms, settings.latitude, settings.longitude);
applyTheme(sun);
const view = seenView("sky");
const facing = COMPASS[Space.compass(view.azimuth)];
const turned = ["azimuth", "altitude", "fov"].some((key) => Math.abs(view[key] - Space.DEFAULTS.sky[key]) >= 0.5);
const moonText = moon.alt > 0 ? ` · Moon ${Math.round(moon.lit * 100)} % ${moon.waxing ? "waxing" : "waning"}` : "";
document.getElementById("sky-note").textContent = `${sun.alt > 0 ? "☀" : "☾"} ${STAGE_LABELS[Ambience.stage(sun.alt)]}${moonText}${turned ? ` · facing ${facing[0]}` : ""}`;
if (space.look) {
const words = `Facing ${facing[1]} · ${((Math.round(view.azimuth) % 360) + 360) % 360}° · altitude ${Math.round(view.altitude)}° · field of view ${Math.round(view.fov)}°`;
space.compass.textContent = words;
sky.canvas.setAttribute("aria-label", `Sky, ${words}. Arrow keys turn the view, plus and minus zoom, 0 resets, Esc ends.`);
}
const canvas = sky.canvas;
const paint = canvas.getContext("2d");
if (!paint) {
return;
}
const ratio = Math.min(window.devicePixelRatio || 1, 2);
const width = window.innerWidth;
const height = window.innerHeight;
if (canvas.width !== Math.round(width * ratio) || canvas.height !== Math.round(height * ratio)) {
canvas.width = Math.round(width * ratio);
canvas.height = Math.round(height * ratio);
}
paint.setTransform(ratio, 0, 0, ratio, 0, 0);
const sight = Space.lens(view, width, height);
const place = (alt, az) => Space.place(sight, alt, az);
const inside = (at, margin) => at.x >= -margin && at.x <= width + margin && at.y >= -margin && at.y <= height + margin;
const seen = (body, at) => body.alt > -2 && Math.abs(at.lon) <= view.fov / 2 + 10 && inside(at, 100);
const colors = settings.theme === "custom" ? {top: Ambience.hex(settings.colors.top), bottom: Ambience.hex(settings.colors.bottom)} : Ambience.skyAt(sun.alt);
const low = place(0, view.azimuth);
const high = place(90, view.azimuth);
const air = paint.createLinearGradient(low.x + (high.x - low.x) / Space.REACH, low.y + (high.y - low.y) / Space.REACH, low.x, low.y);
air.addColorStop(0, Ambience.css(colors.top));
air.addColorStop(1, Ambience.css(colors.bottom));
paint.fillStyle = air;
paint.fillRect(0, 0, width, height);
const dusk = Math.max(0, 1 - Math.abs(sun.alt) / 14);
const glowAt = place(Math.max(sun.alt, -3), sun.az);
if (dusk > 0 && Math.abs(glowAt.lon) <= 135) {
const x = Math.min(Math.max(glowAt.x, sight.cx - sight.kx * 100), sight.cx + sight.kx * 100);
const glow = paint.createRadialGradient(x, glowAt.y, 0, x, glowAt.y, width * 0.5);
glow.addColorStop(0, `rgb(255 186 128 / ${(0.45 * dusk).toFixed(3)})`);
glow.addColorStop(1, "rgb(255 186 128 / 0)");
paint.fillStyle = glow;
paint.fillRect(0, 0, width, height);
}
const light = Ambience.starlight(sun.alt);
if (light > 0) {
const spin = Ambience.sidereal(ms) + settings.longitude;
paint.fillStyle = "#ffffff";
for (const star of sky.stars) {
const spot = Ambience.horizontal(spin - star.ra, star.dec, settings.latitude);
const at = spot.alt > 1 ? place(spot.alt, spot.az) : null;
if (at && Math.abs(at.lon) <= view.fov / 2 && inside(at, 4)) {
paint.globalAlpha = light * (0.25 + 0.75 * star.glow);
paint.beginPath();
paint.arc(at.x, at.y, 0.5 + star.glow * 1.2, 0, Math.PI * 2);
paint.fill();
}
}
paint.globalAlpha = 1;
}
const sunAt = place(sun.alt, sun.az);
if (seen(sun, sunAt)) {
const at = sunAt;
const halo = paint.createRadialGradient(at.x, at.y, 0, at.x, at.y, 90);
halo.addColorStop(0, "rgb(255 236 200 / 0.55)");
halo.addColorStop(1, "rgb(255 236 200 / 0)");
paint.fillStyle = halo;
paint.fillRect(at.x - 90, at.y - 90, 180, 180);
paint.fillStyle = Ambience.css(Ambience.mix([255, 170, 90], [255, 246, 220], Math.min(Math.max(sun.alt / 20, 0), 1)));
paint.beginPath();
paint.arc(at.x, at.y, 15, 0, Math.PI * 2);
paint.fill();
}
const moonAt = place(moon.alt, moon.az);
if (seen(moon, moonAt)) {
drawMoon(paint, moonAt, 12, moon);
}
const edge = Space.horizon(sight, 96);
const bottom = Math.max(height, ...edge.map((at) => at.y)) + 2;
const trace = (lift) => edge.forEach((at, index) => (index ? paint.lineTo(at.x, at.y + lift) : paint.moveTo(at.x, at.y + lift)));
const ground = paint.createLinearGradient(0, low.y, 0, low.y + height - sight.cy);
ground.addColorStop(0, Ambience.css(Ambience.mix(colors.bottom, [4, 6, 10], 0.72)));
ground.addColorStop(1, Ambience.css(Ambience.mix(colors.bottom, [4, 6, 10], 0.9)));
paint.fillStyle = ground;
paint.beginPath();
trace(0);
paint.lineTo(edge[edge.length - 1].x, bottom);
paint.lineTo(edge[0].x, bottom);
paint.closePath();
paint.fill();
paint.strokeStyle = "rgb(255 255 255 / 0.18)";
paint.lineWidth = 1;
paint.beginPath();
trace(0.5);
paint.stroke();
paint.fillStyle = "rgb(255 255 255 / 0.5)";
paint.font = "600 11px system-ui, sans-serif";
paint.textAlign = "center";
COMPASS.forEach(([label], index) => {
const at = place(0, index * 45);
if (Math.abs(at.lon) <= view.fov / 2 + 0.5) {
paint.fillText(label, Math.min(Math.max(at.x, 14), width - 14), at.y + 16);
}
});
if (space.glides.sky) {
redrawSky();
}
}
function redrawSky() {
window.cancelAnimationFrame(sky.frame);
sky.frame = window.requestAnimationFrame(drawSky);
}
function skyLook(on) {
const canvas = sky.canvas;
space.look = on;
document.body.classList.toggle("sky-look", on);
document.getElementById("sky-panel").hidden = !on;
if (on) {
canvas.removeAttribute("aria-hidden");
canvas.setAttribute("tabindex", "0");
canvas.setAttribute("role", "application");
canvas.setAttribute("aria-roledescription", "Sky");
drawSky();
canvas.focus();
return;
}
for (const name of ["tabindex", "role", "aria-roledescription", "aria-label"]) {
canvas.removeAttribute(name);
}
canvas.setAttribute("aria-hidden", "true");
document.getElementById("settings-open").focus();
}
function skyPanel() {
const panel = document.getElementById("sky-panel");
const step = (name, label, run) => iconBtn(name, label, "btn small", run);
space.compass = h("span", {class: "compass", "aria-live": "polite"});
panel.append(space.compass,
step("bi-caret-left-fill", "Turn the view left (left arrow)", () => skyHooks.turnStep(-1, 0)),
step("bi-caret-right-fill", "Turn the view right (right arrow)", () => skyHooks.turnStep(1, 0)),
step("bi-caret-up-fill", "Raise the view (up arrow)", () => skyHooks.turnStep(0, -1)),
step("bi-caret-down-fill", "Lower the view (down arrow)", () => skyHooks.turnStep(0, 1)),
step("bi-zoom-out", "Widen the field of view (minus)", () => skyHooks.zoomStep(1 / ZOOM)),
step("bi-zoom-in", "Zoom in (plus)", () => skyHooks.zoomStep(ZOOM)),
step("bi-arrow-counterclockwise", "Reset the view: south, horizon, field of view 180° (key 0)", () => resetView("sky")),
step("bi-check-lg", "Leave the sky (Esc)", () => skyLook(false)));
steer(sky.canvas, "sky", skyHooks);
}