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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ Standard-Invocations (`dotnet build|test`, in `src/nodepilot-ui` die `package.js

### Testumfang pro Änderung

**Tests schreiben ≠ alle Tests ausführen.** Die Pflicht oben gilt unverändert für das *Schreiben*; lokal *ausgeführt* wird nur, was die Änderung betrifft. Die Voll-Suite ist gemessen unverhältnismäßig (6.597 Backend-Testfälle, 241 Vitest-Dateien, 77 E2E-Specs — die beiden Frontend-Zahlen hält `DocumentationCountsTests` an der Dateiliste fest, die Backend-Zahl bleibt ein Handmaß) und liefert lokal kein neues Signal: das Netz hängt an `ci.yml`, das auf **jedem PR und jedem Push auf main** läuft (Coverage-Gate + E2E eingeschlossen).
**Tests schreiben ≠ alle Tests ausführen.** Die Pflicht oben gilt unverändert für das *Schreiben*; lokal *ausgeführt* wird nur, was die Änderung betrifft. Die Voll-Suite ist gemessen unverhältnismäßig (6.597 Backend-Testfälle, 242 Vitest-Dateien, 77 E2E-Specs — die beiden Frontend-Zahlen hält `DocumentationCountsTests` an der Dateiliste fest, die Backend-Zahl bleibt ein Handmaß) und liefert lokal kein neues Signal: das Netz hängt an `ci.yml`, das auf **jedem PR und jedem Push auf main** läuft (Coverage-Gate + E2E eingeschlossen).

**Der Nightly ist kein verlässlicher zweiter Boden.** Er läuft als Windows-Task um 22:00 gegen den ausgecheckten Baum und wird verpasst, sobald die Maschine dann aus ist. Wer sich auf ihn beruft, prüft vorher `C:\temp\nodepilot-nightly\latest.md` auf sein Datum.

Expand Down
10 changes: 5 additions & 5 deletions src/nodepilot-docs-ui/src/site/experience/missions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import type { Lang } from '../../i18n/languages'

export const additionalMissions = {
de: [
{ id: 'build', title: 'Bauen eines Workflows.', text: 'Drei Activities verbinden, veröffentlichen und den eigenen Lauf prüfen.', steps: ['Activities verbinden', 'Workflow veröffentlichen', 'Ergebnis prüfen'], duration: '4 Minuten' },
{ id: 'build', title: 'Einen Workflow bauen.', text: 'Drei vorbereitete Schritte verbinden, den Workflow veröffentlichen und seine Ausgabe prüfen.', steps: ['Schritte verbinden und speichern', 'Workflow veröffentlichen', 'Starten und Ergebnis prüfen'], duration: '4 Minuten' },
{ id: 'decision', title: 'Verfolge den Weg.', text: 'Bei 8 GB freiem Speicher den kritischen Pfad der Speicherplatzprüfung verfolgen.', steps: ['8 GB eingeben', 'Kritischen Pfad starten', 'Ergebnis prüfen'], duration: '3 Minuten' },
{ id: 'parallel', title: 'Parallele Arbeit beobachten.', text: 'Prüfsumme und Bereinigung laufen vor der gemeinsamen Abschlussmail.', steps: ['Bereinigung starten', 'Beide Zweige verfolgen', 'Junction prüfen'], duration: '3 Minuten' },
{ id: 'parallel', title: 'Parallele Arbeit beobachten.', text: 'Prüfsumme und Bereinigung laufen gleichzeitig. Die Abschlussmail wartet auf beide.', steps: ['Bereinigung starten', 'Beide Zweige verfolgen', 'Zusammenführung prüfen'], duration: '3 Minuten' },
{ id: 'service', title: 'Einen Dienst wiederherstellen.', text: 'Einen gestoppten Dienst starten und seine erneute Prüfung nachvollziehen.', steps: ['Gestoppten Dienst starten', 'Erneute Prüfung verfolgen', 'Meldung prüfen'], duration: '3 Minuten' },
{ id: 'live', title: 'Greife live in einen Lauf ein.', text: 'Eine laufende Ausführung in Live Ops finden und gezielt abbrechen.', steps: ['Lauf starten', 'In Live Ops öffnen', 'Abbruch prüfen'], duration: '2 Minuten' },
{ id: 'versions', title: 'Zwei Workflow Versionen vergleichen.', text: 'Im Diff entdecken, welche Activity der früheren Version fehlt.', steps: ['Versionsvergleich öffnen', 'Ältere Version wählen', 'Unterschied benennen'], duration: '2 Minuten' },
{ id: 'machine', title: 'Prüfung einer Zielmaschine.', text: 'LAB01 finden und Erreichbarkeit sowie Workflow-Nutzung einordnen.', steps: ['LAB01 filtern', 'Status und Nutzung lesen', 'Auswirkung benennen'], duration: '2 Minuten' },
{ id: 'versions', title: 'Zwei Workflow-Versionen vergleichen.', text: 'Im Vergleich erkennen, welcher Schritt der früheren Version fehlt.', steps: ['Versionsvergleich öffnen', 'Ältere Version wählen', 'Unterschied benennen'], duration: '2 Minuten' },
{ id: 'machine', title: 'Eine Zielmaschine prüfen.', text: 'LAB01 finden und den unbekannten Status sowie die Workflow-Nutzung einordnen.', steps: ['LAB01 filtern', 'Status und Nutzung lesen', 'Aussage einordnen'], duration: '2 Minuten' },
{ id: 'maintenance', title: 'Plane ein Wartungsfenster', text: 'Patch night zeitlich ändern und den Geltungsbereich prüfen.', steps: ['Patch night bearbeiten', '23:00–02:00 speichern', 'Geltungsbereich prüfen'], duration: '3 Minuten' },
],
en: [
Expand All @@ -18,7 +18,7 @@ export const additionalMissions = {
{ id: 'service', title: 'Recover a service.', text: 'Start a stopped service and follow its verification.', steps: ['Start with a stopped service', 'Follow verification', 'Check the message'], duration: '3 minutes' },
{ id: 'live', title: 'Control a live run.', text: 'Find a running execution in Live Ops and cancel it.', steps: ['Start the execution', 'Open Live Ops', 'Confirm cancellation'], duration: '2 minutes' },
{ id: 'versions', title: 'Compare two versions.', text: 'Find the activity missing from the previous version.', steps: ['Open version comparison', 'Select the older version', 'Name the difference'], duration: '2 minutes' },
{ id: 'machine', title: 'Inspect a target machine.', text: 'Find LAB01 and assess its reachability and workflow usage.', steps: ['Filter for LAB01', 'Read status and usage', 'Assess the impact'], duration: '2 minutes' },
{ id: 'machine', title: 'Inspect a target machine.', text: 'Find LAB01 and interpret its unknown status and workflow usage.', steps: ['Filter for LAB01', 'Read status and usage', 'Interpret the result'], duration: '2 minutes' },
{ id: 'maintenance', title: 'Plan a maintenance window.', text: 'Change Patch night and check which workflows it covers.', steps: ['Edit Patch night', 'Save 23:00–02:00', 'Check its scope'], duration: '3 minutes' },
],
} as const satisfies Record<Lang, readonly { id: string; title: string; text: string; steps: readonly string[]; duration: string }[]>
2 changes: 1 addition & 1 deletion src/nodepilot-docs-ui/src/site/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const de = {
"intro": "Zehn kurze Aufgaben führen direkt durch die Produktoberfläche: vom ersten Lauf über Entscheidungen und Live Ops bis zur Wartungsplanung.",
"moreAction": "Aufgabe starten",
"firstKicker": "KONFIGURATIONSDATEI BEREITSTELLEN",
"firstTitle": "Auftrag durch Datei.",
"firstTitle": "Eine Konfigurationsdatei bereitstellen.",
"firstText": "Ein Konfigurationswert wird geändert. PowerShell erzeugt die Datei, File Copy bringt sie ins Ziel und Return Data zeigt das Ergebnis.",
"firstStep": "Einen Wert im Startdialog ändern",
"secondStep": "Den Workflow starten und verfolgen",
Expand Down
2 changes: 2 additions & 0 deletions src/nodepilot-ui/demo/net/anchors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* `/api/audit/export?format=…`), but this guards the class rather than that one page: any
* same-origin link that would leave the demo's own directory is routed through the demo
* instead, and a response that carries `Content-Disposition` is saved as a real download.
* The website's task overview is an intentional exit and remains normal navigation.
*/

let detach: (() => void) | null = null;
Expand Down Expand Up @@ -63,6 +64,7 @@ export function installAnchorGuard(): void {
if (resolved.protocol === 'blob:' || resolved.protocol === 'data:') return;
if (resolved.origin !== globalThis.location.origin) return;
if (resolved.pathname === '/demo' || resolved.pathname.startsWith('/demo/')) return;
if (resolved.pathname === '/walkthrough/' || resolved.pathname === '/en/walkthrough/') return;

void handleEscapingClick(event, resolved.toString());
};
Expand Down
2 changes: 1 addition & 1 deletion src/nodepilot-ui/demo/run/missionScenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export function missionOutcome(workflowId: string, node: GraphNode, inputs: Reco
}
if (workflowId === MISSION_WORKFLOW_IDS.live) {
if (id === 'trigger') return result('Started manually.');
if (id === 'wait') return result('Long-running check completed.', {}, 80_000);
if (id === 'wait') return result('This simulated check waits for cancellation.');
if (id === 'result') return result('{"status":"complete"}', { status: 'complete' });
}
return null;
Expand Down
7 changes: 5 additions & 2 deletions src/nodepilot-ui/demo/run/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ export function startRun(workflowId: string, triggeredBy = 'manual', parameters:
if (run.cancelled) return;
const node: GraphNode | undefined = nodeById.get(id);
const activityType = node?.data?.activityType;
const outcome = node && missionOutcome(workflowId, node, inputs)
const guidedOutcome = node && missionOutcome(workflowId, node, inputs);
const outcome = guidedOutcome
|| workflowId === FILE_WORKFLOW_ID && node && fileOutcome(node, inputs, bus)
|| outcomeFor(activityType);
const error = 'error' in outcome ? outcome.error as string | undefined : undefined;
Expand Down Expand Up @@ -208,7 +209,9 @@ export function startRun(workflowId: string, triggeredBy = 'manual', parameters:
},
});

const duration = workflowId === MISSION_WORKFLOW_IDS.live && id === 'wait' ? 20_000 : pacedDuration(outcome.durationMs);
// The cancellation exercise waits for the learner, not a countdown.
if (workflowId === MISSION_WORKFLOW_IDS.live && id === 'wait' && activityType === 'waitForCondition') return;
const duration = guidedOutcome ? Math.max(1000, guidedOutcome.durationMs) : pacedDuration(outcome.durationMs);
schedule(() => {
if (!error && node) publishFileOutcome(bus, node, outcome);
completeStep(row, outcome.output, outcome.outputParameters, error, onComplete);
Expand Down
28 changes: 25 additions & 3 deletions src/nodepilot-ui/demo/seed/missionFixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,25 @@ const sourceIds = {
service: demoId('workflow:service-recovery'),
} as const;

const NOTES: Record<'de' | 'en', Record<string, string>> = {
de: {
note_probe: '1. Freien Speicher simulieren\nIm Startdialog gibst du freeSpaceGb ein. Die Demo übernimmt diesen Wert für die Speicherplatzprüfung; sie liest kein echtes Laufwerk.',
note_branch: '2. Einen Zweig auswählen\nUnter 10 GB: critical. Unter 25 GB: warning. Sonst: healthy. Junction: waitAny führt den gewählten Zweig wieder zum Ergebnis zusammen.',
note_input: '1. Ordner und Alter wählen\npath ist der Quellordner, olderThanDays die Altersgrenze in Tagen. Lass für diese Übung beide Werte unverändert. Es werden keine echten Dateien verändert.',
note_finish: '2. Parallel arbeiten und zusammenführen\nPrüfsumme und Bereinigung laufen gleichzeitig. Junction: waitAll wartet auf beide, bevor die Abschlussmail startet.',
note_check: '1. Dienstzustand simulieren\nStarte manuell mit serviceState = Stopped. Die Entscheidung wählt den Wiederherstellungszweig. Diese Übung hat keinen Zeitplan.',
note_recover: '2. Starten und erneut prüfen\nNach dem Start wartet der Workflow kurz und liest den Status erneut. Running führt zur Erfolgsmeldung; ein anderer Status zur Fehlermeldung.',
},
en: {
note_probe: '1. Simulate free space\nEnter freeSpaceGb in the start dialog. The demo uses that value for the disk-space check; it does not read a real drive.',
note_branch: '2. Select one branch\nBelow 10 GB: critical. Below 25 GB: warning. Otherwise: healthy. Junction: waitAny joins the selected branch back into the result.',
note_input: '1. Choose folder and age\npath is the source folder; olderThanDays is the age threshold in days. Leave both unchanged for this exercise. No real files are modified.',
note_finish: '2. Work in parallel, then join\nChecksum and cleanup run together. Junction: waitAll waits for both before starting the summary email.',
note_check: '1. Simulate a service state\nStart manually with serviceState = Stopped. The decision selects recovery. This exercise does not run on a schedule.',
note_recover: '2. Start and verify\nAfter starting the service, wait briefly and read its state again. Running leads to confirmation; another state leads to a failure notification.',
},
};

function buildNodes(): GraphNode[] {
return [
{ id: 'trigger', type: 'activity', position: { x: 0, y: 200 }, data: { label: 'Manual Trigger: hello', activityType: 'manualTrigger', targetMachineId: null, config: { parameters: [] } } },
Expand All @@ -35,7 +54,7 @@ function liveNodes(): GraphNode[] {
];
}

function fixtureDefinition(id: keyof typeof MISSION_WORKFLOW_IDS): { nodes: GraphNode[]; edges: GraphEdge[] } | null {
function fixtureDefinition(id: keyof typeof MISSION_WORKFLOW_IDS, language: 'de' | 'en'): { nodes: GraphNode[]; edges: GraphEdge[] } | null {
if (id === 'build') return { nodes: buildNodes(), edges: [] };
if (id === 'live') return { nodes: liveNodes(), edges: [
{ id: 'live-trigger-wait', source: 'trigger', target: 'wait', data: { label: 'Always' } },
Expand All @@ -44,6 +63,9 @@ function fixtureDefinition(id: keyof typeof MISSION_WORKFLOW_IDS): { nodes: Grap
const source = getWorld().workflows.find(workflow => workflow.id === sourceIds[id]);
if (!source) return null;
const definition = structuredClone(definitionOf(source));
for (const node of definition.nodes) {
if (node.data?.activityType === 'note' && NOTES[language][node.id]) Object.assign(node.data, { text: NOTES[language][node.id] });
}
if (id === 'decision' || id === 'service') {
const trigger = definition.nodes.find(node => node.id === 'trg');
if (!trigger?.data) return null;
Expand All @@ -53,12 +75,12 @@ function fixtureDefinition(id: keyof typeof MISSION_WORKFLOW_IDS): { nodes: Grap
}

/** Creates one fresh, local copy when a task starts; re-entry never overwrites visitor edits. */
export function ensureMissionWorkflow(id: keyof typeof MISSION_WORKFLOW_IDS): Workflow | null {
export function ensureMissionWorkflow(id: keyof typeof MISSION_WORKFLOW_IDS, language: 'de' | 'en' = 'en'): Workflow | null {
const world = getWorld();
const workflowId = MISSION_WORKFLOW_IDS[id];
const existing = world.workflows.find(workflow => workflow.id === workflowId);
if (existing) return existing;
const definition = fixtureDefinition(id);
const definition = fixtureDefinition(id, language);
if (!definition) return null;
const source = id === 'decision' || id === 'parallel' || id === 'service'
? world.workflows.find(workflow => workflow.id === sourceIds[id]) : world.workflows[0];
Expand Down
Loading
Loading