@revenexx/studio-integrations 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +254 -0
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +2 -6
  4. package/dist/runtime/components/Integrations/ExpressionInput.vue +33 -4
  5. package/dist/runtime/components/Integrations/NodeInspector.vue +40 -0
  6. package/dist/runtime/components/Integrations/RunActionsMenu.vue +3 -2
  7. package/dist/runtime/components/Integrations/RunDetailPanel.vue +9 -6
  8. package/dist/runtime/components/Integrations/RunsTable.vue +15 -3
  9. package/dist/runtime/components/Integrations/WorkflowEditor.vue +27 -1
  10. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +9 -1
  11. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +13 -0
  12. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +38 -3
  13. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +13 -0
  14. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -6
  15. package/dist/runtime/components/Integrations/WorkflowsTable.vue +9 -4
  16. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +9 -2
  17. package/dist/runtime/composables/useIntegrationsDeadLetters.js +27 -18
  18. package/dist/runtime/composables/useIntegrationsRuns.d.ts +30 -1
  19. package/dist/runtime/composables/useIntegrationsRuns.js +70 -29
  20. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +7 -15
  21. package/dist/runtime/composables/useIntegrationsSchedules.js +80 -35
  22. package/dist/runtime/composables/useIntegrationsTriggers.d.ts +8 -1
  23. package/dist/runtime/data/loopBody.d.ts +129 -0
  24. package/dist/runtime/data/loopBody.js +76 -0
  25. package/dist/runtime/i18n/messages/de.json +46 -33
  26. package/dist/runtime/i18n/messages/en.json +13 -0
  27. package/dist/runtime/pages/integrations/index.vue +29 -19
  28. package/dist/runtime/pages/integrations/runs.vue +22 -10
  29. package/dist/runtime/pages/integrations/schedules.vue +64 -7
  30. package/dist/runtime/pages/integrations/workflows/[id].vue +22 -8
  31. package/dist/runtime/types/integrations.d.ts +39 -0
  32. package/dist/runtime/utils/statusStyle.d.ts +76 -0
  33. package/dist/runtime/utils/statusStyle.js +27 -0
  34. package/package.json +1 -1
@@ -1,39 +1,84 @@
1
1
  import { computed } from "vue";
2
+ import { useIntegrationsApi } from "./integrationsApi.js";
3
+ import { useIntegrationsT } from "./useIntegrationsT.js";
4
+ import { useLoadGate } from "./useLoadGate.js";
5
+ import { IntegrationsApiError } from "../utils/apiError.js";
2
6
  import { scheduleTimezone } from "../utils/schedule.js";
3
7
  export function useIntegrationsSchedules() {
4
- const { all, loading, loadAll } = useIntegrationsTriggers();
5
- const entries = computed(
6
- () => all.value.filter((t) => t.type === "schedule").map((t) => {
7
- const cfg = t.config ?? {};
8
- const triggerId = String(t.id);
9
- return {
10
- id: `${t.workflow_id}:${t.id}`,
11
- workflowId: t.workflow_id,
12
- workflowName: t.workflowName,
13
- triggerId,
14
- // Never invented: a trigger without an expression says so on the page.
15
- // Standing in `* * * * *` for it would read as "every minute", and with
16
- // the expression no longer on screen that guess would be invisible.
17
- cron: typeof cfg.cron === "string" && cfg.cron.trim() !== "" ? cfg.cron : null,
18
- // The schedule's own zone, or the platform's default where it has none —
19
- // from the same helper the trigger settings read, so one schedule cannot
20
- // be shown in two zones on two screens.
21
- timezone: scheduleTimezone(cfg.timezone),
22
- active: t.active,
23
- nextRunAt: t.active ? mockNextRun(t.workflow_id, triggerId) : null,
24
- lastRunAt: t.active ? mockLastRun(t.workflow_id, triggerId) : null
25
- };
26
- })
27
- );
28
- const total = computed(() => entries.value.length);
29
- const activeCount = computed(() => entries.value.filter((e) => e.active).length);
30
- return { entries, total, activeCount, loading, load: loadAll };
31
- }
32
- function mockNextRun(workflowId, triggerId) {
33
- const offsetMinutes = (workflowId * 7 + triggerId.length * 3) % 720 + 5;
34
- return new Date(Date.now() + offsetMinutes * 6e4).toISOString();
35
- }
36
- function mockLastRun(workflowId, triggerId) {
37
- const offsetMinutes = (workflowId * 13 + triggerId.length * 11) % 720 + 30;
38
- return new Date(Date.now() - offsetMinutes * 6e4).toISOString();
8
+ const api = useIntegrationsApi();
9
+ const t = useIntegrationsT();
10
+ const entries = useState("integrations.schedules", () => []);
11
+ const loading = useState("integrations.schedules.loading", () => false);
12
+ const loaded = useState("integrations.schedules.loaded", () => false);
13
+ const error = useState("integrations.schedules.error", () => null);
14
+ const meta = useState("integrations.schedules.meta", () => ({
15
+ total: 0,
16
+ live_count: 0,
17
+ timezones: []
18
+ }));
19
+ const gate = useLoadGate("integrations.schedules", { loading, loaded });
20
+ function mapEntry(row) {
21
+ const block = row.schedule;
22
+ const firstFiring = block?.next_fire_times?.[0] ?? null;
23
+ return {
24
+ id: `${row.workflow_id}:${row.id}`,
25
+ workflowId: row.workflow_id,
26
+ workflowName: row.workflow_name ?? "",
27
+ triggerId: String(row.id),
28
+ // Never invented: a trigger without an expression says so on the page.
29
+ // Standing in `* * * * *` for it would read as "every minute", and with the
30
+ // expression no longer on screen that guess would be invisible.
31
+ cron: block?.cron ?? null,
32
+ // The zone the platform resolved, rather than one guessed here — but the
33
+ // platform's default put back where it reports none. `timezone` is null on
34
+ // the wire whenever the expression could not be read AND the schedule
35
+ // carries no zone of its own (`ScheduleTelemetry::present` falls through to
36
+ // `configuredTimezone()`), which is exactly the row AC-9 promises exists.
37
+ //
38
+ // A blank there is not merely ugly, it is unreachable: `meta.timezones`
39
+ // contributes the platform default for a schedule carrying none, and the
40
+ // service's `scopeToTimezone()` matches those rows when the reader narrows
41
+ // to that default — so a row with no zone at all would show a bare globe
42
+ // and then vanish from every timezone the filter offers. The same helper
43
+ // the trigger settings use, so one schedule cannot read two zones
44
+ // (`utils/schedule.ts`, and the note in `NodeInspector.vue`).
45
+ timezone: scheduleTimezone(block?.timezone),
46
+ active: row.active,
47
+ workflowActive: row.workflow_active,
48
+ nextRunAt: block?.next_fire_at ?? null,
49
+ nextRunLocal: firstFiring?.local ?? null,
50
+ // When the run STARTED, which is nullable on the platform
51
+ // (`workflow_runs.started_at`): a run the scheduler has created and the
52
+ // worker has not picked up yet carries a status and no start. So this
53
+ // being null does not mean "never ran" — `lastRunStatus` is what separates
54
+ // the two, and the page states the queued run rather than showing the
55
+ // dash a schedule that has never fired gets (`specs/schedules.md` AC-12).
56
+ lastRunAt: block?.last_run?.started_at ?? null,
57
+ /** Non-null exactly when the platform holds a last run at all. */
58
+ lastRunStatus: block?.last_run?.status ?? null,
59
+ lastDispatchAt: block?.last_dispatch_at ?? null,
60
+ holdsFirings: block?.holds_firings ?? false,
61
+ notFiringReason: block?.not_firing_reason ?? null
62
+ };
63
+ }
64
+ async function load(force = false) {
65
+ if (!await gate.claim(force)) return entries.value;
66
+ error.value = null;
67
+ try {
68
+ const res = await api.get("/schedules");
69
+ entries.value = (res?.data ?? []).map(mapEntry);
70
+ meta.value = res?.meta ?? { total: 0, live_count: 0, timezones: [] };
71
+ loaded.value = true;
72
+ } catch (e) {
73
+ error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.schedules.error.loadFailed");
74
+ loaded.value = false;
75
+ } finally {
76
+ gate.release();
77
+ }
78
+ return entries.value;
79
+ }
80
+ const total = computed(() => meta.value.total);
81
+ const activeCount = computed(() => meta.value.live_count);
82
+ const timezonesInUse = computed(() => meta.value.timezones);
83
+ return { entries, total, activeCount, timezonesInUse, loading, loaded, error, load };
39
84
  }
