Skip to content
Closed
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
1 change: 1 addition & 0 deletions docs/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ Hook-Code/PR-Berichtskorrekturen bleiben bei ihren Ownern.
| PLAN-KR-DELIVERY-CATCHUP | KR-Lieferbelege und Wellenstatus nachführen | srv-plan-kr-delivery-catchup; Stabschef koordiniert, Root nimmt ab | #1894/#1896/#1898 und Vorgänger #1900 (`a19bccb7`) gemergt; serielles PLAN-Schreibfenster | Server | C | Erledigt mit Root-Abnahme und Merge dieses Dokumentenschnitts | Vier neue Masterzeilen, ausschließlich freigegebener Elternsatz KR-06b-2 und zwei Wellenstatus 06b/04b; übriger Inhalt bytegleich. Nur Dokumentationsabschluss; keine neue Produkt-/native-/installierte-/wirksame-/Glass-Gesamt-/UAT-/Release-Abnahme | d87be838-da1c-43b9-a6ac-061c928566cb |
| KR-06b-2 | Projekt-/Flottenfragen in Glass zuordnen und Projektwechsel isolieren | srv-kr-06b-2-scope-r2; Root nimmt ab | Teilpakete #1896 und #1898 gemergt | Server | B; R1900-X1 C | Erledigt: #1900 liefert Fragenumfang, Zuordnung und Wechselisolation | Nur Browser-/Tauri-Mock-Belege; nativer Betrieb nicht belegt. Nachläufer R1900-D1/D2/C4/C5/C6/K4/K5 offen; Elternziel b5998181 im Zustand gemergt durch Root abgenommen (25.10. 06:16Z); native Nachweise bleiben bei V2-ACC1b/UAT | 6c247c5e-a94f-4f09-a3e7-ab1f50b3984f |
| GLASS-GI01-SETTINGS | Vier Einstellungs-Deep-Links mit Glass und Classic-Zugang abstimmen | bestehender Frontend-Owner srv-glass-gi01-settings; Root nimmt ab | begrenzter GI01-Auftrag; eigenes PLAN-Schreibfenster vor P4b1 | Server | B; dieser Zeilennachtrag C | Erledigt: #1869 gemergt | Teil von Elternziel 5936; Gesamtinhalt durch Root mit PLAN-v2.0-4d abgenommen; kein Dispatch; GI02/GI05 sowie Backend-/native/Glass-Gesamtabnahme bleiben offen | 8357b3ff-61b5-45d0-8d24-e6de737d4f9b |
| GLASS-GI05-HIDE | Ausgeblendete Glass-Deep-Links ehrlich anzeigen, konservativer Schnitt B | srv-glass-gi05-hide; Stabs disponiert, Root nimmt ab | Root-Entscheid GI05 (Option B); Basis `5c15cdde` | Server | B | Erledigt nach Merge: ausgeblendete Adresse bleibt erhalten und zeigt Bereichstitel, Hinweis und Einstellungs-Link; Schalter und Standard unverändert | G1–G8 im PR-Bericht: öffentliche App-/Tauri-Mock-Tests und inspizierte Browserbilder gegen Basis und Kopf. Beweise/Steuerung bleiben wie bisher ausgeblendet; 13 Platzhalter nicht umgesetzt; native, installiert/wirksam, Glass/UAT und Elternabnahme offen | 004c3d97-4739-4443-bef1-babe7f3ff518; Vorschlag-Eltern 128ff7bb; Produkt-Eltern 366c57e2-488c-48c1-9405-90b0cf28fab6 bleibt offen |
| DOC-KI24-H1 | KI-24, H1-Lieferung und B1-Schnitt nachführen; [Vollvertrag](archive/plan-2026-10/residuals-2026-10-24.md#z2-originals) | srv-doc-ki24-h1-merge (job75); Chief disponiert, Root nimmt ab | #1825 tatsächlich gemergt; Vorgängerticket 69c61ba2 bleibt Herkunft, keine laufende Sperre | Server | C | Erledigt mit Root-Abnahme/Merge dieses Dokumentenschnitts; KI-24 offen/beobachten, H1 erledigt, B1a/B1b getrennt | KI-24-Nachbeobachtung und B1a/B1b-Gesamtformat bleiben offen. | 2da280fe-e8de-4534-b17d-cbee14d40cf6 |
| FU-DG05-HEADER-HINT | Header-Hinweis und verwaisten Override korrigieren; [Vollvertrag](archive/plan-2026-10/residuals-2026-10-24.md#z2-originals) | srv-fu-dg05-header-hint; Root integriert | DG-05, RED-FIRST-E2E-NAME | Server | B | Erledigt: #1810 gemergt | Breiten- und Testhärtungsbefunde bleiben Nachläufer. | de13bd7b |
| FU-1804-ECHO-QUOTE | EQ-1-Informationsgrenze festhalten; EQ-2 entfallen; falsche Trust-Überschrift zu P9 | Root: EQ-1/P9; EQ-2 entfallen, kein aktiver Owner | PA-HOOKS-2b (#1804) | Server | A (entfallenes EQ-2); C (Dokumentationsabgleich) | Entfallen: EQ-2; EQ-1 bewiesene Grenze → P9; EQ-2 entfallen (Meldung im Produkt unerreichbar, #1813 geschlossen 22.10.); falsche Überschrift bei zurückgehaltenem Trust (R1804-R2-4) gehört zu P9. | Root `msg-0mvizj71n-b8f8d1b7` (22.10. 03:32:15Z), Bericht `srv-fu-1804-echo-quote.md`: Zitat und mögliches aktives Menü haben identische vollständige öffentliche Observation-Verläufe (CR-/Lösch-/Cursoroperationen); synthetischer Informationsbeweis, keine reale CLI-Messung oder Implementierungskorrektur. Fail-closed bleibt richtig; vor künftiger Freigabe des vervollständigten Echos ist generationsgebundene Composer-/Modal-Evidenz nötig; kein neuer Screen-/Producer-/P9-Scope genehmigt. Finale Root-Disposition `msg-0mvj0m263-4b88f439` (22.10. 04:02:29Z) und öffentlicher Kommentar #1813 (04:02:27Z): DROPPED nach Fremdreviews und Roots Quellenprüfung an `d29c144a`; `StartupDialog(CodexTrust)` wird im Produkt nicht erzeugt, der Konstantentest belegt keinen nutzerseitigen Producer. Kandidat `c30f3123ca5e456b436f672d8ab0cd00bb9596c2` geschlossen/ungemergt (04:02:28Z, kein Merge-SHA); nicht angenommen oder installiert, tatsächlicher Branch `codex/fu-1804-echo-quote` bleibt erhalten. Keine weitere EQ-2-Arbeit/Abnahme offen; kein PTY-/P9-PASS oder vollständige Nutzbar-Abnahme | be30833a |
Expand Down
10 changes: 7 additions & 3 deletions e2e/shell.core.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from "@playwright/test";
import { DEFAULT_PATH, ROUTES, SCOPE_ROUTE_IDS } from "../src/shell/routes";
import { ROUTES, SCOPE_ROUTE_IDS } from "../src/shell/routes";
import { GOAL_ROUTE, SETTINGS_CLASSIC_PATH } from "../src/shell/mount/routing";
import { createRunLog } from "../src/testing/e2e/runLog";

Expand Down Expand Up @@ -56,7 +56,7 @@ test("core Glass routes render without console errors and leave a run log", asyn
await testInfo.attach("shell-steps", { path: `${log.directory}/steps.json`, contentType: "application/json" });
});

test("hidden areas redirect to the Leitstand until weitere Bereiche is on", async ({ page }) => {
test("hidden areas retain their URL with an honest note and settings recovery", async ({ page }) => {
await page.addInitScript(() => {
localStorage.setItem("projecta.settings.featureFlag.d1_neue_oberflaeche", "1");
});
Expand All @@ -65,7 +65,11 @@ test("hidden areas redirect to the Leitstand until weitere Bereiche is on", asyn
for (const route of hidden) {
await page.goto(`/#${route.path}`, { waitUntil: "networkidle" });
await expect(page.locator(".g-shell")).toBeVisible();
await expect.poll(() => page.evaluate(() => window.location.hash), { message: route.path }).toBe(`#${DEFAULT_PATH}`);
await expect.poll(() => page.evaluate(() => window.location.hash), { message: route.path }).toBe(`#${route.path}`);
await expect(page.getByText("Dieser Bereich ist noch nicht verbunden und ausgeblendet.", { exact: true })).toBeVisible();
const recovery = page.getByRole("link", { name: "Weitere Bereiche in den Einstellungen einschalten", exact: true });
await expect(recovery).toBeVisible();
await expect(recovery).toHaveAttribute("href", "#/einstellungen");
}
});

Expand Down
4 changes: 3 additions & 1 deletion src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ export const de = {
"shell.unknownPage": "Unbekannte Seite",
"shell.notConnected": "Noch nicht verbunden: dieser Bildschirm kommt mit seinem eigenen Paket.",
"shell.unknownAddress": "Diese Adresse gibt es nicht.",
"shell.hidden": "Dieser Bereich ist noch nicht verbunden und ausgeblendet.",
"shell.hiddenSettings": "Weitere Bereiche in den Einstellungen einschalten",
"shell.search": "Suchen oder Befehl eingeben",
"shell.searchPlaceholder": "Agenten, PRs, Ideen, Befehle",
"shell.estop": "Not-Aus",
Expand Down Expand Up @@ -152,7 +154,7 @@ export const de = {
"flag.kundenprojekte.label": "Kundenprojekte",
"flag.kundenprojekte.description": "Zeigt Kundenprojekte mit Datenschutz; ausgeschaltet bleiben sie unsichtbar.",
"flag.weitereBereiche.label": "Weitere Bereiche",
"flag.weitereBereiche.description": "Zeigt neben Fabrik und Einstellungen alle übrigen Bereiche; ausgeschaltet bleiben sie unsichtbar.",
"flag.weitereBereiche.description": "Zeigt neben Fabrik, Betrieb und Einstellungen auch die noch nicht verbundenen Bereiche sowie Beweise und Core · Steuerung; ausgeschaltet sind sie ausgeblendet.",
"leitstand.tabs": "Ansichten",
"leitstand.tab.classic": "Klassisch",
"leitstand.tab.tools": "Werkzeuge",
Expand Down
14 changes: 7 additions & 7 deletions src/shell/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { applyStoredGlassVariant } from "../design/variants/useGlassVariant";
import { AppearanceControl } from "./AppearanceControl";
import { HeaderTools, StopBand } from "./header";
import { ProjectPicker } from "./header/ProjectPicker";
import { AREAS, DEFAULT_PATH, isHidden, pathFromHash, resolveRoute, visibleRoutes, type ShellRoute } from "./routes";
import { AREAS, isHidden, pathFromHash, resolveRoute, visibleRoutes, type ShellRoute } from "./routes";
import { applyStoredGlassTheme } from "./theme";
import "./shell.css";

Expand Down Expand Up @@ -43,18 +43,20 @@ function Sidebar({ active }: { active: ShellRoute | null }) {
function Placeholder({ path }: { path: string }) {
const t = useT();
const found = resolveRoute(path);
const hidden = isHidden(path);
const title = found ? [found.route.label, found.tab?.label].filter(Boolean).join(t("shell.titleSep")) : t("shell.unknownPage");
return (
<section className="g-shell-panel g-glass" aria-labelledby="g-shell-title">
{found?.route.tabs.length ? (
{found?.route.tabs.length && !hidden ? (
<nav className="g-shell-tabs" aria-label={t("tab.aria")}>
{found.route.tabs.map((tab) => (
<a key={tab.path} href={`#${tab.path}`} aria-current={tab === found.tab ? "page" : undefined}>{tab.label}</a>
))}
</nav>
) : null}
<h1 id="g-shell-title">{title}</h1>
<p className="g-shell-note">{found ? t("shell.notConnected") : t("shell.unknownAddress")}</p>
<p className="g-shell-note">{hidden ? t("shell.hidden") : found ? t("shell.notConnected") : t("shell.unknownAddress")}</p>
{hidden ? <a className="g-btn g-btn--ghost g-btn--sm" href="#/einstellungen">{t("shell.hiddenSettings")}</a> : null}
</section>
);
}
Expand All @@ -68,10 +70,8 @@ export function Shell() {
window.addEventListener("hashchange", onHash);
return () => window.removeEventListener("hashchange", onHash);
}, []);
const [more] = useFeatureFlag("weitere_bereiche");
// A hidden area (typed hash, old goal switch) lands on the Leitstand instead of showing its screen.
useEffect(() => { if (isHidden(path)) window.location.replace(`#${DEFAULT_PATH}`); }, [path, more]);
const active = resolveRoute(path)?.route ?? null;
useFeatureFlag("weitere_bereiche"); // re-render so a hidden area follows the flag
const active = isHidden(path) ? null : resolveRoute(path)?.route ?? null;

return (
<div className="g-shell">
Expand Down
14 changes: 10 additions & 4 deletions src/shell/mount/ShellMount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { createPortal } from "react-dom";

import type { AppGoal } from "../../lib/goals";
import { Shell } from "../Shell";
import { pathFromHash } from "../routes";
import { useFeatureFlag } from "../../flags/useFeatureFlag";
import { DEFAULT_PATH, isHidden, pathFromHash } from "../routes";
import StartRoute from "../../screens/start/StartRoute";
import TerminalRoute from "../../screens/terminal/TerminalRoute";
import VerlaufRoute from "../../screens/leitstand/verlauf/VerlaufRoute";
Expand Down Expand Up @@ -66,7 +67,12 @@ export default function ShellMount({ children }: { children: (props: LegacyRoute
}, []);
const openClassicSettings = useCallback(() => void (window.location.hash = `#${SETTINGS_CLASSIC_PATH}`), []);

const steuerung = isNewSteuerung(path);
useFeatureFlag("weitere_bereiche"); // re-render so a hidden area follows the flag
// A hidden area keeps its URL and shows the Shell notice: no fresh or legacy view opens, the legacy app stays mounted.
const hidden = isHidden(path);
const shown = useRef(hidden ? DEFAULT_PATH : path);
if (!hidden) shown.current = path; // the legacy app keeps its last visible route, so it does not write another hash
const steuerung = isNewSteuerung(path) && !hidden;
const start = isStart(path);
const betrieb = isBetrieb(path);

Expand All @@ -75,7 +81,7 @@ export default function ShellMount({ children }: { children: (props: LegacyRoute
const verlauf = isVerlauf(path);
const gallery = isGallery(path);
const fresh = isNewLeitstand(path) || steuerung || freshSettings || start || betrieb || terminal || verlauf || gallery;
const legacy = hasLegacyPanel(path) && !fresh;
const legacy = hasLegacyPanel(path) && !fresh && !hidden;
return (
<div className="g-mount" ref={frame} data-legacy={legacy ? "on" : "off"} data-fresh={fresh ? "on" : "off"}>
<Shell />
Expand All @@ -92,7 +98,7 @@ export default function ShellMount({ children }: { children: (props: LegacyRoute
{gallery ? <Suspense fallback={null}><Gallery /></Suspense> : null}
<div className="g-mount-legacy" hidden={!legacy} data-tabs={path === CLASSIC_PATH ? "on" : undefined}>
{path === CLASSIC_PATH ? <LeitstandTabs path={path} /> : null}
{children({ routePath: path, onGoalRoute })}
{children({ routePath: shown.current, onGoalRoute })}
</div>
</>,
host,
Expand Down
56 changes: 56 additions & 0 deletions src/shell/mount/scope-entry.mount.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,59 @@ it("hides the setup team action by default and restores its destination when ena
await waitFor(() => expect(window.location.hash).toBe("#/team"));
expect(await screen.findByRole("link", { name: "Betrieb", current: "page" })).toBeVisible();
});

const NOTICE = "Dieser Bereich ist noch nicht verbunden und ausgeblendet.";
const FLAG_KEY = "projecta.settings.featureFlag.weitere_bereiche";
const mountAt = async (hash: string) => {
window.location.hash = hash;
render(<App />);
await act(async () => { await vi.dynamicImportSettled(); });
};
const goTo = (hash: string) => act(async () => { window.location.hash = hash; window.dispatchEvent(new HashChangeEvent("hashchange")); });
const title = () => document.getElementById("g-shell-title")?.textContent;

it("keeps a hidden deep link and shows its honest settings recovery", async () => {
await mountAt("#/gedaechtnis");
expect(window.location.hash).toBe("#/gedaechtnis");
expect(title()).toBe("Wissen");
expect(screen.getByText(NOTICE)).toBeVisible();
await goTo("#/steuerung/autonomie");
expect(window.location.hash).toBe("#/steuerung/autonomie");
expect(title()).toBe("Core · Steuerung · Core & Autonomie");
expect(screen.getByText(NOTICE)).toBeVisible();
expect(screen.queryByRole("link", { name: "Betrieb", current: "page" })).toBeNull();
fireEvent.click(screen.getByRole("link", { name: "Weitere Bereiche in den Einstellungen einschalten" }));
fireEvent.click(await screen.findByRole("switch", { name: "Weitere Bereiche" }));
expect(localStorage.getItem(FLAG_KEY)).toBe("1");
await goTo("#/gedaechtnis");
expect(window.location.hash).toBe("#/gedaechtnis");
expect(title()).toBe("Wissen");
expect(screen.queryByText(NOTICE)).toBeNull();
expect(screen.getByText(/Noch nicht verbunden/)).toBeVisible();
cleanup();
await mountAt("#/gedaechtnis/");
expect(screen.queryByText(NOTICE)).toBeNull();
expect(title()).toBe("Wissen");
});

it("leaves visible and unknown and explicitly switched-off routes as they are", async () => {
await mountAt("#/nirgends");
expect(window.location.hash).toBe("#/nirgends");
expect(screen.getByText("Diese Adresse gibt es nicht.")).toBeVisible();
await goTo("#/start");
expect(screen.getByRole("list", { name: "Schritte der Einrichtung" })).toBeVisible();
localStorage.setItem(FLAG_KEY, "0");
cleanup();
await mountAt("#/team/personas?x=1");
expect(window.location.hash).toBe("#/team/personas?x=1");
expect(title()).toBe("Team · Personas");
expect(screen.getByText(NOTICE)).toBeVisible();
});

it("keeps a hidden deep link after the legacy app has left another goal", async () => {
await mountAt("#/einstellungen");
await goTo("#/beweise/diff");
expect(window.location.hash).toBe("#/beweise/diff");
expect(title()).toBe("Beweise · Diff");
expect(screen.getByText(NOTICE)).toBeVisible();
});
6 changes: 2 additions & 4 deletions src/shell/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,8 @@ export function isHidden(path: string): boolean {
return found !== null && !inScope(found.route);
}

/** Like `lookup`, but a hidden area resolves to the Leitstand; null = unknown. */
export function resolveRoute(path: string): ResolvedRoute | null {
return lookup(isHidden(path) ? DEFAULT_PATH : path);
}
/** The known route behind a path, hidden or not (a hidden one keeps its URL and shows a notice); null = unknown. */
export const resolveRoute = lookup;

/** `#/beweise/diff` to `/beweise/diff`. A query and a trailing slash are dropped; `#/` and an empty hash open the default route. */
export function pathFromHash(hash: string): string {
Expand Down
17 changes: 11 additions & 6 deletions src/shell/scope.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { act, cleanup, fireEvent, render, screen, within } from "@testing-librar
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { setFeatureFlagEnabled } from "../flags/featureFlags";
import { AREAS, resolveRoute } from "./routes";
import { AREAS, isHidden, resolveRoute } from "./routes";
import { Shell } from "./Shell";

vi.mock("../lib/ipc", () => ({ getEmergencyStop: vi.fn().mockResolvedValue(false), setEmergencyStop: vi.fn(), getQuotaState: vi.fn().mockResolvedValue([]), getProviderOverview: vi.fn().mockResolvedValue([]), describeError: String }));
Expand All @@ -22,14 +22,15 @@ beforeEach(() => { localStorage.clear(); window.location.hash = ""; setFeatureFl
afterEach(cleanup);

describe("shell scope (V2-SCOPE-1)", () => {
it("with D1 on and the scope flag off the sidebar and palette omit non-shell areas and resolveRoute lands on /leitstand", async () => {
it("with D1 on and the scope flag off the sidebar and palette omit non-shell areas and resolveRoute keeps the hidden route", async () => {
await mount();
expect(sidebar()).toEqual(SCOPE);
const hits = palette();
expect(hits.length).toBeGreaterThan(0);
expect(hits.every((label) => PALETTE_AREAS.some((area) => label.startsWith(area)))).toBe(true);
expect(resolveRoute("/steuerung/autonomie")?.route.id).toBe("leitstand");
expect(resolveRoute("/beweise")?.tab?.path).toBe("/leitstand");
expect(resolveRoute("/steuerung/autonomie")?.route.id).toBe("steuerung");
expect(resolveRoute("/beweise")?.tab?.path).toBe("/beweise");
expect([isHidden("/beweise/diff/"), isHidden("/leitstand"), isHidden("/start"), isHidden("/nirgends")]).toEqual([true, false, false, false]);
expect(resolveRoute("/start")?.route.id).toBe("start");
expect(resolveRoute("/nirgends")).toBeNull();
});
Expand All @@ -41,10 +42,14 @@ describe("shell scope (V2-SCOPE-1)", () => {
expect(links[0]).toHaveAttribute("aria-current", "page");
});

it("with the scope flag off a hidden hash is replaced by /leitstand while /start stays", async () => {
it("with the scope flag off a hidden hash keeps its URL with a notice and settings link while /start stays", async () => {
window.location.hash = "#/team/personas";
await mount();
expect(window.location.hash).toBe("#/leitstand");
expect(window.location.hash).toBe("#/team/personas");
expect(screen.getByRole("heading", { level: 1 }).textContent).toBe("Team · Personas");
expect(screen.getByText("Dieser Bereich ist noch nicht verbunden und ausgeblendet.")).toBeVisible();
expect(screen.getByRole("link", { name: /Weitere Bereiche/ })).toHaveAttribute("href", "#/einstellungen");
expect(screen.queryByRole("link", { name: "Betrieb", current: "page" })).toBeNull();
await go("#/start");
expect(window.location.hash).toBe("#/start");
expect(screen.getByRole("heading", { level: 1 }).textContent).toBe("Ersteinrichtung");
Expand Down
Loading