@epistola.app/valtimo-plugin 0.9.3 → 0.10.0

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.
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { Injectable, EventEmitter, Output, Input, Component, ChangeDetectionStrategy, inject, NgModule, ENVIRONMENT_INITIALIZER, Injector } from '@angular/core';
3
3
  import * as i1 from '@angular/common/http';
4
- import { HttpHeaders, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
4
+ import { HttpHeaders, HttpClientModule } from '@angular/common/http';
5
5
  import * as i2 from '@valtimo/shared';
6
6
  import { ROLE_ADMIN } from '@valtimo/shared';
7
7
  import { of, BehaviorSubject, combineLatest, take, Subject, debounceTime, takeUntil, merge } from 'rxjs';
@@ -17,11 +17,11 @@ import { FormsModule } from '@angular/forms';
17
17
  import * as _jsonata from 'jsonata';
18
18
  import * as i2$3 from '@valtimo/process-link';
19
19
  import * as i2$4 from '@angular/platform-browser';
20
- import * as i5 from '@formio/angular';
20
+ import * as i4 from '@formio/angular';
21
21
  import { FormioModule } from '@formio/angular';
22
22
  import * as i2$5 from '@angular/router';
23
23
  import { RouterModule, Router } from '@angular/router';
24
- import * as i5$1 from 'carbon-components-angular/tabs';
24
+ import * as i5 from 'carbon-components-angular/tabs';
25
25
  import { TabsModule } from 'carbon-components-angular/tabs';
26
26
  import * as i6 from 'carbon-components-angular/tag';
27
27
  import { TagModule } from 'carbon-components-angular/tag';
@@ -88,10 +88,11 @@ class EpistolaAdminService {
88
88
  return this.http.post(`${this.apiEndpoint}/pending/${encodeURIComponent(executionId)}/reconcile`, null);
89
89
  }
90
90
  /**
91
- * Get the latest BPMN race-safety validation violations across deployed
92
- * process definitions. Empty list = healthy.
91
+ * Get the latest BPMN race-safety validation report across deployed process
92
+ * definitions: the violations (empty = healthy) plus when it was last checked
93
+ * and how often it refreshes.
93
94
  */
94
- getValidationViolations() {
95
+ getValidationReport() {
95
96
  return this.http.get(`${this.apiEndpoint}/validations`);
96
97
  }
97
98
  /**
@@ -126,6 +127,19 @@ class EpistolaAdminService {
126
127
  responseType: 'blob',
127
128
  });
128
129
  }
130
+ // ---- TEMPORARY (removed in 1.0.0): task-id carrier detection + repair ----
131
+ /** Forms whose Epistola components are missing the task-id carrier. */
132
+ getFormCarrierIssues() {
133
+ return this.http.get(`${this.apiEndpoint}/forms/carrier-issues`);
134
+ }
135
+ /** Inject the task-id carrier into a single form's Epistola components. */
136
+ repairFormCarrier(formId) {
137
+ return this.http.post(`${this.apiEndpoint}/forms/${encodeURIComponent(formId)}/repair-carrier`, null);
138
+ }
139
+ /** Repair every flagged form. */
140
+ repairAllFormCarriers() {
141
+ return this.http.post(`${this.apiEndpoint}/forms/repair-carrier`, null);
142
+ }
129
143
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminService, deps: [{ token: i1.HttpClient }, { token: i2.ConfigService }], target: i0.ɵɵFactoryTarget.Injectable });
130
144
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminService });
131
145
  }
@@ -262,13 +276,13 @@ class EpistolaPluginService {
262
276
  /**
263
277
  * Get a dynamically generated Formio form for retrying a failed document generation.
264
278
  *
279
+ * The backend derives the process instance and case document from the authorized task,
280
+ * so only the task id (and optionally the source activity) is sent.
281
+ *
265
282
  * @param taskId Operaton user task id (required — backend authorizes via OperatonTask:VIEW)
266
283
  */
267
- getRetryForm(taskId, processInstanceId, documentId, sourceActivityId) {
268
- const params = { taskId, processInstanceId };
269
- if (documentId) {
270
- params['documentId'] = documentId;
271
- }
284
+ getRetryForm(taskId, sourceActivityId) {
285
+ const params = { taskId };
272
286
  if (sourceActivityId) {
273
287
  params['sourceActivityId'] = sourceActivityId;
274
288
  }
@@ -328,99 +342,96 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
328
342
  }], ctorParameters: () => [{ type: i1.HttpClient }, { type: i2.ConfigService }] });
329
343
 
330
344
  /**
331
- * Holds the currently-open Operaton user task instance id so that custom
332
- * Formio components rendered inside a Valtimo task form (preview, download,
333
- * retry-form) can include it in backend requests for PBAC.
345
+ * Helpers for reading the active user task's id out of a Valtimo task form that was
346
+ * prefilled server-side by the {@code epistola:} value resolver (see the backend
347
+ * {@code EpistolaTaskValueResolverFactory}).
334
348
  *
335
- * Populated by {@link EpistolaTaskContextInterceptor}, which sniffs Valtimo's
336
- * canonical "load process link for task" GET (`/api/v2/process-link/task/{taskId}`)
337
- * the request always fires when a task opens, before the form renders.
349
+ * <p>Background: the Epistola Formio components need the id of the user task whose form
350
+ * they're rendered in, to authorize their backend requests ({@code OperatonTask:VIEW}).
351
+ * Valtimo exposes no service that carries the task id to a custom Formio component at
352
+ * runtime, and earlier URL-sniffing only worked in the direct task-open flow (the
353
+ * task-list / case-detail flow bulk-fetches process links and never fires the per-task
354
+ * call).
338
355
  *
339
- * <p><b>Why this exists:</b> Valtimo 13.21 does not expose the active task
340
- * instance id through any service that custom Formio components can inject
341
- * (`FormIoStateService` carries documentId and processInstanceId only;
342
- * `TaskDetailContentComponent.taskInstanceId$` is private to that component
343
- * and Formio elements live in their own injector tree). This service is a
344
- * workaround until upstream exposes `taskInstanceId` via `FormIoStateService`.
345
- */
346
- class EpistolaTaskContextService {
347
- _taskInstanceId$ = new BehaviorSubject(null);
348
- taskInstanceId$ = this._taskInstanceId$.asObservable();
349
- get taskInstanceId() {
350
- return this._taskInstanceId$.value;
351
- }
352
- setTaskInstanceId(id) {
353
- this._taskInstanceId$.next(id);
354
- }
355
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
356
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextService, providedIn: 'root' });
357
- }
358
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextService, decorators: [{
359
- type: Injectable,
360
- args: [{ providedIn: 'root' }]
361
- }] });
362
-
363
- /**
364
- * Pure helpers for {@link EpistolaTaskContextInterceptor}, extracted so they
365
- * can be unit-tested without pulling in {@code @angular/core} (which ts-jest
366
- * cannot transform without {@code jest-preset-angular}).
356
+ * <p>Form prefill, however, runs server-side in every flow. A form field with
357
+ * {@code properties.sourceKey = "epistola:taskId"} is filled with the task id at prefill
358
+ * time (by the backend {@code EpistolaTaskValueResolverFactory}); this helper reads it back
359
+ * from the Formio root robustly, regardless of how the task was opened.
367
360
  */
361
+ /** The value-resolver source key that yields the current task id at prefill time. */
362
+ const PREFILLED_TASK_ID_SOURCE_KEY = 'epistola:taskId';
363
+ /** Conventional key of the hidden carrier field that holds the prefilled task id. */
364
+ const PREFILLED_TASK_ID_DATA_KEY = 'epistolaTaskId';
368
365
  /**
369
- * Pattern Valtimo uses for the canonical task-open call:
370
- * {@code GET /api/v2/process-link/task/{taskInstanceId}}.
366
+ * Hidden Formio child component that carries the prefilled task id. It is embedded as a
367
+ * nested component inside each Epistola task component's schema, so dropping that component
368
+ * brings the carrier with it — the form author never adds a separate field. Valtimo's
369
+ * server-side prefill fills its {@code defaultValue} from the {@code epistola:taskId}
370
+ * value resolver; {@link readPrefilledTaskId} reads it back from the form definition.
371
371
  *
372
- * Captures the {@code taskInstanceId} (UUID v4-style 36-character hyphenated
373
- * hex string). Anchored at the end of the URL or at a query-string delimiter
374
- * so we don't accidentally match a longer trailing segment.
372
+ * {@code persistent: false} keeps the value out of the submission, so the task id never
373
+ * lands in the case document / process variables.
375
374
  */
376
- const TASK_PROCESS_LINK_PATTERN = /\/api\/v2\/process-link\/task\/([0-9a-fA-F-]{36})(?:\?|$)/;
375
+ const PREFILLED_TASK_ID_CARRIER = {
376
+ type: 'hidden',
377
+ key: PREFILLED_TASK_ID_DATA_KEY,
378
+ input: true,
379
+ persistent: false,
380
+ label: 'Epistola Task Id',
381
+ properties: { sourceKey: PREFILLED_TASK_ID_SOURCE_KEY },
382
+ };
377
383
  /**
378
- * Returns the captured {@code taskInstanceId} from a Valtimo task-open
379
- * request URL, or {@code null} if the request does not match.
384
+ * Reads the prefilled task id from a Formio webform/wizard root, or null when absent.
385
+ *
386
+ * Looks in two places, in order:
387
+ * 1. The (prefilled) form definition — any component whose {@code properties.sourceKey}
388
+ * is {@code epistola:taskId} carries the task id in its {@code defaultValue}. This works
389
+ * even when the carrier is a hidden field that Formio doesn't surface into submission data.
390
+ * 2. The submission data under {@link PREFILLED_TASK_ID_DATA_KEY}, for a rendered sibling
391
+ * hidden field whose value Formio copied into {@code root.data}.
380
392
  */
381
- function extractTaskInstanceIdFromUrl(method, url) {
382
- if (method !== 'GET')
393
+ function readPrefilledTaskId(root) {
394
+ if (!root) {
383
395
  return null;
384
- const match = TASK_PROCESS_LINK_PATTERN.exec(url);
385
- return match ? match[1] : null;
396
+ }
397
+ const fromForm = findSourceKeyDefaultValue(root.form, PREFILLED_TASK_ID_SOURCE_KEY);
398
+ if (typeof fromForm === 'string' && fromForm.length > 0) {
399
+ return fromForm;
400
+ }
401
+ const fromData = root.data?.[PREFILLED_TASK_ID_DATA_KEY];
402
+ if (typeof fromData === 'string' && fromData.length > 0) {
403
+ return fromData;
404
+ }
405
+ return null;
386
406
  }
387
-
388
407
  /**
389
- * Sniffs Valtimo's task-open signal and pushes the active taskInstanceId into
390
- * {@link EpistolaTaskContextService}. The signal is the canonical
391
- * {@code GET /api/v2/process-link/task/{taskId}} call that
392
- * {@code TaskDetailContentComponent.loadTaskDetails(...)} fires unconditionally
393
- * before any task form is rendered (see @valtimo/task internals).
394
- *
395
- * <p>This interceptor does <b>not</b> modify the outgoing request. It only
396
- * captures the taskId from the URL.
397
- *
398
- * <p>Workaround for Valtimo 13.21 not exposing taskInstanceId through any
399
- * injectable service. Remove once upstream adds e.g.
400
- * {@code FormIoStateService.setTaskInstanceId(...)}.
401
- *
402
- * <p>The actual URL-matching logic lives in
403
- * {@link extractTaskInstanceIdFromUrl} so it can be unit-tested without an
404
- * Angular harness.
408
+ * Deep-walks a form definition node looking for a component whose
409
+ * {@code properties.sourceKey} equals {@code sourceKey}, and returns its
410
+ * {@code defaultValue} (the prefilled value). Returns null when not found.
405
411
  */
406
- class EpistolaTaskContextInterceptor {
407
- taskContext;
408
- constructor(taskContext) {
409
- this.taskContext = taskContext;
410
- }
411
- intercept(request, next) {
412
- const taskId = extractTaskInstanceIdFromUrl(request.method, request.url);
413
- if (taskId !== null && taskId !== this.taskContext.taskInstanceId) {
414
- this.taskContext.setTaskInstanceId(taskId);
412
+ function findSourceKeyDefaultValue(node, sourceKey) {
413
+ if (Array.isArray(node)) {
414
+ for (const item of node) {
415
+ const found = findSourceKeyDefaultValue(item, sourceKey);
416
+ if (found != null) {
417
+ return found;
418
+ }
419
+ }
420
+ return null;
421
+ }
422
+ if (node && typeof node === 'object') {
423
+ if (node.properties?.sourceKey === sourceKey && typeof node.defaultValue === 'string') {
424
+ return node.defaultValue;
425
+ }
426
+ for (const key of Object.keys(node)) {
427
+ const found = findSourceKeyDefaultValue(node[key], sourceKey);
428
+ if (found != null) {
429
+ return found;
430
+ }
415
431
  }
416
- return next.handle(request);
417
432
  }
418
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextInterceptor, deps: [{ token: EpistolaTaskContextService }], target: i0.ɵɵFactoryTarget.Injectable });
419
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextInterceptor });
433
+ return null;
420
434
  }
421
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextInterceptor, decorators: [{
422
- type: Injectable
423
- }], ctorParameters: () => [{ type: EpistolaTaskContextService }] });
424
435
 