@@ -7,8 +7,15 @@ import type { ApiTriggerLite } from './useIntegrationsWorkflows.js';
7
7
  * is how the editor's trigger settings work.
8
8
  *
9
9
  * `loadAll()` does NOT: the workflow list carries each workflow's triggers in full,
10
- * so the tenant-wide view is derived from that response rather than fetched per
10
+ * so a tenant-wide view is derived from that response rather than fetched per
11
11
  * workflow (PO-236). What it still costs is the workflow list, once.
12
+ *
13
+ * Nothing in this module reads `loadAll()` any more. The Schedules page was its
14
+ * one caller and now reads `GET /schedules`, which is the only source that can
15
+ * report when a schedule fires next (PO-231). It is kept because these
16
+ * composables are the module's public, auto-imported surface — a host app may
17
+ * hold the tenant's triggers this way, and the Webhooks view this was also
18
+ * written for has never been built.
12
19
  */
13
20
  export interface ApiTrigger {
14
21
  id: number;
@@ -0,0 +1,129 @@
1
+ import type { NodeManifest, WorkflowEdge, WorkflowNode } from '../types/integrations.js';
2
+ /**
3
+ * Which nodes repeat, and which edges are walked once per element (PO-277).
4
+ *
5
+ * The iterator draws its two outputs like any other pair of ports, so nothing
6
+ * on the canvas said that everything hanging off `item` runs once per element
7
+ * while everything off `done` runs once, afterwards. This module computes the
8
+ * repeating part; the canvases mark it (`specs/editor-loop-body.md`).
9
+ *
10
+ * **The set is the engine's, not a tidier one.** `getLoopBodyNodes`
11
+ * (`worker/src/dagUtils.ts`) walks forward from the `item` port and follows
12
+ * EVERY outgoing edge it meets — an error route, a nested iterator's `done`,
13
+ * an edge the author meant as "and afterwards, do this". There is no stop
14
+ * condition, because the loop body has no end anywhere in the workflow. The
15
+ * walk below is that walk, deliberately: a UI that computed the body the
16
+ * author *meant* would draw nothing on the node that surprises them, which is
17
+ * the one case this is for.
18
+ *
19
+ * **Nesting needs no special case.** The engine narrows an inner iterator's
20
+ * body to its outer one (`getLoopBodyNodes(inner, edges, loopBodyIds)`), but a
21
+ * body is closed under reachability and the inner iterator is in the outer
22
+ * body — so everything reachable from the inner `item` is already in the outer
23
+ * one, and the narrowing never removes anything. A node inside two loops is
24
+ * therefore simply a member of both bodies, which is what `depth` counts.
25
+ */
26
+ /** The iterator's repeating output — the port the body hangs off. */
27
+ export declare const ITEM_PORT = "item";
28
+ /** The iterator's post-loop output — everything off it runs once, at the end. */
29
+ export declare const DONE_PORT = "done";
30
+ /**
31
+ * Nodes known to iterate while no manifest says so.
32
+ *
33
+ * The worker learns which nodes iterate at bundle-build time by duck-typing the
34
+ * implementation (`builder/src/generateEntry.ts` → `MANIFEST.iterationNodeIds`),
35
+ * and nothing of that reaches the studio: `GET /nodes` publishes the manifest,
36
+ * and no field in it says "this node repeats what is wired to it". Until one
37
+ * does — {@link NodeManifest.iteration}, read first by {@link manifestIterates}
38
+ * — the editor has to know the published iterator by name. One slug, in one
39
+ * place, rather than the same string spelled out at every call site.
40
+ */
41
+ export declare const ITERATOR_SLUGS: readonly string[];
42
+ /**
43
+ * Whether a manifest describes a node that runs its `item` side per element.
44
+ *
45
+ * A manifest that carries the field at all is believed either way — `false`
46
+ * defeats the slug fallback. Otherwise the day the service starts publishing it,
47
+ * a published `iteration: false` on the very slug below would be overruled by
48
+ * this file, which is the one answer the field exists to give.
49
+ */
50
+ export declare function manifestIterates(manifest: NodeManifest | null | undefined): boolean;
51
+ /** The blob's iterator nodes, in blob order. */
52
+ export declare function iteratorNodeIds(nodes: readonly WorkflowNode[]): string[];
53
+ /** One iterator's body: what repeats, and where it overlaps the `done` side. */
54
+ export interface LoopBody {
55
+ /** The iterator whose `item` port this body hangs off. */
56
+ iteratorId: string;
57
+ /** Every node the engine would run once per element. */
58
+ nodeIds: Set<string>;
59
+ /**
60
+ * Body nodes that are ALSO reachable from `done` — the author wired the body
61
+ * into the part meant to run after the loop, and the engine repeats it.
62
+ *
63
+ * Only the join case is provable. Where `done` is wired nowhere, a node
64
+ * appended to the body is indistinguishable from a node meant to be in it, so
65
+ * nothing here reports it (`specs/editor-loop-body.md` § Gaps). The mark on
66
+ * the card is what covers that case: it says "this repeats" wherever it is
67
+ * true, whether or not anything can tell it was a mistake.
68
+ */
69
+ joinedWithDone: Set<string>;
70
+ }
71
+ /**
72
+ * One body per iterator id, in the order given.
73
+ *
74
+ * Pure on purpose — the catalogue lookup that decides what an iterator IS lives
75
+ * in {@link iteratorNodeIds}, so the walk can be tested against a graph alone.
76
+ */
77
+ export declare function computeLoopBodies(iteratorIds: readonly string[], edges: readonly WorkflowEdge[]): LoopBody[];
78
+ /** What one node card has to say about repeating. */
79
+ export interface LoopBodyMark {
80
+ /**
81
+ * How many bodies this node is in — 1 for a plain loop, 2 inside a nested
82
+ * one. The card shows the number from 2 up: "repeats" is the news at depth 1,
83
+ * "repeats per element of both" is the news above it.
84
+ */
85
+ depth: number;
86
+ /** The iterators, outermost first (a larger body encloses a smaller one). */
87
+ iteratorIds: string[];
88
+ }
89
+ /**
90
+ * Marks for a whole canvas: which nodes repeat, and the bodies behind them.
91
+ *
92
+ * **Nodes only, deliberately — the connections carry nothing.** Tinting the
93
+ * body's edges was the obvious other half, and it is the one thing this could
94
+ * not do: `specs/editor-output-paths.md` AC-2 promises that the lines carry no
95
+ * meaning and stay neutral, and colour and dashing on an edge are both reserved
96
+ * for a run that actually happened (`docs/editor-path-colors.md`). A body drawn
97
+ * in a colour of its own would take that promise back for every workflow with a
98
+ * loop in it.
99
+ */
100
+ export interface LoopBodyMarks {
101
+ byNode: Map<string, LoopBodyMark>;
102
+ bodies: LoopBody[];
103
+ }
104
+ /**
105
+ * The whole computation for one blob, ready to hand to a canvas.
106
+ *
107
+ * Called by all three canvases (editor, run replay, history preview) on every
108
+ * sync: the marks depend on the edges, so anything less would let a rewired
109
+ * body keep an old mark. The cost is one pass over the edges to build the
110
+ * adjacency map, then two breadth-first walks per iterator over it — and a
111
+ * workflow with no iterator in it costs one filter over the nodes.
112
+ */
113
+ export declare function loopBodyMarks(nodes: readonly WorkflowNode[], edges: readonly WorkflowEdge[]): LoopBodyMarks;
114
+ /** What one canvas hands a node card about repetition. */
115
+ export interface NodeLoopData {
116
+ /** The iterators whose body this node is in, outermost first, by label. */
117
+ inside?: string[];
118
+ /** Set on an iterator itself: how many nodes hang off its `item` port. */
119
+ bodySize?: number;
120
+ }
121
+ /**
122
+ * One call per canvas sync: node id → what its card says about repeating.
123
+ *
124
+ * Names rather than ids, because the card shows the sentence — and the same
125
+ * three steps a banner uses to name a node ({@link nodeDisplayLabel}), so a
126
+ * loop the user renamed is named that everywhere. Absent from the map means
127
+ * nothing to say, which is the common case and costs the card nothing.
128
+ */
129
+ export declare function nodeLoopData(nodes: readonly WorkflowNode[], edges: readonly WorkflowEdge[]): Map<string, NodeLoopData>;
@@ -0,0 +1,76 @@
1
+ import { nodeDisplayLabel } from "../utils/validationFindings.js";
2
+ import { findManifest } from "./integrationNodeCatalog.js";
3
+ export const ITEM_PORT = "item";
4
+ export const DONE_PORT = "done";
5
+ export const ITERATOR_SLUGS = ["revenexx:iterator"];
6
+ export function manifestIterates(manifest) {
7
+ if (!manifest) return false;
8
+ if (manifest.iteration !== void 0) return manifest.iteration;
9
+ return ITERATOR_SLUGS.includes(manifest.slug);
10
+ }
11
+ export function iteratorNodeIds(nodes) {
12
+ return nodes.filter((n) => manifestIterates(findManifest(n.nodeSlug, n.nodeVersion))).map((n) => n.id);
13
+ }
14
+ function successors(edges) {
15
+ const out = /* @__PURE__ */ new Map();
16
+ for (const edge of edges) {
17
+ const list = out.get(edge.from.nodeId);
18
+ if (list) list.push(edge.to.nodeId);
19
+ else out.set(edge.from.nodeId, [edge.to.nodeId]);
20
+ }
21
+ return out;
22
+ }
23
+ function reachableFromPort(fromNodeId, output, edges, next = successors(edges)) {
24
+ const visited = /* @__PURE__ */ new Set();
25
+ const queue = [];
26
+ for (const edge of edges) {
27
+ if (edge.from.nodeId === fromNodeId && edge.from.output === output) queue.push(edge.to.nodeId);
28
+ }
29
+ for (let head = 0; head < queue.length; head++) {
30
+ const nodeId = queue[head];
31
+ if (visited.has(nodeId)) continue;
32
+ visited.add(nodeId);
33
+ for (const target of next.get(nodeId) ?? []) {
34
+ if (!visited.has(target)) queue.push(target);
35
+ }
36
+ }
37
+ return visited;
38
+ }
39
+ export function computeLoopBodies(iteratorIds, edges) {
40
+ const next = successors(edges);
41
+ return iteratorIds.map((iteratorId) => {
42
+ const nodeIds = reachableFromPort(iteratorId, ITEM_PORT, edges, next);
43
+ const afterLoop = reachableFromPort(iteratorId, DONE_PORT, edges, next);
44
+ const joinedWithDone = new Set([...nodeIds].filter((id) => afterLoop.has(id)));
45
+ return { iteratorId, nodeIds, joinedWithDone };
46
+ });
47
+ }
48
+ export function loopBodyMarks(nodes, edges) {
49
+ const bodies = computeLoopBodies(iteratorNodeIds(nodes), edges);
50
+ const byNode = /* @__PURE__ */ new Map();
51
+ const outermostFirst = [...bodies].sort((a, b) => b.nodeIds.size - a.nodeIds.size);
52
+ for (const body of outermostFirst) {
53
+ for (const nodeId of body.nodeIds) {
54
+ const mark = byNode.get(nodeId) ?? { depth: 0, iteratorIds: [] };
55
+ mark.depth += 1;
56
+ mark.iteratorIds.push(body.iteratorId);
57
+ byNode.set(nodeId, mark);
58
+ }
59
+ }
60
+ return { byNode, bodies };
61
+ }
62
+ export function nodeLoopData(nodes, edges) {
63
+ const marks = loopBodyMarks(nodes, edges);
64
+ if (!marks.bodies.length) return /* @__PURE__ */ new Map();
65
+ const labelOf = new Map(nodes.map((n) => [n.id, nodeDisplayLabel(n)]));
66
+ const out = /* @__PURE__ */ new Map();
67
+ for (const [nodeId, mark] of marks.byNode) {
68
+ out.set(nodeId, { inside: mark.iteratorIds.map((id) => labelOf.get(id) ?? id) });
69
+ }
70
+ for (const body of marks.bodies) {
71
+ const entry = out.get(body.iteratorId) ?? {};
72
+ entry.bodySize = body.nodeIds.size;
73
+ out.set(body.iteratorId, entry);
74
+ }
75
+ return out;
76
+ }
@@ -71,6 +71,8 @@
71
71
  "integration.canvas.node.action.duplicateAria": "Node duplizieren",
72
72
  "integration.canvas.node.badge.invalidConfig": "Konfiguration unvollständig — {n} Einstellung muss noch behoben werden: {fields} | Konfiguration unvollständig — {n} Einstellungen müssen noch behoben werden: {fields}",
73
73
  "integration.canvas.node.badge.noBuiltCode": "Version {version} ist nicht einsatzbereit — dieser Schritt lässt sich nicht testen",
74
+ "integration.canvas.node.badge.repeats": "Wiederholt sich — läuft einmal pro Element von {iterators} | Wiederholt sich — läuft einmal pro Element jeder der {n} Schleifen, in denen er liegt: {iterators}",
75
+ "integration.canvas.node.badge.loopBody": "Schleife — am Ausgang „Element“ hängt noch nichts, es wiederholt sich also nichts | Schleife — wiederholt den einen Schritt am Ausgang „Element“, einmal pro Element | Schleife — wiederholt die {n} Schritte am Ausgang „Element“, einmal pro Element",
74
76
  "integration.canvas.node.handle.input": "Eingang",
75
77
  "integration.canvas.node.meta.versionTitle": "Version {version}",
76
78
  "integration.canvas.node.note.heading": "Notiz",
@@ -259,7 +261,7 @@
259
261
  "integration.dashboard.action.newWorkflow": "Workflow anlegen",
260
262
  "integration.dashboard.attention.failed": "Fehlgeschlagen.",
261
263
  "integration.dashboard.attention.healthy": "Alle Workflows in Ordnung.",
262
- "integration.dashboard.attention.missedRuns": "{n} verpasste Ausführung | {n} verpasste Ausführungen",
264
+ "integration.dashboard.attention.missedRuns": "{n} verpasster Run | {n} verpasste Runs",
263
265
  "integration.dashboard.attention.missedRuns.hint": "Trigger haben ausgelöst, während ihr Workflow nicht laufen konnte — prüfen und erneut ausführen.",
264
266
  "integration.dashboard.attention.title": "Erfordert Aufmerksamkeit",
265
267
  "integration.dashboard.head.title": "Dashboard · Integration Studio",
@@ -268,14 +270,14 @@
268
270
  "integration.dashboard.kpi.activeWorkflows.label": "Aktive Workflows",
269
271
  "integration.dashboard.kpi.activeWorkflows.sub": "{n} pausiert oder Entwurf | {n} pausiert oder Entwürfe",
270
272
  "integration.dashboard.kpi.aria.group": "Kennzahlen",
271
- "integration.dashboard.kpi.missedRuns.label": "Verpasste Ausführungen",
272
- "integration.dashboard.kpi.missedRuns.sub": "Trigger-Auslösungen ohne Ausführung",
273
+ "integration.dashboard.kpi.missedRuns.label": "Verpasste Runs",
274
+ "integration.dashboard.kpi.missedRuns.sub": "Trigger-Auslösungen ohne Run",
273
275
  "integration.dashboard.kpi.runningNow.label": "Läuft gerade",
274
- "integration.dashboard.kpi.runningNow.sub": "laufende Ausführungen",
276
+ "integration.dashboard.kpi.runningNow.sub": "laufende Runs",
275
277
  "integration.dashboard.kpi.scheduled.label": "Geplant",
276
278
  "integration.dashboard.kpi.scheduled.sub": "aktive Zeitplan-Trigger",
277
- "integration.dashboard.runs.empty": "Noch keine Ausführungen.",
278
- "integration.dashboard.runs.title": "Letzte Ausführungen",
279
+ "integration.dashboard.runs.empty": "Noch keine Runs.",
280
+ "integration.dashboard.runs.title": "Letzte Runs",
279
281
  "integration.dashboard.surface.events.hint": "Live-Events der Plattform, die Workflows abonnieren.",
280
282
  "integration.dashboard.surface.events.label": "Event-Stream",
281
283
  "integration.dashboard.surface.io.hint": "Profile, die Katalog- und Entitätsdaten hinein- und hinausbewegen.",
@@ -298,12 +300,17 @@
298
300
  "integration.deadLetters.panel.title": "Verpasste Runs",
299
301
  "integration.deadLetters.reason.fallback": "Kein Grund erfasst.",
300
302
  "integration.deadLetters.state.empty": "Keine verpassten Runs. Jeder Trigger hat einen Run ausgelöst.",
303
+ "integration.deadLetters.status.discarded": "verworfen",
304
+ "integration.deadLetters.status.pending": "wartet",
305
+ "integration.deadLetters.status.replayed": "wiederholt",
306
+ "integration.deadLetters.status.unknown": "unbekannt ({status})",
301
307
  "integration.deadLetters.tabs.missed.label": "Verpasste Runs",
302
308
  "integration.deadLetters.toast.discard.done": "Verworfen",
303
309
  "integration.deadLetters.toast.discard.error": "Verwerfen fehlgeschlagen.",
304
310
  "integration.deadLetters.toast.replay.done": "Wiederholt",
305
311
  "integration.deadLetters.toast.replay.error": "Wiederholen fehlgeschlagen.",
306
312
  "integration.deadLetters.toast.replay.started": "Wiederholt — Run #{id} gestartet",
313
+ "integration.deadLetters.workflow.fallback": "Workflow #{id}",
307
314
  "integration.editor.action.activate.title": "Workflow aktivieren",
308
315
  "integration.editor.action.activate.title.invalid": "{n} Node-Einstellung vor der Aktivierung beheben | {n} Node-Einstellungen vor der Aktivierung beheben",
309
316
  "integration.editor.action.backToWorkflow": "Zurück zum Workflow",
@@ -326,12 +333,12 @@
326
333
  "integration.editor.breadcrumb.workflows": "Workflows",
327
334
  "integration.editor.build.chip.building": "Build wird erstellt…",
328
335
  "integration.editor.build.chip.queued": "Build in Warteschlange…",
329
- "integration.editor.build.notice.building": "Der Workflow-Build läuft noch — Ausführungen sind erst möglich, wenn er fertig ist.",
336
+ "integration.editor.build.notice.building": "Der Workflow-Build läuft noch — Runs sind erst möglich, wenn er fertig ist.",
330
337
  "integration.editor.build.notice.failed": "Der Workflow-Build ist fehlgeschlagen. Speichern Sie den Workflow erneut, um den Build neu zu starten.",
331
338
  "integration.editor.build.notice.failedWithError": "Der Workflow-Build ist fehlgeschlagen: {error} Speichern Sie den Workflow erneut, um den Build neu zu starten.",
332
- "integration.editor.build.notice.pending": "Der Build des Workflows ist in der Warteschlange — Ausführungen sind erst möglich, wenn er fertig ist.",
333
- "integration.editor.build.toast.failed": "Der Workflow-Build ist fehlgeschlagen — die Ausführung wurde nicht gestartet.",
334
- "integration.editor.build.toast.timeout": "Der Build dauert ungewöhnlich lange — versuchen Sie die Ausführung erneut, sobald er bereit ist.",
339
+ "integration.editor.build.notice.pending": "Der Build des Workflows ist in der Warteschlange — Runs sind erst möglich, wenn er fertig ist.",
340
+ "integration.editor.build.toast.failed": "Der Workflow-Build ist fehlgeschlagen — der Run wurde nicht gestartet.",
341
+ "integration.editor.build.toast.timeout": "Der Build dauert ungewöhnlich lange — versuchen Sie den Run erneut, sobald er bereit ist.",
335
342
  "integration.editor.build.tooltip.failed": "Der Workflow-Build ist fehlgeschlagen.",
336
343
  "integration.editor.clipboard.error.empty": "Die Zwischenablage enthält keine Nodes.",
337
344
  "integration.editor.clipboard.error.read": "Zwischenablage konnte nicht gelesen werden.",
@@ -348,13 +355,13 @@
348
355
  "integration.editor.emptyState.hint": "Ziehen Sie einen Trigger und eine Aktion aus dem Panel links.",
349
356
  "integration.editor.emptyState.title": "Leere Arbeitsfläche",
350
357
  "integration.editor.event.toast.noneReceived": "Noch kein {topic}-Event empfangen.",
351
- "integration.editor.executionMode.async.hint": "Ausführungen werden immer in die Warteschlange gestellt; der Trigger antwortet sofort.",
358
+ "integration.editor.executionMode.async.hint": "Runs werden immer in die Warteschlange gestellt; der Trigger antwortet sofort.",
352
359
  "integration.editor.executionMode.async.label": "Nur asynchron",
353
360
  "integration.editor.executionMode.async.short": "Asynchron",
354
- "integration.editor.executionMode.callerDecides.hint": "Der Aufrufer entscheidet pro Ausführung zwischen synchron und asynchron.",
361
+ "integration.editor.executionMode.callerDecides.hint": "Der Aufrufer entscheidet pro Run zwischen synchron und asynchron.",
355
362
  "integration.editor.executionMode.callerDecides.label": "Aufrufer entscheidet",
356
363
  "integration.editor.executionMode.error.generic": "Der Ausführungsmodus konnte nicht aktualisiert werden.",
357
- "integration.editor.executionMode.sync.hint": "Ausführungen blockieren, bis sie abgeschlossen sind, und liefern das Ergebnis direkt zurück.",
364
+ "integration.editor.executionMode.sync.hint": "Runs blockieren, bis sie abgeschlossen sind, und liefern das Ergebnis direkt zurück.",
358
365
  "integration.editor.executionMode.sync.label": "Nur synchron",
359
366
  "integration.editor.executionMode.sync.short": "Synchron",
360
367
  "integration.editor.group.defaultLabel": "Gruppe {n}",
@@ -387,15 +394,15 @@
387
394
  "integration.editor.quickConnect.toast": "Node auswählen — er wird dort platziert und ist bereits verbunden.",
388
395
  "integration.editor.reauth.action.fix": "in den Zugangsdaten beheben",
389
396
  "integration.editor.reauth.banner": "{n} hier verwendeter Zugangsdaten-Eintrag muss neu verbunden werden | {n} hier verwendete Zugangsdaten-Einträge müssen neu verbunden werden",
390
- "integration.editor.retry.error.generic": "Die Ausführung konnte nicht wiederholt werden.",
397
+ "integration.editor.retry.error.generic": "Der Run konnte nicht wiederholt werden.",
391
398
  "integration.editor.retry.toast.requested": "Wiederholung angefordert",
392
399
  "integration.editor.retry.toast.started": "Run #{id} gestartet",
393
400
  "integration.editor.run.button": "Run",
394
- "integration.editor.run.error.couldNotStart": "Die Ausführung konnte nicht gestartet werden.",
395
- "integration.editor.run.error.loadOpenRun": "Die Ausführung konnte nicht geladen werden.",
401
+ "integration.editor.run.error.couldNotStart": "Der Run konnte nicht gestartet werden.",
402
+ "integration.editor.run.error.loadOpenRun": "Der Run konnte nicht geladen werden.",
396
403
  "integration.editor.run.error.refusedForSettings": "Kann nicht ausgeführt werden — {summary}{where}",
397
- "integration.editor.run.error.syncFailed": "Synchrone Ausführung fehlgeschlagen",
398
- "integration.editor.run.error.syncTimeout": "Synchrone Ausführung: Zeitüberschreitung",
404
+ "integration.editor.run.error.syncFailed": "Synchroner Run fehlgeschlagen",
405
+ "integration.editor.run.error.syncTimeout": "Synchroner Run: Zeitüberschreitung",
399
406
  "integration.editor.run.loading": "Run wird geladen…",
400
407
  "integration.editor.run.waitingForBuild": "Warten auf Build…",
401
408
  "integration.editor.runPayload.action.latestEvent": "Letztes reales Event",
@@ -404,25 +411,25 @@
404
411
  "integration.editor.runPayload.action.sampleEvent": "Beispiel-Event",
405
412
  "integration.editor.runPayload.action.sampleEvent.titleAvailable": "Beispiel-Payload aus dem Katalog einfügen",
406
413
  "integration.editor.runPayload.action.sampleEvent.titleUnavailable": "Der Event-Katalog enthält kein Beispiel für dieses Topic.",
407
- "integration.editor.runPayload.dialog.description": "Startet eine manuelle Ausführung mit diesem Trigger-Payload. Das Ergebnis erscheint im Tab Runs.",
414
+ "integration.editor.runPayload.dialog.description": "Startet einen manuellen Run mit diesem Trigger-Payload. Das Ergebnis erscheint im Tab Runs.",
408
415
  "integration.editor.runPayload.dialog.title": "Mit Payload ausführen",
409
416
  "integration.editor.runPayload.prefill.label": "Vorbelegen mit",
410
417
  "integration.editor.runView.details": "Details",
411
418
  "integration.editor.runView.replay": "Replay",
412
419
  "integration.editor.runView.timeline": "Zeitleiste",
413
- "integration.editor.runs.empty": "Noch keine Ausführungen — im Editor auf Run klicken.",
414
- "integration.editor.runs.error.load": "Ausführungen konnten nicht geladen werden.",
415
- "integration.editor.runs.pagination.count": "{n} Ausführung | {n} Ausführungen",
420
+ "integration.editor.runs.empty": "Noch keine Runs — im Editor auf Run klicken.",
421
+ "integration.editor.runs.error.load": "Runs konnten nicht geladen werden.",
422
+ "integration.editor.runs.pagination.count": "{n} Run | {n} Runs",
416
423
  "integration.editor.runs.pagination.newer": "← Neuer",
417
424
  "integration.editor.runs.pagination.older": "Älter →",
418
425
  "integration.editor.runs.pagination.page": "Seite {page} / {lastPage}",
419
426
  "integration.editor.save.error.generic": "Speichern fehlgeschlagen.",
420
- "integration.editor.splice.error.terminal": "{name} beendet die Ausführung und kann nicht in eine Verbindung eingefügt werden.",
427
+ "integration.editor.splice.error.terminal": "{name} beendet den Run und kann nicht in eine Verbindung eingefügt werden.",
421
428
  "integration.editor.splice.toast": "Node aus der Palette auswählen — er wird in die Verbindung eingefügt.",
422
429
  "integration.editor.state.loading": "Wird geladen…",
423
430
  "integration.editor.status.unsavedChanges": "Nicht gespeicherte Änderungen",
424
431
  "integration.editor.sticky.starterContent": "## Ich bin eine Notiz\n**Doppelklicken**, um mich zu bearbeiten. **Fett**, *kursiv*, `Code`, [Links](https://revenexx.com) und\n- Listen funktionieren",
425
- "integration.editor.tab.deadLetters": "Verpasste Ausführungen",
432
+ "integration.editor.tab.deadLetters": "Verpasste Runs",
426
433
  "integration.editor.tab.editor": "Editor",
427
434
  "integration.editor.tab.history": "Verlauf",
428
435
  "integration.editor.tab.runs": "Runs",
@@ -435,7 +442,7 @@
435
442
  "integration.editor.tool.fitView.tooltip": "Ansicht einpassen",
436
443
  "integration.editor.tool.hand": "Hand (Verschieben)",
437
444
  "integration.editor.tool.run.tooltip": "Workflow ausführen",
438
- "integration.editor.tool.run.tooltipWaiting": "Der Build läuft noch — die Ausführung startet, sobald er bereit ist",
445
+ "integration.editor.tool.run.tooltipWaiting": "Der Build läuft noch — der Run startet, sobald er bereit ist",
439
446
  "integration.editor.tool.select": "Auswählen",
440
447
  "integration.editor.tool.settings.label": "Einstellungen",
441
448
  "integration.editor.tool.settings.tooltip": "Workflow-Einstellungen",
@@ -513,6 +520,7 @@
513
520
  "integration.nodeConfig.dynamicSchema.resolving": "Felder werden geladen…",
514
521
  "integration.nodeConfig.expression.badge.title": "Akzeptiert ${{ … }}-Ausdrücke",
515
522
  "integration.nodeConfig.expression.insertTitle": "Ausdruck einfügen",
523
+ "integration.nodeConfig.expression.insertAria": "Ausdruck einfügen — verfügbare Daten für diese Einstellung",
516
524
  "integration.nodeConfig.expression.picker.aria": "Verfügbare Daten für diese Einstellung",
517
525
  "integration.nodeConfig.expression.picker.empty": "Nichts entspricht „{query}“.",
518
526
  "integration.nodeConfig.expression.picker.filterAria": "Verfügbare Daten filtern",
@@ -622,6 +630,10 @@
622
630
  "integration.nodeInspector.notice.versionNotReady.none": "Version {version} dieses Nodes ist nicht mehr einsatzbereit, und es liegt keine einsatzbereite Version vor. Dieser Schritt lässt sich nicht testen, und Auswahllisten in seinen Einstellungen bleiben leer.",
623
631
  "integration.nodeInspector.notice.versionNotReady.upgrade": "Version {version} dieses Nodes ist nicht mehr einsatzbereit. Um den Node weiter zu verwenden, ist ein Upgrade auf Version {ready} notwendig.",
624
632
  "integration.nodeInspector.output.description": "Was nachgelagerte Nodes von diesem erhalten. Deklariert vom Node-Typ; wird mit echten Werten gefüllt, sobald der Schritt gelaufen ist.",
633
+ "integration.nodeInspector.output.loopBody.heading": "Der Schleifenkörper",
634
+ "integration.nodeInspector.output.loopBody.size": "Am Ausgang „Element“ hängt noch nichts, diese Schleife wiederholt also nichts. | Ein Schritt hängt am Ausgang „Element“ und läuft einmal pro Element. | {n} Schritte hängen am Ausgang „Element“ und laufen jeder einmal pro Element.",
635
+ "integration.nodeInspector.output.loopBody.names": "Diese wiederholen sich: {names}",
636
+ "integration.nodeInspector.output.loopBody.joined": "Einer davon hängt zugleich hinter der Schleife, am Ausgang „Fertig“ — er wiederholt sich also mit dem Schleifenkörper, statt einmal zu laufen: {names} | {n} davon hängen zugleich hinter der Schleife, am Ausgang „Fertig“ — sie wiederholen sich also mit dem Schleifenkörper, statt einmal zu laufen: {names}",
625
637
  "integration.nodeInspector.output.pin.editAction": "Angeheftetes Beispiel bearbeiten",
626
638
  "integration.nodeInspector.output.pin.editorAria": "Angeheftete Beispielausgabe",
627
639
  "integration.nodeInspector.output.pin.hint": "Ein angeheftetes Beispiel steht anstelle von Run-Daten, während Sie nachgelagerte Felder zuordnen.",
@@ -801,8 +813,8 @@
801
813
  "integration.replay.debug.stepCounter": "Schritt {current} / {total}",
802
814
  "integration.replay.debug.titleFallback": "Debug",
803
815
  "integration.replay.error.loadStepsFailed": "Die Run-Schritte konnten nicht geladen werden.",
804
- "integration.replay.runsList.description": "Klicken Sie sich durch die Ausführungen, um sie auf dem Canvas abzuspielen.",
805
- "integration.replay.runsList.empty": "Noch keine Runs — lösen Sie einen über die Schaltfläche „Ausführen“ im Editor aus.",
816
+ "integration.replay.runsList.description": "Klicken Sie sich durch die Runs, um sie auf dem Canvas abzuspielen.",
817
+ "integration.replay.runsList.empty": "Noch keine Runs — im Editor auf Run klicken.",
806
818
  "integration.replay.runsList.heading": "Runs",
807
819
  "integration.replay.runsList.loadMore": "Ältere Runs laden",
808
820
  "integration.replay.state.noSteps": "Für diesen Run wurden keine Schritte aufgezeichnet.",
@@ -818,7 +830,7 @@
818
830
  "integration.runs.action.back": "Zurück",
819
831
  "integration.runs.action.cancel": "Run stoppen",
820
832
  "integration.runs.action.resume": "Fortsetzen",
821
- "integration.runs.action.retry": "Retry",
833
+ "integration.runs.action.retry": "Erneut ausführen",
822
834
  "integration.runs.action.terminate": "Terminieren",
823
835
  "integration.runs.confirm.cancel": "Diesen Run stoppen?",
824
836
  "integration.runs.confirm.resume": "Diesen Run fortsetzen?",
@@ -878,9 +890,9 @@
878
890
  "integration.runs.list.header.title": "Runs",
879
891
  "integration.runs.menu.aria.actions": "Aktionen für Run #{id}",
880
892
  "integration.runs.menu.cancel": "Run stoppen",
881
- "integration.runs.menu.replay": "Wiederholen",
893
+ "integration.runs.menu.replay": "Wiedergabe",
882
894
  "integration.runs.menu.resume": "Fortsetzen",
883
- "integration.runs.menu.retry": "Retry",
895
+ "integration.runs.menu.retry": "Erneut ausführen",
884
896
  "integration.runs.menu.terminate": "Terminieren",
885
897
  "integration.runs.menu.viewDetails": "Details anzeigen",
886
898
  "integration.runs.notice.requested": "{action} angefordert.",
@@ -948,6 +960,7 @@
948
960
  "integration.schedules.cron.weekday.tuesday": "Dienstags",
949
961
  "integration.schedules.cron.weekday.wednesday": "Mittwochs",
950
962
  "integration.schedules.cron.weeklyAt": "{days} um {time}",
963
+ "integration.schedules.error.loadFailed": "Zeitpläne konnten nicht geladen werden.",
951
964
  "integration.schedules.head.title": "Zeitpläne · Integration Studio",
952
965
  "integration.schedules.header.description": "Jeder Zeitplan-Trigger aus allen Ihren Workflows. Klicken Sie auf eine Zeile, um den Workflow zu öffnen.",
953
966
  "integration.schedules.header.title": "Zeitpläne",
@@ -956,8 +969,8 @@
956
969
  "integration.schedules.state.empty": "Noch keine Zeitpläne. Fügen Sie einem Workflow einen Zeitplan-Trigger hinzu.",
957
970
  "integration.schedules.status.live": "aktiv",
958
971
  "integration.schedules.status.paused": "pausiert",
959
- "integration.schedules.table.column.lastRun": "Letzte Ausführung",
960
- "integration.schedules.table.column.nextRun": "Nächste Ausführung",
972
+ "integration.schedules.table.column.lastRun": "Letzter Run",
973
+ "integration.schedules.table.column.nextRun": "Nächster Run",
961
974
  "integration.schedules.table.column.status": "Status",
962
975
  "integration.schedules.table.column.timezone": "Zeitzone",
963
976
  "integration.schedules.table.column.when": "Wann",
@@ -972,7 +985,7 @@
972
985
  "integration.secrets.cell.updated.aria": "Aktualisiert für {key}: {date}",
973
986
  "integration.secrets.dialog.create.description": "Der Wert wird nach dem Speichern mit AES-256-GCM verschlüsselt und nie wieder ausgegeben. Sie können ihn später überschreiben, aber um ihn zu lesen, müssen Sie ihn bereits kennen.",
974
987
  "integration.secrets.dialog.create.title": "Secret anlegen",
975
- "integration.secrets.dialog.delete.body": "wird endgültig entfernt. Workflows, die darauf verweisen, funktionieren beim nächsten Lauf nicht mehr.",
988
+ "integration.secrets.dialog.delete.body": "wird endgültig entfernt. Workflows, die darauf verweisen, funktionieren beim nächsten Run nicht mehr.",
976
989
  "integration.secrets.dialog.delete.title": "Secret löschen?",
977
990
  "integration.secrets.error.invalidKey": "Der Schlüssel muss dem Muster ^[A-Z0-9_]+$ entsprechen.",
978
991
  "integration.secrets.error.loadFailed": "Secrets konnten nicht geladen werden.",