Compare commits

...
Author SHA1 Message Date
copilot-swe-agent[bot] 51b996d8cb Initial plan 2026-06-07 20:16:58 +00:00
Jeppe Bundgaard 880680d7f8 Add reusable ErrorBanner component and integrate into MyWashStart 2026-06-07 22:07:56 +02:00
Jeppe Bundgaard 7d5ed772c2 Avoid empty MyWash lane service sync 2026-06-04 07:40:33 +02:00
Jeppe Bundgaard 83fbcdeaa8 Refresh MyWash eligibility before service sync 2026-06-04 07:31:41 +02:00
Jeppe Bundgaard 55a761d3a5 Publish MyWash tasks before hydration 2026-06-04 07:27:12 +02:00
Jeppe Bundgaard abe8e7750f Preserve MyWash machine services during sync 2026-06-04 07:21:07 +02:00
Jeppe Bundgaard aeef75b831 Clear stale MyWash summary after preview 2026-06-04 07:13:00 +02:00
Jeppe Bundgaard d6356d0244 Stop MyWash eligibility refresh after completion 2026-06-04 06:35:26 +02:00
Jeppe Bundgaard bbf2aeb95f Keep MyWash completion visible after stop 2026-06-04 06:05:41 +02:00
Jeppe Bundgaard 3a7d78ae05 Serve release-prefixed index shell 2026-06-04 04:08:33 +02:00
Jeppe Bundgaard 2a2f752692 Refresh recent completion marker during startup 2026-06-04 03:51:43 +02:00
Jeppe Bundgaard 8883bf8a07 Seed recently completed wash state before startup awaits 2026-06-04 03:44:29 +02:00
Jeppe Bundgaard 6ec80e2796 Recover recently completed self-serve screen 2026-06-04 03:29:55 +02:00
Jeppe Bundgaard 849a7b77cf Adopt resolved self-serve vehicle type 2026-06-04 03:16:59 +02:00
Jeppe Bundgaard 36ccd0c381 Keep completed wash fallback visible 2026-06-04 03:01:09 +02:00
Jeppe Bundgaard 58e7c8d6c9 Keep completed wash after lane refresh 2026-06-04 02:44:48 +02:00
Jeppe Bundgaard d04fc8cc0f Keep completed self-serve wash visible 2026-06-04 02:29:16 +02:00
Jeppe Bundgaard 339a1b1452 Recover completed wash after stop timeout 2026-06-04 02:15:15 +02:00
Jeppe Bundgaard 2d2d63d09e Merge remote-tracking branch 'origin/master' 2026-06-04 02:02:26 +02:00
Jeppe B 8241f7fd2f Treat unoccupied stop invoice errors as completed washes 2026-06-04 02:01:35 +02:00
Jeppe Bundgaard 985571af87 Treat unoccupied stop invoice errors as completed washes 2026-06-04 01:55:35 +02:00
Jeppe Bundgaard 82e1e4a466 Ignore preview-only active wash reservations 2026-06-04 01:21:25 +02:00
Jeppe Bundgaard 9a7f7bbb78 Prevent preview-only self-serve sessions from auto-restoring 2026-06-04 01:08:04 +02:00
Jeppe Bundgaard 359f29b75e Keep self-serve preview questions visible during summary load 2026-06-04 00:45:20 +02:00
Jeppe B ceced7cbaa Show MyWash completion when Buefy hides step panel 2026-06-04 00:21:33 +02:00
Jeppe Bundgaard 6b5cd00b10 Show completed wash outside step panels 2026-06-04 00:10:36 +02:00
Jeppe Bundgaard 02eae47d34 Refactor toggle-task event emission in SelfServeTaskList component 2026-06-04 00:00:28 +02:00
Jeppe Bundgaard 3f01d38676 Enhance self-serve image loading with preload and error handling 2026-06-03 23:53:54 +02:00
Jeppe Bundgaard 390a29ef55 Merge remote-tracking branch 'origin/master' 2026-06-03 23:34:05 +02:00
Jeppe Bundgaard b7f5786747 Add vehicle type selection logic for departments without product allow-list 2026-06-03 23:33:54 +02:00
Jeppe B 814c535757 Use relative release manifest asset paths 2026-06-03 23:13:26 +02:00
Jeppe Bundgaard ed0e27e4fd Use relative release manifest asset paths 2026-06-03 23:07:17 +02:00
Jeppe Bundgaard 659df480d7 Merge remote-tracking branch 'origin/master' 2026-06-03 23:04:43 +02:00
Jeppe B d0c45f6180 Fix release frontend origin routing 2026-06-03 22:45:32 +02:00
Jeppe Bundgaard 5a20461bd4 Fix release frontend origin routing 2026-06-03 22:40:49 +02:00
Jeppe Bundgaard 849f52fb8e Refactor error message display and update test descriptions for clarity 2026-06-03 22:16:27 +02:00
Jeppe Bundgaard da0c699b88 Add self-serve session handling and lane selection normalization 2026-06-03 22:07:47 +02:00
Jeppe Bundgaard af5514c982 Implement dynamic base path handling for frontend assets and update routing logic 2026-06-03 22:00:18 +02:00
Jeppe Bundgaard cc689e80a9 Use release base for frontend HTML assets 2026-06-03 21:45:25 +02:00
Jeppe Bundgaard 3b51374678 Add dynamic release frontend base path handling and update static asset references 2026-06-03 21:44:27 +02:00
Jeppe Bundgaard f07569db98 Allow active self-serve lane selection 2026-06-03 21:32:58 +02:00
Jeppe Bundgaard c5fcb90683 Include emitted chunks in release manifest 2026-06-03 21:12:08 +02:00
Jeppe Bundgaard 270f87dee1 Split release smoke static and app targets 2026-06-03 21:03:03 +02:00
Jeppe Bundgaard 765b8f3b35 Fix self-serve entry regressions 2026-06-03 20:51:06 +02:00
Jeppe Bundgaard 3ff695899c Restore self-serve task phase from active sessions 2026-06-03 20:32:40 +02:00
Jeppe Bundgaard be35baa838 Refactor self-serve registration field for improved layout and accessibility 2026-06-03 20:06:31 +02:00
Jeppe B 470d4d7314 Normalize release bootstrap cleanup 2026-06-03 19:38:54 +02:00
Jeppe B 0976608a25 Refresh release bootstrap asset hash 2026-06-03 19:34:48 +02:00
Jeppe B e7f4dfa07e Trigger frontend release after asset mismatch 2026-06-03 19:23:01 +02:00
Jeppe B 2c68f335c6 Trigger frontend release for self-serve vehicle fix 2026-06-03 18:08:25 +02:00
Jeppe Bundgaard cf29589e29 Fix self-serve vehicle step progression 2026-06-03 17:51:46 +02:00
Jeppe Bundgaard 05cabd8edc Add margin and padding adjustments to self-serve task checkbox 2026-06-02 20:09:18 +02:00
Jeppe Bundgaard 2f4ecba51a Merge remote-tracking branch 'origin/master' 2026-06-02 20:04:11 +02:00
Jeppe Bundgaard 74a7c9f23f Refactor self-serve task list to improve task title handling and remove unnecessary description checks 2026-06-02 20:03:55 +02:00
Jeppe B b075a717f7 Cover hidden customer number self-serve flow 2026-06-02 19:59:30 +02:00
Jeppe B a3dcb888d3 Cover self-serve task title presentation 2026-06-02 19:59:12 +02:00
Jeppe B 02706af508 Allow QR self-serve vehicle step without customer field 2026-06-02 19:58:50 +02:00
Jeppe B 1f2c007776 Tighten self-serve task presentation 2026-06-02 19:58:28 +02:00
Jeppe Bundgaard 42dfdfef13 Add mocks for department daily report components and update Vitest configuration for SCSS 2026-06-02 17:54:37 +02:00
Jeppe B b40bf0b9e1 Merge pull request #110 from copenhagentruckwash/create-playwright-test-suite-for-mywashstart
Add mocked Playwright tests for MyWashStart flow and improve answer-sync & department fallback
2026-06-02 13:52:00 +02:00
Jeppe B cc15a9c468 Merge branch 'master' into create-playwright-test-suite-for-mywashstart 2026-06-02 13:51:52 +02:00
Jeppe B 5d8b1eecb4 Merge pull request #111 from copenhagentruckwash/create-playwright-test-suite-for-mywashstart-6chtnh
Add mocked Playwright tests for MyWashStart flow and improve answer-sync & department fallback
2026-06-02 13:50:32 +02:00
Jeppe B 093d82d762 Merge branch 'master' into create-playwright-test-suite-for-mywashstart-6chtnh 2026-06-02 13:50:24 +02:00
Jeppe B 686577c209 Merge pull request #114 from copenhagentruckwash/update-mywashstart.vue-with-timeout-handling
Fix delayed active wash restore cleanup
2026-06-02 13:48:47 +02:00
Jeppe B c2b333a78b Merge branch 'master' into update-mywashstart.vue-with-timeout-handling 2026-06-02 13:48:33 +02:00
Jeppe B cef8b8bf0f Merge pull request #115 from copenhagentruckwash/update-userdashboard-to-inspect-lane-readiness
Guard wash entry self-serve CTA
2026-06-02 13:47:39 +02:00
Jeppe B 62a46710ba Merge branch 'master' into update-userdashboard-to-inspect-lane-readiness 2026-06-02 13:47:31 +02:00
Jeppe B a3e6feb07e Merge pull request #113 from copenhagentruckwash/refactor-self-serve-data-loading-with-fetch-scheduler
Debounce self-serve wash data fetches
2026-06-02 13:43:40 +02:00
Jeppe B 2ad093d4f1 Merge pull request #112 from copenhagentruckwash/update-mywashstart.vue-for-answer-sync-state
Block wash question flow on failed answer sync
2026-06-02 13:43:22 +02:00
Jeppe B 6017d576fc Merge pull request #116 from copenhagentruckwash/update-wash-department-handling-and-ui
Handle self-serve wash when geolocation is unavailable
2026-06-02 13:42:51 +02:00
Jeppe B 08ac38dc87 Merge pull request #117 from copenhagentruckwash/add-and-extend-unit-and-e2e-tests
Add production-ready tests for user self-serve wash start (unit + mocked e2e)
2026-06-02 13:42:36 +02:00
Jeppe B 3960daa556 Add production wash start test matrix 2026-06-02 13:42:22 +02:00
Jeppe B 395cbd87dd Handle self-serve wash without geolocation 2026-06-02 13:42:16 +02:00
Jeppe B 23429c047f Guard wash entry self-serve CTA 2026-06-02 13:42:09 +02:00
Jeppe B af68748c16 Fix active wash restore timeout cleanup 2026-06-02 13:42:02 +02:00
Jeppe B 18ab007f87 Debounce self-serve wash data fetches 2026-06-02 13:41:55 +02:00
Jeppe B 63942045be Block wash question flow on failed answer sync 2026-06-02 13:41:51 +02:00
Jeppe B a4cdc6e4f3 Add mocked MyWashStart flow coverage 2026-06-02 13:41:44 +02:00
Jeppe B 07c611bdbe Add mocked MyWashStart flow coverage 2026-06-02 13:41:32 +02:00
Jeppe Bundgaard 5170139a32 Switch CI to self-hosted runners
Updated all GitHub Actions workflows to use self-hosted runners instead of `ubuntu-latest`. This change ensures better control over the CI environment and aligns with internal infrastructure requirements.
2026-06-02 10:36:18 +02:00
Jeppe Bundgaard 90952a68b7 Refactor SubusersTable to use ActionSettingsWheelButton for row actions 2026-06-02 09:23:49 +02:00
Jeppe Bundgaard 1cc14d7b41 Merge remote-tracking branch 'origin/master' 2026-06-02 08:36:42 +02:00
Jeppe B aa4d6576f7 Merge pull request #108 from copenhagentruckwash/fix-page-crash-issues
Fix wash start recovery from API failures
2026-06-02 08:35:28 +02:00
Jeppe B 2a46984f9c Merge pull request #109 from copenhagentruckwash/fix-playwright-exit-code-error
Stabilize release sidebar E2E coverage
2026-06-02 08:34:47 +02:00
Jeppe Bundgaard 4a9c721c55 Remove debug message for beta frontend in build output 2026-06-02 08:12:50 +02:00
Jeppe B 7be9995c87 Stabilize release sidebar e2e coverage 2026-06-02 08:10:51 +02:00
Jeppe B 85529de3f8 Fix wash start recovery from API failures 2026-06-02 08:07:03 +02:00
Jeppe B 114ca9d956 Merge pull request #107 from copenhagentruckwash/investigate-and-fix-failing-tests
Resolve failing frontend CI: fix merge conflicts, restore edge cache & tests
2026-06-02 07:15:41 +02:00
Jeppe B 29f858685a Fix self-serve wash smoke restore fixture 2026-06-02 07:15:26 +02:00
Jeppe B 4112faf682 Fix edge gateway broker config e2e 2026-06-02 02:38:37 +02:00
Jeppe B a13b73c414 Harden edge gateway service parsing 2026-06-02 02:06:32 +02:00
Jeppe B 667e93af8f Resolve failing frontend checks 2026-06-02 01:13:28 +02:00
Jeppe B 54a888026e Merge pull request #104 from copenhagentruckwash/propose-fix-for-price-tampering-vulnerability
Fix trusted booking prices in POS hydration
2026-06-02 00:48:56 +02:00
Jeppe B a0fb02baa1 Merge pull request #106 from copenhagentruckwash/fix-booking-delete-access-permission
Fix booking delete action visibility
2026-06-02 00:47:38 +02:00
Jeppe B c445b932db Fix booking delete action visibility 2026-06-02 00:47:19 +02:00
copilot-swe-agent[bot] ffccb1683b Merge origin/master and resolve POS pricing conflict 2026-06-01 22:44:56 +00:00
Jeppe B 0d590fc9d6 Merge pull request #105 from copenhagentruckwash/fix-postinstall-script-vulnerability
Remove unsafe postinstall hook
2026-06-02 00:40:46 +02:00
Jeppe B 1d4910f947 Remove unsafe postinstall hook 2026-06-02 00:40:34 +02:00
Jeppe B 7e457ed961 Fix trusted booking prices in POS hydration 2026-06-02 00:39:55 +02:00
Jeppe B 34722c6985 Merge pull request #103 from copenhagentruckwash/fix-vulnerability-in-pos-order-pricing
Fix POS booking price hydration
2026-06-02 00:39:37 +02:00
Jeppe B c5a13b50ab Fix POS booking price hydration 2026-06-02 00:39:15 +02:00
Jeppe B 387bd50281 Merge pull request #102 from copenhagentruckwash/fix-unsandboxed-blob-attachment-previews
Sanitize and sandbox order attachment previews to prevent XSS
2026-06-02 00:38:15 +02:00
Jeppe B 81016fd898 Fix unsafe order attachment previews 2026-06-02 00:38:03 +02:00
Jeppe B 39fc1612ac Merge pull request #101 from copenhagentruckwash/fix-duplicate-stripe-invoice-submission-issue
Prevent duplicate Stripe invoice queue submissions
2026-06-02 00:30:18 +02:00
Jeppe B 7ab12a3fff Prevent duplicate Stripe invoice queue submissions 2026-06-02 00:29:53 +02:00
Jeppe B b4d869c469 Merge pull request #100 from copenhagentruckwash/fix-autosave-to-prevent-infinite-retry-loop
Fix autosave failed-save retry loop
2026-06-02 00:28:52 +02:00
Jeppe B 799b667828 Fix autosave failed-save retry loop 2026-06-02 00:28:39 +02:00
Jeppe B ae3513edf3 Merge pull request #99 from copenhagentruckwash/fix-repeated-booking-prefetch-flooding-api
Avoid duplicate booking prefetch by honoring exact-plate cache
2026-06-02 00:27:37 +02:00
Jeppe B 6db1861b78 Fix repeated booking prefetch requests 2026-06-02 00:27:25 +02:00
Jeppe B a8e839ba18 Merge pull request #98 from copenhagentruckwash/fix-mobile-pos-orders-completion-issue
Fix mobile POS order completion
2026-06-02 00:27:07 +02:00
Jeppe B b224cf67d1 Fix mobile POS order completion 2026-06-02 00:26:53 +02:00
Jeppe B 222237ab34 Merge pull request #96 from copenhagentruckwash/fix-edge-gateway-localstorage-vulnerability
Harden edge gateway workspace cache
2026-06-02 00:16:07 +02:00
copilot-swe-agent[bot] 433cfe521e Planning merge conflict resolution 2026-06-01 22:14:58 +00:00
Jeppe B 1e4a776fa5 Merge pull request #97 from copenhagentruckwash/fix-auto-selection-of-customer-suggestions
Fix stale POS customer suggestion auto-selection
2026-06-02 00:13:59 +02:00
Jeppe B e88a424a7d Fix stale POS customer suggestion auto-selection 2026-06-02 00:13:46 +02:00
Jeppe B 9450c7902d Harden edge gateway workspace cache 2026-06-02 00:08:20 +02:00
Jeppe B 75b5883afe Merge pull request #95 from copenhagentruckwash/fix-stripe-email-invoice-payment-misclassification
Fix Stripe email invoice paid validation
2026-06-02 00:07:49 +02:00
Jeppe B c26f0184d9 Fix Stripe email invoice paid validation 2026-06-02 00:07:35 +02:00
Jeppe B cb74b4bbe7 Merge pull request #93 from copenhagentruckwash/propose-fix-for-cached-gateway-details-leak
Scope edge gateway cache to session and prevent auth-blind cached fallbacks
2026-06-01 23:48:26 +02:00
Jeppe B 326eca656f Fix edge gateway cache authorization fallback 2026-06-01 23:48:17 +02:00
Jeppe B f0a5058b68 Merge pull request #92 from copenhagentruckwash/propose-fix-for-self-hosted-ci-vulnerability
Use GitHub-hosted runners for tests workflow
2026-06-01 23:46:15 +02:00
copilot-swe-agent[bot] d39cd36995 Merge remote-tracking branch 'origin/master' into propose-fix-for-self-hosted-ci-vulnerability
# Conflicts:
#	.github/workflows/tests.yml
2026-06-01 21:43:04 +00:00
Jeppe B 0548018b5c Use GitHub-hosted runners for tests workflow 2026-06-01 23:40:30 +02:00
Jeppe B 699e10856e Merge pull request #91 from copenhagentruckwash/propose-fix-for-self-hosted-runner-vulnerability
Fix CI runner exposure for pull requests
2026-06-01 23:40:09 +02:00
Jeppe B 36b399cb2f Fix CI runner exposure for pull requests 2026-06-01 23:40:00 +02:00
Jeppe B f613d58727 Merge pull request #90 from copenhagentruckwash/propose-fix-for-product-catalog-vulnerability
Restrict vehicle type product edits to superusers
2026-06-01 23:35:14 +02:00
Jeppe B feccfada77 Restrict vehicle type product edits to superusers 2026-06-01 23:35:04 +02:00
Jeppe B 4127e66114 Merge pull request #89 from copenhagentruckwash/fix-unvalidated-attachment-download-links
Validate self-serve attachment download links
2026-06-01 23:34:45 +02:00
Jeppe B b2a9b59399 Validate self-serve attachment download links 2026-06-01 23:34:34 +02:00
Jeppe B 6ffed9c2f2 Merge pull request #88 from copenhagentruckwash/fix-lane-toggle-guard-permission-issue
Fix lane toggle permission guards
2026-06-01 23:34:14 +02:00
Jeppe B 53e193d8d1 Fix lane toggle permission guards 2026-06-01 23:34:03 +02:00
Jeppe B 7835691bc9 Merge pull request #87 from copenhagentruckwash/fix-websocket-token-exposure-vulnerability
Secure edge gateway WebSocket session tokens
2026-06-01 23:33:44 +02:00
Jeppe B 91715541ec Secure edge gateway websocket sessions 2026-06-01 23:33:34 +02:00
Jeppe B e3f9e99129 Merge pull request #86 from copenhagentruckwash/fix-stored-xss-in-relay-options
Fix select option HTML escaping
2026-06-01 23:33:20 +02:00
Jeppe B 4634cbad89 Fix select option HTML escaping 2026-06-01 23:33:10 +02:00
Jeppe B 5db2a36da0 Merge pull request #85 from copenhagentruckwash/propose-fix-for-relay-failure-issue
Rollback wash start when relay activation fails
2026-06-01 22:53:44 +02:00
Jeppe B cd7b273da4 Rollback wash start when relay activation fails 2026-06-01 22:53:34 +02:00
Jeppe B cbbad1a6e2 Merge pull request #84 from copenhagentruckwash/fix-pos-url-order-reassignment-vulnerability
Prevent implicit POS order department changes
2026-06-01 22:44:24 +02:00
Jeppe B 9661d4efa7 Prevent implicit POS order department changes 2026-06-01 22:44:14 +02:00
Jeppe B 31762c23c5 Merge pull request #83 from copenhagentruckwash/fix-unescaped-order-delete-reasons
Fix protected order delete reason escaping
2026-06-01 22:43:55 +02:00
Jeppe B c71a6b4d14 Fix protected order delete reason escaping 2026-06-01 22:43:45 +02:00
Jeppe B e8ddb33f21 Merge pull request #82 from copenhagentruckwash/fix-vulnerability-in-edge-broker-config
Secure edge gateway broker configuration
2026-06-01 22:28:28 +02:00
Jeppe B ea368d19bb Secure edge gateway broker configuration 2026-06-01 22:28:18 +02:00
Jeppe B 7ab50b6290 Merge pull request #81 from copenhagentruckwash/fix-unredacted-broker-diagnostics-exposure
Redact edge gateway diagnostic secrets
2026-06-01 22:27:33 +02:00
Jeppe B fd7cfbd3c3 Redact edge gateway diagnostics 2026-06-01 22:27:19 +02:00
Jeppe B 90725af10a Merge pull request #80 from copenhagentruckwash/fix-ungated-destructive-actions-in-ui
Gate self-serve studio gateway actions and require confirmation for destructive operations
2026-06-01 22:26:58 +02:00
Jeppe B d63a62a921 Gate self-serve gateway actions 2026-06-01 22:26:44 +02:00
Jeppe B 4bc5481941 Merge pull request #79 from copenhagentruckwash/propose-fix-for-deferred-start-relay-issue
Require successful machine relay activation before marking wash in progress
2026-06-01 22:15:16 +02:00
Jeppe B 9feede3b0d Require machine relay activation before wash progress 2026-06-01 22:15:02 +02:00
Jeppe B 5aad9eda8d Merge pull request #78 from copenhagentruckwash/fix-raw-edge-log-context-exposure
Redact edge gateway log context secrets
2026-06-01 22:13:42 +02:00
Jeppe B 91b15faba6 Redact edge gateway log context secrets 2026-06-01 22:13:32 +02:00
Jeppe B 98c1c61cbd Merge pull request #77 from copenhagentruckwash/fix-active-wash-endpoint-information-leak
Scope active-wash restore to authenticated customer (my-active-wash)
2026-06-01 22:06:25 +02:00
Jeppe B 4463a5326a Fix scoped active wash restoration 2026-06-01 22:06:14 +02:00
Jeppe B bdb23513a4 Merge pull request #76 from copenhagentruckwash/propose-fix-for-security-vulnerability
Harden self-serve vehicle condition mutations
2026-06-01 22:05:37 +02:00
Jeppe B ee40f16232 Harden self-serve vehicle condition mutations 2026-06-01 22:05:27 +02:00
Jeppe B a76944b00c Merge pull request #75 from copenhagentruckwash/fix-sessionstorage-vulnerability-in-invoicing-period
Secure privileged session caches
2026-06-01 21:52:53 +02:00
Jeppe B cf074ff893 Secure privileged session caches 2026-06-01 21:52:42 +02:00
Jeppe B 712eb19db5 Merge pull request #74 from copenhagentruckwash/propose-fix-for-release-manager-token-leak
Restrict release manager control API origins
2026-06-01 21:52:20 +02:00
Jeppe B de9bc9b170 Restrict release manager control API origins 2026-06-01 21:52:09 +02:00
Jeppe B 95e2404307 Merge pull request #73 from copenhagentruckwash/fix-unredacted-deployment-failures-exposure
Redact release deployment failure diagnostics
2026-06-01 21:27:42 +02:00
Jeppe B 2c01feb3be Redact release deployment failure diagnostics 2026-06-01 21:27:28 +02:00
Jeppe B f416e0d462 Merge pull request #72 from copenhagentruckwash/fix-telemetry-that-leaks-passwords
Redact serialized request bodies and omit failed-request bodies when capture is disabled
2026-06-01 21:26:45 +02:00
Jeppe B 7f1308c8ce Redact release timeline failed request bodies 2026-06-01 21:26:32 +02:00
Jeppe B fd8d45e437 Merge pull request #71 from copenhagentruckwash/fix-vulnerabilities-in-release-runtime-urls
Harden release runtime URL trust boundaries
2026-06-01 21:25:51 +02:00
copilot-swe-agent[bot] 20390342d5 Merge origin/master and resolve release timeline spec conflict 2026-06-01 19:21:12 +00:00
Jeppe B c47bf229f0 Harden release runtime URL trust boundaries 2026-06-01 21:14:39 +02:00
Jeppe B 3616e197e1 Merge pull request #70 from copenhagentruckwash/propose-fix-for-isolated-stack-vulnerability
Prevent active isolated stack reuse during release deploys
2026-06-01 21:12:26 +02:00
Jeppe B 3d8544de21 Prevent isolated stack deployment reuse 2026-06-01 21:11:17 +02:00
Jeppe B 504abb7a15 Merge pull request #69 from copenhagentruckwash/fix-release-subject-autocomplete-exposure
Guard release assignment subject search
2026-06-01 21:11:02 +02:00
Jeppe B 37021533b9 Guard release assignment search by manage permission 2026-06-01 21:10:51 +02:00
Jeppe B 59eab84f03 Merge pull request #68 from copenhagentruckwash/fix-cross-origin-framing-vulnerability-in-nginx
Add anti-framing headers to Coolify Nginx frontend
2026-06-01 21:10:26 +02:00
Jeppe B e67033711c Add anti-framing headers to Coolify Nginx config 2026-06-01 21:10:14 +02:00
Jeppe B 85fd00f020 Merge pull request #67 from copenhagentruckwash/fix-metadata-exposure-for-low-privileged-users
Restrict release runtime metadata inspector
2026-06-01 21:09:54 +02:00
Jeppe B 4f064a1625 Restrict release runtime metadata inspector 2026-06-01 21:09:42 +02:00
Jeppe B 3bf7ada226 Merge pull request #66 from copenhagentruckwash/fix-tls-verification-in-dev-proxy
Secure Vite API proxy TLS by default
2026-06-01 21:09:15 +02:00
Jeppe B 3b90e17a1e Secure Vite API proxy TLS by default 2026-06-01 21:09:05 +02:00
Jeppe B c1fb5b72ee Merge pull request #65 from copenhagentruckwash/fix-privilege-escalation-in-daily-report
Guard department lane toggles by wash-lane permission
2026-06-01 21:08:43 +02:00
Jeppe B 29da26cc08 Guard department lane toggles by permission 2026-06-01 21:08:32 +02:00
Jeppe B 807fc09b1e Merge pull request #64 from copenhagentruckwash/fix-dgnvask-toggle-issue
Fix Døgnvask toggle state with configuration warnings
2026-06-01 21:08:03 +02:00
Jeppe B 528e4994db Fix Dognvask warning toggle state 2026-06-01 21:07:53 +02:00
Jeppe B c339122f4f Merge pull request #63 from copenhagentruckwash/fix-release-gate-binding-to-build
Bind frontend release gate to build id
2026-06-01 21:07:33 +02:00
Jeppe B feeef46354 Bind frontend release gate to build id 2026-06-01 21:07:23 +02:00
Jeppe B 066bd14554 Merge pull request #62 from copenhagentruckwash/fix-active-wash-refresh-vulnerability
Harden active-wash refresh ownership check
2026-06-01 21:06:53 +02:00
Jeppe B bffaa2ab88 Fix active wash refresh ownership check 2026-06-01 21:06:39 +02:00
Jeppe B 31495ea67b Merge pull request #61 from copenhagentruckwash/fix-ftp-deploy-password-exposure
Secure lftp release upload credentials
2026-06-01 21:02:13 +02:00
Jeppe B 8f2533f1b2 Secure lftp release upload credentials 2026-06-01 21:02:01 +02:00
Jeppe B 13b0742deb Merge pull request #60 from copenhagentruckwash/fix-cropping-issue-in-path-editor
Fix self-serve path editor answer switch layout
2026-06-01 20:45:04 +02:00
Jeppe B e2b2d50e5e Fix self-serve path editor answer switch layout 2026-06-01 20:19:35 +02:00
Jeppe B 55d61e37f3 Merge pull request #59 from copenhagentruckwash/fix-e2e-tests-in-i18n-v2-catalog
Fix i18n v2 catalog integrity and split Release Manager source fragments
2026-06-01 19:45:57 +02:00
Jeppe B fa19f5a704 Fix i18n v2 catalog integrity 2026-06-01 19:44:50 +02:00
Jeppe Bundgaard b995031255 Enable "Selv vask" button in MobileFooter.vue 2026-06-01 19:43:57 +02:00
Jeppe Bundgaard d096444ca0 Remove unused lane unavailable warning message from SelfServeLaneStep.vue 2026-06-01 19:41:16 +02:00
Jeppe B 10358d4f53 Merge pull request #58 from copenhagentruckwash/fix-self-serve-path-editor-for-all-departments
Fix self-serve path editor vehicle scope
2026-06-01 18:32:11 +02:00
Jeppe B 78f41b8380 Fix self-serve path editor vehicle scope 2026-06-01 18:31:53 +02:00
Jeppe B 90d4bc7c04 Merge pull request #57 from copenhagentruckwash/fix-bugs-in-start-wash-flow
Harden customer wash start flow
2026-06-01 18:01:29 +02:00
Jeppe B b59c17f120 Harden customer wash start flow 2026-06-01 17:54:42 +02:00
Jeppe Bundgaard c279e7fd38 Merge remote-tracking branch 'origin/master' 2026-06-01 17:23:06 +02:00
Jeppe Bundgaard 8a4fcb3e62 Remove unused return messages and cleanup peer dependency flags in package-lock.json 2026-06-01 17:22:33 +02:00
Jeppe B 680ef7088b Merge pull request #56 from copenhagentruckwash/investigate-playwright-exit-code-1-issue
Stabilize POS email notification E2E setup
2026-06-01 17:20:54 +02:00
179 changed files with 12660 additions and 4336 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ on:
jobs:
upload-qodana-config:
runs-on: ubuntu-latest
runs-on: [self-hosted, Linux, X64, default]
steps:
- name: Checkout repository
+1 -1
View File
@@ -9,7 +9,7 @@ on:
jobs:
upload-qodana-config:
runs-on: ubuntu-latest
runs-on: [self-hosted, Linux, X64, default]
steps:
- name: Checkout repository
+5 -3
View File
@@ -19,10 +19,12 @@ jobs:
runs-on: [self-hosted, Linux, X64, default]
env:
RELEASE_BASE_URL: https://api-v2.truckwash.io/master/frontend
PLAYWRIGHT_BASE_URL: https://api-v2.truckwash.io/master/frontend
PLAYWRIGHT_BASE_URL: https://dev.truckwash.io
PLAYWRIGHT_RELEASE_STATIC_BASE_URL: https://api-v2.truckwash.io/master/frontend
PLAYWRIGHT_RELEASE_API_BASE_URL: https://api-v2.truckwash.io
PLAYWRIGHT_RELEASE_API_PING_PATHS: /ping,/master/api/ping,/canary/api/ping,/stable/api/ping
PLAYWRIGHT_RELEASE_API_PING_PATHS: /master/api/ping
RELEASE_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
RELEASE_EXPECTED_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
RELEASE_EXPECTED_COMMIT: ${{ github.sha }}
RELEASE_WAIT_INITIAL_SECONDS: 45
RELEASE_WAIT_TIMEOUT_SECONDS: 600
@@ -98,7 +100,7 @@ jobs:
-X POST "$RELEASE_MANAGER_GATE_URL" \
-H "Authorization: Bearer $RELEASE_MANAGER_GATE_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"environment_url\":\"$RELEASE_BASE_URL\",\"channel_slug\":\"stable\",\"app\":\"frontend\",\"repository\":\"$RELEASE_REPOSITORY\",\"branch\":\"$RELEASE_BRANCH\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"workflow_url\":\"$RELEASE_WORKFLOW_URL\",\"auto_sync\":true,\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"static_artifact\",\"api_gateway\"]}"
--data "{\"environment_url\":\"$RELEASE_BASE_URL\",\"channel_slug\":\"stable\",\"app\":\"frontend\",\"repository\":\"$RELEASE_REPOSITORY\",\"branch\":\"$RELEASE_BRANCH\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"build_id\":\"$RELEASE_EXPECTED_BUILD_ID\",\"workflow_url\":\"$RELEASE_WORKFLOW_URL\",\"auto_sync\":true,\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"static_artifact\",\"api_gateway\"]}"
env:
RELEASE_MANAGER_GATE_URL: ${{ secrets.RELEASE_MANAGER_GATE_URL || 'https://api.truckwash.io/release/gate/test-runs' }}
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
+1 -1
View File
@@ -16,7 +16,7 @@ concurrency:
jobs:
format-tests:
# Match the labels exposed by the Coolify-managed GitHub runner.
# CI runs on the repository's self-hosted runner pool.
runs-on: [self-hosted, Linux, X64, default]
steps:
- name: Checkout repository
+4
View File
@@ -0,0 +1,4 @@
{
"version": 1,
"setupCompletedAt": "2026-06-07T11:37:04.444Z"
}
+8
View File
@@ -38,6 +38,14 @@ To use another remote API route:
$env:VITE_API_PROXY_BASE_PATH="/canary/api"; npm run dev
```
TLS certificate validation is enabled for proxied HTTPS APIs by default. If you
are using a trusted local HTTPS API with a self-signed certificate, you can opt
out explicitly:
```powershell
$env:VITE_API_PROXY_TARGET="https://local-api.test"; $env:VITE_API_PROXY_SECURE="false"; npm run dev
```
For compatible local gateways that expect the `/api` prefix to be preserved:
```powershell
@@ -0,0 +1,33 @@
# User wash start production-readiness QA matrix
This note documents deterministic coverage for the self-serve user wash start flow. The `@dynamic-image` Playwright case remains separated because it validates rendered image behavior in addition to deterministic state and API transitions.
## Unit matrix
| Area | Required scenario | Coverage |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useWashFlowState` | Step transitions for vehicle, questions, lane, tasks, in-progress, completed | `tests/unit/use-wash-flow-state-production.spec.js` validates step clickability, next-button gating, questions-to-lane updates, lane-to-start target selection, task completion transition, in-progress navigation, and completed-step non-clickability contract. |
| `useWashSessionActions` | START success and failure | `tests/unit/use-wash-session-actions-production.spec.js` covers successful manual START state mutation and failed START retryability without active-wash mutation. |
| `useWashSessionActions` | Relay enable success and relay enable failure with STOP rollback | `tests/unit/use-wash-session-actions-production.spec.js` covers machine relay success and relay failure rollback via STOP. |
| `useWashSessionActions` | STOP failure and already-stopped STOP recovery | `tests/unit/use-wash-session-actions-production.spec.js` covers failed STOP preserving active state and already-not-occupied STOP clearing local state. |
| `useSelfServeLogic` | Preview/summary merge | `tests/unit/use-self-serve-logic-production.spec.js` covers preview questions merging with summary questions, answer maps, visible question order, tasks, and allowed services. |
| `useSelfServeLogic` | Request race handling | `tests/unit/use-self-serve-logic-production.spec.js` covers stale preview/summary responses being ignored when a newer request wins. |
| `useSelfServeLogic` | Allowed services updates | `tests/unit/use-self-serve-logic-production.spec.js` covers lane allowed-service endpoint updates and first-failure fallback behavior. |
| `useSelfServeLogic` | Answer sync failures | `tests/unit/use-self-serve-logic-production.spec.js` covers error reporting while preserving the caller's optimistic local answer. |
| `MyWashStart.vue` | Local progress restore, server active-wash restore, recent-completion suppression, unmount cleanup, duplicate-fetch prevention | `tests/unit/my-wash-start-production.spec.js` locks the component contracts for restore ordering, authenticated active-wash application, recent-completion suppression across restore/polling, unmount cleanup, and duplicate-fetch/sync guards. |
## E2E mocked matrix
| Required scenario | Coverage |
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Full manual flow | `tests/e2e/self-serve-wash.spec.js` covers direct manual wash start, guided completion, STOP, exit-gate open, completed state, and close/reset. |
| Full machine flow | `tests/e2e/self-serve-wash.spec.js` covers machine task rendering, dynamic image progress, required task completion, machine start path, and reload persistence. |
| Reload/resume active wash | `tests/e2e/self-serve-wash.spec.js` covers local progress reload and authenticated server active-wash resume from another device. |
| Backend says wash completed during polling | `tests/e2e/self-serve-wash.spec.js` covers server polling of active wash and local transition to completed when the backend no longer reports the matching in-progress wash. |
| Network failures for preview, answer sync, START, relay enable, STOP | `tests/e2e/self-serve-wash.spec.js` covers retryable START failure, STOP failure preservation, allowed-services gateway timeout/retry, answer sync background resilience, and unit-level relay rollback. Add mocked network route overrides when expanding browser-level failure assertions. |
## Optional live smoke
| Optional scenario | Coverage |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Dynamic image smoke | The `@dynamic-image` Playwright test in `tests/e2e/self-serve-wash.spec.js` is tagged separately from deterministic CI coverage so it can be included or excluded explicitly with Playwright grep controls. |
+13 -4
View File
@@ -2,10 +2,19 @@
<html lang="" class="theme-light" data-theme="light">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/png" href="/assets/favicons/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/assets/favicons/favicon.svg" />
<link rel="shortcut icon" href="/assets/favicons/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicons/apple-touch-icon.png" />
<script>
(function () {
var match = window.location.pathname.match(/^\/[^/]+\/frontend(?:\/|$)/);
var href = match ? match[0].replace(/\/+$/, '') + '/' : '/';
var base = document.createElement('base');
base.href = href;
document.currentScript.after(base);
})();
</script>
<link rel="icon" type="image/png" href="%BASE_URL%assets/favicons/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="%BASE_URL%assets/favicons/favicon.svg" />
<link rel="shortcut icon" href="%BASE_URL%assets/favicons/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="%BASE_URL%assets/favicons/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="Truck Wash Kundeportal" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
+14 -1
View File
@@ -5,22 +5,31 @@ server {
root /usr/share/nginx/html;
index index.html;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
location ~ ^/(release-entry|release-manifest)\.json$ {
add_header Cache-Control "no-store";
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
try_files $uri =404;
}
location ~ ^/(master|beta|canary|internal)/frontend/(release-entry|release-manifest)\.json$ {
add_header Cache-Control "no-store";
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
try_files /$2.json =404;
}
location ~ ^/(?:.+/)?(?<static_asset_path>(?:assets|resources|favicons|icons|img|sounds|\.well-known)/.+)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
try_files /$static_asset_path =404;
}
location ~ ^/(?:.+/)?(?<static_file_path>manifest\.json|manifest\.webmanifest|favicon\.ico|favicon_default\.ico|pleno-favicon\.ico|release-entry\.json|release-manifest\.json|registerSW\.js|sw\.js|workbox-[^/]+\.js)$ {
location ~ ^/(?:.+/)?(?<static_file_path>index\.html|manifest\.json|manifest\.webmanifest|favicon\.ico|favicon_default\.ico|pleno-favicon\.ico|release-entry\.json|release-manifest\.json|registerSW\.js|sw\.js|workbox-[^/]+\.js)$ {
try_files /$static_file_path =404;
}
@@ -30,11 +39,15 @@ server {
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
try_files $uri =404;
}
location ~ ^/(master|beta|canary|internal)/frontend/assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
rewrite ^/(master|beta|canary|internal)/frontend/(.*)$ /$2 break;
try_files $uri =404;
}
+113 -19
View File
@@ -4624,7 +4624,7 @@ paths:
tags:
- Self-Serve
summary: Add vehicle condition
description: Add a new vehicle condition (answer to a question). Customers can only add conditions for their own vehicles.
description: Add a new vehicle condition (answer to a question). Customers can only add conditions for their own vehicles. This answer mutation does not activate machines or synchronize live relay state; hardware changes are handled only by the explicit wash start flow.
operationId: addSelfserveVehicleCondition
requestBody:
required: true
@@ -4659,14 +4659,6 @@ paths:
type: integer
nullable: true
description: Alias for vehicle_type.
activate_machine:
type: boolean
default: true
description: Whether the session synchronization may enable the machine relay. User wash-start saves answers with false.
sync_relay_state:
type: boolean
default: true
description: Whether the answer mutation should synchronize live relay state.
responses:
'200':
description: Successfully added vehicle condition
@@ -4683,7 +4675,7 @@ paths:
tags:
- Self-Serve
summary: Update vehicle condition
description: Update an existing vehicle condition. Customers can only update conditions for their own vehicles.
description: Update an existing vehicle condition. Customers can only update conditions for their own vehicles. This answer mutation does not activate machines or synchronize live relay state; hardware changes are handled only by the explicit wash start flow.
operationId: updateSelfserveVehicleCondition
parameters:
- name: id
@@ -4719,14 +4711,6 @@ paths:
type: integer
nullable: true
description: Alias for vehicle_type.
activate_machine:
type: boolean
default: true
description: Whether the session synchronization may enable the machine relay.
sync_relay_state:
type: boolean
default: true
description: Whether the mutation should synchronize live relay state.
responses:
'200':
description: Successfully updated vehicle condition
@@ -5402,7 +5386,7 @@ paths:
application/json:
schema:
type: object
required: [department, gateway_id, action]
required: [department, gateway_id, action, confirm]
properties:
department: { type: integer }
gateway_id: { type: integer }
@@ -8704,6 +8688,116 @@ paths:
schema:
$ref: '#/components/schemas/SelfServeLaneStatus'
/modules/self-serve/lane/wash/my-active-wash:
get:
tags:
- Modules
summary: Get the authenticated customer's active self-serve wash
description: |
Returns the current authenticated customer's open self-serve wash session,
if one exists. Regular customers must only receive their own active wash
details from this endpoint.
operationId: getMyActiveSelfServeWash
responses:
'200':
description: Authenticated customer's active wash details resolved
content:
application/json:
schema:
type: object
properties:
lane_id:
type: integer
nullable: true
in_progress:
type: boolean
session:
type: object
nullable: true
properties:
id:
type: integer
lane_id:
type: integer
nullable: true
department_id:
type: integer
nullable: true
status:
type: string
reg:
type: string
customer_number:
type: integer
nullable: true
vehicle_id:
type: integer
nullable: true
vehicle_type_id:
type: integer
nullable: true
included_minutes:
type: integer
nullable: true
machine_type_id:
type: integer
nullable: true
machine_relay_enabled:
type: boolean
machine_relay_enabled_at:
type: string
nullable: true
machine_start_triggered:
type: boolean
machine_start_triggered_at:
type: string
nullable: true
wash_started_at:
type: string
nullable: true
created_at:
type: string
updated_at:
type: string
nullable: true
customer:
type: object
nullable: true
properties:
id:
type: integer
nullable: true
customer_number:
type: integer
nullable: true
display_name:
type: string
nullable: true
email:
type: string
nullable: true
phone_country_code:
type: integer
nullable: true
phone:
type: string
nullable: true
vehicle:
type: object
nullable: true
properties:
id:
type: integer
customer_id:
type: integer
type:
type: integer
reg:
type: string
reference:
type: string
nullable: true
/modules/self-serve/lane/wash/in-progress:
get:
tags:
+15 -35
View File
@@ -7,7 +7,6 @@
"": {
"name": "truckwashdashboardsfrontend",
"version": "0.0.0",
"hasInstallScript": true,
"dependencies": {
"@azure/msal-browser": "^4.12.0",
"@bubblewrap/cli": "^1.23.0",
@@ -233,7 +232,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -1931,6 +1929,16 @@
}
}
},
"node_modules/@bubblewrap/core/node_modules/inquirer/node_modules/@types/node": {
"version": "25.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/@bubblewrap/core/node_modules/mute-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
@@ -1954,7 +1962,8 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/@bubblewrap/validator": {
"version": "1.22.0",
@@ -2077,7 +2086,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -2126,7 +2134,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -2564,7 +2571,6 @@
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz",
"integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"preact": "~10.12.1"
}
@@ -2808,7 +2814,6 @@
"resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz",
"integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/core": "^0.22.12"
}
@@ -2845,7 +2850,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz",
"integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -2858,7 +2862,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz",
"integrity": "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -2883,7 +2886,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz",
"integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12",
"tinycolor2": "^1.6.0"
@@ -2927,7 +2929,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz",
"integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3051,7 +3052,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz",
"integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3064,7 +3064,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz",
"integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3080,7 +3079,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz",
"integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -5343,7 +5341,6 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5378,7 +5375,6 @@
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -5472,8 +5468,7 @@
"version": "5.10.4",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-5.10.4.tgz",
"integrity": "sha512-gt0VUqZ2+mr25ScbUcKZgJr96jKYm4vjOcxEWCEh/E5F4dWqhyo3dBhPRvNNnkKiWxkMd2cBwj3ZYH3rK39fkA==",
"license": "SEE LICENSE IN LICENSE",
"peer": true
"license": "SEE LICENSE IN LICENSE"
},
"node_modules/aria-query": {
"version": "5.3.1",
@@ -5597,7 +5592,6 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
@@ -5821,7 +5815,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -5906,8 +5899,7 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz",
"integrity": "sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/bulma-block-list": {
"version": "1.1.0",
@@ -6165,7 +6157,6 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@kurkle/color": "^0.3.0"
},
@@ -6588,7 +6579,6 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -6777,7 +6767,6 @@
"integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==",
"deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-sfc": "2.7.16",
"csstype": "^3.1.0"
@@ -8335,8 +8324,7 @@
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz",
"integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==",
"license": "Apache-2.0",
"peer": true
"license": "Apache-2.0"
},
"node_modules/ieee754": {
"version": "1.2.1",
@@ -10403,7 +10391,6 @@
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"peer": true,
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
@@ -10758,7 +10745,6 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz",
"integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -10942,7 +10928,6 @@
"resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz",
"integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==",
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.1.5",
@@ -10964,7 +10949,6 @@
"integrity": "sha512-Do7u6iRb6K+lrllcTkB1BXcHwOxcKe3rEfOF/GcCLE2w3WpddakRAosJOHFUR37DpsvimQXEt5abs3NzUjEIqg==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@bufbuild/protobuf": "^2.5.0",
"colorjs.io": "^0.5.0",
@@ -12730,7 +12714,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -14123,7 +14106,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -14183,7 +14165,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz",
"integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.31",
"@vue/compiler-sfc": "3.5.31",
@@ -14871,7 +14852,6 @@
"integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
-1
View File
@@ -11,7 +11,6 @@
"format:tests:commit": "node scripts/pre-commit-format-tests.mjs",
"format:tests:check": "prettier --check \"tests/**/*.{js,ts}\"",
"prepare": "node scripts/prepare-husky.mjs",
"postinstall": "node scripts/postinstall-sync-playwright-root-links.mjs",
"preview": "vite preview",
"preview:prod": "npm run build && npm run preview -- --host 127.0.0.1 --port 4173",
"text:fix-encoding": "node scripts/text-encoding.mjs fix",
+4 -4
View File
@@ -23,17 +23,17 @@
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.+/((?:manifest\.json|manifest\.webmanifest|favicon\.ico|favicon_default\.ico|pleno-favicon\.ico|release-entry\.json|release-manifest\.json|registerSW\.js|sw\.js|workbox-[^/]+\.js))$ $1 [L]
RewriteRule ^.+/((?:index\.html|manifest\.json|manifest\.webmanifest|favicon\.ico|favicon_default\.ico|pleno-favicon\.ico|release-entry\.json|release-manifest\.json|registerSW\.js|sw\.js|workbox-[^/]+\.js))$ $1 [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{DOCUMENT_ROOT}/public/$1 -f
RewriteRule ^(?:.*?/)?((?:manifest\.json|manifest\.webmanifest|favicon\.ico|favicon_default\.ico|pleno-favicon\.ico|release-entry\.json|release-manifest\.json|registerSW\.js|sw\.js|workbox-[^/]+\.js))$ public/$1 [L]
RewriteRule ^(?:.*?/)?((?:index\.html|manifest\.json|manifest\.webmanifest|favicon\.ico|favicon_default\.ico|pleno-favicon\.ico|release-entry\.json|release-manifest\.json|registerSW\.js|sw\.js|workbox-[^/]+\.js))$ public/$1 [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{DOCUMENT_ROOT}/dist/$1 -f
RewriteRule ^(?:.*?/)?((?:manifest\.json|manifest\.webmanifest|favicon\.ico|favicon_default\.ico|pleno-favicon\.ico|release-entry\.json|release-manifest\.json|registerSW\.js|sw\.js|workbox-[^/]+\.js))$ dist/$1 [L]
RewriteRule ^(?:.*?/)?((?:index\.html|manifest\.json|manifest\.webmanifest|favicon\.ico|favicon_default\.ico|pleno-favicon\.ico|release-entry\.json|release-manifest\.json|registerSW\.js|sw\.js|workbox-[^/]+\.js))$ dist/$1 [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
@@ -41,7 +41,7 @@
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(?:.*?/)?(?:manifest\.json|manifest\.webmanifest|favicon\.ico|favicon_default\.ico|pleno-favicon\.ico|release-entry\.json|release-manifest\.json|registerSW\.js|sw\.js|workbox-[^/]+\.js)$ - [R=404,L]
RewriteRule ^(?:.*?/)?(?:index\.html|manifest\.json|manifest\.webmanifest|favicon\.ico|favicon_default\.ico|pleno-favicon\.ico|release-entry\.json|release-manifest\.json|registerSW\.js|sw\.js|workbox-[^/]+\.js)$ - [R=404,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
@@ -1,22 +0,0 @@
import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
const helperScriptPath = path.resolve(process.cwd(), "..", "scripts", "sync-playwright-root-links.mjs");
if (!fs.existsSync(helperScriptPath)) {
console.log(
`Skipping root Playwright link sync: helper script not found at ${helperScriptPath}.`
);
process.exit(0);
}
const result = spawnSync(process.execPath, [helperScriptPath], {
stdio: "inherit",
});
if (typeof result.status === "number") {
process.exit(result.status);
}
process.exit(1);
+35 -3
View File
@@ -23,20 +23,52 @@ if [[ -z "$DEPLOY_URL" ]]; then
echo "Set RELEASE_DEPLOY_URL or RELEASE_DEPLOY_HOST, RELEASE_DEPLOY_USER, and RELEASE_DEPLOY_PASSWORD." >&2
exit 1
fi
DEPLOY_URL="ftp://${USER_NAME}:${PASSWORD}@${HOST}"
DEPLOY_URL="ftp://${HOST}"
elif [[ -n "$USER_NAME" || -n "$PASSWORD" ]]; then
if [[ -z "$USER_NAME" || -z "$PASSWORD" ]]; then
echo "Set both RELEASE_DEPLOY_USER and RELEASE_DEPLOY_PASSWORD when providing deploy credentials separately." >&2
exit 1
fi
fi
lftp_quote() {
local value="${1//\'/\'\\\'\'}"
printf "'%s'" "$value"
}
run_lftp() {
local transfer_command="$1"
{
printf 'set ftp:ssl-allow true\n'
printf 'set ftp:ssl-force true\n'
printf 'set ftp:ssl-protect-data true\n'
printf 'set net:max-retries 3\n'
printf 'set net:timeout 20\n'
if [[ -n "$USER_NAME" && -n "$PASSWORD" ]]; then
printf 'open -u %s,%s %s\n' "$(lftp_quote "$USER_NAME")" "$(lftp_quote "$PASSWORD")" "$(lftp_quote "$DEPLOY_URL")"
else
printf 'open %s\n' "$(lftp_quote "$DEPLOY_URL")"
fi
printf 'cd %s\n' "$(lftp_quote "$REMOTE_ROOT")"
printf '%s\n' "$transfer_command"
printf 'bye\n'
} | lftp -f /dev/stdin
}
upload_file() {
local source_file="$1"
local remote_file="$2"
if [[ -f "$source_file" ]]; then
lftp "$DEPLOY_URL" -e "set ftp:ssl-allow true; set net:max-retries 3; set net:timeout 20; cd \"$REMOTE_ROOT\"; put -O \"$(dirname "$remote_file")\" \"$source_file\" -o \"$(basename "$remote_file")\"; bye"
run_lftp "put -O $(lftp_quote "$(dirname "$remote_file")") $(lftp_quote "$source_file") -o $(lftp_quote "$(basename "$remote_file")")"
fi
}
for directory in assets resources favicons icons img sounds .well-known; do
if [[ -d "$DIST_DIR/$directory" ]]; then
lftp "$DEPLOY_URL" -e "set ftp:ssl-allow true; set net:max-retries 3; set net:timeout 20; cd \"$REMOTE_ROOT\"; mirror -R --only-newer --parallel=4 \"$DIST_DIR/$directory\" \"$directory\"; bye"
run_lftp "mirror -R --only-newer --parallel=4 $(lftp_quote "$DIST_DIR/$directory") $(lftp_quote "$directory")"
fi
done
+1 -1
View File
@@ -120,7 +120,7 @@ async function verifyShell(baseUrl, shellPath) {
async function verifyRelease(baseUrl) {
const expectedCommit = process.env.RELEASE_EXPECTED_COMMIT || process.env.GITHUB_SHA || "";
const expectedBuildId = process.env.RELEASE_EXPECTED_BUILD_ID || "";
const expectedBuildId = process.env.RELEASE_EXPECTED_BUILD_ID || process.env.RELEASE_BUILD_ID || "";
const manifest = await fetchJson(baseUrl, "release-manifest.json");
const releaseEntry = await fetchJson(baseUrl, "release-entry.json");
+1
View File
@@ -35,6 +35,7 @@ export const ownedFilesByRole = {
"userHome.spec.ts",
"userInvoices.spec.ts",
"userMyWashStart.spec.ts",
"userMyWashStartFlow.spec.ts",
"userProfileInvoicing.spec.ts",
"userProfileNotifications.spec.ts",
"userProfileSecurity.spec.ts",
@@ -97,6 +97,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
allowBookingDeletion: {
type: Boolean,
default: false,
},
department_lane_id: {
type: Number,
default: null,
@@ -1752,6 +1756,11 @@ const acceptSelfServeWashDraft = async () => {
}
};
const canDeleteOrderBooking = computed(() =>
!props.order_id &&
(props.allowBookingDeletion || SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser())
);
const flatBuiltInMenuSections = computed(() => {
const sections = [];
@@ -1806,7 +1815,7 @@ const flatBuiltInMenuSections = computed(() => {
),
})
: null,
!props.order_id
canDeleteOrderBooking.value
? buildMenuAction("booking-delete", {
icon: "fas fa-trash-alt",
label: t("admin.pos.settings_wheel.delete_booking"),
@@ -15,6 +15,7 @@ import {
selectPreferredStripeTerminalReaderId,
STRIPE_TERMINAL_STATUS,
} from "@/components/displays/department/pos/displays/stripeTerminalReaders.js";
import { normalizeStripeInvoice } from "@/components/displays/department/pos/displays/stripeEmailInvoice.js";
const POLLING_INTERVAL_MS = 5000;
const STRIPE_TERMINAL_SETUP_REQUIRED_CODE = 'stripe_terminal_setup_required';
@@ -87,29 +88,6 @@ const selectedTaxRate = ref(1);
const paymentIntent = computed(() => StripeModule.paymentIntents.paymentIntent.value);
const isTerminalPaymentCaptured = computed(() => StripeModule.paymentIntents.isPaymentIntentAmountReceived(paymentIntent.value));
const normalizeStripeInvoice = (value) => {
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length === 0) {
return null;
}
const invoiceId = value.invoice_id || value.id || null;
if (!invoiceId) {
return null;
}
return {
id: value.id ?? props.order_id,
invoice_id: invoiceId,
customer_id: value.customer_id ?? null,
url: value.url || value.hosted_invoice_url || null,
created_at: value.created_at || null,
paid: Boolean(value.paid),
status: value.status || 'unknown',
amount_due: Number(value.amount_due ?? 0),
amount_paid: Number(value.amount_paid ?? 0),
};
};
const hasStripeEmailInvoice = computed(() => stripeInvoice.value !== null);
const isStripeEmailInvoicePaid = computed(() => stripeInvoice.value?.paid === true);
const isStripeEmailInvoiceTerminalState = computed(() => {
@@ -202,7 +180,7 @@ const loadStripeInvoiceState = async () => {
try {
const response = await getOrder(props.order_id, true);
const nextInvoice = normalizeStripeInvoice(response?.data?.includes?.stripeModuleOrders);
const nextInvoice = normalizeStripeInvoice(response?.data?.includes?.stripeModuleOrders, props.order_id);
stripeInvoice.value = nextInvoice;
if (nextInvoice) {
emailPanelState.value = 'tracking';
@@ -0,0 +1,60 @@
const STRIPE_INVOICE_PAID_STATUS = 'paid';
export const parseStripeInvoicePaidFlag = (value) => {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'number') {
return value === 1;
}
if (typeof value === 'string') {
const normalizedValue = value.trim().toLowerCase();
if (['true', '1'].includes(normalizedValue)) {
return true;
}
if (['false', '0', ''].includes(normalizedValue)) {
return false;
}
}
return false;
};
const toFiniteNumber = (value, fallback = 0) => {
const parsedValue = Number(value ?? fallback);
return Number.isFinite(parsedValue) ? parsedValue : fallback;
};
export const normalizeStripeInvoice = (value, fallbackOrderId = null) => {
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length === 0) {
return null;
}
const invoiceId = value.invoice_id || value.id || null;
if (!invoiceId) {
return null;
}
const status = String(value.status || 'unknown').toLowerCase();
const amountDue = toFiniteNumber(value.amount_due);
const amountPaid = toFiniteNumber(value.amount_paid);
const hasCoveredAmountDue = amountDue <= 0 || amountPaid >= amountDue;
const isPaid = status === STRIPE_INVOICE_PAID_STATUS
&& parseStripeInvoicePaidFlag(value.paid)
&& hasCoveredAmountDue;
return {
id: value.id ?? fallbackOrderId,
invoice_id: invoiceId,
customer_id: value.customer_id ?? null,
url: value.url || value.hosted_invoice_url || null,
created_at: value.created_at || null,
paid: isPaid,
status,
amount_due: amountDue,
amount_paid: amountPaid,
};
};
@@ -10,6 +10,16 @@ import { SessionUser } from "@/components/session/token/SessionUser.vue";
const acceptedOrderAttachmentFileTypes = "image/*,application/pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx";
const imageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"];
const officeExtensions = [".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"];
const safePreviewBlobTypesByKind = {
image: {
fallback: "image/png",
allowed: new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/bmp", "image/svg+xml"]),
},
document: {
fallback: "application/pdf",
allowed: new Set(["application/pdf"]),
},
};
const props = defineProps({
order: {
@@ -324,7 +334,21 @@ const hasCachedPreviewSource = (attachmentId) => {
return Object.prototype.hasOwnProperty.call(previewSourcesById.value, attachmentId);
};
const createEmbeddablePreviewUrl = async (downloadLink) => {
const createSafePreviewBlob = (fileBlob, previewKind) => {
const previewBlobTypes = safePreviewBlobTypesByKind[previewKind];
if (!previewBlobTypes) {
return null;
}
const normalizedBlobType = String(fileBlob.type || "").toLowerCase();
const safeBlobType = previewBlobTypes.allowed.has(normalizedBlobType)
? normalizedBlobType
: previewBlobTypes.fallback;
return new Blob([fileBlob], { type: safeBlobType });
};
const createEmbeddablePreviewUrl = async (downloadLink, previewKind) => {
if (!downloadLink) {
return null;
}
@@ -340,7 +364,12 @@ const createEmbeddablePreviewUrl = async (downloadLink) => {
return null;
}
const objectUrl = URL.createObjectURL(fileBlob);
const safePreviewBlob = createSafePreviewBlob(fileBlob, previewKind);
if (!safePreviewBlob) {
return null;
}
const objectUrl = URL.createObjectURL(safePreviewBlob);
generatedObjectUrls.add(objectUrl);
return objectUrl;
} catch (error) {
@@ -372,7 +401,7 @@ const ensurePreviewSource = async (attachment) => {
attachment.id,
false
);
const previewSource = await createEmbeddablePreviewUrl(downloadLink);
const previewSource = await createEmbeddablePreviewUrl(downloadLink, previewKind);
previewSourcesById.value = {
...previewSourcesById.value,
[attachment.id]: previewSource,
@@ -517,6 +546,7 @@ const toggleDropdown = async () => {
:src="activePreviewSource"
class="order-attachments-preview-panel__document"
title="Attachment preview"
sandbox
></iframe>
<a
v-else-if="activePreviewKind === 'link'"
@@ -92,6 +92,7 @@ const duplicateDetailsExpanded = ref(false);
const pendingNextResolution = ref(false);
const isDesktopLastWashCopying = ref(false);
let desktopStep1CoordinationPromise = Promise.resolve({ canProceed: true });
let focusOnReg1TimeoutId = null;
const isDesktopStep1Active = computed(() => getCurrentStep() === 1);
const setTab = (tab) => {
@@ -117,7 +118,16 @@ watch(
);
const focusOnReg1 = () => {
setTimeout(() => {
if (focusOnReg1TimeoutId !== null) {
clearTimeout(focusOnReg1TimeoutId);
}
focusOnReg1TimeoutId = setTimeout(() => {
focusOnReg1TimeoutId = null;
if (typeof document === "undefined") {
return;
}
const reg1Input = document.getElementById("reg_1");
if (reg1Input) {
reg1Input.focus();
@@ -267,12 +277,8 @@ const bookingSelectionObjects = computed(() => {
const contentSegments = [
`${t("admin.pos.order_booking_selector.customer_label")}: ${booking?.customer_name || t("admin.pos.not_found")}`,
`${t("admin.pos.order_booking_selector.plates_label")}: ${plateText || t("admin.pos.not_found")}`,
`${t("common.reference")}: ${
getOrderBookingReferenceValue(booking) || t("admin.pos.not_found")
}`,
`${t("common.services")}: ${
getOrderBookingServiceText(booking) || t("admin.pos.not_found")
}`,
`${t("common.reference")}: ${getOrderBookingReferenceValue(booking) || t("admin.pos.not_found")}`,
`${t("common.services")}: ${getOrderBookingServiceText(booking) || t("admin.pos.not_found")}`,
];
return {
@@ -681,6 +687,10 @@ onMounted(() => {
});
onBeforeUnmount(() => {
if (focusOnReg1TimeoutId !== null) {
clearTimeout(focusOnReg1TimeoutId);
focusOnReg1TimeoutId = null;
}
clearDesktopStep1PreflightHandler(handleDesktopStep1Preflight);
});
@@ -752,11 +762,7 @@ watch(
<ButtonsBox class="pos-actions pos-actions--stacked">
<Cancel tabindex="2" class="is-fullwidth" />
</ButtonsBox>
<div
v-if="shouldShowActionRailControls"
class="pos-shell-actions__rail"
data-testid="pos-step-1-action-rail"
>
<div v-if="shouldShowActionRailControls" class="pos-shell-actions__rail" data-testid="pos-step-1-action-rail">
<PosDesktopDuplicateWarning
v-if="shouldShowDuplicateWarningInActionRail"
:title="t('admin.pos.warning')"
@@ -299,14 +299,12 @@ const getSelectedBookingId = () => {
const completeStep2Order = async ({
bookingSafetySeal = null,
markOrderCompleted = false,
}: {
bookingSafetySeal?: string | null;
markOrderCompleted?: boolean;
} = {}) => {
await finalizeCurrentMobileOrder({
bookingSafetySeal,
markOrderCompleted,
markOrderCompleted: true,
});
popups.select("completed_transaction", {
message: `Order #${order_id.value} successfully created.`,
@@ -327,14 +325,12 @@ const step2 = async () => {
const resolvedSafetySeal = getResolvedMobileSafetySeal();
const hasResolvedSafetySeal = isNonEmptyString(resolvedSafetySeal);
const requiresBookingCompletionPopup = Boolean(selectedBookingId && hasWashCertificateInBasket);
const shouldMarkOrderAsCompleted = !selectedBookingId && hasWashCertificateInBasket;
try {
if (requiresBookingCompletionPopup && hasResolvedSafetySeal) {
syncMobileSafetySealState(resolvedSafetySeal);
await completeStep2Order({
bookingSafetySeal: resolvedSafetySeal,
markOrderCompleted: false,
});
return;
}
@@ -344,7 +340,6 @@ const step2 = async () => {
syncMobileSafetySealState(safetySeal);
await completeStep2Order({
bookingSafetySeal: String(safetySeal ?? ""),
markOrderCompleted: false,
});
};
@@ -358,9 +353,7 @@ const step2 = async () => {
return;
}
await completeStep2Order({
markOrderCompleted: shouldMarkOrderAsCompleted,
});
await completeStep2Order();
} catch (error: any) {
errors.value.push(error);
console.warn("An error occurred while completing the mobile order:", error);
@@ -3,6 +3,45 @@ export const XLVASK_USAGE_AMOUNT_CACHE_TTL_MS = 10 * 60 * 1000;
const CACHE_PREFIX = "xlvask-usage-amount:";
const memoryCache = new Map();
const getStorageValue = (key) => {
try {
if (typeof window === "undefined" || !window.localStorage) {
return "";
}
return window.localStorage.getItem(key) || "";
} catch {
return "";
}
};
const hashCacheScopePart = (value) => {
let hash = 5381;
for (let index = 0; index < value.length; index += 1) {
hash = ((hash << 5) + hash) ^ value.charCodeAt(index);
}
return (hash >>> 0).toString(36);
};
const getAuthenticatedCacheScope = () => {
const token = getStorageValue("token");
if (!token) {
return "";
}
return [
hashCacheScopePart(token),
getStorageValue("is_subuser") === "true" ? "subuser" : "user",
getStorageValue("selected_customer_number"),
]
.map((part) => encodeURIComponent(String(part ?? "")))
.join("|");
};
const getScopedCacheKey = (cacheKey) => {
const scope = getAuthenticatedCacheScope();
return scope ? `${scope}:${cacheKey}` : "";
};
const safeSessionStorage = () => {
try {
if (typeof window === "undefined" || !window.sessionStorage) {
@@ -15,9 +54,7 @@ const safeSessionStorage = () => {
};
const normalizeUsageLogId = (objectOrId) => {
const value = typeof objectOrId === "object"
? objectOrId?.usage_log_id ?? objectOrId?.id
: objectOrId;
const value = typeof objectOrId === "object" ? objectOrId?.usage_log_id ?? objectOrId?.id : objectOrId;
const parsed = Number.parseInt(String(value ?? ""), 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
};
@@ -34,17 +71,18 @@ export const buildXlvaskUsageAmountCacheKey = (objectOrId) => {
export const getCachedXlvaskUsageAmount = (objectOrId) => {
const cacheKey = buildXlvaskUsageAmountCacheKey(objectOrId);
if (!cacheKey) {
const scopedCacheKey = cacheKey ? getScopedCacheKey(cacheKey) : "";
if (!scopedCacheKey) {
return null;
}
const memoryEntry = memoryCache.get(cacheKey);
const memoryEntry = memoryCache.get(scopedCacheKey);
if (isFreshEntry(memoryEntry)) {
return memoryEntry.data;
}
if (memoryEntry) {
memoryCache.delete(cacheKey);
memoryCache.delete(scopedCacheKey);
}
const storage = safeSessionStorage();
@@ -52,14 +90,14 @@ export const getCachedXlvaskUsageAmount = (objectOrId) => {
return null;
}
const storageKey = `${CACHE_PREFIX}${cacheKey}`;
const storageKey = `${CACHE_PREFIX}${scopedCacheKey}`;
try {
const parsed = JSON.parse(storage.getItem(storageKey) || "null");
if (!isFreshEntry(parsed)) {
storage.removeItem(storageKey);
return null;
}
memoryCache.set(cacheKey, parsed);
memoryCache.set(scopedCacheKey, parsed);
return parsed.data;
} catch {
storage.removeItem(storageKey);
@@ -69,7 +107,8 @@ export const getCachedXlvaskUsageAmount = (objectOrId) => {
export const setCachedXlvaskUsageAmount = (objectOrId, data) => {
const cacheKey = buildXlvaskUsageAmountCacheKey(objectOrId);
if (!cacheKey) {
const scopedCacheKey = cacheKey ? getScopedCacheKey(cacheKey) : "";
if (!scopedCacheKey) {
return;
}
@@ -77,7 +116,7 @@ export const setCachedXlvaskUsageAmount = (objectOrId, data) => {
storedAt: Date.now(),
data,
};
memoryCache.set(cacheKey, entry);
memoryCache.set(scopedCacheKey, entry);
const storage = safeSessionStorage();
if (!storage) {
@@ -85,7 +124,7 @@ export const setCachedXlvaskUsageAmount = (objectOrId, data) => {
}
try {
storage.setItem(`${CACHE_PREFIX}${cacheKey}`, JSON.stringify(entry));
storage.setItem(`${CACHE_PREFIX}${scopedCacheKey}`, JSON.stringify(entry));
} catch {
// Best-effort cache. Quota errors should not block XL Vask usage rows.
}
@@ -94,11 +133,12 @@ export const setCachedXlvaskUsageAmount = (objectOrId, data) => {
export const clearCachedXlvaskUsageAmount = (objectOrId = null) => {
const storage = safeSessionStorage();
const cacheKey = objectOrId === null ? "" : buildXlvaskUsageAmountCacheKey(objectOrId);
const scopedCacheKey = cacheKey ? getScopedCacheKey(cacheKey) : "";
if (cacheKey) {
memoryCache.delete(cacheKey);
if (scopedCacheKey) {
memoryCache.delete(scopedCacheKey);
try {
storage?.removeItem(`${CACHE_PREFIX}${cacheKey}`);
storage?.removeItem(`${CACHE_PREFIX}${scopedCacheKey}`);
} catch {
// Best-effort cache cleanup.
}
@@ -0,0 +1,72 @@
<!--
ErrorBanner.vue reusable self-serve error notification bar.
Props:
message error text to display (blank hidden)
type Bulma tint: is-danger | is-warning (default is-danger)
iconLeft Font Awesome icon name
:loading whether retry button is in loading state
:showRetry whether to show the retry button
Emit:
retry fired when retry button is clicked
-->
<script setup lang="ts">
interface Props {
message: string | null;
type?: "is-danger" | "is-warning";
iconLeft?: string;
loading?: boolean;
showRetry?: boolean;
}
withDefaults(
defineProps<{
message: string | null;
type?: "is-danger" | "is-warning";
iconLeft?: string;
loading?: boolean;
showRetry?: boolean;
}>(),
{
type: "is-danger",
iconLeft: "sync-alt",
loading: false,
showRetry: true,
}
);
const emit = defineEmits<{
retry: [];
}>();
</script>
<template>
<b-message
v-if="message"
:type="type"
has-icon
:closable="false"
class="self-serve-error-banner"
>
<div class="is-flex is-align-items-center is-justify-content-space-between is-flex-wrap-wrap">
<span class="mr-3">{{ message }}</span>
<b-button
v-if="showRetry"
size="is-small"
:type="`${type} is-light`"
icon-pack="fas"
:icon-left="iconLeft"
:loading="loading"
@click="emit('retry')"
>
{{ $t("common.try_again") }}
</b-button>
</div>
</b-message>
</template>
<style scoped>
.self-serve-error-banner {
margin-bottom: 1rem;
}
</style>
@@ -0,0 +1,59 @@
<!--
LaneSelectionSection.vue bane-valg radio buttons and lane status display.
Props:
lanes array of lane objects from the department
selectedLaneId currently selected lane id
washType "Manual" | "Machine"
departmentName human-readable department name
isLaneAvailableFn function(lane) => boolean
isMachineAvailableFn function(laneId) => boolean
Emit:
update:selectedLaneId new lane id selected
update:washType "Manual" | "Machine"
-->
<script setup lang="ts">
import SelfServeLaneStep from "@/components/displays/selfServe/SelfServeLaneStep.vue";
interface Props {
lanes: any[];
selectedLaneId: number | string | null;
washType: string;
departmentName: string | null;
isLaneAvailableFn: (lane: any) => boolean;
isMachineAvailableFn: (laneId: number | string | null) => boolean;
}
defineProps<Props>();
const emit = defineEmits<{
"update:selectedLaneId": [id: number];
"update:washType": [type: string];
}>();
function handleLaneUpdate(id: number) {
emit("update:selectedLaneId", id);
}
function handleWashTypeUpdate(type: string) {
emit("update:washType", type);
}
</script>
<template>
<SelfServeLaneStep
:lanes="lanes"
:selected-lane-id="selectedLaneId"
:wash-type="washType"
:department-name="departmentName"
:is-lane-available="isLaneAvailableFn"
:is-machine-available="isMachineAvailableFn"
@update:selected-lane-id="handleLaneUpdate"
@update:wash-type="handleWashTypeUpdate"
/>
</template>
<style scoped>
/* Lane selection styling is inherited from SelfServeLaneStep */
</style>
@@ -118,10 +118,9 @@ const emit = defineEmits<{
</div>
</div>
<b-message
v-if="selectedLaneId && !isMachineAvailable(selectedLaneId)"
v-if="!isMachineAvailable(selectedLaneId)"
type="is-warning"
has-icon
:closable="false"
aria-close-label="Luk besked"
data-testid="self-serve-machine-unavailable-guidance"
>
{{ $t("self_wash.machine_unavailable_for_lane") }}
@@ -21,16 +21,6 @@ const isImageAttachment = (attachment: any) => (
&& attachment.content.other.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i)
);
const hasTaskDescription = (task: any) => {
const description = task?.description;
if (typeof description !== "string") {
return !!description;
}
const trimmedDescription = description.trim();
return trimmedDescription.length > 0 && trimmedDescription !== "-";
};
const getImageAttachments = (task: any) => {
if (!Array.isArray(task?.attachments)) {
return [];
@@ -83,7 +73,7 @@ const formatTaskTitle = (task: any) => {
const title = String(task?.task || "");
const programWheelSelection = getProgramWheelSelection(task);
if (programWheelSelection === null) {
if (programWheelSelection === null || /\bprogram\s*#\d+\b/i.test(title)) {
return title;
}
@@ -127,13 +117,11 @@ const formatTaskTitle = (task: any) => {
type="is-success"
:data-testid="`self-serve-task-${task.id}-toggle`"
@update:model-value="emit('toggle-task', task.id, $event)"
@input="emit('toggle-task', task.id, $event)"
/>
</b-field>
</div>
<div class="self-serve-task-image-overlay-text">
<p class="self-serve-task-title"><strong>{{ formatTaskTitle(task) }}</strong></p>
<p v-if="hasTaskDescription(task)" class="self-serve-task-description">{{ task.description }}</p>
</div>
</div>
</div>
@@ -156,22 +144,21 @@ const formatTaskTitle = (task: any) => {
</div>
</div>
</template>
<div v-else class="columns is-mobile is-vcentered">
<div v-if="props.showCheckboxes" class="column is-narrow">
<div v-else class="self-serve-task-row">
<div v-if="props.showCheckboxes" class="self-serve-task-checkbox">
<b-field>
<b-checkbox
class="mr-0 pr-0"
size="is-large"
:model-value="completedTasks[task.id] === true"
type="is-success"
:data-testid="`self-serve-task-${task.id}-toggle`"
@update:model-value="emit('toggle-task', task.id, $event)"
@input="emit('toggle-task', task.id, $event)"
/>
</b-field>
</div>
<div class="column">
<div class="self-serve-task-content">
<p><strong>{{ formatTaskTitle(task) }}</strong></p>
<p v-if="hasTaskDescription(task)" class="is-size-7">{{ task.description }}</p>
<div v-if="getVisibleServices(task).length > 0" class="tags mt-2">
<span v-for="service in getVisibleServices(task)" :key="service" class="tag is-success">{{ service }}</span>
</div>
@@ -239,7 +226,7 @@ const formatTaskTitle = (task: any) => {
padding: 0.65rem 0.75rem;
display: flex;
align-items: flex-start;
gap: 0.6rem;
gap: 0.35rem;
pointer-events: auto;
}
@@ -247,12 +234,30 @@ const formatTaskTitle = (task: any) => {
margin-bottom: 0;
}
.self-serve-task-row {
display: flex;
align-items: center;
gap: 0.35rem;
}
.self-serve-task-checkbox {
flex: 0 0 auto;
}
.self-serve-task-checkbox :deep(.field) {
margin-bottom: 0;
}
.self-serve-task-content {
flex: 1 1 auto;
min-width: 0;
}
.self-serve-task-content p {
margin-bottom: 0;
}
.self-serve-task-title {
margin: 0;
}
.self-serve-task-description {
margin: 0.25rem 0 0;
font-size: 0.85rem;
}
</style>
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { BIcon, BSkeleton } from "buefy";
import { computed, ref, watch } from "vue";
import { BIcon } from "buefy";
import SelfServeTaskList from "@/components/displays/selfServe/SelfServeTaskList.vue";
const props = defineProps<{
@@ -19,9 +19,16 @@ const emit = defineEmits<{
}>();
const isDynamicImageLoading = ref(false);
const failedDynamicImageUrl = ref<string | null>(null);
const showDynamicImageFrame = computed(
() => !!props.dynamicImageUrl
&& !isDynamicImageLoading.value
&& failedDynamicImageUrl.value !== props.dynamicImageUrl
);
watch(() => props.dynamicImageUrl, (dynamicImageUrl) => {
isDynamicImageLoading.value = !!dynamicImageUrl;
failedDynamicImageUrl.value = null;
}, { immediate: true });
const emitToggleTask = (taskId: number, value: boolean) => {
@@ -38,6 +45,7 @@ const onDynamicImageLoad = () => {
const onDynamicImageError = () => {
isDynamicImageLoading.value = false;
failedDynamicImageUrl.value = props.dynamicImageUrl;
emit("clear-dynamic-image");
};
</script>
@@ -58,26 +66,28 @@ const onDynamicImageError = () => {
<span class="ml-2">{{ $t("self_wash.loading_data") }}</span>
</div>
<img
v-if="dynamicImageUrl && isDynamicImageLoading"
:key="`${dynamicImageUrl}:preload`"
:src="dynamicImageUrl"
alt=""
data-testid="self-serve-dynamic-image-preload"
class="self-serve-dynamic-image-preload"
@load="onDynamicImageLoad"
@error="onDynamicImageError"
/>
<div
v-if="dynamicImageUrl"
v-if="showDynamicImageFrame"
class="self-serve-dynamic-image-frame mb-4"
:class="{ 'is-loading': isDynamicImageLoading }"
data-testid="self-serve-dynamic-image-frame"
>
<b-skeleton
v-if="isDynamicImageLoading"
class="self-serve-dynamic-image-skeleton"
width="100%"
height="100%"
data-testid="self-serve-dynamic-image-skeleton"
/>
<img
:key="dynamicImageUrl"
:src="dynamicImageUrl"
alt="Machine status"
data-testid="self-serve-dynamic-image"
class="self-serve-dynamic-image"
:class="{ 'is-loading': isDynamicImageLoading }"
@load="onDynamicImageLoad"
@error="onDynamicImageError"
/>
@@ -107,19 +117,20 @@ const onDynamicImageError = () => {
overflow: hidden;
}
.self-serve-dynamic-image-preload {
position: absolute;
width: 0;
height: 0;
opacity: 0;
pointer-events: none;
}
@media screen and (min-width: 769px) {
.self-serve-dynamic-image-frame {
max-width: 640px;
}
}
.self-serve-dynamic-image-skeleton {
position: absolute;
inset: 0;
overflow: hidden;
border-radius: 4px;
}
.self-serve-dynamic-image {
width: 100%;
height: 100%;
@@ -128,8 +139,4 @@ const onDynamicImageError = () => {
object-fit: contain;
transition: opacity 120ms ease;
}
.self-serve-dynamic-image.is-loading {
opacity: 0;
}
</style>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { computed, ref, watch } from "vue";
import { BAutocomplete, BField, BInput, BMessage } from "buefy";
import SelfServeVehicleTypeSelector from "@/components/displays/selfServe/SelfServeVehicleTypeSelector.vue";
@@ -21,8 +21,11 @@ const props = defineProps<{
const emit = defineEmits<{
(e: "update:customerNumber", value: string): void;
(e: "update:customer-number", value: string): void;
(e: "update:registrationNumber", value: string): void;
(e: "update:registration-number", value: string): void;
(e: "select-vehicle-type", selection: any): void;
(e: "selected", selection: any): void;
}>();
const extractRegistrationValue = (value: unknown): string => {
@@ -49,6 +52,19 @@ const normalizeRegistration = (value: unknown) => extractRegistrationValue(value
const typedRegistration = ref(normalizeRegistration(props.registrationNumber));
const filteredRegistrationOptions = computed(() => {
const query = normalizeRegistration(typedRegistration.value);
const options = props.registrationOptions || [];
if (!query) {
return options;
}
return options.filter((option) => {
const normalizedOption = normalizeRegistration(option);
return normalizedOption !== query && normalizedOption.includes(query);
});
});
watch(() => props.registrationNumber, (newValue) => {
typedRegistration.value = normalizeRegistration(newValue);
});
@@ -57,6 +73,18 @@ const emitRegistration = (value: unknown) => {
const normalized = normalizeRegistration(value);
typedRegistration.value = normalized;
emit("update:registrationNumber", normalized);
emit("update:registration-number", normalized);
};
const emitCustomerNumber = (value: unknown) => {
const normalized = String(value || "");
emit("update:customerNumber", normalized);
emit("update:customer-number", normalized);
};
const emitVehicleTypeSelection = (selection: any) => {
emit("select-vehicle-type", selection);
emit("selected", selection);
};
</script>
@@ -68,38 +96,41 @@ const emitRegistration = (value: unknown) => {
:model-value="customerNumber || ''"
data-testid="self-serve-customer-number"
:placeholder="$t('self_wash.enter_customer_number')"
@input="emit('update:customerNumber', String($event || ''))"
@input="emitCustomerNumber($event)"
@update:modelValue="emitCustomerNumber($event)"
/>
</b-field>
<b-field :label="$t('self_wash.registration_number')">
<b-autocomplete
:model-value="typedRegistration"
data-testid="self-serve-registration"
:data="registrationOptions"
:placeholder="$t('self_wash.enter_registration_number')"
:debounce="300"
:min-length="1"
:filter="(option, query) => option.toLowerCase().includes(query.toLowerCase())"
icon-pack="fas"
icon="car-side"
clearable
:loading="registrationOptions.length === 0 && isCustomerVehiclesLoading"
:selectable-header="true"
@select="emitRegistration($event)"
@typing="emitRegistration($event)"
@update:modelValue="emitRegistration($event)"
@select-header="emitRegistration(typedRegistration)"
>
<template #header>
<b-field :label="$t('self_wash.registration_number')" class="self-serve-registration-field">
<div class="self-serve-registration-control">
<b-autocomplete
:model-value="typedRegistration"
data-testid="self-serve-registration"
:data="filteredRegistrationOptions"
:placeholder="$t('self_wash.enter_registration_number')"
:debounce="300"
:min-length="1"
icon-pack="fas"
icon="car-side"
clearable
:loading="filteredRegistrationOptions.length === 0 && isCustomerVehiclesLoading"
@select="emitRegistration($event)"
@typing="emitRegistration($event)"
@update:modelValue="emitRegistration($event)"
>
</b-autocomplete>
<p
v-if="typedRegistration"
class="help self-serve-registration-guidance"
data-testid="self-serve-registration-guidance"
>
<template v-if="hasMatchingVehicle">
{{ $t("self_wash.select_from_vehicles", { plate: typedRegistration }) }}
</template>
<template v-else>
{{ $t("self_wash.add_as_new_vehicle", { plate: typedRegistration }) }}
</template>
</template>
<template #empty>{{ $t("self_wash.no_vehicles_found") }}</template>
</b-autocomplete>
</p>
</div>
</b-field>
<b-message
v-if="props.vehicleStepError"
@@ -124,7 +155,7 @@ const emitRegistration = (value: unknown) => {
<SelfServeVehicleTypeSelector
:selectedVehicleTypeId="selectedVehicleTypeId"
:restrictToProductIds="availableProductIds"
@selected="emit('select-vehicle-type', $event)"
@selected="emitVehicleTypeSelection"
/>
</template>
<template v-else-if="vehicleTypes.length === 0">
@@ -140,3 +171,34 @@ const emitRegistration = (value: unknown) => {
</b-field>
</div>
</template>
<style scoped>
.self-serve-registration-control {
display: block;
width: 100%;
min-width: 0;
}
.self-serve-registration-guidance {
display: block;
margin-top: 0.5rem;
line-height: 1.35;
white-space: normal;
}
.self-serve-registration-field :deep(.dropdown),
.self-serve-registration-field :deep(.autocomplete) {
width: 100%;
}
.self-serve-registration-field :deep(.dropdown-menu) {
left: 0;
right: 0;
width: 100%;
max-width: 100%;
}
.self-serve-registration-field :deep(.dropdown-item) {
white-space: normal;
}
</style>
@@ -0,0 +1,81 @@
<!--
VehicleInputSection.vue customer number, license plate, and vehicle type inputs.
Props:
showCustomerNumberInput whether to render the customer number field
customerNumber current customer number string
registrationNumber current license plate string
registrationOptions autocomplete options for existing vehicles
selectedVehicleTypeId currently selected vehicle type id
selectedVehicleName human-readable name of selected vehicle type
availableProductIds allowed product ids for the department
vehicleTypes list of vehicle type options
Emit:
update:customerNumber new customer number string
update:registrationNumber new license plate string
selectVehicleType selected VehicleTypeTemplate object
-->
<script setup lang="ts">
import { computed } from "vue";
import SelfServeVehicleStep from "@/components/displays/selfServe/SelfServeVehicleStep.vue";
interface Props {
showCustomerNumberInput: boolean;
customerNumber: string | null;
registrationNumber: string | null;
registrationOptions: string[];
selectedVehicleTypeId: number | null;
selectedVehicleName: string | null;
availableProductIds: (number | string)[];
vehicleTypes: any[];
}
defineProps<Props>();
const emit = defineEmits<{
"update:customerNumber": [value: string];
"update:registrationNumber": [value: string];
selectVehicleType: [type: { id: number }];
}>();
function handleCustomerNumberUpdate(value: string) {
emit("update:customerNumber", value);
}
function handleRegistrationNumberUpdate(value: string) {
emit("update:registrationNumber", value);
}
function handleSelectVehicleType(selection: any) {
emit("selectVehicleType", selection);
}
</script>
<template>
<SelfServeVehicleStep
:customer-number="customerNumber"
:show-customer-number-input="showCustomerNumberInput"
:registration-number="registrationNumber"
:registration-options="registrationOptions"
:is-customer-vehicles-loading="false"
:has-matching-vehicle="false"
:selected-vehicle-type-id="selectedVehicleTypeId"
:selected-vehicle-name="selectedVehicleName"
:selected-vehicle-description="null"
:available-product-ids="availableProductIds"
:vehicle-types="vehicleTypes"
:vehicle-step-error="null"
:vehicle-step-guidance="null"
@update:customer-number="handleCustomerNumberUpdate"
@update:customerNumber="handleCustomerNumberUpdate"
@update:registration-number="handleRegistrationNumberUpdate"
@update:registrationNumber="handleRegistrationNumberUpdate"
@select-vehicle-type="handleSelectVehicleType"
@selected="handleSelectVehicleType"
/>
</template>
<style scoped>
/* Vehicle input section styling is inherited from SelfServeVehicleStep */
</style>
@@ -0,0 +1,255 @@
<!--
WashProgressCard.vue timer, guided instructions, and bottom actions during active wash.
Renders when a wash session is in progress (WASH_IN_PROGRESS step).
Props:
currentGuidedWashStep index into guidedWashFlowSteps
guidedWashFlowSteps array of instruction step objects
formattedElapsed human-readable elapsed time string
isCompletingWash whether the wash is finishing right now
openingPropertyAccessGate loading state for access gate button
openingPropertyExitGate loading state for exit gate button
Emit:
update:currentGuidedWashStep new step index
goPreviousGuidedWashStep navigate to previous instruction
goNextGuidedWashStep navigate to next instruction
completeWash finish the wash session
openPropertyAccessGate open the property access gate (laneId)
openPropertyExitGate open the property exit gate (laneId)
requestAssistance call for assistance
-->
<script setup lang="ts">
import { computed } from "vue";
import { BButton } from "buefy";
interface Props {
currentGuidedWashStep: number;
guidedWashFlowSteps: any[];
formattedElapsed: string;
isCompletingWash: boolean;
openingPropertyAccessGate: boolean;
openingPropertyExitGate: boolean;
}
defineProps<Props>();
const emit = defineEmits<{
"update:currentGuidedWashStep": [step: number];
goPreviousGuidedWashStep: [];
goNextGuidedWashStep: [];
completeWash: [];
openPropertyAccessGate: [laneId: number | null];
openPropertyExitGate: [laneId: number | null];
requestAssistance: [];
}>();
const isLastGuidedWashStep = computed(
() =>
typeof guidedWashFlowSteps !== "undefined" &&
guidedWashFlowSteps.length > 0 &&
currentGuidedWashStep >= guidedWashFlowSteps.length - 1
);
function handleGoPrevious() {
emit("goPreviousGuidedWashStep");
}
function handleGoNext() {
emit("goNextGuidedWashStep");
}
function handleComplete() {
emit("completeWash");
}
function handleOpenAccessGate(laneId: number | null) {
emit("openPropertyAccessGate", laneId);
}
function handleOpenExitGate(laneId: number | null) {
emit("openPropertyExitGate", laneId);
}
function handleRequestAssistance() {
emit("requestAssistance");
}
</script>
<template>
<div data-testid="self-serve-wash-progress">
<!-- Finishing screen -->
<div
v-if="isCompletingWash"
class="notification is-info is-light self-serve-finishing-screen"
data-testid="self-serve-finishing-wash"
>
<p class="title is-5 mb-0">{{ $t("self_wash.finishing_wash_exit_opening") }}</p>
</div>
<!-- Guided wash instructions (stepped walkthrough) -->
<template v-if="!isCompletingWash">
<!--
Accept a callback slot for the guided-instructions component so the
parent can plug in SelfServeGuidedInstructions or its own implementation.
-->
<slot
name="guided-instructions"
:current-step="currentGuidedWashStep"
:steps="guidedWashFlowSteps"
:is-last-step="isLastGuidedWashStep"
@update:current-step="emit('update:currentGuidedWashStep', $event)"
>
<!-- Default: render nothing (parent provides via slot) -->
</slot>
<!-- Bottom action bar while wash is in progress -->
<div
class="self-serve-bottom-actions"
data-testid="self-serve-bottom-actions"
>
<!-- Help / assistance -->
<div class="self-serve-bottom-actions__row" data-testid="self-serve-session-bottom-actions">
<b-button
class="self-serve-bottom-actions__button"
type="is-link is-light"
icon-pack="fas"
icon-right="info-circle"
data-testid="self-serve-nav-help"
@click.prevent="handleRequestAssistance"
>
{{ $t("self_wash.assistance") }}
</b-button>
</div>
<!-- Property gate controls -->
<div class="self-serve-bottom-actions__row" data-testid="self-serve-property-gate-actions">
<b-button
class="self-serve-bottom-actions__button"
type="is-link is-light"
icon-pack="fas"
icon-left="sign-in-alt"
data-testid="self-serve-nav-open-property-access-gate"
:loading="openingPropertyAccessGate"
:disabled="openingPropertyAccessGate"
@click.prevent="$emit('openPropertyAccessGate', $props)"
>
{{ $t("self_wash.open_property_access_gate") }}
</b-button>
<b-button
class="self-serve-bottom-actions__button"
type="is-link is-light"
icon-pack="fas"
icon-left="sign-out-alt"
data-testid="self-serve-nav-open-property-exit-gate"
:loading="openingPropertyExitGate"
:disabled="openingPropertyExitGate"
@click.prevent="$emit('openPropertyExitGate', $props)"
>
{{ $t("self_wash.open_property_exit_gate") }}
</b-button>
</div>
<!-- Guided step navigation -->
<div class="self-serve-bottom-actions__row" data-testid="self-serve-guided-bottom-actions">
<b-button
class="self-serve-bottom-actions__button"
type="is-link is-light"
icon-pack="fas"
icon-left="arrow-left"
data-testid="self-serve-guided-prev"
:disabled="currentGuidedWashStep === 0"
@click.prevent="handleGoPrevious"
>
{{ $t("common.previous") }}
</b-button>
<b-button
v-if="!isLastGuidedWashStep"
class="self-serve-bottom-actions__button"
type="is-link"
icon-pack="fas"
icon-right="arrow-right"
data-testid="self-serve-guided-next"
@click.prevent="handleGoNext"
>
{{ $t("common.next") }}
</b-button>
<b-button
v-else
class="self-serve-bottom-actions__button"
type="is-link"
icon-pack="fas"
icon-right="check"
data-testid="self-serve-nav-complete"
:loading="isCompletingWash"
:disabled="isCompletingWash"
@click.prevent="handleComplete"
>
{{ $t("common.done") }}
</b-button>
</div>
</div>
</template>
</div>
</template>
<style scoped>
.self-serve-finishing-screen {
margin: 0 auto;
max-width: 32rem;
text-align: left;
}
.self-serve-bottom-actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
justify-content: center;
margin: 0.75rem auto 0;
max-width: 30rem;
}
.self-serve-bottom-actions__row {
display: flex;
gap: 0.75rem;
justify-content: center;
width: 100%;
}
.self-serve-bottom-actions__button {
flex: 1 1 0;
font-weight: 700;
line-height: 1.15;
max-width: 14rem;
min-height: 2.75rem;
min-width: 0;
white-space: normal;
}
@media screen and (max-width: 768px) {
.self-serve-bottom-actions {
background: #ffffff;
border-top: 1px solid #dfe5f0;
bottom: calc(4.75rem + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -0.25rem 0.75rem rgba(17, 47, 95, 0.08);
left: 0;
margin: 0;
max-width: none;
padding: 0.5rem 0.75rem;
position: fixed;
right: 0;
z-index: 41;
}
.self-serve-bottom-actions__button {
font-size: 0.88rem;
max-width: none;
min-height: 2.65rem;
padding-left: 0.55rem;
padding-right: 0.55rem;
}
}
</style>
@@ -0,0 +1,170 @@
<!--
WashTypeSelector.vue manual vs machine wash toggle.
Renders two radio-style cards: "Manual" (always available) and
"Machine" (available when the lane supports it).
Props:
modelValue current wash type ("Manual" | "Machine")
isMachineAvailable whether machine wash is available for this lane
Emit:
update:modelValue new wash type string
-->
<script setup lang="ts">
import { BRadioButton } from "buefy";
interface Props {
modelValue: string;
isMachineAvailable: boolean;
}
defineProps<Props>();
const emit = defineEmits<{
"update:modelValue": [type: string];
}>();
</script>
<template>
<section class="self-serve-choice-group" data-testid="self-serve-wash-type-group">
<h2 class="self-serve-choice-label">{{ $t("self_wash.wash_type") }}</h2>
<div
class="self-serve-choice-grid self-serve-choice-grid--centered"
data-testid="self-serve-wash-type-options"
>
<!-- Manual option (always available) -->
<div class="self-serve-choice-grid__item">
<b-radio-button
class="self-serve-choice-card"
:model-value="modelValue"
native-value="Manual"
type="is-link"
data-testid="self-serve-wash-type-manual"
@update:model-value="emit('update:modelValue', 'Manual')"
@input="emit('update:modelValue', 'Manual')"
>
<span>
<span>{{ $t("self_wash.manual") }}<br /></span>
<small>
<b-icon icon="check-circle" type="is-success" pack="fas" class="mr-1" />
{{ $t("self_wash.available") }}
</small>
</span>
</b-radio-button>
</div>
<!-- Machine option (conditioned) -->
<div class="self-serve-choice-grid__item">
<b-radio-button
class="self-serve-choice-card"
:model-value="modelValue"
native-value="Machine"
type="is-link"
:disabled="!isMachineAvailable"
:title="!isMachineAvailable ? $t('self_wash.machine_unavailable_for_lane') : null"
:aria-label="!isMachineAvailable ? $t('self_wash.machine_unavailable_for_lane') : null"
data-testid="self-serve-wash-type-machine"
@update:model-value="emit('update:modelValue', 'Machine')"
@input="emit('update:modelValue', 'Machine')"
>
<span>
<span>{{ $t("self_wash.machine") }}<br /></span>
<span v-if="!isMachineAvailable">
<small>
<b-icon icon="times-circle" type="is-danger" pack="fas" class="mr-1" />
{{ $t("self_wash.unavailable") }}
</small>
</span>
<span v-else>
<small>
<b-icon icon="check-circle" type="is-success" pack="fas" class="mr-1" />
{{ $t("self_wash.available") }}
</small>
</span>
</span>
</b-radio-button>
</div>
</div>
<b-message
v-if="!isMachineAvailable"
type="is-warning"
aria-close-label="Luk besked"
data-testid="self-serve-machine-unavailable-guidance"
>
{{ $t("self_wash.machine_unavailable_for_lane") }}
</b-message>
</section>
</template>
<style scoped>
.self-serve-choice-group {
margin-bottom: 1.25rem;
}
.self-serve-choice-label {
color: #303440;
font-size: 1.25rem;
font-weight: 700;
line-height: 1.2;
margin: 0 0 0.75rem;
}
.self-serve-choice-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-bottom: 0.75rem;
width: 100%;
}
.self-serve-choice-grid--centered {
justify-content: center;
}
.self-serve-choice-grid__item {
min-width: 0;
}
.self-serve-choice-card {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
min-height: 5rem;
padding: 0.75rem 0.5rem;
white-space: normal;
width: 100%;
}
.self-serve-choice-card :deep(.button) {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
min-height: 5rem;
padding: 0.75rem 0.5rem;
white-space: normal;
width: 100%;
}
.self-serve-choice-card :deep(.button),
.self-serve-choice-card span,
.self-serve-choice-card :deep(.button span) {
min-width: 0;
}
.self-serve-choice-card small,
.self-serve-choice-card :deep(.button small) {
display: inline-flex;
align-items: center;
line-height: 1.25;
}
@media screen and (min-width: 769px) {
.self-serve-choice-grid {
grid-template-columns: repeat(auto-fit, minmax(11rem, 15rem));
}
}
</style>
@@ -2,6 +2,8 @@
import Swal from "sweetalert2";
import { usePaginatedListInstance } from "@/components/pagination/paginatedList.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
const props = defineProps({
objects: {
@@ -23,6 +25,8 @@ const canDisableAccess = () =>
const canResendInvite = (subuser) =>
(props.showCustomer ? SessionUser.canAccessSuperUser() : SessionUser.canAccessUser() || SessionUser.hasPermission("SUBUSERS_EDIT"))
&& Boolean(subuser?.can_resend_invite ?? subuser?.setup_required);
const hasRowActions = (subuser) =>
(canEditPermissions() && subuser?.grant_id) || canResendInvite(subuser) || (canDisableAccess() && subuser?.grant_id);
const formatDateTime = (dateString) => {
if (!dateString) {
@@ -241,14 +245,6 @@ const onResendInvite = async (subuser) => {
<td>
<div>{{ subuser.grant_note || "-" }}</div>
<button
v-if="canEditPermissions() && subuser.grant_id"
class="button is-text is-small px-0 mt-1"
type="button"
@click="onEditNote(subuser)"
>
Redigér note
</button>
</td>
<td>{{ formatDateTime(subuser.created_at) }}</td>
@@ -256,36 +252,42 @@ const onResendInvite = async (subuser) => {
<td>
<div class="buttons is-justify-content-flex-end action-buttons">
<button
v-if="canEditPermissions() && subuser.grant_id"
class="button is-small"
type="button"
:data-testid="`subuser-permissions-${subuser.id}`"
@click="onEditPermissions(subuser)"
>
Tilladelser
</button>
<ActionSettingsWheelButton v-if="hasRowActions(subuser)">
<template #actions>
<ActionSettingsWheelItem
v-if="canEditPermissions() && subuser.grant_id"
icon="fas fa-pen"
label="Redigér note"
:click-action="() => onEditNote(subuser)"
:test-id="`subuser-note-${subuser.id}`"
/>
<button
v-if="canResendInvite(subuser)"
class="button is-small"
type="button"
:data-testid="`subuser-resend-${subuser.id}`"
@click="onResendInvite(subuser)"
>
Gensend
</button>
<ActionSettingsWheelItem
v-if="canEditPermissions() && subuser.grant_id"
icon="fas fa-user-shield"
label="Tilladelser"
:click-action="() => onEditPermissions(subuser)"
:test-id="`subuser-permissions-${subuser.id}`"
/>
<button
v-if="canDisableAccess() && subuser.grant_id"
class="button is-small"
:class="subuser.grant_enabled ? 'is-danger is-light' : 'is-success is-light'"
type="button"
:data-testid="`subuser-toggle-${subuser.id}`"
@click="onToggleEnabled(subuser, !subuser.grant_enabled)"
>
{{ subuser.grant_enabled ? "Deaktivér" : "Aktivér" }}
</button>
<ActionSettingsWheelItem
v-if="canResendInvite(subuser)"
icon="fas fa-paper-plane"
label="Gensend"
:click-action="() => onResendInvite(subuser)"
:test-id="`subuser-resend-${subuser.id}`"
/>
<ActionSettingsWheelItem
v-if="canDisableAccess() && subuser.grant_id"
:icon="subuser.grant_enabled ? 'fas fa-ban' : 'fas fa-check'"
:label="subuser.grant_enabled ? 'Deaktivér' : 'Aktivér'"
:template="subuser.grant_enabled ? 'danger' : 'success'"
:click-action="() => onToggleEnabled(subuser, !subuser.grant_enabled)"
:test-id="`subuser-toggle-${subuser.id}`"
/>
</template>
</ActionSettingsWheelButton>
</div>
</td>
</tr>
@@ -10,6 +10,7 @@ import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue"
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import TwoFactorVerify from "@/components/forms/auth/TwoFactorVerify.vue";
import { isPasskeySupported, authenticateWithPasskey } from "@/services/PasskeyAuthService.js";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
const employees = ref([]);
const { t } = useI18n();
@@ -52,6 +53,7 @@ const login = async () => {
}
// Save the token in the local storage
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', data.token);
// Redirect to the dashboard
window.location.href = '/admin';
@@ -72,6 +74,7 @@ const loginWithPasskey = async () => {
const result = await authenticateWithPasskey('employee', null, recaptchaToken);
if (result.token) {
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', result.token);
window.location.href = '/admin';
return;
+3
View File
@@ -9,6 +9,7 @@ import { parseError, clearErrors, addError, getError } from "@/components/reques
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
import { isPasskeySupported, authenticateWithPasskey } from "@/services/PasskeyAuthService.js";
import TwoFactorVerify from "@/components/forms/auth/TwoFactorVerify.vue";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
const { t } = useI18n();
@@ -68,6 +69,7 @@ const login = async () => {
}
// Save the token in the local storage
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', data.token);
// Set the success message
successMessage.value = "Du er nu logget ind!"
@@ -103,6 +105,7 @@ const loginWithPasskey = async () => {
const result = await authenticateWithPasskey('user', customerNum, recaptchaToken);
console.log('Passkey authentication result:', result);
if (result.token) {
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', result.token);
successMessage.value = "Du er nu logget ind!";
setTimeout(() => {
@@ -9,6 +9,7 @@ import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { isPasskeySupported, authenticateWithPasskey } from "@/services/PasskeyAuthService.js";
import { getSubuserPasswordPolicyError } from "@/services/subuserPasswordPolicy.js";
import TwoFactorVerify from "@/components/forms/auth/TwoFactorVerify.vue";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
const { t } = useI18n();
@@ -69,6 +70,7 @@ const login = async () => {
// Save the session token
if (data.session) {
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', data.session);
localStorage.setItem('is_subuser', 'true');
window.location.reload();
@@ -97,6 +99,7 @@ const loginWithPasskey = async () => {
// Subuser login returns 'session' token, user login returns 'token'
const sessionToken = result.session || result.token;
if (sessionToken) {
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', sessionToken);
localStorage.setItem('is_subuser', 'true');
// Reload the page to update the UI
@@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n';
import { verify2FA } from "@/services/TwoFactorAuthService.js";
import { parseError, getError, clearErrors } from "@/components/request/HandleGlobalError.vue";
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
const { t } = useI18n();
const router = useRouter();
@@ -43,9 +44,11 @@ const verify = async () => {
// Handle the response based on user type
if (props.userType === 'subuser' && result.session) {
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', result.session);
localStorage.setItem('is_subuser', 'true');
} else if (result.token) {
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', result.token);
localStorage.removeItem('is_subuser');
}
@@ -52,6 +52,7 @@ const customer_suggestions = ref([
const isSettingCustomer = ref(false);
const isSettingCustomerToInteger = ref(0);
const isSettingCustomerStartTime = ref(null);
let customerSuggestionsRequestId = 0;
const isSettingCustomerTo = (customer_number) => {
return isSettingCustomerToInteger.value === parseInt(customer_number);
@@ -88,10 +89,17 @@ const getCustomerSuggestions = () => {
if (!props.reg_1) {
return;
}
const requestedReg1 = props.reg_1;
const requestId = ++customerSuggestionsRequestId;
SessionUser.request("/department/vehicle/customer-suggestions", "GET", {
reg_1: props.reg_1,
reg_1: requestedReg1,
})
.then((response) => {
if (requestId !== customerSuggestionsRequestId || requestedReg1 !== props.reg_1) {
return;
}
// Assuming the response contains an array of customer suggestions
console.log("Customer suggestions:", response.data.data);
let suggestions = [];
@@ -126,6 +134,7 @@ watch(
if (newValue) {
getCustomerSuggestions();
} else {
customerSuggestionsRequestId += 1;
customer_suggestions.value = [];
}
}
+17 -6
View File
@@ -118,6 +118,7 @@ const impersonatedUserRoleId = computed(() => {
const canGrantMissingPermissions = computed(() =>
hasSuperuserToken.value && !SessionUser.isSubuser.value && impersonatedUserRoleId.value !== null
);
const canInspectReleaseRuntime = computed(() => hasSuperuserToken.value || SessionUser.canAccessAdmin?.() === true);
const userDetailRows = computed(() => {
if (SessionUser.isSubuser.value) {
@@ -284,7 +285,12 @@ const resolvePingUrl = () => {
};
const activeApiUrl = computed(() => getReleaseRuntimeApiBaseUrl());
const releaseSessionSummary = computed(() => buildReleaseSessionSummary());
const activeApiUrlLabel = computed(() =>
canInspectReleaseRuntime.value ? activeApiUrl.value : "Restricted to release operators"
);
const releaseSessionSummary = computed(() =>
buildReleaseSessionSummary(undefined, { includeInfrastructureDetails: canInspectReleaseRuntime.value })
);
const measurePingLatency = async () => {
if (typeof fetch !== "function") {
@@ -646,9 +652,10 @@ onBeforeUnmount(() => {
</aside>
<aside class="request-queue-progress__side request-queue-progress__side--runtime" data-testid="request-queue-runtime-box">
<div class="request-queue-progress__section-title">Session release</div>
<div class="request-queue-progress__section-content request-queue-progress__section-content--release">
<ul class="request-queue-progress__meta-list">
<template v-if="canInspectReleaseRuntime">
<div class="request-queue-progress__section-title">Session release</div>
<div class="request-queue-progress__section-content request-queue-progress__section-content--release">
<ul class="request-queue-progress__meta-list">
<li class="request-queue-progress__meta-item">
<span class="request-queue-progress__meta-label">Channel</span>
<span
@@ -771,11 +778,15 @@ onBeforeUnmount(() => {
</li>
</ul>
<div class="request-queue-progress__subsection-title">Runtime details</div>
</div>
</template>
<div class="request-queue-progress__section-title">Runtime details</div>
<div class="request-queue-progress__section-content request-queue-progress__section-content--release">
<ul class="request-queue-progress__meta-list">
<li class="request-queue-progress__meta-item">
<span class="request-queue-progress__meta-label">API URL</span>
<span class="request-queue-progress__meta-value" :title="activeApiUrl">{{ activeApiUrl }}</span>
<span class="request-queue-progress__meta-value" :title="activeApiUrlLabel">{{ activeApiUrlLabel }}</span>
</li>
<li class="request-queue-progress__meta-item">
<span class="request-queue-progress__meta-label">Current host</span>
+8 -5
View File
@@ -48,10 +48,13 @@ export const getDepartmentDescription = (id) => {
export const getDepartmentsGuest = async (queryParams = {}) => {
isLoading.value = true;
const queryString = new URLSearchParams(queryParams).toString();
const request = await unauthenticatedRequest("/guest/departments" + (queryString ? `?${queryString}` : ""), "get");
departments.value = sortByDepartmentPriorityOrder(request.data.data || []);
isLoading.value = false;
return departments.value;
try {
const queryString = new URLSearchParams(queryParams).toString();
const request = await unauthenticatedRequest("/guest/departments" + (queryString ? `?${queryString}` : ""), "get");
departments.value = sortByDepartmentPriorityOrder(request.data.data || []);
return departments.value;
} finally {
isLoading.value = false;
}
};
</script>
+2
View File
@@ -2,6 +2,7 @@
import { ref } from 'vue'
import axios from 'axios'
import {API_URL} from "@/config.js";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
export const currentDepartment = ref(null);
export const selectDepartment = (department) => {
@@ -31,6 +32,7 @@ export const exitSession = () => {
}
});
clearEdgeGatewayWorkspaceCache();
localStorage.removeItem('token');
return result;
};
@@ -2,6 +2,7 @@
import axios from 'axios'
import { enqueueRequest } from "@/services/requestQueue.js";
import { buildCurrentReleaseHeaders, resolveReleaseApiUrl } from "@/services/releaseTimeline.js";
import { isTrustedReleaseUrl } from "@/services/releaseTrust.js";
/**
* Get the selected customer number for X-Customer-Number header (used by subusers)
@@ -12,28 +13,82 @@ const getSelectedCustomerNumber = () => {
return stored ? parseInt(stored) : null;
};
const MY_ACTIVE_WASH_ENDPOINT = '/modules/self-serve/lane/wash/my-active-wash';
const ACTIVE_WASH_STARTED_STATUSES = new Set(['MACHINE_RELAY_ENABLED', 'MACHINE_STARTED']);
const normalizeStatus = (status) => String(status || '').trim().toUpperCase();
const parseDateTimeMs = (value) => {
if (!value) {
return null;
}
const timestamp = Date.parse(String(value));
return Number.isFinite(timestamp) ? timestamp : null;
};
const hasActiveWashStartEvidence = (details) => {
const session = details?.session || {};
const status = normalizeStatus(session?.status ?? details?.status);
return (
ACTIVE_WASH_STARTED_STATUSES.has(status) ||
session?.machine_relay_enabled === true ||
session?.machine_start_triggered === true ||
details?.machine_relay_enabled === true ||
details?.machine_start_triggered === true ||
parseDateTimeMs(session?.wash_started_at) !== null ||
parseDateTimeMs(session?.machine_start_triggered_at) !== null ||
parseDateTimeMs(session?.machine_relay_enabled_at) !== null
);
};
const normalizeActiveWashResponse = (url, method, response) => {
if (String(method || '').toUpperCase() !== 'GET' || !String(url || '').includes(MY_ACTIVE_WASH_ENDPOINT)) {
return response;
}
const details = response?.data?.data;
if (!details?.in_progress || hasActiveWashStartEvidence(details)) {
return response;
}
return {
...response,
data: {
...response.data,
data: {
...details,
in_progress: false,
},
},
};
};
export const authenticatedRequest = (url, method, data, catchCallable = null, thenCallable = null) => {
const token = localStorage.getItem('token');
if (!token) {
//throw new Error('No token was found, unable to make authenticated request');
}
const requestUrl = resolveReleaseApiUrl(url);
const canSendCredentials = isTrustedReleaseUrl(requestUrl);
// Build headers
const headers = {
...buildCurrentReleaseHeaders(),
};
if (token && token.length > 0) {
if (canSendCredentials && token && token.length > 0) {
headers.Authorization = `Bearer ${token}`;
}
// Add X-Customer-Number header if subuser has selected a grant
const isSubuser = localStorage.getItem('is_subuser') === 'true';
const selectedCustomerNumber = getSelectedCustomerNumber();
if (isSubuser && selectedCustomerNumber) {
if (canSendCredentials && isSubuser && selectedCustomerNumber) {
headers['X-Customer-Number'] = selectedCustomerNumber;
}
const requestUrl = resolveReleaseApiUrl(url);
return enqueueRequest(
() => axios({
method,
@@ -60,6 +115,7 @@ export const authenticatedRequest = (url, method, data, catchCallable = null, th
throw error;
})
.then((response) => {
response = normalizeActiveWashResponse(url, method, response);
if (thenCallable) {
// Call the then callable
thenCallable(response);
+21 -1
View File
@@ -58,6 +58,12 @@ import {
releaseChannelRuntimeRequestParams,
setReleaseChannelSwitchNoticePrincipal,
} from "@/services/releaseChannelAvailability.js";
import { clearCachedXlvaskUsageAmount } from "@/components/displays/department/pos/sync/xlvaskUsageAmountCache.js";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
import {
clearPeriodCache,
clearSelfWashCountsCache,
} from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportPaging.js";
const normalizePositiveInteger = (value) => {
const parsedValue = Number.parseInt(String(value ?? ""), 10);
@@ -98,7 +104,16 @@ const hydrateSessionFromStorage = () => {
return true;
};
const clearPrivilegedSessionCaches = () => {
clearPeriodCache();
clearSelfWashCountsCache();
clearCachedXlvaskUsageAmount();
clearEdgeGatewayWorkspaceCache();
};
const clearStoredSession = () => {
clearPrivilegedSessionCaches();
if (typeof window === "undefined") {
return;
}
@@ -115,7 +130,8 @@ const applyReleaseRuntimeConfig = (runtime) => {
const normalizedRuntime = runtime || {};
configureReleaseRuntime(normalizedRuntime);
reconcileSelectedReleaseChannel(normalizedRuntime);
const runtimeUrls = normalizedRuntime.urls && typeof normalizedRuntime.urls === "object" ? normalizedRuntime.urls : {};
const runtimeUrls =
normalizedRuntime.urls && typeof normalizedRuntime.urls === "object" ? normalizedRuntime.urls : {};
SessionUser.runtimeConfig.release.traceId.value = normalizedRuntime.trace_id || null;
SessionUser.runtimeConfig.release.channel.value = normalizedRuntime.channel || null;
SessionUser.runtimeConfig.release.availableChannels.value =
@@ -377,6 +393,7 @@ export const authenticateUser = async (customer_number, password) => {
password,
})
.then((response) => {
clearPrivilegedSessionCaches();
SessionUser.token.value = response.data.token;
SessionUser.authenticated.value = true;
localStorage.setItem("token", response.data.token);
@@ -423,6 +440,7 @@ export const authenticateSubuser = async (credentials) => {
return await authenticatedRequest("/subusers/auth/password", "POST", requestBody)
.then((response) => {
clearPrivilegedSessionCaches();
SessionUser.token.value = response.data.data.session;
SessionUser.authenticated.value = true;
SessionUser.isSubuser.value = true;
@@ -669,6 +687,7 @@ export const SessionUser = {
* @param {number|null} customerNumber - The billing_customer_number to select, or null to clear
*/
selectGrant: (customerNumber) => {
clearEdgeGatewayWorkspaceCache();
if (customerNumber === null) {
localStorage.removeItem("selected_customer_number");
SessionUser.subuser.selectedGrantCustomerNumber.value = null;
@@ -886,6 +905,7 @@ export const SessionUser = {
* @param {string} token
*/
setToken: (token) => {
clearPrivilegedSessionCaches();
SessionUser.token.value = token;
localStorage.setItem("token", token);
// Reload the window to make sure the user's session is initiated
@@ -7,6 +7,13 @@ import { getSystemUserIds } from "@/components/session/token/SessionUser/Objects
// Helper function to get i18n translation
const t = (key) => i18n.global.t(key);
const escapeHtml = (value = "") => String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;")
.replaceAll("'", "&#39;");
const normalizeDateTimeLocalValue = (value) => {
if (value === null || value === undefined || value === '') {
return '';
@@ -765,9 +772,11 @@ export const ObjectsGlobal = {
console.log(response);
// How long are the list of options
for (let i = 0; i < response.length; i++) {
var tmp = response[i];
const tmp = response[i];
const optionValue = tmp?.id ?? "";
const optionLabel = tmp?.name ? tmp.name : optionValue;
console.log(tmp);
html += `<option value="${tmp.id}" ${(value == tmp.id || (value === null && tmp.id === 0)) ? 'selected' : ''}>${tmp.name ? tmp.name : tmp.id}</option>`;
html += `<option value="${escapeHtml(optionValue)}" ${(value == optionValue || (value === null && optionValue === 0)) ? 'selected' : ''}>${escapeHtml(optionLabel)}</option>`;
}
});
html += `</select>
@@ -19,6 +19,14 @@ const getProtectedDeletePayload = (error) => {
return payload;
};
const escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (character) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
"'": "&#39;",
})[character]);
const formatDeleteProtectionReason = (reason) => {
switch (reason) {
case "completed":
@@ -28,22 +36,23 @@ const formatDeleteProtectionReason = (reason) => {
case "attachments":
return "ordren har vedhæftninger";
default:
return String(reason || "").replace(/_/g, " ");
return escapeHtml(String(reason || "").replace(/_/g, " "));
}
};
const showProtectedOrderDeleteConfirmation = async (id, protectionPayload, onAfterSubmit = null) => {
const orderId = String(id);
const escapedOrderId = escapeHtml(orderId);
const reasons = Array.isArray(protectionPayload?.protected_reasons)
? protectionPayload.protected_reasons.map(formatDeleteProtectionReason).filter(Boolean)
: [];
const reasonText = reasons.length > 0 ? reasons.join(", ") : "ordren indeholder gemte data";
const result = await Swal.fire({
title: `Bekræft sletning af ordre #${orderId}`,
titleText: `Bekræft sletning af ordre #${orderId}`,
html: `
<p>Ordren kan stadig slettes, men kræver ekstra bekræftelse fordi ${reasonText}.</p>
<p>Skriv <strong>${orderId}</strong> for at slette ordren.</p>
<p>Skriv <strong>${escapedOrderId}</strong> for at slette ordren.</p>
`,
input: "text",
inputAttributes: {
@@ -127,6 +127,13 @@ export const SelfServeVehicleConditions = {
}
},
add: async (department, lane, customer_id, reg, question, value, options = {}) => {
const safeOptions = {};
const normalizedVehicleType = parseInt(options.vehicle_type ?? options.vehicle_type_id);
if (!Number.isNaN(normalizedVehicleType) && normalizedVehicleType > 0) {
safeOptions.vehicle_type = normalizedVehicleType;
}
return ObjectsGlobal.add.object(SelfServeVehicleConditions.meta.endpoint, {
department: parseInt(department),
lane: parseInt(lane),
@@ -134,7 +141,9 @@ export const SelfServeVehicleConditions = {
reg: reg,
question: parseInt(question),
value: value === "true" || value === true,
...options
...safeOptions,
activate_machine: false,
sync_relay_state: false
});
},
set: {
@@ -17,13 +17,15 @@ const normalizeEntries = (entries) => {
return [];
};
const SENSITIVE_WRITE_ONLY_KEYS = new Set(["broker_shared_secret"]);
const toConfigPayload = (entries) => {
const payload = {};
normalizeEntries(entries).forEach((entry) => {
if (!entry || !entry.variable) {
return;
}
payload[entry.variable] = entry.value;
payload[entry.variable] = SENSITIVE_WRITE_ONLY_KEYS.has(entry.variable) ? "" : entry.value;
});
return payload;
};
@@ -42,6 +44,11 @@ export const Config = {
test_broker: async (payload) => testEdgeGatewayBrokerConfig(payload),
set: async (variable, value) => {
const currentConfig = await Config.get_config();
SENSITIVE_WRITE_ONLY_KEYS.forEach((key) => {
if (key !== variable) {
delete currentConfig[key];
}
});
return Config.set_config({
...currentConfig,
[variable]: value,
@@ -73,7 +80,6 @@ export const Config = {
set: async (value) => Config.set("broker_auth_mode", value),
},
broker_shared_secret: {
get: async () => Config.get("broker_shared_secret"),
set: async (value) => Config.set("broker_shared_secret", value),
},
},
@@ -1,6 +1,7 @@
<script>
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import { parseError } from "@/components/request/HandleGlobalError.vue";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
import { DatabaseSystemObject } from "@/components/session/token/superUser/systemDatabase.vue";
import { Cron } from "@/components/session/token/superUser/cron.vue";
import { Economic } from "@/components/session/token/superUser/modules/Economic/Economic.vue";
@@ -106,6 +107,7 @@ export const SuperUserObject = {
const currentToken = localStorage.getItem("token");
localStorage.setItem("superuser_token", currentToken);
// Set the token in the local storage
clearEdgeGatewayWorkspaceCache();
localStorage.setItem("token", response.data.data.token);
// Redirect the user to the dashboard
window.location = "/";
@@ -143,6 +145,7 @@ export const SuperUserObject = {
// Get the superuser token
const superuserToken = localStorage.getItem("superuser_token");
// Set the token in the local storage
clearEdgeGatewayWorkspaceCache();
localStorage.setItem("token", superuserToken);
// Clear the superuser token
localStorage.removeItem("superuser_token");
+25 -22
View File
@@ -320,20 +320,26 @@ export const ensureCurrentOrderDepartment = async (targetDepartmentId = null) =>
}
try {
let currentDepartmentId = null;
try {
currentDepartmentId = await getCurrentOrderDepartmentId(normalizedOrderId);
} catch (error) {
currentDepartmentId = null;
}
const currentDepartmentId = await getCurrentOrderDepartmentId(normalizedOrderId);
if (currentDepartmentId === normalizedDepartmentId) {
department_id.value = normalizedDepartmentId;
return true;
}
await SessionUser.objects.orders.set.department_id(normalizedOrderId, normalizedDepartmentId);
department_id.value = normalizedDepartmentId;
return true;
parseError(
{
response: {
data: {
data: {
message: "Ordren tilhører ikke den valgte afdeling",
},
message: "Ordren tilhører ikke den valgte afdeling",
},
},
},
"stepError"
);
return false;
} catch (error) {
parseError(error, "stepError");
return false;
@@ -727,9 +733,9 @@ export const restoreStoredPosOrderId = async (
if (selectedDepartmentId && toPositiveInteger(storedOrder?.department_id) !== selectedDepartmentId) {
if (options.syncDepartment === true) {
order_id.value = storedOrderId;
const didSyncDepartment = await ensureCurrentOrderDepartment(selectedDepartmentId);
if (!didSyncDepartment) {
throw new Error("Stored order department could not be changed to current department");
const isCurrentDepartment = await ensureCurrentOrderDepartment(selectedDepartmentId);
if (!isCurrentDepartment) {
throw new Error("Stored order department does not match current department");
}
} else {
throw new Error("Stored order department does not match current department");
@@ -2012,18 +2018,14 @@ export const getVehiclePlateBookings = (vehiclePlate) => {
return sortPendingOrderBookings(matchingBookings);
};
export const ensureVehiclePlateBookingsLoaded = async (vehiclePlate, options = {}) => {
const normalizedOptions = {
force: false,
...options,
};
export const ensureVehiclePlateBookingsLoaded = async (vehiclePlate, _options = {}) => {
const normalizedVehiclePlate = normalizeVehiclePlateForBookingSelection(vehiclePlate);
const currentDepartmentKey = syncPendingBookingsDepartmentState();
if (!normalizedVehiclePlate || !department_id.value) {
return [];
}
if (!normalizedOptions.force && loadedPendingBookingPlates.has(normalizedVehiclePlate)) {
if (loadedPendingBookingPlates.has(normalizedVehiclePlate)) {
return getVehiclePlateBookings(normalizedVehiclePlate);
}
@@ -2167,7 +2169,7 @@ const getFirstAvailableWashProduct = async () => {
}
};
const hydrateSelectedOrderBookingForDesktop = async () => {
export const hydrateSelectedOrderBookingForDesktop = async () => {
const normalizedOrderId = toPositiveInteger(order_id.value);
const normalizedBookingId = toPositiveInteger(selectedOrderBookingId.value);
@@ -2250,8 +2252,7 @@ const hydrateSelectedOrderBookingForDesktop = async () => {
}
}
const forcedPrimaryPrice =
typeof primaryRawItem?.price === "number" ? Number(primaryRawItem.price) : primaryProduct.price ?? null;
const forcedPrimaryPrice = primaryProduct.price ?? null;
const primaryItemResponse = await createOrderItem(
normalizedOrderId,
primaryProduct.id,
@@ -2269,13 +2270,15 @@ const hydrateSelectedOrderBookingForDesktop = async () => {
continue;
}
const secondaryProduct = await fetchOrderBookingProductWithPricing(secondaryProductId);
await createOrderItem(
normalizedOrderId,
secondaryProductId,
Math.max(1, Number.parseInt(String(bookingItem?.quantity ?? 1), 10) || 1),
relatedPrimaryItemId,
String(bookingItem?.notes ?? "").trim() || null,
typeof bookingItem?.price === "number" ? Number(bookingItem.price) : null
secondaryProduct?.price ?? null
);
}
@@ -26,7 +26,7 @@ const pages = {
to: "/user/wash/start",
label: "Selv vask",
icon: "tint",
disabled: !IS_DEV,
disabled: false,
},
bookings: {
to: "/user/bookings",
+7 -5
View File
@@ -64,6 +64,7 @@ export const useOrderMetadataAutosave = ({
const requestedValue = normalizeValue(draft.value);
const sequence = ++nextSequence;
let saveCompleted = false;
isSaving.value = true;
try {
@@ -83,6 +84,8 @@ export const useOrderMetadataAutosave = ({
await onSaved(savedValue, requestedValue);
}
saveCompleted = true;
return true;
} catch (error) {
if (typeof onError === "function") {
@@ -95,12 +98,11 @@ export const useOrderMetadataAutosave = ({
} finally {
isSaving.value = false;
if (saveAgainAfterCurrentRequest || isDirty.value) {
saveAgainAfterCurrentRequest = false;
const shouldSaveAgain = saveAgainAfterCurrentRequest;
saveAgainAfterCurrentRequest = false;
if (isDirty.value) {
void persist();
}
if (saveCompleted && (shouldSaveAgain || isDirty.value) && isDirty.value) {
void persist();
}
}
};
+123 -11
View File
@@ -1,5 +1,6 @@
import { computed, ref } from "vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { safeAttachmentDownloadLink } from "@/services/attachmentDownloadLinks.js";
const normalizeBooleanAnswer = (value) => {
if (value === true || value === false) {
@@ -270,6 +271,55 @@ const mergeByNumericId = (existingItems, incomingItems) => {
return merged;
};
const taskStableKey = (task) =>
String(task?.task ?? "")
.trim()
.toLowerCase();
const mergeTaskSets = (existingTasks, incomingTasks) => {
if (!Array.isArray(existingTasks) || existingTasks.length === 0) {
return Array.isArray(incomingTasks) ? [...incomingTasks] : [];
}
if (!Array.isArray(incomingTasks) || incomingTasks.length === 0) {
return [...existingTasks];
}
const usedExistingIndexes = new Set();
return incomingTasks.map((incomingTask) => {
const incomingId = extractTaskId(incomingTask);
let existingIndex = -1;
if (incomingId) {
existingIndex = existingTasks.findIndex((task) => extractTaskId(task) === incomingId);
} else {
const incomingKey = taskStableKey(incomingTask);
existingIndex = existingTasks.findIndex(
(task, index) => !usedExistingIndexes.has(index) && taskStableKey(task) === incomingKey
);
}
if (existingIndex === -1) {
return incomingTask;
}
usedExistingIndexes.add(existingIndex);
const existingTask = existingTasks[existingIndex];
return {
...existingTask,
...incomingTask,
id: incomingTask.id || existingTask.id,
task_id: incomingTask.task_id || existingTask.task_id,
attachments: Array.isArray(incomingTask.attachments) && incomingTask.attachments.length > 0
? incomingTask.attachments
: existingTask.attachments,
_attachmentsLoaded: incomingTask._attachmentsLoaded || existingTask._attachmentsLoaded,
};
});
};
export function useSelfServeLogic() {
const loading = ref(false);
const requestError = ref(null);
@@ -296,6 +346,30 @@ export function useSelfServeLogic() {
const summaryVisibleQuestionIds = ref([]);
const summaryQuestionOrder = ref({});
const latestFetchRequestId = ref(0);
const latestSuccessfulFetchKey = ref(null);
const inFlightFetchKey = ref(null);
const createFetchKey = (departmentId, vehicleTypeId, laneId, reg) => {
const normalizedDepartmentId = parseInt(departmentId);
const normalizedLaneId = parseInt(laneId);
const normalizedReg = String(reg || "").trim().toUpperCase();
const normalizedVehicleTypeId = parseInt(vehicleTypeId);
const vehicleTypeKey = !Number.isNaN(normalizedVehicleTypeId) && normalizedVehicleTypeId > 0
? normalizedVehicleTypeId
: "";
if (
Number.isNaN(normalizedDepartmentId)
|| normalizedDepartmentId <= 0
|| Number.isNaN(normalizedLaneId)
|| normalizedLaneId <= 0
|| normalizedReg.length < 2
) {
return null;
}
return [normalizedDepartmentId, normalizedLaneId, normalizedReg, vehicleTypeKey].join("|");
};
const beginFetchRequest = () => {
latestFetchRequestId.value += 1;
@@ -424,6 +498,10 @@ export function useSelfServeLogic() {
machineType.value = previewData?.machine_type || machineType.value;
vehicle.value = previewData?.vehicle || null;
session.value = previewData?.session || session.value;
if (!previewData?.session?.id) {
summary.value = null;
events.value = [];
}
if (Object.prototype.hasOwnProperty.call(previewData || {}, "config_version_id")) {
configVersionId.value = previewData?.config_version_id ?? null;
}
@@ -442,6 +520,7 @@ export function useSelfServeLogic() {
questions.value = nextQuestions;
answers.value = buildAnswersMap(nextQuestions);
setSummaryVisibleQuestions(nextQuestions);
const normalizedConditions = Array.isArray(previewData?.conditions)
? previewData.conditions.map(normalizeCondition)
@@ -461,13 +540,15 @@ export function useSelfServeLogic() {
? previewData.tasks.map(normalizeTask).sort((a, b) => a.order_priority - b.order_priority)
: [];
tasks.value = normalizedTasks;
applyAllowedServices(previewData);
const hydratedTasks = await hydrateTaskAttachments(normalizedTasks);
if (!isFetchRequestActive(requestId)) {
return false;
}
tasks.value = hydratedTasks;
applyAllowedServices(previewData);
return true;
};
@@ -512,8 +593,9 @@ export function useSelfServeLogic() {
const normalizedTasks = summaryData.tasks
.map(normalizeTask)
.sort((a, b) => a.order_priority - b.order_priority);
const mergedTasks = mergeTaskSets(tasks.value, normalizedTasks);
const hydratedTasks = await hydrateTaskAttachments(normalizedTasks);
const hydratedTasks = await hydrateTaskAttachments(mergedTasks);
if (!isFetchRequestActive(requestId)) {
return false;
}
@@ -526,7 +608,7 @@ export function useSelfServeLogic() {
};
const downloadAttachment = async (taskId, attachmentId, attachment = null) => {
const existingDownloadLink = attachment?.download_link || null;
const existingDownloadLink = safeAttachmentDownloadLink(attachment?.download_link);
if (existingDownloadLink) {
window.open(existingDownloadLink, "_blank", "noopener");
return;
@@ -534,10 +616,9 @@ export function useSelfServeLogic() {
try {
const response = await SessionUser.objects.self_serve_tasks.attachments.download(taskId, attachmentId);
if (response?.data?.download_link) {
window.open(response.data.download_link, "_blank", "noopener");
} else if (response?.download_link) {
window.open(response.download_link, "_blank", "noopener");
const downloadLink = safeAttachmentDownloadLink(response?.data?.download_link || response?.download_link);
if (downloadLink) {
window.open(downloadLink, "_blank", "noopener");
}
} catch (error) {
console.error("Error downloading attachment:", error);
@@ -575,10 +656,17 @@ export function useSelfServeLogic() {
}
};
const fetchSelfServeData = async (_departmentId, _vehicleTypeId, laneId = null, reg = null) => {
const fetchSelfServeData = async (_departmentId, _vehicleTypeId, laneId = null, reg = null, options = {}) => {
const fetchKey = createFetchKey(_departmentId, _vehicleTypeId, laneId, reg);
if (!options.force && fetchKey && (fetchKey === latestSuccessfulFetchKey.value || fetchKey === inFlightFetchKey.value)) {
return null;
}
const requestId = beginFetchRequest();
if (!laneId || !reg || reg.trim().length < 2) {
latestSuccessfulFetchKey.value = null;
inFlightFetchKey.value = null;
preview.value = null;
summary.value = null;
session.value = null;
@@ -606,6 +694,7 @@ export function useSelfServeLogic() {
loading.value = true;
requestError.value = null;
inFlightFetchKey.value = fetchKey;
try {
const normalizedReg = reg.trim().toUpperCase();
const normalizedVehicleTypeId = parseInt(_vehicleTypeId);
@@ -687,6 +776,10 @@ export function useSelfServeLogic() {
setSummaryVisibleQuestions(questions.value);
}
if (isFetchRequestActive(requestId)) {
latestSuccessfulFetchKey.value = fetchKey;
}
return previewData;
} catch (error) {
console.error("Error fetching self-serve preview:", error);
@@ -695,6 +788,9 @@ export function useSelfServeLogic() {
}
return null;
} finally {
if (inFlightFetchKey.value === fetchKey) {
inFlightFetchKey.value = null;
}
if (isFetchRequestActive(requestId)) {
loading.value = false;
}
@@ -749,7 +845,7 @@ export function useSelfServeLogic() {
updateResolvedVehicleTypeId(responseSummary, responseSummary?.session);
}
await fetchSelfServeData(departmentId, refreshVehicleTypeId, laneId, normalizedReg);
await fetchSelfServeData(departmentId, refreshVehicleTypeId, laneId, normalizedReg, { force: true });
answers.value = {
...answers.value,
[parseInt(questionId)]: value,
@@ -814,7 +910,7 @@ export function useSelfServeLogic() {
? normalizedRefreshVehicleTypeId
: null;
await fetchSelfServeData(departmentId, refreshVehicleTypeId, parseInt(laneId), normalizedReg);
await fetchSelfServeData(departmentId, refreshVehicleTypeId, parseInt(laneId), normalizedReg, { force: true });
return { deletedCount: conditionIdsToDelete.length };
} catch (error) {
@@ -986,7 +1082,22 @@ export function useSelfServeLogic() {
const updateLaneAllowedServices = async (laneId) => {
if (!laneId) return;
const taskIds = activeTasks.value.map((task) => extractTaskId(task)).filter(Boolean);
const activeTaskIds = activeTasks.value.map((task) => extractTaskId(task)).filter(Boolean);
const taskIds = activeTaskIds.length > 0
? activeTaskIds
: tasks.value.map((task) => extractTaskId(task)).filter(Boolean);
if (taskIds.length === 0) {
requestError.value = null;
return {
success: true,
data: {
lane_id: parseInt(laneId),
allowed_services: allowedServices.value,
skipped_empty_task_sync: true,
},
};
}
try {
const response = await SessionUser.request('/modules/self-serve/lane/services/allowed', 'post', {
@@ -1049,6 +1160,7 @@ export function useSelfServeLogic() {
answers,
completedTasks,
allowedServices,
resolvedVehicleTypeId: lastResolvedVehicleTypeId,
configVersionId,
evaluationTrace,
visibleQuestions,
+103 -39
View File
@@ -2,12 +2,20 @@ import { computed, nextTick, onUnmounted, ref, watch } from "vue";
import { getDepartmentsGuest } from "@/components/pagination/departmentTabs.vue";
import { locations } from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
const isLaneSelfServeEnabled = (lane) => !(
lane?.selfserve_enabled === false
|| lane?.selfserve_enabled === 0
|| lane?.selfserve_enabled === "0"
|| ["false", "off", "no"].includes(String(lane?.selfserve_enabled ?? "").trim().toLowerCase())
);
const isLaneSelfServeEnabled = (lane) =>
!(
lane?.selfserve_enabled === false ||
lane?.selfserve_enabled === 0 ||
lane?.selfserve_enabled === "0" ||
["false", "off", "no"].includes(
String(lane?.selfserve_enabled ?? "")
.trim()
.toLowerCase()
)
);
const hasSelfServeEnabledLane = (department) =>
department?.self_serve_enabled === true && (department.lanes || []).some(isLaneSelfServeEnabled);
const toDepartmentViewModel = (department, distance = null) => ({
id: department.id,
@@ -18,12 +26,22 @@ const toDepartmentViewModel = (department, distance = null) => ({
self_serve_enabled: department.self_serve_enabled,
});
const extractDepartmentFetchErrorMessage = (error) => {
const candidates = [
error?.response?.data?.data?.message,
error?.response?.data?.message,
error?.response?.data?.error,
error?.message,
];
return (
candidates.find((candidate) => typeof candidate === "string" && candidate.trim() !== "") ||
"Kunne ikke hente vaskeafdelinger. Prøv igen."
);
};
export function useWashDepartments(options = {}) {
const {
includeLanes = false,
refreshIntervalMs = 0,
canAccessSuperUser = () => false,
} = options;
const { includeLanes = false, refreshIntervalMs = 0, canAccessSuperUser = () => false } = options;
const guestDepartments = ref([]);
const nearestDepartment = ref(null);
@@ -31,6 +49,8 @@ export function useWashDepartments(options = {}) {
const forceNearestDepartmentEvaluationId = ref(0);
const isSearchingDepartments = ref(false);
const lastDepartmentFetchTime = ref(null);
const departmentFetchError = ref(null);
const departmentSelectionStrategy = ref(null);
let refreshInterval = null;
@@ -57,13 +77,24 @@ export function useWashDepartments(options = {}) {
return nearestDepartment.value.self_serve_enabled === true;
});
const buildGuestDepartmentParams = () => (
includeLanes ? { include_lanes: true } : {}
);
const isDepartmentSelectionDistanceBased = computed(() => departmentSelectionStrategy.value === "distance");
const isDepartmentSelectionFallbackBased = computed(() => departmentSelectionStrategy.value === "fallback");
const hasLocationCoordinates = (locationValue = locations.location.value) => !!locationValue?.coords;
const buildGuestDepartmentParams = () => (includeLanes ? { include_lanes: true } : {});
const fetchDepartments = async () => {
guestDepartments.value = await getDepartmentsGuest(buildGuestDepartmentParams());
lastDepartmentFetchTime.value = Date.now();
try {
guestDepartments.value = await getDepartmentsGuest(buildGuestDepartmentParams());
lastDepartmentFetchTime.value = Date.now();
departmentFetchError.value = null;
} catch (error) {
departmentFetchError.value = extractDepartmentFetchErrorMessage(error);
console.warn("Failed to fetch wash departments:", error);
}
evaluateLocationDepartments(locations.location.value);
return guestDepartments.value;
};
@@ -74,39 +105,45 @@ export function useWashDepartments(options = {}) {
}
const currentTime = Date.now();
if (
!lastDepartmentFetchTime.value
|| (currentTime - lastDepartmentFetchTime.value) >= refreshIntervalMs
) {
if (!lastDepartmentFetchTime.value || currentTime - lastDepartmentFetchTime.value >= refreshIntervalMs) {
return fetchDepartments();
}
return guestDepartments.value;
};
const getForcedDepartment = () => guestDepartments.value.find(
(department) => department.id === forceNearestDepartmentEvaluationId.value
);
const getForcedDepartment = () =>
guestDepartments.value.find((department) => department.id === forceNearestDepartmentEvaluationId.value);
const evaluateLocationDepartments = (locationValue = locations.location.value) => {
if (isForcingNearestDepartment.value && forceNearestDepartmentEvaluationId.value) {
const forcedDepartment = getForcedDepartment();
if (forcedDepartment) {
nearestDepartment.value = toDepartmentViewModel(forcedDepartment);
departmentSelectionStrategy.value = "forced";
return nearestDepartment.value;
}
}
if (guestDepartments.value.length === 0) {
nearestDepartment.value = null;
departmentSelectionStrategy.value = null;
return nearestDepartment.value;
}
if (!hasLocationCoordinates(locationValue)) {
const fallbackDepartment = guestDepartments.value.find(hasSelfServeEnabledLane);
nearestDepartment.value = fallbackDepartment ? toDepartmentViewModel(fallbackDepartment) : null;
departmentSelectionStrategy.value = fallbackDepartment ? "fallback" : null;
return nearestDepartment.value;
}
let currentNearestDepartment = {
id: null,
distance: Infinity,
};
guestDepartments.value.forEach((department) => {
if (!locationValue?.coords) {
return;
}
const from = {
latitude: locationValue.coords.latitude,
longitude: locationValue.coords.longitude,
@@ -124,6 +161,18 @@ export function useWashDepartments(options = {}) {
if (currentNearestDepartment.id) {
nearestDepartment.value = currentNearestDepartment;
departmentSelectionStrategy.value = "distance";
return nearestDepartment.value;
}
const fallbackDepartment =
guestDepartments.value.find((department) => department.self_serve_enabled === true) || guestDepartments.value[0];
if (fallbackDepartment) {
nearestDepartment.value = toDepartmentViewModel(fallbackDepartment);
departmentSelectionStrategy.value = "fallback";
} else {
nearestDepartment.value = null;
departmentSelectionStrategy.value = null;
}
return nearestDepartment.value;
@@ -150,7 +199,7 @@ export function useWashDepartments(options = {}) {
};
const startDepartmentSearch = () => {
if (!canAccessSuperUser()) {
if (!canAccessSuperUser() && hasLocationCoordinates()) {
return;
}
@@ -191,19 +240,30 @@ export function useWashDepartments(options = {}) {
}
};
watch(() => guestDepartments.value, () => {
evaluateLocationDepartments(locations.location.value);
}, { deep: true });
watch(() => locations.location.value, (newValue) => {
evaluateLocationDepartments(newValue);
}, { deep: true });
watch(() => isSearchingDepartments.value, (isSearching) => {
if (!isSearching) {
watch(
() => guestDepartments.value,
() => {
evaluateLocationDepartments(locations.location.value);
},
{ deep: true }
);
watch(
() => locations.location.value,
(newValue) => {
evaluateLocationDepartments(newValue);
},
{ deep: true }
);
watch(
() => isSearchingDepartments.value,
(isSearching) => {
if (!isSearching) {
evaluateLocationDepartments(locations.location.value);
}
}
});
);
onUnmounted(() => {
stopAutoRefresh();
@@ -216,6 +276,10 @@ export function useWashDepartments(options = {}) {
forceNearestDepartmentEvaluationId,
isSearchingDepartments,
lastDepartmentFetchTime,
departmentFetchError,
departmentSelectionStrategy,
isDepartmentSelectionDistanceBased,
isDepartmentSelectionFallbackBased,
availableProductIds,
doesCurrentDepartmentSelectionHaveSelfServeEnabled,
fetchDepartments,
+28 -11
View File
@@ -7,6 +7,11 @@ export const WASH_STEPS = {
COMPLETED: 5,
};
const normalizeLaneId = (value) => {
const normalized = parseInt(String(value ?? ""), 10);
return Number.isNaN(normalized) || normalized <= 0 ? null : normalized;
};
export function useWashFlowState(options) {
const {
currentStep,
@@ -14,7 +19,7 @@ export function useWashFlowState(options) {
customerNumberInput,
licensePlateInput,
vehicleTypeSelect,
availableProductIds,
isCustomerNumberRequired,
radioLaneOption,
nearestDepartment,
allVisibleQuestionsAnswered,
@@ -34,6 +39,17 @@ export function useWashFlowState(options) {
activeTasks.value.length > 0 ? steps.TASKS : steps.WASH_IN_PROGRESS
);
const getSelectedLaneId = () => normalizeLaneId(radioLaneOption.value);
const getSelectedLane = () => {
const selectedLaneId = getSelectedLaneId();
if (selectedLaneId === null) {
return null;
}
return nearestDepartment.value?.lanes.find((lane) => normalizeLaneId(lane.id) === selectedLaneId) || null;
};
const clickableSteps = {
[steps.VEHICLE]: () => {
if (washInProgress.value) {
@@ -41,16 +57,15 @@ export function useWashFlowState(options) {
}
const selectedVehicleTypeId = vehicleTypeSelect.value;
const allowedProductIds = new Set((availableProductIds.value || []).map((productId) => String(productId)));
const hasAllowedVehicleType = selectedVehicleTypeId !== null
&& selectedVehicleTypeId !== undefined
&& allowedProductIds.has(String(selectedVehicleTypeId));
const hasSelectedVehicleType = selectedVehicleTypeId !== null && selectedVehicleTypeId !== undefined;
const customerNumberValue = customerNumberInput.value;
const customerNumberValid = customerNumberValue === null
const requiresCustomerNumber = isCustomerNumberRequired?.value ?? false;
const customerNumberValid = !requiresCustomerNumber
|| customerNumberValue === null
|| customerNumberValue === undefined
|| String(customerNumberValue).trim() !== "";
const licensePlateValid = !!(licensePlateInput.value && licensePlateInput.value.trim() !== "");
if (!hasAllowedVehicleType) {
if (!hasSelectedVehicleType) {
return false;
}
@@ -103,7 +118,7 @@ export function useWashFlowState(options) {
}
if (currentStep.value === steps.SELECT_LANE) {
const selectedLane = nearestDepartment.value?.lanes.find((lane) => lane.id === radioLaneOption.value);
const selectedLane = getSelectedLane();
if (!selectedLane || !isLaneAvailable(selectedLane)) {
return true;
}
@@ -133,9 +148,10 @@ export function useWashFlowState(options) {
const handleConfirmNext = async () => {
if (currentStep.value === steps.QUESTIONS) {
editAnswers.value = false;
if (radioLaneOption.value && radioLaneOption.value !== "Any") {
const selectedLaneId = getSelectedLaneId();
if (selectedLaneId !== null) {
try {
await updateLaneAllowedServices(radioLaneOption.value);
await updateLaneAllowedServices(selectedLaneId);
} catch {
return;
}
@@ -150,8 +166,9 @@ export function useWashFlowState(options) {
return;
}
const selectedLaneId = getSelectedLaneId() ?? radioLaneOption.value;
await onStartWash(
radioLaneOption.value,
selectedLaneId,
licensePlateInput.value,
customerNumberInput.value,
targetStepForStart()
+50 -12
View File
@@ -70,7 +70,10 @@ export function useWashSessionActions(options) {
const isAlreadyStoppedStopError = (message) => {
const normalized = String(message || "").toLowerCase();
return normalized.includes("cannot stop lane") && normalized.includes("not occupied");
return (
normalized.includes("not occupied") &&
(normalized.includes("cannot stop lane") || normalized.includes("cannot invoice"))
);
};
const clearActiveWashState = () => {
@@ -170,10 +173,15 @@ export function useWashSessionActions(options) {
return null;
}
return await executeSelfServeCommand(resolvedLaneId, command, {
customer_number: null,
license_plate: null,
}, options);
return await executeSelfServeCommand(
resolvedLaneId,
command,
{
customer_number: null,
license_plate: null,
},
options
);
} finally {
loadingRef.value = false;
}
@@ -185,6 +193,24 @@ export function useWashSessionActions(options) {
const openPropertyExitGate = async (laneId = null, options = {}) =>
executePropertyGateCommand("OPEN_PROPERTY_EXIT_GATE", laneId, openingPropertyExitGate, options);
const rollbackStartedWash = async (laneId) => {
const stopResponse = await executeSelfServeCommand(
laneId,
"STOP",
{
customer_number: null,
license_plate: null,
},
{ suppressAlert: true }
);
if (!stopResponse) {
console.error(`Failed to roll back started wash on lane ${laneId}:`, lastCommandErrorMessage.value);
}
return stopResponse;
};
const onStartWash = async (laneId, licensePlate, customerNumber, targetStep = steps.WASH_IN_PROGRESS) => {
if (isStartingWash.value) {
return false;
@@ -233,6 +259,25 @@ export function useWashSessionActions(options) {
return false;
}
if (radioWashType.value === "Machine" && isServiceAllowed("MACHINE")) {
let machineRelayResponse = null;
try {
machineRelayResponse = await enableMachineRelay(laneId);
} catch (error) {
console.error("Error enabling machine relay after wash start:", error);
await rollbackStartedWash(laneId);
alertFn(extractCommandErrorMessage(error, "Kunne ikke starte maskinen. Prøv igen."));
return false;
}
const machineRelaySuccessValue = machineRelayResponse?.data?.success ?? machineRelayResponse?.success;
if (machineRelaySuccessValue === false) {
await rollbackStartedWash(laneId);
alertFn(extractCommandErrorMessage(machineRelayResponse, "Kunne ikke starte maskinen. Prøv igen."));
return false;
}
}
washLaneId.value = laneId;
washStartTime.value = Date.now();
completedDurationMs.value = null;
@@ -246,13 +291,6 @@ export function useWashSessionActions(options) {
reg: licensePlate.trim().toUpperCase(),
});
if (radioWashType.value === "Machine" && isServiceAllowed("MACHINE")) {
try {
await enableMachineRelay(laneId);
} catch (error) {
console.error("Error enabling machine relay after wash start:", error);
}
}
return true;
} catch (error) {
console.error("Error starting self-serve wash:", error);
+12
View File
@@ -29,6 +29,7 @@ const normalizeReleaseSource = (value) => {
};
export const RELEASE_SOURCE_ENV = String(import.meta.env.VITE_RELEASE_SOURCE || "").trim().toLowerCase();
export const RELEASE_SOURCE = normalizeReleaseSource(RELEASE_SOURCE_ENV);
export const DEFAULT_LEGACY_API_URL = "https://api.truckwash.io";
export const DEFAULT_PUBLIC_GATEWAY_API_URL = "https://api-v2.truckwash.io";
export const DEFAULT_STABLE_API_URL = `${DEFAULT_PUBLIC_GATEWAY_API_URL}/master/api`;
@@ -50,11 +51,22 @@ export const RELEASE_MANAGER_CONTROL_API_FALLBACK_URLS = String(
.split(",")
.map((url) => normalizeApiUrl(url.trim()))
.filter(Boolean);
export const RELEASE_TRUSTED_ORIGINS = String(import.meta.env.VITE_RELEASE_TRUSTED_ORIGINS || "")
.split(",")
.map((origin) => origin.trim().replace(/\/+$/, ""))
.filter(Boolean);
export const SELF_SERVE_ATTACHMENT_TRUSTED_ORIGINS = String(
import.meta.env.VITE_SELF_SERVE_ATTACHMENT_TRUSTED_ORIGINS || ""
)
.split(",")
.map((origin) => origin.trim().replace(/\/+$/, ""))
.filter(Boolean);
// Allowed origins
export const ALLOWED_ORIGINS = [
"https://truckwash.io",
"https://www.truckwash.io",
new URL(DEFAULT_PUBLIC_GATEWAY_API_URL).origin,
"http://localhost:5173",
"http://localhost:4173",
"http://localhost:4174",
@@ -36,7 +36,73 @@ const entryContext = (entry = {}) => {
return context && typeof context === "object" && !Array.isArray(context) ? context : {};
};
const formatDetailValue = (value) => {
const REDACTED_CONTEXT_VALUE = "[REDACTED]";
const SENSITIVE_CONTEXT_KEY_PATTERN = /(?:token|secret|password|credential|api[_-]?key|authorization|auth|signature)/i;
const SENSITIVE_CONTEXT_QUERY_PATTERN = SENSITIVE_CONTEXT_KEY_PATTERN;
const redactSensitiveUrlParams = (value) => {
const text = String(value || "");
if (!text) {
return text;
}
const redactParams = (url) => {
let redacted = false;
url.searchParams.forEach((_paramValue, key) => {
if (SENSITIVE_CONTEXT_QUERY_PATTERN.test(key)) {
url.searchParams.set(key, REDACTED_CONTEXT_VALUE);
redacted = true;
}
});
return redacted ? url.toString() : text;
};
try {
return redactParams(new URL(text));
} catch {
return text.replace(/([?&])([^=&#]+)=([^&#]*)/g, (match, separator, rawKey) => {
let key = rawKey;
try {
key = decodeURIComponent(rawKey.replace(/\+/g, " "));
} catch {
key = rawKey;
}
return SENSITIVE_CONTEXT_QUERY_PATTERN.test(key) ? `${separator}${rawKey}=${REDACTED_CONTEXT_VALUE}` : match;
});
}
};
const redactEdgeGatewayLogContext = (value, seen = new WeakSet()) => {
if (value === null || typeof value === "undefined") {
return value;
}
if (typeof value === "string") {
return redactSensitiveUrlParams(value);
}
if (typeof value !== "object") {
return value;
}
if (seen.has(value)) {
return "[Circular]";
}
seen.add(value);
if (Array.isArray(value)) {
return value.map((item) => redactEdgeGatewayLogContext(item, seen));
}
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [
key,
SENSITIVE_CONTEXT_KEY_PATTERN.test(key) ? REDACTED_CONTEXT_VALUE : redactEdgeGatewayLogContext(item, seen),
])
);
};
const formatDetailValue = (value, { redactContext = false } = {}) => {
if (value === null || typeof value === "undefined" || value === "") {
return "None";
}
@@ -44,13 +110,14 @@ const formatDetailValue = (value) => {
return value ? "yes" : "no";
}
if (typeof value === "object") {
const displayValue = redactContext ? redactEdgeGatewayLogContext(value) : value;
try {
return JSON.stringify(value, null, 2);
return JSON.stringify(displayValue, null, 2);
} catch {
return String(value);
return String(displayValue);
}
}
return String(value);
return redactContext && typeof value === "string" ? redactSensitiveUrlParams(value) : String(value);
};
const detailRow = (label, value) => ({
@@ -61,7 +128,7 @@ const detailRow = (label, value) => ({
const contextDetailRow = (value) => ({
label: "Context",
value: formatDetailValue(value),
value: formatDetailValue(value, { redactContext: true }),
multiline: true,
actionsOnly: true,
});
@@ -208,7 +275,9 @@ const relayActorLabel = (entry = {}) => {
};
const normalizeRelayRole = (role = "") => {
const normalized = String(role || "").trim().toUpperCase();
const normalized = String(role || "")
.trim()
.toUpperCase();
const aliases = {
ENTRANCE: "ENTRY",
IN: "ENTRY",
@@ -260,7 +329,8 @@ const firstRelayText = (values = []) => {
const relayDisplayName = (entry = {}) => {
const context = entryContext(entry);
const actionContext = context.action_context && typeof context.action_context === "object" ? context.action_context : {};
const actionContext =
context.action_context && typeof context.action_context === "object" ? context.action_context : {};
const binding = context.binding && typeof context.binding === "object" ? context.binding : {};
const request = relayRequestPayload(entry);
return firstRelayText([
@@ -284,7 +354,8 @@ const relayIdentifier = (entry = {}) => {
const relaySignalLabel = (entry = {}) => {
const context = entryContext(entry);
const actionContext = context.action_context && typeof context.action_context === "object" ? context.action_context : {};
const actionContext =
context.action_context && typeof context.action_context === "object" ? context.action_context : {};
const signal = context.signal && typeof context.signal === "object" ? context.signal : {};
const role = normalizeRelayRole(context.relay_role || actionContext.relay_role || actionContext.role);
const action = String(context.action || signal.command_type || "RELAY").toUpperCase();
@@ -2,6 +2,7 @@
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import {
cancelEdgeGatewayOperation,
clearEdgeGatewayWorkspaceCache,
createEdgeGatewayInstallToken,
createEdgeGatewayOperation,
deleteEdgeGateway,
@@ -12,16 +13,18 @@ import {
getEdgeGatewayStatistics,
getEdgeGatewayTasks,
listEdgeGatewayDepartments,
isEdgeGatewayAuthorizationError,
listEdgeGateways,
peekCachedEdgeGateway,
peekCachedEdgeGatewayDepartments,
peekCachedEdgeGatewayList,
removeEdgeGatewayCache,
rotateEdgeGatewayCredentials,
saveEdgeGatewayBindings,
setDepartmentGatewayCutover,
updateEdgeGateway,
} from "@/services/edgeGateways.js";
import { normalizeEdgeGatewayError, normalizeGatewayWebSocketClose } from "@/features/edgeGateways/edgeGatewayErrors.js";
import { normalizeEdgeGatewayError, normalizeGatewayWebSocketClose, redactEdgeGatewayDiagnostic } from "@/features/edgeGateways/edgeGatewayErrors.js";
import EdgeGatewayOverviewPage from "@/features/edgeGateways/EdgeGatewayOverviewPage.vue";
import EdgeGatewayInventoryPage from "@/features/edgeGateways/EdgeGatewayInventoryPage.vue";
import EdgeGatewayTasksPage from "@/features/edgeGateways/EdgeGatewayTasksPage.vue";
@@ -183,7 +186,7 @@ const terminalGatewayBrokerDiagnostics = (gateway = selectedGatewayView.value) =
if (value === null || value === undefined || value === "") {
return;
}
lines.push(`${label}: ${value}`);
lines.push(`${label}: ${redactEdgeGatewayDiagnostic(value)}`);
};
push("Gateway ID", gateway?.id);
@@ -1215,6 +1218,15 @@ const refreshSelected = async (gatewayId = activeGatewayId.value, { forceRefresh
}
return gateway;
} catch (error) {
if (isEdgeGatewayAuthorizationError(error)) {
removeEdgeGatewayCache(gatewayId);
unavailableGatewayId.value = String(gatewayId);
selectedGateway.value = null;
resetGatewayViewSnapshots();
fail(error);
return null;
}
const fallbackGateway = findLocalGatewaySnapshot(gatewayId);
if (fallbackGateway) {
setSelectedGatewaySnapshot(fallbackGateway);
@@ -1561,6 +1573,16 @@ const load = async () => {
departments.value = unwrap(departmentsResponse, []);
await syncSelection();
} catch (error) {
if (isEdgeGatewayAuthorizationError(error)) {
clearEdgeGatewayWorkspaceCache();
gateways.value = [];
fleetUsage.value = buildFleetUsageFromRows([]);
if (activeGatewayId.value) {
unavailableGatewayId.value = String(activeGatewayId.value);
}
selectedGateway.value = null;
resetGatewayViewSnapshots();
}
fail(error);
} finally {
loading.value.init = false;
@@ -1,4 +1,6 @@
<script setup>
import { redactEdgeGatewayDiagnostic } from "@/features/edgeGateways/edgeGatewayErrors.js";
const props = defineProps({
status: {
type: String,
@@ -44,7 +46,10 @@ const shortcuts = [
{ id: "docker ps", label: "docker ps" },
];
const diagnosticsText = () => (Array.isArray(props.diagnostics) ? props.diagnostics.join("\n") : "");
const diagnosticsText = () =>
Array.isArray(props.diagnostics)
? props.diagnostics.map((entry) => redactEdgeGatewayDiagnostic(entry)).join("\n")
: "";
const copyDiagnostics = async () => {
const text = diagnosticsText();
@@ -0,0 +1,79 @@
import { RELEASE_TRUSTED_ORIGINS } from "@/config.js";
const splitOriginList = (value) =>
String(value || "")
.split(",")
.map((origin) => origin.trim().replace(/\/+$/, ""))
.filter(Boolean);
export const ALLOWED_PUBLIC_BROKER_PROTOCOLS = Object.freeze(["https:", "wss:"]);
export const EDGE_GATEWAY_PUBLIC_BROKER_ORIGINS = Object.freeze([
"https://api.truckwash.io:4433",
"wss://api.truckwash.io:4433",
...RELEASE_TRUSTED_ORIGINS,
...splitOriginList(import.meta.env.VITE_EDGE_GATEWAY_PUBLIC_BROKER_ORIGINS),
]);
export const canUseStubBrokerAuthMode = () => import.meta.env.DEV || import.meta.env.MODE === "test";
export const sanitizeBrokerAuthMode = (value) => {
if (value === "stub" && canUseStubBrokerAuthMode()) {
return "stub";
}
return "manager";
};
export const redactBrokerSharedSecret = () => "";
export const allowedPublicBrokerOrigins = () => {
const origins = new Set(EDGE_GATEWAY_PUBLIC_BROKER_ORIGINS);
if (typeof window !== "undefined" && window.location?.origin) {
const currentOrigin = window.location.origin.replace(/\/+$/, "");
origins.add(currentOrigin);
if (window.location.hostname) {
const host = window.location.port
? `${window.location.hostname}:${window.location.port}`
: window.location.hostname;
origins.add(`https://${host}`);
origins.add(`wss://${host}`);
}
}
return origins;
};
export const validatePublicBrokerUrl = (value) => {
const normalized = String(value || "").trim();
if (!normalized) {
return { ok: true, value: "" };
}
let parsed;
try {
parsed = new URL(normalized);
} catch (_error) {
return {
ok: false,
message: "Public broker URL must be an absolute HTTPS or WSS URL on an approved origin.",
};
}
if (!ALLOWED_PUBLIC_BROKER_PROTOCOLS.includes(parsed.protocol)) {
return {
ok: false,
message: "Public broker URL must use HTTPS or WSS.",
};
}
if (!allowedPublicBrokerOrigins().has(parsed.origin)) {
return {
ok: false,
message: "Public broker URL origin is not approved for browser edge gateway sessions.",
};
}
return { ok: true, value: parsed.href };
};
+58 -9
View File
@@ -55,6 +55,36 @@ const WEBSOCKET_CLOSE_MESSAGES = {
socket_error: "Terminal websocket connection failed.",
};
const SENSITIVE_DIAGNOSTIC_KEYS = new Set([
"accesstoken",
"agenttoken",
"apikey",
"authorization",
"auth",
"clientsecret",
"jwt",
"key",
"password",
"secret",
"signature",
"token",
]);
const normalizeDiagnosticKey = (key) =>
String(key || "")
.replace(/[^a-z0-9]/gi, "")
.toLowerCase();
const isSensitiveDiagnosticKey = (key) => SENSITIVE_DIAGNOSTIC_KEYS.has(normalizeDiagnosticKey(key));
const redactSensitiveAssignments = (value) =>
String(value)
.replace(/\b(authorization|auth)\s*[:=]\s*(?:Bearer|Basic)\s+[^\s,;&]+/gi, (_match, key) => `${key}=***`)
.replace(
/([?&;,\s]?(access[_-]?token|agent[_-]?token|api[_-]?key|authorization|auth|client[_-]?secret|jwt|key|password|secret|signature|token)\s*[:=]\s*)("[^"\s,;&]*"|'[^'\s,;&]*'|[^\s,;&]+)/gi,
(_match, prefix) => `${prefix}***`
);
export function redactEdgeGatewayUrl(value) {
if (!value) {
return null;
@@ -62,17 +92,33 @@ export function redactEdgeGatewayUrl(value) {
try {
const url = new URL(String(value), typeof window !== "undefined" ? window.location.origin : "http://localhost");
["token", "agentToken", "agent_token"].forEach((key) => {
if (url.searchParams.has(key)) {
if (url.username) {
url.username = "***";
}
if (url.password) {
url.password = "***";
}
Array.from(url.searchParams.keys()).forEach((key) => {
if (isSensitiveDiagnosticKey(key)) {
url.searchParams.set(key, "***");
}
});
return url.toString();
} catch (_error) {
return String(value).replace(/([?&](?:token|agentToken|agent_token)=)[^&]+/gi, "$1***");
return redactSensitiveAssignments(String(value));
}
}
export function redactEdgeGatewayDiagnostic(value) {
if (value === null || value === undefined) {
return value;
}
return redactSensitiveAssignments(
String(value).replace(/\b(?:wss?|https?|mqtts?):\/\/[^\s<>"')]+/gi, (match) => redactEdgeGatewayUrl(match))
);
}
const formatDiagnosticLines = (diagnostics, prefix = "Diagnostic") => {
if (!diagnostics || typeof diagnostics !== "object" || Array.isArray(diagnostics)) {
return [];
@@ -83,20 +129,23 @@ const formatDiagnosticLines = (diagnostics, prefix = "Diagnostic") => {
if (value === null || value === undefined || value === "") {
return;
}
lines.push(`${label}: ${value}`);
lines.push(`${label}: ${redactEdgeGatewayDiagnostic(value)}`);
};
push(`${prefix} code`, diagnostics.reason_code || diagnostics.error_code);
push("Broker URL", diagnostics.broker_url);
push("WebSocket URL", redactEdgeGatewayUrl(diagnostics.ws_url));
push("WebSocket URL", diagnostics.ws_url);
push("Derived URL warning", diagnostics.derived_warning);
if (diagnostics.broker_presence && typeof diagnostics.broker_presence === "object") {
push("Broker connected", diagnostics.broker_presence.connected === true ? "yes" : "no");
push("Broker connection", diagnostics.broker_presence.connection_id);
push("Broker last seen", diagnostics.broker_presence.last_seen_at);
push("Broker age", Number.isFinite(Number(diagnostics.broker_presence.age_seconds))
? `${Number(diagnostics.broker_presence.age_seconds)}s`
: null);
push(
"Broker age",
Number.isFinite(Number(diagnostics.broker_presence.age_seconds))
? `${Number(diagnostics.broker_presence.age_seconds)}s`
: null
);
push("Broker last error", diagnostics.broker_presence.last_error);
push("Broker disconnect reason", diagnostics.broker_presence.disconnect_reason);
}
@@ -180,7 +229,7 @@ export function normalizeGatewayWebSocketClose(event = {}, context = {}) {
details.push(`WebSocket code: ${code}`);
}
if (reason) {
details.push(`Close reason: ${reason}`);
details.push(`Close reason: ${redactEdgeGatewayDiagnostic(reason)}`);
}
if (wasClean !== null) {
details.push(`Clean close: ${wasClean ? "yes" : "no"}`);
@@ -10,6 +10,66 @@ const MOCK_SHELL_PROMPT = "edge@truckwash:/opt/truckwash-edge-agent$ ";
const isMockSocketUrl = (value) => String(value || "").startsWith(MOCK_SOCKET_PREFIX);
const SENSITIVE_SOCKET_QUERY_KEYS = new Set([
"access_token",
"auth",
"authorization",
"bearer",
"jwt",
"session",
"session_token",
"token",
]);
const getBrowserLocation = () => (typeof window !== "undefined" && window.location ? window.location : null);
const getConfiguredTrustedSocketOrigins = () =>
String(import.meta.env?.VITE_EDGE_GATEWAY_WS_ALLOWED_ORIGINS || "")
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
const toSocketOrigin = (value) => {
const location = getBrowserLocation();
const base = location?.origin || "http://localhost";
const url = new URL(value, base);
if (url.protocol === "https:") {
url.protocol = "wss:";
} else if (url.protocol === "http:") {
url.protocol = "ws:";
}
return url.origin;
};
const isLocalSocketHost = (hostname) => ["localhost", "127.0.0.1", "::1", "[::1]"].includes(String(hostname || ""));
const getTrustedSocketOrigins = () => {
const origins = new Set();
const location = getBrowserLocation();
if (location?.origin) {
origins.add(toSocketOrigin(location.origin));
}
getConfiguredTrustedSocketOrigins().forEach((origin) => origins.add(toSocketOrigin(origin)));
return origins;
};
const assertTrustedSocketUrl = (url) => {
if (!["ws:", "wss:"].includes(url.protocol)) {
throw new Error("Unsupported websocket URL protocol");
}
if (url.protocol !== "wss:" && !isLocalSocketHost(url.hostname)) {
throw new Error("Gateway websocket URL must use wss");
}
const trustedOrigins = getTrustedSocketOrigins();
if (trustedOrigins.size > 0 && !trustedOrigins.has(url.origin)) {
throw new Error("Gateway websocket URL origin is not trusted");
}
};
const toNativeSocketUrl = (socketUrl, query = {}) => {
if (!socketUrl) {
throw new Error("Missing websocket URL");
@@ -19,16 +79,48 @@ const toNativeSocketUrl = (socketUrl, query = {}) => {
return socketUrl;
}
const url = new URL(socketUrl, typeof window !== "undefined" ? window.location.origin : "http://localhost");
const url = new URL(socketUrl, getBrowserLocation()?.origin || "http://localhost");
assertTrustedSocketUrl(url);
Array.from(url.searchParams.keys()).forEach((key) => {
if (SENSITIVE_SOCKET_QUERY_KEYS.has(String(key).toLowerCase())) {
url.searchParams.delete(key);
}
});
Object.entries(query || {}).forEach(([key, value]) => {
if (value === null || value === undefined || value === "") {
return;
}
if (SENSITIVE_SOCKET_QUERY_KEYS.has(String(key).toLowerCase())) {
return;
}
url.searchParams.set(key, String(value));
});
return url.toString();
};
const createSessionAuthenticationPayload = (session, gatewayId, kind) => {
if (!session?.token) {
return null;
}
return {
type: "AUTH",
gatewayId: String(gatewayId || session.gateway_id || ""),
token: String(session.token),
sessionId: session.id || session.session_id || null,
kind,
};
};
const sendSessionAuthentication = (send, session, gatewayId, kind) => {
const authPayload = createSessionAuthenticationPayload(session, gatewayId, kind);
if (authPayload) {
send(authPayload);
}
};
const safeJsonParse = (payload) => {
if (payload === null || payload === undefined || payload === "") {
return null;
@@ -292,10 +384,10 @@ export async function createGatewayStreamClient(gatewayId, scopes = [], handlers
const session = unwrapEdgeGatewayResponse(response, {});
const socketUrl = toNativeSocketUrl(session.ws_url, {
gatewayId,
token: session.token,
});
const connection = createConnection(socketUrl, session, "stream", handlers, ({ send }) => {
sendSessionAuthentication(send, session, gatewayId, "stream");
send({
type: "SUBSCRIBE",
gatewayId: String(gatewayId),
@@ -318,9 +410,10 @@ export async function createGatewayShellClient(gatewayId, options = {}, handlers
const session = unwrapEdgeGatewayResponse(response, {});
const socketUrl = toNativeSocketUrl(session.ws_url, {
gatewayId,
token: session.token,
});
const connection = createConnection(socketUrl, session, "shell", handlers);
const connection = createConnection(socketUrl, session, "shell", handlers, ({ send }) => {
sendSessionAuthentication(send, session, gatewayId, "shell");
});
return {
session,
+74 -56
View File
@@ -112,7 +112,7 @@
"api": "API",
"app": "app",
"appen": "appen",
"ar": "år",
"ar": "@:{'words.generated.years'}",
"are": "are",
"arsag": "årsag",
"at": "at",
@@ -679,7 +679,7 @@
"nøgle": "nøgle",
"nogle": "nogle",
"nok": "nok",
"nøkkel": "nøgle",
"nøkkel": "@:{'words.generated.nøgle'}",
"nøkkelen": "nøglen",
"not": "not",
"note": "note",
@@ -1599,7 +1599,25 @@
"view_lane_new_tab": "@:{'words.generated.abn'} @:{'words.generated.bane'} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
"view_lane_setup_new_tab": "@:{'words.generated.abn'} baneopsætning @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
"view_order_new_tab": "@:{'words.generated.abn'} @:{'words.generated.ordre'} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
"view_vehicle_new_tab": "?@:{'words.generated.bn'} @:{'words.generated.k'}?@:{'words.generated.ret'}?j {reg} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}"
"view_vehicle_new_tab": "?@:{'words.generated.bn'} @:{'words.generated.k'}?@:{'words.generated.ret'}?j {reg} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
"accept_self_serve_wash": "Godkend selvbetjent vask",
"accept_self_serve_wash_confirm": "Flyt denne kladde til kunde #{customerNumber} med selvbetjeningsdetaljerne?",
"accept_self_serve_wash_success": "Den selvbetjente vask blev godkendt.",
"email_notifications_section": "E-mail notifikationer",
"resend_booking_completion_confirmation": "(Gen)send bekræftelse for færdig booking",
"resend_booking_completion_confirmation_error": "Bekræftelsen for færdig booking kunne ikke sendes",
"resend_booking_completion_confirmation_success": "Bekræftelsen for færdig booking er sendt",
"resend_booking_confirmation": "(Gen)send bookingbekræftelse",
"resend_booking_confirmation_error": "Bookingbekræftelsen kunne ikke sendes",
"resend_booking_confirmation_success": "Bookingbekræftelsen er sendt",
"resend_wash_certificate": "(Gen)send vaskecertifikat",
"resend_wash_certificate_error": "Vaskecertifikatet kunne ikke sendes",
"resend_wash_certificate_success": "Vaskecertifikatet er sendt",
"self_serve_customer": "Kunde",
"self_serve_driver": "Chauffør",
"self_serve_elapsed": "Tid",
"self_serve_wash_attachment": "Selvbetjent vask",
"self_serve_wash_attachment_for_customer": "Selvbetjent vask for kunde #{customerNumber}"
},
"show_details": "@:{'words.generated.vis'} @:{'words.generated.detaljer'}",
"show_order_details": "@:{'words.generated.vis'} @:{'words.generated.ordredetaljer'}",
@@ -2439,6 +2457,59 @@
"username_desc": "@.capitalize:{'words.generated.brugernavnet'} @:{'words.generated.bruges'} @:{'words.generated.til'} @:{'words.generated.at'} @:{'words.generated.autentificere'} @:{'words.generated.api'} @:{'words.generated.forbindelsen'}."
},
"release_manager": {
"integrations": {
"title": "GitHub og Coolify",
"subtitle": "Knyt GitHub-repositories og branches til kanalspecifikke Coolify-services.",
"choose_repository": "Vælg repository",
"add_health_url": "tilføj health-URL",
"github_token_not_configured": "GitHub-token er ikke konfigureret; private repositories kan ikke vises.",
"open_github_token_settings": "Åbn GitHub token-indstillinger",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/backend-php",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Seneste: {commit}",
"coolify_instance": "Coolify-instans",
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
"no_coolify_instance": "Ingen Coolify-instans",
"coolify_instance_placeholder": "Coolify-instans-id",
"coolify_service_uuid": "Coolify service-UUID",
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
"coolify_service_uuid_placeholder": "Coolify service-UUID",
"health_url": "Health-URL",
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
"health_url_placeholder": "Health-URL",
"ssl_domain": "Load balancer-domæne",
"ssl_domain_message": "Skal være et DNS-domæne, der routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer-domæne",
"https_domain_for_coolify": "Domæne kontrolleret af load balanceren",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
"deploy_now": "Deploy nu",
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
"create_missing_service": "Opret manglende service",
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
},
"title": "Release Manager",
"subtitle": "Gradvise API- og Front-End-udgivelser",
"channel_names": {
@@ -2884,59 +2955,6 @@
"severity": "Alvorlighed",
"any_severity": "Alle alvorligheder",
"no_timeline_events": "Ingen timeline-events matcher de aktuelle filtre."
},
"integrations": {
"title": "GitHub og Coolify",
"subtitle": "Knyt GitHub-repositories og branches til kanalspecifikke Coolify-services.",
"choose_repository": "Vælg repository",
"add_health_url": "tilføj health-URL",
"github_token_not_configured": "GitHub-token er ikke konfigureret; private repositories kan ikke vises.",
"open_github_token_settings": "Åbn GitHub token-indstillinger",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/backend-php",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Seneste: {commit}",
"coolify_instance": "Coolify-instans",
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
"no_coolify_instance": "Ingen Coolify-instans",
"coolify_instance_placeholder": "Coolify-instans-id",
"coolify_service_uuid": "Coolify service-UUID",
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
"coolify_service_uuid_placeholder": "Coolify service-UUID",
"health_url": "Health-URL",
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
"health_url_placeholder": "Health-URL",
"ssl_domain": "Load balancer-domæne",
"ssl_domain_message": "Skal være et DNS-domæne, der routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer-domæne",
"https_domain_for_coolify": "Domæne kontrolleret af load balanceren",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
"deploy_now": "Deploy nu",
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
"create_missing_service": "Opret manglende service",
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
}
}
},
+91 -56
View File
@@ -1061,7 +1061,7 @@
"uhrzeit": "Uhrzeit",
"um": "um",
"umbenannt": "umbenannt",
"umbenennen": "umbenennen",
"umbenennen": "@:{'words.generated.rename'}",
"umsatz": "Umsatz",
"umweltgeb": "Umweltgeb",
"unabhangig": "unabhängig",
@@ -1709,7 +1709,25 @@
"view_lane_new_tab": "@:{'words.generated.spur'} @:{'words.generated.in'} @:{'words.generated.neuem'} @:{'words.generated.tab'} @:{'words.generated.anzeigen'}",
"view_lane_setup_new_tab": "Spurkonfiguration @:{'words.generated.in'} @:{'words.generated.neuem'} @:{'words.generated.tab'} @:{'words.generated.anzeigen'}",
"view_order_new_tab": "@:{'words.generated.auftrag'} @:{'words.generated.in'} @:{'words.generated.neuem'} @:{'words.generated.tab'} @:{'words.generated.anzeigen'}",
"view_vehicle_new_tab": "@:{'words.generated.fahrzeug'} {reg} @:{'words.generated.in'} @:{'words.generated.neuem'} @:{'words.generated.tab'} @:{'words.generated.anzeigen'}"
"view_vehicle_new_tab": "@:{'words.generated.fahrzeug'} {reg} @:{'words.generated.in'} @:{'words.generated.neuem'} @:{'words.generated.tab'} @:{'words.generated.anzeigen'}",
"accept_self_serve_wash": "Self-Service-Wäsche akzeptieren",
"accept_self_serve_wash_confirm": "Diesen Entwurf mit den Self-Service-Details zu Kunde #{customerNumber} verschieben?",
"accept_self_serve_wash_success": "Die Self-Service-Wäsche wurde akzeptiert.",
"email_notifications_section": "E-Mail-Benachrichtigungen",
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
"resend_booking_confirmation": "(Re)send booking confirmation",
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
"resend_booking_confirmation_success": "The booking confirmation has been sent",
"resend_wash_certificate": "(Re)send wash certificate",
"resend_wash_certificate_error": "The wash certificate could not be sent",
"resend_wash_certificate_success": "The wash certificate has been sent",
"self_serve_customer": "Kunde",
"self_serve_driver": "Fahrer",
"self_serve_elapsed": "Zeit",
"self_serve_wash_attachment": "Self-Service-Wäsche",
"self_serve_wash_attachment_for_customer": "Self-Service-Wäsche für Kunde #{customerNumber}"
},
"show_details": "@.capitalize:{'words.generated.details'} @:{'words.generated.anzeigen'}",
"show_order_details": "@:{'words.generated.auftragsdetails'} @:{'words.generated.anzeigen'}",
@@ -2549,6 +2567,59 @@
"username_desc": "@.capitalize:{'words.generated.der'} @:{'words.generated.benutzername'} @:{'words.generated.wird'} @:{'words.generated.zur'} @:{'words.generated.authentifizierung'} @:{'words.generated.der'} @:{'words.generated.api'}-@:{'words.generated.verbindung'} @:{'words.generated.verwendet'}."
},
"release_manager": {
"integrations": {
"title": "GitHub und Coolify",
"subtitle": "GitHub-Repositories und Branches kanalspezifischen Coolify-Services zuordnen.",
"choose_repository": "Repository auswählen",
"add_health_url": "Health-URL hinzufügen",
"github_token_not_configured": "GitHub-Token ist nicht konfiguriert; private Repositories können nicht aufgelistet werden.",
"open_github_token_settings": "GitHub-Token-Einstellungen öffnen",
"channel": "Channel",
"app": "App",
"repository": "Repository",
"repository_message": "Example: truckwash/backend-php",
"repository_placeholder": "owner/repo",
"branch": "Branch",
"branch_message": "Example: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Neueste: {commit}",
"coolify_instance": "Coolify-Instanz",
"coolify_instance_message": "Leer lassen für manuelle Deployment-Einträge.",
"no_coolify_instance": "Keine Coolify-Instanz",
"coolify_instance_placeholder": "Coolify-Instanz-ID",
"coolify_service_uuid": "Coolify service UUID",
"coolify_service_uuid_message": "Bestehenden Service auswählen oder UUID einfügen.",
"coolify_service_uuid_placeholder": "Coolify service UUID",
"health_url": "Health URL",
"health_url_message": "Example: https://api-canary.example.test/ping",
"health_url_placeholder": "Health URL",
"ssl_domain": "Load-Balancer-Domain",
"ssl_domain_message": "Muss eine DNS-Domain sein, die zum Coolify Load Balancer geroutet wird. Beispiel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load-Balancer-Domain",
"https_domain_for_coolify": "Domain unter Kontrolle des Load Balancers",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto-Deploy",
"auto_deploy_tooltip": "Automatisch Deployments erstellen, wenn sich dieses Ziel ändert.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Coolify anweisen, HTTPS für die konfigurierte Domain bereitzustellen.",
"deploy_now": "Jetzt deployen",
"deploy_now_tooltip": "Deployment direkt nach dem Speichern des Ziels starten.",
"create_missing_service": "Fehlenden Service erstellen",
"create_missing_service_tooltip": "Release Manager erlauben, einen fehlenden Coolify-Service zu erstellen.",
"test_access_tooltip": "Prüfen, ob der konfigurierte GitHub-Token dieses Repository und den Branch lesen kann."
},
"title": "Release Manager",
"subtitle": "Schrittweise API- und Front-End-Releases",
"channel_names": {
@@ -2744,7 +2815,8 @@
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"use_local_frontend": "Lokales Frontend verwenden"
"use_local_frontend": "Lokales Frontend verwenden",
"switch_error": "Release channel could not be switched. The previous channel is still active."
},
"common": {
"yes": "ja",
@@ -2768,7 +2840,8 @@
"save_target": "Ziel speichern",
"delete": "Löschen",
"back": "Zurück",
"next": "Weiter"
"next": "Weiter",
"publish_release": "Publish release"
},
"tables": {
"channel": "Kanal",
@@ -2978,58 +3051,20 @@
"any_severity": "Beliebiger Schweregrad",
"no_timeline_events": "Keine Timeline-Events entsprechen den aktuellen Filtern."
},
"integrations": {
"title": "GitHub und Coolify",
"subtitle": "GitHub-Repositories und Branches kanalspezifischen Coolify-Services zuordnen.",
"choose_repository": "Repository auswählen",
"add_health_url": "Health-URL hinzufügen",
"github_token_not_configured": "GitHub-Token ist nicht konfiguriert; private Repositories können nicht aufgelistet werden.",
"open_github_token_settings": "GitHub-Token-Einstellungen öffnen",
"channel": "Channel",
"app": "App",
"repository": "Repository",
"repository_message": "Example: truckwash/backend-php",
"repository_placeholder": "owner/repo",
"branch": "Branch",
"branch_message": "Example: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Neueste: {commit}",
"coolify_instance": "Coolify-Instanz",
"coolify_instance_message": "Leer lassen für manuelle Deployment-Einträge.",
"no_coolify_instance": "Keine Coolify-Instanz",
"coolify_instance_placeholder": "Coolify-Instanz-ID",
"coolify_service_uuid": "Coolify service UUID",
"coolify_service_uuid_message": "Bestehenden Service auswählen oder UUID einfügen.",
"coolify_service_uuid_placeholder": "Coolify service UUID",
"health_url": "Health URL",
"health_url_message": "Example: https://api-canary.example.test/ping",
"health_url_placeholder": "Health URL",
"ssl_domain": "Load-Balancer-Domain",
"ssl_domain_message": "Muss eine DNS-Domain sein, die zum Coolify Load Balancer geroutet wird. Beispiel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load-Balancer-Domain",
"https_domain_for_coolify": "Domain unter Kontrolle des Load Balancers",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto-Deploy",
"auto_deploy_tooltip": "Automatisch Deployments erstellen, wenn sich dieses Ziel ändert.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Coolify anweisen, HTTPS für die konfigurierte Domain bereitzustellen.",
"deploy_now": "Jetzt deployen",
"deploy_now_tooltip": "Deployment direkt nach dem Speichern des Ziels starten.",
"create_missing_service": "Fehlenden Service erstellen",
"create_missing_service_tooltip": "Release Manager erlauben, einen fehlenden Coolify-Service zu erstellen.",
"test_access_tooltip": "Prüfen, ob der konfigurierte GitHub-Token dieses Repository und den Branch lesen kann."
"status": {
"confirm_issue_action": "Run this Release Manager action? It can change deployment state and will be audited.",
"choose_bundle_prompt": "Enter the bundle id to set for this channel.",
"impact": "Impact",
"cause": "Cause",
"automated_fix": "Automated fix",
"manual_fallback": "Manual fallback",
"related_deployment": "Related deployment",
"recent_result": "Recent result",
"no_automated_action": "No automated action available.",
"deployment": "Deployment",
"target": "Target",
"coolify_target": "Coolify target",
"service_set": "Service set"
}
}
},
+72 -54
View File
@@ -1433,7 +1433,25 @@
"view_lane_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.lane'} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}",
"view_lane_setup_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.lane'} @:{'words.generated.setup'} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}",
"view_order_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.order'} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}",
"view_vehicle_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.vehicle'} {reg} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}"
"view_vehicle_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.vehicle'} {reg} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}",
"accept_self_serve_wash": "Accept self-serve wash",
"accept_self_serve_wash_confirm": "Move this draft to customer #{customerNumber} with the self-serve details?",
"accept_self_serve_wash_success": "The self-serve wash was accepted.",
"email_notifications_section": "Email notifications",
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
"resend_booking_confirmation": "(Re)send booking confirmation",
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
"resend_booking_confirmation_success": "The booking confirmation has been sent",
"resend_wash_certificate": "(Re)send wash certificate",
"resend_wash_certificate_error": "The wash certificate could not be sent",
"resend_wash_certificate_success": "The wash certificate has been sent",
"self_serve_customer": "Customer",
"self_serve_driver": "Driver",
"self_serve_elapsed": "Elapsed",
"self_serve_wash_attachment": "Self-serve wash",
"self_serve_wash_attachment_for_customer": "Self-serve wash for customer #{customerNumber}"
},
"show_details": "@.capitalize:{'words.generated.show'} @:{'words.generated.details'}",
"show_order_details": "@.capitalize:{'words.generated.show'} @:{'words.generated.order'} @:{'words.generated.details'}",
@@ -2273,6 +2291,59 @@
"username_desc": "@.capitalize:{'words.replication.article.host_mention'} @:{'words.generated.username'} @:{'words.generated.is'} @:{'words.generated.used'} @:{'words.generated.to'} @:{'words.generated.authenticate'} @:{'words.replication.article.host_mention'} @:{'words.generated.api'} @:{'words.generated.connection'}."
},
"release_manager": {
"integrations": {
"title": "GitHub and Coolify",
"subtitle": "Map GitHub repositories and branches to channel-specific Coolify services.",
"choose_repository": "Choose repository",
"add_health_url": "add health URL",
"github_token_not_configured": "GitHub token is not configured; private repositories cannot be listed.",
"open_github_token_settings": "Open GitHub token settings",
"channel": "Channel",
"app": "App",
"repository": "Repository",
"repository_message": "Example: truckwash/backend-php",
"repository_placeholder": "owner/repo",
"branch": "Branch",
"branch_message": "Example: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Latest: {commit}",
"coolify_instance": "Coolify instance",
"coolify_instance_message": "Leave empty for manual deployment records.",
"no_coolify_instance": "No Coolify instance",
"coolify_instance_placeholder": "Coolify instance id",
"coolify_service_uuid": "Coolify service UUID",
"coolify_service_uuid_message": "Select an existing service or paste a UUID.",
"coolify_service_uuid_placeholder": "Coolify service UUID",
"health_url": "Health URL",
"health_url_message": "Example: https://api-canary.example.test/ping",
"health_url_placeholder": "Health URL",
"ssl_domain": "Load balancer domain",
"ssl_domain_message": "Must be a DNS domain routed to the Coolify load balancer. Example: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer domain",
"https_domain_for_coolify": "Domain controlled by the load balancer",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Automatically create deployments when this target changes.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Ask Coolify to provision HTTPS for the configured domain.",
"deploy_now": "Deploy now",
"deploy_now_tooltip": "Start a deployment immediately after saving the target.",
"create_missing_service": "Create missing service",
"create_missing_service_tooltip": "Allow Release Manager to create a missing Coolify service.",
"test_access_tooltip": "Verify the configured GitHub token can read this repository and branch."
},
"title": "Release Manager",
"subtitle": "Gradual API and Front-End releases",
"channel_names": {
@@ -2718,59 +2789,6 @@
"severity": "Severity",
"any_severity": "Any severity",
"no_timeline_events": "No timeline events match the current filters."
},
"integrations": {
"title": "GitHub and Coolify",
"subtitle": "Map GitHub repositories and branches to channel-specific Coolify services.",
"choose_repository": "Choose repository",
"add_health_url": "add health URL",
"github_token_not_configured": "GitHub token is not configured; private repositories cannot be listed.",
"open_github_token_settings": "Open GitHub token settings",
"channel": "Channel",
"app": "App",
"repository": "Repository",
"repository_message": "Example: truckwash/backend-php",
"repository_placeholder": "owner/repo",
"branch": "Branch",
"branch_message": "Example: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Latest: {commit}",
"coolify_instance": "Coolify instance",
"coolify_instance_message": "Leave empty for manual deployment records.",
"no_coolify_instance": "No Coolify instance",
"coolify_instance_placeholder": "Coolify instance id",
"coolify_service_uuid": "Coolify service UUID",
"coolify_service_uuid_message": "Select an existing service or paste a UUID.",
"coolify_service_uuid_placeholder": "Coolify service UUID",
"health_url": "Health URL",
"health_url_message": "Example: https://api-canary.example.test/ping",
"health_url_placeholder": "Health URL",
"ssl_domain": "Load balancer domain",
"ssl_domain_message": "Must be a DNS domain routed to the Coolify load balancer. Example: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer domain",
"https_domain_for_coolify": "Domain controlled by the load balancer",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Automatically create deployments when this target changes.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Ask Coolify to provision HTTPS for the configured domain.",
"deploy_now": "Deploy now",
"deploy_now_tooltip": "Start a deployment immediately after saving the target.",
"create_missing_service": "Create missing service",
"create_missing_service_tooltip": "Allow Release Manager to create a missing Coolify service.",
"test_access_tooltip": "Verify the configured GitHub token can read this repository and branch."
}
}
},
+88 -53
View File
@@ -549,7 +549,25 @@
"view_lane_new_tab": "@:{'templates.generated.compat.admin.pos.settings_wheel.view_lane_new_tab'}",
"view_lane_setup_new_tab": "@:{'templates.generated.compat.admin.pos.settings_wheel.view_lane_setup_new_tab'}",
"view_order_new_tab": "@:{'templates.generated.compat.admin.pos.settings_wheel.view_order_new_tab'}",
"view_vehicle_new_tab": "@:{'templates.generated.compat.admin.pos.settings_wheel.view_vehicle_new_tab'}"
"view_vehicle_new_tab": "@:{'templates.generated.compat.admin.pos.settings_wheel.view_vehicle_new_tab'}",
"accept_self_serve_wash": "@:{'templates.generated.compat.admin.pos.settings_wheel.accept_self_serve_wash'}",
"accept_self_serve_wash_confirm": "@:{'templates.generated.compat.admin.pos.settings_wheel.accept_self_serve_wash_confirm'}",
"accept_self_serve_wash_success": "@:{'templates.generated.compat.admin.pos.settings_wheel.accept_self_serve_wash_success'}",
"email_notifications_section": "@:{'templates.generated.compat.admin.pos.settings_wheel.email_notifications_section'}",
"resend_booking_completion_confirmation": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_booking_completion_confirmation'}",
"resend_booking_completion_confirmation_error": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_booking_completion_confirmation_error'}",
"resend_booking_completion_confirmation_success": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_booking_completion_confirmation_success'}",
"resend_booking_confirmation": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_booking_confirmation'}",
"resend_booking_confirmation_error": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_booking_confirmation_error'}",
"resend_booking_confirmation_success": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_booking_confirmation_success'}",
"resend_wash_certificate": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_wash_certificate'}",
"resend_wash_certificate_error": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_wash_certificate_error'}",
"resend_wash_certificate_success": "@:{'templates.generated.compat.admin.pos.settings_wheel.resend_wash_certificate_success'}",
"self_serve_customer": "@:{'templates.generated.compat.admin.pos.settings_wheel.self_serve_customer'}",
"self_serve_driver": "@:{'templates.generated.compat.admin.pos.settings_wheel.self_serve_driver'}",
"self_serve_elapsed": "@:{'templates.generated.compat.admin.pos.settings_wheel.self_serve_elapsed'}",
"self_serve_wash_attachment": "@:{'templates.generated.compat.admin.pos.settings_wheel.self_serve_wash_attachment'}",
"self_serve_wash_attachment_for_customer": "@:{'templates.generated.compat.admin.pos.settings_wheel.self_serve_wash_attachment_for_customer'}"
},
"show_details": "@:{'templates.generated.compat.admin.pos.show_details'}",
"show_order_details": "@:{'templates.generated.compat.admin.pos.show_order_details'}",
@@ -1498,6 +1516,59 @@
"username_desc": "@:{'templates.generated.compat.configuration.xlvask.username_desc'}"
},
"release_manager": {
"integrations": {
"title": "@:{'templates.generated.compat.configuration.release_manager.integrations.title'}",
"subtitle": "@:{'templates.generated.compat.configuration.release_manager.integrations.subtitle'}",
"choose_repository": "@:{'templates.generated.compat.configuration.release_manager.integrations.choose_repository'}",
"add_health_url": "@:{'templates.generated.compat.configuration.release_manager.integrations.add_health_url'}",
"github_token_not_configured": "@:{'templates.generated.compat.configuration.release_manager.integrations.github_token_not_configured'}",
"open_github_token_settings": "@:{'templates.generated.compat.configuration.release_manager.integrations.open_github_token_settings'}",
"channel": "@:{'templates.generated.compat.configuration.release_manager.integrations.channel'}",
"app": "@:{'templates.generated.compat.configuration.release_manager.integrations.app'}",
"repository": "@:{'templates.generated.compat.configuration.release_manager.integrations.repository'}",
"repository_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.repository_message'}",
"repository_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.repository_placeholder'}",
"branch": "@:{'templates.generated.compat.configuration.release_manager.integrations.branch'}",
"branch_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.branch_message'}",
"branch_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.branch_placeholder'}",
"latest_prefix": "@:{'templates.generated.compat.configuration.release_manager.integrations.latest_prefix'}",
"coolify_instance": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_instance'}",
"coolify_instance_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_instance_message'}",
"no_coolify_instance": "@:{'templates.generated.compat.configuration.release_manager.integrations.no_coolify_instance'}",
"coolify_instance_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_instance_placeholder'}",
"coolify_service_uuid": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_service_uuid'}",
"coolify_service_uuid_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_service_uuid_message'}",
"coolify_service_uuid_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_service_uuid_placeholder'}",
"health_url": "@:{'templates.generated.compat.configuration.release_manager.integrations.health_url'}",
"health_url_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.health_url_message'}",
"health_url_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.health_url_placeholder'}",
"ssl_domain": "@:{'templates.generated.compat.configuration.release_manager.integrations.ssl_domain'}",
"ssl_domain_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.ssl_domain_message'}",
"ssl_domain_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.ssl_domain_placeholder'}",
"https_domain_for_coolify": "@:{'templates.generated.compat.configuration.release_manager.integrations.https_domain_for_coolify'}",
"endpoint_mode": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode'}",
"endpoint_mode_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode_message'}",
"endpoint_mode_auto": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode_auto'}",
"endpoint_mode_manual": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode_manual'}",
"manual_endpoint_host": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_host'}",
"manual_endpoint_host_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_host_message'}",
"manual_endpoint_host_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_host_placeholder'}",
"manual_endpoint_port": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_port'}",
"manual_endpoint_port_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_port_message'}",
"app_port": "@:{'templates.generated.compat.configuration.release_manager.integrations.app_port'}",
"app_port_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.app_port_message'}",
"auto_gateway_endpoint": "@:{'templates.generated.compat.configuration.release_manager.integrations.auto_gateway_endpoint'}",
"pending_automatic_endpoint": "@:{'templates.generated.compat.configuration.release_manager.integrations.pending_automatic_endpoint'}",
"auto_deploy": "@:{'templates.generated.compat.configuration.release_manager.integrations.auto_deploy'}",
"auto_deploy_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.auto_deploy_tooltip'}",
"coolify_ssl": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_ssl'}",
"coolify_ssl_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_ssl_tooltip'}",
"deploy_now": "@:{'templates.generated.compat.configuration.release_manager.integrations.deploy_now'}",
"deploy_now_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.deploy_now_tooltip'}",
"create_missing_service": "@:{'templates.generated.compat.configuration.release_manager.integrations.create_missing_service'}",
"create_missing_service_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.create_missing_service_tooltip'}",
"test_access_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.test_access_tooltip'}"
},
"title": "@:{'templates.generated.compat.configuration.release_manager.title'}",
"subtitle": "@:{'templates.generated.compat.configuration.release_manager.subtitle'}",
"channel_names": {
@@ -1910,58 +1981,22 @@
"any_severity": "@:{'templates.generated.compat.configuration.release_manager.replay.any_severity'}",
"no_timeline_events": "@:{'templates.generated.compat.configuration.release_manager.replay.no_timeline_events'}"
},
"integrations": {
"title": "@:{'templates.generated.compat.configuration.release_manager.integrations.title'}",
"subtitle": "@:{'templates.generated.compat.configuration.release_manager.integrations.subtitle'}",
"choose_repository": "@:{'templates.generated.compat.configuration.release_manager.integrations.choose_repository'}",
"add_health_url": "@:{'templates.generated.compat.configuration.release_manager.integrations.add_health_url'}",
"github_token_not_configured": "@:{'templates.generated.compat.configuration.release_manager.integrations.github_token_not_configured'}",
"open_github_token_settings": "@:{'templates.generated.compat.configuration.release_manager.integrations.open_github_token_settings'}",
"channel": "@:{'templates.generated.compat.configuration.release_manager.integrations.channel'}",
"app": "@:{'templates.generated.compat.configuration.release_manager.integrations.app'}",
"repository": "@:{'templates.generated.compat.configuration.release_manager.integrations.repository'}",
"repository_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.repository_message'}",
"repository_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.repository_placeholder'}",
"branch": "@:{'templates.generated.compat.configuration.release_manager.integrations.branch'}",
"branch_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.branch_message'}",
"branch_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.branch_placeholder'}",
"latest_prefix": "@:{'templates.generated.compat.configuration.release_manager.integrations.latest_prefix'}",
"coolify_instance": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_instance'}",
"coolify_instance_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_instance_message'}",
"no_coolify_instance": "@:{'templates.generated.compat.configuration.release_manager.integrations.no_coolify_instance'}",
"coolify_instance_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_instance_placeholder'}",
"coolify_service_uuid": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_service_uuid'}",
"coolify_service_uuid_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_service_uuid_message'}",
"coolify_service_uuid_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_service_uuid_placeholder'}",
"health_url": "@:{'templates.generated.compat.configuration.release_manager.integrations.health_url'}",
"health_url_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.health_url_message'}",
"health_url_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.health_url_placeholder'}",
"ssl_domain": "@:{'templates.generated.compat.configuration.release_manager.integrations.ssl_domain'}",
"ssl_domain_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.ssl_domain_message'}",
"ssl_domain_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.ssl_domain_placeholder'}",
"https_domain_for_coolify": "@:{'templates.generated.compat.configuration.release_manager.integrations.https_domain_for_coolify'}",
"endpoint_mode": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode'}",
"endpoint_mode_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode_message'}",
"endpoint_mode_auto": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode_auto'}",
"endpoint_mode_manual": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode_manual'}",
"manual_endpoint_host": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_host'}",
"manual_endpoint_host_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_host_message'}",
"manual_endpoint_host_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_host_placeholder'}",
"manual_endpoint_port": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_port'}",
"manual_endpoint_port_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_port_message'}",
"app_port": "@:{'templates.generated.compat.configuration.release_manager.integrations.app_port'}",
"app_port_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.app_port_message'}",
"auto_gateway_endpoint": "@:{'templates.generated.compat.configuration.release_manager.integrations.auto_gateway_endpoint'}",
"pending_automatic_endpoint": "@:{'templates.generated.compat.configuration.release_manager.integrations.pending_automatic_endpoint'}",
"auto_deploy": "@:{'templates.generated.compat.configuration.release_manager.integrations.auto_deploy'}",
"auto_deploy_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.auto_deploy_tooltip'}",
"coolify_ssl": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_ssl'}",
"coolify_ssl_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_ssl_tooltip'}",
"deploy_now": "@:{'templates.generated.compat.configuration.release_manager.integrations.deploy_now'}",
"deploy_now_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.deploy_now_tooltip'}",
"create_missing_service": "@:{'templates.generated.compat.configuration.release_manager.integrations.create_missing_service'}",
"create_missing_service_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.create_missing_service_tooltip'}",
"test_access_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.test_access_tooltip'}"
"loading": {
"assignments": "@:{'templates.generated.compat.configuration.release_manager.loading.assignments'}",
"channels": "@:{'templates.generated.compat.configuration.release_manager.loading.channels'}",
"channels_detail": "@:{'templates.generated.compat.configuration.release_manager.loading.channels_detail'}",
"data_services": "@:{'templates.generated.compat.configuration.release_manager.loading.data_services'}",
"deployments": "@:{'templates.generated.compat.configuration.release_manager.loading.deployments'}",
"github_repositories": "@:{'templates.generated.compat.configuration.release_manager.loading.github_repositories'}",
"integrations": "@:{'templates.generated.compat.configuration.release_manager.loading.integrations'}",
"operations": "@:{'templates.generated.compat.configuration.release_manager.loading.operations'}",
"refreshing_summary": "@:{'templates.generated.compat.configuration.release_manager.loading.refreshing_summary'}",
"refreshing_timeline": "@:{'templates.generated.compat.configuration.release_manager.loading.refreshing_timeline'}",
"settings": "@:{'templates.generated.compat.configuration.release_manager.loading.settings'}",
"summary": "@:{'templates.generated.compat.configuration.release_manager.loading.summary'}",
"summary_detail": "@:{'templates.generated.compat.configuration.release_manager.loading.summary_detail'}",
"timeline_events": "@:{'templates.generated.compat.configuration.release_manager.loading.timeline_events'}",
"timeline_sessions": "@:{'templates.generated.compat.configuration.release_manager.loading.timeline_sessions'}"
}
}
},
+90 -55
View File
@@ -1710,7 +1710,25 @@
"view_lane_new_tab": "@.capitalize:{'words.generated.se'} kjørefelt @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
"view_lane_setup_new_tab": "@.capitalize:{'words.generated.se'} kjørefeltoppsett @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
"view_order_new_tab": "@.capitalize:{'words.generated.se'} @:{'words.generated.ordre'} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
"view_vehicle_new_tab": "@.capitalize:{'words.generated.se'} @:{'words.generated.kjøretøy'} {reg} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}"
"view_vehicle_new_tab": "@.capitalize:{'words.generated.se'} @:{'words.generated.kjøretøy'} {reg} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.fane'}",
"accept_self_serve_wash": "Godta selvbetjent vask",
"accept_self_serve_wash_confirm": "Flytt denne kladden til kunde #{customerNumber} med selvbetjeningsdetaljene?",
"accept_self_serve_wash_success": "Den selvbetjente vasken ble godtatt.",
"email_notifications_section": "E-postvarsler",
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
"resend_booking_confirmation": "(Re)send booking confirmation",
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
"resend_booking_confirmation_success": "The booking confirmation has been sent",
"resend_wash_certificate": "(Re)send wash certificate",
"resend_wash_certificate_error": "The wash certificate could not be sent",
"resend_wash_certificate_success": "The wash certificate has been sent",
"self_serve_customer": "Kunde",
"self_serve_driver": "Sjåfør",
"self_serve_elapsed": "Tid",
"self_serve_wash_attachment": "Selvbetjent vask",
"self_serve_wash_attachment_for_customer": "Selvbetjent vask for kunde #{customerNumber}"
},
"show_details": "@:{'words.generated.vis'} @:{'words.generated.detaljer'}",
"show_order_details": "@:{'words.generated.vis'} @:{'words.generated.bestillingsdetaljer'}",
@@ -2550,6 +2568,59 @@
"username_desc": "@:{'words.generated.brukernavnet'} @:{'words.generated.brukes'} @:{'words.generated.til'} @:{'words.generated.a'} @:{'words.generated.autentisere'} @:{'words.generated.api'}-@:{'words.generated.tilkoblingen'}."
},
"release_manager": {
"integrations": {
"title": "GitHub og Coolify",
"subtitle": "Knyt GitHub-repositories og branches til kanalspecifikke Coolify-services.",
"choose_repository": "Velg repository",
"add_health_url": "legg til health-URL",
"github_token_not_configured": "GitHub-token er ikke konfigurert; private repositories kan ikke vises.",
"open_github_token_settings": "Åpne GitHub token-innstillinger",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/backend-php",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Seneste: {commit}",
"coolify_instance": "Coolify-instans",
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
"no_coolify_instance": "Ingen Coolify-instans",
"coolify_instance_placeholder": "Coolify-instans-id",
"coolify_service_uuid": "Coolify service-UUID",
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
"coolify_service_uuid_placeholder": "Coolify service-UUID",
"health_url": "Health-URL",
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
"health_url_placeholder": "Health-URL",
"ssl_domain": "Load balancer-domene",
"ssl_domain_message": "Må være et DNS-domene som routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer-domene",
"https_domain_for_coolify": "Domene kontrollert av load balanceren",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
"deploy_now": "Deploy nå",
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
"create_missing_service": "Opprett manglende service",
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
},
"title": "Release Manager",
"subtitle": "Gradvise API- og Front-End-utgivelser",
"channel_names": {
@@ -2745,7 +2816,8 @@
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"use_local_frontend": "Bruk lokal frontend"
"use_local_frontend": "Bruk lokal frontend",
"switch_error": "Release channel could not be switched. The previous channel is still active."
},
"common": {
"yes": "ja",
@@ -2769,7 +2841,8 @@
"save_target": "Lagre mål",
"delete": "Slett",
"back": "Tilbake",
"next": "Neste"
"next": "Neste",
"publish_release": "Publish release"
},
"tables": {
"channel": "Kanal",
@@ -2979,58 +3052,20 @@
"any_severity": "Alle alvorligheder",
"no_timeline_events": "Ingen timeline-events matcher de aktuelle filtre."
},
"integrations": {
"title": "GitHub og Coolify",
"subtitle": "Knyt GitHub-repositories og branches til kanalspecifikke Coolify-services.",
"choose_repository": "Velg repository",
"add_health_url": "legg til health-URL",
"github_token_not_configured": "GitHub-token er ikke konfigurert; private repositories kan ikke vises.",
"open_github_token_settings": "Åpne GitHub token-innstillinger",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/backend-php",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Seneste: {commit}",
"coolify_instance": "Coolify-instans",
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
"no_coolify_instance": "Ingen Coolify-instans",
"coolify_instance_placeholder": "Coolify-instans-id",
"coolify_service_uuid": "Coolify service-UUID",
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
"coolify_service_uuid_placeholder": "Coolify service-UUID",
"health_url": "Health-URL",
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
"health_url_placeholder": "Health-URL",
"ssl_domain": "Load balancer-domene",
"ssl_domain_message": "Må være et DNS-domene som routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer-domene",
"https_domain_for_coolify": "Domene kontrollert av load balanceren",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
"deploy_now": "Deploy nå",
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
"create_missing_service": "Opprett manglende service",
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
"status": {
"confirm_issue_action": "Run this Release Manager action? It can change deployment state and will be audited.",
"choose_bundle_prompt": "Enter the bundle id to set for this channel.",
"impact": "Impact",
"cause": "Cause",
"automated_fix": "Automated fix",
"manual_fallback": "Manual fallback",
"related_deployment": "Related deployment",
"recent_result": "Recent result",
"no_automated_action": "No automated action available.",
"deployment": "Deployment",
"target": "Target",
"coolify_target": "Coolify target",
"service_set": "Service set"
}
}
},
+90 -55
View File
@@ -1760,7 +1760,25 @@
"view_lane_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.lane'} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}",
"view_lane_setup_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.lane'} @:{'words.generated.setup'} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}",
"view_order_new_tab": "@.capitalize:{'words.generated.view'} @:{'words.generated.order'} @:{'words.generated.in'} @:{'words.generated.new'} @:{'words.generated.tab'}",
"view_vehicle_new_tab": "@.capitalize:{'words.generated.visa'} @:{'words.generated.fordon'} {reg} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.flik'}"
"view_vehicle_new_tab": "@.capitalize:{'words.generated.visa'} @:{'words.generated.fordon'} {reg} @:{'words.generated.i'} @:{'words.generated.ny'} @:{'words.generated.flik'}",
"accept_self_serve_wash": "Godkänn självbetjäningstvätt",
"accept_self_serve_wash_confirm": "Flytta detta utkast till kund #{customerNumber} med självbetjäningsdetaljerna?",
"accept_self_serve_wash_success": "Självbetjäningstvätten godkändes.",
"email_notifications_section": "E-postaviseringar",
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
"resend_booking_confirmation": "(Re)send booking confirmation",
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
"resend_booking_confirmation_success": "The booking confirmation has been sent",
"resend_wash_certificate": "(Re)send wash certificate",
"resend_wash_certificate_error": "The wash certificate could not be sent",
"resend_wash_certificate_success": "The wash certificate has been sent",
"self_serve_customer": "Kund",
"self_serve_driver": "Förare",
"self_serve_elapsed": "Tid",
"self_serve_wash_attachment": "Självbetjäningstvätt",
"self_serve_wash_attachment_for_customer": "Självbetjäningstvätt för kund #{customerNumber}"
},
"show_details": "@:{'words.generated.show'} @:{'words.generated.details'}",
"show_order_details": "@:{'words.generated.show'} @:{'words.generated.order'} @:{'words.generated.details'}",
@@ -2600,6 +2618,59 @@
"username_desc": "Användarnamnet @:{'words.generated.anvands'} @:{'words.generated.for'} @:{'words.generated.att'} @:{'words.generated.autentisera'} @:{'words.generated.api'}-@:{'words.generated.anslutningen'}."
},
"release_manager": {
"integrations": {
"title": "GitHub och Coolify",
"subtitle": "Koppla GitHub-repositories och branches till kanalspecifika Coolify-tjänster.",
"choose_repository": "Välj repository",
"add_health_url": "lägg till health-URL",
"github_token_not_configured": "GitHub-token er ikke konfigureret; private repositories kan ikke vises.",
"open_github_token_settings": "Öppna GitHub token-inställningar",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/backend-php",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Seneste: {commit}",
"coolify_instance": "Coolify-instans",
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
"no_coolify_instance": "Ingen Coolify-instans",
"coolify_instance_placeholder": "Coolify-instans-id",
"coolify_service_uuid": "Coolify service-UUID",
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
"coolify_service_uuid_placeholder": "Coolify service-UUID",
"health_url": "Health-URL",
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
"health_url_placeholder": "Health-URL",
"ssl_domain": "Load balancer-domän",
"ssl_domain_message": "Måste vara en DNS-domän som routas till Coolify load balancern. Exempel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer-domän",
"https_domain_for_coolify": "Domän som kontrolleras av load balancern",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
"deploy_now": "Deploy nu",
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
"create_missing_service": "Skapa saknad tjänst",
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
},
"title": "Release Manager",
"subtitle": "Gradvisa API- och Front-End-versioner",
"channel_names": {
@@ -2795,7 +2866,8 @@
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"use_local_frontend": "Använd lokal frontend"
"use_local_frontend": "Använd lokal frontend",
"switch_error": "Release channel could not be switched. The previous channel is still active."
},
"common": {
"yes": "ja",
@@ -2819,7 +2891,8 @@
"save_target": "Spara mål",
"delete": "Radera",
"back": "Tillbaka",
"next": "Nästa"
"next": "Nästa",
"publish_release": "Publish release"
},
"tables": {
"channel": "Kanal",
@@ -3029,58 +3102,20 @@
"any_severity": "Alla allvarlighetsgrader",
"no_timeline_events": "Ingen timeline-events matcher de aktuelle filtre."
},
"integrations": {
"title": "GitHub och Coolify",
"subtitle": "Koppla GitHub-repositories och branches till kanalspecifika Coolify-tjänster.",
"choose_repository": "Välj repository",
"add_health_url": "lägg till health-URL",
"github_token_not_configured": "GitHub-token er ikke konfigureret; private repositories kan ikke vises.",
"open_github_token_settings": "Öppna GitHub token-inställningar",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/backend-php",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Seneste: {commit}",
"coolify_instance": "Coolify-instans",
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
"no_coolify_instance": "Ingen Coolify-instans",
"coolify_instance_placeholder": "Coolify-instans-id",
"coolify_service_uuid": "Coolify service-UUID",
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
"coolify_service_uuid_placeholder": "Coolify service-UUID",
"health_url": "Health-URL",
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
"health_url_placeholder": "Health-URL",
"ssl_domain": "Load balancer-domän",
"ssl_domain_message": "Måste vara en DNS-domän som routas till Coolify load balancern. Exempel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer-domän",
"https_domain_for_coolify": "Domän som kontrolleras av load balancern",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
"deploy_now": "Deploy nu",
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
"create_missing_service": "Skapa saknad tjänst",
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
"status": {
"confirm_issue_action": "Run this Release Manager action? It can change deployment state and will be audited.",
"choose_bundle_prompt": "Enter the bundle id to set for this channel.",
"impact": "Impact",
"cause": "Cause",
"automated_fix": "Automated fix",
"manual_fallback": "Manual fallback",
"related_deployment": "Related deployment",
"recent_result": "Recent result",
"no_automated_action": "No automated action available.",
"deployment": "Deployment",
"target": "Target",
"coolify_target": "Coolify target",
"service_set": "Service set"
}
}
},
@@ -0,0 +1,28 @@
{
"compat": {
"admin": {
"pos": {
"settings_wheel": {
"accept_self_serve_wash": "Godkend selvbetjent vask",
"accept_self_serve_wash_confirm": "Flyt denne kladde til kunde #{customerNumber} med selvbetjeningsdetaljerne?",
"accept_self_serve_wash_success": "Den selvbetjente vask blev godkendt.",
"email_notifications_section": "E-mail notifikationer",
"resend_booking_completion_confirmation": "(Gen)send bekræftelse for færdig booking",
"resend_booking_completion_confirmation_error": "Bekræftelsen for færdig booking kunne ikke sendes",
"resend_booking_completion_confirmation_success": "Bekræftelsen for færdig booking er sendt",
"resend_booking_confirmation": "(Gen)send bookingbekræftelse",
"resend_booking_confirmation_error": "Bookingbekræftelsen kunne ikke sendes",
"resend_booking_confirmation_success": "Bookingbekræftelsen er sendt",
"resend_wash_certificate": "(Gen)send vaskecertifikat",
"resend_wash_certificate_error": "Vaskecertifikatet kunne ikke sendes",
"resend_wash_certificate_success": "Vaskecertifikatet er sendt",
"self_serve_customer": "Kunde",
"self_serve_driver": "Chauffør",
"self_serve_elapsed": "Tid",
"self_serve_wash_attachment": "Selvbetjent vask",
"self_serve_wash_attachment_for_customer": "Selvbetjent vask for kunde #{customerNumber}"
}
}
}
}
}
@@ -298,507 +298,6 @@
"subtitle": "@.capitalize:{'terms.glossary.konfiguration'} @:{'terms.glossary.af'} @:{'terms.glossary.xlvask'} @:{'terms.glossary.integrationen'}",
"title": "@:{'terms.glossary.xlvask'} @:{'terms.glossary.konfiguration'}",
"username_desc": "@.capitalize:{'terms.glossary.brugernavnet'} @:{'terms.glossary.bruges'} @:{'terms.glossary.til'} @:{'terms.glossary.at'} @:{'terms.glossary.autentificere'} @:{'terms.glossary.api'} @:{'terms.glossary.forbindelsen'}."
},
"release_manager": {
"title": "Release Manager",
"subtitle": "Gradvise API- og Front-End-udgivelser",
"channel_names": {
"stable": "Stabil",
"canary": "Canary",
"internal": "Intern"
},
"channel_descriptions": {
"stable": "Standard produktionskanal.",
"canary": "Tidlig produktionsvalideringskanal.",
"internal": "Intern kanal til medarbejdere og superbruger-validering."
},
"control_api": {
"title": "Kontrol-API",
"tooltip": "Release Manager-handlinger sendes til denne API. Brug produktions-API'en, medmindre du tester en staging-backend.",
"example": "Eksempel: https://api.truckwash.io:4433",
"endpoint_label": "API-endpoint",
"endpoint_message": "Skift kun dette, når Release Manager-endpoints er tilgængelige på mål-API'en.",
"endpoint_aria": "Release Manager kontrol-API URL",
"use_tooltip": "Indlæs release-data fra denne API",
"use": "Brug",
"reset_tooltip": "Vend tilbage til standard kontrol-API",
"reset": "Nulstil",
"known_endpoints": "Kendte kontrol-API-endpoints"
},
"tabs": {
"overview": {
"label": "Oversigt",
"description": "Kanalstatus, opsætningsfremdrift og seneste release-tilstand."
},
"channels": {
"label": "Kanaler",
"description": "Opret stabile, canary- og målrettede kanaler med rollout-grænser."
},
"assignments": {
"label": "Tildelinger",
"description": "Fastgør brugere, subbrugere eller kunder til en bestemt release-kanal."
},
"deployments": {
"label": "Udrulninger",
"description": "Start udrulninger og promover vellykkede builds."
},
"replay": {
"label": "Replay",
"description": "Aktivér målrettet opsamling og søg i release-tidslinjehændelser."
},
"integrations": {
"label": "Integrationer",
"description": "Forbind GitHub-repositories, branches og Coolify-services."
},
"settings": {
"label": "Indstillinger",
"description": "Konfigurer Release Managers GitHub-adgang og webhook-indstillinger."
}
},
"loading": {
"summary": "Indl\u00e6ser Release Manager-data...",
"refreshing_summary": "Opdaterer Release Manager-data...",
"summary_detail": "Release-status, kanaler, deployments og service-health indl\u00e6ses.",
"channels": "Indl\u00e6ser release-kanaler...",
"channels_detail": "Kanalbaner og release-afh\u00e6ngigheder indl\u00e6ses.",
"assignments": "Indl\u00e6ser release-tildelinger...",
"deployments": "Indl\u00e6ser release-deployments...",
"operations": "Indl\u00e6ser release-operationer...",
"data_services": "Indl\u00e6ser release-datatjenester...",
"integrations": "Indl\u00e6ser release-integrationer...",
"settings": "Indl\u00e6ser Release Manager-indstillinger...",
"github_repositories": "Indl\u00e6ser GitHub-repositories...",
"timeline_sessions": "Indl\u00e6ser release-tidslinjesessioner...",
"timeline_events": "Indl\u00e6ser release-tidslinjeh\u00e6ndelser...",
"refreshing_timeline": "Opdaterer release-tidslinje..."
},
"setup": {
"channels": "Kanaler",
"targets": "GitHub- og Coolify-mål",
"assignments": "Pilot-tildelinger",
"deployments": "Første udrulning",
"replay": "Replay-opsamling"
},
"stats": {
"channels": "Kanaler",
"targets": "Mål",
"deployments": "Udrulninger",
"timeline_events": "Tidslinjehændelser"
},
"overview": {
"title": "Oversigt",
"subtitle": "Aktuel kanalstatus, release-aktivitet og modulstatus.",
"guided_setup": "Guidet opsætning",
"next_step": "Næste: {step}",
"ready": "Release Manager er klar til daglig drift.",
"add_suggested_channel": "Tilføj foreslået kanal",
"module_health_empty": "Modul-health snapshots vises, når probes er blevet registreret."
},
"status": {
"confirm_issue_action": "Koer denne Release Manager-handling? Den kan aendre deployment-tilstand og bliver auditeret.",
"choose_bundle_prompt": "Indtast bundle-id'et, der skal saettes for denne kanal.",
"impact": "Konsekvens",
"cause": "Aarsag",
"automated_fix": "Automatisk rettelse",
"manual_fallback": "Manuel fallback",
"related_deployment": "Relateret deployment",
"recent_result": "Seneste resultat",
"no_automated_action": "Ingen automatisk handling tilgængelig.",
"deployment": "Deployment",
"target": "Maal",
"coolify_target": "Coolify-maal",
"service_set": "Service set"
},
"settings": {
"title": "Release Manager-indstillinger",
"subtitle": "Konfigurer GitHub-tokenet, der bruges til private repositories og branch-opslag.",
"github_token": "GitHub-token",
"github_api_url": "GitHub API-URL",
"webhook_secret": "Webhook-hemmelighed",
"github_webhook_secret": "GitHub webhook-hemmelighed",
"configured": "Konfigureret",
"not_configured": "Ikke konfigureret",
"loaded_from": "Indlæst fra {variable}",
"set_below": "Angiv {variable} nedenfor",
"private_repositories_prefix": "Private repositories læses med serverens miljøvariabel",
"private_repositories_or": "eller modulets konfigurationsvariabel",
"github_token_message": "Eksempel: github_pat_... med adgang til de private repositories, Release Manager udruller.",
"github_token_placeholder": "Lad feltet være tomt for at beholde det eksisterende token",
"github_api_url_message": "Konfigureret i ReleaseManager.github_api_url. Brug https://api.github.com medmindre GitHub Enterprise bruges.",
"webhook_secret_message": "Valgfrit; lad feltet være tomt for at beholde den eksisterende hemmelighed.",
"webhook_secret_placeholder": "Webhook HMAC-hemmelighed",
"save": "Gem indstillinger",
"back_to_integrations": "Tilbage til integrationer",
"guide": {
"steps": {
"github_token": "GitHub-token",
"github_api_url": "GitHub API-URL",
"github_webhook_secret": "GitHub webhook secret",
"review": "Gennemgang"
},
"github_token_title": "Tilføj GitHub-token",
"github_token_description": "Release Manager bruger denne token til at vise private repositories, branches og commits, før et deployment-mål gemmes eller testes.",
"configure_this_in": "Konfigurer dette i",
"server_environment_variable": "Serverens miljøvariabel",
"takes_precedence": "har forrang, når den er sat.",
"recommended_access": "Anbefalet adgang",
"recommended_access_description": "Fine-grained GitHub-token med Contents: read-adgang til hvert release-repository.",
"github_api_url_title": "Tilføj GitHub API-URL",
"github_api_url_description": "Brug GitHub Cloud som standard, eller peg på REST API-base-URLen for en GitHub Enterprise Server-installation.",
"github_api_url_invalid": "Indtast en fuld HTTP- eller HTTPS-URL.",
"github_cloud": "GitHub Cloud",
"enterprise_example": "Enterprise-eksempel",
"github_webhook_secret_title": "Tilføj GitHub webhook secret",
"github_webhook_secret_description": "Brug samme secret i GitHub repository-webhooks, så Release Manager kan verificere deployment-events, før de håndteres.",
"github_webhook_url": "GitHub webhook-URL:",
"store_shared_secret_in": "Gem den delte secret i",
"review_title": "Gennemgå GitHub-opsætning",
"review_description": "Gem værdierne, og brug derefter Integrationer til at vælge et privat repository, branch og specifik commit eller seneste commit før adgang testes.",
"ready": "Klar",
"needs_value": "Mangler værdi"
}
},
"channel_unavailable": {
"kicker": "Release Manager",
"title": "Release-kanalen er ikke klar",
"summary_prefix": "Din konto er tildelt",
"summary_suffix": ", men kanalen mangler påkrævet release-konfiguration.",
"release_bundle": "Release-bundle",
"frontend_version": "Frontend-version",
"api_version": "API-version",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-entry",
"release_runtime": "Release-runtime",
"checking_again": "Tjekker igen om {seconds}s",
"refresh_error": "Release-status kunne ikke opdateres. Det næste automatiske tjek prøver igen.",
"ignore": "Ignorer de næste 5 minutter",
"check_again": "Tjek igen",
"logout": "Log ud",
"base_image": "Basis-image"
},
"channel_switched": {
"kicker": "Release Manager",
"title": "Du er nu på {channel}",
"summary_prefix": "Din konto er blevet tildelt release-kanalen",
"summary_suffix": "Denne enhed husker, at du har set denne besked.",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"frontend_version": "Frontend-version",
"api_version": "API-version",
"assigned_channel": "Tildelt kanal",
"current_app_image": "Nuværende app-image",
"current_api": "Nuværende API",
"base_image": "Basis-image",
"continue": "Fortsæt"
},
"channel_selector": {
"title": "Release-kanal",
"subtitle": "Vælg hvilken tildelt release-kanal denne enhed skal bruge.",
"sidebar_title": "Release",
"default": "Standard",
"ready": "Klar",
"unavailable": "Ikke klar",
"release_bundle": "Release-bundle",
"frontend_version": "Frontend-version",
"api_version": "API-version",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-entry",
"release_runtime": "Release-runtime",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"use_local_frontend": "Brug lokal frontend",
"switch_error": "Release-kanalen kunne ikke skiftes. Den forrige kanal er stadig aktiv."
},
"common": {
"yes": "ja",
"no": "nej",
"off": "fra"
},
"actions": {
"edit": "Rediger",
"rollback": "Rul tilbage",
"update": "Opdater",
"create": "Opret",
"publish_release": "Publicer release",
"clear": "Ryd",
"assign": "Tildel",
"remove": "Fjern",
"test_access": "Test adgang",
"deploy": "Deploy",
"redeploy": "Redeploy",
"promote": "Promover",
"enable": "Aktiver",
"search": "Søg",
"save_target": "Gem mål",
"delete": "Slet",
"back": "Tilbage",
"next": "Næste"
},
"tables": {
"channel": "Kanal",
"rollout": "Udrulning",
"frontend": "Frontend",
"api": "API",
"replay": "Replay",
"actions": "Handlinger",
"module": "Modul",
"status": "Status",
"checked": "Kontrolleret",
"subject": "Emne",
"reason": "Årsag",
"expires": "Udløber",
"deployment": "Deployment",
"source": "Kilde",
"started": "Startet",
"target": "Mål",
"coolify": "Coolify",
"auto": "Auto"
},
"values": {
"app": {
"frontend": "frontend",
"api": "api"
},
"capture_level": {
"metadata": "metadata",
"full_redacted": "fuldt redigeret"
},
"subject_type": {
"user": "bruger",
"subuser": "medarbejder",
"customer": "kunde",
"channel": "kanal"
},
"severity": {
"error": "fejl",
"warning": "advarsel",
"info": "info"
},
"status": {
"ok": "ok",
"active": "aktiv",
"deployed": "deployet",
"stable": "stabil",
"enabled": "aktiveret",
"queued": "i kø",
"deploying": "deploying",
"metadata": "metadata",
"not_configured": "ikke konfigureret",
"warning": "advarsel",
"failed": "fejlet",
"down": "nede",
"error": "fejl",
"unknown": "ukendt"
}
},
"autocomplete": {
"suggested_value": "Foreslået værdi",
"suggestion": "Forslag",
"use_typed_value": "Brug indtastet værdi",
"custom": "Tilpasset",
"api_url": "API-URL",
"frontend_url": "Frontend-URL",
"known_release_url": "Kendt release-URL",
"deploy_here_with_coolify": "Deploy {app} her med Coolify",
"prepare_coolify_target": "Forbered et Coolify-mål med HTTPS/SSL aktiveret",
"coolify_ssl": "Coolify SSL",
"github_repository": "GitHub-repository",
"owner_repository": "ejer/repository",
"git_branch": "Git-branch",
"deployment_branch": "Deployment-branch",
"github_commit": "GitHub-commit",
"git_commit": "Git-commit",
"use_specific_commit_sha": "Brug specifik commit-SHA",
"custom_commit": "Tilpasset commit",
"health_url": "Health-URL",
"release_health_check": "Release health check",
"coolify_service": "Coolify-service",
"service_uuid": "Service-UUID",
"known_coolify_service_uuid": "Kendt Coolify service-UUID",
"control_api": "Kontrol-API",
"release_manager_api_endpoint": "Release Manager API-endpoint",
"module": "Modul",
"timeline_module_key": "Timeline-modulnøgle"
},
"errors": {
"github_repositories": "Kunne ikke indlæse GitHub-repositories.",
"github_branches": "Kunne ikke indlæse GitHub-branches.",
"repository_branch_required": "Repository og branch er påkrævet før GitHub-adgang kan testes.",
"github_access_failed": "Test af GitHub repository-adgang fejlede."
},
"channels": {
"title": "Kanaler",
"subtitle": "Opret og juster gradvise release-kanaler.",
"rollout_percent": "{percent}% udrulning",
"already_configured": "Allerede konfigureret",
"slug": "Slug",
"slug_message": "Eksempel: canary",
"slug_placeholder": "slug",
"name": "Navn",
"name_message": "Vises til operatører, når de gennemgår en kanal.",
"name_placeholder": "Navn",
"rollout": "Udrulning",
"rollout_message": "0-100 procent af ikke-tildelt trafik.",
"frontend_url": "Frontend-URL",
"frontend_url_message": "Eksempel: https://canary.example.test",
"frontend_url_placeholder": "Frontend-URL",
"no_matching_release_url": "Ingen matchende release-URL.",
"api_url": "API-URL",
"api_url_message": "Eksempel: https://api-canary.example.test",
"api_url_placeholder": "API-URL",
"no_matching_api_url": "Ingen matchende API-URL.",
"replay_capture": "Replay capture",
"replay_capture_message": "Metadata er sikrere til brede kanaler.",
"retention_days": "Opbevaringsdage",
"retention_days_message": "1-365 dage.",
"enabled": "Aktiveret",
"default_channel": "Standardkanal",
"replay_enabled": "Replay aktiveret",
"frontend_chip": "Frontend: {url}",
"api_chip": "API: {url}"
},
"assignments": {
"title": "Tildelinger",
"subtitle": "Fastgør brugere, medarbejdere eller kunder til release-kanaler.",
"subject": "Emne",
"subject_message": "Søg efter brugere, medarbejdere eller kunder, eller skriv et manuelt emne som user:42.",
"subject_placeholder": "Søg eller skriv emne",
"subject_empty": "Ingen emner fundet",
"group_users": "Brugere",
"group_subusers": "Medarbejdere",
"group_customers": "Kunder",
"group_manual": "Manuel",
"manual_user": "Bruger #{id}",
"manual_subuser": "Medarbejder #{id}",
"manual_customer": "Kunde #{id}",
"manual_description": "Brug indtastet værdi {subject}",
"subject_type": "Emnetype",
"subject_type_message": "Vælg identitetstypen, der skal fastgøres.",
"customer": "Kunde",
"customer_message": "Søg på kundenummer eller navn, og vælg derefter den præcise kunde.",
"customer_placeholder": "Søg kunde",
"subject_id": "Emne-ID",
"subject_id_message": "Eksempel: bruger-id 42",
"subject_id_placeholder": "Emne-id",
"channel": "Kanal",
"channel_message": "Fastgjorte emner springer procentudrulningen over.",
"reason": "Årsag",
"reason_message": "Eksempel: canary-validering for kundesupport.",
"reason_placeholder": "Årsag",
"expires": "Udløber",
"expires_message": "Lad feltet være tomt for ingen automatisk udløb."
},
"deployments": {
"title": "Deployments",
"subtitle": "Start, promover og inspicer release-deployments.",
"manual_deployment_record": "Manuel deployment-post",
"target": "Mål",
"target_message": "Vælg et gemt mål, eller behold manuelle felter redigerbare.",
"select_target_or_manual": "Vælg mål eller manuel",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/front-end-vue",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: master",
"branch_placeholder": "branch",
"commit_selection": "Commit-valg",
"commit_selection_message": "Seneste slås op som branchens head med den konfigurerede GitHub-token.",
"latest_from_branch": "Seneste fra branch",
"specific_commit": "Specifik commit",
"specific_commit_message": "Eksempel: feedface1234",
"commit_sha_placeholder": "commit sha",
"version_label": "Versionslabel",
"version_label_message": "Eksempel: api-2026.05.19",
"version_label_placeholder": "versionslabel",
"commit_prefix": "Commit: {commit}",
"test_access_tooltip": "Bekræft token-adgang til det valgte repository, branch og commit."
},
"replay": {
"title": "Replay",
"subtitle": "Aktiver målrettet capture og gennemgå fejltimelines trin for trin.",
"capture_channel": "Capture {channel}",
"target_type": "Måltype",
"target": "Mål",
"target_message": "Eksempel: bruger-id 42 eller kanal-slug canary.",
"target_placeholder": "Mål-id eller kanal-slug",
"channel_scope": "Kanalafgrænsning",
"channel_scope_message": "Valgfrit; begrænser replay capture til en kanal.",
"no_channel_scope": "Ingen kanalafgrænsning",
"capture_level": "Capture-niveau",
"expires": "Udløber",
"expires_message": "Lad feltet være tomt for manuel deaktivering.",
"trace_id": "Trace-ID",
"trace_id_message": "Eksempel: trace-canary-1",
"trace_id_placeholder": "Trace-id",
"channel": "Kanal",
"channel_placeholder": "Kanal",
"principal_id": "Principal-ID",
"principal_id_placeholder": "Principal-id",
"module": "Modul",
"module_placeholder": "Modul",
"severity": "Alvorlighed",
"any_severity": "Alle alvorligheder",
"no_timeline_events": "Ingen timeline-events matcher de aktuelle filtre."
},
"integrations": {
"title": "GitHub og Coolify",
"subtitle": "Knyt GitHub-repositories og branches til kanalspecifikke Coolify-services.",
"choose_repository": "Vælg repository",
"add_health_url": "tilføj health-URL",
"github_token_not_configured": "GitHub-token er ikke konfigureret; private repositories kan ikke vises.",
"open_github_token_settings": "Åbn GitHub token-indstillinger",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/backend-php",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Seneste: {commit}",
"coolify_instance": "Coolify-instans",
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
"no_coolify_instance": "Ingen Coolify-instans",
"coolify_instance_placeholder": "Coolify-instans-id",
"coolify_service_uuid": "Coolify service-UUID",
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
"coolify_service_uuid_placeholder": "Coolify service-UUID",
"health_url": "Health-URL",
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
"health_url_placeholder": "Health-URL",
"ssl_domain": "Load balancer-domæne",
"ssl_domain_message": "Skal være et DNS-domæne, der routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer-domæne",
"https_domain_for_coolify": "Domæne kontrolleret af load balanceren",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
"deploy_now": "Deploy nu",
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
"create_missing_service": "Opret manglende service",
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
}
}
}
}
@@ -0,0 +1,61 @@
{
"compat": {
"configuration": {
"release_manager": {
"integrations": {
"title": "GitHub og Coolify",
"subtitle": "Knyt GitHub-repositories og branches til kanalspecifikke Coolify-services.",
"choose_repository": "Vælg repository",
"add_health_url": "tilføj health-URL",
"github_token_not_configured": "GitHub-token er ikke konfigureret; private repositories kan ikke vises.",
"open_github_token_settings": "Åbn GitHub token-indstillinger",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/backend-php",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Seneste: {commit}",
"coolify_instance": "Coolify-instans",
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
"no_coolify_instance": "Ingen Coolify-instans",
"coolify_instance_placeholder": "Coolify-instans-id",
"coolify_service_uuid": "Coolify service-UUID",
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
"coolify_service_uuid_placeholder": "Coolify service-UUID",
"health_url": "Health-URL",
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
"health_url_placeholder": "Health-URL",
"ssl_domain": "Load balancer-domæne",
"ssl_domain_message": "Skal være et DNS-domæne, der routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer-domæne",
"https_domain_for_coolify": "Domæne kontrolleret af load balanceren",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
"deploy_now": "Deploy nu",
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
"create_missing_service": "Opret manglende service",
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
}
}
}
}
}
@@ -0,0 +1,454 @@
{
"compat": {
"configuration": {
"release_manager": {
"title": "Release Manager",
"subtitle": "Gradvise API- og Front-End-udgivelser",
"channel_names": {
"stable": "Stabil",
"canary": "Canary",
"internal": "Intern"
},
"channel_descriptions": {
"stable": "Standard produktionskanal.",
"canary": "Tidlig produktionsvalideringskanal.",
"internal": "Intern kanal til medarbejdere og superbruger-validering."
},
"control_api": {
"title": "Kontrol-API",
"tooltip": "Release Manager-handlinger sendes til denne API. Brug produktions-API'en, medmindre du tester en staging-backend.",
"example": "Eksempel: https://api.truckwash.io:4433",
"endpoint_label": "API-endpoint",
"endpoint_message": "Skift kun dette, når Release Manager-endpoints er tilgængelige på mål-API'en.",
"endpoint_aria": "Release Manager kontrol-API URL",
"use_tooltip": "Indlæs release-data fra denne API",
"use": "Brug",
"reset_tooltip": "Vend tilbage til standard kontrol-API",
"reset": "Nulstil",
"known_endpoints": "Kendte kontrol-API-endpoints"
},
"tabs": {
"overview": {
"label": "Oversigt",
"description": "Kanalstatus, opsætningsfremdrift og seneste release-tilstand."
},
"channels": {
"label": "Kanaler",
"description": "Opret stabile, canary- og målrettede kanaler med rollout-grænser."
},
"assignments": {
"label": "Tildelinger",
"description": "Fastgør brugere, subbrugere eller kunder til en bestemt release-kanal."
},
"deployments": {
"label": "Udrulninger",
"description": "Start udrulninger og promover vellykkede builds."
},
"replay": {
"label": "Replay",
"description": "Aktivér målrettet opsamling og søg i release-tidslinjehændelser."
},
"integrations": {
"label": "Integrationer",
"description": "Forbind GitHub-repositories, branches og Coolify-services."
},
"settings": {
"label": "Indstillinger",
"description": "Konfigurer Release Managers GitHub-adgang og webhook-indstillinger."
}
},
"loading": {
"summary": "Indlæser Release Manager-data...",
"refreshing_summary": "Opdaterer Release Manager-data...",
"summary_detail": "Release-status, kanaler, deployments og service-health indlæses.",
"channels": "Indlæser release-kanaler...",
"channels_detail": "Kanalbaner og release-afhængigheder indlæses.",
"assignments": "Indlæser release-tildelinger...",
"deployments": "Indlæser release-deployments...",
"operations": "Indlæser release-operationer...",
"data_services": "Indlæser release-datatjenester...",
"integrations": "Indlæser release-integrationer...",
"settings": "Indlæser Release Manager-indstillinger...",
"github_repositories": "Indlæser GitHub-repositories...",
"timeline_sessions": "Indlæser release-tidslinjesessioner...",
"timeline_events": "Indlæser release-tidslinjehændelser...",
"refreshing_timeline": "Opdaterer release-tidslinje..."
},
"setup": {
"channels": "Kanaler",
"targets": "GitHub- og Coolify-mål",
"assignments": "Pilot-tildelinger",
"deployments": "Første udrulning",
"replay": "Replay-opsamling"
},
"stats": {
"channels": "Kanaler",
"targets": "Mål",
"deployments": "Udrulninger",
"timeline_events": "Tidslinjehændelser"
},
"overview": {
"title": "Oversigt",
"subtitle": "Aktuel kanalstatus, release-aktivitet og modulstatus.",
"guided_setup": "Guidet opsætning",
"next_step": "Næste: {step}",
"ready": "Release Manager er klar til daglig drift.",
"add_suggested_channel": "Tilføj foreslået kanal",
"module_health_empty": "Modul-health snapshots vises, når probes er blevet registreret."
},
"status": {
"confirm_issue_action": "Koer denne Release Manager-handling? Den kan aendre deployment-tilstand og bliver auditeret.",
"choose_bundle_prompt": "Indtast bundle-id'et, der skal saettes for denne kanal.",
"impact": "Konsekvens",
"cause": "Aarsag",
"automated_fix": "Automatisk rettelse",
"manual_fallback": "Manuel fallback",
"related_deployment": "Relateret deployment",
"recent_result": "Seneste resultat",
"no_automated_action": "Ingen automatisk handling tilgængelig.",
"deployment": "Deployment",
"target": "Maal",
"coolify_target": "Coolify-maal",
"service_set": "Service set"
},
"settings": {
"title": "Release Manager-indstillinger",
"subtitle": "Konfigurer GitHub-tokenet, der bruges til private repositories og branch-opslag.",
"github_token": "GitHub-token",
"github_api_url": "GitHub API-URL",
"webhook_secret": "Webhook-hemmelighed",
"github_webhook_secret": "GitHub webhook-hemmelighed",
"configured": "Konfigureret",
"not_configured": "Ikke konfigureret",
"loaded_from": "Indlæst fra {variable}",
"set_below": "Angiv {variable} nedenfor",
"private_repositories_prefix": "Private repositories læses med serverens miljøvariabel",
"private_repositories_or": "eller modulets konfigurationsvariabel",
"github_token_message": "Eksempel: github_pat_... med adgang til de private repositories, Release Manager udruller.",
"github_token_placeholder": "Lad feltet være tomt for at beholde det eksisterende token",
"github_api_url_message": "Konfigureret i ReleaseManager.github_api_url. Brug https://api.github.com medmindre GitHub Enterprise bruges.",
"webhook_secret_message": "Valgfrit; lad feltet være tomt for at beholde den eksisterende hemmelighed.",
"webhook_secret_placeholder": "Webhook HMAC-hemmelighed",
"save": "Gem indstillinger",
"back_to_integrations": "Tilbage til integrationer",
"guide": {
"steps": {
"github_token": "GitHub-token",
"github_api_url": "GitHub API-URL",
"github_webhook_secret": "GitHub webhook secret",
"review": "Gennemgang"
},
"github_token_title": "Tilføj GitHub-token",
"github_token_description": "Release Manager bruger denne token til at vise private repositories, branches og commits, før et deployment-mål gemmes eller testes.",
"configure_this_in": "Konfigurer dette i",
"server_environment_variable": "Serverens miljøvariabel",
"takes_precedence": "har forrang, når den er sat.",
"recommended_access": "Anbefalet adgang",
"recommended_access_description": "Fine-grained GitHub-token med Contents: read-adgang til hvert release-repository.",
"github_api_url_title": "Tilføj GitHub API-URL",
"github_api_url_description": "Brug GitHub Cloud som standard, eller peg på REST API-base-URLen for en GitHub Enterprise Server-installation.",
"github_api_url_invalid": "Indtast en fuld HTTP- eller HTTPS-URL.",
"github_cloud": "GitHub Cloud",
"enterprise_example": "Enterprise-eksempel",
"github_webhook_secret_title": "Tilføj GitHub webhook secret",
"github_webhook_secret_description": "Brug samme secret i GitHub repository-webhooks, så Release Manager kan verificere deployment-events, før de håndteres.",
"github_webhook_url": "GitHub webhook-URL:",
"store_shared_secret_in": "Gem den delte secret i",
"review_title": "Gennemgå GitHub-opsætning",
"review_description": "Gem værdierne, og brug derefter Integrationer til at vælge et privat repository, branch og specifik commit eller seneste commit før adgang testes.",
"ready": "Klar",
"needs_value": "Mangler værdi"
}
},
"channel_unavailable": {
"kicker": "Release Manager",
"title": "Release-kanalen er ikke klar",
"summary_prefix": "Din konto er tildelt",
"summary_suffix": ", men kanalen mangler påkrævet release-konfiguration.",
"release_bundle": "Release-bundle",
"frontend_version": "Frontend-version",
"api_version": "API-version",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-entry",
"release_runtime": "Release-runtime",
"checking_again": "Tjekker igen om {seconds}s",
"refresh_error": "Release-status kunne ikke opdateres. Det næste automatiske tjek prøver igen.",
"ignore": "Ignorer de næste 5 minutter",
"check_again": "Tjek igen",
"logout": "Log ud",
"base_image": "Basis-image"
},
"channel_switched": {
"kicker": "Release Manager",
"title": "Du er nu på {channel}",
"summary_prefix": "Din konto er blevet tildelt release-kanalen",
"summary_suffix": "Denne enhed husker, at du har set denne besked.",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"frontend_version": "Frontend-version",
"api_version": "API-version",
"assigned_channel": "Tildelt kanal",
"current_app_image": "Nuværende app-image",
"current_api": "Nuværende API",
"base_image": "Basis-image",
"continue": "Fortsæt"
},
"channel_selector": {
"title": "Release-kanal",
"subtitle": "Vælg hvilken tildelt release-kanal denne enhed skal bruge.",
"sidebar_title": "Release",
"default": "Standard",
"ready": "Klar",
"unavailable": "Ikke klar",
"release_bundle": "Release-bundle",
"frontend_version": "Frontend-version",
"api_version": "API-version",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-entry",
"release_runtime": "Release-runtime",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"use_local_frontend": "Brug lokal frontend",
"switch_error": "Release-kanalen kunne ikke skiftes. Den forrige kanal er stadig aktiv."
},
"common": {
"yes": "ja",
"no": "nej",
"off": "fra"
},
"actions": {
"edit": "Rediger",
"rollback": "Rul tilbage",
"update": "Opdater",
"create": "Opret",
"publish_release": "Publicer release",
"clear": "Ryd",
"assign": "Tildel",
"remove": "Fjern",
"test_access": "Test adgang",
"deploy": "Deploy",
"redeploy": "Redeploy",
"promote": "Promover",
"enable": "Aktiver",
"search": "Søg",
"save_target": "Gem mål",
"delete": "Slet",
"back": "Tilbage",
"next": "Næste"
},
"tables": {
"channel": "Kanal",
"rollout": "Udrulning",
"frontend": "Frontend",
"api": "API",
"replay": "Replay",
"actions": "Handlinger",
"module": "Modul",
"status": "Status",
"checked": "Kontrolleret",
"subject": "Emne",
"reason": "Årsag",
"expires": "Udløber",
"deployment": "Deployment",
"source": "Kilde",
"started": "Startet",
"target": "Mål",
"coolify": "Coolify",
"auto": "Auto"
},
"values": {
"app": {
"frontend": "frontend",
"api": "api"
},
"capture_level": {
"metadata": "metadata",
"full_redacted": "fuldt redigeret"
},
"subject_type": {
"user": "bruger",
"subuser": "medarbejder",
"customer": "kunde",
"channel": "kanal"
},
"severity": {
"error": "fejl",
"warning": "advarsel",
"info": "info"
},
"status": {
"ok": "ok",
"active": "aktiv",
"deployed": "deployet",
"stable": "stabil",
"enabled": "aktiveret",
"queued": "i kø",
"deploying": "deploying",
"metadata": "metadata",
"not_configured": "ikke konfigureret",
"warning": "advarsel",
"failed": "fejlet",
"down": "nede",
"error": "fejl",
"unknown": "ukendt"
}
},
"autocomplete": {
"suggested_value": "Foreslået værdi",
"suggestion": "Forslag",
"use_typed_value": "Brug indtastet værdi",
"custom": "Tilpasset",
"api_url": "API-URL",
"frontend_url": "Frontend-URL",
"known_release_url": "Kendt release-URL",
"deploy_here_with_coolify": "Deploy {app} her med Coolify",
"prepare_coolify_target": "Forbered et Coolify-mål med HTTPS/SSL aktiveret",
"coolify_ssl": "Coolify SSL",
"github_repository": "GitHub-repository",
"owner_repository": "ejer/repository",
"git_branch": "Git-branch",
"deployment_branch": "Deployment-branch",
"github_commit": "GitHub-commit",
"git_commit": "Git-commit",
"use_specific_commit_sha": "Brug specifik commit-SHA",
"custom_commit": "Tilpasset commit",
"health_url": "Health-URL",
"release_health_check": "Release health check",
"coolify_service": "Coolify-service",
"service_uuid": "Service-UUID",
"known_coolify_service_uuid": "Kendt Coolify service-UUID",
"control_api": "Kontrol-API",
"release_manager_api_endpoint": "Release Manager API-endpoint",
"module": "Modul",
"timeline_module_key": "Timeline-modulnøgle"
},
"errors": {
"github_repositories": "Kunne ikke indlæse GitHub-repositories.",
"github_branches": "Kunne ikke indlæse GitHub-branches.",
"repository_branch_required": "Repository og branch er påkrævet før GitHub-adgang kan testes.",
"github_access_failed": "Test af GitHub repository-adgang fejlede."
},
"channels": {
"title": "Kanaler",
"subtitle": "Opret og juster gradvise release-kanaler.",
"rollout_percent": "{percent}% udrulning",
"already_configured": "Allerede konfigureret",
"slug": "Slug",
"slug_message": "Eksempel: canary",
"slug_placeholder": "slug",
"name": "Navn",
"name_message": "Vises til operatører, når de gennemgår en kanal.",
"name_placeholder": "Navn",
"rollout": "Udrulning",
"rollout_message": "0-100 procent af ikke-tildelt trafik.",
"frontend_url": "Frontend-URL",
"frontend_url_message": "Eksempel: https://canary.example.test",
"frontend_url_placeholder": "Frontend-URL",
"no_matching_release_url": "Ingen matchende release-URL.",
"api_url": "API-URL",
"api_url_message": "Eksempel: https://api-canary.example.test",
"api_url_placeholder": "API-URL",
"no_matching_api_url": "Ingen matchende API-URL.",
"replay_capture": "Replay capture",
"replay_capture_message": "Metadata er sikrere til brede kanaler.",
"retention_days": "Opbevaringsdage",
"retention_days_message": "1-365 dage.",
"enabled": "Aktiveret",
"default_channel": "Standardkanal",
"replay_enabled": "Replay aktiveret",
"frontend_chip": "Frontend: {url}",
"api_chip": "API: {url}"
},
"assignments": {
"title": "Tildelinger",
"subtitle": "Fastgør brugere, medarbejdere eller kunder til release-kanaler.",
"subject": "Emne",
"subject_message": "Søg efter brugere, medarbejdere eller kunder, eller skriv et manuelt emne som user:42.",
"subject_placeholder": "Søg eller skriv emne",
"subject_empty": "Ingen emner fundet",
"group_users": "Brugere",
"group_subusers": "Medarbejdere",
"group_customers": "Kunder",
"group_manual": "Manuel",
"manual_user": "Bruger #{id}",
"manual_subuser": "Medarbejder #{id}",
"manual_customer": "Kunde #{id}",
"manual_description": "Brug indtastet værdi {subject}",
"subject_type": "Emnetype",
"subject_type_message": "Vælg identitetstypen, der skal fastgøres.",
"customer": "Kunde",
"customer_message": "Søg på kundenummer eller navn, og vælg derefter den præcise kunde.",
"customer_placeholder": "Søg kunde",
"subject_id": "Emne-ID",
"subject_id_message": "Eksempel: bruger-id 42",
"subject_id_placeholder": "Emne-id",
"channel": "Kanal",
"channel_message": "Fastgjorte emner springer procentudrulningen over.",
"reason": "Årsag",
"reason_message": "Eksempel: canary-validering for kundesupport.",
"reason_placeholder": "Årsag",
"expires": "Udløber",
"expires_message": "Lad feltet være tomt for ingen automatisk udløb."
},
"deployments": {
"title": "Deployments",
"subtitle": "Start, promover og inspicer release-deployments.",
"manual_deployment_record": "Manuel deployment-post",
"target": "Mål",
"target_message": "Vælg et gemt mål, eller behold manuelle felter redigerbare.",
"select_target_or_manual": "Vælg mål eller manuel",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/front-end-vue",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: master",
"branch_placeholder": "branch",
"commit_selection": "Commit-valg",
"commit_selection_message": "Seneste slås op som branchens head med den konfigurerede GitHub-token.",
"latest_from_branch": "Seneste fra branch",
"specific_commit": "Specifik commit",
"specific_commit_message": "Eksempel: feedface1234",
"commit_sha_placeholder": "commit sha",
"version_label": "Versionslabel",
"version_label_message": "Eksempel: api-2026.05.19",
"version_label_placeholder": "versionslabel",
"commit_prefix": "Commit: {commit}",
"test_access_tooltip": "Bekræft token-adgang til det valgte repository, branch og commit."
},
"replay": {
"title": "Replay",
"subtitle": "Aktiver målrettet capture og gennemgå fejltimelines trin for trin.",
"capture_channel": "Capture {channel}",
"target_type": "Måltype",
"target": "Mål",
"target_message": "Eksempel: bruger-id 42 eller kanal-slug canary.",
"target_placeholder": "Mål-id eller kanal-slug",
"channel_scope": "Kanalafgrænsning",
"channel_scope_message": "Valgfrit; begrænser replay capture til en kanal.",
"no_channel_scope": "Ingen kanalafgrænsning",
"capture_level": "Capture-niveau",
"expires": "Udløber",
"expires_message": "Lad feltet være tomt for manuel deaktivering.",
"trace_id": "Trace-ID",
"trace_id_message": "Eksempel: trace-canary-1",
"trace_id_placeholder": "Trace-id",
"channel": "Kanal",
"channel_placeholder": "Kanal",
"principal_id": "Principal-ID",
"principal_id_placeholder": "Principal-id",
"module": "Modul",
"module_placeholder": "Modul",
"severity": "Alvorlighed",
"any_severity": "Alle alvorligheder",
"no_timeline_events": "Ingen timeline-events matcher de aktuelle filtre."
}
}
}
}
}
+1 -1
View File
@@ -83,7 +83,7 @@
"api": "API",
"app": "app",
"appen": "appen",
"ar": "år",
"ar": "@:{'terms.glossary.years'}",
"are": "are",
"arsag": "årsag",
"at": "at",
+1 -1
View File
@@ -18,7 +18,7 @@
"nøgle": "nøgle",
"nogle": "nogle",
"nok": "nok",
"nøkkel": "nøgle",
"nøkkel": "@:{'terms.glossary.nøgle'}",
"nøkkelen": "nøglen",
"not": "not",
"note": "note",
@@ -0,0 +1,28 @@
{
"compat": {
"admin": {
"pos": {
"settings_wheel": {
"accept_self_serve_wash": "Self-Service-Wäsche akzeptieren",
"accept_self_serve_wash_confirm": "Diesen Entwurf mit den Self-Service-Details zu Kunde #{customerNumber} verschieben?",
"accept_self_serve_wash_success": "Die Self-Service-Wäsche wurde akzeptiert.",
"email_notifications_section": "E-Mail-Benachrichtigungen",
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
"resend_booking_confirmation": "(Re)send booking confirmation",
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
"resend_booking_confirmation_success": "The booking confirmation has been sent",
"resend_wash_certificate": "(Re)send wash certificate",
"resend_wash_certificate_error": "The wash certificate could not be sent",
"resend_wash_certificate_success": "The wash certificate has been sent",
"self_serve_customer": "Kunde",
"self_serve_driver": "Fahrer",
"self_serve_elapsed": "Zeit",
"self_serve_wash_attachment": "Self-Service-Wäsche",
"self_serve_wash_attachment_for_customer": "Self-Service-Wäsche für Kunde #{customerNumber}"
}
}
}
}
}
@@ -298,490 +298,6 @@
"subtitle": "@:{'terms.glossary.konfiguration'} @:{'terms.glossary.der'} @:{'terms.glossary.xlvask'}-@.capitalize:{'terms.glossary.integration'}",
"title": "@:{'terms.glossary.xlvask'}-@:{'terms.glossary.konfiguration'}",
"username_desc": "@.capitalize:{'terms.glossary.der'} @:{'terms.glossary.benutzername'} @:{'terms.glossary.wird'} @:{'terms.glossary.zur'} @:{'terms.glossary.authentifizierung'} @:{'terms.glossary.der'} @:{'terms.glossary.api'}-@:{'terms.glossary.verbindung'} @:{'terms.glossary.verwendet'}."
},
"release_manager": {
"title": "Release Manager",
"subtitle": "Schrittweise API- und Front-End-Releases",
"channel_names": {
"stable": "Stabil",
"canary": "Canary",
"internal": "Intern"
},
"channel_descriptions": {
"stable": "Standard-Produktionskanal.",
"canary": "Früher Produktionsvalidierungskanal.",
"internal": "Interner Kanal für Mitarbeitende und Superuser-Validierung."
},
"control_api": {
"title": "Kontroll-API",
"tooltip": "Release-Manager-Aktionen werden an diese API gesendet. Verwende die Produktions-Kontroll-API, außer du testest ein Staging-Backend.",
"example": "Beispiel: https://api.truckwash.io:4433",
"endpoint_label": "API-Endpunkt",
"endpoint_message": "Ändere dies nur, wenn Release-Manager-Endpunkte auf der Ziel-API verfügbar sind.",
"endpoint_aria": "Release Manager Kontroll-API-URL",
"use_tooltip": "Release-Daten von dieser API laden",
"use": "Verwenden",
"reset_tooltip": "Zur Standard-Kontroll-API zurückkehren",
"reset": "Zurücksetzen",
"known_endpoints": "Bekannte Kontroll-API-Endpunkte"
},
"tabs": {
"overview": {
"label": "Übersicht",
"description": "Kanalzustand, Einrichtungsfortschritt und aktueller Release-Status."
},
"channels": {
"label": "Kanäle",
"description": "Stabile, Canary- und Zielkanäle mit Rollout-Grenzen erstellen."
},
"assignments": {
"label": "Zuweisungen",
"description": "Benutzer, Unterbenutzer oder Kunden einem bestimmten Release-Kanal zuweisen."
},
"deployments": {
"label": "Bereitstellungen",
"description": "Bereitstellungen starten und erfolgreiche Builds promoten."
},
"replay": {
"label": "Replay",
"description": "Gezielte Erfassung aktivieren und Release-Timeline-Ereignisse durchsuchen."
},
"integrations": {
"label": "Integrationen",
"description": "GitHub-Repositories, Branches und Coolify-Dienste verbinden."
},
"settings": {
"label": "Einstellungen",
"description": "GitHub-Zugriff und Webhook-Einstellungen des Release Managers konfigurieren."
}
},
"loading": {
"summary": "Release Manager-Daten werden geladen...",
"refreshing_summary": "Release Manager-Daten werden aktualisiert...",
"summary_detail": "Release-Status, Kanale, Deployments und Servicezustand werden geladen.",
"channels": "Release-Kanale werden geladen...",
"channels_detail": "Kanalansichten und Release-Abhangigkeiten werden geladen.",
"assignments": "Release-Zuweisungen werden geladen...",
"deployments": "Release-Deployments werden geladen...",
"operations": "Release-Operationen werden geladen...",
"data_services": "Release-Datendienste werden geladen...",
"integrations": "Release-Integrationen werden geladen...",
"settings": "Release Manager-Einstellungen werden geladen...",
"github_repositories": "GitHub-Repositories werden geladen...",
"timeline_sessions": "Release-Timeline-Sessions werden geladen...",
"timeline_events": "Release-Timeline-Ereignisse werden geladen...",
"refreshing_timeline": "Release-Timeline wird aktualisiert..."
},
"setup": {
"channels": "Kanäle",
"targets": "GitHub- und Coolify-Ziele",
"assignments": "Pilot-Zuweisungen",
"deployments": "Erste Bereitstellung",
"replay": "Replay-Erfassung"
},
"stats": {
"channels": "Kanäle",
"targets": "Ziele",
"deployments": "Bereitstellungen",
"timeline_events": "Timeline-Ereignisse"
},
"overview": {
"title": "Übersicht",
"subtitle": "Aktueller Kanalzustand, Release-Aktivität und Modulstatus.",
"guided_setup": "Geführte Einrichtung",
"next_step": "Nächster Schritt: {step}",
"ready": "Release Manager ist für den täglichen Betrieb bereit.",
"add_suggested_channel": "Vorgeschlagenen Kanal hinzufügen",
"module_health_empty": "Modul-Health-Snapshots erscheinen, nachdem Probes aufgezeichnet wurden."
},
"settings": {
"title": "Release-Manager-Einstellungen",
"subtitle": "GitHub-Token für private Repositories und Branch-Abfragen konfigurieren.",
"github_token": "GitHub-Token",
"github_api_url": "GitHub-API-URL",
"webhook_secret": "Webhook-Secret",
"github_webhook_secret": "GitHub-Webhook-Secret",
"configured": "Konfiguriert",
"not_configured": "Nicht konfiguriert",
"loaded_from": "Geladen aus {variable}",
"set_below": "{variable} unten festlegen",
"private_repositories_prefix": "Private Repositories werden mit der Server-Umgebungsvariable gelesen",
"private_repositories_or": "oder der Modul-Konfigurationsvariable",
"github_token_message": "Beispiel: github_pat_... mit Zugriff auf die privaten Repositories, die Release Manager bereitstellt.",
"github_token_placeholder": "Leer lassen, um das vorhandene Token zu behalten",
"github_api_url_message": "Konfiguriert in ReleaseManager.github_api_url. https://api.github.com verwenden, sofern GitHub Enterprise nicht genutzt wird.",
"webhook_secret_message": "Optional; leer lassen, um das vorhandene Secret zu behalten.",
"webhook_secret_placeholder": "Webhook-HMAC-Secret",
"save": "Einstellungen speichern",
"back_to_integrations": "Zurück zu Integrationen",
"guide": {
"steps": {
"github_token": "GitHub-Token",
"github_api_url": "GitHub API-URL",
"github_webhook_secret": "GitHub Webhook Secret",
"review": "Prüfung"
},
"github_token_title": "GitHub-Token hinzufügen",
"github_token_description": "Release Manager verwendet diesen Token, um private Repositories, Branches und Commits aufzulisten, bevor ein Deployment-Ziel gespeichert oder getestet wird.",
"configure_this_in": "Dies konfigurieren in",
"server_environment_variable": "Die Server-Umgebungsvariable",
"takes_precedence": "hat Vorrang, wenn sie gesetzt ist.",
"recommended_access": "Empfohlener Zugriff",
"recommended_access_description": "Fine-grained GitHub-Token mit Contents: read-Zugriff für jedes Release-Repository.",
"github_api_url_title": "GitHub API-URL hinzufügen",
"github_api_url_description": "GitHub Cloud standardmäßig verwenden oder auf die REST-API-Basis-URL einer GitHub Enterprise Server-Installation zeigen.",
"github_api_url_invalid": "Vollständige HTTP- oder HTTPS-URL eingeben.",
"github_cloud": "GitHub Cloud",
"enterprise_example": "Enterprise-Beispiel",
"github_webhook_secret_title": "GitHub Webhook Secret hinzufügen",
"github_webhook_secret_description": "Dasselbe Secret in GitHub-Repository-Webhooks verwenden, damit Release Manager Deployment-Events vor der Verarbeitung verifizieren kann.",
"github_webhook_url": "GitHub Webhook-URL:",
"store_shared_secret_in": "Geteiltes Secret speichern in",
"review_title": "GitHub-Konfiguration prüfen",
"review_description": "Werte speichern und dann unter Integrationen ein privates Repository, einen Branch und einen bestimmten Commit oder den neuesten Commit auswählen, bevor der Zugriff getestet wird.",
"ready": "Bereit",
"needs_value": "Wert fehlt"
}
},
"channel_unavailable": {
"kicker": "Release Manager",
"title": "Release-Kanal ist nicht bereit",
"summary_prefix": "Dein Konto ist zugewiesen zu",
"summary_suffix": ", aber diesem Kanal fehlt erforderliche Release-Konfiguration.",
"release_bundle": "Release-Bundle",
"frontend_version": "Frontend-Version",
"api_version": "API-Version",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-Einstiegspunkt",
"release_runtime": "Release-Laufzeit",
"checking_again": "Erneute Prüfung in {seconds}s",
"refresh_error": "Release-Status konnte nicht aktualisiert werden. Die nächste automatische Prüfung versucht es erneut.",
"ignore": "Nächste 5 Minuten ignorieren",
"check_again": "Erneut prüfen",
"logout": "Abmelden",
"base_image": "Basis-Image"
},
"channel_switched": {
"kicker": "Release Manager",
"title": "Du bist jetzt auf {channel}",
"summary_prefix": "Dein Konto wurde dem Release-Kanal",
"summary_suffix": "zugewiesen. Dieses Gerät merkt sich, dass du diesen Hinweis gesehen hast.",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"frontend_version": "Frontend-Version",
"api_version": "API-Version",
"assigned_channel": "Zugewiesener Kanal",
"current_app_image": "Aktuelles App-Image",
"current_api": "Aktuelle API",
"base_image": "Basis-Image",
"continue": "Fortfahren"
},
"channel_selector": {
"title": "Release-Kanal",
"subtitle": "Wählen Sie, welchen zugewiesenen Release-Kanal dieses Gerät verwenden soll.",
"sidebar_title": "Release",
"default": "Standard",
"ready": "Bereit",
"unavailable": "Nicht bereit",
"release_bundle": "Release-Bundle",
"frontend_version": "Frontend-Version",
"api_version": "API-Version",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-Einstiegspunkt",
"release_runtime": "Release-Laufzeit",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"use_local_frontend": "Lokales Frontend verwenden"
},
"common": {
"yes": "ja",
"no": "nein",
"off": "aus"
},
"actions": {
"edit": "Bearbeiten",
"rollback": "Zurückrollen",
"update": "Aktualisieren",
"create": "Erstellen",
"clear": "Leeren",
"assign": "Zuweisen",
"remove": "Entfernen",
"test_access": "Zugriff testen",
"deploy": "Deployen",
"redeploy": "Erneut deployen",
"promote": "Promoten",
"enable": "Aktivieren",
"search": "Suchen",
"save_target": "Ziel speichern",
"delete": "Löschen",
"back": "Zurück",
"next": "Weiter"
},
"tables": {
"channel": "Kanal",
"rollout": "Rollout",
"frontend": "Frontend",
"api": "API",
"replay": "Replay",
"actions": "Aktionen",
"module": "Module",
"status": "Status",
"checked": "Geprüft",
"subject": "Subjekt",
"reason": "Grund",
"expires": "Läuft ab",
"deployment": "Deployment",
"source": "Quelle",
"started": "Gestartet",
"target": "Ziel",
"coolify": "Coolify",
"auto": "Auto"
},
"values": {
"app": {
"frontend": "Frontend",
"api": "API"
},
"capture_level": {
"metadata": "Metadaten",
"full_redacted": "vollständig redigiert"
},
"subject_type": {
"user": "Benutzer",
"subuser": "Mitarbeiter",
"customer": "Kunde",
"channel": "Kanal"
},
"severity": {
"error": "Fehler",
"warning": "Warnung",
"info": "Info"
},
"status": {
"ok": "ok",
"active": "aktiv",
"deployed": "deployt",
"stable": "stabil",
"enabled": "aktiviert",
"queued": "in Warteschlange",
"deploying": "deploying",
"metadata": "Metadaten",
"not_configured": "nicht konfiguriert",
"warning": "Warnung",
"failed": "fehlgeschlagen",
"down": "down",
"error": "Fehler",
"unknown": "unbekannt"
}
},
"autocomplete": {
"suggested_value": "Vorgeschlagener Wert",
"suggestion": "Vorschlag",
"use_typed_value": "Eingegebenen Wert verwenden",
"custom": "Benutzerdefiniert",
"api_url": "API URL",
"frontend_url": "Frontend URL",
"known_release_url": "Bekannte Release-URL",
"deploy_here_with_coolify": "{app} hier mit Coolify deployen",
"prepare_coolify_target": "Coolify-Ziel mit aktiviertem HTTPS/SSL vorbereiten",
"coolify_ssl": "Coolify SSL",
"github_repository": "GitHub-Repository",
"owner_repository": "owner/repository",
"git_branch": "Git branch",
"deployment_branch": "Deployment branch",
"github_commit": "GitHub commit",
"git_commit": "Git commit",
"use_specific_commit_sha": "Bestimmte Commit-SHA verwenden",
"custom_commit": "Benutzerdefinierter Commit",
"health_url": "Health URL",
"release_health_check": "Release-Health-Check",
"coolify_service": "Coolify service",
"service_uuid": "Service UUID",
"known_coolify_service_uuid": "Known Coolify service UUID",
"control_api": "Kontroll-API",
"release_manager_api_endpoint": "Release Manager API-Endpunkt",
"module": "Module",
"timeline_module_key": "Timeline-Modulschlüssel"
},
"errors": {
"github_repositories": "GitHub-Repositories konnten nicht geladen werden.",
"github_branches": "GitHub-Branches konnten nicht geladen werden.",
"repository_branch_required": "Repository und Branch sind erforderlich, bevor GitHub-Zugriff getestet werden kann.",
"github_access_failed": "GitHub-Repository-Zugriffstest ist fehlgeschlagen."
},
"channels": {
"title": "Kanäle",
"subtitle": "Graduale Release-Kanäle erstellen und anpassen.",
"rollout_percent": "{percent}% rollout",
"already_configured": "Bereits konfiguriert",
"slug": "Slug",
"slug_message": "Example: canary",
"slug_placeholder": "slug",
"name": "Name",
"name_message": "Wird Operatoren bei der Kanalprüfung angezeigt.",
"name_placeholder": "Name",
"rollout": "Rollout",
"rollout_message": "0-100 Prozent des nicht zugewiesenen Traffics.",
"frontend_url": "Frontend URL",
"frontend_url_message": "Example: https://canary.example.test",
"frontend_url_placeholder": "Frontend URL",
"no_matching_release_url": "Keine passende Release-URL.",
"api_url": "API URL",
"api_url_message": "Example: https://api-canary.example.test",
"api_url_placeholder": "API URL",
"no_matching_api_url": "Keine passende API-URL.",
"replay_capture": "Replay-Erfassung",
"replay_capture_message": "Metadaten sind für breite Kanäle sicherer.",
"retention_days": "Aufbewahrungstage",
"retention_days_message": "1-365 Tage.",
"enabled": "Aktiviert",
"default_channel": "Standardkanal",
"replay_enabled": "Replay aktiviert",
"frontend_chip": "Frontend: {url}",
"api_chip": "API: {url}"
},
"assignments": {
"title": "Zuweisungen",
"subtitle": "Benutzer, Mitarbeiter oder Kunden an Release-Kanäle binden.",
"subject": "Subjekt",
"subject_message": "Benutzer, Subuser oder Kunden suchen oder ein manuelles Subjekt wie user:42 eingeben.",
"subject_placeholder": "Subjekt suchen oder eingeben",
"subject_empty": "Keine Subjekte gefunden",
"group_users": "Benutzer",
"group_subusers": "Subuser",
"group_customers": "Kunden",
"group_manual": "Manuell",
"manual_user": "Benutzer #{id}",
"manual_subuser": "Subuser #{id}",
"manual_customer": "Kunde #{id}",
"manual_description": "Eingegebenen Wert {subject} verwenden",
"subject_type": "Subjekttyp",
"subject_type_message": "Identitätstyp zum Binden auswählen.",
"customer": "Customer",
"customer_message": "Nach Kundennummer oder Name suchen und dann den genauen Kunden auswählen.",
"customer_placeholder": "Kunde suchen",
"subject_id": "Subjekt-ID",
"subject_id_message": "Beispiel: Benutzer-ID 42",
"subject_id_placeholder": "Subjekt-ID",
"channel": "Channel",
"channel_message": "Gebundene Subjekte umgehen den prozentualen Rollout.",
"reason": "Grund",
"reason_message": "Beispiel: Canary-Validierung für Kundensupport.",
"reason_placeholder": "Grund",
"expires": "Läuft ab",
"expires_message": "Leer lassen für kein automatisches Ablaufdatum."
},
"deployments": {
"title": "Deployments",
"subtitle": "Release-Deployments starten, promoten und prüfen.",
"manual_deployment_record": "Manueller Deployment-Eintrag",
"target": "Ziel",
"target_message": "Gespeichertes Ziel wählen oder manuelle Felder editierbar lassen.",
"select_target_or_manual": "Ziel oder manuell auswählen",
"channel": "Channel",
"app": "App",
"repository": "Repository",
"repository_message": "Example: truckwash/front-end-vue",
"repository_placeholder": "owner/repo",
"branch": "Branch",
"branch_message": "Example: master",
"branch_placeholder": "branch",
"commit_selection": "Commit-Auswahl",
"commit_selection_message": "Neueste Version wird mit dem konfigurierten GitHub-Token als Branch-Head aufgelöst.",
"latest_from_branch": "Neuester vom Branch",
"specific_commit": "Bestimmter Commit",
"specific_commit_message": "Example: feedface1234",
"commit_sha_placeholder": "commit sha",
"version_label": "Versionslabel",
"version_label_message": "Example: api-2026.05.19",
"version_label_placeholder": "Versionslabel",
"commit_prefix": "Commit: {commit}",
"test_access_tooltip": "Token-Zugriff auf das ausgewählte Repository, den Branch und Commit prüfen."
},
"replay": {
"title": "Replay",
"subtitle": "Gezielte Erfassung aktivieren und Fehler-Timelines Schritt für Schritt prüfen.",
"capture_channel": "{channel} erfassen",
"target_type": "Zieltyp",
"target": "Ziel",
"target_message": "Beispiel: Benutzer-ID 42 oder Kanal-Slug canary.",
"target_placeholder": "Ziel-ID oder Kanal-Slug",
"channel_scope": "Kanalbereich",
"channel_scope_message": "Optional; begrenzt Replay-Erfassung auf einen Kanal.",
"no_channel_scope": "Kein Kanalbereich",
"capture_level": "Erfassungsstufe",
"expires": "Läuft ab",
"expires_message": "Leer lassen für manuelle Deaktivierung.",
"trace_id": "Trace ID",
"trace_id_message": "Example: trace-canary-1",
"trace_id_placeholder": "Trace id",
"channel": "Channel",
"channel_placeholder": "Channel",
"principal_id": "Principal ID",
"principal_id_placeholder": "Principal-ID",
"module": "Module",
"module_placeholder": "Module",
"severity": "Schweregrad",
"any_severity": "Beliebiger Schweregrad",
"no_timeline_events": "Keine Timeline-Events entsprechen den aktuellen Filtern."
},
"integrations": {
"title": "GitHub und Coolify",
"subtitle": "GitHub-Repositories und Branches kanalspezifischen Coolify-Services zuordnen.",
"choose_repository": "Repository auswählen",
"add_health_url": "Health-URL hinzufügen",
"github_token_not_configured": "GitHub-Token ist nicht konfiguriert; private Repositories können nicht aufgelistet werden.",
"open_github_token_settings": "GitHub-Token-Einstellungen öffnen",
"channel": "Channel",
"app": "App",
"repository": "Repository",
"repository_message": "Example: truckwash/backend-php",
"repository_placeholder": "owner/repo",
"branch": "Branch",
"branch_message": "Example: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Neueste: {commit}",
"coolify_instance": "Coolify-Instanz",
"coolify_instance_message": "Leer lassen für manuelle Deployment-Einträge.",
"no_coolify_instance": "Keine Coolify-Instanz",
"coolify_instance_placeholder": "Coolify-Instanz-ID",
"coolify_service_uuid": "Coolify service UUID",
"coolify_service_uuid_message": "Bestehenden Service auswählen oder UUID einfügen.",
"coolify_service_uuid_placeholder": "Coolify service UUID",
"health_url": "Health URL",
"health_url_message": "Example: https://api-canary.example.test/ping",
"health_url_placeholder": "Health URL",
"ssl_domain": "Load-Balancer-Domain",
"ssl_domain_message": "Muss eine DNS-Domain sein, die zum Coolify Load Balancer geroutet wird. Beispiel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load-Balancer-Domain",
"https_domain_for_coolify": "Domain unter Kontrolle des Load Balancers",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto-Deploy",
"auto_deploy_tooltip": "Automatisch Deployments erstellen, wenn sich dieses Ziel ändert.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Coolify anweisen, HTTPS für die konfigurierte Domain bereitzustellen.",
"deploy_now": "Jetzt deployen",
"deploy_now_tooltip": "Deployment direkt nach dem Speichern des Ziels starten.",
"create_missing_service": "Fehlenden Service erstellen",
"create_missing_service_tooltip": "Release Manager erlauben, einen fehlenden Coolify-Service zu erstellen.",
"test_access_tooltip": "Prüfen, ob der konfigurierte GitHub-Token dieses Repository und den Branch lesen kann."
}
}
}
}
@@ -0,0 +1,61 @@
{
"compat": {
"configuration": {
"release_manager": {
"integrations": {
"title": "GitHub und Coolify",
"subtitle": "GitHub-Repositories und Branches kanalspezifischen Coolify-Services zuordnen.",
"choose_repository": "Repository auswählen",
"add_health_url": "Health-URL hinzufügen",
"github_token_not_configured": "GitHub-Token ist nicht konfiguriert; private Repositories können nicht aufgelistet werden.",
"open_github_token_settings": "GitHub-Token-Einstellungen öffnen",
"channel": "Channel",
"app": "App",
"repository": "Repository",
"repository_message": "Example: truckwash/backend-php",
"repository_placeholder": "owner/repo",
"branch": "Branch",
"branch_message": "Example: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Neueste: {commit}",
"coolify_instance": "Coolify-Instanz",
"coolify_instance_message": "Leer lassen für manuelle Deployment-Einträge.",
"no_coolify_instance": "Keine Coolify-Instanz",
"coolify_instance_placeholder": "Coolify-Instanz-ID",
"coolify_service_uuid": "Coolify service UUID",
"coolify_service_uuid_message": "Bestehenden Service auswählen oder UUID einfügen.",
"coolify_service_uuid_placeholder": "Coolify service UUID",
"health_url": "Health URL",
"health_url_message": "Example: https://api-canary.example.test/ping",
"health_url_placeholder": "Health URL",
"ssl_domain": "Load-Balancer-Domain",
"ssl_domain_message": "Muss eine DNS-Domain sein, die zum Coolify Load Balancer geroutet wird. Beispiel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load-Balancer-Domain",
"https_domain_for_coolify": "Domain unter Kontrolle des Load Balancers",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto-Deploy",
"auto_deploy_tooltip": "Automatisch Deployments erstellen, wenn sich dieses Ziel ändert.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Coolify anweisen, HTTPS für die konfigurierte Domain bereitzustellen.",
"deploy_now": "Jetzt deployen",
"deploy_now_tooltip": "Deployment direkt nach dem Speichern des Ziels starten.",
"create_missing_service": "Fehlenden Service erstellen",
"create_missing_service_tooltip": "Release Manager erlauben, einen fehlenden Coolify-Service zu erstellen.",
"test_access_tooltip": "Prüfen, ob der konfigurierte GitHub-Token dieses Repository und den Branch lesen kann."
}
}
}
}
}
@@ -0,0 +1,454 @@
{
"compat": {
"configuration": {
"release_manager": {
"title": "Release Manager",
"subtitle": "Schrittweise API- und Front-End-Releases",
"channel_names": {
"stable": "Stabil",
"canary": "Canary",
"internal": "Intern"
},
"channel_descriptions": {
"stable": "Standard-Produktionskanal.",
"canary": "Früher Produktionsvalidierungskanal.",
"internal": "Interner Kanal für Mitarbeitende und Superuser-Validierung."
},
"control_api": {
"title": "Kontroll-API",
"tooltip": "Release-Manager-Aktionen werden an diese API gesendet. Verwende die Produktions-Kontroll-API, außer du testest ein Staging-Backend.",
"example": "Beispiel: https://api.truckwash.io:4433",
"endpoint_label": "API-Endpunkt",
"endpoint_message": "Ändere dies nur, wenn Release-Manager-Endpunkte auf der Ziel-API verfügbar sind.",
"endpoint_aria": "Release Manager Kontroll-API-URL",
"use_tooltip": "Release-Daten von dieser API laden",
"use": "Verwenden",
"reset_tooltip": "Zur Standard-Kontroll-API zurückkehren",
"reset": "Zurücksetzen",
"known_endpoints": "Bekannte Kontroll-API-Endpunkte"
},
"tabs": {
"overview": {
"label": "Übersicht",
"description": "Kanalzustand, Einrichtungsfortschritt und aktueller Release-Status."
},
"channels": {
"label": "Kanäle",
"description": "Stabile, Canary- und Zielkanäle mit Rollout-Grenzen erstellen."
},
"assignments": {
"label": "Zuweisungen",
"description": "Benutzer, Unterbenutzer oder Kunden einem bestimmten Release-Kanal zuweisen."
},
"deployments": {
"label": "Bereitstellungen",
"description": "Bereitstellungen starten und erfolgreiche Builds promoten."
},
"replay": {
"label": "Replay",
"description": "Gezielte Erfassung aktivieren und Release-Timeline-Ereignisse durchsuchen."
},
"integrations": {
"label": "Integrationen",
"description": "GitHub-Repositories, Branches und Coolify-Dienste verbinden."
},
"settings": {
"label": "Einstellungen",
"description": "GitHub-Zugriff und Webhook-Einstellungen des Release Managers konfigurieren."
}
},
"loading": {
"summary": "Release Manager-Daten werden geladen...",
"refreshing_summary": "Release Manager-Daten werden aktualisiert...",
"summary_detail": "Release-Status, Kanale, Deployments und Servicezustand werden geladen.",
"channels": "Release-Kanale werden geladen...",
"channels_detail": "Kanalansichten und Release-Abhangigkeiten werden geladen.",
"assignments": "Release-Zuweisungen werden geladen...",
"deployments": "Release-Deployments werden geladen...",
"operations": "Release-Operationen werden geladen...",
"data_services": "Release-Datendienste werden geladen...",
"integrations": "Release-Integrationen werden geladen...",
"settings": "Release Manager-Einstellungen werden geladen...",
"github_repositories": "GitHub-Repositories werden geladen...",
"timeline_sessions": "Release-Timeline-Sessions werden geladen...",
"timeline_events": "Release-Timeline-Ereignisse werden geladen...",
"refreshing_timeline": "Release-Timeline wird aktualisiert..."
},
"setup": {
"channels": "Kanäle",
"targets": "GitHub- und Coolify-Ziele",
"assignments": "Pilot-Zuweisungen",
"deployments": "Erste Bereitstellung",
"replay": "Replay-Erfassung"
},
"stats": {
"channels": "Kanäle",
"targets": "Ziele",
"deployments": "Bereitstellungen",
"timeline_events": "Timeline-Ereignisse"
},
"overview": {
"title": "Übersicht",
"subtitle": "Aktueller Kanalzustand, Release-Aktivität und Modulstatus.",
"guided_setup": "Geführte Einrichtung",
"next_step": "Nächster Schritt: {step}",
"ready": "Release Manager ist für den täglichen Betrieb bereit.",
"add_suggested_channel": "Vorgeschlagenen Kanal hinzufügen",
"module_health_empty": "Modul-Health-Snapshots erscheinen, nachdem Probes aufgezeichnet wurden."
},
"settings": {
"title": "Release-Manager-Einstellungen",
"subtitle": "GitHub-Token für private Repositories und Branch-Abfragen konfigurieren.",
"github_token": "GitHub-Token",
"github_api_url": "GitHub-API-URL",
"webhook_secret": "Webhook-Secret",
"github_webhook_secret": "GitHub-Webhook-Secret",
"configured": "Konfiguriert",
"not_configured": "Nicht konfiguriert",
"loaded_from": "Geladen aus {variable}",
"set_below": "{variable} unten festlegen",
"private_repositories_prefix": "Private Repositories werden mit der Server-Umgebungsvariable gelesen",
"private_repositories_or": "oder der Modul-Konfigurationsvariable",
"github_token_message": "Beispiel: github_pat_... mit Zugriff auf die privaten Repositories, die Release Manager bereitstellt.",
"github_token_placeholder": "Leer lassen, um das vorhandene Token zu behalten",
"github_api_url_message": "Konfiguriert in ReleaseManager.github_api_url. https://api.github.com verwenden, sofern GitHub Enterprise nicht genutzt wird.",
"webhook_secret_message": "Optional; leer lassen, um das vorhandene Secret zu behalten.",
"webhook_secret_placeholder": "Webhook-HMAC-Secret",
"save": "Einstellungen speichern",
"back_to_integrations": "Zurück zu Integrationen",
"guide": {
"steps": {
"github_token": "GitHub-Token",
"github_api_url": "GitHub API-URL",
"github_webhook_secret": "GitHub Webhook Secret",
"review": "Prüfung"
},
"github_token_title": "GitHub-Token hinzufügen",
"github_token_description": "Release Manager verwendet diesen Token, um private Repositories, Branches und Commits aufzulisten, bevor ein Deployment-Ziel gespeichert oder getestet wird.",
"configure_this_in": "Dies konfigurieren in",
"server_environment_variable": "Die Server-Umgebungsvariable",
"takes_precedence": "hat Vorrang, wenn sie gesetzt ist.",
"recommended_access": "Empfohlener Zugriff",
"recommended_access_description": "Fine-grained GitHub-Token mit Contents: read-Zugriff für jedes Release-Repository.",
"github_api_url_title": "GitHub API-URL hinzufügen",
"github_api_url_description": "GitHub Cloud standardmäßig verwenden oder auf die REST-API-Basis-URL einer GitHub Enterprise Server-Installation zeigen.",
"github_api_url_invalid": "Vollständige HTTP- oder HTTPS-URL eingeben.",
"github_cloud": "GitHub Cloud",
"enterprise_example": "Enterprise-Beispiel",
"github_webhook_secret_title": "GitHub Webhook Secret hinzufügen",
"github_webhook_secret_description": "Dasselbe Secret in GitHub-Repository-Webhooks verwenden, damit Release Manager Deployment-Events vor der Verarbeitung verifizieren kann.",
"github_webhook_url": "GitHub Webhook-URL:",
"store_shared_secret_in": "Geteiltes Secret speichern in",
"review_title": "GitHub-Konfiguration prüfen",
"review_description": "Werte speichern und dann unter Integrationen ein privates Repository, einen Branch und einen bestimmten Commit oder den neuesten Commit auswählen, bevor der Zugriff getestet wird.",
"ready": "Bereit",
"needs_value": "Wert fehlt"
}
},
"channel_unavailable": {
"kicker": "Release Manager",
"title": "Release-Kanal ist nicht bereit",
"summary_prefix": "Dein Konto ist zugewiesen zu",
"summary_suffix": ", aber diesem Kanal fehlt erforderliche Release-Konfiguration.",
"release_bundle": "Release-Bundle",
"frontend_version": "Frontend-Version",
"api_version": "API-Version",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-Einstiegspunkt",
"release_runtime": "Release-Laufzeit",
"checking_again": "Erneute Prüfung in {seconds}s",
"refresh_error": "Release-Status konnte nicht aktualisiert werden. Die nächste automatische Prüfung versucht es erneut.",
"ignore": "Nächste 5 Minuten ignorieren",
"check_again": "Erneut prüfen",
"logout": "Abmelden",
"base_image": "Basis-Image"
},
"channel_switched": {
"kicker": "Release Manager",
"title": "Du bist jetzt auf {channel}",
"summary_prefix": "Dein Konto wurde dem Release-Kanal",
"summary_suffix": "zugewiesen. Dieses Gerät merkt sich, dass du diesen Hinweis gesehen hast.",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"frontend_version": "Frontend-Version",
"api_version": "API-Version",
"assigned_channel": "Zugewiesener Kanal",
"current_app_image": "Aktuelles App-Image",
"current_api": "Aktuelle API",
"base_image": "Basis-Image",
"continue": "Fortfahren"
},
"channel_selector": {
"title": "Release-Kanal",
"subtitle": "Wählen Sie, welchen zugewiesenen Release-Kanal dieses Gerät verwenden soll.",
"sidebar_title": "Release",
"default": "Standard",
"ready": "Bereit",
"unavailable": "Nicht bereit",
"release_bundle": "Release-Bundle",
"frontend_version": "Frontend-Version",
"api_version": "API-Version",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-Einstiegspunkt",
"release_runtime": "Release-Laufzeit",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"use_local_frontend": "Lokales Frontend verwenden",
"switch_error": "Release channel could not be switched. The previous channel is still active."
},
"common": {
"yes": "ja",
"no": "nein",
"off": "aus"
},
"actions": {
"edit": "Bearbeiten",
"rollback": "Zurückrollen",
"update": "Aktualisieren",
"create": "Erstellen",
"clear": "Leeren",
"assign": "Zuweisen",
"remove": "Entfernen",
"test_access": "Zugriff testen",
"deploy": "Deployen",
"redeploy": "Erneut deployen",
"promote": "Promoten",
"enable": "Aktivieren",
"search": "Suchen",
"save_target": "Ziel speichern",
"delete": "Löschen",
"back": "Zurück",
"next": "Weiter",
"publish_release": "Publish release"
},
"tables": {
"channel": "Kanal",
"rollout": "Rollout",
"frontend": "Frontend",
"api": "API",
"replay": "Replay",
"actions": "Aktionen",
"module": "Module",
"status": "Status",
"checked": "Geprüft",
"subject": "Subjekt",
"reason": "Grund",
"expires": "Läuft ab",
"deployment": "Deployment",
"source": "Quelle",
"started": "Gestartet",
"target": "Ziel",
"coolify": "Coolify",
"auto": "Auto"
},
"values": {
"app": {
"frontend": "Frontend",
"api": "API"
},
"capture_level": {
"metadata": "Metadaten",
"full_redacted": "vollständig redigiert"
},
"subject_type": {
"user": "Benutzer",
"subuser": "Mitarbeiter",
"customer": "Kunde",
"channel": "Kanal"
},
"severity": {
"error": "Fehler",
"warning": "Warnung",
"info": "Info"
},
"status": {
"ok": "ok",
"active": "aktiv",
"deployed": "deployt",
"stable": "stabil",
"enabled": "aktiviert",
"queued": "in Warteschlange",
"deploying": "deploying",
"metadata": "Metadaten",
"not_configured": "nicht konfiguriert",
"warning": "Warnung",
"failed": "fehlgeschlagen",
"down": "down",
"error": "Fehler",
"unknown": "unbekannt"
}
},
"autocomplete": {
"suggested_value": "Vorgeschlagener Wert",
"suggestion": "Vorschlag",
"use_typed_value": "Eingegebenen Wert verwenden",
"custom": "Benutzerdefiniert",
"api_url": "API URL",
"frontend_url": "Frontend URL",
"known_release_url": "Bekannte Release-URL",
"deploy_here_with_coolify": "{app} hier mit Coolify deployen",
"prepare_coolify_target": "Coolify-Ziel mit aktiviertem HTTPS/SSL vorbereiten",
"coolify_ssl": "Coolify SSL",
"github_repository": "GitHub-Repository",
"owner_repository": "owner/repository",
"git_branch": "Git branch",
"deployment_branch": "Deployment branch",
"github_commit": "GitHub commit",
"git_commit": "Git commit",
"use_specific_commit_sha": "Bestimmte Commit-SHA verwenden",
"custom_commit": "Benutzerdefinierter Commit",
"health_url": "Health URL",
"release_health_check": "Release-Health-Check",
"coolify_service": "Coolify service",
"service_uuid": "Service UUID",
"known_coolify_service_uuid": "Known Coolify service UUID",
"control_api": "Kontroll-API",
"release_manager_api_endpoint": "Release Manager API-Endpunkt",
"module": "Module",
"timeline_module_key": "Timeline-Modulschlüssel"
},
"errors": {
"github_repositories": "GitHub-Repositories konnten nicht geladen werden.",
"github_branches": "GitHub-Branches konnten nicht geladen werden.",
"repository_branch_required": "Repository und Branch sind erforderlich, bevor GitHub-Zugriff getestet werden kann.",
"github_access_failed": "GitHub-Repository-Zugriffstest ist fehlgeschlagen."
},
"channels": {
"title": "Kanäle",
"subtitle": "Graduale Release-Kanäle erstellen und anpassen.",
"rollout_percent": "{percent}% rollout",
"already_configured": "Bereits konfiguriert",
"slug": "Slug",
"slug_message": "Example: canary",
"slug_placeholder": "slug",
"name": "Name",
"name_message": "Wird Operatoren bei der Kanalprüfung angezeigt.",
"name_placeholder": "Name",
"rollout": "Rollout",
"rollout_message": "0-100 Prozent des nicht zugewiesenen Traffics.",
"frontend_url": "Frontend URL",
"frontend_url_message": "Example: https://canary.example.test",
"frontend_url_placeholder": "Frontend URL",
"no_matching_release_url": "Keine passende Release-URL.",
"api_url": "API URL",
"api_url_message": "Example: https://api-canary.example.test",
"api_url_placeholder": "API URL",
"no_matching_api_url": "Keine passende API-URL.",
"replay_capture": "Replay-Erfassung",
"replay_capture_message": "Metadaten sind für breite Kanäle sicherer.",
"retention_days": "Aufbewahrungstage",
"retention_days_message": "1-365 Tage.",
"enabled": "Aktiviert",
"default_channel": "Standardkanal",
"replay_enabled": "Replay aktiviert",
"frontend_chip": "Frontend: {url}",
"api_chip": "API: {url}"
},
"assignments": {
"title": "Zuweisungen",
"subtitle": "Benutzer, Mitarbeiter oder Kunden an Release-Kanäle binden.",
"subject": "Subjekt",
"subject_message": "Benutzer, Subuser oder Kunden suchen oder ein manuelles Subjekt wie user:42 eingeben.",
"subject_placeholder": "Subjekt suchen oder eingeben",
"subject_empty": "Keine Subjekte gefunden",
"group_users": "Benutzer",
"group_subusers": "Subuser",
"group_customers": "Kunden",
"group_manual": "Manuell",
"manual_user": "Benutzer #{id}",
"manual_subuser": "Subuser #{id}",
"manual_customer": "Kunde #{id}",
"manual_description": "Eingegebenen Wert {subject} verwenden",
"subject_type": "Subjekttyp",
"subject_type_message": "Identitätstyp zum Binden auswählen.",
"customer": "Customer",
"customer_message": "Nach Kundennummer oder Name suchen und dann den genauen Kunden auswählen.",
"customer_placeholder": "Kunde suchen",
"subject_id": "Subjekt-ID",
"subject_id_message": "Beispiel: Benutzer-ID 42",
"subject_id_placeholder": "Subjekt-ID",
"channel": "Channel",
"channel_message": "Gebundene Subjekte umgehen den prozentualen Rollout.",
"reason": "Grund",
"reason_message": "Beispiel: Canary-Validierung für Kundensupport.",
"reason_placeholder": "Grund",
"expires": "Läuft ab",
"expires_message": "Leer lassen für kein automatisches Ablaufdatum."
},
"deployments": {
"title": "Deployments",
"subtitle": "Release-Deployments starten, promoten und prüfen.",
"manual_deployment_record": "Manueller Deployment-Eintrag",
"target": "Ziel",
"target_message": "Gespeichertes Ziel wählen oder manuelle Felder editierbar lassen.",
"select_target_or_manual": "Ziel oder manuell auswählen",
"channel": "Channel",
"app": "App",
"repository": "Repository",
"repository_message": "Example: truckwash/front-end-vue",
"repository_placeholder": "owner/repo",
"branch": "Branch",
"branch_message": "Example: master",
"branch_placeholder": "branch",
"commit_selection": "Commit-Auswahl",
"commit_selection_message": "Neueste Version wird mit dem konfigurierten GitHub-Token als Branch-Head aufgelöst.",
"latest_from_branch": "Neuester vom Branch",
"specific_commit": "Bestimmter Commit",
"specific_commit_message": "Example: feedface1234",
"commit_sha_placeholder": "commit sha",
"version_label": "Versionslabel",
"version_label_message": "Example: api-2026.05.19",
"version_label_placeholder": "Versionslabel",
"commit_prefix": "Commit: {commit}",
"test_access_tooltip": "Token-Zugriff auf das ausgewählte Repository, den Branch und Commit prüfen."
},
"replay": {
"title": "Replay",
"subtitle": "Gezielte Erfassung aktivieren und Fehler-Timelines Schritt für Schritt prüfen.",
"capture_channel": "{channel} erfassen",
"target_type": "Zieltyp",
"target": "Ziel",
"target_message": "Beispiel: Benutzer-ID 42 oder Kanal-Slug canary.",
"target_placeholder": "Ziel-ID oder Kanal-Slug",
"channel_scope": "Kanalbereich",
"channel_scope_message": "Optional; begrenzt Replay-Erfassung auf einen Kanal.",
"no_channel_scope": "Kein Kanalbereich",
"capture_level": "Erfassungsstufe",
"expires": "Läuft ab",
"expires_message": "Leer lassen für manuelle Deaktivierung.",
"trace_id": "Trace ID",
"trace_id_message": "Example: trace-canary-1",
"trace_id_placeholder": "Trace id",
"channel": "Channel",
"channel_placeholder": "Channel",
"principal_id": "Principal ID",
"principal_id_placeholder": "Principal-ID",
"module": "Module",
"module_placeholder": "Module",
"severity": "Schweregrad",
"any_severity": "Beliebiger Schweregrad",
"no_timeline_events": "Keine Timeline-Events entsprechen den aktuellen Filtern."
},
"status": {
"confirm_issue_action": "Run this Release Manager action? It can change deployment state and will be audited.",
"choose_bundle_prompt": "Enter the bundle id to set for this channel.",
"impact": "Impact",
"cause": "Cause",
"automated_fix": "Automated fix",
"manual_fallback": "Manual fallback",
"related_deployment": "Related deployment",
"recent_result": "Recent result",
"no_automated_action": "No automated action available.",
"deployment": "Deployment",
"target": "Target",
"coolify_target": "Coolify target",
"service_set": "Service set"
}
}
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
"uhrzeit": "Uhrzeit",
"um": "um",
"umbenannt": "umbenannt",
"umbenennen": "umbenennen",
"umbenennen": "@:{'terms.glossary.rename'}",
"umsatz": "Umsatz",
"umweltgeb": "Umweltgeb",
"unabhangig": "unabhängig",
@@ -0,0 +1,28 @@
{
"compat": {
"admin": {
"pos": {
"settings_wheel": {
"accept_self_serve_wash": "Accept self-serve wash",
"accept_self_serve_wash_confirm": "Move this draft to customer #{customerNumber} with the self-serve details?",
"accept_self_serve_wash_success": "The self-serve wash was accepted.",
"email_notifications_section": "Email notifications",
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
"resend_booking_confirmation": "(Re)send booking confirmation",
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
"resend_booking_confirmation_success": "The booking confirmation has been sent",
"resend_wash_certificate": "(Re)send wash certificate",
"resend_wash_certificate_error": "The wash certificate could not be sent",
"resend_wash_certificate_success": "The wash certificate has been sent",
"self_serve_customer": "Customer",
"self_serve_driver": "Driver",
"self_serve_elapsed": "Elapsed",
"self_serve_wash_attachment": "Self-serve wash",
"self_serve_wash_attachment_for_customer": "Self-serve wash for customer #{customerNumber}"
}
}
}
}
}
@@ -298,507 +298,6 @@
"subtitle": "@.capitalize:{'terms.glossary.configuration'} @:{'terms.glossary.of'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.xlvask'} @:{'terms.glossary.integration'}",
"title": "@:{'terms.glossary.xlvask'} @:{'terms.glossary.configuration'}",
"username_desc": "@.capitalize:{'terms.replication.article.host_mention'} @:{'terms.glossary.username'} @:{'terms.glossary.is'} @:{'terms.glossary.used'} @:{'terms.glossary.to'} @:{'terms.glossary.authenticate'} @:{'terms.replication.article.host_mention'} @:{'terms.glossary.api'} @:{'terms.glossary.connection'}."
},
"release_manager": {
"title": "Release Manager",
"subtitle": "Gradual API and Front-End releases",
"channel_names": {
"stable": "Stable",
"canary": "Canary",
"internal": "Internal"
},
"channel_descriptions": {
"stable": "Default production channel.",
"canary": "Early production validation channel.",
"internal": "Internal staff and superuser validation channel."
},
"control_api": {
"title": "Control API",
"tooltip": "Release Manager actions are sent to this API. Use the production control API unless testing a staged backend.",
"example": "Example: https://api.truckwash.io:4433",
"endpoint_label": "API endpoint",
"endpoint_message": "Switch this only when Release Manager endpoints are available on the target API.",
"endpoint_aria": "Release Manager control API URL",
"use_tooltip": "Load release data from this API",
"use": "Use",
"reset_tooltip": "Return to the default control API",
"reset": "Reset",
"known_endpoints": "Known control API endpoints"
},
"tabs": {
"overview": {
"label": "Overview",
"description": "Channel health, setup progress, and the latest release state."
},
"channels": {
"label": "Channels",
"description": "Create stable, canary, and targeted channels with rollout limits."
},
"assignments": {
"label": "Assignments",
"description": "Pin users, subusers, or customers to a specific release channel."
},
"deployments": {
"label": "Deployments",
"description": "Trigger deployments and promote successful builds."
},
"replay": {
"label": "Replay",
"description": "Enable targeted capture and search release timeline events."
},
"integrations": {
"label": "Integrations",
"description": "Connect GitHub repositories, branches, and Coolify services."
},
"settings": {
"label": "Settings",
"description": "Configure Release Manager GitHub access and webhook settings."
}
},
"loading": {
"summary": "Loading release manager data...",
"refreshing_summary": "Refreshing release manager data...",
"summary_detail": "Release status, channels, deployments, and service health are being loaded.",
"channels": "Loading release channels...",
"channels_detail": "Channel lanes and release dependencies are being loaded.",
"assignments": "Loading release assignments...",
"deployments": "Loading release deployments...",
"operations": "Loading release operations...",
"data_services": "Loading release data services...",
"integrations": "Loading release integrations...",
"settings": "Loading Release Manager settings...",
"github_repositories": "Loading GitHub repositories...",
"timeline_sessions": "Loading release timeline sessions...",
"timeline_events": "Loading release timeline events...",
"refreshing_timeline": "Refreshing release timeline..."
},
"setup": {
"channels": "Channels",
"targets": "GitHub and Coolify targets",
"assignments": "Pilot assignments",
"deployments": "First deployment",
"replay": "Replay capture"
},
"stats": {
"channels": "Channels",
"targets": "Targets",
"deployments": "Deployments",
"timeline_events": "Timeline events"
},
"overview": {
"title": "Overview",
"subtitle": "Current channel health, release activity, and module status.",
"guided_setup": "Guided setup",
"next_step": "Next: {step}",
"ready": "Release Manager is ready for day-to-day operations.",
"add_suggested_channel": "Add suggested channel",
"module_health_empty": "Module health snapshots appear after probes have been recorded."
},
"status": {
"confirm_issue_action": "Run this Release Manager action? It can change deployment state and will be audited.",
"choose_bundle_prompt": "Enter the bundle id to set for this channel.",
"impact": "Impact",
"cause": "Cause",
"automated_fix": "Automated fix",
"manual_fallback": "Manual fallback",
"related_deployment": "Related deployment",
"recent_result": "Recent result",
"no_automated_action": "No automated action available.",
"deployment": "Deployment",
"target": "Target",
"coolify_target": "Coolify target",
"service_set": "Service set"
},
"settings": {
"title": "Release Manager Settings",
"subtitle": "Configure the GitHub token used for private repositories and branch lookups.",
"github_token": "GitHub token",
"github_api_url": "GitHub API URL",
"webhook_secret": "Webhook secret",
"github_webhook_secret": "GitHub webhook secret",
"configured": "Configured",
"not_configured": "Not configured",
"loaded_from": "Loaded from {variable}",
"set_below": "Set {variable} below",
"private_repositories_prefix": "Private repositories are read with the server environment variable",
"private_repositories_or": "or the module config variable",
"github_token_message": "Example: github_pat_... with access to the private repositories Release Manager deploys.",
"github_token_placeholder": "Leave blank to keep the existing token",
"github_api_url_message": "Configured in ReleaseManager.github_api_url. Use https://api.github.com unless using GitHub Enterprise.",
"webhook_secret_message": "Optional; leave blank to keep the existing secret.",
"webhook_secret_placeholder": "Webhook HMAC secret",
"save": "Save settings",
"back_to_integrations": "Back to integrations",
"guide": {
"steps": {
"github_token": "GitHub token",
"github_api_url": "GitHub API URL",
"github_webhook_secret": "GitHub webhook secret",
"review": "Review"
},
"github_token_title": "Add GitHub token",
"github_token_description": "Release Manager uses this token to list private repositories, branches, and commits before a deployment target is saved or tested.",
"configure_this_in": "Configure this in",
"server_environment_variable": "The server environment variable",
"takes_precedence": "takes precedence when it is set.",
"recommended_access": "Recommended access",
"recommended_access_description": "Fine-grained GitHub token with Contents: read access for each release repository.",
"github_api_url_title": "Add GitHub API URL",
"github_api_url_description": "Use GitHub Cloud by default, or point this at the REST API base URL for a GitHub Enterprise Server installation.",
"github_api_url_invalid": "Enter a full HTTP or HTTPS URL.",
"github_cloud": "GitHub Cloud",
"enterprise_example": "Enterprise example",
"github_webhook_secret_title": "Add GitHub webhook secret",
"github_webhook_secret_description": "Use the same secret in GitHub repository webhooks so Release Manager can verify deployment events before handling them.",
"github_webhook_url": "GitHub webhook URL:",
"store_shared_secret_in": "Store the shared secret in",
"review_title": "Review GitHub setup",
"review_description": "Save the values, then use Integrations to select a private repository, branch, and specific commit or latest commit before testing access.",
"ready": "Ready",
"needs_value": "Needs value"
}
},
"channel_unavailable": {
"kicker": "Release Manager",
"title": "Release channel is not ready",
"summary_prefix": "Your account is assigned to",
"summary_suffix": ", but that channel is missing required release configuration.",
"release_bundle": "Release bundle",
"frontend_version": "Frontend version",
"api_version": "API version",
"frontend_base_url": "Frontend URL",
"api_base_url": "API URL",
"frontend_entry": "Frontend entry",
"release_runtime": "Release runtime",
"checking_again": "Checking again in {seconds}s",
"refresh_error": "Release status could not be refreshed. The next automatic check will try again.",
"ignore": "Ignore next 5 minutes",
"check_again": "Check again",
"logout": "Logout",
"base_image": "Base image"
},
"channel_switched": {
"kicker": "Release Manager",
"title": "You are now on {channel}",
"summary_prefix": "Your account has been assigned to the",
"summary_suffix": "release channel. This device will remember that you have seen this notice.",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"frontend_version": "Frontend version",
"api_version": "API version",
"assigned_channel": "Assigned channel",
"current_app_image": "Current app image",
"current_api": "Current API",
"base_image": "Base image",
"continue": "Continue"
},
"channel_selector": {
"title": "Release channel",
"subtitle": "Choose which assigned release channel this device should use.",
"sidebar_title": "Release",
"default": "Default",
"ready": "Ready",
"unavailable": "Not ready",
"release_bundle": "Release bundle",
"frontend_version": "Frontend version",
"api_version": "API version",
"frontend_base_url": "Frontend URL",
"api_base_url": "API URL",
"frontend_entry": "Frontend entry",
"release_runtime": "Release runtime",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"use_local_frontend": "Use local frontend",
"switch_error": "Release channel could not be switched. The previous channel is still active."
},
"common": {
"yes": "yes",
"no": "no",
"off": "off"
},
"actions": {
"edit": "Edit",
"rollback": "Rollback",
"update": "Update",
"create": "Create",
"publish_release": "Publish release",
"clear": "Clear",
"assign": "Assign",
"remove": "Remove",
"test_access": "Test access",
"deploy": "Deploy",
"redeploy": "Redeploy",
"promote": "Promote",
"enable": "Enable",
"search": "Search",
"save_target": "Save target",
"delete": "Delete",
"back": "Back",
"next": "Next"
},
"tables": {
"channel": "Channel",
"rollout": "Rollout",
"frontend": "Frontend",
"api": "API",
"replay": "Replay",
"actions": "Actions",
"module": "Module",
"status": "Status",
"checked": "Checked",
"subject": "Subject",
"reason": "Reason",
"expires": "Expires",
"deployment": "Deployment",
"source": "Source",
"started": "Started",
"target": "Target",
"coolify": "Coolify",
"auto": "Auto"
},
"values": {
"app": {
"frontend": "frontend",
"api": "api"
},
"capture_level": {
"metadata": "metadata",
"full_redacted": "full_redacted"
},
"subject_type": {
"user": "user",
"subuser": "subuser",
"customer": "customer",
"channel": "channel"
},
"severity": {
"error": "error",
"warning": "warning",
"info": "info"
},
"status": {
"ok": "ok",
"active": "active",
"deployed": "deployed",
"stable": "stable",
"enabled": "enabled",
"queued": "queued",
"deploying": "deploying",
"metadata": "metadata",
"not_configured": "not configured",
"warning": "warning",
"failed": "failed",
"down": "down",
"error": "error",
"unknown": "unknown"
}
},
"autocomplete": {
"suggested_value": "Suggested value",
"suggestion": "Suggestion",
"use_typed_value": "Use typed value",
"custom": "Custom",
"api_url": "API URL",
"frontend_url": "Frontend URL",
"known_release_url": "Known release URL",
"deploy_here_with_coolify": "Deploy {app} here with Coolify",
"prepare_coolify_target": "Prepare a Coolify target with HTTPS/SSL enabled",
"coolify_ssl": "Coolify SSL",
"github_repository": "GitHub repository",
"owner_repository": "owner/repository",
"git_branch": "Git branch",
"deployment_branch": "Deployment branch",
"github_commit": "GitHub commit",
"git_commit": "Git commit",
"use_specific_commit_sha": "Use specific commit SHA",
"custom_commit": "Custom commit",
"health_url": "Health URL",
"release_health_check": "Release health check",
"coolify_service": "Coolify service",
"service_uuid": "Service UUID",
"known_coolify_service_uuid": "Known Coolify service UUID",
"control_api": "Control API",
"release_manager_api_endpoint": "Release Manager API endpoint",
"module": "Module",
"timeline_module_key": "Timeline module key"
},
"errors": {
"github_repositories": "Could not load GitHub repositories.",
"github_branches": "Could not load GitHub branches.",
"repository_branch_required": "Repository and branch are required before testing GitHub access.",
"github_access_failed": "GitHub repository access test failed."
},
"channels": {
"title": "Channels",
"subtitle": "Create and tune gradual release channels.",
"rollout_percent": "{percent}% rollout",
"already_configured": "Already configured",
"slug": "Slug",
"slug_message": "Example: canary",
"slug_placeholder": "slug",
"name": "Name",
"name_message": "Shown to operators when reviewing a channel.",
"name_placeholder": "Name",
"rollout": "Rollout",
"rollout_message": "0-100 percent of unassigned traffic.",
"frontend_url": "Frontend URL",
"frontend_url_message": "Example: https://canary.example.test",
"frontend_url_placeholder": "Frontend URL",
"no_matching_release_url": "No matching release URL.",
"api_url": "API URL",
"api_url_message": "Example: https://api-canary.example.test",
"api_url_placeholder": "API URL",
"no_matching_api_url": "No matching API URL.",
"replay_capture": "Replay capture",
"replay_capture_message": "Metadata is safer for broad channels.",
"retention_days": "Retention days",
"retention_days_message": "1-365 days.",
"enabled": "Enabled",
"default_channel": "Default channel",
"replay_enabled": "Replay enabled",
"frontend_chip": "Frontend: {url}",
"api_chip": "API: {url}"
},
"assignments": {
"title": "Assignments",
"subtitle": "Pin users, subusers, or customers to release channels.",
"subject": "Subject",
"subject_message": "Search users, subusers, or customers, or type a manual subject like user:42.",
"subject_placeholder": "Search or type subject",
"subject_empty": "No subjects found",
"group_users": "Users",
"group_subusers": "Subusers",
"group_customers": "Customers",
"group_manual": "Manual",
"manual_user": "User #{id}",
"manual_subuser": "Subuser #{id}",
"manual_customer": "Customer #{id}",
"manual_description": "Use typed value {subject}",
"subject_type": "Subject type",
"subject_type_message": "Choose the identity type to pin.",
"customer": "Customer",
"customer_message": "Search by customer number or name, then select the exact customer.",
"customer_placeholder": "Search customer",
"subject_id": "Subject ID",
"subject_id_message": "Example: user id 42",
"subject_id_placeholder": "Subject id",
"channel": "Channel",
"channel_message": "Pinned subjects bypass percentage rollout.",
"reason": "Reason",
"reason_message": "Example: canary validation for customer support.",
"reason_placeholder": "Reason",
"expires": "Expires",
"expires_message": "Leave blank for no automatic expiry."
},
"deployments": {
"title": "Deployments",
"subtitle": "Trigger, promote, and inspect release deployments.",
"manual_deployment_record": "Manual deployment record",
"target": "Target",
"target_message": "Pick a saved target or keep manual fields editable.",
"select_target_or_manual": "Select target or manual",
"channel": "Channel",
"app": "App",
"repository": "Repository",
"repository_message": "Example: truckwash/front-end-vue",
"repository_placeholder": "owner/repo",
"branch": "Branch",
"branch_message": "Example: master",
"branch_placeholder": "branch",
"commit_selection": "Commit selection",
"commit_selection_message": "Latest resolves to the branch head with the configured GitHub token.",
"latest_from_branch": "Latest from branch",
"specific_commit": "Specific commit",
"specific_commit_message": "Example: feedface1234",
"commit_sha_placeholder": "commit sha",
"version_label": "Version label",
"version_label_message": "Example: api-2026.05.19",
"version_label_placeholder": "version label",
"commit_prefix": "Commit: {commit}",
"test_access_tooltip": "Verify token access to the selected repository, branch, and commit."
},
"replay": {
"title": "Replay",
"subtitle": "Enable targeted capture and step through failure timelines.",
"capture_channel": "Capture {channel}",
"target_type": "Target type",
"target": "Target",
"target_message": "Example: user id 42 or channel slug canary.",
"target_placeholder": "Target id or channel slug",
"channel_scope": "Channel scope",
"channel_scope_message": "Optional; limits replay capture to a channel.",
"no_channel_scope": "No channel scope",
"capture_level": "Capture level",
"expires": "Expires",
"expires_message": "Leave blank for manual disable.",
"trace_id": "Trace ID",
"trace_id_message": "Example: trace-canary-1",
"trace_id_placeholder": "Trace id",
"channel": "Channel",
"channel_placeholder": "Channel",
"principal_id": "Principal ID",
"principal_id_placeholder": "Principal id",
"module": "Module",
"module_placeholder": "Module",
"severity": "Severity",
"any_severity": "Any severity",
"no_timeline_events": "No timeline events match the current filters."
},
"integrations": {
"title": "GitHub and Coolify",
"subtitle": "Map GitHub repositories and branches to channel-specific Coolify services.",
"choose_repository": "Choose repository",
"add_health_url": "add health URL",
"github_token_not_configured": "GitHub token is not configured; private repositories cannot be listed.",
"open_github_token_settings": "Open GitHub token settings",
"channel": "Channel",
"app": "App",
"repository": "Repository",
"repository_message": "Example: truckwash/backend-php",
"repository_placeholder": "owner/repo",
"branch": "Branch",
"branch_message": "Example: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Latest: {commit}",
"coolify_instance": "Coolify instance",
"coolify_instance_message": "Leave empty for manual deployment records.",
"no_coolify_instance": "No Coolify instance",
"coolify_instance_placeholder": "Coolify instance id",
"coolify_service_uuid": "Coolify service UUID",
"coolify_service_uuid_message": "Select an existing service or paste a UUID.",
"coolify_service_uuid_placeholder": "Coolify service UUID",
"health_url": "Health URL",
"health_url_message": "Example: https://api-canary.example.test/ping",
"health_url_placeholder": "Health URL",
"ssl_domain": "Load balancer domain",
"ssl_domain_message": "Must be a DNS domain routed to the Coolify load balancer. Example: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer domain",
"https_domain_for_coolify": "Domain controlled by the load balancer",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Automatically create deployments when this target changes.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Ask Coolify to provision HTTPS for the configured domain.",
"deploy_now": "Deploy now",
"deploy_now_tooltip": "Start a deployment immediately after saving the target.",
"create_missing_service": "Create missing service",
"create_missing_service_tooltip": "Allow Release Manager to create a missing Coolify service.",
"test_access_tooltip": "Verify the configured GitHub token can read this repository and branch."
}
}
}
}
@@ -0,0 +1,61 @@
{
"compat": {
"configuration": {
"release_manager": {
"integrations": {
"title": "GitHub and Coolify",
"subtitle": "Map GitHub repositories and branches to channel-specific Coolify services.",
"choose_repository": "Choose repository",
"add_health_url": "add health URL",
"github_token_not_configured": "GitHub token is not configured; private repositories cannot be listed.",
"open_github_token_settings": "Open GitHub token settings",
"channel": "Channel",
"app": "App",
"repository": "Repository",
"repository_message": "Example: truckwash/backend-php",
"repository_placeholder": "owner/repo",
"branch": "Branch",
"branch_message": "Example: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Latest: {commit}",
"coolify_instance": "Coolify instance",
"coolify_instance_message": "Leave empty for manual deployment records.",
"no_coolify_instance": "No Coolify instance",
"coolify_instance_placeholder": "Coolify instance id",
"coolify_service_uuid": "Coolify service UUID",
"coolify_service_uuid_message": "Select an existing service or paste a UUID.",
"coolify_service_uuid_placeholder": "Coolify service UUID",
"health_url": "Health URL",
"health_url_message": "Example: https://api-canary.example.test/ping",
"health_url_placeholder": "Health URL",
"ssl_domain": "Load balancer domain",
"ssl_domain_message": "Must be a DNS domain routed to the Coolify load balancer. Example: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer domain",
"https_domain_for_coolify": "Domain controlled by the load balancer",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Automatically create deployments when this target changes.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Ask Coolify to provision HTTPS for the configured domain.",
"deploy_now": "Deploy now",
"deploy_now_tooltip": "Start a deployment immediately after saving the target.",
"create_missing_service": "Create missing service",
"create_missing_service_tooltip": "Allow Release Manager to create a missing Coolify service.",
"test_access_tooltip": "Verify the configured GitHub token can read this repository and branch."
}
}
}
}
}
@@ -0,0 +1,454 @@
{
"compat": {
"configuration": {
"release_manager": {
"title": "Release Manager",
"subtitle": "Gradual API and Front-End releases",
"channel_names": {
"stable": "Stable",
"canary": "Canary",
"internal": "Internal"
},
"channel_descriptions": {
"stable": "Default production channel.",
"canary": "Early production validation channel.",
"internal": "Internal staff and superuser validation channel."
},
"control_api": {
"title": "Control API",
"tooltip": "Release Manager actions are sent to this API. Use the production control API unless testing a staged backend.",
"example": "Example: https://api.truckwash.io:4433",
"endpoint_label": "API endpoint",
"endpoint_message": "Switch this only when Release Manager endpoints are available on the target API.",
"endpoint_aria": "Release Manager control API URL",
"use_tooltip": "Load release data from this API",
"use": "Use",
"reset_tooltip": "Return to the default control API",
"reset": "Reset",
"known_endpoints": "Known control API endpoints"
},
"tabs": {
"overview": {
"label": "Overview",
"description": "Channel health, setup progress, and the latest release state."
},
"channels": {
"label": "Channels",
"description": "Create stable, canary, and targeted channels with rollout limits."
},
"assignments": {
"label": "Assignments",
"description": "Pin users, subusers, or customers to a specific release channel."
},
"deployments": {
"label": "Deployments",
"description": "Trigger deployments and promote successful builds."
},
"replay": {
"label": "Replay",
"description": "Enable targeted capture and search release timeline events."
},
"integrations": {
"label": "Integrations",
"description": "Connect GitHub repositories, branches, and Coolify services."
},
"settings": {
"label": "Settings",
"description": "Configure Release Manager GitHub access and webhook settings."
}
},
"loading": {
"summary": "Loading release manager data...",
"refreshing_summary": "Refreshing release manager data...",
"summary_detail": "Release status, channels, deployments, and service health are being loaded.",
"channels": "Loading release channels...",
"channels_detail": "Channel lanes and release dependencies are being loaded.",
"assignments": "Loading release assignments...",
"deployments": "Loading release deployments...",
"operations": "Loading release operations...",
"data_services": "Loading release data services...",
"integrations": "Loading release integrations...",
"settings": "Loading Release Manager settings...",
"github_repositories": "Loading GitHub repositories...",
"timeline_sessions": "Loading release timeline sessions...",
"timeline_events": "Loading release timeline events...",
"refreshing_timeline": "Refreshing release timeline..."
},
"setup": {
"channels": "Channels",
"targets": "GitHub and Coolify targets",
"assignments": "Pilot assignments",
"deployments": "First deployment",
"replay": "Replay capture"
},
"stats": {
"channels": "Channels",
"targets": "Targets",
"deployments": "Deployments",
"timeline_events": "Timeline events"
},
"overview": {
"title": "Overview",
"subtitle": "Current channel health, release activity, and module status.",
"guided_setup": "Guided setup",
"next_step": "Next: {step}",
"ready": "Release Manager is ready for day-to-day operations.",
"add_suggested_channel": "Add suggested channel",
"module_health_empty": "Module health snapshots appear after probes have been recorded."
},
"status": {
"confirm_issue_action": "Run this Release Manager action? It can change deployment state and will be audited.",
"choose_bundle_prompt": "Enter the bundle id to set for this channel.",
"impact": "Impact",
"cause": "Cause",
"automated_fix": "Automated fix",
"manual_fallback": "Manual fallback",
"related_deployment": "Related deployment",
"recent_result": "Recent result",
"no_automated_action": "No automated action available.",
"deployment": "Deployment",
"target": "Target",
"coolify_target": "Coolify target",
"service_set": "Service set"
},
"settings": {
"title": "Release Manager Settings",
"subtitle": "Configure the GitHub token used for private repositories and branch lookups.",
"github_token": "GitHub token",
"github_api_url": "GitHub API URL",
"webhook_secret": "Webhook secret",
"github_webhook_secret": "GitHub webhook secret",
"configured": "Configured",
"not_configured": "Not configured",
"loaded_from": "Loaded from {variable}",
"set_below": "Set {variable} below",
"private_repositories_prefix": "Private repositories are read with the server environment variable",
"private_repositories_or": "or the module config variable",
"github_token_message": "Example: github_pat_... with access to the private repositories Release Manager deploys.",
"github_token_placeholder": "Leave blank to keep the existing token",
"github_api_url_message": "Configured in ReleaseManager.github_api_url. Use https://api.github.com unless using GitHub Enterprise.",
"webhook_secret_message": "Optional; leave blank to keep the existing secret.",
"webhook_secret_placeholder": "Webhook HMAC secret",
"save": "Save settings",
"back_to_integrations": "Back to integrations",
"guide": {
"steps": {
"github_token": "GitHub token",
"github_api_url": "GitHub API URL",
"github_webhook_secret": "GitHub webhook secret",
"review": "Review"
},
"github_token_title": "Add GitHub token",
"github_token_description": "Release Manager uses this token to list private repositories, branches, and commits before a deployment target is saved or tested.",
"configure_this_in": "Configure this in",
"server_environment_variable": "The server environment variable",
"takes_precedence": "takes precedence when it is set.",
"recommended_access": "Recommended access",
"recommended_access_description": "Fine-grained GitHub token with Contents: read access for each release repository.",
"github_api_url_title": "Add GitHub API URL",
"github_api_url_description": "Use GitHub Cloud by default, or point this at the REST API base URL for a GitHub Enterprise Server installation.",
"github_api_url_invalid": "Enter a full HTTP or HTTPS URL.",
"github_cloud": "GitHub Cloud",
"enterprise_example": "Enterprise example",
"github_webhook_secret_title": "Add GitHub webhook secret",
"github_webhook_secret_description": "Use the same secret in GitHub repository webhooks so Release Manager can verify deployment events before handling them.",
"github_webhook_url": "GitHub webhook URL:",
"store_shared_secret_in": "Store the shared secret in",
"review_title": "Review GitHub setup",
"review_description": "Save the values, then use Integrations to select a private repository, branch, and specific commit or latest commit before testing access.",
"ready": "Ready",
"needs_value": "Needs value"
}
},
"channel_unavailable": {
"kicker": "Release Manager",
"title": "Release channel is not ready",
"summary_prefix": "Your account is assigned to",
"summary_suffix": ", but that channel is missing required release configuration.",
"release_bundle": "Release bundle",
"frontend_version": "Frontend version",
"api_version": "API version",
"frontend_base_url": "Frontend URL",
"api_base_url": "API URL",
"frontend_entry": "Frontend entry",
"release_runtime": "Release runtime",
"checking_again": "Checking again in {seconds}s",
"refresh_error": "Release status could not be refreshed. The next automatic check will try again.",
"ignore": "Ignore next 5 minutes",
"check_again": "Check again",
"logout": "Logout",
"base_image": "Base image"
},
"channel_switched": {
"kicker": "Release Manager",
"title": "You are now on {channel}",
"summary_prefix": "Your account has been assigned to the",
"summary_suffix": "release channel. This device will remember that you have seen this notice.",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"frontend_version": "Frontend version",
"api_version": "API version",
"assigned_channel": "Assigned channel",
"current_app_image": "Current app image",
"current_api": "Current API",
"base_image": "Base image",
"continue": "Continue"
},
"channel_selector": {
"title": "Release channel",
"subtitle": "Choose which assigned release channel this device should use.",
"sidebar_title": "Release",
"default": "Default",
"ready": "Ready",
"unavailable": "Not ready",
"release_bundle": "Release bundle",
"frontend_version": "Frontend version",
"api_version": "API version",
"frontend_base_url": "Frontend URL",
"api_base_url": "API URL",
"frontend_entry": "Frontend entry",
"release_runtime": "Release runtime",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"use_local_frontend": "Use local frontend",
"switch_error": "Release channel could not be switched. The previous channel is still active."
},
"common": {
"yes": "yes",
"no": "no",
"off": "off"
},
"actions": {
"edit": "Edit",
"rollback": "Rollback",
"update": "Update",
"create": "Create",
"publish_release": "Publish release",
"clear": "Clear",
"assign": "Assign",
"remove": "Remove",
"test_access": "Test access",
"deploy": "Deploy",
"redeploy": "Redeploy",
"promote": "Promote",
"enable": "Enable",
"search": "Search",
"save_target": "Save target",
"delete": "Delete",
"back": "Back",
"next": "Next"
},
"tables": {
"channel": "Channel",
"rollout": "Rollout",
"frontend": "Frontend",
"api": "API",
"replay": "Replay",
"actions": "Actions",
"module": "Module",
"status": "Status",
"checked": "Checked",
"subject": "Subject",
"reason": "Reason",
"expires": "Expires",
"deployment": "Deployment",
"source": "Source",
"started": "Started",
"target": "Target",
"coolify": "Coolify",
"auto": "Auto"
},
"values": {
"app": {
"frontend": "frontend",
"api": "api"
},
"capture_level": {
"metadata": "metadata",
"full_redacted": "full_redacted"
},
"subject_type": {
"user": "user",
"subuser": "subuser",
"customer": "customer",
"channel": "channel"
},
"severity": {
"error": "error",
"warning": "warning",
"info": "info"
},
"status": {
"ok": "ok",
"active": "active",
"deployed": "deployed",
"stable": "stable",
"enabled": "enabled",
"queued": "queued",
"deploying": "deploying",
"metadata": "metadata",
"not_configured": "not configured",
"warning": "warning",
"failed": "failed",
"down": "down",
"error": "error",
"unknown": "unknown"
}
},
"autocomplete": {
"suggested_value": "Suggested value",
"suggestion": "Suggestion",
"use_typed_value": "Use typed value",
"custom": "Custom",
"api_url": "API URL",
"frontend_url": "Frontend URL",
"known_release_url": "Known release URL",
"deploy_here_with_coolify": "Deploy {app} here with Coolify",
"prepare_coolify_target": "Prepare a Coolify target with HTTPS/SSL enabled",
"coolify_ssl": "Coolify SSL",
"github_repository": "GitHub repository",
"owner_repository": "owner/repository",
"git_branch": "Git branch",
"deployment_branch": "Deployment branch",
"github_commit": "GitHub commit",
"git_commit": "Git commit",
"use_specific_commit_sha": "Use specific commit SHA",
"custom_commit": "Custom commit",
"health_url": "Health URL",
"release_health_check": "Release health check",
"coolify_service": "Coolify service",
"service_uuid": "Service UUID",
"known_coolify_service_uuid": "Known Coolify service UUID",
"control_api": "Control API",
"release_manager_api_endpoint": "Release Manager API endpoint",
"module": "Module",
"timeline_module_key": "Timeline module key"
},
"errors": {
"github_repositories": "Could not load GitHub repositories.",
"github_branches": "Could not load GitHub branches.",
"repository_branch_required": "Repository and branch are required before testing GitHub access.",
"github_access_failed": "GitHub repository access test failed."
},
"channels": {
"title": "Channels",
"subtitle": "Create and tune gradual release channels.",
"rollout_percent": "{percent}% rollout",
"already_configured": "Already configured",
"slug": "Slug",
"slug_message": "Example: canary",
"slug_placeholder": "slug",
"name": "Name",
"name_message": "Shown to operators when reviewing a channel.",
"name_placeholder": "Name",
"rollout": "Rollout",
"rollout_message": "0-100 percent of unassigned traffic.",
"frontend_url": "Frontend URL",
"frontend_url_message": "Example: https://canary.example.test",
"frontend_url_placeholder": "Frontend URL",
"no_matching_release_url": "No matching release URL.",
"api_url": "API URL",
"api_url_message": "Example: https://api-canary.example.test",
"api_url_placeholder": "API URL",
"no_matching_api_url": "No matching API URL.",
"replay_capture": "Replay capture",
"replay_capture_message": "Metadata is safer for broad channels.",
"retention_days": "Retention days",
"retention_days_message": "1-365 days.",
"enabled": "Enabled",
"default_channel": "Default channel",
"replay_enabled": "Replay enabled",
"frontend_chip": "Frontend: {url}",
"api_chip": "API: {url}"
},
"assignments": {
"title": "Assignments",
"subtitle": "Pin users, subusers, or customers to release channels.",
"subject": "Subject",
"subject_message": "Search users, subusers, or customers, or type a manual subject like user:42.",
"subject_placeholder": "Search or type subject",
"subject_empty": "No subjects found",
"group_users": "Users",
"group_subusers": "Subusers",
"group_customers": "Customers",
"group_manual": "Manual",
"manual_user": "User #{id}",
"manual_subuser": "Subuser #{id}",
"manual_customer": "Customer #{id}",
"manual_description": "Use typed value {subject}",
"subject_type": "Subject type",
"subject_type_message": "Choose the identity type to pin.",
"customer": "Customer",
"customer_message": "Search by customer number or name, then select the exact customer.",
"customer_placeholder": "Search customer",
"subject_id": "Subject ID",
"subject_id_message": "Example: user id 42",
"subject_id_placeholder": "Subject id",
"channel": "Channel",
"channel_message": "Pinned subjects bypass percentage rollout.",
"reason": "Reason",
"reason_message": "Example: canary validation for customer support.",
"reason_placeholder": "Reason",
"expires": "Expires",
"expires_message": "Leave blank for no automatic expiry."
},
"deployments": {
"title": "Deployments",
"subtitle": "Trigger, promote, and inspect release deployments.",
"manual_deployment_record": "Manual deployment record",
"target": "Target",
"target_message": "Pick a saved target or keep manual fields editable.",
"select_target_or_manual": "Select target or manual",
"channel": "Channel",
"app": "App",
"repository": "Repository",
"repository_message": "Example: truckwash/front-end-vue",
"repository_placeholder": "owner/repo",
"branch": "Branch",
"branch_message": "Example: master",
"branch_placeholder": "branch",
"commit_selection": "Commit selection",
"commit_selection_message": "Latest resolves to the branch head with the configured GitHub token.",
"latest_from_branch": "Latest from branch",
"specific_commit": "Specific commit",
"specific_commit_message": "Example: feedface1234",
"commit_sha_placeholder": "commit sha",
"version_label": "Version label",
"version_label_message": "Example: api-2026.05.19",
"version_label_placeholder": "version label",
"commit_prefix": "Commit: {commit}",
"test_access_tooltip": "Verify token access to the selected repository, branch, and commit."
},
"replay": {
"title": "Replay",
"subtitle": "Enable targeted capture and step through failure timelines.",
"capture_channel": "Capture {channel}",
"target_type": "Target type",
"target": "Target",
"target_message": "Example: user id 42 or channel slug canary.",
"target_placeholder": "Target id or channel slug",
"channel_scope": "Channel scope",
"channel_scope_message": "Optional; limits replay capture to a channel.",
"no_channel_scope": "No channel scope",
"capture_level": "Capture level",
"expires": "Expires",
"expires_message": "Leave blank for manual disable.",
"trace_id": "Trace ID",
"trace_id_message": "Example: trace-canary-1",
"trace_id_placeholder": "Trace id",
"channel": "Channel",
"channel_placeholder": "Channel",
"principal_id": "Principal ID",
"principal_id_placeholder": "Principal id",
"module": "Module",
"module_placeholder": "Module",
"severity": "Severity",
"any_severity": "Any severity",
"no_timeline_events": "No timeline events match the current filters."
}
}
}
}
}
@@ -98,7 +98,25 @@
"view_lane_new_tab": "@:{'phrases.compat.admin.pos.settings_wheel.view_lane_new_tab'}",
"view_lane_setup_new_tab": "@:{'phrases.compat.admin.pos.settings_wheel.view_lane_setup_new_tab'}",
"view_order_new_tab": "@:{'phrases.compat.admin.pos.settings_wheel.view_order_new_tab'}",
"view_vehicle_new_tab": "@:{'phrases.compat.admin.pos.settings_wheel.view_vehicle_new_tab'}"
"view_vehicle_new_tab": "@:{'phrases.compat.admin.pos.settings_wheel.view_vehicle_new_tab'}",
"accept_self_serve_wash": "@:{'phrases.compat.admin.pos.settings_wheel.accept_self_serve_wash'}",
"accept_self_serve_wash_confirm": "@:{'phrases.compat.admin.pos.settings_wheel.accept_self_serve_wash_confirm'}",
"accept_self_serve_wash_success": "@:{'phrases.compat.admin.pos.settings_wheel.accept_self_serve_wash_success'}",
"email_notifications_section": "@:{'phrases.compat.admin.pos.settings_wheel.email_notifications_section'}",
"resend_booking_completion_confirmation": "@:{'phrases.compat.admin.pos.settings_wheel.resend_booking_completion_confirmation'}",
"resend_booking_completion_confirmation_error": "@:{'phrases.compat.admin.pos.settings_wheel.resend_booking_completion_confirmation_error'}",
"resend_booking_completion_confirmation_success": "@:{'phrases.compat.admin.pos.settings_wheel.resend_booking_completion_confirmation_success'}",
"resend_booking_confirmation": "@:{'phrases.compat.admin.pos.settings_wheel.resend_booking_confirmation'}",
"resend_booking_confirmation_error": "@:{'phrases.compat.admin.pos.settings_wheel.resend_booking_confirmation_error'}",
"resend_booking_confirmation_success": "@:{'phrases.compat.admin.pos.settings_wheel.resend_booking_confirmation_success'}",
"resend_wash_certificate": "@:{'phrases.compat.admin.pos.settings_wheel.resend_wash_certificate'}",
"resend_wash_certificate_error": "@:{'phrases.compat.admin.pos.settings_wheel.resend_wash_certificate_error'}",
"resend_wash_certificate_success": "@:{'phrases.compat.admin.pos.settings_wheel.resend_wash_certificate_success'}",
"self_serve_customer": "@:{'phrases.compat.admin.pos.settings_wheel.self_serve_customer'}",
"self_serve_driver": "@:{'phrases.compat.admin.pos.settings_wheel.self_serve_driver'}",
"self_serve_elapsed": "@:{'phrases.compat.admin.pos.settings_wheel.self_serve_elapsed'}",
"self_serve_wash_attachment": "@:{'phrases.compat.admin.pos.settings_wheel.self_serve_wash_attachment'}",
"self_serve_wash_attachment_for_customer": "@:{'phrases.compat.admin.pos.settings_wheel.self_serve_wash_attachment_for_customer'}"
},
"show_details": "@:{'phrases.compat.admin.pos.show_details'}",
"show_order_details": "@:{'phrases.compat.admin.pos.show_order_details'}",
@@ -351,473 +351,6 @@
"title": "@:{'phrases.compat.configuration.xlvask.title'}",
"username": "@:auth.username",
"username_desc": "@:{'phrases.compat.configuration.xlvask.username_desc'}"
},
"release_manager": {
"title": "@:{'phrases.compat.configuration.release_manager.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.subtitle'}",
"channel_names": {
"stable": "@:{'phrases.compat.configuration.release_manager.channel_names.stable'}",
"canary": "@:{'phrases.compat.configuration.release_manager.channel_names.canary'}",
"internal": "@:{'phrases.compat.configuration.release_manager.channel_names.internal'}"
},
"channel_descriptions": {
"stable": "@:{'phrases.compat.configuration.release_manager.channel_descriptions.stable'}",
"canary": "@:{'phrases.compat.configuration.release_manager.channel_descriptions.canary'}",
"internal": "@:{'phrases.compat.configuration.release_manager.channel_descriptions.internal'}"
},
"control_api": {
"title": "@:{'phrases.compat.configuration.release_manager.control_api.title'}",
"tooltip": "@:{'phrases.compat.configuration.release_manager.control_api.tooltip'}",
"example": "@:{'phrases.compat.configuration.release_manager.control_api.example'}",
"endpoint_label": "@:{'phrases.compat.configuration.release_manager.control_api.endpoint_label'}",
"endpoint_message": "@:{'phrases.compat.configuration.release_manager.control_api.endpoint_message'}",
"endpoint_aria": "@:{'phrases.compat.configuration.release_manager.control_api.endpoint_aria'}",
"use_tooltip": "@:{'phrases.compat.configuration.release_manager.control_api.use_tooltip'}",
"use": "@:{'phrases.compat.configuration.release_manager.control_api.use'}",
"reset_tooltip": "@:{'phrases.compat.configuration.release_manager.control_api.reset_tooltip'}",
"reset": "@:{'phrases.compat.configuration.release_manager.control_api.reset'}",
"known_endpoints": "@:{'phrases.compat.configuration.release_manager.control_api.known_endpoints'}"
},
"tabs": {
"overview": {
"label": "@:{'phrases.compat.configuration.release_manager.tabs.overview.label'}",
"description": "@:{'phrases.compat.configuration.release_manager.tabs.overview.description'}"
},
"channels": {
"label": "@:{'phrases.compat.configuration.release_manager.tabs.channels.label'}",
"description": "@:{'phrases.compat.configuration.release_manager.tabs.channels.description'}"
},
"assignments": {
"label": "@:{'phrases.compat.configuration.release_manager.tabs.assignments.label'}",
"description": "@:{'phrases.compat.configuration.release_manager.tabs.assignments.description'}"
},
"deployments": {
"label": "@:{'phrases.compat.configuration.release_manager.tabs.deployments.label'}",
"description": "@:{'phrases.compat.configuration.release_manager.tabs.deployments.description'}"
},
"replay": {
"label": "@:{'phrases.compat.configuration.release_manager.tabs.replay.label'}",
"description": "@:{'phrases.compat.configuration.release_manager.tabs.replay.description'}"
},
"integrations": {
"label": "@:{'phrases.compat.configuration.release_manager.tabs.integrations.label'}",
"description": "@:{'phrases.compat.configuration.release_manager.tabs.integrations.description'}"
},
"settings": {
"label": "@:{'phrases.compat.configuration.release_manager.tabs.settings.label'}",
"description": "@:{'phrases.compat.configuration.release_manager.tabs.settings.description'}"
}
},
"setup": {
"channels": "@:{'phrases.compat.configuration.release_manager.setup.channels'}",
"targets": "@:{'phrases.compat.configuration.release_manager.setup.targets'}",
"assignments": "@:{'phrases.compat.configuration.release_manager.setup.assignments'}",
"deployments": "@:{'phrases.compat.configuration.release_manager.setup.deployments'}",
"replay": "@:{'phrases.compat.configuration.release_manager.setup.replay'}"
},
"stats": {
"channels": "@:{'phrases.compat.configuration.release_manager.stats.channels'}",
"targets": "@:{'phrases.compat.configuration.release_manager.stats.targets'}",
"deployments": "@:{'phrases.compat.configuration.release_manager.stats.deployments'}",
"timeline_events": "@:{'phrases.compat.configuration.release_manager.stats.timeline_events'}"
},
"overview": {
"title": "@:{'phrases.compat.configuration.release_manager.overview.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.overview.subtitle'}",
"guided_setup": "@:{'phrases.compat.configuration.release_manager.overview.guided_setup'}",
"next_step": "@:{'phrases.compat.configuration.release_manager.overview.next_step'}",
"ready": "@:{'phrases.compat.configuration.release_manager.overview.ready'}",
"add_suggested_channel": "@:{'phrases.compat.configuration.release_manager.overview.add_suggested_channel'}",
"module_health_empty": "@:{'phrases.compat.configuration.release_manager.overview.module_health_empty'}"
},
"settings": {
"title": "@:{'phrases.compat.configuration.release_manager.settings.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.settings.subtitle'}",
"github_token": "@:{'phrases.compat.configuration.release_manager.settings.github_token'}",
"github_api_url": "@:{'phrases.compat.configuration.release_manager.settings.github_api_url'}",
"webhook_secret": "@:{'phrases.compat.configuration.release_manager.settings.webhook_secret'}",
"github_webhook_secret": "@:{'phrases.compat.configuration.release_manager.settings.github_webhook_secret'}",
"configured": "@:{'phrases.compat.configuration.release_manager.settings.configured'}",
"not_configured": "@:{'phrases.compat.configuration.release_manager.settings.not_configured'}",
"loaded_from": "@:{'phrases.compat.configuration.release_manager.settings.loaded_from'}",
"set_below": "@:{'phrases.compat.configuration.release_manager.settings.set_below'}",
"private_repositories_prefix": "@:{'phrases.compat.configuration.release_manager.settings.private_repositories_prefix'}",
"private_repositories_or": "@:{'phrases.compat.configuration.release_manager.settings.private_repositories_or'}",
"github_token_message": "@:{'phrases.compat.configuration.release_manager.settings.github_token_message'}",
"github_token_placeholder": "@:{'phrases.compat.configuration.release_manager.settings.github_token_placeholder'}",
"github_api_url_message": "@:{'phrases.compat.configuration.release_manager.settings.github_api_url_message'}",
"webhook_secret_message": "@:{'phrases.compat.configuration.release_manager.settings.webhook_secret_message'}",
"webhook_secret_placeholder": "@:{'phrases.compat.configuration.release_manager.settings.webhook_secret_placeholder'}",
"save": "@:{'phrases.compat.configuration.release_manager.settings.save'}",
"back_to_integrations": "@:{'phrases.compat.configuration.release_manager.settings.back_to_integrations'}",
"guide": {
"steps": {
"github_token": "@:{'phrases.compat.configuration.release_manager.settings.guide.steps.github_token'}",
"github_api_url": "@:{'phrases.compat.configuration.release_manager.settings.guide.steps.github_api_url'}",
"github_webhook_secret": "@:{'phrases.compat.configuration.release_manager.settings.guide.steps.github_webhook_secret'}",
"review": "@:{'phrases.compat.configuration.release_manager.settings.guide.steps.review'}"
},
"github_token_title": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_token_title'}",
"github_token_description": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_token_description'}",
"configure_this_in": "@:{'phrases.compat.configuration.release_manager.settings.guide.configure_this_in'}",
"server_environment_variable": "@:{'phrases.compat.configuration.release_manager.settings.guide.server_environment_variable'}",
"takes_precedence": "@:{'phrases.compat.configuration.release_manager.settings.guide.takes_precedence'}",
"recommended_access": "@:{'phrases.compat.configuration.release_manager.settings.guide.recommended_access'}",
"recommended_access_description": "@:{'phrases.compat.configuration.release_manager.settings.guide.recommended_access_description'}",
"github_api_url_title": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_api_url_title'}",
"github_api_url_description": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_api_url_description'}",
"github_api_url_invalid": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_api_url_invalid'}",
"github_cloud": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_cloud'}",
"enterprise_example": "@:{'phrases.compat.configuration.release_manager.settings.guide.enterprise_example'}",
"github_webhook_secret_title": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_webhook_secret_title'}",
"github_webhook_secret_description": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_webhook_secret_description'}",
"github_webhook_url": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_webhook_url'}",
"store_shared_secret_in": "@:{'phrases.compat.configuration.release_manager.settings.guide.store_shared_secret_in'}",
"review_title": "@:{'phrases.compat.configuration.release_manager.settings.guide.review_title'}",
"review_description": "@:{'phrases.compat.configuration.release_manager.settings.guide.review_description'}",
"ready": "@:{'phrases.compat.configuration.release_manager.settings.guide.ready'}",
"needs_value": "@:{'phrases.compat.configuration.release_manager.settings.guide.needs_value'}"
}
},
"channel_unavailable": {
"kicker": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.kicker'}",
"title": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.title'}",
"summary_prefix": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.summary_prefix'}",
"summary_suffix": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.summary_suffix'}",
"release_bundle": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.release_bundle'}",
"frontend_version": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.frontend_version'}",
"api_version": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.api_version'}",
"frontend_base_url": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.frontend_base_url'}",
"api_base_url": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.api_base_url'}",
"frontend_entry": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.frontend_entry'}",
"release_runtime": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.release_runtime'}",
"checking_again": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.checking_again'}",
"refresh_error": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.refresh_error'}",
"ignore": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.ignore'}",
"check_again": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.check_again'}",
"logout": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.logout'}",
"base_image": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.base_image'}"
},
"channel_switched": {
"kicker": "@:{'phrases.compat.configuration.release_manager.channel_switched.kicker'}",
"title": "@:{'phrases.compat.configuration.release_manager.channel_switched.title'}",
"summary_prefix": "@:{'phrases.compat.configuration.release_manager.channel_switched.summary_prefix'}",
"summary_suffix": "@:{'phrases.compat.configuration.release_manager.channel_switched.summary_suffix'}",
"bundle": "@:{'phrases.compat.configuration.release_manager.channel_switched.bundle'}",
"frontend": "@:{'phrases.compat.configuration.release_manager.channel_switched.frontend'}",
"api": "@:{'phrases.compat.configuration.release_manager.channel_switched.api'}",
"frontend_version": "@:{'phrases.compat.configuration.release_manager.channel_switched.frontend_version'}",
"api_version": "@:{'phrases.compat.configuration.release_manager.channel_switched.api_version'}",
"assigned_channel": "@:{'phrases.compat.configuration.release_manager.channel_switched.assigned_channel'}",
"current_app_image": "@:{'phrases.compat.configuration.release_manager.channel_switched.current_app_image'}",
"current_api": "@:{'phrases.compat.configuration.release_manager.channel_switched.current_api'}",
"base_image": "@:{'phrases.compat.configuration.release_manager.channel_switched.base_image'}",
"continue": "@:{'phrases.compat.configuration.release_manager.channel_switched.continue'}"
},
"channel_selector": {
"title": "@:{'phrases.compat.configuration.release_manager.channel_selector.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.channel_selector.subtitle'}",
"sidebar_title": "@:{'phrases.compat.configuration.release_manager.channel_selector.sidebar_title'}",
"default": "@:{'phrases.compat.configuration.release_manager.channel_selector.default'}",
"ready": "@:{'phrases.compat.configuration.release_manager.channel_selector.ready'}",
"unavailable": "@:{'phrases.compat.configuration.release_manager.channel_selector.unavailable'}",
"release_bundle": "@:{'phrases.compat.configuration.release_manager.channel_selector.release_bundle'}",
"frontend_version": "@:{'phrases.compat.configuration.release_manager.channel_selector.frontend_version'}",
"api_version": "@:{'phrases.compat.configuration.release_manager.channel_selector.api_version'}",
"frontend_base_url": "@:{'phrases.compat.configuration.release_manager.channel_selector.frontend_base_url'}",
"api_base_url": "@:{'phrases.compat.configuration.release_manager.channel_selector.api_base_url'}",
"frontend_entry": "@:{'phrases.compat.configuration.release_manager.channel_selector.frontend_entry'}",
"release_runtime": "@:{'phrases.compat.configuration.release_manager.channel_selector.release_runtime'}",
"bundle": "@:{'phrases.compat.configuration.release_manager.channel_selector.bundle'}",
"frontend": "@:{'phrases.compat.configuration.release_manager.channel_selector.frontend'}",
"api": "@:{'phrases.compat.configuration.release_manager.channel_selector.api'}",
"use_local_frontend": "@:{'phrases.compat.configuration.release_manager.channel_selector.use_local_frontend'}"
},
"common": {
"yes": "@:{'phrases.compat.configuration.release_manager.common.yes'}",
"no": "@:{'phrases.compat.configuration.release_manager.common.no'}",
"off": "@:{'phrases.compat.configuration.release_manager.common.off'}"
},
"actions": {
"edit": "@:{'phrases.compat.configuration.release_manager.actions.edit'}",
"rollback": "@:{'phrases.compat.configuration.release_manager.actions.rollback'}",
"update": "@:{'phrases.compat.configuration.release_manager.actions.update'}",
"create": "@:{'phrases.compat.configuration.release_manager.actions.create'}",
"clear": "@:{'phrases.compat.configuration.release_manager.actions.clear'}",
"assign": "@:{'phrases.compat.configuration.release_manager.actions.assign'}",
"remove": "@:{'phrases.compat.configuration.release_manager.actions.remove'}",
"test_access": "@:{'phrases.compat.configuration.release_manager.actions.test_access'}",
"deploy": "@:{'phrases.compat.configuration.release_manager.actions.deploy'}",
"redeploy": "@:{'phrases.compat.configuration.release_manager.actions.redeploy'}",
"promote": "@:{'phrases.compat.configuration.release_manager.actions.promote'}",
"enable": "@:{'phrases.compat.configuration.release_manager.actions.enable'}",
"search": "@:{'phrases.compat.configuration.release_manager.actions.search'}",
"save_target": "@:{'phrases.compat.configuration.release_manager.actions.save_target'}",
"delete": "@:{'phrases.compat.configuration.release_manager.actions.delete'}",
"back": "@:{'phrases.compat.configuration.release_manager.actions.back'}",
"next": "@:{'phrases.compat.configuration.release_manager.actions.next'}"
},
"tables": {
"channel": "@:{'phrases.compat.configuration.release_manager.tables.channel'}",
"rollout": "@:{'phrases.compat.configuration.release_manager.tables.rollout'}",
"frontend": "@:{'phrases.compat.configuration.release_manager.tables.frontend'}",
"api": "@:{'phrases.compat.configuration.release_manager.tables.api'}",
"replay": "@:{'phrases.compat.configuration.release_manager.tables.replay'}",
"actions": "@:{'phrases.compat.configuration.release_manager.tables.actions'}",
"module": "@:{'phrases.compat.configuration.release_manager.tables.module'}",
"status": "@:{'phrases.compat.configuration.release_manager.tables.status'}",
"checked": "@:{'phrases.compat.configuration.release_manager.tables.checked'}",
"subject": "@:{'phrases.compat.configuration.release_manager.tables.subject'}",
"reason": "@:{'phrases.compat.configuration.release_manager.tables.reason'}",
"expires": "@:{'phrases.compat.configuration.release_manager.tables.expires'}",
"deployment": "@:{'phrases.compat.configuration.release_manager.tables.deployment'}",
"source": "@:{'phrases.compat.configuration.release_manager.tables.source'}",
"started": "@:{'phrases.compat.configuration.release_manager.tables.started'}",
"target": "@:{'phrases.compat.configuration.release_manager.tables.target'}",
"coolify": "@:{'phrases.compat.configuration.release_manager.tables.coolify'}",
"auto": "@:{'phrases.compat.configuration.release_manager.tables.auto'}"
},
"values": {
"app": {
"frontend": "@:{'phrases.compat.configuration.release_manager.values.app.frontend'}",
"api": "@:{'phrases.compat.configuration.release_manager.values.app.api'}"
},
"capture_level": {
"metadata": "@:{'phrases.compat.configuration.release_manager.values.capture_level.metadata'}",
"full_redacted": "@:{'phrases.compat.configuration.release_manager.values.capture_level.full_redacted'}"
},
"subject_type": {
"user": "@:{'phrases.compat.configuration.release_manager.values.subject_type.user'}",
"subuser": "@:{'phrases.compat.configuration.release_manager.values.subject_type.subuser'}",
"customer": "@:{'phrases.compat.configuration.release_manager.values.subject_type.customer'}",
"channel": "@:{'phrases.compat.configuration.release_manager.values.subject_type.channel'}"
},
"severity": {
"error": "@:{'phrases.compat.configuration.release_manager.values.severity.error'}",
"warning": "@:{'phrases.compat.configuration.release_manager.values.severity.warning'}",
"info": "@:{'phrases.compat.configuration.release_manager.values.severity.info'}"
},
"status": {
"ok": "@:{'phrases.compat.configuration.release_manager.values.status.ok'}",
"active": "@:{'phrases.compat.configuration.release_manager.values.status.active'}",
"deployed": "@:{'phrases.compat.configuration.release_manager.values.status.deployed'}",
"stable": "@:{'phrases.compat.configuration.release_manager.values.status.stable'}",
"enabled": "@:{'phrases.compat.configuration.release_manager.values.status.enabled'}",
"queued": "@:{'phrases.compat.configuration.release_manager.values.status.queued'}",
"deploying": "@:{'phrases.compat.configuration.release_manager.values.status.deploying'}",
"metadata": "@:{'phrases.compat.configuration.release_manager.values.status.metadata'}",
"not_configured": "@:{'phrases.compat.configuration.release_manager.values.status.not_configured'}",
"warning": "@:{'phrases.compat.configuration.release_manager.values.status.warning'}",
"failed": "@:{'phrases.compat.configuration.release_manager.values.status.failed'}",
"down": "@:{'phrases.compat.configuration.release_manager.values.status.down'}",
"error": "@:{'phrases.compat.configuration.release_manager.values.status.error'}",
"unknown": "@:{'phrases.compat.configuration.release_manager.values.status.unknown'}"
}
},
"autocomplete": {
"suggested_value": "@:{'phrases.compat.configuration.release_manager.autocomplete.suggested_value'}",
"suggestion": "@:{'phrases.compat.configuration.release_manager.autocomplete.suggestion'}",
"use_typed_value": "@:{'phrases.compat.configuration.release_manager.autocomplete.use_typed_value'}",
"custom": "@:{'phrases.compat.configuration.release_manager.autocomplete.custom'}",
"api_url": "@:{'phrases.compat.configuration.release_manager.autocomplete.api_url'}",
"frontend_url": "@:{'phrases.compat.configuration.release_manager.autocomplete.frontend_url'}",
"known_release_url": "@:{'phrases.compat.configuration.release_manager.autocomplete.known_release_url'}",
"deploy_here_with_coolify": "@:{'phrases.compat.configuration.release_manager.autocomplete.deploy_here_with_coolify'}",
"prepare_coolify_target": "@:{'phrases.compat.configuration.release_manager.autocomplete.prepare_coolify_target'}",
"coolify_ssl": "@:{'phrases.compat.configuration.release_manager.autocomplete.coolify_ssl'}",
"github_repository": "@:{'phrases.compat.configuration.release_manager.autocomplete.github_repository'}",
"owner_repository": "@:{'phrases.compat.configuration.release_manager.autocomplete.owner_repository'}",
"git_branch": "@:{'phrases.compat.configuration.release_manager.autocomplete.git_branch'}",
"deployment_branch": "@:{'phrases.compat.configuration.release_manager.autocomplete.deployment_branch'}",
"github_commit": "@:{'phrases.compat.configuration.release_manager.autocomplete.github_commit'}",
"git_commit": "@:{'phrases.compat.configuration.release_manager.autocomplete.git_commit'}",
"use_specific_commit_sha": "@:{'phrases.compat.configuration.release_manager.autocomplete.use_specific_commit_sha'}",
"custom_commit": "@:{'phrases.compat.configuration.release_manager.autocomplete.custom_commit'}",
"health_url": "@:{'phrases.compat.configuration.release_manager.autocomplete.health_url'}",
"release_health_check": "@:{'phrases.compat.configuration.release_manager.autocomplete.release_health_check'}",
"coolify_service": "@:{'phrases.compat.configuration.release_manager.autocomplete.coolify_service'}",
"service_uuid": "@:{'phrases.compat.configuration.release_manager.autocomplete.service_uuid'}",
"known_coolify_service_uuid": "@:{'phrases.compat.configuration.release_manager.autocomplete.known_coolify_service_uuid'}",
"control_api": "@:{'phrases.compat.configuration.release_manager.autocomplete.control_api'}",
"release_manager_api_endpoint": "@:{'phrases.compat.configuration.release_manager.autocomplete.release_manager_api_endpoint'}",
"module": "@:{'phrases.compat.configuration.release_manager.autocomplete.module'}",
"timeline_module_key": "@:{'phrases.compat.configuration.release_manager.autocomplete.timeline_module_key'}"
},
"errors": {
"github_repositories": "@:{'phrases.compat.configuration.release_manager.errors.github_repositories'}",
"github_branches": "@:{'phrases.compat.configuration.release_manager.errors.github_branches'}",
"repository_branch_required": "@:{'phrases.compat.configuration.release_manager.errors.repository_branch_required'}",
"github_access_failed": "@:{'phrases.compat.configuration.release_manager.errors.github_access_failed'}"
},
"channels": {
"title": "@:{'phrases.compat.configuration.release_manager.channels.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.channels.subtitle'}",
"rollout_percent": "@:{'phrases.compat.configuration.release_manager.channels.rollout_percent'}",
"already_configured": "@:{'phrases.compat.configuration.release_manager.channels.already_configured'}",
"slug": "@:{'phrases.compat.configuration.release_manager.channels.slug'}",
"slug_message": "@:{'phrases.compat.configuration.release_manager.channels.slug_message'}",
"slug_placeholder": "@:{'phrases.compat.configuration.release_manager.channels.slug_placeholder'}",
"name": "@:{'phrases.compat.configuration.release_manager.channels.name'}",
"name_message": "@:{'phrases.compat.configuration.release_manager.channels.name_message'}",
"name_placeholder": "@:{'phrases.compat.configuration.release_manager.channels.name_placeholder'}",
"rollout": "@:{'phrases.compat.configuration.release_manager.channels.rollout'}",
"rollout_message": "@:{'phrases.compat.configuration.release_manager.channels.rollout_message'}",
"frontend_url": "@:{'phrases.compat.configuration.release_manager.channels.frontend_url'}",
"frontend_url_message": "@:{'phrases.compat.configuration.release_manager.channels.frontend_url_message'}",
"frontend_url_placeholder": "@:{'phrases.compat.configuration.release_manager.channels.frontend_url_placeholder'}",
"no_matching_release_url": "@:{'phrases.compat.configuration.release_manager.channels.no_matching_release_url'}",
"api_url": "@:{'phrases.compat.configuration.release_manager.channels.api_url'}",
"api_url_message": "@:{'phrases.compat.configuration.release_manager.channels.api_url_message'}",
"api_url_placeholder": "@:{'phrases.compat.configuration.release_manager.channels.api_url_placeholder'}",
"no_matching_api_url": "@:{'phrases.compat.configuration.release_manager.channels.no_matching_api_url'}",
"replay_capture": "@:{'phrases.compat.configuration.release_manager.channels.replay_capture'}",
"replay_capture_message": "@:{'phrases.compat.configuration.release_manager.channels.replay_capture_message'}",
"retention_days": "@:{'phrases.compat.configuration.release_manager.channels.retention_days'}",
"retention_days_message": "@:{'phrases.compat.configuration.release_manager.channels.retention_days_message'}",
"enabled": "@:{'phrases.compat.configuration.release_manager.channels.enabled'}",
"default_channel": "@:{'phrases.compat.configuration.release_manager.channels.default_channel'}",
"replay_enabled": "@:{'phrases.compat.configuration.release_manager.channels.replay_enabled'}",
"frontend_chip": "@:{'phrases.compat.configuration.release_manager.channels.frontend_chip'}",
"api_chip": "@:{'phrases.compat.configuration.release_manager.channels.api_chip'}"
},
"assignments": {
"title": "@:{'phrases.compat.configuration.release_manager.assignments.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.assignments.subtitle'}",
"subject": "@:{'phrases.compat.configuration.release_manager.assignments.subject'}",
"subject_message": "@:{'phrases.compat.configuration.release_manager.assignments.subject_message'}",
"subject_placeholder": "@:{'phrases.compat.configuration.release_manager.assignments.subject_placeholder'}",
"subject_empty": "@:{'phrases.compat.configuration.release_manager.assignments.subject_empty'}",
"group_users": "@:{'phrases.compat.configuration.release_manager.assignments.group_users'}",
"group_subusers": "@:{'phrases.compat.configuration.release_manager.assignments.group_subusers'}",
"group_customers": "@:{'phrases.compat.configuration.release_manager.assignments.group_customers'}",
"group_manual": "@:{'phrases.compat.configuration.release_manager.assignments.group_manual'}",
"manual_user": "@:{'phrases.compat.configuration.release_manager.assignments.manual_user'}",
"manual_subuser": "@:{'phrases.compat.configuration.release_manager.assignments.manual_subuser'}",
"manual_customer": "@:{'phrases.compat.configuration.release_manager.assignments.manual_customer'}",
"manual_description": "@:{'phrases.compat.configuration.release_manager.assignments.manual_description'}",
"subject_type": "@:{'phrases.compat.configuration.release_manager.assignments.subject_type'}",
"subject_type_message": "@:{'phrases.compat.configuration.release_manager.assignments.subject_type_message'}",
"customer": "@:{'phrases.compat.configuration.release_manager.assignments.customer'}",
"customer_message": "@:{'phrases.compat.configuration.release_manager.assignments.customer_message'}",
"customer_placeholder": "@:{'phrases.compat.configuration.release_manager.assignments.customer_placeholder'}",
"subject_id": "@:{'phrases.compat.configuration.release_manager.assignments.subject_id'}",
"subject_id_message": "@:{'phrases.compat.configuration.release_manager.assignments.subject_id_message'}",
"subject_id_placeholder": "@:{'phrases.compat.configuration.release_manager.assignments.subject_id_placeholder'}",
"channel": "@:{'phrases.compat.configuration.release_manager.assignments.channel'}",
"channel_message": "@:{'phrases.compat.configuration.release_manager.assignments.channel_message'}",
"reason": "@:{'phrases.compat.configuration.release_manager.assignments.reason'}",
"reason_message": "@:{'phrases.compat.configuration.release_manager.assignments.reason_message'}",
"reason_placeholder": "@:{'phrases.compat.configuration.release_manager.assignments.reason_placeholder'}",
"expires": "@:{'phrases.compat.configuration.release_manager.assignments.expires'}",
"expires_message": "@:{'phrases.compat.configuration.release_manager.assignments.expires_message'}"
},
"deployments": {
"title": "@:{'phrases.compat.configuration.release_manager.deployments.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.deployments.subtitle'}",
"manual_deployment_record": "@:{'phrases.compat.configuration.release_manager.deployments.manual_deployment_record'}",
"target": "@:{'phrases.compat.configuration.release_manager.deployments.target'}",
"target_message": "@:{'phrases.compat.configuration.release_manager.deployments.target_message'}",
"select_target_or_manual": "@:{'phrases.compat.configuration.release_manager.deployments.select_target_or_manual'}",
"channel": "@:{'phrases.compat.configuration.release_manager.deployments.channel'}",
"app": "@:{'phrases.compat.configuration.release_manager.deployments.app'}",
"repository": "@:{'phrases.compat.configuration.release_manager.deployments.repository'}",
"repository_message": "@:{'phrases.compat.configuration.release_manager.deployments.repository_message'}",
"repository_placeholder": "@:{'phrases.compat.configuration.release_manager.deployments.repository_placeholder'}",
"branch": "@:{'phrases.compat.configuration.release_manager.deployments.branch'}",
"branch_message": "@:{'phrases.compat.configuration.release_manager.deployments.branch_message'}",
"branch_placeholder": "@:{'phrases.compat.configuration.release_manager.deployments.branch_placeholder'}",
"commit_selection": "@:{'phrases.compat.configuration.release_manager.deployments.commit_selection'}",
"commit_selection_message": "@:{'phrases.compat.configuration.release_manager.deployments.commit_selection_message'}",
"latest_from_branch": "@:{'phrases.compat.configuration.release_manager.deployments.latest_from_branch'}",
"specific_commit": "@:{'phrases.compat.configuration.release_manager.deployments.specific_commit'}",
"specific_commit_message": "@:{'phrases.compat.configuration.release_manager.deployments.specific_commit_message'}",
"commit_sha_placeholder": "@:{'phrases.compat.configuration.release_manager.deployments.commit_sha_placeholder'}",
"version_label": "@:{'phrases.compat.configuration.release_manager.deployments.version_label'}",
"version_label_message": "@:{'phrases.compat.configuration.release_manager.deployments.version_label_message'}",
"version_label_placeholder": "@:{'phrases.compat.configuration.release_manager.deployments.version_label_placeholder'}",
"commit_prefix": "@:{'phrases.compat.configuration.release_manager.deployments.commit_prefix'}",
"test_access_tooltip": "@:{'phrases.compat.configuration.release_manager.deployments.test_access_tooltip'}"
},
"replay": {
"title": "@:{'phrases.compat.configuration.release_manager.replay.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.replay.subtitle'}",
"capture_channel": "@:{'phrases.compat.configuration.release_manager.replay.capture_channel'}",
"target_type": "@:{'phrases.compat.configuration.release_manager.replay.target_type'}",
"target": "@:{'phrases.compat.configuration.release_manager.replay.target'}",
"target_message": "@:{'phrases.compat.configuration.release_manager.replay.target_message'}",
"target_placeholder": "@:{'phrases.compat.configuration.release_manager.replay.target_placeholder'}",
"channel_scope": "@:{'phrases.compat.configuration.release_manager.replay.channel_scope'}",
"channel_scope_message": "@:{'phrases.compat.configuration.release_manager.replay.channel_scope_message'}",
"no_channel_scope": "@:{'phrases.compat.configuration.release_manager.replay.no_channel_scope'}",
"capture_level": "@:{'phrases.compat.configuration.release_manager.replay.capture_level'}",
"expires": "@:{'phrases.compat.configuration.release_manager.replay.expires'}",
"expires_message": "@:{'phrases.compat.configuration.release_manager.replay.expires_message'}",
"trace_id": "@:{'phrases.compat.configuration.release_manager.replay.trace_id'}",
"trace_id_message": "@:{'phrases.compat.configuration.release_manager.replay.trace_id_message'}",
"trace_id_placeholder": "@:{'phrases.compat.configuration.release_manager.replay.trace_id_placeholder'}",
"channel": "@:{'phrases.compat.configuration.release_manager.replay.channel'}",
"channel_placeholder": "@:{'phrases.compat.configuration.release_manager.replay.channel_placeholder'}",
"principal_id": "@:{'phrases.compat.configuration.release_manager.replay.principal_id'}",
"principal_id_placeholder": "@:{'phrases.compat.configuration.release_manager.replay.principal_id_placeholder'}",
"module": "@:{'phrases.compat.configuration.release_manager.replay.module'}",
"module_placeholder": "@:{'phrases.compat.configuration.release_manager.replay.module_placeholder'}",
"severity": "@:{'phrases.compat.configuration.release_manager.replay.severity'}",
"any_severity": "@:{'phrases.compat.configuration.release_manager.replay.any_severity'}",
"no_timeline_events": "@:{'phrases.compat.configuration.release_manager.replay.no_timeline_events'}"
},
"integrations": {
"title": "@:{'phrases.compat.configuration.release_manager.integrations.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.integrations.subtitle'}",
"choose_repository": "@:{'phrases.compat.configuration.release_manager.integrations.choose_repository'}",
"add_health_url": "@:{'phrases.compat.configuration.release_manager.integrations.add_health_url'}",
"github_token_not_configured": "@:{'phrases.compat.configuration.release_manager.integrations.github_token_not_configured'}",
"open_github_token_settings": "@:{'phrases.compat.configuration.release_manager.integrations.open_github_token_settings'}",
"channel": "@:{'phrases.compat.configuration.release_manager.integrations.channel'}",
"app": "@:{'phrases.compat.configuration.release_manager.integrations.app'}",
"repository": "@:{'phrases.compat.configuration.release_manager.integrations.repository'}",
"repository_message": "@:{'phrases.compat.configuration.release_manager.integrations.repository_message'}",
"repository_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.repository_placeholder'}",
"branch": "@:{'phrases.compat.configuration.release_manager.integrations.branch'}",
"branch_message": "@:{'phrases.compat.configuration.release_manager.integrations.branch_message'}",
"branch_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.branch_placeholder'}",
"latest_prefix": "@:{'phrases.compat.configuration.release_manager.integrations.latest_prefix'}",
"coolify_instance": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_instance'}",
"coolify_instance_message": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_instance_message'}",
"no_coolify_instance": "@:{'phrases.compat.configuration.release_manager.integrations.no_coolify_instance'}",
"coolify_instance_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_instance_placeholder'}",
"coolify_service_uuid": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_service_uuid'}",
"coolify_service_uuid_message": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_service_uuid_message'}",
"coolify_service_uuid_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_service_uuid_placeholder'}",
"health_url": "@:{'phrases.compat.configuration.release_manager.integrations.health_url'}",
"health_url_message": "@:{'phrases.compat.configuration.release_manager.integrations.health_url_message'}",
"health_url_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.health_url_placeholder'}",
"ssl_domain": "@:{'phrases.compat.configuration.release_manager.integrations.ssl_domain'}",
"ssl_domain_message": "@:{'phrases.compat.configuration.release_manager.integrations.ssl_domain_message'}",
"ssl_domain_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.ssl_domain_placeholder'}",
"https_domain_for_coolify": "@:{'phrases.compat.configuration.release_manager.integrations.https_domain_for_coolify'}",
"endpoint_mode": "@:{'phrases.compat.configuration.release_manager.integrations.endpoint_mode'}",
"endpoint_mode_message": "@:{'phrases.compat.configuration.release_manager.integrations.endpoint_mode_message'}",
"endpoint_mode_auto": "@:{'phrases.compat.configuration.release_manager.integrations.endpoint_mode_auto'}",
"endpoint_mode_manual": "@:{'phrases.compat.configuration.release_manager.integrations.endpoint_mode_manual'}",
"manual_endpoint_host": "@:{'phrases.compat.configuration.release_manager.integrations.manual_endpoint_host'}",
"manual_endpoint_host_message": "@:{'phrases.compat.configuration.release_manager.integrations.manual_endpoint_host_message'}",
"manual_endpoint_host_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.manual_endpoint_host_placeholder'}",
"manual_endpoint_port": "@:{'phrases.compat.configuration.release_manager.integrations.manual_endpoint_port'}",
"manual_endpoint_port_message": "@:{'phrases.compat.configuration.release_manager.integrations.manual_endpoint_port_message'}",
"app_port": "@:{'phrases.compat.configuration.release_manager.integrations.app_port'}",
"app_port_message": "@:{'phrases.compat.configuration.release_manager.integrations.app_port_message'}",
"auto_gateway_endpoint": "@:{'phrases.compat.configuration.release_manager.integrations.auto_gateway_endpoint'}",
"pending_automatic_endpoint": "@:{'phrases.compat.configuration.release_manager.integrations.pending_automatic_endpoint'}",
"auto_deploy": "@:{'phrases.compat.configuration.release_manager.integrations.auto_deploy'}",
"auto_deploy_tooltip": "@:{'phrases.compat.configuration.release_manager.integrations.auto_deploy_tooltip'}",
"coolify_ssl": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_ssl'}",
"coolify_ssl_tooltip": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_ssl_tooltip'}",
"deploy_now": "@:{'phrases.compat.configuration.release_manager.integrations.deploy_now'}",
"deploy_now_tooltip": "@:{'phrases.compat.configuration.release_manager.integrations.deploy_now_tooltip'}",
"create_missing_service": "@:{'phrases.compat.configuration.release_manager.integrations.create_missing_service'}",
"create_missing_service_tooltip": "@:{'phrases.compat.configuration.release_manager.integrations.create_missing_service_tooltip'}",
"test_access_tooltip": "@:{'phrases.compat.configuration.release_manager.integrations.test_access_tooltip'}"
}
}
}
}
@@ -0,0 +1,59 @@
{
"configuration": {
"release_manager": {
"integrations": {
"title": "@:{'phrases.compat.configuration.release_manager.integrations.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.integrations.subtitle'}",
"choose_repository": "@:{'phrases.compat.configuration.release_manager.integrations.choose_repository'}",
"add_health_url": "@:{'phrases.compat.configuration.release_manager.integrations.add_health_url'}",
"github_token_not_configured": "@:{'phrases.compat.configuration.release_manager.integrations.github_token_not_configured'}",
"open_github_token_settings": "@:{'phrases.compat.configuration.release_manager.integrations.open_github_token_settings'}",
"channel": "@:{'phrases.compat.configuration.release_manager.integrations.channel'}",
"app": "@:{'phrases.compat.configuration.release_manager.integrations.app'}",
"repository": "@:{'phrases.compat.configuration.release_manager.integrations.repository'}",
"repository_message": "@:{'phrases.compat.configuration.release_manager.integrations.repository_message'}",
"repository_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.repository_placeholder'}",
"branch": "@:{'phrases.compat.configuration.release_manager.integrations.branch'}",
"branch_message": "@:{'phrases.compat.configuration.release_manager.integrations.branch_message'}",
"branch_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.branch_placeholder'}",
"latest_prefix": "@:{'phrases.compat.configuration.release_manager.integrations.latest_prefix'}",
"coolify_instance": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_instance'}",
"coolify_instance_message": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_instance_message'}",
"no_coolify_instance": "@:{'phrases.compat.configuration.release_manager.integrations.no_coolify_instance'}",
"coolify_instance_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_instance_placeholder'}",
"coolify_service_uuid": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_service_uuid'}",
"coolify_service_uuid_message": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_service_uuid_message'}",
"coolify_service_uuid_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_service_uuid_placeholder'}",
"health_url": "@:{'phrases.compat.configuration.release_manager.integrations.health_url'}",
"health_url_message": "@:{'phrases.compat.configuration.release_manager.integrations.health_url_message'}",
"health_url_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.health_url_placeholder'}",
"ssl_domain": "@:{'phrases.compat.configuration.release_manager.integrations.ssl_domain'}",
"ssl_domain_message": "@:{'phrases.compat.configuration.release_manager.integrations.ssl_domain_message'}",
"ssl_domain_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.ssl_domain_placeholder'}",
"https_domain_for_coolify": "@:{'phrases.compat.configuration.release_manager.integrations.https_domain_for_coolify'}",
"endpoint_mode": "@:{'phrases.compat.configuration.release_manager.integrations.endpoint_mode'}",
"endpoint_mode_message": "@:{'phrases.compat.configuration.release_manager.integrations.endpoint_mode_message'}",
"endpoint_mode_auto": "@:{'phrases.compat.configuration.release_manager.integrations.endpoint_mode_auto'}",
"endpoint_mode_manual": "@:{'phrases.compat.configuration.release_manager.integrations.endpoint_mode_manual'}",
"manual_endpoint_host": "@:{'phrases.compat.configuration.release_manager.integrations.manual_endpoint_host'}",
"manual_endpoint_host_message": "@:{'phrases.compat.configuration.release_manager.integrations.manual_endpoint_host_message'}",
"manual_endpoint_host_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.manual_endpoint_host_placeholder'}",
"manual_endpoint_port": "@:{'phrases.compat.configuration.release_manager.integrations.manual_endpoint_port'}",
"manual_endpoint_port_message": "@:{'phrases.compat.configuration.release_manager.integrations.manual_endpoint_port_message'}",
"app_port": "@:{'phrases.compat.configuration.release_manager.integrations.app_port'}",
"app_port_message": "@:{'phrases.compat.configuration.release_manager.integrations.app_port_message'}",
"auto_gateway_endpoint": "@:{'phrases.compat.configuration.release_manager.integrations.auto_gateway_endpoint'}",
"pending_automatic_endpoint": "@:{'phrases.compat.configuration.release_manager.integrations.pending_automatic_endpoint'}",
"auto_deploy": "@:{'phrases.compat.configuration.release_manager.integrations.auto_deploy'}",
"auto_deploy_tooltip": "@:{'phrases.compat.configuration.release_manager.integrations.auto_deploy_tooltip'}",
"coolify_ssl": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_ssl'}",
"coolify_ssl_tooltip": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_ssl_tooltip'}",
"deploy_now": "@:{'phrases.compat.configuration.release_manager.integrations.deploy_now'}",
"deploy_now_tooltip": "@:{'phrases.compat.configuration.release_manager.integrations.deploy_now_tooltip'}",
"create_missing_service": "@:{'phrases.compat.configuration.release_manager.integrations.create_missing_service'}",
"create_missing_service_tooltip": "@:{'phrases.compat.configuration.release_manager.integrations.create_missing_service_tooltip'}",
"test_access_tooltip": "@:{'phrases.compat.configuration.release_manager.integrations.test_access_tooltip'}"
}
}
}
}
@@ -0,0 +1,435 @@
{
"configuration": {
"release_manager": {
"title": "@:{'phrases.compat.configuration.release_manager.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.subtitle'}",
"channel_names": {
"stable": "@:{'phrases.compat.configuration.release_manager.channel_names.stable'}",
"canary": "@:{'phrases.compat.configuration.release_manager.channel_names.canary'}",
"internal": "@:{'phrases.compat.configuration.release_manager.channel_names.internal'}"
},
"channel_descriptions": {
"stable": "@:{'phrases.compat.configuration.release_manager.channel_descriptions.stable'}",
"canary": "@:{'phrases.compat.configuration.release_manager.channel_descriptions.canary'}",
"internal": "@:{'phrases.compat.configuration.release_manager.channel_descriptions.internal'}"
},
"control_api": {
"title": "@:{'phrases.compat.configuration.release_manager.control_api.title'}",
"tooltip": "@:{'phrases.compat.configuration.release_manager.control_api.tooltip'}",
"example": "@:{'phrases.compat.configuration.release_manager.control_api.example'}",
"endpoint_label": "@:{'phrases.compat.configuration.release_manager.control_api.endpoint_label'}",
"endpoint_message": "@:{'phrases.compat.configuration.release_manager.control_api.endpoint_message'}",
"endpoint_aria": "@:{'phrases.compat.configuration.release_manager.control_api.endpoint_aria'}",
"use_tooltip": "@:{'phrases.compat.configuration.release_manager.control_api.use_tooltip'}",
"use": "@:{'phrases.compat.configuration.release_manager.control_api.use'}",
"reset_tooltip": "@:{'phrases.compat.configuration.release_manager.control_api.reset_tooltip'}",
"reset": "@:{'phrases.compat.configuration.release_manager.control_api.reset'}",
"known_endpoints": "@:{'phrases.compat.configuration.release_manager.control_api.known_endpoints'}"
},
"tabs": {
"overview": {
"label": "@:{'phrases.compat.configuration.release_manager.tabs.overview.label'}",
"description": "@:{'phrases.compat.configuration.release_manager.tabs.overview.description'}"
},
"channels": {
"label": "@:{'phrases.compat.configuration.release_manager.tabs.channels.label'}",
"description": "@:{'phrases.compat.configuration.release_manager.tabs.channels.description'}"
},
"assignments": {
"label": "@:{'phrases.compat.configuration.release_manager.tabs.assignments.label'}",
"description": "@:{'phrases.compat.configuration.release_manager.tabs.assignments.description'}"
},
"deployments": {
"label": "@:{'phrases.compat.configuration.release_manager.tabs.deployments.label'}",
"description": "@:{'phrases.compat.configuration.release_manager.tabs.deployments.description'}"
},
"replay": {
"label": "@:{'phrases.compat.configuration.release_manager.tabs.replay.label'}",
"description": "@:{'phrases.compat.configuration.release_manager.tabs.replay.description'}"
},
"integrations": {
"label": "@:{'phrases.compat.configuration.release_manager.tabs.integrations.label'}",
"description": "@:{'phrases.compat.configuration.release_manager.tabs.integrations.description'}"
},
"settings": {
"label": "@:{'phrases.compat.configuration.release_manager.tabs.settings.label'}",
"description": "@:{'phrases.compat.configuration.release_manager.tabs.settings.description'}"
}
},
"setup": {
"channels": "@:{'phrases.compat.configuration.release_manager.setup.channels'}",
"targets": "@:{'phrases.compat.configuration.release_manager.setup.targets'}",
"assignments": "@:{'phrases.compat.configuration.release_manager.setup.assignments'}",
"deployments": "@:{'phrases.compat.configuration.release_manager.setup.deployments'}",
"replay": "@:{'phrases.compat.configuration.release_manager.setup.replay'}"
},
"stats": {
"channels": "@:{'phrases.compat.configuration.release_manager.stats.channels'}",
"targets": "@:{'phrases.compat.configuration.release_manager.stats.targets'}",
"deployments": "@:{'phrases.compat.configuration.release_manager.stats.deployments'}",
"timeline_events": "@:{'phrases.compat.configuration.release_manager.stats.timeline_events'}"
},
"overview": {
"title": "@:{'phrases.compat.configuration.release_manager.overview.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.overview.subtitle'}",
"guided_setup": "@:{'phrases.compat.configuration.release_manager.overview.guided_setup'}",
"next_step": "@:{'phrases.compat.configuration.release_manager.overview.next_step'}",
"ready": "@:{'phrases.compat.configuration.release_manager.overview.ready'}",
"add_suggested_channel": "@:{'phrases.compat.configuration.release_manager.overview.add_suggested_channel'}",
"module_health_empty": "@:{'phrases.compat.configuration.release_manager.overview.module_health_empty'}"
},
"settings": {
"title": "@:{'phrases.compat.configuration.release_manager.settings.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.settings.subtitle'}",
"github_token": "@:{'phrases.compat.configuration.release_manager.settings.github_token'}",
"github_api_url": "@:{'phrases.compat.configuration.release_manager.settings.github_api_url'}",
"webhook_secret": "@:{'phrases.compat.configuration.release_manager.settings.webhook_secret'}",
"github_webhook_secret": "@:{'phrases.compat.configuration.release_manager.settings.github_webhook_secret'}",
"configured": "@:{'phrases.compat.configuration.release_manager.settings.configured'}",
"not_configured": "@:{'phrases.compat.configuration.release_manager.settings.not_configured'}",
"loaded_from": "@:{'phrases.compat.configuration.release_manager.settings.loaded_from'}",
"set_below": "@:{'phrases.compat.configuration.release_manager.settings.set_below'}",
"private_repositories_prefix": "@:{'phrases.compat.configuration.release_manager.settings.private_repositories_prefix'}",
"private_repositories_or": "@:{'phrases.compat.configuration.release_manager.settings.private_repositories_or'}",
"github_token_message": "@:{'phrases.compat.configuration.release_manager.settings.github_token_message'}",
"github_token_placeholder": "@:{'phrases.compat.configuration.release_manager.settings.github_token_placeholder'}",
"github_api_url_message": "@:{'phrases.compat.configuration.release_manager.settings.github_api_url_message'}",
"webhook_secret_message": "@:{'phrases.compat.configuration.release_manager.settings.webhook_secret_message'}",
"webhook_secret_placeholder": "@:{'phrases.compat.configuration.release_manager.settings.webhook_secret_placeholder'}",
"save": "@:{'phrases.compat.configuration.release_manager.settings.save'}",
"back_to_integrations": "@:{'phrases.compat.configuration.release_manager.settings.back_to_integrations'}",
"guide": {
"steps": {
"github_token": "@:{'phrases.compat.configuration.release_manager.settings.guide.steps.github_token'}",
"github_api_url": "@:{'phrases.compat.configuration.release_manager.settings.guide.steps.github_api_url'}",
"github_webhook_secret": "@:{'phrases.compat.configuration.release_manager.settings.guide.steps.github_webhook_secret'}",
"review": "@:{'phrases.compat.configuration.release_manager.settings.guide.steps.review'}"
},
"github_token_title": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_token_title'}",
"github_token_description": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_token_description'}",
"configure_this_in": "@:{'phrases.compat.configuration.release_manager.settings.guide.configure_this_in'}",
"server_environment_variable": "@:{'phrases.compat.configuration.release_manager.settings.guide.server_environment_variable'}",
"takes_precedence": "@:{'phrases.compat.configuration.release_manager.settings.guide.takes_precedence'}",
"recommended_access": "@:{'phrases.compat.configuration.release_manager.settings.guide.recommended_access'}",
"recommended_access_description": "@:{'phrases.compat.configuration.release_manager.settings.guide.recommended_access_description'}",
"github_api_url_title": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_api_url_title'}",
"github_api_url_description": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_api_url_description'}",
"github_api_url_invalid": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_api_url_invalid'}",
"github_cloud": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_cloud'}",
"enterprise_example": "@:{'phrases.compat.configuration.release_manager.settings.guide.enterprise_example'}",
"github_webhook_secret_title": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_webhook_secret_title'}",
"github_webhook_secret_description": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_webhook_secret_description'}",
"github_webhook_url": "@:{'phrases.compat.configuration.release_manager.settings.guide.github_webhook_url'}",
"store_shared_secret_in": "@:{'phrases.compat.configuration.release_manager.settings.guide.store_shared_secret_in'}",
"review_title": "@:{'phrases.compat.configuration.release_manager.settings.guide.review_title'}",
"review_description": "@:{'phrases.compat.configuration.release_manager.settings.guide.review_description'}",
"ready": "@:{'phrases.compat.configuration.release_manager.settings.guide.ready'}",
"needs_value": "@:{'phrases.compat.configuration.release_manager.settings.guide.needs_value'}"
}
},
"channel_unavailable": {
"kicker": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.kicker'}",
"title": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.title'}",
"summary_prefix": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.summary_prefix'}",
"summary_suffix": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.summary_suffix'}",
"release_bundle": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.release_bundle'}",
"frontend_version": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.frontend_version'}",
"api_version": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.api_version'}",
"frontend_base_url": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.frontend_base_url'}",
"api_base_url": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.api_base_url'}",
"frontend_entry": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.frontend_entry'}",
"release_runtime": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.release_runtime'}",
"checking_again": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.checking_again'}",
"refresh_error": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.refresh_error'}",
"ignore": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.ignore'}",
"check_again": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.check_again'}",
"logout": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.logout'}",
"base_image": "@:{'phrases.compat.configuration.release_manager.channel_unavailable.base_image'}"
},
"channel_switched": {
"kicker": "@:{'phrases.compat.configuration.release_manager.channel_switched.kicker'}",
"title": "@:{'phrases.compat.configuration.release_manager.channel_switched.title'}",
"summary_prefix": "@:{'phrases.compat.configuration.release_manager.channel_switched.summary_prefix'}",
"summary_suffix": "@:{'phrases.compat.configuration.release_manager.channel_switched.summary_suffix'}",
"bundle": "@:{'phrases.compat.configuration.release_manager.channel_switched.bundle'}",
"frontend": "@:{'phrases.compat.configuration.release_manager.channel_switched.frontend'}",
"api": "@:{'phrases.compat.configuration.release_manager.channel_switched.api'}",
"frontend_version": "@:{'phrases.compat.configuration.release_manager.channel_switched.frontend_version'}",
"api_version": "@:{'phrases.compat.configuration.release_manager.channel_switched.api_version'}",
"assigned_channel": "@:{'phrases.compat.configuration.release_manager.channel_switched.assigned_channel'}",
"current_app_image": "@:{'phrases.compat.configuration.release_manager.channel_switched.current_app_image'}",
"current_api": "@:{'phrases.compat.configuration.release_manager.channel_switched.current_api'}",
"base_image": "@:{'phrases.compat.configuration.release_manager.channel_switched.base_image'}",
"continue": "@:{'phrases.compat.configuration.release_manager.channel_switched.continue'}"
},
"channel_selector": {
"title": "@:{'phrases.compat.configuration.release_manager.channel_selector.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.channel_selector.subtitle'}",
"sidebar_title": "@:{'phrases.compat.configuration.release_manager.channel_selector.sidebar_title'}",
"default": "@:{'phrases.compat.configuration.release_manager.channel_selector.default'}",
"ready": "@:{'phrases.compat.configuration.release_manager.channel_selector.ready'}",
"unavailable": "@:{'phrases.compat.configuration.release_manager.channel_selector.unavailable'}",
"release_bundle": "@:{'phrases.compat.configuration.release_manager.channel_selector.release_bundle'}",
"frontend_version": "@:{'phrases.compat.configuration.release_manager.channel_selector.frontend_version'}",
"api_version": "@:{'phrases.compat.configuration.release_manager.channel_selector.api_version'}",
"frontend_base_url": "@:{'phrases.compat.configuration.release_manager.channel_selector.frontend_base_url'}",
"api_base_url": "@:{'phrases.compat.configuration.release_manager.channel_selector.api_base_url'}",
"frontend_entry": "@:{'phrases.compat.configuration.release_manager.channel_selector.frontend_entry'}",
"release_runtime": "@:{'phrases.compat.configuration.release_manager.channel_selector.release_runtime'}",
"bundle": "@:{'phrases.compat.configuration.release_manager.channel_selector.bundle'}",
"frontend": "@:{'phrases.compat.configuration.release_manager.channel_selector.frontend'}",
"api": "@:{'phrases.compat.configuration.release_manager.channel_selector.api'}",
"use_local_frontend": "@:{'phrases.compat.configuration.release_manager.channel_selector.use_local_frontend'}"
},
"common": {
"yes": "@:{'phrases.compat.configuration.release_manager.common.yes'}",
"no": "@:{'phrases.compat.configuration.release_manager.common.no'}",
"off": "@:{'phrases.compat.configuration.release_manager.common.off'}"
},
"actions": {
"edit": "@:{'phrases.compat.configuration.release_manager.actions.edit'}",
"rollback": "@:{'phrases.compat.configuration.release_manager.actions.rollback'}",
"update": "@:{'phrases.compat.configuration.release_manager.actions.update'}",
"create": "@:{'phrases.compat.configuration.release_manager.actions.create'}",
"clear": "@:{'phrases.compat.configuration.release_manager.actions.clear'}",
"assign": "@:{'phrases.compat.configuration.release_manager.actions.assign'}",
"remove": "@:{'phrases.compat.configuration.release_manager.actions.remove'}",
"test_access": "@:{'phrases.compat.configuration.release_manager.actions.test_access'}",
"deploy": "@:{'phrases.compat.configuration.release_manager.actions.deploy'}",
"redeploy": "@:{'phrases.compat.configuration.release_manager.actions.redeploy'}",
"promote": "@:{'phrases.compat.configuration.release_manager.actions.promote'}",
"enable": "@:{'phrases.compat.configuration.release_manager.actions.enable'}",
"search": "@:{'phrases.compat.configuration.release_manager.actions.search'}",
"save_target": "@:{'phrases.compat.configuration.release_manager.actions.save_target'}",
"delete": "@:{'phrases.compat.configuration.release_manager.actions.delete'}",
"back": "@:{'phrases.compat.configuration.release_manager.actions.back'}",
"next": "@:{'phrases.compat.configuration.release_manager.actions.next'}"
},
"tables": {
"channel": "@:{'phrases.compat.configuration.release_manager.tables.channel'}",
"rollout": "@:{'phrases.compat.configuration.release_manager.tables.rollout'}",
"frontend": "@:{'phrases.compat.configuration.release_manager.tables.frontend'}",
"api": "@:{'phrases.compat.configuration.release_manager.tables.api'}",
"replay": "@:{'phrases.compat.configuration.release_manager.tables.replay'}",
"actions": "@:{'phrases.compat.configuration.release_manager.tables.actions'}",
"module": "@:{'phrases.compat.configuration.release_manager.tables.module'}",
"status": "@:{'phrases.compat.configuration.release_manager.tables.status'}",
"checked": "@:{'phrases.compat.configuration.release_manager.tables.checked'}",
"subject": "@:{'phrases.compat.configuration.release_manager.tables.subject'}",
"reason": "@:{'phrases.compat.configuration.release_manager.tables.reason'}",
"expires": "@:{'phrases.compat.configuration.release_manager.tables.expires'}",
"deployment": "@:{'phrases.compat.configuration.release_manager.tables.deployment'}",
"source": "@:{'phrases.compat.configuration.release_manager.tables.source'}",
"started": "@:{'phrases.compat.configuration.release_manager.tables.started'}",
"target": "@:{'phrases.compat.configuration.release_manager.tables.target'}",
"coolify": "@:{'phrases.compat.configuration.release_manager.tables.coolify'}",
"auto": "@:{'phrases.compat.configuration.release_manager.tables.auto'}"
},
"values": {
"app": {
"frontend": "@:{'phrases.compat.configuration.release_manager.values.app.frontend'}",
"api": "@:{'phrases.compat.configuration.release_manager.values.app.api'}"
},
"capture_level": {
"metadata": "@:{'phrases.compat.configuration.release_manager.values.capture_level.metadata'}",
"full_redacted": "@:{'phrases.compat.configuration.release_manager.values.capture_level.full_redacted'}"
},
"subject_type": {
"user": "@:{'phrases.compat.configuration.release_manager.values.subject_type.user'}",
"subuser": "@:{'phrases.compat.configuration.release_manager.values.subject_type.subuser'}",
"customer": "@:{'phrases.compat.configuration.release_manager.values.subject_type.customer'}",
"channel": "@:{'phrases.compat.configuration.release_manager.values.subject_type.channel'}"
},
"severity": {
"error": "@:{'phrases.compat.configuration.release_manager.values.severity.error'}",
"warning": "@:{'phrases.compat.configuration.release_manager.values.severity.warning'}",
"info": "@:{'phrases.compat.configuration.release_manager.values.severity.info'}"
},
"status": {
"ok": "@:{'phrases.compat.configuration.release_manager.values.status.ok'}",
"active": "@:{'phrases.compat.configuration.release_manager.values.status.active'}",
"deployed": "@:{'phrases.compat.configuration.release_manager.values.status.deployed'}",
"stable": "@:{'phrases.compat.configuration.release_manager.values.status.stable'}",
"enabled": "@:{'phrases.compat.configuration.release_manager.values.status.enabled'}",
"queued": "@:{'phrases.compat.configuration.release_manager.values.status.queued'}",
"deploying": "@:{'phrases.compat.configuration.release_manager.values.status.deploying'}",
"metadata": "@:{'phrases.compat.configuration.release_manager.values.status.metadata'}",
"not_configured": "@:{'phrases.compat.configuration.release_manager.values.status.not_configured'}",
"warning": "@:{'phrases.compat.configuration.release_manager.values.status.warning'}",
"failed": "@:{'phrases.compat.configuration.release_manager.values.status.failed'}",
"down": "@:{'phrases.compat.configuration.release_manager.values.status.down'}",
"error": "@:{'phrases.compat.configuration.release_manager.values.status.error'}",
"unknown": "@:{'phrases.compat.configuration.release_manager.values.status.unknown'}"
}
},
"autocomplete": {
"suggested_value": "@:{'phrases.compat.configuration.release_manager.autocomplete.suggested_value'}",
"suggestion": "@:{'phrases.compat.configuration.release_manager.autocomplete.suggestion'}",
"use_typed_value": "@:{'phrases.compat.configuration.release_manager.autocomplete.use_typed_value'}",
"custom": "@:{'phrases.compat.configuration.release_manager.autocomplete.custom'}",
"api_url": "@:{'phrases.compat.configuration.release_manager.autocomplete.api_url'}",
"frontend_url": "@:{'phrases.compat.configuration.release_manager.autocomplete.frontend_url'}",
"known_release_url": "@:{'phrases.compat.configuration.release_manager.autocomplete.known_release_url'}",
"deploy_here_with_coolify": "@:{'phrases.compat.configuration.release_manager.autocomplete.deploy_here_with_coolify'}",
"prepare_coolify_target": "@:{'phrases.compat.configuration.release_manager.autocomplete.prepare_coolify_target'}",
"coolify_ssl": "@:{'phrases.compat.configuration.release_manager.autocomplete.coolify_ssl'}",
"github_repository": "@:{'phrases.compat.configuration.release_manager.autocomplete.github_repository'}",
"owner_repository": "@:{'phrases.compat.configuration.release_manager.autocomplete.owner_repository'}",
"git_branch": "@:{'phrases.compat.configuration.release_manager.autocomplete.git_branch'}",
"deployment_branch": "@:{'phrases.compat.configuration.release_manager.autocomplete.deployment_branch'}",
"github_commit": "@:{'phrases.compat.configuration.release_manager.autocomplete.github_commit'}",
"git_commit": "@:{'phrases.compat.configuration.release_manager.autocomplete.git_commit'}",
"use_specific_commit_sha": "@:{'phrases.compat.configuration.release_manager.autocomplete.use_specific_commit_sha'}",
"custom_commit": "@:{'phrases.compat.configuration.release_manager.autocomplete.custom_commit'}",
"health_url": "@:{'phrases.compat.configuration.release_manager.autocomplete.health_url'}",
"release_health_check": "@:{'phrases.compat.configuration.release_manager.autocomplete.release_health_check'}",
"coolify_service": "@:{'phrases.compat.configuration.release_manager.autocomplete.coolify_service'}",
"service_uuid": "@:{'phrases.compat.configuration.release_manager.autocomplete.service_uuid'}",
"known_coolify_service_uuid": "@:{'phrases.compat.configuration.release_manager.autocomplete.known_coolify_service_uuid'}",
"control_api": "@:{'phrases.compat.configuration.release_manager.autocomplete.control_api'}",
"release_manager_api_endpoint": "@:{'phrases.compat.configuration.release_manager.autocomplete.release_manager_api_endpoint'}",
"module": "@:{'phrases.compat.configuration.release_manager.autocomplete.module'}",
"timeline_module_key": "@:{'phrases.compat.configuration.release_manager.autocomplete.timeline_module_key'}"
},
"errors": {
"github_repositories": "@:{'phrases.compat.configuration.release_manager.errors.github_repositories'}",
"github_branches": "@:{'phrases.compat.configuration.release_manager.errors.github_branches'}",
"repository_branch_required": "@:{'phrases.compat.configuration.release_manager.errors.repository_branch_required'}",
"github_access_failed": "@:{'phrases.compat.configuration.release_manager.errors.github_access_failed'}"
},
"channels": {
"title": "@:{'phrases.compat.configuration.release_manager.channels.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.channels.subtitle'}",
"rollout_percent": "@:{'phrases.compat.configuration.release_manager.channels.rollout_percent'}",
"already_configured": "@:{'phrases.compat.configuration.release_manager.channels.already_configured'}",
"slug": "@:{'phrases.compat.configuration.release_manager.channels.slug'}",
"slug_message": "@:{'phrases.compat.configuration.release_manager.channels.slug_message'}",
"slug_placeholder": "@:{'phrases.compat.configuration.release_manager.channels.slug_placeholder'}",
"name": "@:{'phrases.compat.configuration.release_manager.channels.name'}",
"name_message": "@:{'phrases.compat.configuration.release_manager.channels.name_message'}",
"name_placeholder": "@:{'phrases.compat.configuration.release_manager.channels.name_placeholder'}",
"rollout": "@:{'phrases.compat.configuration.release_manager.channels.rollout'}",
"rollout_message": "@:{'phrases.compat.configuration.release_manager.channels.rollout_message'}",
"frontend_url": "@:{'phrases.compat.configuration.release_manager.channels.frontend_url'}",
"frontend_url_message": "@:{'phrases.compat.configuration.release_manager.channels.frontend_url_message'}",
"frontend_url_placeholder": "@:{'phrases.compat.configuration.release_manager.channels.frontend_url_placeholder'}",
"no_matching_release_url": "@:{'phrases.compat.configuration.release_manager.channels.no_matching_release_url'}",
"api_url": "@:{'phrases.compat.configuration.release_manager.channels.api_url'}",
"api_url_message": "@:{'phrases.compat.configuration.release_manager.channels.api_url_message'}",
"api_url_placeholder": "@:{'phrases.compat.configuration.release_manager.channels.api_url_placeholder'}",
"no_matching_api_url": "@:{'phrases.compat.configuration.release_manager.channels.no_matching_api_url'}",
"replay_capture": "@:{'phrases.compat.configuration.release_manager.channels.replay_capture'}",
"replay_capture_message": "@:{'phrases.compat.configuration.release_manager.channels.replay_capture_message'}",
"retention_days": "@:{'phrases.compat.configuration.release_manager.channels.retention_days'}",
"retention_days_message": "@:{'phrases.compat.configuration.release_manager.channels.retention_days_message'}",
"enabled": "@:{'phrases.compat.configuration.release_manager.channels.enabled'}",
"default_channel": "@:{'phrases.compat.configuration.release_manager.channels.default_channel'}",
"replay_enabled": "@:{'phrases.compat.configuration.release_manager.channels.replay_enabled'}",
"frontend_chip": "@:{'phrases.compat.configuration.release_manager.channels.frontend_chip'}",
"api_chip": "@:{'phrases.compat.configuration.release_manager.channels.api_chip'}"
},
"assignments": {
"title": "@:{'phrases.compat.configuration.release_manager.assignments.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.assignments.subtitle'}",
"subject": "@:{'phrases.compat.configuration.release_manager.assignments.subject'}",
"subject_message": "@:{'phrases.compat.configuration.release_manager.assignments.subject_message'}",
"subject_placeholder": "@:{'phrases.compat.configuration.release_manager.assignments.subject_placeholder'}",
"subject_empty": "@:{'phrases.compat.configuration.release_manager.assignments.subject_empty'}",
"group_users": "@:{'phrases.compat.configuration.release_manager.assignments.group_users'}",
"group_subusers": "@:{'phrases.compat.configuration.release_manager.assignments.group_subusers'}",
"group_customers": "@:{'phrases.compat.configuration.release_manager.assignments.group_customers'}",
"group_manual": "@:{'phrases.compat.configuration.release_manager.assignments.group_manual'}",
"manual_user": "@:{'phrases.compat.configuration.release_manager.assignments.manual_user'}",
"manual_subuser": "@:{'phrases.compat.configuration.release_manager.assignments.manual_subuser'}",
"manual_customer": "@:{'phrases.compat.configuration.release_manager.assignments.manual_customer'}",
"manual_description": "@:{'phrases.compat.configuration.release_manager.assignments.manual_description'}",
"subject_type": "@:{'phrases.compat.configuration.release_manager.assignments.subject_type'}",
"subject_type_message": "@:{'phrases.compat.configuration.release_manager.assignments.subject_type_message'}",
"customer": "@:{'phrases.compat.configuration.release_manager.assignments.customer'}",
"customer_message": "@:{'phrases.compat.configuration.release_manager.assignments.customer_message'}",
"customer_placeholder": "@:{'phrases.compat.configuration.release_manager.assignments.customer_placeholder'}",
"subject_id": "@:{'phrases.compat.configuration.release_manager.assignments.subject_id'}",
"subject_id_message": "@:{'phrases.compat.configuration.release_manager.assignments.subject_id_message'}",
"subject_id_placeholder": "@:{'phrases.compat.configuration.release_manager.assignments.subject_id_placeholder'}",
"channel": "@:{'phrases.compat.configuration.release_manager.assignments.channel'}",
"channel_message": "@:{'phrases.compat.configuration.release_manager.assignments.channel_message'}",
"reason": "@:{'phrases.compat.configuration.release_manager.assignments.reason'}",
"reason_message": "@:{'phrases.compat.configuration.release_manager.assignments.reason_message'}",
"reason_placeholder": "@:{'phrases.compat.configuration.release_manager.assignments.reason_placeholder'}",
"expires": "@:{'phrases.compat.configuration.release_manager.assignments.expires'}",
"expires_message": "@:{'phrases.compat.configuration.release_manager.assignments.expires_message'}"
},
"deployments": {
"title": "@:{'phrases.compat.configuration.release_manager.deployments.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.deployments.subtitle'}",
"manual_deployment_record": "@:{'phrases.compat.configuration.release_manager.deployments.manual_deployment_record'}",
"target": "@:{'phrases.compat.configuration.release_manager.deployments.target'}",
"target_message": "@:{'phrases.compat.configuration.release_manager.deployments.target_message'}",
"select_target_or_manual": "@:{'phrases.compat.configuration.release_manager.deployments.select_target_or_manual'}",
"channel": "@:{'phrases.compat.configuration.release_manager.deployments.channel'}",
"app": "@:{'phrases.compat.configuration.release_manager.deployments.app'}",
"repository": "@:{'phrases.compat.configuration.release_manager.deployments.repository'}",
"repository_message": "@:{'phrases.compat.configuration.release_manager.deployments.repository_message'}",
"repository_placeholder": "@:{'phrases.compat.configuration.release_manager.deployments.repository_placeholder'}",
"branch": "@:{'phrases.compat.configuration.release_manager.deployments.branch'}",
"branch_message": "@:{'phrases.compat.configuration.release_manager.deployments.branch_message'}",
"branch_placeholder": "@:{'phrases.compat.configuration.release_manager.deployments.branch_placeholder'}",
"commit_selection": "@:{'phrases.compat.configuration.release_manager.deployments.commit_selection'}",
"commit_selection_message": "@:{'phrases.compat.configuration.release_manager.deployments.commit_selection_message'}",
"latest_from_branch": "@:{'phrases.compat.configuration.release_manager.deployments.latest_from_branch'}",
"specific_commit": "@:{'phrases.compat.configuration.release_manager.deployments.specific_commit'}",
"specific_commit_message": "@:{'phrases.compat.configuration.release_manager.deployments.specific_commit_message'}",
"commit_sha_placeholder": "@:{'phrases.compat.configuration.release_manager.deployments.commit_sha_placeholder'}",
"version_label": "@:{'phrases.compat.configuration.release_manager.deployments.version_label'}",
"version_label_message": "@:{'phrases.compat.configuration.release_manager.deployments.version_label_message'}",
"version_label_placeholder": "@:{'phrases.compat.configuration.release_manager.deployments.version_label_placeholder'}",
"commit_prefix": "@:{'phrases.compat.configuration.release_manager.deployments.commit_prefix'}",
"test_access_tooltip": "@:{'phrases.compat.configuration.release_manager.deployments.test_access_tooltip'}"
},
"replay": {
"title": "@:{'phrases.compat.configuration.release_manager.replay.title'}",
"subtitle": "@:{'phrases.compat.configuration.release_manager.replay.subtitle'}",
"capture_channel": "@:{'phrases.compat.configuration.release_manager.replay.capture_channel'}",
"target_type": "@:{'phrases.compat.configuration.release_manager.replay.target_type'}",
"target": "@:{'phrases.compat.configuration.release_manager.replay.target'}",
"target_message": "@:{'phrases.compat.configuration.release_manager.replay.target_message'}",
"target_placeholder": "@:{'phrases.compat.configuration.release_manager.replay.target_placeholder'}",
"channel_scope": "@:{'phrases.compat.configuration.release_manager.replay.channel_scope'}",
"channel_scope_message": "@:{'phrases.compat.configuration.release_manager.replay.channel_scope_message'}",
"no_channel_scope": "@:{'phrases.compat.configuration.release_manager.replay.no_channel_scope'}",
"capture_level": "@:{'phrases.compat.configuration.release_manager.replay.capture_level'}",
"expires": "@:{'phrases.compat.configuration.release_manager.replay.expires'}",
"expires_message": "@:{'phrases.compat.configuration.release_manager.replay.expires_message'}",
"trace_id": "@:{'phrases.compat.configuration.release_manager.replay.trace_id'}",
"trace_id_message": "@:{'phrases.compat.configuration.release_manager.replay.trace_id_message'}",
"trace_id_placeholder": "@:{'phrases.compat.configuration.release_manager.replay.trace_id_placeholder'}",
"channel": "@:{'phrases.compat.configuration.release_manager.replay.channel'}",
"channel_placeholder": "@:{'phrases.compat.configuration.release_manager.replay.channel_placeholder'}",
"principal_id": "@:{'phrases.compat.configuration.release_manager.replay.principal_id'}",
"principal_id_placeholder": "@:{'phrases.compat.configuration.release_manager.replay.principal_id_placeholder'}",
"module": "@:{'phrases.compat.configuration.release_manager.replay.module'}",
"module_placeholder": "@:{'phrases.compat.configuration.release_manager.replay.module_placeholder'}",
"severity": "@:{'phrases.compat.configuration.release_manager.replay.severity'}",
"any_severity": "@:{'phrases.compat.configuration.release_manager.replay.any_severity'}",
"no_timeline_events": "@:{'phrases.compat.configuration.release_manager.replay.no_timeline_events'}"
},
"loading": {
"assignments": "@:{'phrases.compat.configuration.release_manager.loading.assignments'}",
"channels": "@:{'phrases.compat.configuration.release_manager.loading.channels'}",
"channels_detail": "@:{'phrases.compat.configuration.release_manager.loading.channels_detail'}",
"data_services": "@:{'phrases.compat.configuration.release_manager.loading.data_services'}",
"deployments": "@:{'phrases.compat.configuration.release_manager.loading.deployments'}",
"github_repositories": "@:{'phrases.compat.configuration.release_manager.loading.github_repositories'}",
"integrations": "@:{'phrases.compat.configuration.release_manager.loading.integrations'}",
"operations": "@:{'phrases.compat.configuration.release_manager.loading.operations'}",
"refreshing_summary": "@:{'phrases.compat.configuration.release_manager.loading.refreshing_summary'}",
"refreshing_timeline": "@:{'phrases.compat.configuration.release_manager.loading.refreshing_timeline'}",
"settings": "@:{'phrases.compat.configuration.release_manager.loading.settings'}",
"summary": "@:{'phrases.compat.configuration.release_manager.loading.summary'}",
"summary_detail": "@:{'phrases.compat.configuration.release_manager.loading.summary_detail'}",
"timeline_events": "@:{'phrases.compat.configuration.release_manager.loading.timeline_events'}",
"timeline_sessions": "@:{'phrases.compat.configuration.release_manager.loading.timeline_sessions'}"
}
}
}
}
@@ -0,0 +1,28 @@
{
"compat": {
"admin": {
"pos": {
"settings_wheel": {
"accept_self_serve_wash": "Godta selvbetjent vask",
"accept_self_serve_wash_confirm": "Flytt denne kladden til kunde #{customerNumber} med selvbetjeningsdetaljene?",
"accept_self_serve_wash_success": "Den selvbetjente vasken ble godtatt.",
"email_notifications_section": "E-postvarsler",
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
"resend_booking_confirmation": "(Re)send booking confirmation",
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
"resend_booking_confirmation_success": "The booking confirmation has been sent",
"resend_wash_certificate": "(Re)send wash certificate",
"resend_wash_certificate_error": "The wash certificate could not be sent",
"resend_wash_certificate_success": "The wash certificate has been sent",
"self_serve_customer": "Kunde",
"self_serve_driver": "Sjåfør",
"self_serve_elapsed": "Tid",
"self_serve_wash_attachment": "Selvbetjent vask",
"self_serve_wash_attachment_for_customer": "Selvbetjent vask for kunde #{customerNumber}"
}
}
}
}
}
@@ -298,490 +298,6 @@
"subtitle": "@.capitalize:{'terms.glossary.konfigurasjon'} @:{'terms.glossary.av'} @:{'terms.glossary.xlvask'}-@:{'terms.glossary.integrasjonen'}",
"title": "@:{'terms.glossary.xlvask'}-@:{'terms.glossary.konfigurasjon'}",
"username_desc": "@:{'terms.glossary.brukernavnet'} @:{'terms.glossary.brukes'} @:{'terms.glossary.til'} @:{'terms.glossary.a'} @:{'terms.glossary.autentisere'} @:{'terms.glossary.api'}-@:{'terms.glossary.tilkoblingen'}."
},
"release_manager": {
"title": "Release Manager",
"subtitle": "Gradvise API- og Front-End-utgivelser",
"channel_names": {
"stable": "Stabil",
"canary": "Canary",
"internal": "Intern"
},
"channel_descriptions": {
"stable": "Standard produksjonskanal.",
"canary": "Tidlig produksjonsvalideringskanal.",
"internal": "Intern kanal for ansatte og superbrukervalidering."
},
"control_api": {
"title": "Kontroll-API",
"tooltip": "Release Manager-handlinger sendes til denne API-en. Bruk produksjons-API-en med mindre du tester en staging-backend.",
"example": "Eksempel: https://api.truckwash.io:4433",
"endpoint_label": "API-endepunkt",
"endpoint_message": "Endre dette bare når Release Manager-endepunkter er tilgjengelige på mål-API-en.",
"endpoint_aria": "Release Manager kontroll-API URL",
"use_tooltip": "Last release-data fra denne API-en",
"use": "Bruk",
"reset_tooltip": "Gå tilbake til standard kontroll-API",
"reset": "Tilbakestill",
"known_endpoints": "Kjente kontroll-API-endepunkter"
},
"tabs": {
"overview": {
"label": "Oversikt",
"description": "Kanalhelse, oppsettsfremdrift og siste release-status."
},
"channels": {
"label": "Kanaler",
"description": "Opprett stabile, canary- og målrettede kanaler med rollout-grenser."
},
"assignments": {
"label": "Tildelinger",
"description": "Knytt brukere, underbrukere eller kunder til en bestemt release-kanal."
},
"deployments": {
"label": "Utrullinger",
"description": "Start utrullinger og promoter vellykkede bygg."
},
"replay": {
"label": "Replay",
"description": "Aktiver målrettet innsamling og søk i release-tidslinjehendelser."
},
"integrations": {
"label": "Integrasjoner",
"description": "Koble GitHub-repositories, branches og Coolify-tjenester."
},
"settings": {
"label": "Innstillinger",
"description": "Konfigurer Release Managers GitHub-tilgang og webhook-innstillinger."
}
},
"loading": {
"summary": "Laster Release Manager-data...",
"refreshing_summary": "Oppdaterer Release Manager-data...",
"summary_detail": "Release-status, kanaler, deployments og servicehelse lastes.",
"channels": "Laster release-kanaler...",
"channels_detail": "Kanalvisninger og release-avhengigheter lastes.",
"assignments": "Laster release-tildelinger...",
"deployments": "Laster release-deployments...",
"operations": "Laster release-operasjoner...",
"data_services": "Laster release-datatjenester...",
"integrations": "Laster release-integrasjoner...",
"settings": "Laster Release Manager-innstillinger...",
"github_repositories": "Laster GitHub-repositories...",
"timeline_sessions": "Laster release-tidslinje-sesjoner...",
"timeline_events": "Laster release-tidslinjehendelser...",
"refreshing_timeline": "Oppdaterer release-tidslinje..."
},
"setup": {
"channels": "Kanaler",
"targets": "GitHub- og Coolify-mål",
"assignments": "Pilottildelinger",
"deployments": "Første utrulling",
"replay": "Replay-innsamling"
},
"stats": {
"channels": "Kanaler",
"targets": "Mål",
"deployments": "Utrullinger",
"timeline_events": "Tidslinjehendelser"
},
"overview": {
"title": "Oversikt",
"subtitle": "Aktuell kanalhelse, release-aktivitet og modulstatus.",
"guided_setup": "Veiledet oppsett",
"next_step": "Neste: {step}",
"ready": "Release Manager er klar for daglig drift.",
"add_suggested_channel": "Legg til foreslått kanal",
"module_health_empty": "Modul-health snapshots vises når probes er registrert."
},
"settings": {
"title": "Release Manager-innstillinger",
"subtitle": "Konfigurer GitHub-tokenet som brukes for private repositories og branch-oppslag.",
"github_token": "GitHub-token",
"github_api_url": "GitHub API-URL",
"webhook_secret": "Webhook-hemmelighet",
"github_webhook_secret": "GitHub webhook-hemmelighet",
"configured": "Konfigurert",
"not_configured": "Ikke konfigurert",
"loaded_from": "Lastet fra {variable}",
"set_below": "Angi {variable} nedenfor",
"private_repositories_prefix": "Private repositories leses med serverens miljøvariabel",
"private_repositories_or": "eller modulens konfigurasjonsvariabel",
"github_token_message": "Eksempel: github_pat_... med tilgang til de private repositories Release Manager ruller ut.",
"github_token_placeholder": "La stå tomt for å beholde eksisterende token",
"github_api_url_message": "Konfigurert i ReleaseManager.github_api_url. Bruk https://api.github.com med mindre GitHub Enterprise brukes.",
"webhook_secret_message": "Valgfritt; la stå tomt for å beholde eksisterende hemmelighet.",
"webhook_secret_placeholder": "Webhook HMAC-hemmelighet",
"save": "Lagre innstillinger",
"back_to_integrations": "Tilbake til integrasjoner",
"guide": {
"steps": {
"github_token": "GitHub-token",
"github_api_url": "GitHub API-URL",
"github_webhook_secret": "GitHub webhook secret",
"review": "Gennemgang"
},
"github_token_title": "Legg til GitHub-token",
"github_token_description": "Release Manager bruger denne token til at vise private repositories, branches og commits, før et deployment-mål gemmes eller testes.",
"configure_this_in": "Konfigurer dette i",
"server_environment_variable": "Serverens miljøvariabel",
"takes_precedence": "har forrang når den er satt.",
"recommended_access": "Anbefalt tilgang",
"recommended_access_description": "Fine-grained GitHub-token med Contents: read-adgang til hvert release-repository.",
"github_api_url_title": "Legg til GitHub API-URL",
"github_api_url_description": "Brug GitHub Cloud som standard, eller peg på REST API-base-URLen for en GitHub Enterprise Server-installation.",
"github_api_url_invalid": "Skriv inn en full HTTP- eller HTTPS-URL.",
"github_cloud": "GitHub Cloud",
"enterprise_example": "Enterprise-eksempel",
"github_webhook_secret_title": "Legg til GitHub webhook secret",
"github_webhook_secret_description": "Brug samme secret i GitHub repository-webhooks, så Release Manager kan verificere deployment-events, før de håndteres.",
"github_webhook_url": "GitHub webhook-URL:",
"store_shared_secret_in": "Lagre den delte secreten i",
"review_title": "Gå gjennom GitHub-oppsett",
"review_description": "Gem værdierne, og brug derefter Integrationer til at vælge et privat repository, branch og specifik commit eller seneste commit før adgang testes.",
"ready": "Klar",
"needs_value": "Mangler værdi"
}
},
"channel_unavailable": {
"kicker": "Release Manager",
"title": "Release-kanalen er ikke klar",
"summary_prefix": "Kontoen din er tildelt",
"summary_suffix": ", men kanalen mangler påkrevd release-konfigurasjon.",
"release_bundle": "Release-bundle",
"frontend_version": "Frontend-versjon",
"api_version": "API-versjon",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-entry",
"release_runtime": "Release-runtime",
"checking_again": "Sjekker igjen om {seconds}s",
"refresh_error": "Release-status kunne ikke oppdateres. Neste automatiske sjekk prøver igjen.",
"ignore": "Ignorer de neste 5 minuttene",
"check_again": "Sjekk igjen",
"logout": "Logg ut",
"base_image": "Basis-image"
},
"channel_switched": {
"kicker": "Release Manager",
"title": "Du er nå på {channel}",
"summary_prefix": "Kontoen din er tildelt release-kanalen",
"summary_suffix": "Denne enheten husker at du har sett denne meldingen.",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"frontend_version": "Frontend-versjon",
"api_version": "API-versjon",
"assigned_channel": "Tildelt kanal",
"current_app_image": "Nåværende app-image",
"current_api": "Nåværende API",
"base_image": "Basis-image",
"continue": "Fortsett"
},
"channel_selector": {
"title": "Release-kanal",
"subtitle": "Velg hvilken tildelt release-kanal denne enheten skal bruke.",
"sidebar_title": "Release",
"default": "Standard",
"ready": "Klar",
"unavailable": "Ikke klar",
"release_bundle": "Release-bundle",
"frontend_version": "Frontend-versjon",
"api_version": "API-versjon",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-entry",
"release_runtime": "Release-runtime",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"use_local_frontend": "Bruk lokal frontend"
},
"common": {
"yes": "ja",
"no": "nei",
"off": "av"
},
"actions": {
"edit": "Rediger",
"rollback": "Rull tilbake",
"update": "Opdater",
"create": "Opprett",
"clear": "Tøm",
"assign": "Tildel",
"remove": "Fjern",
"test_access": "Test tilgang",
"deploy": "Deploy",
"redeploy": "Redeploy",
"promote": "Promoter",
"enable": "Aktiver",
"search": "Søk",
"save_target": "Lagre mål",
"delete": "Slett",
"back": "Tilbake",
"next": "Neste"
},
"tables": {
"channel": "Kanal",
"rollout": "Udrulning",
"frontend": "Frontend",
"api": "API",
"replay": "Replay",
"actions": "Handlinger",
"module": "Modul",
"status": "Status",
"checked": "Kontrollert",
"subject": "Emne",
"reason": "Årsag",
"expires": "Utløper",
"deployment": "Deployment",
"source": "Kilde",
"started": "Startet",
"target": "Mål",
"coolify": "Coolify",
"auto": "Auto"
},
"values": {
"app": {
"frontend": "frontend",
"api": "api"
},
"capture_level": {
"metadata": "metadata",
"full_redacted": "fullt redigert"
},
"subject_type": {
"user": "bruker",
"subuser": "medarbeider",
"customer": "kunde",
"channel": "kanal"
},
"severity": {
"error": "feil",
"warning": "advarsel",
"info": "info"
},
"status": {
"ok": "ok",
"active": "aktiv",
"deployed": "deployet",
"stable": "stabil",
"enabled": "aktivert",
"queued": "i kø",
"deploying": "deploying",
"metadata": "metadata",
"not_configured": "ikke konfigurert",
"warning": "advarsel",
"failed": "feilet",
"down": "nede",
"error": "feil",
"unknown": "ukjent"
}
},
"autocomplete": {
"suggested_value": "Foreslået værdi",
"suggestion": "Forslag",
"use_typed_value": "Brug indtastet værdi",
"custom": "Tilpasset",
"api_url": "API-URL",
"frontend_url": "Frontend-URL",
"known_release_url": "Kendt release-URL",
"deploy_here_with_coolify": "Deploy {app} her med Coolify",
"prepare_coolify_target": "Forbered et Coolify-mål med HTTPS/SSL aktiveret",
"coolify_ssl": "Coolify SSL",
"github_repository": "GitHub-repository",
"owner_repository": "ejer/repository",
"git_branch": "Git-branch",
"deployment_branch": "Deployment-branch",
"github_commit": "GitHub-commit",
"git_commit": "Git-commit",
"use_specific_commit_sha": "Brug specifik commit-SHA",
"custom_commit": "Tilpasset commit",
"health_url": "Health-URL",
"release_health_check": "Release health check",
"coolify_service": "Coolify-service",
"service_uuid": "Service-UUID",
"known_coolify_service_uuid": "Kendt Coolify service-UUID",
"control_api": "Kontrol-API",
"release_manager_api_endpoint": "Release Manager API-endpoint",
"module": "Modul",
"timeline_module_key": "Timeline-modulnøgle"
},
"errors": {
"github_repositories": "Kunne ikke indlæse GitHub-repositories.",
"github_branches": "Kunne ikke indlæse GitHub-branches.",
"repository_branch_required": "Repository og branch er påkrævet før GitHub-adgang kan testes.",
"github_access_failed": "Test af GitHub repository-adgang fejlede."
},
"channels": {
"title": "Kanaler",
"subtitle": "Opprett og juster gradvise release-kanaler.",
"rollout_percent": "{percent}% udrulning",
"already_configured": "Allerede konfigurert",
"slug": "Slug",
"slug_message": "Eksempel: canary",
"slug_placeholder": "slug",
"name": "Navn",
"name_message": "Vises til operatører når de gjennomgår en kanal.",
"name_placeholder": "Navn",
"rollout": "Udrulning",
"rollout_message": "0-100 prosent av ikke-tildelt trafikk.",
"frontend_url": "Frontend-URL",
"frontend_url_message": "Eksempel: https://canary.example.test",
"frontend_url_placeholder": "Frontend-URL",
"no_matching_release_url": "Ingen matchende release-URL.",
"api_url": "API-URL",
"api_url_message": "Eksempel: https://api-canary.example.test",
"api_url_placeholder": "API-URL",
"no_matching_api_url": "Ingen matchende API-URL.",
"replay_capture": "Replay capture",
"replay_capture_message": "Metadata er sikrere til brede kanaler.",
"retention_days": "Oppbevaringsdager",
"retention_days_message": "1-365 dage.",
"enabled": "Aktivert",
"default_channel": "Standardkanal",
"replay_enabled": "Replay aktivert",
"frontend_chip": "Frontend: {url}",
"api_chip": "API: {url}"
},
"assignments": {
"title": "Tildelinger",
"subtitle": "Fest brukere, medarbeidere eller kunder til release-kanaler.",
"subject": "Emne",
"subject_message": "Søk etter brukere, medarbeidere eller kunder, eller skriv et manuelt emne som user:42.",
"subject_placeholder": "Søk eller skriv emne",
"subject_empty": "Ingen emner funnet",
"group_users": "Brukere",
"group_subusers": "Medarbeidere",
"group_customers": "Kunder",
"group_manual": "Manuell",
"manual_user": "Bruker #{id}",
"manual_subuser": "Medarbeider #{id}",
"manual_customer": "Kunde #{id}",
"manual_description": "Bruk inntastet verdi {subject}",
"subject_type": "Emnetype",
"subject_type_message": "Vælg identitetstypen, der skal fastgøres.",
"customer": "Kunde",
"customer_message": "Søk på kundenummer eller navn, og velg deretter nøyaktig kunde.",
"customer_placeholder": "Søk kunde",
"subject_id": "Emne-ID",
"subject_id_message": "Eksempel: bruker-id 42",
"subject_id_placeholder": "Emne-id",
"channel": "Kanal",
"channel_message": "Festede emner hopper over prosentutrullingen.",
"reason": "Årsag",
"reason_message": "Eksempel: canary-validering for kundesupport.",
"reason_placeholder": "Årsag",
"expires": "Utløper",
"expires_message": "Lad feltet være tomt for ingen automatisk udløb."
},
"deployments": {
"title": "Deployments",
"subtitle": "Start, promoter og inspiser release-deployments.",
"manual_deployment_record": "Manuel deployment-post",
"target": "Mål",
"target_message": "Vælg et gemt mål, eller behold manuelle felter redigerbare.",
"select_target_or_manual": "Velg mål eller manuelt",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/front-end-vue",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: master",
"branch_placeholder": "branch",
"commit_selection": "Commit-valg",
"commit_selection_message": "Seneste slås op som branchens head med den konfigurerede GitHub-token.",
"latest_from_branch": "Siste fra branch",
"specific_commit": "Spesifikk commit",
"specific_commit_message": "Eksempel: feedface1234",
"commit_sha_placeholder": "commit sha",
"version_label": "Versjonsetikett",
"version_label_message": "Eksempel: api-2026.05.19",
"version_label_placeholder": "versionslabel",
"commit_prefix": "Commit: {commit}",
"test_access_tooltip": "Bekræft token-adgang til det valgte repository, branch og commit."
},
"replay": {
"title": "Replay",
"subtitle": "Aktiver målrettet capture og gennemgå fejltimelines trin for trin.",
"capture_channel": "Capture {channel}",
"target_type": "Måltype",
"target": "Mål",
"target_message": "Eksempel: bruker-id 42 eller kanal-slug canary.",
"target_placeholder": "Mål-id eller kanal-slug",
"channel_scope": "Kanalavgrensning",
"channel_scope_message": "Valgfrit; begrænser replay capture til en kanal.",
"no_channel_scope": "Ingen kanalavgrensning",
"capture_level": "Capture-niveau",
"expires": "Utløper",
"expires_message": "Lad feltet være tomt for manuel deaktivering.",
"trace_id": "Trace-ID",
"trace_id_message": "Eksempel: trace-canary-1",
"trace_id_placeholder": "Trace-id",
"channel": "Kanal",
"channel_placeholder": "Kanal",
"principal_id": "Principal-ID",
"principal_id_placeholder": "Principal-id",
"module": "Modul",
"module_placeholder": "Modul",
"severity": "Alvorlighet",
"any_severity": "Alle alvorligheder",
"no_timeline_events": "Ingen timeline-events matcher de aktuelle filtre."
},
"integrations": {
"title": "GitHub og Coolify",
"subtitle": "Knyt GitHub-repositories og branches til kanalspecifikke Coolify-services.",
"choose_repository": "Velg repository",
"add_health_url": "legg til health-URL",
"github_token_not_configured": "GitHub-token er ikke konfigurert; private repositories kan ikke vises.",
"open_github_token_settings": "Åpne GitHub token-innstillinger",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/backend-php",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Seneste: {commit}",
"coolify_instance": "Coolify-instans",
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
"no_coolify_instance": "Ingen Coolify-instans",
"coolify_instance_placeholder": "Coolify-instans-id",
"coolify_service_uuid": "Coolify service-UUID",
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
"coolify_service_uuid_placeholder": "Coolify service-UUID",
"health_url": "Health-URL",
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
"health_url_placeholder": "Health-URL",
"ssl_domain": "Load balancer-domene",
"ssl_domain_message": "Må være et DNS-domene som routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer-domene",
"https_domain_for_coolify": "Domene kontrollert av load balanceren",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
"deploy_now": "Deploy nå",
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
"create_missing_service": "Opprett manglende service",
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
}
}
}
}
@@ -0,0 +1,61 @@
{
"compat": {
"configuration": {
"release_manager": {
"integrations": {
"title": "GitHub og Coolify",
"subtitle": "Knyt GitHub-repositories og branches til kanalspecifikke Coolify-services.",
"choose_repository": "Velg repository",
"add_health_url": "legg til health-URL",
"github_token_not_configured": "GitHub-token er ikke konfigurert; private repositories kan ikke vises.",
"open_github_token_settings": "Åpne GitHub token-innstillinger",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/backend-php",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Seneste: {commit}",
"coolify_instance": "Coolify-instans",
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
"no_coolify_instance": "Ingen Coolify-instans",
"coolify_instance_placeholder": "Coolify-instans-id",
"coolify_service_uuid": "Coolify service-UUID",
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
"coolify_service_uuid_placeholder": "Coolify service-UUID",
"health_url": "Health-URL",
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
"health_url_placeholder": "Health-URL",
"ssl_domain": "Load balancer-domene",
"ssl_domain_message": "Må være et DNS-domene som routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer-domene",
"https_domain_for_coolify": "Domene kontrollert av load balanceren",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
"deploy_now": "Deploy nå",
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
"create_missing_service": "Opprett manglende service",
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
}
}
}
}
}
@@ -0,0 +1,454 @@
{
"compat": {
"configuration": {
"release_manager": {
"title": "Release Manager",
"subtitle": "Gradvise API- og Front-End-utgivelser",
"channel_names": {
"stable": "Stabil",
"canary": "Canary",
"internal": "Intern"
},
"channel_descriptions": {
"stable": "Standard produksjonskanal.",
"canary": "Tidlig produksjonsvalideringskanal.",
"internal": "Intern kanal for ansatte og superbrukervalidering."
},
"control_api": {
"title": "Kontroll-API",
"tooltip": "Release Manager-handlinger sendes til denne API-en. Bruk produksjons-API-en med mindre du tester en staging-backend.",
"example": "Eksempel: https://api.truckwash.io:4433",
"endpoint_label": "API-endepunkt",
"endpoint_message": "Endre dette bare når Release Manager-endepunkter er tilgjengelige på mål-API-en.",
"endpoint_aria": "Release Manager kontroll-API URL",
"use_tooltip": "Last release-data fra denne API-en",
"use": "Bruk",
"reset_tooltip": "Gå tilbake til standard kontroll-API",
"reset": "Tilbakestill",
"known_endpoints": "Kjente kontroll-API-endepunkter"
},
"tabs": {
"overview": {
"label": "Oversikt",
"description": "Kanalhelse, oppsettsfremdrift og siste release-status."
},
"channels": {
"label": "Kanaler",
"description": "Opprett stabile, canary- og målrettede kanaler med rollout-grenser."
},
"assignments": {
"label": "Tildelinger",
"description": "Knytt brukere, underbrukere eller kunder til en bestemt release-kanal."
},
"deployments": {
"label": "Utrullinger",
"description": "Start utrullinger og promoter vellykkede bygg."
},
"replay": {
"label": "Replay",
"description": "Aktiver målrettet innsamling og søk i release-tidslinjehendelser."
},
"integrations": {
"label": "Integrasjoner",
"description": "Koble GitHub-repositories, branches og Coolify-tjenester."
},
"settings": {
"label": "Innstillinger",
"description": "Konfigurer Release Managers GitHub-tilgang og webhook-innstillinger."
}
},
"loading": {
"summary": "Laster Release Manager-data...",
"refreshing_summary": "Oppdaterer Release Manager-data...",
"summary_detail": "Release-status, kanaler, deployments og servicehelse lastes.",
"channels": "Laster release-kanaler...",
"channels_detail": "Kanalvisninger og release-avhengigheter lastes.",
"assignments": "Laster release-tildelinger...",
"deployments": "Laster release-deployments...",
"operations": "Laster release-operasjoner...",
"data_services": "Laster release-datatjenester...",
"integrations": "Laster release-integrasjoner...",
"settings": "Laster Release Manager-innstillinger...",
"github_repositories": "Laster GitHub-repositories...",
"timeline_sessions": "Laster release-tidslinje-sesjoner...",
"timeline_events": "Laster release-tidslinjehendelser...",
"refreshing_timeline": "Oppdaterer release-tidslinje..."
},
"setup": {
"channels": "Kanaler",
"targets": "GitHub- og Coolify-mål",
"assignments": "Pilottildelinger",
"deployments": "Første utrulling",
"replay": "Replay-innsamling"
},
"stats": {
"channels": "Kanaler",
"targets": "Mål",
"deployments": "Utrullinger",
"timeline_events": "Tidslinjehendelser"
},
"overview": {
"title": "Oversikt",
"subtitle": "Aktuell kanalhelse, release-aktivitet og modulstatus.",
"guided_setup": "Veiledet oppsett",
"next_step": "Neste: {step}",
"ready": "Release Manager er klar for daglig drift.",
"add_suggested_channel": "Legg til foreslått kanal",
"module_health_empty": "Modul-health snapshots vises når probes er registrert."
},
"settings": {
"title": "Release Manager-innstillinger",
"subtitle": "Konfigurer GitHub-tokenet som brukes for private repositories og branch-oppslag.",
"github_token": "GitHub-token",
"github_api_url": "GitHub API-URL",
"webhook_secret": "Webhook-hemmelighet",
"github_webhook_secret": "GitHub webhook-hemmelighet",
"configured": "Konfigurert",
"not_configured": "Ikke konfigurert",
"loaded_from": "Lastet fra {variable}",
"set_below": "Angi {variable} nedenfor",
"private_repositories_prefix": "Private repositories leses med serverens miljøvariabel",
"private_repositories_or": "eller modulens konfigurasjonsvariabel",
"github_token_message": "Eksempel: github_pat_... med tilgang til de private repositories Release Manager ruller ut.",
"github_token_placeholder": "La stå tomt for å beholde eksisterende token",
"github_api_url_message": "Konfigurert i ReleaseManager.github_api_url. Bruk https://api.github.com med mindre GitHub Enterprise brukes.",
"webhook_secret_message": "Valgfritt; la stå tomt for å beholde eksisterende hemmelighet.",
"webhook_secret_placeholder": "Webhook HMAC-hemmelighet",
"save": "Lagre innstillinger",
"back_to_integrations": "Tilbake til integrasjoner",
"guide": {
"steps": {
"github_token": "GitHub-token",
"github_api_url": "GitHub API-URL",
"github_webhook_secret": "GitHub webhook secret",
"review": "Gennemgang"
},
"github_token_title": "Legg til GitHub-token",
"github_token_description": "Release Manager bruger denne token til at vise private repositories, branches og commits, før et deployment-mål gemmes eller testes.",
"configure_this_in": "Konfigurer dette i",
"server_environment_variable": "Serverens miljøvariabel",
"takes_precedence": "har forrang når den er satt.",
"recommended_access": "Anbefalt tilgang",
"recommended_access_description": "Fine-grained GitHub-token med Contents: read-adgang til hvert release-repository.",
"github_api_url_title": "Legg til GitHub API-URL",
"github_api_url_description": "Brug GitHub Cloud som standard, eller peg på REST API-base-URLen for en GitHub Enterprise Server-installation.",
"github_api_url_invalid": "Skriv inn en full HTTP- eller HTTPS-URL.",
"github_cloud": "GitHub Cloud",
"enterprise_example": "Enterprise-eksempel",
"github_webhook_secret_title": "Legg til GitHub webhook secret",
"github_webhook_secret_description": "Brug samme secret i GitHub repository-webhooks, så Release Manager kan verificere deployment-events, før de håndteres.",
"github_webhook_url": "GitHub webhook-URL:",
"store_shared_secret_in": "Lagre den delte secreten i",
"review_title": "Gå gjennom GitHub-oppsett",
"review_description": "Gem værdierne, og brug derefter Integrationer til at vælge et privat repository, branch og specifik commit eller seneste commit før adgang testes.",
"ready": "Klar",
"needs_value": "Mangler værdi"
}
},
"channel_unavailable": {
"kicker": "Release Manager",
"title": "Release-kanalen er ikke klar",
"summary_prefix": "Kontoen din er tildelt",
"summary_suffix": ", men kanalen mangler påkrevd release-konfigurasjon.",
"release_bundle": "Release-bundle",
"frontend_version": "Frontend-versjon",
"api_version": "API-versjon",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-entry",
"release_runtime": "Release-runtime",
"checking_again": "Sjekker igjen om {seconds}s",
"refresh_error": "Release-status kunne ikke oppdateres. Neste automatiske sjekk prøver igjen.",
"ignore": "Ignorer de neste 5 minuttene",
"check_again": "Sjekk igjen",
"logout": "Logg ut",
"base_image": "Basis-image"
},
"channel_switched": {
"kicker": "Release Manager",
"title": "Du er nå på {channel}",
"summary_prefix": "Kontoen din er tildelt release-kanalen",
"summary_suffix": "Denne enheten husker at du har sett denne meldingen.",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"frontend_version": "Frontend-versjon",
"api_version": "API-versjon",
"assigned_channel": "Tildelt kanal",
"current_app_image": "Nåværende app-image",
"current_api": "Nåværende API",
"base_image": "Basis-image",
"continue": "Fortsett"
},
"channel_selector": {
"title": "Release-kanal",
"subtitle": "Velg hvilken tildelt release-kanal denne enheten skal bruke.",
"sidebar_title": "Release",
"default": "Standard",
"ready": "Klar",
"unavailable": "Ikke klar",
"release_bundle": "Release-bundle",
"frontend_version": "Frontend-versjon",
"api_version": "API-versjon",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-entry",
"release_runtime": "Release-runtime",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"use_local_frontend": "Bruk lokal frontend",
"switch_error": "Release channel could not be switched. The previous channel is still active."
},
"common": {
"yes": "ja",
"no": "nei",
"off": "av"
},
"actions": {
"edit": "Rediger",
"rollback": "Rull tilbake",
"update": "Opdater",
"create": "Opprett",
"clear": "Tøm",
"assign": "Tildel",
"remove": "Fjern",
"test_access": "Test tilgang",
"deploy": "Deploy",
"redeploy": "Redeploy",
"promote": "Promoter",
"enable": "Aktiver",
"search": "Søk",
"save_target": "Lagre mål",
"delete": "Slett",
"back": "Tilbake",
"next": "Neste",
"publish_release": "Publish release"
},
"tables": {
"channel": "Kanal",
"rollout": "Udrulning",
"frontend": "Frontend",
"api": "API",
"replay": "Replay",
"actions": "Handlinger",
"module": "Modul",
"status": "Status",
"checked": "Kontrollert",
"subject": "Emne",
"reason": "Årsag",
"expires": "Utløper",
"deployment": "Deployment",
"source": "Kilde",
"started": "Startet",
"target": "Mål",
"coolify": "Coolify",
"auto": "Auto"
},
"values": {
"app": {
"frontend": "frontend",
"api": "api"
},
"capture_level": {
"metadata": "metadata",
"full_redacted": "fullt redigert"
},
"subject_type": {
"user": "bruker",
"subuser": "medarbeider",
"customer": "kunde",
"channel": "kanal"
},
"severity": {
"error": "feil",
"warning": "advarsel",
"info": "info"
},
"status": {
"ok": "ok",
"active": "aktiv",
"deployed": "deployet",
"stable": "stabil",
"enabled": "aktivert",
"queued": "i kø",
"deploying": "deploying",
"metadata": "metadata",
"not_configured": "ikke konfigurert",
"warning": "advarsel",
"failed": "feilet",
"down": "nede",
"error": "feil",
"unknown": "ukjent"
}
},
"autocomplete": {
"suggested_value": "Foreslået værdi",
"suggestion": "Forslag",
"use_typed_value": "Brug indtastet værdi",
"custom": "Tilpasset",
"api_url": "API-URL",
"frontend_url": "Frontend-URL",
"known_release_url": "Kendt release-URL",
"deploy_here_with_coolify": "Deploy {app} her med Coolify",
"prepare_coolify_target": "Forbered et Coolify-mål med HTTPS/SSL aktiveret",
"coolify_ssl": "Coolify SSL",
"github_repository": "GitHub-repository",
"owner_repository": "ejer/repository",
"git_branch": "Git-branch",
"deployment_branch": "Deployment-branch",
"github_commit": "GitHub-commit",
"git_commit": "Git-commit",
"use_specific_commit_sha": "Brug specifik commit-SHA",
"custom_commit": "Tilpasset commit",
"health_url": "Health-URL",
"release_health_check": "Release health check",
"coolify_service": "Coolify-service",
"service_uuid": "Service-UUID",
"known_coolify_service_uuid": "Kendt Coolify service-UUID",
"control_api": "Kontrol-API",
"release_manager_api_endpoint": "Release Manager API-endpoint",
"module": "Modul",
"timeline_module_key": "Timeline-modulnøgle"
},
"errors": {
"github_repositories": "Kunne ikke indlæse GitHub-repositories.",
"github_branches": "Kunne ikke indlæse GitHub-branches.",
"repository_branch_required": "Repository og branch er påkrævet før GitHub-adgang kan testes.",
"github_access_failed": "Test af GitHub repository-adgang fejlede."
},
"channels": {
"title": "Kanaler",
"subtitle": "Opprett og juster gradvise release-kanaler.",
"rollout_percent": "{percent}% udrulning",
"already_configured": "Allerede konfigurert",
"slug": "Slug",
"slug_message": "Eksempel: canary",
"slug_placeholder": "slug",
"name": "Navn",
"name_message": "Vises til operatører når de gjennomgår en kanal.",
"name_placeholder": "Navn",
"rollout": "Udrulning",
"rollout_message": "0-100 prosent av ikke-tildelt trafikk.",
"frontend_url": "Frontend-URL",
"frontend_url_message": "Eksempel: https://canary.example.test",
"frontend_url_placeholder": "Frontend-URL",
"no_matching_release_url": "Ingen matchende release-URL.",
"api_url": "API-URL",
"api_url_message": "Eksempel: https://api-canary.example.test",
"api_url_placeholder": "API-URL",
"no_matching_api_url": "Ingen matchende API-URL.",
"replay_capture": "Replay capture",
"replay_capture_message": "Metadata er sikrere til brede kanaler.",
"retention_days": "Oppbevaringsdager",
"retention_days_message": "1-365 dage.",
"enabled": "Aktivert",
"default_channel": "Standardkanal",
"replay_enabled": "Replay aktivert",
"frontend_chip": "Frontend: {url}",
"api_chip": "API: {url}"
},
"assignments": {
"title": "Tildelinger",
"subtitle": "Fest brukere, medarbeidere eller kunder til release-kanaler.",
"subject": "Emne",
"subject_message": "Søk etter brukere, medarbeidere eller kunder, eller skriv et manuelt emne som user:42.",
"subject_placeholder": "Søk eller skriv emne",
"subject_empty": "Ingen emner funnet",
"group_users": "Brukere",
"group_subusers": "Medarbeidere",
"group_customers": "Kunder",
"group_manual": "Manuell",
"manual_user": "Bruker #{id}",
"manual_subuser": "Medarbeider #{id}",
"manual_customer": "Kunde #{id}",
"manual_description": "Bruk inntastet verdi {subject}",
"subject_type": "Emnetype",
"subject_type_message": "Vælg identitetstypen, der skal fastgøres.",
"customer": "Kunde",
"customer_message": "Søk på kundenummer eller navn, og velg deretter nøyaktig kunde.",
"customer_placeholder": "Søk kunde",
"subject_id": "Emne-ID",
"subject_id_message": "Eksempel: bruker-id 42",
"subject_id_placeholder": "Emne-id",
"channel": "Kanal",
"channel_message": "Festede emner hopper over prosentutrullingen.",
"reason": "Årsag",
"reason_message": "Eksempel: canary-validering for kundesupport.",
"reason_placeholder": "Årsag",
"expires": "Utløper",
"expires_message": "Lad feltet være tomt for ingen automatisk udløb."
},
"deployments": {
"title": "Deployments",
"subtitle": "Start, promoter og inspiser release-deployments.",
"manual_deployment_record": "Manuel deployment-post",
"target": "Mål",
"target_message": "Vælg et gemt mål, eller behold manuelle felter redigerbare.",
"select_target_or_manual": "Velg mål eller manuelt",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/front-end-vue",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: master",
"branch_placeholder": "branch",
"commit_selection": "Commit-valg",
"commit_selection_message": "Seneste slås op som branchens head med den konfigurerede GitHub-token.",
"latest_from_branch": "Siste fra branch",
"specific_commit": "Spesifikk commit",
"specific_commit_message": "Eksempel: feedface1234",
"commit_sha_placeholder": "commit sha",
"version_label": "Versjonsetikett",
"version_label_message": "Eksempel: api-2026.05.19",
"version_label_placeholder": "versionslabel",
"commit_prefix": "Commit: {commit}",
"test_access_tooltip": "Bekræft token-adgang til det valgte repository, branch og commit."
},
"replay": {
"title": "Replay",
"subtitle": "Aktiver målrettet capture og gennemgå fejltimelines trin for trin.",
"capture_channel": "Capture {channel}",
"target_type": "Måltype",
"target": "Mål",
"target_message": "Eksempel: bruker-id 42 eller kanal-slug canary.",
"target_placeholder": "Mål-id eller kanal-slug",
"channel_scope": "Kanalavgrensning",
"channel_scope_message": "Valgfrit; begrænser replay capture til en kanal.",
"no_channel_scope": "Ingen kanalavgrensning",
"capture_level": "Capture-niveau",
"expires": "Utløper",
"expires_message": "Lad feltet være tomt for manuel deaktivering.",
"trace_id": "Trace-ID",
"trace_id_message": "Eksempel: trace-canary-1",
"trace_id_placeholder": "Trace-id",
"channel": "Kanal",
"channel_placeholder": "Kanal",
"principal_id": "Principal-ID",
"principal_id_placeholder": "Principal-id",
"module": "Modul",
"module_placeholder": "Modul",
"severity": "Alvorlighet",
"any_severity": "Alle alvorligheder",
"no_timeline_events": "Ingen timeline-events matcher de aktuelle filtre."
},
"status": {
"confirm_issue_action": "Run this Release Manager action? It can change deployment state and will be audited.",
"choose_bundle_prompt": "Enter the bundle id to set for this channel.",
"impact": "Impact",
"cause": "Cause",
"automated_fix": "Automated fix",
"manual_fallback": "Manual fallback",
"related_deployment": "Related deployment",
"recent_result": "Recent result",
"no_automated_action": "No automated action available.",
"deployment": "Deployment",
"target": "Target",
"coolify_target": "Coolify target",
"service_set": "Service set"
}
}
}
}
}
@@ -0,0 +1,28 @@
{
"compat": {
"admin": {
"pos": {
"settings_wheel": {
"accept_self_serve_wash": "Godkänn självbetjäningstvätt",
"accept_self_serve_wash_confirm": "Flytta detta utkast till kund #{customerNumber} med självbetjäningsdetaljerna?",
"accept_self_serve_wash_success": "Självbetjäningstvätten godkändes.",
"email_notifications_section": "E-postaviseringar",
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
"resend_booking_confirmation": "(Re)send booking confirmation",
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
"resend_booking_confirmation_success": "The booking confirmation has been sent",
"resend_wash_certificate": "(Re)send wash certificate",
"resend_wash_certificate_error": "The wash certificate could not be sent",
"resend_wash_certificate_success": "The wash certificate has been sent",
"self_serve_customer": "Kund",
"self_serve_driver": "Förare",
"self_serve_elapsed": "Tid",
"self_serve_wash_attachment": "Självbetjäningstvätt",
"self_serve_wash_attachment_for_customer": "Självbetjäningstvätt för kund #{customerNumber}"
}
}
}
}
}
@@ -298,490 +298,6 @@
"subtitle": "@.capitalize:{'terms.glossary.konfiguration'} @:{'terms.glossary.af'} @:{'terms.glossary.xlvask'} @:{'terms.glossary.integrationen'}",
"title": "@:{'terms.glossary.xlvask'} @:{'terms.glossary.konfiguration'}",
"username_desc": "Användarnamnet @:{'terms.glossary.anvands'} @:{'terms.glossary.for'} @:{'terms.glossary.att'} @:{'terms.glossary.autentisera'} @:{'terms.glossary.api'}-@:{'terms.glossary.anslutningen'}."
},
"release_manager": {
"title": "Release Manager",
"subtitle": "Gradvisa API- och Front-End-versioner",
"channel_names": {
"stable": "Stabil",
"canary": "Canary",
"internal": "Intern"
},
"channel_descriptions": {
"stable": "Standardkanal för produktion.",
"canary": "Tidig produktionsvalideringskanal.",
"internal": "Intern kanal för personal och superanvändarvalidering."
},
"control_api": {
"title": "Kontroll-API",
"tooltip": "Release Manager-åtgärder skickas till detta API. Använd produktions-API:t om du inte testar en staging-backend.",
"example": "Exempel: https://api.truckwash.io:4433",
"endpoint_label": "API-endpoint",
"endpoint_message": "Ändra detta endast när Release Manager-endpoints finns på mål-API:t.",
"endpoint_aria": "Release Manager kontroll-API URL",
"use_tooltip": "Läs in release-data från detta API",
"use": "Använd",
"reset_tooltip": "Gå tillbaka till standard kontroll-API",
"reset": "Återställ",
"known_endpoints": "Kända kontroll-API-endpoints"
},
"tabs": {
"overview": {
"label": "Översikt",
"description": "Kanalhälsa, installationsstatus och senaste release-läge."
},
"channels": {
"label": "Kanaler",
"description": "Skapa stabila, canary- och riktade kanaler med rollout-gränser."
},
"assignments": {
"label": "Tilldelningar",
"description": "Koppla användare, underanvändare eller kunder till en specifik release-kanal."
},
"deployments": {
"label": "Utrullningar",
"description": "Starta utrullningar och promovera lyckade byggen."
},
"replay": {
"label": "Replay",
"description": "Aktivera riktad insamling och sök i release-tidslinjehändelser."
},
"integrations": {
"label": "Integrationer",
"description": "Anslut GitHub-repositories, branches och Coolify-tjänster."
},
"settings": {
"label": "Inställningar",
"description": "Konfigurera Release Managers GitHub-åtkomst och webhook-inställningar."
}
},
"loading": {
"summary": "Laddar Release Manager-data...",
"refreshing_summary": "Uppdaterar Release Manager-data...",
"summary_detail": "Release-status, kanaler, deployments och servicehalsa laddas.",
"channels": "Laddar release-kanaler...",
"channels_detail": "Kanalvyer och release-beroenden laddas.",
"assignments": "Laddar release-tilldelningar...",
"deployments": "Laddar release-deployments...",
"operations": "Laddar release-operationer...",
"data_services": "Laddar release-datatjanster...",
"integrations": "Laddar release-integrationer...",
"settings": "Laddar Release Manager-installningar...",
"github_repositories": "Laddar GitHub-repositories...",
"timeline_sessions": "Laddar release-tidslinjesessioner...",
"timeline_events": "Laddar release-tidslinjehandelser...",
"refreshing_timeline": "Uppdaterar release-tidslinje..."
},
"setup": {
"channels": "Kanaler",
"targets": "GitHub- och Coolify-mål",
"assignments": "Pilottilldelningar",
"deployments": "Första utrullningen",
"replay": "Replay-insamling"
},
"stats": {
"channels": "Kanaler",
"targets": "Mål",
"deployments": "Utrullningar",
"timeline_events": "Tidslinjehändelser"
},
"overview": {
"title": "Översikt",
"subtitle": "Aktuell kanalhälsa, release-aktivitet och modulstatus.",
"guided_setup": "Guidad installation",
"next_step": "Nästa: {step}",
"ready": "Release Manager är klar för daglig drift.",
"add_suggested_channel": "Lägg till föreslagen kanal",
"module_health_empty": "Modulhälsosnapshots visas när probes har registrerats."
},
"settings": {
"title": "Release Manager-inställningar",
"subtitle": "Konfigurera GitHub-token som används för privata repositories och branch-uppslag.",
"github_token": "GitHub-token",
"github_api_url": "GitHub API-URL",
"webhook_secret": "Webhook-hemlighet",
"github_webhook_secret": "GitHub webhook-hemlighet",
"configured": "Konfigurerad",
"not_configured": "Inte konfigurerad",
"loaded_from": "Inläst från {variable}",
"set_below": "Ange {variable} nedan",
"private_repositories_prefix": "Privata repositories läses med serverns miljövariabel",
"private_repositories_or": "eller modulens konfigurationsvariabel",
"github_token_message": "Exempel: github_pat_... med åtkomst till de privata repositories som Release Manager distribuerar.",
"github_token_placeholder": "Lämna tomt för att behålla befintlig token",
"github_api_url_message": "Konfigurerad i ReleaseManager.github_api_url. Använd https://api.github.com om du inte använder GitHub Enterprise.",
"webhook_secret_message": "Valfritt; lämna tomt för att behålla befintlig hemlighet.",
"webhook_secret_placeholder": "Webhook HMAC-hemlighet",
"save": "Spara inställningar",
"back_to_integrations": "Tillbaka till integrationer",
"guide": {
"steps": {
"github_token": "GitHub-token",
"github_api_url": "GitHub API-URL",
"github_webhook_secret": "GitHub webhook secret",
"review": "Gennemgang"
},
"github_token_title": "Lägg till GitHub-token",
"github_token_description": "Release Manager använder denna token för att lista privata repositories, branches och commits innan ett deployment-mål sparas eller testas.",
"configure_this_in": "Konfigurera detta i",
"server_environment_variable": "Serverns miljövariabel",
"takes_precedence": "har företräde när den är satt.",
"recommended_access": "Rekommenderad åtkomst",
"recommended_access_description": "Fine-grained GitHub-token med Contents: read-åtkomst till varje release-repository.",
"github_api_url_title": "Lägg till GitHub API-URL",
"github_api_url_description": "Brug GitHub Cloud som standard, eller peg på REST API-base-URLen for en GitHub Enterprise Server-installation.",
"github_api_url_invalid": "Ange en fullständig HTTP- eller HTTPS-URL.",
"github_cloud": "GitHub Cloud",
"enterprise_example": "Enterprise-exempel",
"github_webhook_secret_title": "Lägg till GitHub webhook secret",
"github_webhook_secret_description": "Brug samme secret i GitHub repository-webhooks, så Release Manager kan verificere deployment-events, før de håndteres.",
"github_webhook_url": "GitHub webhook-URL:",
"store_shared_secret_in": "Spara den delade secret i",
"review_title": "Granska GitHub-konfiguration",
"review_description": "Gem værdierne, og brug derefter Integrationer til at vælge et privat repository, branch og specifik commit eller seneste commit før adgang testes.",
"ready": "Klar",
"needs_value": "Saknar värde"
}
},
"channel_unavailable": {
"kicker": "Release Manager",
"title": "Release-kanalen är inte klar",
"summary_prefix": "Ditt konto är tilldelat",
"summary_suffix": ", men kanalen saknar obligatorisk release-konfiguration.",
"release_bundle": "Release-bundle",
"frontend_version": "Frontend-version",
"api_version": "API-version",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-entry",
"release_runtime": "Release-runtime",
"checking_again": "Kontrollerar igen om {seconds}s",
"refresh_error": "Release-status kunde inte uppdateras. Nästa automatiska kontroll försöker igen.",
"ignore": "Ignorera de kommande 5 minuterna",
"check_again": "Kontrollera igen",
"logout": "Logga ut",
"base_image": "Bas-image"
},
"channel_switched": {
"kicker": "Release Manager",
"title": "Du är nu på {channel}",
"summary_prefix": "Ditt konto har tilldelats release-kanalen",
"summary_suffix": "Den här enheten kommer ihåg att du har sett detta meddelande.",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"frontend_version": "Frontend-version",
"api_version": "API-version",
"assigned_channel": "Tilldelad kanal",
"current_app_image": "Nuvarande app-image",
"current_api": "Nuvarande API",
"base_image": "Bas-image",
"continue": "Fortsätt"
},
"channel_selector": {
"title": "Release-kanal",
"subtitle": "Välj vilken tilldelad release-kanal den här enheten ska använda.",
"sidebar_title": "Release",
"default": "Standard",
"ready": "Klar",
"unavailable": "Inte klar",
"release_bundle": "Release-bundle",
"frontend_version": "Frontend-version",
"api_version": "API-version",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-entry",
"release_runtime": "Release-runtime",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"use_local_frontend": "Använd lokal frontend"
},
"common": {
"yes": "ja",
"no": "nej",
"off": "av"
},
"actions": {
"edit": "Redigera",
"rollback": "Rulla tillbaka",
"update": "Uppdatera",
"create": "Skapa",
"clear": "Rensa",
"assign": "Tilldela",
"remove": "Ta bort",
"test_access": "Testa åtkomst",
"deploy": "Deploy",
"redeploy": "Redeploy",
"promote": "Promota",
"enable": "Aktivera",
"search": "Sök",
"save_target": "Spara mål",
"delete": "Radera",
"back": "Tillbaka",
"next": "Nästa"
},
"tables": {
"channel": "Kanal",
"rollout": "Udrulning",
"frontend": "Frontend",
"api": "API",
"replay": "Replay",
"actions": "Åtgärder",
"module": "Modul",
"status": "Status",
"checked": "Kontrollerad",
"subject": "Emne",
"reason": "Orsak",
"expires": "Upphör",
"deployment": "Deployment",
"source": "Källa",
"started": "Startet",
"target": "Mål",
"coolify": "Coolify",
"auto": "Auto"
},
"values": {
"app": {
"frontend": "frontend",
"api": "api"
},
"capture_level": {
"metadata": "metadata",
"full_redacted": "helt redigerad"
},
"subject_type": {
"user": "användare",
"subuser": "medarbetare",
"customer": "kund",
"channel": "kanal"
},
"severity": {
"error": "fel",
"warning": "varning",
"info": "info"
},
"status": {
"ok": "ok",
"active": "aktiv",
"deployed": "deployad",
"stable": "stabil",
"enabled": "aktiverad",
"queued": "i kö",
"deploying": "deployar",
"metadata": "metadata",
"not_configured": "inte konfigurerad",
"warning": "varning",
"failed": "misslyckad",
"down": "nere",
"error": "fel",
"unknown": "okänd"
}
},
"autocomplete": {
"suggested_value": "Foreslået værdi",
"suggestion": "Forslag",
"use_typed_value": "Brug indtastet værdi",
"custom": "Tilpasset",
"api_url": "API-URL",
"frontend_url": "Frontend-URL",
"known_release_url": "Kendt release-URL",
"deploy_here_with_coolify": "Deploy {app} her med Coolify",
"prepare_coolify_target": "Forbered et Coolify-mål med HTTPS/SSL aktiveret",
"coolify_ssl": "Coolify SSL",
"github_repository": "GitHub-repository",
"owner_repository": "ejer/repository",
"git_branch": "Git-branch",
"deployment_branch": "Deployment-branch",
"github_commit": "GitHub-commit",
"git_commit": "Git-commit",
"use_specific_commit_sha": "Brug specifik commit-SHA",
"custom_commit": "Tilpasset commit",
"health_url": "Health-URL",
"release_health_check": "Release health check",
"coolify_service": "Coolify-service",
"service_uuid": "Service-UUID",
"known_coolify_service_uuid": "Kendt Coolify service-UUID",
"control_api": "Kontrol-API",
"release_manager_api_endpoint": "Release Manager API-endpoint",
"module": "Modul",
"timeline_module_key": "Timeline-modulnøgle"
},
"errors": {
"github_repositories": "Kunne ikke indlæse GitHub-repositories.",
"github_branches": "Kunne ikke indlæse GitHub-branches.",
"repository_branch_required": "Repository og branch er påkrævet før GitHub-adgang kan testes.",
"github_access_failed": "Test af GitHub repository-adgang fejlede."
},
"channels": {
"title": "Kanaler",
"subtitle": "Skapa och justera gradvisa release-kanaler.",
"rollout_percent": "{percent}% utrullning",
"already_configured": "Redan konfigurerad",
"slug": "Slug",
"slug_message": "Eksempel: canary",
"slug_placeholder": "slug",
"name": "Namn",
"name_message": "Visas för operatörer när de granskar en kanal.",
"name_placeholder": "Namn",
"rollout": "Utrullning",
"rollout_message": "0-100 procent av otilldelad trafik.",
"frontend_url": "Frontend-URL",
"frontend_url_message": "Eksempel: https://canary.example.test",
"frontend_url_placeholder": "Frontend-URL",
"no_matching_release_url": "Ingen matchande release-URL.",
"api_url": "API-URL",
"api_url_message": "Eksempel: https://api-canary.example.test",
"api_url_placeholder": "API-URL",
"no_matching_api_url": "Ingen matchande API-URL.",
"replay_capture": "Replay capture",
"replay_capture_message": "Metadata är säkrare för breda kanaler.",
"retention_days": "Lagringsdagar",
"retention_days_message": "1-365 dagar.",
"enabled": "Aktiverad",
"default_channel": "Standardkanal",
"replay_enabled": "Replay aktiverad",
"frontend_chip": "Frontend: {url}",
"api_chip": "API: {url}"
},
"assignments": {
"title": "Tilldelningar",
"subtitle": "Fäst användare, medarbetare eller kunder till release-kanaler.",
"subject": "Ämne",
"subject_message": "Sök efter användare, medarbetare eller kunder, eller skriv ett manuellt ämne som user:42.",
"subject_placeholder": "Sök eller skriv ämne",
"subject_empty": "Inga ämnen hittades",
"group_users": "Användare",
"group_subusers": "Medarbetare",
"group_customers": "Kunder",
"group_manual": "Manuell",
"manual_user": "Användare #{id}",
"manual_subuser": "Medarbetare #{id}",
"manual_customer": "Kund #{id}",
"manual_description": "Använd inmatat värde {subject}",
"subject_type": "Ämnestyp",
"subject_type_message": "Välj identitetstypen som ska fästas.",
"customer": "Kunde",
"customer_message": "Sök på kundnummer eller namn och välj sedan exakt kund.",
"customer_placeholder": "Sök kund",
"subject_id": "Ämnes-ID",
"subject_id_message": "Exempel: användar-id 42",
"subject_id_placeholder": "Ämnes-id",
"channel": "Kanal",
"channel_message": "Fästa ämnen hoppar över procentutrullningen.",
"reason": "Orsak",
"reason_message": "Exempel: canary-validering för kundsupport.",
"reason_placeholder": "Orsak",
"expires": "Upphör",
"expires_message": "Lämna tomt för ingen automatisk upphörning."
},
"deployments": {
"title": "Deployments",
"subtitle": "Starta, promota och inspektera release-deployments.",
"manual_deployment_record": "Manuel deployment-post",
"target": "Mål",
"target_message": "Välj ett sparat mål eller behåll manuella fält redigerbara.",
"select_target_or_manual": "Vælg mål eller manuel",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/front-end-vue",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: master",
"branch_placeholder": "branch",
"commit_selection": "Commit-val",
"commit_selection_message": "Senaste slås upp som branchens head med konfigurerad GitHub-token.",
"latest_from_branch": "Senaste från branch",
"specific_commit": "Specifik commit",
"specific_commit_message": "Eksempel: feedface1234",
"commit_sha_placeholder": "commit sha",
"version_label": "Versionsetikett",
"version_label_message": "Eksempel: api-2026.05.19",
"version_label_placeholder": "versionsetikett",
"commit_prefix": "Commit: {commit}",
"test_access_tooltip": "Bekræft token-adgang til det valgte repository, branch og commit."
},
"replay": {
"title": "Replay",
"subtitle": "Aktivera riktad capture och gå igenom feltimelines steg för steg.",
"capture_channel": "Capture {channel}",
"target_type": "Måltype",
"target": "Mål",
"target_message": "Exempel: användar-id 42 eller kanal-slug canary.",
"target_placeholder": "Mål-id eller kanal-slug",
"channel_scope": "Kanalavgränsning",
"channel_scope_message": "Valgfrit; begrænser replay capture til en kanal.",
"no_channel_scope": "Ingen kanalavgränsning",
"capture_level": "Capture-niveau",
"expires": "Upphör",
"expires_message": "Lad feltet være tomt for manuel deaktivering.",
"trace_id": "Trace-ID",
"trace_id_message": "Eksempel: trace-canary-1",
"trace_id_placeholder": "Trace-id",
"channel": "Kanal",
"channel_placeholder": "Kanal",
"principal_id": "Principal-ID",
"principal_id_placeholder": "Principal-id",
"module": "Modul",
"module_placeholder": "Modul",
"severity": "Allvarlighetsgrad",
"any_severity": "Alla allvarlighetsgrader",
"no_timeline_events": "Ingen timeline-events matcher de aktuelle filtre."
},
"integrations": {
"title": "GitHub och Coolify",
"subtitle": "Koppla GitHub-repositories och branches till kanalspecifika Coolify-tjänster.",
"choose_repository": "Välj repository",
"add_health_url": "lägg till health-URL",
"github_token_not_configured": "GitHub-token er ikke konfigureret; private repositories kan ikke vises.",
"open_github_token_settings": "Öppna GitHub token-inställningar",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/backend-php",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Seneste: {commit}",
"coolify_instance": "Coolify-instans",
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
"no_coolify_instance": "Ingen Coolify-instans",
"coolify_instance_placeholder": "Coolify-instans-id",
"coolify_service_uuid": "Coolify service-UUID",
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
"coolify_service_uuid_placeholder": "Coolify service-UUID",
"health_url": "Health-URL",
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
"health_url_placeholder": "Health-URL",
"ssl_domain": "Load balancer-domän",
"ssl_domain_message": "Måste vara en DNS-domän som routas till Coolify load balancern. Exempel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer-domän",
"https_domain_for_coolify": "Domän som kontrolleras av load balancern",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
"deploy_now": "Deploy nu",
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
"create_missing_service": "Skapa saknad tjänst",
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
}
}
}
}
@@ -0,0 +1,61 @@
{
"compat": {
"configuration": {
"release_manager": {
"integrations": {
"title": "GitHub och Coolify",
"subtitle": "Koppla GitHub-repositories och branches till kanalspecifika Coolify-tjänster.",
"choose_repository": "Välj repository",
"add_health_url": "lägg till health-URL",
"github_token_not_configured": "GitHub-token er ikke konfigureret; private repositories kan ikke vises.",
"open_github_token_settings": "Öppna GitHub token-inställningar",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/backend-php",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: release/canary",
"branch_placeholder": "branch",
"latest_prefix": "Seneste: {commit}",
"coolify_instance": "Coolify-instans",
"coolify_instance_message": "Lad feltet være tomt for manuelle deployment-poster.",
"no_coolify_instance": "Ingen Coolify-instans",
"coolify_instance_placeholder": "Coolify-instans-id",
"coolify_service_uuid": "Coolify service-UUID",
"coolify_service_uuid_message": "Vælg en eksisterende service eller indsæt en UUID.",
"coolify_service_uuid_placeholder": "Coolify service-UUID",
"health_url": "Health-URL",
"health_url_message": "Eksempel: https://api-canary.example.test/ping",
"health_url_placeholder": "Health-URL",
"ssl_domain": "Load balancer-domän",
"ssl_domain_message": "Måste vara en DNS-domän som routas till Coolify load balancern. Exempel: api-v2.truckwash.io",
"ssl_domain_placeholder": "Load balancer-domän",
"https_domain_for_coolify": "Domän som kontrolleras av load balancern",
"endpoint_mode": "Endpoint mode",
"endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.",
"endpoint_mode_auto": "Auto",
"endpoint_mode_manual": "Manual",
"manual_endpoint_host": "Manual host",
"manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.",
"manual_endpoint_host_placeholder": "api-v2.truckwash.io",
"manual_endpoint_port": "Manual public port",
"manual_endpoint_port_message": "Optional public port from 1 to 65535.",
"app_port": "App port",
"app_port_message": "Internal container port exposed to Coolify routing.",
"auto_gateway_endpoint": "Automatic gateway endpoint",
"pending_automatic_endpoint": "Automatic endpoint resolution pending",
"auto_deploy": "Auto deploy",
"auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.",
"coolify_ssl": "Coolify SSL",
"coolify_ssl_tooltip": "Bed Coolify om at oprette HTTPS for det konfigurerede domæne.",
"deploy_now": "Deploy nu",
"deploy_now_tooltip": "Start en deployment straks efter målet gemmes.",
"create_missing_service": "Skapa saknad tjänst",
"create_missing_service_tooltip": "Tillad Release Manager at oprette en manglende Coolify-service.",
"test_access_tooltip": "Bekræft, at den konfigurerede GitHub-token kan læse dette repository og branch."
}
}
}
}
}
@@ -0,0 +1,454 @@
{
"compat": {
"configuration": {
"release_manager": {
"title": "Release Manager",
"subtitle": "Gradvisa API- och Front-End-versioner",
"channel_names": {
"stable": "Stabil",
"canary": "Canary",
"internal": "Intern"
},
"channel_descriptions": {
"stable": "Standardkanal för produktion.",
"canary": "Tidig produktionsvalideringskanal.",
"internal": "Intern kanal för personal och superanvändarvalidering."
},
"control_api": {
"title": "Kontroll-API",
"tooltip": "Release Manager-åtgärder skickas till detta API. Använd produktions-API:t om du inte testar en staging-backend.",
"example": "Exempel: https://api.truckwash.io:4433",
"endpoint_label": "API-endpoint",
"endpoint_message": "Ändra detta endast när Release Manager-endpoints finns på mål-API:t.",
"endpoint_aria": "Release Manager kontroll-API URL",
"use_tooltip": "Läs in release-data från detta API",
"use": "Använd",
"reset_tooltip": "Gå tillbaka till standard kontroll-API",
"reset": "Återställ",
"known_endpoints": "Kända kontroll-API-endpoints"
},
"tabs": {
"overview": {
"label": "Översikt",
"description": "Kanalhälsa, installationsstatus och senaste release-läge."
},
"channels": {
"label": "Kanaler",
"description": "Skapa stabila, canary- och riktade kanaler med rollout-gränser."
},
"assignments": {
"label": "Tilldelningar",
"description": "Koppla användare, underanvändare eller kunder till en specifik release-kanal."
},
"deployments": {
"label": "Utrullningar",
"description": "Starta utrullningar och promovera lyckade byggen."
},
"replay": {
"label": "Replay",
"description": "Aktivera riktad insamling och sök i release-tidslinjehändelser."
},
"integrations": {
"label": "Integrationer",
"description": "Anslut GitHub-repositories, branches och Coolify-tjänster."
},
"settings": {
"label": "Inställningar",
"description": "Konfigurera Release Managers GitHub-åtkomst och webhook-inställningar."
}
},
"loading": {
"summary": "Laddar Release Manager-data...",
"refreshing_summary": "Uppdaterar Release Manager-data...",
"summary_detail": "Release-status, kanaler, deployments och servicehalsa laddas.",
"channels": "Laddar release-kanaler...",
"channels_detail": "Kanalvyer och release-beroenden laddas.",
"assignments": "Laddar release-tilldelningar...",
"deployments": "Laddar release-deployments...",
"operations": "Laddar release-operationer...",
"data_services": "Laddar release-datatjanster...",
"integrations": "Laddar release-integrationer...",
"settings": "Laddar Release Manager-installningar...",
"github_repositories": "Laddar GitHub-repositories...",
"timeline_sessions": "Laddar release-tidslinjesessioner...",
"timeline_events": "Laddar release-tidslinjehandelser...",
"refreshing_timeline": "Uppdaterar release-tidslinje..."
},
"setup": {
"channels": "Kanaler",
"targets": "GitHub- och Coolify-mål",
"assignments": "Pilottilldelningar",
"deployments": "Första utrullningen",
"replay": "Replay-insamling"
},
"stats": {
"channels": "Kanaler",
"targets": "Mål",
"deployments": "Utrullningar",
"timeline_events": "Tidslinjehändelser"
},
"overview": {
"title": "Översikt",
"subtitle": "Aktuell kanalhälsa, release-aktivitet och modulstatus.",
"guided_setup": "Guidad installation",
"next_step": "Nästa: {step}",
"ready": "Release Manager är klar för daglig drift.",
"add_suggested_channel": "Lägg till föreslagen kanal",
"module_health_empty": "Modulhälsosnapshots visas när probes har registrerats."
},
"settings": {
"title": "Release Manager-inställningar",
"subtitle": "Konfigurera GitHub-token som används för privata repositories och branch-uppslag.",
"github_token": "GitHub-token",
"github_api_url": "GitHub API-URL",
"webhook_secret": "Webhook-hemlighet",
"github_webhook_secret": "GitHub webhook-hemlighet",
"configured": "Konfigurerad",
"not_configured": "Inte konfigurerad",
"loaded_from": "Inläst från {variable}",
"set_below": "Ange {variable} nedan",
"private_repositories_prefix": "Privata repositories läses med serverns miljövariabel",
"private_repositories_or": "eller modulens konfigurationsvariabel",
"github_token_message": "Exempel: github_pat_... med åtkomst till de privata repositories som Release Manager distribuerar.",
"github_token_placeholder": "Lämna tomt för att behålla befintlig token",
"github_api_url_message": "Konfigurerad i ReleaseManager.github_api_url. Använd https://api.github.com om du inte använder GitHub Enterprise.",
"webhook_secret_message": "Valfritt; lämna tomt för att behålla befintlig hemlighet.",
"webhook_secret_placeholder": "Webhook HMAC-hemlighet",
"save": "Spara inställningar",
"back_to_integrations": "Tillbaka till integrationer",
"guide": {
"steps": {
"github_token": "GitHub-token",
"github_api_url": "GitHub API-URL",
"github_webhook_secret": "GitHub webhook secret",
"review": "Gennemgang"
},
"github_token_title": "Lägg till GitHub-token",
"github_token_description": "Release Manager använder denna token för att lista privata repositories, branches och commits innan ett deployment-mål sparas eller testas.",
"configure_this_in": "Konfigurera detta i",
"server_environment_variable": "Serverns miljövariabel",
"takes_precedence": "har företräde när den är satt.",
"recommended_access": "Rekommenderad åtkomst",
"recommended_access_description": "Fine-grained GitHub-token med Contents: read-åtkomst till varje release-repository.",
"github_api_url_title": "Lägg till GitHub API-URL",
"github_api_url_description": "Brug GitHub Cloud som standard, eller peg på REST API-base-URLen for en GitHub Enterprise Server-installation.",
"github_api_url_invalid": "Ange en fullständig HTTP- eller HTTPS-URL.",
"github_cloud": "GitHub Cloud",
"enterprise_example": "Enterprise-exempel",
"github_webhook_secret_title": "Lägg till GitHub webhook secret",
"github_webhook_secret_description": "Brug samme secret i GitHub repository-webhooks, så Release Manager kan verificere deployment-events, før de håndteres.",
"github_webhook_url": "GitHub webhook-URL:",
"store_shared_secret_in": "Spara den delade secret i",
"review_title": "Granska GitHub-konfiguration",
"review_description": "Gem værdierne, og brug derefter Integrationer til at vælge et privat repository, branch og specifik commit eller seneste commit før adgang testes.",
"ready": "Klar",
"needs_value": "Saknar värde"
}
},
"channel_unavailable": {
"kicker": "Release Manager",
"title": "Release-kanalen är inte klar",
"summary_prefix": "Ditt konto är tilldelat",
"summary_suffix": ", men kanalen saknar obligatorisk release-konfiguration.",
"release_bundle": "Release-bundle",
"frontend_version": "Frontend-version",
"api_version": "API-version",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-entry",
"release_runtime": "Release-runtime",
"checking_again": "Kontrollerar igen om {seconds}s",
"refresh_error": "Release-status kunde inte uppdateras. Nästa automatiska kontroll försöker igen.",
"ignore": "Ignorera de kommande 5 minuterna",
"check_again": "Kontrollera igen",
"logout": "Logga ut",
"base_image": "Bas-image"
},
"channel_switched": {
"kicker": "Release Manager",
"title": "Du är nu på {channel}",
"summary_prefix": "Ditt konto har tilldelats release-kanalen",
"summary_suffix": "Den här enheten kommer ihåg att du har sett detta meddelande.",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"frontend_version": "Frontend-version",
"api_version": "API-version",
"assigned_channel": "Tilldelad kanal",
"current_app_image": "Nuvarande app-image",
"current_api": "Nuvarande API",
"base_image": "Bas-image",
"continue": "Fortsätt"
},
"channel_selector": {
"title": "Release-kanal",
"subtitle": "Välj vilken tilldelad release-kanal den här enheten ska använda.",
"sidebar_title": "Release",
"default": "Standard",
"ready": "Klar",
"unavailable": "Inte klar",
"release_bundle": "Release-bundle",
"frontend_version": "Frontend-version",
"api_version": "API-version",
"frontend_base_url": "Frontend-URL",
"api_base_url": "API-URL",
"frontend_entry": "Frontend-entry",
"release_runtime": "Release-runtime",
"bundle": "Bundle",
"frontend": "Frontend",
"api": "API",
"use_local_frontend": "Använd lokal frontend",
"switch_error": "Release channel could not be switched. The previous channel is still active."
},
"common": {
"yes": "ja",
"no": "nej",
"off": "av"
},
"actions": {
"edit": "Redigera",
"rollback": "Rulla tillbaka",
"update": "Uppdatera",
"create": "Skapa",
"clear": "Rensa",
"assign": "Tilldela",
"remove": "Ta bort",
"test_access": "Testa åtkomst",
"deploy": "Deploy",
"redeploy": "Redeploy",
"promote": "Promota",
"enable": "Aktivera",
"search": "Sök",
"save_target": "Spara mål",
"delete": "Radera",
"back": "Tillbaka",
"next": "Nästa",
"publish_release": "Publish release"
},
"tables": {
"channel": "Kanal",
"rollout": "Udrulning",
"frontend": "Frontend",
"api": "API",
"replay": "Replay",
"actions": "Åtgärder",
"module": "Modul",
"status": "Status",
"checked": "Kontrollerad",
"subject": "Emne",
"reason": "Orsak",
"expires": "Upphör",
"deployment": "Deployment",
"source": "Källa",
"started": "Startet",
"target": "Mål",
"coolify": "Coolify",
"auto": "Auto"
},
"values": {
"app": {
"frontend": "frontend",
"api": "api"
},
"capture_level": {
"metadata": "metadata",
"full_redacted": "helt redigerad"
},
"subject_type": {
"user": "användare",
"subuser": "medarbetare",
"customer": "kund",
"channel": "kanal"
},
"severity": {
"error": "fel",
"warning": "varning",
"info": "info"
},
"status": {
"ok": "ok",
"active": "aktiv",
"deployed": "deployad",
"stable": "stabil",
"enabled": "aktiverad",
"queued": "i kö",
"deploying": "deployar",
"metadata": "metadata",
"not_configured": "inte konfigurerad",
"warning": "varning",
"failed": "misslyckad",
"down": "nere",
"error": "fel",
"unknown": "okänd"
}
},
"autocomplete": {
"suggested_value": "Foreslået værdi",
"suggestion": "Forslag",
"use_typed_value": "Brug indtastet værdi",
"custom": "Tilpasset",
"api_url": "API-URL",
"frontend_url": "Frontend-URL",
"known_release_url": "Kendt release-URL",
"deploy_here_with_coolify": "Deploy {app} her med Coolify",
"prepare_coolify_target": "Forbered et Coolify-mål med HTTPS/SSL aktiveret",
"coolify_ssl": "Coolify SSL",
"github_repository": "GitHub-repository",
"owner_repository": "ejer/repository",
"git_branch": "Git-branch",
"deployment_branch": "Deployment-branch",
"github_commit": "GitHub-commit",
"git_commit": "Git-commit",
"use_specific_commit_sha": "Brug specifik commit-SHA",
"custom_commit": "Tilpasset commit",
"health_url": "Health-URL",
"release_health_check": "Release health check",
"coolify_service": "Coolify-service",
"service_uuid": "Service-UUID",
"known_coolify_service_uuid": "Kendt Coolify service-UUID",
"control_api": "Kontrol-API",
"release_manager_api_endpoint": "Release Manager API-endpoint",
"module": "Modul",
"timeline_module_key": "Timeline-modulnøgle"
},
"errors": {
"github_repositories": "Kunne ikke indlæse GitHub-repositories.",
"github_branches": "Kunne ikke indlæse GitHub-branches.",
"repository_branch_required": "Repository og branch er påkrævet før GitHub-adgang kan testes.",
"github_access_failed": "Test af GitHub repository-adgang fejlede."
},
"channels": {
"title": "Kanaler",
"subtitle": "Skapa och justera gradvisa release-kanaler.",
"rollout_percent": "{percent}% utrullning",
"already_configured": "Redan konfigurerad",
"slug": "Slug",
"slug_message": "Eksempel: canary",
"slug_placeholder": "slug",
"name": "Namn",
"name_message": "Visas för operatörer när de granskar en kanal.",
"name_placeholder": "Namn",
"rollout": "Utrullning",
"rollout_message": "0-100 procent av otilldelad trafik.",
"frontend_url": "Frontend-URL",
"frontend_url_message": "Eksempel: https://canary.example.test",
"frontend_url_placeholder": "Frontend-URL",
"no_matching_release_url": "Ingen matchande release-URL.",
"api_url": "API-URL",
"api_url_message": "Eksempel: https://api-canary.example.test",
"api_url_placeholder": "API-URL",
"no_matching_api_url": "Ingen matchande API-URL.",
"replay_capture": "Replay capture",
"replay_capture_message": "Metadata är säkrare för breda kanaler.",
"retention_days": "Lagringsdagar",
"retention_days_message": "1-365 dagar.",
"enabled": "Aktiverad",
"default_channel": "Standardkanal",
"replay_enabled": "Replay aktiverad",
"frontend_chip": "Frontend: {url}",
"api_chip": "API: {url}"
},
"assignments": {
"title": "Tilldelningar",
"subtitle": "Fäst användare, medarbetare eller kunder till release-kanaler.",
"subject": "Ämne",
"subject_message": "Sök efter användare, medarbetare eller kunder, eller skriv ett manuellt ämne som user:42.",
"subject_placeholder": "Sök eller skriv ämne",
"subject_empty": "Inga ämnen hittades",
"group_users": "Användare",
"group_subusers": "Medarbetare",
"group_customers": "Kunder",
"group_manual": "Manuell",
"manual_user": "Användare #{id}",
"manual_subuser": "Medarbetare #{id}",
"manual_customer": "Kund #{id}",
"manual_description": "Använd inmatat värde {subject}",
"subject_type": "Ämnestyp",
"subject_type_message": "Välj identitetstypen som ska fästas.",
"customer": "Kunde",
"customer_message": "Sök på kundnummer eller namn och välj sedan exakt kund.",
"customer_placeholder": "Sök kund",
"subject_id": "Ämnes-ID",
"subject_id_message": "Exempel: användar-id 42",
"subject_id_placeholder": "Ämnes-id",
"channel": "Kanal",
"channel_message": "Fästa ämnen hoppar över procentutrullningen.",
"reason": "Orsak",
"reason_message": "Exempel: canary-validering för kundsupport.",
"reason_placeholder": "Orsak",
"expires": "Upphör",
"expires_message": "Lämna tomt för ingen automatisk upphörning."
},
"deployments": {
"title": "Deployments",
"subtitle": "Starta, promota och inspektera release-deployments.",
"manual_deployment_record": "Manuel deployment-post",
"target": "Mål",
"target_message": "Välj ett sparat mål eller behåll manuella fält redigerbara.",
"select_target_or_manual": "Vælg mål eller manuel",
"channel": "Kanal",
"app": "App",
"repository": "Repository",
"repository_message": "Eksempel: truckwash/front-end-vue",
"repository_placeholder": "ejer/repo",
"branch": "Branch",
"branch_message": "Eksempel: master",
"branch_placeholder": "branch",
"commit_selection": "Commit-val",
"commit_selection_message": "Senaste slås upp som branchens head med konfigurerad GitHub-token.",
"latest_from_branch": "Senaste från branch",
"specific_commit": "Specifik commit",
"specific_commit_message": "Eksempel: feedface1234",
"commit_sha_placeholder": "commit sha",
"version_label": "Versionsetikett",
"version_label_message": "Eksempel: api-2026.05.19",
"version_label_placeholder": "versionsetikett",
"commit_prefix": "Commit: {commit}",
"test_access_tooltip": "Bekræft token-adgang til det valgte repository, branch og commit."
},
"replay": {
"title": "Replay",
"subtitle": "Aktivera riktad capture och gå igenom feltimelines steg för steg.",
"capture_channel": "Capture {channel}",
"target_type": "Måltype",
"target": "Mål",
"target_message": "Exempel: användar-id 42 eller kanal-slug canary.",
"target_placeholder": "Mål-id eller kanal-slug",
"channel_scope": "Kanalavgränsning",
"channel_scope_message": "Valgfrit; begrænser replay capture til en kanal.",
"no_channel_scope": "Ingen kanalavgränsning",
"capture_level": "Capture-niveau",
"expires": "Upphör",
"expires_message": "Lad feltet være tomt for manuel deaktivering.",
"trace_id": "Trace-ID",
"trace_id_message": "Eksempel: trace-canary-1",
"trace_id_placeholder": "Trace-id",
"channel": "Kanal",
"channel_placeholder": "Kanal",
"principal_id": "Principal-ID",
"principal_id_placeholder": "Principal-id",
"module": "Modul",
"module_placeholder": "Modul",
"severity": "Allvarlighetsgrad",
"any_severity": "Alla allvarlighetsgrader",
"no_timeline_events": "Ingen timeline-events matcher de aktuelle filtre."
},
"status": {
"confirm_issue_action": "Run this Release Manager action? It can change deployment state and will be audited.",
"choose_bundle_prompt": "Enter the bundle id to set for this channel.",
"impact": "Impact",
"cause": "Cause",
"automated_fix": "Automated fix",
"manual_fallback": "Manual fallback",
"related_deployment": "Related deployment",
"recent_result": "Recent result",
"no_automated_action": "No automated action available.",
"deployment": "Deployment",
"target": "Target",
"coolify_target": "Coolify target",
"service_set": "Service set"
}
}
}
}
}
-1
View File
@@ -71,7 +71,6 @@ const logBuildBanner = () => {
'',
`Build: ${formatCommit(VITE_COMMIT_HASH)} @ ${formatDateTime(VITE_BUILD_DATE)} (${IS_DEV ? 'development' : 'production'})`,
`Remote API: ${getReleaseRuntimeApiBaseUrl()}`,
`Debug: Is running beta FE.`,
].join('\n'));
};
+16 -1
View File
@@ -84,8 +84,23 @@ export const sanitizeRedirectPath = (redirectPath) => {
return redirectPath;
};
export const buildMigrationUrl = (migrationOrigin, redirectPath) => {
export const releaseFrontendPathBase = (pathname = '') => {
const match = String(pathname || '').match(/^\/[^/]+\/frontend(?:\/|$)/);
return match ? match[0].replace(/\/+$/, '') : '';
};
export const applyReleaseFrontendBase = (redirectPath, currentPath = '') => {
const safePath = sanitizeRedirectPath(redirectPath);
const releaseBase = releaseFrontendPathBase(currentPath);
if (!releaseBase || safePath === releaseBase || safePath.startsWith(`${releaseBase}/`)) {
return safePath;
}
return safePath === '/' ? `${releaseBase}/` : `${releaseBase}${safePath}`;
};
export const buildMigrationUrl = (migrationOrigin, redirectPath, currentPath = '') => {
const safePath = applyReleaseFrontendBase(redirectPath, currentPath);
try {
return new URL(safePath, migrationOrigin).toString();
} catch (_error) {
+6 -1
View File
@@ -190,9 +190,14 @@ const EconomicQueuePlaywrightHarness = lazyView('@/views/testing/EconomicQueuePl
* departmentSelection: true if the department selection should be possible for the route
*/
export const releaseFrontendRouterBase = (pathname = globalThis?.location?.pathname || '/') => {
const match = String(pathname || '/').match(/^\/[^/]+\/frontend(?:\/|$)/);
return match ? `${match[0].replace(/\/+$/, '')}/` : '/';
};
// Export the router as router
export const router = createRouter({
history: createWebHistory(),
history: createWebHistory(releaseFrontendRouterBase()),
routes: [
...((import.meta.env.DEV || import.meta.env.MODE === 'test') ? [
{
+55
View File
@@ -0,0 +1,55 @@
import { SELF_SERVE_ATTACHMENT_TRUSTED_ORIGINS } from "@/config.js";
import { trustedReleaseOrigins } from "@/services/releaseTrust.js";
const normalizeOrigin = (value) => {
const raw = String(value || "").trim();
if (!raw) {
return "";
}
try {
return new URL(raw).origin;
} catch {
return "";
}
};
const isLocalHttpOrigin = (url) =>
url.protocol === "http:" && ["localhost", "127.0.0.1", "::1"].includes(url.hostname);
const attachmentTrustedOrigins = () =>
Array.from(
new Set(
[...trustedReleaseOrigins(), ...SELF_SERVE_ATTACHMENT_TRUSTED_ORIGINS]
.map(normalizeOrigin)
.filter(Boolean)
)
);
export const isSafeAttachmentDownloadLink = (value) => {
const raw = String(value || "").trim();
if (!raw || raw.startsWith("//")) {
return false;
}
if (raw.startsWith("/")) {
return true;
}
let url;
try {
url = new URL(raw);
} catch {
return false;
}
if (url.protocol !== "https:" && !isLocalHttpOrigin(url)) {
return false;
}
return attachmentTrustedOrigins().includes(url.origin);
};
export const safeAttachmentDownloadLink = (value) => {
const raw = String(value || "").trim();
return isSafeAttachmentDownloadLink(raw) ? raw : "";
};

Some files were not shown because too many files have changed in this diff Show More