425
436
  class EpistolaConfigurationComponent {
426
437
  save$;
@@ -1634,6 +1645,37 @@ function expandDotNotation(flat) {
1634
1645
  }
1635
1646
  return result;
1636
1647
  }
1648
+ /**
1649
+ * A preview is "override-driven" when it has a non-empty override mapping: its
1650
+ * input data comes from the form via the mapping, so it must wait for that data
1651
+ * before it can render. Previews without a mapping load straight from the base
1652
+ * doc/case data.
1653
+ */
1654
+ function isOverrideDriven(mapping) {
1655
+ return !!mapping && Object.keys(mapping).length > 0;
1656
+ }
1657
+ /**
1658
+ * Whether the computed input overrides carry any usable data yet.
1659
+ */
1660
+ function hasUsableOverrides(overrides) {
1661
+ return !!overrides && Object.keys(overrides).length > 0;
1662
+ }
1663
+ /**
1664
+ * Decide whether a preview request should fire given the configured override
1665
+ * mapping and the currently computed overrides.
1666
+ *
1667
+ * - Override-driven previews only load once the mapped form data is present;
1668
+ * before that they show a "complete the form" placeholder and fire nothing
1669
+ * (avoids a doomed request that Epistola rejects with a 400 for missing
1670
+ * required fields).
1671
+ * - Previews without a mapping always load (base data is the whole input).
1672
+ */
1673
+ function shouldLoadPreview(mapping, overrides) {
1674
+ if (isOverrideDriven(mapping)) {
1675
+ return hasUsableOverrides(overrides);
1676
+ }
1677
+ return true;
1678
+ }
1637
1679
  /**
1638
1680
  * Given an override mapping (scope -> { inputPath -> "form:<componentKey>" })
1639
1681
  * and form data, produce the inputOverrides object for the backend.
@@ -2225,6 +2267,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2225
2267
  type: Output
2226
2268
  }] } });
2227
2269
 
2270
+ const DEFAULT_STORAGE_TARGET = 'TEMPORARY_RESOURCE';
2271
+ /** Normalize a (possibly undefined) storageTarget to a concrete target, applying the default. */
2272
+ function resolveStorageTarget(target) {
2273
+ return target === 'PROCESS_VARIABLE' ? 'PROCESS_VARIABLE' : DEFAULT_STORAGE_TARGET;
2274
+ }
2275
+ /**
2276
+ * A config is valid when the input variable and the output variable that matches the chosen
2277
+ * storage target are both set.
2278
+ */
2279
+ function isDownloadDocumentConfigValid(config) {
2280
+ if (!config?.documentVariable) {
2281
+ return false;
2282
+ }
2283
+ return resolveStorageTarget(config.storageTarget) === 'PROCESS_VARIABLE'
2284
+ ? !!config.contentVariable
2285
+ : !!config.resourceIdVariable;
2286
+ }
2287
+
2228
2288
  class DownloadDocumentConfigurationComponent {
2229
2289
  save$;
2230
2290
  disabled$;
@@ -2243,11 +2303,24 @@ class DownloadDocumentConfigurationComponent {
2243
2303
  resolvedPrefill = {};
2244
2304
  prefillResolved$ = new BehaviorSubject(false);
2245
2305
  safeDisabled$;
2306
+ /**
2307
+ * Static option set for the storage target. Values match the backend
2308
+ * {@code DocumentStorageTarget} enum constants; labels are explained further via the
2309
+ * translated field title/tooltip.
2310
+ */
2311
+ storageTargetOptions = [
2312
+ { id: 'TEMPORARY_RESOURCE', text: 'Temporary resource storage' },
2313
+ { id: 'PROCESS_VARIABLE', text: 'Process variable (inline bytes)' },
2314
+ ];
2315
+ defaultStorageTarget = DEFAULT_STORAGE_TARGET;
2316
+ /** Drives which output-variable field is shown (resource id vs inline content). */
2317
+ selectedTarget$ = new BehaviorSubject(this.defaultStorageTarget);
2246
2318
  ngOnInit() {
2247
2319
  this.safeDisabled$ = this.disabled$.pipe(startWith(true), delay(0));
2248
2320
  const prefill$ = this.prefillConfiguration$ ?? of({});
2249
2321
  prefill$.pipe(take(1)).subscribe((prefill) => {
2250
2322
  this.resolvedPrefill = prefill ?? {};
2323
+ this.selectedTarget$.next(resolveStorageTarget(this.resolvedPrefill.storageTarget));
2251
2324
  this.prefillResolved$.next(true);
2252
2325
  });
2253
2326
  this.openSaveSubscription();
@@ -2257,11 +2330,14 @@ class DownloadDocumentConfigurationComponent {
2257
2330
  }
2258
2331
  formValueChange(formOutput) {
2259
2332
  const formValue = formOutput;
2333
+ if (formValue?.storageTarget) {
2334
+ this.selectedTarget$.next(formValue.storageTarget);
2335
+ }
2260
2336
  this.formValue$.next(formValue);
2261
2337
  this.handleValid(formValue);
2262
2338
  }
2263
2339
  handleValid(formValue) {
2264
- const valid = !!(formValue?.documentVariable && formValue?.contentVariable);
2340
+ const valid = isDownloadDocumentConfigValid(formValue);
2265
2341
  this.valid$.next(valid);
2266
2342
  this.valid.emit(valid);
2267
2343
  }
@@ -2277,11 +2353,11 @@ class DownloadDocumentConfigurationComponent {
2277
2353
  });
2278
2354
  }
2279
2355
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DownloadDocumentConfigurationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2280
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: DownloadDocumentConfigurationComponent, isStandalone: true, selector: "epistola-download-document-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form (valueChange)=\"formValueChange($event)\" *ngIf=\"prefillResolved$ | async\">\n <v-input\n name=\"documentVariable\"\n [title]=\"'documentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.documentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.contentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }] });
2356
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: DownloadDocumentConfigurationComponent, isStandalone: true, selector: "epistola-download-document-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form (valueChange)=\"formValueChange($event)\" *ngIf=\"prefillResolved$ | async\">\n <v-input\n name=\"documentVariable\"\n [title]=\"'documentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.documentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-select\n name=\"storageTarget\"\n [title]=\"'storageTarget' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'storageTargetTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"storageTargetOptions\"\n [defaultSelectionId]=\"resolvedPrefill?.storageTarget || defaultStorageTarget\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-select>\n\n <v-input\n *ngIf=\"(selectedTarget$ | async) === 'TEMPORARY_RESOURCE'\"\n name=\"resourceIdVariable\"\n [title]=\"'resourceIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resourceIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.resourceIdVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n *ngIf=\"(selectedTarget$ | async) === 'PROCESS_VARIABLE'\"\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.contentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i3.SelectComponent, selector: "v-select", inputs: ["items", "defaultSelection", "defaultSelectionId", "defaultSelectionIds", "disabled", "dropUp", "invalid", "multiple", "margin", "widthInPx", "notFoundText", "clearAllText", "clearText", "clearable", "name", "title", "titleTranslationKey", "clearSelectionSubject$", "tooltip", "required", "loading", "loadingText", "placeholder", "smallMargin", "carbonTheme", "appendInline", "warn", "warnText", "dataTestId"], outputs: ["selectedChange"] }] });
2281
2357
  }
