Surface (requirement 001 phase 2): navigation Stream (Inbox, Regeln) / Agents / Artifacts with keys 1-3 and hash routes (#graph and #tree redirect), header with title, command palette and settings only, server-side badges (unsnoozed inbox, beats within 900 s, open jobs), a Mayday banner with "aufheben", four stream chips with agent:/art:/seit: search, Filter and time menus, the inbox in main, an agents toolbar (Funk an alle with Newsletter, Broadcast, Pan-Pan, Security, Mayday; Priorität; Tag/Nacht), Post or Call in the agent panel, Ambiente in the settings and read-aloud over the seven classes with Mayday locked. Rename (decision 9): hub/ is orchestrator/, the package kvb_orchestrator, the unit kvb-orchestrator-runner.service, scripts/orchestrator.sh, environment KVB_ORCHESTRATOR_*, make targets orchestrator-*, the MCP server, image and compose project kvb-orchestrator, the guard header X-Kvb-Orchestrator and the message marker [kvb-orchestrator msg <id>]. install runs migrate first: it stops the old container, removes its image and code, moves state, approvals and pinned keys, swaps the MCP registration, the PermissionRequest hook and the runner unit, and changes nothing on a second run (scratch-HOME test with stub docker, claude and systemctl). Saved views fall back to the old localStorage key once. MEDUSA acceptances carried to the renamed paths: 82 rows whose lines are unchanged (reason prefixed "renamed from <old path>, line unchanged"), and 7 rows whose text changed only in the product name, the package name, a temp prefix or a new class argument, each upheld by one kvb-think-criticize skeptic (approval.py:60, messaging.py:53, server.py:56, tests/hub_support.py:64, passkey.py:205, page-agent.js:75, language.py:46). Cost 101 %: the longer name adds 15 tokens of skill text; the operator approved it with the commit. 🎯 Fix: 92 % 💥 Break: 4 % 💸 Cost: 101 % ⏱️ Time: – 🛡️ Security: 100 % 💰 Value: ? 🏆 Score: 87 🚦 Gate: ⚠️ cost 101 % > 100: ask the operator before the commit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
207 lines
9.7 KiB
JavaScript
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;
|
||
requestSpace();
|
||
}
|
||
|
||
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 ? ` · Mond ${Math.round(moon.lit * 100)} % ${moon.waxing ? "zunehmend" : "abnehmend"}` : "";
|
||
document.getElementById("sky-note").textContent = `${sun.alt > 0 ? "☀" : "☾"} ${STAGE_LABELS[Ambience.stage(sun.alt)]}${moonText}${turned ? ` · Blick ${facing[0]}` : ""}`;
|
||
if (space.look) {
|
||
const words = `Blick nach ${facing[1]} · ${((Math.round(view.azimuth) % 360) + 360) % 360}° · Höhe ${Math.round(view.altitude)}° · Sichtfeld ${Math.round(view.fov)}°`;
|
||
space.compass.textContent = words;
|
||
sky.canvas.setAttribute("aria-label", `Himmel, ${words}. Pfeiltasten drehen den Blick, Plus und Minus zoomen, 0 setzt zurück, Esc beendet.`);
|
||
}
|
||
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", "Himmel");
|
||
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 = (text, label, run) => h("button", {type: "button", class: "btn small", title: label, "aria-label": label, on: {click: run}}, text);
|
||
space.compass = h("span", {class: "compass", "aria-live": "polite"});
|
||
panel.append(space.compass,
|
||
step("◀", "Blick nach links drehen (Pfeil links)", () => skyHooks.turnStep(-1, 0)),
|
||
step("▶", "Blick nach rechts drehen (Pfeil rechts)", () => skyHooks.turnStep(1, 0)),
|
||
step("▲", "Blick heben (Pfeil hoch)", () => skyHooks.turnStep(0, -1)),
|
||
step("▼", "Blick senken (Pfeil runter)", () => skyHooks.turnStep(0, 1)),
|
||
step("−", "Sichtfeld weiten (Minus)", () => skyHooks.zoomStep(1 / ZOOM)),
|
||
step("+", "Hineinzoomen (Plus)", () => skyHooks.zoomStep(ZOOM)),
|
||
step("⟲ Zurücksetzen", "Blick zurücksetzen: Süden, Horizont, Sichtfeld 180° (Taste 0)", () => resetView("sky")),
|
||
step("Fertig", "Himmel verlassen (Esc)", () => skyLook(false)));
|
||
steer(sky.canvas, "sky", skyHooks);
|
||
}
|