2282
2358
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DownloadDocumentConfigurationComponent, decorators: [{
2283
2359
  type: Component,
2284
- args: [{ selector: 'epistola-download-document-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule], template: "<v-form (valueChange)=\"formValueChange($event)\" *ngIf=\"prefillResolved$ | async\">\n <v-input\n name=\"documentVariable\"\n [title]=\"'documentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.documentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.contentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n" }]
2360
+ args: [{ selector: 'epistola-download-document-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule, SelectModule], template: "<v-form (valueChange)=\"formValueChange($event)\" *ngIf=\"prefillResolved$ | async\">\n <v-input\n name=\"documentVariable\"\n [title]=\"'documentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.documentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-select\n name=\"storageTarget\"\n [title]=\"'storageTarget' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'storageTargetTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"storageTargetOptions\"\n [defaultSelectionId]=\"resolvedPrefill?.storageTarget || defaultStorageTarget\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-select>\n\n <v-input\n *ngIf=\"(selectedTarget$ | async) === 'TEMPORARY_RESOURCE'\"\n name=\"resourceIdVariable\"\n [title]=\"'resourceIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resourceIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.resourceIdVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n *ngIf=\"(selectedTarget$ | async) === 'PROCESS_VARIABLE'\"\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.contentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n" }]
2285
2361
  }], propDecorators: { save$: [{
2286
2362
  type: Input
2287
2363
  }], disabled$: [{
@@ -2316,7 +2392,6 @@ class EpistolaDocumentComponent {
2316
2392
  epistolaPluginService;
2317
2393
  sanitizer;
2318
2394
  formIoStateService;
2319
- taskContext;
2320
2395
  cdr;
2321
2396
  value;
2322
2397
  valueChange = new EventEmitter();
@@ -2337,6 +2412,11 @@ class EpistolaDocumentComponent {
2337
2412
  tenantIdVariable = 'epistolaTenantId';
2338
2413
  /** Filename used for the download disposition. */
2339
2414
  filename = 'document.pdf';
2415
+ /**
2416
+ * Task id forwarded by the Formio wrapper from the server-prefilled form
2417
+ * ({@code epistola:taskId} value resolver), populated in every Valtimo task-open flow.
2418
+ */
2419
+ taskInstanceId;
2340
2420
  loading = false;
2341
2421
  downloading = false;
2342
2422
  error = null;
@@ -2346,18 +2426,21 @@ class EpistolaDocumentComponent {
2346
2426
  get designMode() {
2347
2427
  return !this.formIoStateService.documentId;
2348
2428
  }
2349
- constructor(epistolaPluginService, sanitizer, formIoStateService, taskContext, cdr) {
2429
+ constructor(epistolaPluginService, sanitizer, formIoStateService, cdr) {
2350
2430
  this.epistolaPluginService = epistolaPluginService;
2351
2431
  this.sanitizer = sanitizer;
2352
2432
  this.formIoStateService = formIoStateService;
2353
- this.taskContext = taskContext;
2354
2433
  this.cdr = cdr;
2355
2434
  }
2356
- ngOnInit() {
2357
- if (this.designMode) {
2435
+ ngOnChanges(changes) {
2436
+ if (this.designMode || this.display === 'button') {
2358
2437
  return;
2359
2438
  }
2360
- if (this.display !== 'button') {
2439
+ // The Formio wrapper sets taskInstanceId around attach, so it can arrive after the
2440
+ // first change — (re)load the inline document once it's available instead of leaving
2441
+ // the "Document is alleen beschikbaar binnen een taak" message until a manual refresh.
2442
+ // (For display="button" the download() click reads the task id on demand.)
2443
+ if (changes['taskInstanceId'] && this.taskInstanceId) {
2361
2444
  this.loadInline();
2362
2445
  }
2363
2446
  }
@@ -2424,7 +2507,7 @@ class EpistolaDocumentComponent {
2424
2507
  });
2425
2508
  }
2426
2509
  buildRequest(disposition) {
2427
- const taskId = this.taskContext.taskInstanceId;
2510
+ const taskId = this.taskInstanceId ?? null;
2428
2511
  const caseDocumentId = this.formIoStateService.documentId;
2429
2512
  if (!taskId || !caseDocumentId) {
2430
2513
  this.error = 'Document is alleen beschikbaar binnen een taak.';
@@ -2447,8 +2530,8 @@ class EpistolaDocumentComponent {
2447
2530
  this.previewUrl = null;
2448
2531
  }
2449
2532
  }
2450
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentComponent, deps: [{ token: EpistolaPluginService }, { token: i2$4.DomSanitizer }, { token: i3.FormIoStateService }, { token: EpistolaTaskContextService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2451
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaDocumentComponent, isStandalone: true, selector: "epistola-document-component", inputs: { value: "value", disabled: "disabled", label: "label", display: "display", documentVariable: "documentVariable", tenantIdVariable: "tenantIdVariable", filename: "filename" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: `
2533
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentComponent, deps: [{ token: EpistolaPluginService }, { token: i2$4.DomSanitizer }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2534
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaDocumentComponent, isStandalone: true, selector: "epistola-document-component", inputs: { value: "value", disabled: "disabled", label: "label", display: "display", documentVariable: "documentVariable", tenantIdVariable: "tenantIdVariable", filename: "filename", taskInstanceId: "taskInstanceId" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
2452
2535
  <!-- Design-time placeholder -->
2453
2536
  <div *ngIf="designMode" class="epistola-doc-panel">
2454
2537
  <div class="doc-header">
@@ -2603,7 +2686,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2603
2686
  </div>
2604
2687
  </div>
2605
2688
  `, styles: [".epistola-doc-panel{border:1px solid #dee2e6;border-radius:4px;background:#f8f9fa;display:flex;flex-direction:column}.doc-header{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;color:#495057}.doc-controls{display:flex;gap:.25rem}.doc-icon-btn{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.25rem .5rem;font-size:.9rem;cursor:pointer}.doc-icon-btn:hover:not(:disabled){background:#e9ecef}.doc-icon-btn:disabled{opacity:.5;cursor:not-allowed}.doc-body{display:flex;flex-direction:column;min-height:500px}.doc-loading,.doc-unavailable{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.doc-unavailable i{margin-right:.25rem}.doc-pdf{width:100%;flex:1;min-height:500px}.design-info{padding:1rem;min-height:auto}.design-section{margin-bottom:.5rem}.design-label{font-size:.7rem;text-transform:uppercase;color:#868e96;font-weight:600;letter-spacing:.05em}.design-value{font-family:monospace;font-size:.85rem;color:#212529;margin-bottom:.25rem}.design-tag{font-size:.7rem;font-weight:400;color:#6c757d;font-style:italic}\n"] }]
2606
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$4.DomSanitizer }, { type: i3.FormIoStateService }, { type: EpistolaTaskContextService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
2689
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$4.DomSanitizer }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
2607
2690
  type: Input
2608
2691
  }], valueChange: [{
2609
2692
  type: Output
@@ -2619,19 +2702,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2619
2702
  type: Input
2620
2703
  }], filename: [{
2621
2704
  type: Input
2705
+ }], taskInstanceId: [{
2706
+ type: Input
2622
2707
  }] } });
2623
2708
 
2624
2709
  class EpistolaRetryFormComponent {
2625
2710
  epistolaPluginService;
2626
- formIoStateService;
2627
2711
  cdr;
2628
2712
  sanitizer;
2629
- taskContext;
2630
2713
  value;
2631
2714
  valueChange = new EventEmitter();
2632
2715
  disabled = false;
2633
2716
  label = 'Document Data';
2634
2717
  sourceActivityId;
2718
+ /**
2719
+ * Task id forwarded by the Formio wrapper from the server-prefilled form
2720
+ * ({@code epistola:taskId} value resolver), populated in every Valtimo task-open flow.
2721
+ */
2722
+ taskInstanceId;
2635
2723
  formDefinition;
2636
2724
  submission;
2637
2725
  loading = true;
@@ -2646,17 +2734,14 @@ class EpistolaRetryFormComponent {
2646
2734
  previewSubject = new Subject();
2647
2735
  currentBlobUrl = null;
2648
2736
  resolvedSourceActivityId;
2649
- processDefinitionKey;
2650
2737
  formOptions = {
2651
2738
  noAlerts: true,
2652
2739
  buttonSettings: { showCancel: false, showSubmit: false, showPrevious: false, showNext: false },
2653
2740
  };
2654
- constructor(epistolaPluginService, formIoStateService, cdr, sanitizer, taskContext) {
2741
+ constructor(epistolaPluginService, cdr, sanitizer) {
2655
2742
  this.epistolaPluginService = epistolaPluginService;
2656
- this.formIoStateService = formIoStateService;
2657
2743
  this.cdr = cdr;
2658
2744
  this.sanitizer = sanitizer;
2659
- this.taskContext = taskContext;
2660
2745
  // Debounce preview calls
2661
2746
  this.previewSubscription = this.previewSubject.pipe(debounceTime$1(1500)).subscribe((data) => {
2662
2747
  this.loadPreview(data);
@@ -2666,6 +2751,13 @@ class EpistolaRetryFormComponent {
2666
2751
  if (!this.loaded) {
2667
2752
  this.loaded = true;
2668
2753
  this.loadForm();
2754
+ return;
2755
+ }
2756
+ // The Formio wrapper sets taskInstanceId after attach, so it can land after the
2757
+ // first render — if the form failed to load for lack of a task id, retry once it
2758
+ // arrives instead of leaving the "only available from within a user task" message.
2759
+ if (changes['taskInstanceId'] && this.taskInstanceId && !this.formDefinition) {
2760
+ this.loadForm();
2669
2761
  }
2670
2762
  }
2671
2763
  ngOnDestroy() {
@@ -2689,11 +2781,8 @@ class EpistolaRetryFormComponent {
2689
2781
  }
2690
2782
  }
2691
2783
  loadPreview(formData) {
2692
- const documentId = this.formIoStateService.documentId;
2693
- const processInstanceId = this.formIoStateService.processInstanceId;
2694
- if (!documentId || !processInstanceId)
2695
- return;
2696
- const taskId = this.taskContext.taskInstanceId;
2784
+ // The backend derives the process instance and case document from the task.
2785
+ const taskId = this.taskInstanceId ?? null;
2697
2786
  if (!taskId) {
2698
2787
  this.previewError = 'Preview is only available from within a user task.';
2699
2788
  this.cdr.markForCheck();
@@ -2710,8 +2799,6 @@ class EpistolaRetryFormComponent {
2710
2799
  this.epistolaPluginService
2711
2800
  .previewToBlob({
2712
2801
  taskId,
2713
- documentId,
2714
- processInstanceId,
2715
2802
  sourceActivityId: this.sourceActivityId || null,
2716
2803
  overrides: formData,
2717
2804
  })
@@ -2748,15 +2835,8 @@ class EpistolaRetryFormComponent {
2748
2835
  });
2749
2836
  }
2750
2837
  loadForm() {
2751
- const processInstanceId = this.formIoStateService.processInstanceId;
2752
- const documentId = this.formIoStateService.documentId;
2753
- if (!processInstanceId) {
2754
- this.error = 'Could not determine process instance ID.';
2755
- this.loading = false;
2756
- this.cdr.markForCheck();
2757
- return;
2758
- }
2759
- const taskId = this.taskContext.taskInstanceId;
2838
+ // The backend derives the process instance and case document from the task.
2839
+ const taskId = this.taskInstanceId ?? null;
2760
2840
  if (!taskId) {
2761
2841
  this.error = 'Retry form is only available from within a user task.';
2762
2842
  this.loading = false;
@@ -2764,7 +2844,7 @@ class EpistolaRetryFormComponent {
2764
2844
  return;
2765
2845
  }
2766
2846
  this.loadSubscription = this.epistolaPluginService
2767
- .getRetryForm(taskId, processInstanceId, documentId ?? undefined, this.sourceActivityId)
2847
+ .getRetryForm(taskId, this.sourceActivityId)
2768
2848
  .subscribe({
2769
2849
  next: (form) => {
2770
2850
  this.formDefinition = form;
@@ -2787,8 +2867,8 @@ class EpistolaRetryFormComponent {
2787
2867
  },
2788
2868
  });
2789
2869
  }
2790
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaRetryFormComponent, deps: [{ token: EpistolaPluginService }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }, { token: i2$4.DomSanitizer }, { token: EpistolaTaskContextService }], target: i0.ɵɵFactoryTarget.Component });
2791
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaRetryFormComponent, isStandalone: true, selector: "epistola-retry-form-component", inputs: { value: "value", disabled: "disabled", label: "label", sourceActivityId: "sourceActivityId" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
2870
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaRetryFormComponent, deps: [{ token: EpistolaPluginService }, { token: i0.ChangeDetectorRef }, { token: i2$4.DomSanitizer }], target: i0.ɵɵFactoryTarget.Component });
2871
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaRetryFormComponent, isStandalone: true, selector: "epistola-retry-form-component", inputs: { value: "value", disabled: "disabled", label: "label", sourceActivityId: "sourceActivityId", taskInstanceId: "taskInstanceId" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
2792
2872
  <div *ngIf="loading" class="epistola-retry-loading">Loading form...</div>
2793
2873
  <div *ngIf="error" class="epistola-retry-error">{{ error }}</div>
2794
2874
  <div
@@ -2826,7 +2906,7 @@ class EpistolaRetryFormComponent {
2826
2906
  </div>
2827
2907
  </div>
2828
2908
  </div>
2829
- `, isInline: true, styles: [".epistola-retry-loading{padding:1rem;color:#6c757d}.epistola-retry-error{padding:.5rem;color:#dc3545}.epistola-retry-container{display:flex;gap:1rem}.epistola-retry-form{flex:2;min-width:0}.epistola-retry-preview{flex:1;min-width:0;border:1px solid #dee2e6;border-radius:4px;padding:1rem;background:#f8f9fa;display:flex;flex-direction:column}.preview-expanded .epistola-retry-preview{flex:1}.preview-header{display:flex;justify-content:space-between;align-items:center;font-weight:700;margin-bottom:.5rem;color:#495057}.preview-toggle{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.2rem .5rem;font-size:.75rem;cursor:pointer}.preview-toggle:hover{background:#e9ecef}.preview-loading{color:#6c757d;font-style:italic}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-expanded .preview-pdf{min-height:80vh}.preview-error{color:#dc3545}.preview-empty{color:#6c757d;font-style:italic}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormioModule }, { kind: "component", type: i5.FormioComponent, selector: "formio" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2909
+ `, isInline: true, styles: [".epistola-retry-loading{padding:1rem;color:#6c757d}.epistola-retry-error{padding:.5rem;color:#dc3545}.epistola-retry-container{display:flex;gap:1rem}.epistola-retry-form{flex:2;min-width:0}.epistola-retry-preview{flex:1;min-width:0;border:1px solid #dee2e6;border-radius:4px;padding:1rem;background:#f8f9fa;display:flex;flex-direction:column}.preview-expanded .epistola-retry-preview{flex:1}.preview-header{display:flex;justify-content:space-between;align-items:center;font-weight:700;margin-bottom:.5rem;color:#495057}.preview-toggle{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.2rem .5rem;font-size:.75rem;cursor:pointer}.preview-toggle:hover{background:#e9ecef}.preview-loading{color:#6c757d;font-style:italic}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-expanded .preview-pdf{min-height:80vh}.preview-error{color:#dc3545}.preview-empty{color:#6c757d;font-style:italic}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormioModule }, { kind: "component", type: i4.FormioComponent, selector: "formio" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2830
2910
  }
2831
2911
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaRetryFormComponent, decorators: [{
2832
2912
  type: Component,
@@ -2869,7 +2949,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2869
2949
  </div>
2870
2950
  </div>
2871
2951
  `, styles: [".epistola-retry-loading{padding:1rem;color:#6c757d}.epistola-retry-error{padding:.5rem;color:#dc3545}.epistola-retry-container{display:flex;gap:1rem}.epistola-retry-form{flex:2;min-width:0}.epistola-retry-preview{flex:1;min-width:0;border:1px solid #dee2e6;border-radius:4px;padding:1rem;background:#f8f9fa;display:flex;flex-direction:column}.preview-expanded .epistola-retry-preview{flex:1}.preview-header{display:flex;justify-content:space-between;align-items:center;font-weight:700;margin-bottom:.5rem;color:#495057}.preview-toggle{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.2rem .5rem;font-size:.75rem;cursor:pointer}.preview-toggle:hover{background:#e9ecef}.preview-loading{color:#6c757d;font-style:italic}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-expanded .preview-pdf{min-height:80vh}.preview-error{color:#dc3545}.preview-empty{color:#6c757d;font-style:italic}\n"] }]
2872
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }, { type: i2$4.DomSanitizer }, { type: EpistolaTaskContextService }], propDecorators: { value: [{
2952
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i0.ChangeDetectorRef }, { type: i2$4.DomSanitizer }], propDecorators: { value: [{
2873
2953
  type: Input
2874
2954
  }], valueChange: [{
2875
2955
  type: Output
@@ -2879,6 +2959,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2879
2959
  type: Input
2880
2960
  }], sourceActivityId: [{
2881
2961
  type: Input
2962
+ }], taskInstanceId: [{
2963
+ type: Input
2882
2964
  }] } });
2883
2965
 
2884
2966
  class EpistolaDocumentPreviewComponent {
@@ -2886,7 +2968,6 @@ class EpistolaDocumentPreviewComponent {
2886
2968
  sanitizer;
2887
2969
  formIoStateService;
2888
2970
  cdr;
2889
- taskContext;
2890
2971
  value;
2891
2972
  valueChange = new EventEmitter();
2892
2973
  disabled = false;
@@ -2894,6 +2975,11 @@ class EpistolaDocumentPreviewComponent {
2894
2975
  processDefinitionKey;
2895
2976
  sourceActivityId;
2896
2977
  overrideMapping;
2978
+ /**
2979
+ * Task id forwarded by the Formio wrapper from the server-prefilled form
2980
+ * ({@code epistola:taskId} value resolver), populated in every Valtimo task-open flow.
2981
+ */
2982
+ taskInstanceId;
2897
2983
  loading = false;
2898
2984
  error = null;
2899
2985
  previewUrl = null;
@@ -2901,20 +2987,19 @@ class EpistolaDocumentPreviewComponent {
2901
2987
  initialized = false;
2902
2988
  currentBlobUrl = null;
2903
2989
  previewSubscription;
2904
- constructor(epistolaPluginService, sanitizer, formIoStateService, cdr, taskContext) {
2990
+ constructor(epistolaPluginService, sanitizer, formIoStateService, cdr) {
2905
2991
  this.epistolaPluginService = epistolaPluginService;
2906
2992
  this.sanitizer = sanitizer;
2907
2993
  this.formIoStateService = formIoStateService;
2908
2994
  this.cdr = cdr;
2909
- this.taskContext = taskContext;
2910
2995
  }
2911
2996
  /**
2912
- * Resolve the active task id from {@link EpistolaTaskContextService}, populated
2913
- * by {@code EpistolaTaskContextInterceptor} on the canonical Valtimo task-open
2914
- * call. Returns null when used outside a task context (e.g. Formio builder).
2997
+ * The active task id, forwarded by the Formio wrapper from the server-prefilled
2998
+ * form ({@code epistola:taskId} value resolver). Null outside a task context
2999
+ * (e.g. Formio builder), in which case the component fails closed.
2915
3000
  */
2916
3001
  get currentTaskId() {
2917
- return this.taskContext.taskInstanceId;
3002
+ return this.taskInstanceId ?? null;
2918
3003
  }
2919
3004
  get overrideMappingScopes() {
2920
3005
  return this.overrideMapping ? Object.keys(this.overrideMapping) : [];
@@ -2940,12 +3025,17 @@ class EpistolaDocumentPreviewComponent {
2940
3025
  this.cdr.markForCheck();
2941
3026
  return;
2942
3027
  }
2943
- this.loadPreview();
3028
+ this.triggerPreview();
2944
3029
  return;
2945
3030
  }
2946
- // React to value changes (input overrides from the Formio wrapper).
2947
- if (changes['value']) {
2948
- this.loadPreview();
3031
+ if (this.designMode)
3032
+ return;
3033
+ // React to input-override changes, and to the task id arriving late: the Formio
3034
+ // wrapper sets taskInstanceId after attach, so it can land after the first render —
3035
+ // re-run the preview once it does, instead of leaving the "only available from
3036
+ // within a user task" message until a manual refresh.
3037
+ if (changes['value'] || changes['taskInstanceId']) {
3038
+ this.triggerPreview();
2949
3039
  }
2950
3040
  }
2951
3041
  ngOnDestroy() {
@@ -2953,31 +3043,42 @@ class EpistolaDocumentPreviewComponent {
2953
3043
  this.revokeBlobUrl();
2954
3044
  }
2955
3045
  refresh() {
3046
+ this.triggerPreview();
3047
+ }
3048
+ /**
3049
+ * Load the preview only when there is enough data for it. Override-driven
3050
+ * previews (those with an override mapping) wait until the mapped form data
3051
+ * has been computed; until then they show a placeholder rather than firing a
3052
+ * request that Epistola would reject with a 400 for missing required fields.
3053
+ */
3054
+ triggerPreview() {
3055
+ if (!shouldLoadPreview(this.overrideMapping, this.value)) {
3056
+ this.showWaitingForInput();
3057
+ return;
3058
+ }
2956
3059
  this.loadPreview();
2957
3060
  }
3061
+ showWaitingForInput() {
3062
+ this.revokeBlobUrl();
3063
+ this.previewSubscription?.unsubscribe();
3064
+ this.previewUrl = null;
3065
+ this.loading = false;
3066
+ this.error = 'Complete the form to generate a preview.';
3067
+ this.cdr.markForCheck();
3068
+ }
2958
3069
  /**
2959
3070
  * Preview using the explicitly configured process link + input overrides.
2960
3071
  * Requires a runtime task context — the backend authorizes the request against
2961
3072
  * the task's process instance and case document, so all three ids must match.
2962
3073
  */
2963
3074
  loadPreview() {
2964
- const documentId = this.formIoStateService.documentId;
2965
- if (!documentId) {
2966
- this.error = 'Could not determine document ID from context.';
2967
- this.cdr.markForCheck();
2968
- return;
2969
- }
2970
3075
  if (!this.sourceActivityId) {
2971
3076
  this.error = 'Preview is not configured: set the source activity on the form component.';
2972
3077
  this.cdr.markForCheck();
2973
3078
  return;
2974
3079
  }
2975
- const processInstanceId = this.formIoStateService.processInstanceId;
2976
- if (!processInstanceId) {
2977
- this.error = 'Preview is only available from within a running process.';
2978
- this.cdr.markForCheck();
2979
- return;
2980
- }
3080
+ // The backend derives the process instance and case document from the task, so the
3081
+ // task id is the only runtime context the request carries.
2981
3082
  const taskId = this.currentTaskId;
2982
3083
  if (!taskId) {
2983
3084
  this.error = 'Preview is only available from within a user task.';
@@ -2992,9 +3093,6 @@ class EpistolaDocumentPreviewComponent {
2992
3093
  this.previewSubscription = this.epistolaPluginService
2993
3094
  .previewToBlob({
2994
3095
  taskId,
2995
- documentId,
2996
- processDefinitionKey: this.processDefinitionKey || null,
2997
- processInstanceId,
2998
3096
  sourceActivityId: this.sourceActivityId,
2999
3097
  inputOverrides: this.value || null,
3000
3098
  overrides: null,
@@ -3039,8 +3137,8 @@ class EpistolaDocumentPreviewComponent {
3039
3137
  this.previewUrl = null;
3040
3138
  }
3041
3139
  }
3042
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentPreviewComponent, deps: [{ token: EpistolaPluginService }, { token: i2$4.DomSanitizer }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }, { token: EpistolaTaskContextService }], target: i0.ɵɵFactoryTarget.Component });
3043
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaDocumentPreviewComponent, isStandalone: true, selector: "epistola-document-preview-component", inputs: { value: "value", disabled: "disabled", label: "label", processDefinitionKey: "processDefinitionKey", sourceActivityId: "sourceActivityId", overrideMapping: "overrideMapping" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
3140
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentPreviewComponent, deps: [{ token: EpistolaPluginService }, { token: i2$4.DomSanitizer }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
3141
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaDocumentPreviewComponent, isStandalone: true, selector: "epistola-document-preview-component", inputs: { value: "value", disabled: "disabled", label: "label", processDefinitionKey: "processDefinitionKey", sourceActivityId: "sourceActivityId", overrideMapping: "overrideMapping", taskInstanceId: "taskInstanceId" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
3044
3142
  <!-- Design-time view: show configuration summary when no runtime context -->
3045
3143
  <div *ngIf="designMode" class="epistola-preview-panel">
3046
3144
  <div class="preview-header">
@@ -3159,7 +3257,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3159
3257
  </div>
3160
3258
  </div>
3161
3259
  `, styles: [".epistola-preview-panel{border:1px solid #dee2e6;border-radius:4px;background:#f8f9fa;display:flex;flex-direction:column}.preview-header{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;color:#495057;flex-wrap:wrap;gap:.5rem}.preview-controls{display:flex;align-items:center;gap:.5rem}.preview-select{border:1px solid #ced4da;border-radius:4px;padding:.25rem .5rem;font-size:.8rem;background:#fff;max-width:300px}.preview-refresh{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.25rem .75rem;font-size:.8rem;cursor:pointer;display:flex;align-items:center;white-space:nowrap}.preview-refresh:hover:not(:disabled){background:#e9ecef}.preview-refresh:disabled{opacity:.5;cursor:not-allowed}.preview-body{display:flex;flex-direction:column;min-height:500px}.preview-loading{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable{padding:1.5rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable i{margin-right:.25rem}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-empty{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.design-info{padding:1rem;min-height:auto}.design-section{margin-bottom:.75rem}.design-label{font-size:.7rem;text-transform:uppercase;color:#868e96;font-weight:600;letter-spacing:.05em}.design-value{font-family:monospace;font-size:.85rem;color:#212529;margin-bottom:.25rem}.design-mapping{margin-top:.25rem}.design-entry{font-family:monospace;font-size:.8rem;color:#495057;padding:.15rem 0}.design-scope{color:#0d6efd}.design-field{color:#198754}.design-entry i{font-size:.7rem;margin:0 .25rem;color:#adb5bd}.design-unconfigured{color:#6c757d;font-style:italic;font-size:.85rem}\n"] }]
3162
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$4.DomSanitizer }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }, { type: EpistolaTaskContextService }], propDecorators: { value: [{
3260
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$4.DomSanitizer }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
3163
3261
  type: Input
3164
3262
  }], valueChange: [{
3165
3263
  type: Output
@@ -3173,6 +3271,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3173
3271
  type: Input
3174
3272
  }], overrideMapping: [{
3175
3273
  type: Input
3274
+ }], taskInstanceId: [{
3275
+ type: Input
3176
3276
  }] } });
3177
3277
 
3178
3278
  class EpistolaAdminPageComponent {
@@ -3182,18 +3282,25 @@ class EpistolaAdminPageComponent {
3182
3282
  cards = [];
3183
3283
  selectedCard = null;
3184
3284
  activeTab = 'actions';
3285
+ // NOTE: the 'forms' tab is TEMPORARY (remove in 1.0.0) — see the carrier-repair block below.
3185
3286
  overviewTab = 'configurations';
3186
3287
  loading = false;
3187
3288
  pluginVersion = null;
3188
3289
  changelog = null;
3189
3290
  changelogLoading = false;
3190
- validationViolations = [];
3291
+ validationReport = null;
3191
3292
  reconcilingExecutionIds = new Set();
3192
3293
  reconcileFeedback = null;
3193
3294
  catalogs = [];
3194
3295
  catalogsLoading = false;
3195
3296
  redeployingSlugs = new Set();
3196
3297
  catalogFeedback = null;
3298
+ // TEMPORARY (removed in 1.0.0): task-id carrier detection + repair.
3299
+ formIssues = null;
3300
+ formIssuesLoading = false;
3301
+ repairingFormIds = new Set();
3302
+ repairingAll = false;
3303
+ formFeedback = null;
3197
3304
  connectionStatuses = [];
3198
3305
  usageEntries = [];
3199
3306
  pendingJobs = [];
@@ -3206,6 +3313,14 @@ class EpistolaAdminPageComponent {
3206
3313
  this.route = route;
3207
3314
  this.router = router;
3208
3315
  }
3316
+ /** Violations from the latest report (empty when healthy or not yet loaded). */
3317
+ get validationViolations() {
3318
+ return this.validationReport?.violations ?? [];
3319
+ }
3320
+ /** Scan cadence in whole minutes, for the "refreshes every N min" note. */
3321
+ get refreshIntervalMinutes() {
3322
+ return Math.round((this.validationReport?.refreshIntervalMs ?? 600000) / 60000);
3323
+ }
3209
3324
  ngOnInit() {
3210
3325
  this.deepLinkConfigId = this.route.snapshot.queryParamMap.get('configurationId');
3211
3326
  const tab = this.route.snapshot.queryParamMap.get('tab');
@@ -3232,11 +3347,79 @@ class EpistolaAdminPageComponent {
3232
3347
  this.activeTab = tab;
3233
3348
  this.updateUrl(this.selectedCard?.configurationId ?? null, tab);
3234
3349
  }
3350
+ // 'forms' is TEMPORARY (remove in 1.0.0).
3235
3351
  setOverviewTab(tab) {
3236
3352
  this.overviewTab = tab;
3237
3353
  if (tab === 'changelog' && this.changelog === null && !this.changelogLoading) {
3238
3354
  this.loadChangelog();
3239
3355
  }
3356
+ if (tab === 'forms' && this.formIssues === null && !this.formIssuesLoading) {
3357
+ this.loadFormIssues();
3358
+ }
3359
+ }
3360
+ // ---- TEMPORARY (removed in 1.0.0): task-id carrier detection + repair ----
3361
+ loadFormIssues() {
3362
+ this.formIssuesLoading = true;
3363
+ this.formFeedback = null;
3364
+ this.adminService.getFormCarrierIssues().subscribe({
3365
+ next: (issues) => {
3366
+ this.formIssues = issues;
3367
+ this.formIssuesLoading = false;
3368
+ },
3369
+ error: () => {
3370
+ this.formIssues = [];
3371
+ this.formIssuesLoading = false;
3372
+ },
3373
+ });
3374
+ }
3375
+ isRepairingForm(issue) {
3376
+ return this.repairingFormIds.has(issue.formId);
3377
+ }
3378
+ repairForm(issue) {
3379
+ if (this.repairingFormIds.has(issue.formId)) {
3380
+ return;
3381
+ }
3382
+ this.repairingFormIds.add(issue.formId);
3383
+ this.formFeedback = null;
3384
+ this.adminService.repairFormCarrier(issue.formId).subscribe({
3385
+ next: (result) => {
3386
+ this.repairingFormIds.delete(issue.formId);
3387
+ this.formFeedback = {
3388
+ formId: issue.formId,
3389
+ type: 'success',
3390
+ message: `OK — ${result.componentsPatched} component(s) patched`,
3391
+ };
3392
+ this.loadFormIssues();
3393
+ },
3394
+ error: (err) => {
3395
+ this.repairingFormIds.delete(issue.formId);
3396
+ const message = err?.error?.errorMessage ?? err?.error?.message ?? err?.message ?? 'unknown error';
3397
+ this.formFeedback = { formId: issue.formId, type: 'error', message };
3398
+ },
3399
+ });
3400
+ }
3401
+ repairAllForms() {
3402
+ if (this.repairingAll) {
3403
+ return;
3404
+ }
3405
+ this.repairingAll = true;
3406
+ this.formFeedback = null;
3407
+ this.adminService.repairAllFormCarriers().subscribe({
3408
+ next: (summary) => {
3409
+ this.repairingAll = false;
3410
+ this.formFeedback = {
3411
+ formId: 'all',
3412
+ type: summary.failed > 0 ? 'error' : 'success',
3413
+ message: `Repaired ${summary.formsRepaired} form(s), ${summary.componentsPatched} component(s)${summary.failed > 0 ? `, ${summary.failed} failed` : ''}`,
3414
+ };
3415
+ this.loadFormIssues();
3416
+ },
3417
+ error: (err) => {
3418
+ this.repairingAll = false;
3419
+ const message = err?.error?.message ?? err?.message ?? 'unknown error';
3420
+ this.formFeedback = { formId: 'all', type: 'error', message };
3421
+ },
3422
+ });
3240
3423
  }
3241
3424
  loadChangelog() {
3242
3425
  this.changelogLoading = true;
@@ -3425,14 +3608,14 @@ class EpistolaAdminPageComponent {
3425
3608
  this.tryBuildCards();
3426
3609
  },
3427
3610
  });
3428
- // Validation violations are independent of cards — load alongside but don't
3429
- // gate the loading flag on them.
3430
- this.adminService.getValidationViolations().subscribe({
3431
- next: (violations) => {
3432
- this.validationViolations = violations;
3611
+ // Validation report is independent of cards — load alongside but don't
3612
+ // gate the loading flag on it.
3613
+ this.adminService.getValidationReport().subscribe({
3614
+ next: (report) => {
3615
+ this.validationReport = report;
3433
3616
  },
3434
3617
  error: () => {
3435
- this.validationViolations = [];
3618
+ this.validationReport = null;
3436
3619
  },
3437
3620
  });
3438
3621
  }
@@ -3477,11 +3660,11 @@ class EpistolaAdminPageComponent {
3477
3660
  });
3478
3661
  }
3479
3662
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, deps: [{ token: EpistolaAdminService }, { token: i2$5.ActivatedRoute }, { token: i2$5.Router }], target: i0.ɵɵFactoryTarget.Component });
3480
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaAdminPageComponent, isStandalone: true, selector: "epistola-admin-page", ngImport: i0, template: "<div class=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #changelogHeading>\n {{ 'epistolaAdminChangelog' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"configurationsHeading\"\n [active]=\"overviewTab === 'configurations'\"\n (selected)=\"setOverviewTab('configurations')\"\n >\n <div *ngIf=\"loading\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length === 0\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length > 0\" class=\"card-grid\">\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"validationsHeading\"\n [active]=\"overviewTab === 'validations'\"\n (selected)=\"setOverviewTab('validations')\"\n >\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let v of validationViolations\">\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"changelogHeading\"\n [active]=\"overviewTab === 'changelog'\"\n (selected)=\"setOverviewTab('changelog')\"\n >\n <div class=\"d-flex align-items-center mt-3 mb-3\">\n <span class=\"text-muted me-2\">{{\n 'epistolaAdminRunningVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">v{{ pluginVersion || '\u2014' }}</cds-tag>\n </div>\n\n <div *ngIf=\"changelogLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoChangelog' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length > 0\"\n class=\"epistola-changelog\"\n >\n <section *ngFor=\"let release of changelog\" class=\"changelog-release\">\n <div class=\"changelog-release__header\">\n <cds-tag size=\"sm\" type=\"purple\">{{ release.version }}</cds-tag>\n <span *ngIf=\"release.date\" class=\"text-muted ms-2\">{{ release.date }}</span>\n </div>\n <div *ngFor=\"let section of release.sections\" class=\"changelog-section\">\n <h6 class=\"changelog-section__title\">{{ section.title }}</h6>\n <ul class=\"changelog-section__items\">\n <li *ngFor=\"let item of section.items\">{{ item }}</li>\n </ul>\n </div>\n </section>\n </div>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #catalogsHeading>\n {{ 'epistolaAdminCatalogs' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ catalogs.length }}</cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let job of selectedCard.pendingJobs\">\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <code>{{ job.requestId }}</code>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"reconcilePending(job)\"\n [disabled]=\"isReconciling(job)\"\n [title]=\"'epistolaAdminReconcileTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isReconciling(job)\">\n {{ 'epistolaAdminReconcile' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isReconciling(job)\">\n {{ 'epistolaAdminReconciling' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"catalogsHeading\"\n [active]=\"activeTab === 'catalogs'\"\n (selected)=\"setActiveTab('catalogs')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminCatalogsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"catalogsLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!catalogsLoading && catalogs.length === 0\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminNoCatalogs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"!catalogsLoading && catalogs.length > 0\" class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCatalogSlug' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogVersion' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let catalog of catalogs\">\n <td>\n <code>{{ catalog.slug }}</code>\n </td>\n <td>{{ catalog.version }}</td>\n <td>\n <cds-tag *ngIf=\"catalog.status === 'IN_EPISTOLA'\" size=\"sm\" type=\"green\">\n {{ 'epistolaAdminCatalogInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'NOT_IN_EPISTOLA'\" size=\"sm\" type=\"red\">\n {{ 'epistolaAdminCatalogNotInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'UNKNOWN'\" size=\"sm\" type=\"gray\">\n {{ 'epistolaAdminCatalogStatusUnknown' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"redeployCatalog(catalog)\"\n [disabled]=\"isRedeploying(catalog)\"\n [title]=\"'epistolaAdminRedeployTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploy' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploying' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"catalogFeedback && catalogFeedback.slug === catalog.slug\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"catalogFeedback.type === 'success'\"\n [class.text-danger]=\"catalogFeedback.type === 'error'\"\n >\n {{ catalogFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}.epistola-admin .epistola-changelog{max-height:60vh;overflow:auto;padding:.5rem 1rem;border:1px solid #dee2e6;border-radius:4px}.epistola-admin .epistola-changelog .changelog-release{padding:.75rem 0}.epistola-admin .epistola-changelog .changelog-release+.changelog-release{border-top:1px solid #eee}.epistola-admin .epistola-changelog .changelog-release__header{display:flex;align-items:center;margin-bottom:.5rem}.epistola-admin .epistola-changelog .changelog-section{margin:.25rem 0 .75rem}.epistola-admin .epistola-changelog .changelog-section__title{font-size:.8125rem;font-weight:600;text-transform:uppercase;letter-spacing:.02em;color:#6f6f6f;margin-bottom:.25rem}.epistola-admin .epistola-changelog .changelog-section__items{margin:0;padding-left:1.25rem;font-size:.8125rem;line-height:1.5}.epistola-admin .epistola-changelog .changelog-section__items li{margin-bottom:.25rem;word-break:break-word}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$5.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i5$1.Tabs, selector: "cds-tabs, ibm-tabs", inputs: ["position", "cacheActive", "followFocus", "isNavigation", "ariaLabel", "ariaLabelledby", "type", "theme", "skeleton"] }, { kind: "component", type: i5$1.Tab, selector: "cds-tab, ibm-tab", inputs: ["heading", "title", "context", "active", "disabled", "tabIndex", "id", "cacheActive", "tabContent", "templateContext"], outputs: ["selected"] }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i6.Tag, selector: "cds-tag, ibm-tag", inputs: ["type", "size", "class", "skeleton"] }] });
3663
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaAdminPageComponent, isStandalone: true, selector: "epistola-admin-page", ngImport: i0, template: "<div class=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #changelogHeading>\n {{ 'epistolaAdminChangelog' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <ng-template #formsHeading>\n {{ 'epistolaAdminForms' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n *ngIf=\"formIssues\"\n size=\"sm\"\n [type]=\"formIssues.length > 0 ? 'red' : 'gray'\"\n class=\"ms-1\"\n >{{ formIssues.length }}</cds-tag\n >\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"configurationsHeading\"\n [active]=\"overviewTab === 'configurations'\"\n (selected)=\"setOverviewTab('configurations')\"\n >\n <div *ngIf=\"loading\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length === 0\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length > 0\" class=\"card-grid\">\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"validationsHeading\"\n [active]=\"overviewTab === 'validations'\"\n (selected)=\"setOverviewTab('validations')\"\n >\n <div class=\"text-muted small mt-3\">\n <div>\n {{ 'epistolaAdminValidationLastChecked' | pluginTranslate: 'epistola' | async }}:\n <ng-container *ngIf=\"validationReport?.lastCheckedAt; else notYetRun\">\n {{ validationReport?.lastCheckedAt | date: 'medium' }}\n </ng-container>\n <ng-template #notYetRun>\n {{ 'epistolaAdminValidationNotYetRun' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n \u00B7 {{ 'epistolaAdminValidationAutoRefresh' | pluginTranslate: 'epistola' | async }}\n {{ refreshIntervalMinutes }} min.\n </div>\n <div>\n {{ 'epistolaAdminValidationLatestVersionNote' | pluginTranslate: 'epistola' | async }}\n </div>\n </div>\n\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let v of validationViolations\">\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"changelogHeading\"\n [active]=\"overviewTab === 'changelog'\"\n (selected)=\"setOverviewTab('changelog')\"\n >\n <div class=\"d-flex align-items-center mt-3 mb-3\">\n <span class=\"text-muted me-2\">{{\n 'epistolaAdminRunningVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">v{{ pluginVersion || '\u2014' }}</cds-tag>\n </div>\n\n <div *ngIf=\"changelogLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoChangelog' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length > 0\"\n class=\"epistola-changelog\"\n >\n <section *ngFor=\"let release of changelog\" class=\"changelog-release\">\n <div class=\"changelog-release__header\">\n <cds-tag size=\"sm\" type=\"purple\">{{ release.version }}</cds-tag>\n <span *ngIf=\"release.date\" class=\"text-muted ms-2\">{{ release.date }}</span>\n </div>\n <div *ngFor=\"let section of release.sections\" class=\"changelog-section\">\n <h6 class=\"changelog-section__title\">{{ section.title }}</h6>\n <ul class=\"changelog-section__items\">\n <li *ngFor=\"let item of section.items\">{{ item }}</li>\n </ul>\n </div>\n </section>\n </div>\n </cds-tab>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <cds-tab\n [heading]=\"formsHeading\"\n [active]=\"overviewTab === 'forms'\"\n (selected)=\"setOverviewTab('forms')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminFormsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"formIssuesLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!formIssuesLoading && formIssues && formIssues.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoFormIssues' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!formIssuesLoading && formIssues && formIssues.length > 0\">\n <div class=\"mb-2\">\n <button\n class=\"btn btn-sm btn-primary\"\n (click)=\"repairAllForms()\"\n [disabled]=\"repairingAll\"\n >\n {{\n (repairingAll ? 'epistolaAdminRepairing' : 'epistolaAdminRepairAll')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <span\n *ngIf=\"formFeedback && formFeedback.formId === 'all'\"\n class=\"small ms-2\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >{{ formFeedback.message }}</span\n >\n </div>\n\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminFormName' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminFormMissing' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let issue of formIssues\">\n <td>\n <code>{{ issue.name }}</code>\n <cds-tag\n *ngIf=\"issue.readOnly\"\n size=\"sm\"\n type=\"warm-gray\"\n class=\"ms-1\"\n [title]=\"'epistolaAdminFormReadOnlyHint' | pluginTranslate: 'epistola' | async\"\n >{{\n 'epistolaAdminFormReadOnly' | pluginTranslate: 'epistola' | async\n }}</cds-tag\n >\n </td>\n <td>{{ issue.missingComponents }}</td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"repairForm(issue)\"\n [disabled]=\"isRepairingForm(issue)\"\n [title]=\"'epistolaAdminRepairTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{\n (isRepairingForm(issue) ? 'epistolaAdminRepairing' : 'epistolaAdminRepair')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <div\n *ngIf=\"formFeedback && formFeedback.formId === issue.formId\"\n class=\"small mt-1\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >\n {{ formFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #catalogsHeading>\n {{ 'epistolaAdminCatalogs' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ catalogs.length }}</cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let job of selectedCard.pendingJobs\">\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <code>{{ job.requestId }}</code>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"reconcilePending(job)\"\n [disabled]=\"isReconciling(job)\"\n [title]=\"'epistolaAdminReconcileTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isReconciling(job)\">\n {{ 'epistolaAdminReconcile' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isReconciling(job)\">\n {{ 'epistolaAdminReconciling' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"catalogsHeading\"\n [active]=\"activeTab === 'catalogs'\"\n (selected)=\"setActiveTab('catalogs')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminCatalogsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"catalogsLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!catalogsLoading && catalogs.length === 0\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminNoCatalogs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"!catalogsLoading && catalogs.length > 0\" class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCatalogSlug' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogVersion' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let catalog of catalogs\">\n <td>\n <code>{{ catalog.slug }}</code>\n </td>\n <td>{{ catalog.version }}</td>\n <td>\n <cds-tag *ngIf=\"catalog.status === 'IN_EPISTOLA'\" size=\"sm\" type=\"green\">\n {{ 'epistolaAdminCatalogInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'NOT_IN_EPISTOLA'\" size=\"sm\" type=\"red\">\n {{ 'epistolaAdminCatalogNotInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'UNKNOWN'\" size=\"sm\" type=\"gray\">\n {{ 'epistolaAdminCatalogStatusUnknown' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"redeployCatalog(catalog)\"\n [disabled]=\"isRedeploying(catalog)\"\n [title]=\"'epistolaAdminRedeployTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploy' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploying' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"catalogFeedback && catalogFeedback.slug === catalog.slug\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"catalogFeedback.type === 'success'\"\n [class.text-danger]=\"catalogFeedback.type === 'error'\"\n >\n {{ catalogFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}.epistola-admin .epistola-changelog{max-height:60vh;overflow:auto;padding:.5rem 1rem;border:1px solid #dee2e6;border-radius:4px}.epistola-admin .epistola-changelog .changelog-release{padding:.75rem 0}.epistola-admin .epistola-changelog .changelog-release+.changelog-release{border-top:1px solid #eee}.epistola-admin .epistola-changelog .changelog-release__header{display:flex;align-items:center;margin-bottom:.5rem}.epistola-admin .epistola-changelog .changelog-section{margin:.25rem 0 .75rem}.epistola-admin .epistola-changelog .changelog-section__title{font-size:.8125rem;font-weight:600;text-transform:uppercase;letter-spacing:.02em;color:#6f6f6f;margin-bottom:.25rem}.epistola-admin .epistola-changelog .changelog-section__items{margin:0;padding-left:1.25rem;font-size:.8125rem;line-height:1.5}.epistola-admin .epistola-changelog .changelog-section__items li{margin-bottom:.25rem;word-break:break-word}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.DatePipe, name: "date" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$5.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i5.Tabs, selector: "cds-tabs, ibm-tabs", inputs: ["position", "cacheActive", "followFocus", "isNavigation", "ariaLabel", "ariaLabelledby", "type", "theme", "skeleton"] }, { kind: "component", type: i5.Tab, selector: "cds-tab, ibm-tab", inputs: ["heading", "title", "context", "active", "disabled", "tabIndex", "id", "cacheActive", "tabContent", "templateContext"], outputs: ["selected"] }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i6.Tag, selector: "cds-tag, ibm-tag", inputs: ["type", "size", "class", "skeleton"] }] });
3481
3664
  }
3482
3665
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, decorators: [{
3483
3666
  type: Component,
3484
- args: [{ selector: 'epistola-admin-page', standalone: true, imports: [CommonModule, RouterModule, PluginTranslatePipeModule, TabsModule, TagModule], template: "<div class=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #changelogHeading>\n {{ 'epistolaAdminChangelog' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"configurationsHeading\"\n [active]=\"overviewTab === 'configurations'\"\n (selected)=\"setOverviewTab('configurations')\"\n >\n <div *ngIf=\"loading\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length === 0\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length > 0\" class=\"card-grid\">\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"validationsHeading\"\n [active]=\"overviewTab === 'validations'\"\n (selected)=\"setOverviewTab('validations')\"\n >\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let v of validationViolations\">\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"changelogHeading\"\n [active]=\"overviewTab === 'changelog'\"\n (selected)=\"setOverviewTab('changelog')\"\n >\n <div class=\"d-flex align-items-center mt-3 mb-3\">\n <span class=\"text-muted me-2\">{{\n 'epistolaAdminRunningVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">v{{ pluginVersion || '\u2014' }}</cds-tag>\n </div>\n\n <div *ngIf=\"changelogLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoChangelog' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length > 0\"\n class=\"epistola-changelog\"\n >\n <section *ngFor=\"let release of changelog\" class=\"changelog-release\">\n <div class=\"changelog-release__header\">\n <cds-tag size=\"sm\" type=\"purple\">{{ release.version }}</cds-tag>\n <span *ngIf=\"release.date\" class=\"text-muted ms-2\">{{ release.date }}</span>\n </div>\n <div *ngFor=\"let section of release.sections\" class=\"changelog-section\">\n <h6 class=\"changelog-section__title\">{{ section.title }}</h6>\n <ul class=\"changelog-section__items\">\n <li *ngFor=\"let item of section.items\">{{ item }}</li>\n </ul>\n </div>\n </section>\n </div>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #catalogsHeading>\n {{ 'epistolaAdminCatalogs' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ catalogs.length }}</cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let job of selectedCard.pendingJobs\">\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <code>{{ job.requestId }}</code>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"reconcilePending(job)\"\n [disabled]=\"isReconciling(job)\"\n [title]=\"'epistolaAdminReconcileTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isReconciling(job)\">\n {{ 'epistolaAdminReconcile' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isReconciling(job)\">\n {{ 'epistolaAdminReconciling' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"catalogsHeading\"\n [active]=\"activeTab === 'catalogs'\"\n (selected)=\"setActiveTab('catalogs')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminCatalogsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"catalogsLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!catalogsLoading && catalogs.length === 0\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminNoCatalogs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"!catalogsLoading && catalogs.length > 0\" class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCatalogSlug' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogVersion' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let catalog of catalogs\">\n <td>\n <code>{{ catalog.slug }}</code>\n </td>\n <td>{{ catalog.version }}</td>\n <td>\n <cds-tag *ngIf=\"catalog.status === 'IN_EPISTOLA'\" size=\"sm\" type=\"green\">\n {{ 'epistolaAdminCatalogInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'NOT_IN_EPISTOLA'\" size=\"sm\" type=\"red\">\n {{ 'epistolaAdminCatalogNotInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'UNKNOWN'\" size=\"sm\" type=\"gray\">\n {{ 'epistolaAdminCatalogStatusUnknown' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"redeployCatalog(catalog)\"\n [disabled]=\"isRedeploying(catalog)\"\n [title]=\"'epistolaAdminRedeployTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploy' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploying' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"catalogFeedback && catalogFeedback.slug === catalog.slug\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"catalogFeedback.type === 'success'\"\n [class.text-danger]=\"catalogFeedback.type === 'error'\"\n >\n {{ catalogFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}.epistola-admin .epistola-changelog{max-height:60vh;overflow:auto;padding:.5rem 1rem;border:1px solid #dee2e6;border-radius:4px}.epistola-admin .epistola-changelog .changelog-release{padding:.75rem 0}.epistola-admin .epistola-changelog .changelog-release+.changelog-release{border-top:1px solid #eee}.epistola-admin .epistola-changelog .changelog-release__header{display:flex;align-items:center;margin-bottom:.5rem}.epistola-admin .epistola-changelog .changelog-section{margin:.25rem 0 .75rem}.epistola-admin .epistola-changelog .changelog-section__title{font-size:.8125rem;font-weight:600;text-transform:uppercase;letter-spacing:.02em;color:#6f6f6f;margin-bottom:.25rem}.epistola-admin .epistola-changelog .changelog-section__items{margin:0;padding-left:1.25rem;font-size:.8125rem;line-height:1.5}.epistola-admin .epistola-changelog .changelog-section__items li{margin-bottom:.25rem;word-break:break-word}\n"] }]
3667
+ args: [{ selector: 'epistola-admin-page', standalone: true, imports: [CommonModule, RouterModule, PluginTranslatePipeModule, TabsModule, TagModule], template: "<div class=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #changelogHeading>\n {{ 'epistolaAdminChangelog' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <ng-template #formsHeading>\n {{ 'epistolaAdminForms' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n *ngIf=\"formIssues\"\n size=\"sm\"\n [type]=\"formIssues.length > 0 ? 'red' : 'gray'\"\n class=\"ms-1\"\n >{{ formIssues.length }}</cds-tag\n >\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"configurationsHeading\"\n [active]=\"overviewTab === 'configurations'\"\n (selected)=\"setOverviewTab('configurations')\"\n >\n <div *ngIf=\"loading\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length === 0\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length > 0\" class=\"card-grid\">\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"validationsHeading\"\n [active]=\"overviewTab === 'validations'\"\n (selected)=\"setOverviewTab('validations')\"\n >\n <div class=\"text-muted small mt-3\">\n <div>\n {{ 'epistolaAdminValidationLastChecked' | pluginTranslate: 'epistola' | async }}:\n <ng-container *ngIf=\"validationReport?.lastCheckedAt; else notYetRun\">\n {{ validationReport?.lastCheckedAt | date: 'medium' }}\n </ng-container>\n <ng-template #notYetRun>\n {{ 'epistolaAdminValidationNotYetRun' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n \u00B7 {{ 'epistolaAdminValidationAutoRefresh' | pluginTranslate: 'epistola' | async }}\n {{ refreshIntervalMinutes }} min.\n </div>\n <div>\n {{ 'epistolaAdminValidationLatestVersionNote' | pluginTranslate: 'epistola' | async }}\n </div>\n </div>\n\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let v of validationViolations\">\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"changelogHeading\"\n [active]=\"overviewTab === 'changelog'\"\n (selected)=\"setOverviewTab('changelog')\"\n >\n <div class=\"d-flex align-items-center mt-3 mb-3\">\n <span class=\"text-muted me-2\">{{\n 'epistolaAdminRunningVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">v{{ pluginVersion || '\u2014' }}</cds-tag>\n </div>\n\n <div *ngIf=\"changelogLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoChangelog' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length > 0\"\n class=\"epistola-changelog\"\n >\n <section *ngFor=\"let release of changelog\" class=\"changelog-release\">\n <div class=\"changelog-release__header\">\n <cds-tag size=\"sm\" type=\"purple\">{{ release.version }}</cds-tag>\n <span *ngIf=\"release.date\" class=\"text-muted ms-2\">{{ release.date }}</span>\n </div>\n <div *ngFor=\"let section of release.sections\" class=\"changelog-section\">\n <h6 class=\"changelog-section__title\">{{ section.title }}</h6>\n <ul class=\"changelog-section__items\">\n <li *ngFor=\"let item of section.items\">{{ item }}</li>\n </ul>\n </div>\n </section>\n </div>\n </cds-tab>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <cds-tab\n [heading]=\"formsHeading\"\n [active]=\"overviewTab === 'forms'\"\n (selected)=\"setOverviewTab('forms')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminFormsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"formIssuesLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!formIssuesLoading && formIssues && formIssues.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoFormIssues' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!formIssuesLoading && formIssues && formIssues.length > 0\">\n <div class=\"mb-2\">\n <button\n class=\"btn btn-sm btn-primary\"\n (click)=\"repairAllForms()\"\n [disabled]=\"repairingAll\"\n >\n {{\n (repairingAll ? 'epistolaAdminRepairing' : 'epistolaAdminRepairAll')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <span\n *ngIf=\"formFeedback && formFeedback.formId === 'all'\"\n class=\"small ms-2\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >{{ formFeedback.message }}</span\n >\n </div>\n\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminFormName' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminFormMissing' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let issue of formIssues\">\n <td>\n <code>{{ issue.name }}</code>\n <cds-tag\n *ngIf=\"issue.readOnly\"\n size=\"sm\"\n type=\"warm-gray\"\n class=\"ms-1\"\n [title]=\"'epistolaAdminFormReadOnlyHint' | pluginTranslate: 'epistola' | async\"\n >{{\n 'epistolaAdminFormReadOnly' | pluginTranslate: 'epistola' | async\n }}</cds-tag\n >\n </td>\n <td>{{ issue.missingComponents }}</td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"repairForm(issue)\"\n [disabled]=\"isRepairingForm(issue)\"\n [title]=\"'epistolaAdminRepairTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{\n (isRepairingForm(issue) ? 'epistolaAdminRepairing' : 'epistolaAdminRepair')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <div\n *ngIf=\"formFeedback && formFeedback.formId === issue.formId\"\n class=\"small mt-1\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >\n {{ formFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #catalogsHeading>\n {{ 'epistolaAdminCatalogs' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ catalogs.length }}</cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let job of selectedCard.pendingJobs\">\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <code>{{ job.requestId }}</code>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"reconcilePending(job)\"\n [disabled]=\"isReconciling(job)\"\n [title]=\"'epistolaAdminReconcileTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isReconciling(job)\">\n {{ 'epistolaAdminReconcile' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isReconciling(job)\">\n {{ 'epistolaAdminReconciling' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"catalogsHeading\"\n [active]=\"activeTab === 'catalogs'\"\n (selected)=\"setActiveTab('catalogs')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminCatalogsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"catalogsLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!catalogsLoading && catalogs.length === 0\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminNoCatalogs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"!catalogsLoading && catalogs.length > 0\" class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCatalogSlug' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogVersion' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let catalog of catalogs\">\n <td>\n <code>{{ catalog.slug }}</code>\n </td>\n <td>{{ catalog.version }}</td>\n <td>\n <cds-tag *ngIf=\"catalog.status === 'IN_EPISTOLA'\" size=\"sm\" type=\"green\">\n {{ 'epistolaAdminCatalogInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'NOT_IN_EPISTOLA'\" size=\"sm\" type=\"red\">\n {{ 'epistolaAdminCatalogNotInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'UNKNOWN'\" size=\"sm\" type=\"gray\">\n {{ 'epistolaAdminCatalogStatusUnknown' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"redeployCatalog(catalog)\"\n [disabled]=\"isRedeploying(catalog)\"\n [title]=\"'epistolaAdminRedeployTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploy' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploying' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"catalogFeedback && catalogFeedback.slug === catalog.slug\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"catalogFeedback.type === 'success'\"\n [class.text-danger]=\"catalogFeedback.type === 'error'\"\n >\n {{ catalogFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}.epistola-admin .epistola-changelog{max-height:60vh;overflow:auto;padding:.5rem 1rem;border:1px solid #dee2e6;border-radius:4px}.epistola-admin .epistola-changelog .changelog-release{padding:.75rem 0}.epistola-admin .epistola-changelog .changelog-release+.changelog-release{border-top:1px solid #eee}.epistola-admin .epistola-changelog .changelog-release__header{display:flex;align-items:center;margin-bottom:.5rem}.epistola-admin .epistola-changelog .changelog-section{margin:.25rem 0 .75rem}.epistola-admin .epistola-changelog .changelog-section__title{font-size:.8125rem;font-weight:600;text-transform:uppercase;letter-spacing:.02em;color:#6f6f6f;margin-bottom:.25rem}.epistola-admin .epistola-changelog .changelog-section__items{margin:0;padding-left:1.25rem;font-size:.8125rem;line-height:1.5}.epistola-admin .epistola-changelog .changelog-section__items li{margin-bottom:.25rem;word-break:break-word}\n"] }]
3485
3668
  }], ctorParameters: () => [{ type: EpistolaAdminService }, { type: i2$5.ActivatedRoute }, { type: i2$5.Router }] });
3486
3669
 
3487
3670
  function isRuntimeWindow(value) {
@@ -3548,12 +3731,59 @@ const EPISTOLA_DOCUMENT_OPTIONS = {
3548
3731
  icon: 'file-pdf-o',
3549
3732
  emptyValue: null,
3550
3733
  fieldOptions: ['label', 'display', 'documentVariable', 'tenantIdVariable', 'filename'],
3734
+ // Embed the hidden task-id carrier so dropping this component is enough — no separate
3735
+ // field for the author to add. Valtimo prefills it server-side via the epistola: resolver.
3736
+ schema: { components: [PREFILLED_TASK_ID_CARRIER] },
3551
3737
  };
3552
3738
  function registerEpistolaDocumentComponent(injector) {
3553
3739
  if (customElements.get(EPISTOLA_DOCUMENT_OPTIONS.selector)) {
3554
3740
  return;
3555
3741
  }
3556
3742
  registerCustomFormioComponent(EPISTOLA_DOCUMENT_OPTIONS, EpistolaDocumentComponent, injector);
3743
+ // Extend the base class to forward the server-prefilled task id (epistola: value
3744
+ // resolver) to the Angular element, so the download authorizes against the exact task in
3745
+ // every Valtimo task-open flow.
3746
+ const Formio = window.Formio;
3747
+ const BaseComponent = Formio?.Components?.components?.[EPISTOLA_DOCUMENT_OPTIONS.type];
3748
+ if (!BaseComponent) {
3749
+ return;
3750
+ }
3751
+ class EpistolaDocumentWithTaskContext extends BaseComponent {
3752
+ attach(element) {
3753
+ const result = super.attach(element);
3754
+ if (this._customAngularElement) {
3755
+ const prefilledTaskId = readPrefilledTaskId(this.root);
3756
+ if (prefilledTaskId) {
3757
+ this._customAngularElement['taskInstanceId'] = prefilledTaskId;
3758
+ }
3759
+ }
3760
+ return result;
3761
+ }
3762
+ }
3763
+ Formio.Components.setComponent(EPISTOLA_DOCUMENT_OPTIONS.type, EpistolaDocumentWithTaskContext);
3764
+ }
3765
+
3766
+ /**
3767
+ * Hides a registered custom Formio component from the builder's component palette,
3768
+ * while keeping it fully usable inside other components' `editForm`s and at runtime.
3769
+ *
3770
+ * Formio's `WebformBuilder` only adds a component to the palette when
3771
+ * `component.builderInfo && component.builderInfo.schema` is truthy. Overriding the
3772
+ * registered class's static `builderInfo` getter to `false` therefore removes it from
3773
+ * the palette. Runtime instantiation and editForm usage don't consult `builderInfo`,
3774
+ * so they are unaffected.
3775
+ *
3776
+ * Call this AFTER the component is registered (and after any `setComponent` re-registration),
3777
+ * so it targets the final class in `Formio.Components.components[type]`.
3778
+ */
3779
+ function hideFormioComponentFromBuilder(type) {
3780
+ const registered = window.Formio?.Components?.components?.[type];
3781
+ if (registered) {
3782
+ Object.defineProperty(registered, 'builderInfo', {
3783
+ get: () => false,
3784
+ configurable: true,
3785
+ });
3786
+ }
3557
3787
  }
3558
3788
 
3559
3789
  const EPISTOLA_RETRY_FORM_OPTIONS = {
@@ -3564,11 +3794,39 @@ const EPISTOLA_RETRY_FORM_OPTIONS = {
3564
3794
  icon: 'refresh',
3565
3795
  emptyValue: null,
3566
3796
  fieldOptions: ['sourceActivityId', 'label'], // sourceActivityId is optional (set via BPMN input parameter)
3797
+ // Embed the hidden task-id carrier so dropping this component is enough — no separate
3798
+ // field for the author to add. Valtimo prefills it server-side via the epistola: resolver.
3799
+ schema: { components: [PREFILLED_TASK_ID_CARRIER] },
3567
3800
  };
3568
3801
  function registerEpistolaRetryFormComponent(injector) {
3569
- if (!customElements.get(EPISTOLA_RETRY_FORM_OPTIONS.selector)) {
3570
- registerCustomFormioComponent(EPISTOLA_RETRY_FORM_OPTIONS, EpistolaRetryFormComponent, injector);
3802
+ if (customElements.get(EPISTOLA_RETRY_FORM_OPTIONS.selector)) {
3803
+ return;
3804
+ }
3805
+ registerCustomFormioComponent(EPISTOLA_RETRY_FORM_OPTIONS, EpistolaRetryFormComponent, injector);
3806
+ // Extend the base class to forward the server-prefilled task id (epistola: value
3807
+ // resolver) to the Angular element, so the retry form authorizes against the exact task in
3808
+ // every Valtimo task-open flow.
3809
+ const Formio = window.Formio;
3810
+ const BaseComponent = Formio?.Components?.components?.[EPISTOLA_RETRY_FORM_OPTIONS.type];
3811
+ if (!BaseComponent) {
3812
+ return;
3813
+ }
3814
+ class EpistolaRetryFormWithTaskContext extends BaseComponent {
3815
+ attach(element) {
3816
+ const result = super.attach(element);
3817
+ if (this._customAngularElement) {
3818
+ const prefilledTaskId = readPrefilledTaskId(this.root);
3819
+ if (prefilledTaskId) {
3820
+ this._customAngularElement['taskInstanceId'] = prefilledTaskId;
3821
+ }
3822
+ }
3823
+ return result;
3824
+ }
3571
3825
  }
3826
+ Formio.Components.setComponent(EPISTOLA_RETRY_FORM_OPTIONS.type, EpistolaRetryFormWithTaskContext);
3827
+ // Part of the plugin's auto-deployed retry form, not a drop-anywhere component — hide it
3828
+ // from the builder palette. It still renders wherever it's already present in a form.
3829
+ hideFormioComponentFromBuilder(EPISTOLA_RETRY_FORM_OPTIONS.type);
3572
3830
  }
3573
3831
 
3574
3832
  const EPISTOLA_DOCUMENT_PREVIEW_OPTIONS = {
@@ -3579,6 +3837,9 @@ const EPISTOLA_DOCUMENT_PREVIEW_OPTIONS = {
3579
3837
  icon: 'file-pdf-o',
3580
3838
  emptyValue: null,
3581
3839
  fieldOptions: ['label', 'processDefinitionKey', 'sourceActivityId', 'overrideMapping'],
3840
+ // Embed the hidden task-id carrier so dropping this component is enough — no separate
3841
+ // field for the author to add. Valtimo prefills it server-side via the epistola: resolver.
3842
+ schema: { components: [PREFILLED_TASK_ID_CARRIER] },
3582
3843
  editForm: () => ({
3583
3844
  components: [
3584
3845
  {
@@ -3614,7 +3875,15 @@ function registerEpistolaDocumentPreviewComponent(injector) {
3614
3875
  class PreviewWithOverrides extends BasePreviewComponent {
3615
3876
  _debounceTimer = null;
3616
3877
  _changeListenerAttached = false;
3878
+ _changeHandler = null;
3879
+ _destroyed = false;
3617
3880
  attach(element) {
3881
+ // Formio detaches and re-attaches components on every redraw — not only at
3882
+ // teardown — so a re-attach means the component is alive again. Clear the
3883
+ // destroyed flag here; it only stays set when detach() is the final call
3884
+ // (genuine teardown, e.g. task completion), which is what suppresses the
3885
+ // post-submit preview.
3886
+ this._destroyed = false;
3618
3887
  // Bidirectional sync between processLinkSelection object and separate properties.
3619
3888
  // The editForm uses processLinkSelection (single field), while the component
3620
3889
  // config and Angular inputs use processDefinitionKey + sourceActivityId.
@@ -3634,32 +3903,61 @@ function registerEpistolaDocumentPreviewComponent(injector) {
3634
3903
  this._customAngularElement['processDefinitionKey'] =
3635
3904
  this.component.processDefinitionKey || '';
3636
3905
  this._customAngularElement['sourceActivityId'] = this.component.sourceActivityId || '';
3906
+ // Forward the server-prefilled task id (epistola: value resolver) so the
3907
+ // component authorizes against the exact task in every Valtimo task-open flow.
3908
+ const prefilledTaskId = readPrefilledTaskId(this.root);
3909
+ if (prefilledTaskId) {
3910
+ this._customAngularElement['taskInstanceId'] = prefilledTaskId;
3911
+ }
3637
3912
  }
3638
3913
  // Listen to form changes and compute input overrides from the mapping
3639
3914
  if (this.root && this.component?.overrideMapping && !this._changeListenerAttached) {
3640
3915
  this._changeListenerAttached = true;
3641
- this.root.on('change', () => {
3642
- this._computeAndSetOverrides();
3643
- });
3644
- // Compute initial value
3645
- this._computeAndSetOverrides();
3916
+ this._changeHandler = () => this._computeAndSetOverrides();
3917
+ this.root.on('change', this._changeHandler);
3918
+ // Compute the initial overrides immediately (no debounce) so a pre-filled
3919
+ // form paints its preview without the 1.5s delay.
3920
+ this._computeAndSetOverrides(true);
3646
3921
  }
3647
3922
  return result;
3648
3923
  }
3649
- _computeAndSetOverrides() {
3924
+ // Tear down the change listener and any pending debounce so a preview is never
3925
+ // fired after the form is unmounted (e.g. on task completion). Without this the
3926
+ // 1.5s debounce can outlive submit and POST /preview with reset/incomplete data,
3927
+ // which Epistola rejects with a 400.
3928
+ detach() {
3929
+ this._destroyed = true;
3930
+ if (this._debounceTimer) {
3931
+ clearTimeout(this._debounceTimer);
3932
+ this._debounceTimer = null;
3933
+ }
3934
+ if (this._changeHandler && this.root?.off) {
3935
+ this.root.off('change', this._changeHandler);
3936
+ this._changeHandler = null;
3937
+ }
3938
+ this._changeListenerAttached = false;
3939
+ return super.detach();
3940
+ }
3941
+ _computeAndSetOverrides(immediate = false) {
3650
3942
  if (this._debounceTimer) {
3651
3943
  clearTimeout(this._debounceTimer);
3652
3944
  }
3653
3945
  this._debounceTimer = setTimeout(() => {
3946
+ // Skip if the form is being/has been submitted or the component is gone —
3947
+ // those previews would run with incomplete/reset data and 400 from Epistola.
3948
+ if (this._destroyed || this.root?.submitting || this.root?.submitted) {
3949
+ return;
3950
+ }
3654
3951
  const mapping = this.component?.overrideMapping;
3655
3952
  const formData = this.root?.data;
3656
- if (mapping && formData) {
3657
- const overrides = computeInputOverrides(mapping, formData);
3658
- if (Object.keys(overrides).length > 0) {
3659
- this.setValue(overrides);
3660
- }
3953
+ if (!mapping || !formData) {
3954
+ return;
3661
3955
  }
3662
- }, 1500);
3956
+ const overrides = computeInputOverrides(mapping, formData);
3957
+ // Push null when there's nothing usable yet so the component reverts to
3958
+ // its "complete the form" placeholder instead of keeping a stale preview.
3959
+ this.setValue(Object.keys(overrides).length > 0 ? overrides : null);
3960
+ }, immediate ? 0 : 1500);
3663
3961
  }
3664
3962
  }
3665
3963
  // Re-register with the extended class
@@ -3986,6 +4284,20 @@ function registerEpistolaOverrideBuilderComponent(injector) {
3986
4284
  }
3987
4285
  // Re-register with the extended class
3988
4286
  Formio.Components.setComponent(EPISTOLA_OVERRIDE_BUILDER_OPTIONS.type, OverrideBuilderWithFields);
4287
+ // Internal editForm widget — not a standalone form field. Hide it from the builder palette.
4288
+ hideFormioComponentFromBuilder(EPISTOLA_OVERRIDE_BUILDER_OPTIONS.type);
4289
+ }
4290
+
4291
+ /**
4292
+ * The plugin action definition key the backend serializes for generate-document
4293
+ * process links. It carries the `epistola-` prefix — see `EPISTOLA_ACTION_KEYS`
4294
+ * in `EpistolaAdminService` on the backend. The selector must match this exact
4295
+ * value; the un-prefixed `generate-document` never appears in the API response.
4296
+ */
4297
+ const GENERATE_DOCUMENT_ACTION_KEY = 'epistola-generate-document';
4298
+ /** Keeps only the generate-document process links from a usage overview. */
4299
+ function filterGenerateDocumentEntries(entries) {
4300
+ return entries.filter((e) => e.actionKey === GENERATE_DOCUMENT_ACTION_KEY);
3989
4301
  }
3990
4302
 
3991
4303
  class EpistolaProcessLinkSelectorComponent {
@@ -4038,7 +4350,7 @@ class EpistolaProcessLinkSelectorComponent {
4038
4350
  this.cdr.markForCheck();
4039
4351
  this.loadSubscription = this.adminService.getPluginUsage().subscribe({
4040
4352
  next: (entries) => {
4041
- this.filteredEntries = entries.filter((e) => e.actionKey === 'generate-document');
4353
+ this.filteredEntries = filterGenerateDocumentEntries(entries);
4042
4354
  this.loading = false;
4043
4355
  // Restore selection from value
4044
4356
  if (this.value) {
@@ -4113,6 +4425,8 @@ const EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS = {
4113
4425
  function registerEpistolaProcessLinkSelectorComponent(injector) {
4114
4426
  if (!customElements.get(EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS.selector)) {
4115
4427
  registerCustomFormioComponent(EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS, EpistolaProcessLinkSelectorComponent, injector);
4428
+ // Internal editForm widget — not a standalone form field. Hide it from the builder palette.
4429
+ hideFormioComponentFromBuilder(EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS.type);
4116
4430
  }
4117
4431
  }
4118
4432
 
@@ -4150,11 +4464,6 @@ class EpistolaPluginModule {
4150
4464
  EpistolaPluginService,
4151
4465
  EpistolaAdminService,
4152
4466
  EpistolaMenuService,
4153
- {
4154
- provide: HTTP_INTERCEPTORS,
4155
- useClass: EpistolaTaskContextInterceptor,
4156
- multi: true,
4157
- },
4158
4467
  {
4159
4468
  provide: ENVIRONMENT_INITIALIZER,
4160
4469
  multi: true,
@@ -4221,11 +4530,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
4221
4530
  EpistolaPluginService,
4222
4531
  EpistolaAdminService,
4223
4532
  EpistolaMenuService,
4224
- {
4225
- provide: HTTP_INTERCEPTORS,
4226
- useClass: EpistolaTaskContextInterceptor,
4227
- multi: true,
4228
- },
4229
4533
  {
4230
4534
  provide: ENVIRONMENT_INITIALIZER,
4231
4535
  multi: true,
@@ -4370,8 +4674,12 @@ const epistolaPluginSpecification = {
4370
4674
  'epistola-download-document': 'Download Document',
4371
4675
  documentVariable: 'Document Variabele',
4372
4676
  documentVariableTooltip: 'Naam van de procesvariabele met het Epistola resultaat. Mag een String document ID zijn (legacy) of een rich-result object met een documentId-veld; de actie haalt het document ID eruit.',
4677
+ storageTarget: 'Opslagdoel',
4678
+ storageTargetTooltip: 'Waar het gedownloade PDF wordt opgeslagen. "Tijdelijke resource-opslag" (aanbevolen) zet alleen een resource-id in de variabele — geschikt om door te geven aan documenten-api:store-temp-document. "Procesvariabele" zet de ruwe bytes inline in de variabele (alleen voor kleine, niet-gevoelige documenten; deze worden in de taakrespons meegestuurd).',
4679
+ resourceIdVariable: 'Resource-ID Variabele',
4680
+ resourceIdVariableTooltip: 'Naam van de procesvariabele waarin het tijdelijke resource-id wordt opgeslagen (geef deze door aan documenten-api:store-temp-document).',
4373
4681
  contentVariable: 'Inhoud Variabele',
4374
- contentVariableTooltip: 'Naam van de procesvariabele waarin de documentinhoud (Base64) wordt opgeslagen',
4682
+ contentVariableTooltip: 'Naam van de procesvariabele waarin de ruwe PDF-bytes inline worden opgeslagen (alleen voor kleine, niet-gevoelige documenten).',
4375
4683
  // Admin page
4376
4684
  epistolaAdminOverview: 'Overzicht',
4377
4685
  epistolaAdminRefresh: 'Vernieuwen',
@@ -4406,6 +4714,10 @@ const epistolaPluginSpecification = {
4406
4714
  epistolaAdminValidationWarningBody: 'In deze procesdefinities is de grens tussen de generate-document service task en de EpistolaDocumentGenerated catch event niet synchroon. Resultaten kunnen verloren gaan; gebruik in dat geval de Hersynchroniseer-knop in de Wachtende taken-tab.',
4407
4715
  epistolaAdminValidationCode: 'Code',
4408
4716
  epistolaAdminValidationMessage: 'Bericht',
4717
+ epistolaAdminValidationLastChecked: 'Laatst gecontroleerd',
4718
+ epistolaAdminValidationNotYetRun: 'nog niet uitgevoerd',
4719
+ epistolaAdminValidationAutoRefresh: 'automatisch opnieuw gecontroleerd, ongeveer elke',
4720
+ epistolaAdminValidationLatestVersionNote: 'Alleen de meest recente versie van elke procesdefinitie wordt gecontroleerd; oudere versies met nog lopende instanties kunnen problemen hebben die hier niet worden getoond.',
4409
4721
  epistolaAdminCatalogs: 'Catalogi',
4410
4722
  epistolaAdminCatalogsIntro: 'Catalogi op het classpath van de applicatie. Deze worden bij het opstarten automatisch uitgerold; gebruik Opnieuw uitrollen om een catalogus handmatig (geforceerd) naar deze Epistola-omgeving te sturen, bijvoorbeeld als de automatische uitrol is mislukt.',
4411
4723
  epistolaAdminNoCatalogs: 'Geen catalogi gevonden op het classpath.',
@@ -4421,6 +4733,18 @@ const epistolaPluginSpecification = {
4421
4733
  epistolaAdminChangelog: 'Changelog',
4422
4734
  epistolaAdminRunningVersion: 'Actieve plugin-versie:',
4423
4735
  epistolaAdminNoChangelog: 'Geen changelog beschikbaar in deze build.',
4736
+ // TEMPORARY (remove in 1.0.0): task-id carrier repair (admin "Forms" tab)
4737
+ epistolaAdminForms: 'Formulieren',
4738
+ epistolaAdminFormsIntro: 'Formulieren met een Epistola-component dat het verborgen task-id veld mist. Zonder dat veld werkt het voorbeeld/downloaden/opnieuw genereren niet in elke taak-openflow. Herstel voegt het veld toe.',
4739
+ epistolaAdminFormName: 'Formulier',
4740
+ epistolaAdminFormMissing: 'Ontbrekende componenten',
4741
+ epistolaAdminFormReadOnly: 'Alleen-lezen',
4742
+ epistolaAdminFormReadOnlyHint: 'Dit formulier komt van het classpath en wordt bij de volgende herstart teruggezet naar de bron. Voeg het veld toe aan de bron (component opnieuw plaatsen) voor een blijvende oplossing.',
4743
+ epistolaAdminRepair: 'Herstellen',
4744
+ epistolaAdminRepairAll: 'Alles herstellen',
4745
+ epistolaAdminRepairing: 'Bezig...',
4746
+ epistolaAdminRepairTooltip: 'Voeg het verborgen task-id veld toe aan alle Epistola-componenten in dit formulier.',
4747
+ epistolaAdminNoFormIssues: 'Geen formulieren met een ontbrekend task-id veld gevonden.',
4424
4748
  },
4425
4749
  en: {
4426
4750
  title: 'Epistola Document Suite',
@@ -4526,8 +4850,12 @@ const epistolaPluginSpecification = {
4526
4850
  'epistola-download-document': 'Download Document',
4527
4851
  documentVariable: 'Document Variable',
4528
4852
  documentVariableTooltip: 'Name of the process variable holding the Epistola result. May be a String document id (legacy) or a rich result object with a `documentId` key — the action extracts the document id from it.',
4853
+ storageTarget: 'Storage Target',
4854
+ storageTargetTooltip: 'Where the downloaded PDF is stored. "Temporary resource storage" (recommended) writes only a resource id to the variable — ready to hand to documenten-api:store-temp-document. "Process variable" writes the raw bytes inline (small, non-sensitive documents only; they are included in the task response).',
4855
+ resourceIdVariable: 'Resource ID Variable',
4856
+ resourceIdVariableTooltip: 'Name of the process variable to store the temporary resource id in (hand this to documenten-api:store-temp-document).',
4529
4857
  contentVariable: 'Content Variable',
4530
- contentVariableTooltip: 'Name of the process variable to store the document content (Base64) in',
4858
+ contentVariableTooltip: 'Name of the process variable to store the raw PDF bytes inline in (small, non-sensitive documents only).',
4531
4859
  // Admin page
4532
4860
  epistolaAdminOverview: 'Overview',
4533
4861
  epistolaAdminRefresh: 'Refresh',
@@ -4562,6 +4890,10 @@ const epistolaPluginSpecification = {
4562
4890
  epistolaAdminValidationWarningBody: 'These process definitions have a non-synchronous boundary between the generate-document service task and the EpistolaDocumentGenerated catch event. Results can be missed; use the Reconcile button on the Pending Jobs tab to recover.',
4563
4891
  epistolaAdminValidationCode: 'Code',
4564
4892
  epistolaAdminValidationMessage: 'Message',
4893
+ epistolaAdminValidationLastChecked: 'Last checked',
4894
+ epistolaAdminValidationNotYetRun: 'not yet run',
4895
+ epistolaAdminValidationAutoRefresh: 'automatically re-checked roughly every',
4896
+ epistolaAdminValidationLatestVersionNote: "Only the latest deployed version of each process definition is checked; older versions with running instances may have problems that aren't shown here.",
4565
4897
  epistolaAdminCatalogs: 'Catalogs',
4566
4898
  epistolaAdminCatalogsIntro: 'Catalogs on the application classpath. These are deployed automatically on startup; use Redeploy to manually (force) push one to this Epistola installation — for example when the automatic startup deploy failed.',
4567
4899
  epistolaAdminNoCatalogs: 'No catalogs found on the classpath.',
@@ -4577,6 +4909,18 @@ const epistolaPluginSpecification = {
4577
4909
  epistolaAdminChangelog: 'Changelog',
4578
4910
  epistolaAdminRunningVersion: 'Running plugin version:',
4579
4911
  epistolaAdminNoChangelog: 'No changelog available in this build.',
4912
+ // TEMPORARY (remove in 1.0.0): task-id carrier repair (admin "Forms" tab)
4913
+ epistolaAdminForms: 'Forms',
4914
+ epistolaAdminFormsIntro: 'Forms with an Epistola component that is missing the hidden task-id field. Without it, preview/download/retry do not work in every task-open flow. Repair adds the field.',
4915
+ epistolaAdminFormName: 'Form',
4916
+ epistolaAdminFormMissing: 'Missing components',
4917
+ epistolaAdminFormReadOnly: 'Read-only',
4918
+ epistolaAdminFormReadOnlyHint: 'This form is deployed from the classpath and is reconciled to its source on the next restart. Add the field to the source (re-drop the component) for a permanent fix.',
4919
+ epistolaAdminRepair: 'Repair',
4920
+ epistolaAdminRepairAll: 'Repair all',
4921
+ epistolaAdminRepairing: 'Working...',
4922
+ epistolaAdminRepairTooltip: 'Add the hidden task-id field to all Epistola components in this form.',
4923
+ epistolaAdminNoFormIssues: 'No forms with a missing task-id field found.',
4580
4924
  },
4581
4925
  },
4582
4926
  };
@@ -4589,5 +4933,5 @@ const epistolaPluginSpecification = {
4589
4933
  * Generated bundle index. Do not edit.
4590
4934
  */
4591
4935
 
4592
- export { CheckJobStatusConfigurationComponent, DownloadDocumentConfigurationComponent, EPISTOLA_DOCUMENT_OPTIONS, EPISTOLA_DOCUMENT_PREVIEW_OPTIONS, EPISTOLA_OVERRIDE_BUILDER_OPTIONS, EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS, EPISTOLA_RETRY_FORM_OPTIONS, EpistolaAdminPageComponent, EpistolaAdminRoutingModule, EpistolaAdminService, EpistolaConfigurationComponent, EpistolaDocumentComponent, EpistolaDocumentPreviewComponent, EpistolaMenuService, EpistolaOverrideBuilderComponent, EpistolaPluginModule, EpistolaPluginService, EpistolaProcessLinkSelectorComponent, EpistolaRetryFormComponent, EpistolaTaskContextInterceptor, EpistolaTaskContextService, GenerateDocumentConfigurationComponent, JsonataEditorComponent, MappingBuilderComponent, epistolaPluginSpecification, errorResource, initialResource, isEpistolaEnabled, loadingResource, registerEpistolaDocumentComponent, registerEpistolaDocumentPreviewComponent, registerEpistolaOverrideBuilderComponent, registerEpistolaProcessLinkSelectorComponent, registerEpistolaRetryFormComponent, successResource };
4936
+ export { CheckJobStatusConfigurationComponent, DownloadDocumentConfigurationComponent, EPISTOLA_DOCUMENT_OPTIONS, EPISTOLA_DOCUMENT_PREVIEW_OPTIONS, EPISTOLA_OVERRIDE_BUILDER_OPTIONS, EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS, EPISTOLA_RETRY_FORM_OPTIONS, EpistolaAdminPageComponent, EpistolaAdminRoutingModule, EpistolaAdminService, EpistolaConfigurationComponent, EpistolaDocumentComponent, EpistolaDocumentPreviewComponent, EpistolaMenuService, EpistolaOverrideBuilderComponent, EpistolaPluginModule, EpistolaPluginService, EpistolaProcessLinkSelectorComponent, EpistolaRetryFormComponent, GenerateDocumentConfigurationComponent, JsonataEditorComponent, MappingBuilderComponent, PREFILLED_TASK_ID_CARRIER, PREFILLED_TASK_ID_DATA_KEY, PREFILLED_TASK_ID_SOURCE_KEY, epistolaPluginSpecification, errorResource, initialResource, isEpistolaEnabled, loadingResource, readPrefilledTaskId, registerEpistolaDocumentComponent, registerEpistolaDocumentPreviewComponent, registerEpistolaOverrideBuilderComponent, registerEpistolaProcessLinkSelectorComponent, registerEpistolaRetryFormComponent, successResource };
4593
4937
  //# sourceMappingURL=epistola.app-valtimo-plugin.mjs.map