Add unit tests for release manager i18n validation and implement ReleaseReplayInspector Vue component for timeline inspection and replay functionality.
This commit is contained in:
@@ -7,9 +7,17 @@ import {
|
||||
releaseChannelSwitchedStatus,
|
||||
} from "@/services/releaseChannelAvailability.js";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
const { t, te } = useI18n({ useScope: "global" });
|
||||
const tr = (key, params = {}) => t(`configuration.release_manager.channel_switched.${key}`, params);
|
||||
const status = computed(() => releaseChannelSwitchedStatus.value);
|
||||
const localizedChannelName = computed(() => {
|
||||
const key = `configuration.release_manager.channel_names.${status.value.channelSlug}`;
|
||||
return status.value.channelSlug && te(key) ? t(key) : status.value.channelName;
|
||||
});
|
||||
const localizedChannelDescription = computed(() => {
|
||||
const key = `configuration.release_manager.channel_descriptions.${status.value.channelSlug}`;
|
||||
return status.value.channelSlug && te(key) ? t(key) : status.value.description;
|
||||
});
|
||||
const frontendLabel = computed(() => status.value.frontendBaseUrl || tr("current_app_image"));
|
||||
const apiLabel = computed(() => status.value.apiBaseUrl || tr("current_api"));
|
||||
const frontendVersion = computed(() => status.value.versions?.frontend || null);
|
||||
@@ -42,25 +50,37 @@ const continueToApp = () => {
|
||||
</div>
|
||||
<div class="release-card release-card--channel">
|
||||
<i class="fas fa-code-branch"></i>
|
||||
<span>{{ status.channelSlug || "channel" }}</span>
|
||||
<span>{{ localizedChannelName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="release-switch-copy">
|
||||
<p class="release-kicker">{{ tr("kicker") }}</p>
|
||||
<h1>{{ tr("title", { channel: status.channelName }) }}</h1>
|
||||
<h1>{{ tr("title", { channel: localizedChannelName }) }}</h1>
|
||||
<p class="release-summary">
|
||||
{{ tr("summary_prefix") }} <strong>{{ status.channelName }}</strong> {{ tr("summary_suffix") }}
|
||||
{{ tr("summary_prefix") }} <strong>{{ localizedChannelName }}</strong> {{ tr("summary_suffix") }}
|
||||
</p>
|
||||
<p v-if="status.description" class="release-description">
|
||||
{{ status.description }}
|
||||
<p v-if="localizedChannelDescription" class="release-description">
|
||||
{{ localizedChannelDescription }}
|
||||
</p>
|
||||
|
||||
<div class="release-details" data-testid="release-channel-switched-details">
|
||||
<span><strong>{{ tr("frontend") }}</strong>{{ frontendLabel }}</span>
|
||||
<span><strong>{{ tr("api") }}</strong>{{ apiLabel }}</span>
|
||||
<span><strong>{{ tr("frontend_version") }}</strong>{{ versionLabel(frontendVersion, tr("assigned_channel")) }}</span>
|
||||
<span><strong>{{ tr("api_version") }}</strong>{{ versionLabel(apiVersion, tr("assigned_channel")) }}</span>
|
||||
<span
|
||||
><strong>{{ tr("frontend") }}</strong
|
||||
>{{ frontendLabel }}</span
|
||||
>
|
||||
<span
|
||||
><strong>{{ tr("api") }}</strong
|
||||
>{{ apiLabel }}</span
|
||||
>
|
||||
<span
|
||||
><strong>{{ tr("frontend_version") }}</strong
|
||||
>{{ versionLabel(frontendVersion, tr("assigned_channel")) }}</span
|
||||
>
|
||||
<span
|
||||
><strong>{{ tr("api_version") }}</strong
|
||||
>{{ versionLabel(apiVersion, tr("assigned_channel")) }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="release-actions">
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@/services/releaseChannelAvailability.js";
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
const { t, te } = useI18n({ useScope: "global" });
|
||||
const tr = (key, params = {}) => t(`configuration.release_manager.channel_unavailable.${key}`, params);
|
||||
const checking = ref(false);
|
||||
const checkError = ref("");
|
||||
@@ -21,6 +21,14 @@ let pollTimer = null;
|
||||
let countdownTimer = null;
|
||||
|
||||
const status = computed(() => releaseChannelUnavailableStatus.value);
|
||||
const localizedChannelName = computed(() => {
|
||||
const key = `configuration.release_manager.channel_names.${status.value.channelSlug}`;
|
||||
return status.value.channelSlug && te(key) ? t(key) : status.value.channelName;
|
||||
});
|
||||
const localizedChannelDescription = computed(() => {
|
||||
const key = `configuration.release_manager.channel_descriptions.${status.value.channelSlug}`;
|
||||
return status.value.channelSlug && te(key) ? t(key) : status.value.description;
|
||||
});
|
||||
const missingLabels = computed(() =>
|
||||
status.value.missing.map((key) =>
|
||||
key === "frontend_base_url" ? tr("frontend_url") : key === "api_base_url" ? tr("api_url") : key
|
||||
@@ -91,7 +99,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<div class="release-card release-card--channel">
|
||||
<i class="fas fa-code-branch"></i>
|
||||
<span>{{ status.channelSlug || "channel" }}</span>
|
||||
<span>{{ localizedChannelName }}</span>
|
||||
</div>
|
||||
<div class="release-card release-card--base">
|
||||
<i class="fas fa-layer-group"></i>
|
||||
@@ -103,11 +111,11 @@ onBeforeUnmount(() => {
|
||||
<p class="release-kicker">{{ tr("kicker") }}</p>
|
||||
<h1>{{ tr("title") }}</h1>
|
||||
<p class="release-summary">
|
||||
{{ tr("summary_prefix") }} <strong>{{ status.channelName }}</strong
|
||||
{{ tr("summary_prefix") }} <strong>{{ localizedChannelName }}</strong
|
||||
>{{ tr("summary_suffix") }}
|
||||
</p>
|
||||
<p v-if="status.description" class="release-description">
|
||||
{{ status.description }}
|
||||
<p v-if="localizedChannelDescription" class="release-description">
|
||||
{{ localizedChannelDescription }}
|
||||
</p>
|
||||
<div class="release-missing" data-testid="release-channel-missing">
|
||||
<span v-for="label in missingLabels" :key="label">{{ label }}</span>
|
||||
|
||||
@@ -0,0 +1,751 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { BAutocomplete } from "buefy";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||
import {
|
||||
getReleaseTimelineSession,
|
||||
listReleaseTimelineSessions,
|
||||
searchReleaseTimeline,
|
||||
setReleaseReplayTarget,
|
||||
} from "@/services/superuserReleases.js";
|
||||
|
||||
const props = defineProps({
|
||||
channels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
moduleKeys: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
canReplay: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["refresh-summary"]);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
const tr = (key, params = {}) => t(`configuration.release_manager.${key}`, params);
|
||||
const trFallback = (key, fallback, params = {}) => {
|
||||
const fullKey = `configuration.release_manager.${key}`;
|
||||
const translated = t(fullKey, params);
|
||||
return translated === fullKey ? fallback : translated;
|
||||
};
|
||||
const valueLabel = (group, value) => trFallback(`values.${group}.${value}`, String(value || ""));
|
||||
|
||||
const replayForm = reactive({
|
||||
target_type: "user",
|
||||
target_id: "",
|
||||
channel_id: null,
|
||||
capture_level: "full_redacted",
|
||||
expires_at: "",
|
||||
});
|
||||
|
||||
const timelineFilters = reactive({
|
||||
trace_id: "",
|
||||
channel_slug: "",
|
||||
principal_type: "",
|
||||
principal_id: "",
|
||||
customer_number: "",
|
||||
module_key: "",
|
||||
severity: "",
|
||||
event_type: "",
|
||||
device_type: "",
|
||||
frontend_version: "",
|
||||
api_version: "",
|
||||
date_from: "",
|
||||
date_to: "",
|
||||
has_error_report: false,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
const busy = ref("");
|
||||
const errors = ref([]);
|
||||
const timelineEvents = ref([]);
|
||||
const timelineSessions = ref([]);
|
||||
const selectedDetail = ref(null);
|
||||
const selectedDetailTab = ref("events");
|
||||
|
||||
const responseData = (response, fallback) => response?.data?.data ?? fallback;
|
||||
const parseError = (error) =>
|
||||
error?.response?.data?.data?.message || error?.response?.data?.message || error?.message || "Unknown error";
|
||||
|
||||
const channelOptions = computed(() => props.channels || []);
|
||||
const moduleOptions = computed(() => {
|
||||
const query = String(timelineFilters.module_key || "").toLowerCase();
|
||||
return (props.moduleKeys || [])
|
||||
.map((key) => ({
|
||||
value: key,
|
||||
title: key,
|
||||
description: trFallback("autocomplete.timeline_module_key", "Timeline module"),
|
||||
icon: "fas fa-puzzle-piece",
|
||||
}))
|
||||
.filter((option) => !query || option.value.toLowerCase().includes(query))
|
||||
.slice(0, 10);
|
||||
});
|
||||
|
||||
const sessionRows = computed(() => (Array.isArray(timelineSessions.value) ? timelineSessions.value : []));
|
||||
const selectedSession = computed(() => selectedDetail.value?.session || null);
|
||||
const detailEvents = computed(() =>
|
||||
Array.isArray(selectedDetail.value?.events) ? selectedDetail.value.events : []
|
||||
);
|
||||
const detailErrorReports = computed(() =>
|
||||
Array.isArray(selectedDetail.value?.error_reports) ? selectedDetail.value.error_reports : []
|
||||
);
|
||||
const selectedRelease = computed(() => selectedDetail.value?.release || selectedSession.value?.release || {});
|
||||
|
||||
const filterPayload = () => {
|
||||
const payload = {};
|
||||
for (const [key, value] of Object.entries(timelineFilters)) {
|
||||
if (value === "" || value === null || value === false) {
|
||||
continue;
|
||||
}
|
||||
payload[key] = value;
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
const statusClass = (status) => ({
|
||||
"is-ok": ["ok", "active", "deployed", "promoted", "ready", "stable", "enabled", "info"].includes(status),
|
||||
"is-degraded": ["queued", "deploying", "draft", "metadata", "warning"].includes(status),
|
||||
"is-down": ["failed", "down", "error"].includes(status),
|
||||
});
|
||||
|
||||
const formatDate = (value) => {
|
||||
if (!value) {
|
||||
return "--";
|
||||
}
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
||||
};
|
||||
|
||||
const shortValue = (value) => {
|
||||
const text = String(value || "");
|
||||
return text.length > 16 ? `${text.slice(0, 12)}...` : text || "--";
|
||||
};
|
||||
|
||||
const userLabel = (session) => {
|
||||
const user = session?.user || {};
|
||||
return [user.label, user.customer_number ? `Customer ${user.customer_number}` : null].filter(Boolean).join(" / ") || "--";
|
||||
};
|
||||
|
||||
const releaseLabel = (session, app) => {
|
||||
const release = session?.release?.[app] || {};
|
||||
return release.version_label || release.commit_sha || "--";
|
||||
};
|
||||
|
||||
const releaseApp = (app) => selectedRelease.value?.[app] || selectedSession.value?.release?.[app] || {};
|
||||
|
||||
const versionReferenceLabel = (app) => {
|
||||
const version = releaseApp(app)?.version;
|
||||
return version?.id ? `#${version.id} ${version.status || ""}`.trim() : "--";
|
||||
};
|
||||
|
||||
const deploymentReferenceLabel = (app) => {
|
||||
const deployment = releaseApp(app)?.deployment;
|
||||
return deployment?.id ? `#${deployment.id} ${deployment.status || ""}`.trim() : "--";
|
||||
};
|
||||
|
||||
const bundleReferenceLabel = () => {
|
||||
const bundle = selectedRelease.value?.bundle;
|
||||
return bundle?.id ? `#${bundle.id} ${bundle.version_label || bundle.status || ""}`.trim() : "--";
|
||||
};
|
||||
|
||||
const deviceLabel = (session) => {
|
||||
const device = session?.device || {};
|
||||
return [device.type, device.browser_name, device.os_name].filter(Boolean).join(" / ") || "--";
|
||||
};
|
||||
|
||||
const viewportLabel = (session) => {
|
||||
const device = session?.device || {};
|
||||
if (!device.viewport_width || !device.viewport_height) {
|
||||
return "--";
|
||||
}
|
||||
return `${device.viewport_width}x${device.viewport_height} @ ${device.device_pixel_ratio || 1}`;
|
||||
};
|
||||
|
||||
const jsonPreview = (value) => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "--";
|
||||
}
|
||||
return JSON.stringify(value, null, 2);
|
||||
};
|
||||
|
||||
async function run(key, callback) {
|
||||
busy.value = key;
|
||||
errors.value = [];
|
||||
try {
|
||||
await callback();
|
||||
} catch (error) {
|
||||
errors.value.push(error);
|
||||
} finally {
|
||||
busy.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function enableReplayTarget() {
|
||||
await run("replay:target", async () => {
|
||||
await setReleaseReplayTarget({ ...replayForm, enabled: true });
|
||||
Object.assign(replayForm, { target_id: "", expires_at: "" });
|
||||
emit("refresh-summary");
|
||||
await loadReplayData();
|
||||
});
|
||||
}
|
||||
|
||||
async function loadReplayData() {
|
||||
await run("timeline:search", async () => {
|
||||
const filters = filterPayload();
|
||||
const [sessionsResponse, eventsResponse] = await Promise.all([
|
||||
listReleaseTimelineSessions(filters),
|
||||
searchReleaseTimeline(filters),
|
||||
]);
|
||||
timelineSessions.value = responseData(sessionsResponse, []);
|
||||
timelineEvents.value = responseData(eventsResponse, []);
|
||||
if (selectedSession.value && !timelineSessions.value.some((session) => session.trace_id === selectedSession.value.trace_id)) {
|
||||
selectedDetail.value = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function inspectSession(session) {
|
||||
if (!session?.trace_id) {
|
||||
return;
|
||||
}
|
||||
await run(`session:${session.trace_id}`, async () => {
|
||||
selectedDetail.value = responseData(await getReleaseTimelineSession(session.trace_id), null);
|
||||
selectedDetailTab.value = "events";
|
||||
});
|
||||
}
|
||||
|
||||
function applyChannelTarget(channel) {
|
||||
Object.assign(replayForm, { target_type: "channel", target_id: channel.slug, channel_id: channel.id });
|
||||
}
|
||||
|
||||
function selectModule(option) {
|
||||
timelineFilters.module_key = option?.value || option || "";
|
||||
}
|
||||
|
||||
function closeDetailModal() {
|
||||
selectedDetail.value = null;
|
||||
selectedDetailTab.value = "events";
|
||||
}
|
||||
|
||||
onMounted(loadReplayData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="release-replay-inspector">
|
||||
<div v-if="errors.length" class="notification is-danger is-light">
|
||||
<p v-for="(error, index) in errors" :key="index">{{ parseError(error) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="release-chip-row" data-testid="release-replay-target-suggestions">
|
||||
<b-button
|
||||
v-for="channel in channelOptions"
|
||||
:key="channel.id"
|
||||
size="is-small"
|
||||
type="is-light"
|
||||
icon-left="circle"
|
||||
icon-pack="fas"
|
||||
@click="applyChannelTarget(channel)"
|
||||
>
|
||||
{{ tr("replay.capture_channel", { channel: channel.slug }) }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<form class="release-form" data-testid="release-replay-form" @submit.prevent="enableReplayTarget">
|
||||
<b-field :label="tr('replay.target_type')">
|
||||
<b-select v-model="replayForm.target_type" expanded>
|
||||
<option value="user">{{ valueLabel("subject_type", "user") }}</option>
|
||||
<option value="subuser">{{ valueLabel("subject_type", "subuser") }}</option>
|
||||
<option value="customer">{{ valueLabel("subject_type", "customer") }}</option>
|
||||
<option value="channel">{{ valueLabel("subject_type", "channel") }}</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.target')" :message="tr('replay.target_message')">
|
||||
<b-input v-model="replayForm.target_id" :placeholder="tr('replay.target_placeholder')" />
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.channel_scope')" :message="tr('replay.channel_scope_message')">
|
||||
<b-select v-model.number="replayForm.channel_id" expanded>
|
||||
<option :value="null">{{ tr("replay.no_channel_scope") }}</option>
|
||||
<option v-for="channel in channelOptions" :key="channel.id" :value="channel.id">{{ channel.slug }}</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.capture_level')">
|
||||
<b-select v-model="replayForm.capture_level" expanded>
|
||||
<option value="full_redacted">{{ valueLabel("capture_level", "full_redacted") }}</option>
|
||||
<option value="metadata">{{ valueLabel("capture_level", "metadata") }}</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.expires')" :message="tr('replay.expires_message')">
|
||||
<b-input v-model="replayForm.expires_at" type="datetime-local" />
|
||||
</b-field>
|
||||
<div class="release-form-actions">
|
||||
<b-button
|
||||
type="is-dark"
|
||||
native-type="submit"
|
||||
icon-left="play"
|
||||
icon-pack="fas"
|
||||
:disabled="!props.canReplay"
|
||||
:loading="busy === 'replay:target'"
|
||||
>
|
||||
{{ tr("actions.enable") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form class="release-form mt-3" data-testid="release-timeline-filter-form" @submit.prevent="loadReplayData">
|
||||
<b-field :label="tr('replay.trace_id')" :message="tr('replay.trace_id_message')">
|
||||
<b-input v-model="timelineFilters.trace_id" :placeholder="tr('replay.trace_id_placeholder')" />
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.channel')">
|
||||
<b-input v-model="timelineFilters.channel_slug" :placeholder="tr('replay.channel_placeholder')" />
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.principal_id')">
|
||||
<b-input v-model="timelineFilters.principal_id" :placeholder="tr('replay.principal_id_placeholder')" />
|
||||
</b-field>
|
||||
<b-field :label="trFallback('replay.customer_number', 'Customer number')">
|
||||
<b-input v-model="timelineFilters.customer_number" placeholder="Customer number" />
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.module')">
|
||||
<BAutocomplete
|
||||
v-model="timelineFilters.module_key"
|
||||
:data="moduleOptions"
|
||||
field="value"
|
||||
:placeholder="tr('replay.module_placeholder')"
|
||||
open-on-focus
|
||||
keep-first
|
||||
expanded
|
||||
@select="selectModule"
|
||||
>
|
||||
<template #default="slotProps">
|
||||
<div class="release-autocomplete-option">
|
||||
<i :class="slotProps.option.icon" aria-hidden="true"></i>
|
||||
<span>
|
||||
<strong>{{ slotProps.option.title }}</strong>
|
||||
<small>{{ slotProps.option.description }}</small>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</BAutocomplete>
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.severity')">
|
||||
<b-select v-model="timelineFilters.severity" expanded>
|
||||
<option value="">{{ tr("replay.any_severity") }}</option>
|
||||
<option value="error">{{ valueLabel("severity", "error") }}</option>
|
||||
<option value="warning">{{ valueLabel("severity", "warning") }}</option>
|
||||
<option value="info">{{ valueLabel("severity", "info") }}</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="trFallback('replay.event_type', 'Event type')">
|
||||
<b-input v-model="timelineFilters.event_type" placeholder="request_failed" />
|
||||
</b-field>
|
||||
<b-field :label="trFallback('replay.device_type', 'Device type')">
|
||||
<b-select v-model="timelineFilters.device_type" expanded>
|
||||
<option value="">{{ trFallback("replay.any_device", "Any device") }}</option>
|
||||
<option value="desktop">desktop</option>
|
||||
<option value="tablet">tablet</option>
|
||||
<option value="mobile">mobile</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="trFallback('replay.frontend_release', 'Frontend release')">
|
||||
<b-input v-model="timelineFilters.frontend_version" placeholder="frontend-2026.05.19" />
|
||||
</b-field>
|
||||
<b-field :label="trFallback('replay.api_release', 'API release')">
|
||||
<b-input v-model="timelineFilters.api_version" placeholder="api-2026.05.19" />
|
||||
</b-field>
|
||||
<b-field :label="trFallback('replay.date_from', 'From')">
|
||||
<b-input v-model="timelineFilters.date_from" type="datetime-local" />
|
||||
</b-field>
|
||||
<b-field :label="trFallback('replay.date_to', 'To')">
|
||||
<b-input v-model="timelineFilters.date_to" type="datetime-local" />
|
||||
</b-field>
|
||||
<b-field :label="trFallback('replay.error_reports', 'Error reports')">
|
||||
<b-checkbox v-model="timelineFilters.has_error_report">
|
||||
{{ trFallback("replay.has_error_report", "Has error report") }}
|
||||
</b-checkbox>
|
||||
</b-field>
|
||||
<div class="release-form-actions">
|
||||
<b-button
|
||||
native-type="submit"
|
||||
icon-left="search"
|
||||
icon-pack="fas"
|
||||
:disabled="!props.canReplay"
|
||||
:loading="busy === 'timeline:search'"
|
||||
>
|
||||
{{ tr("actions.search") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-container mt-3">
|
||||
<table class="table is-fullwidth is-hoverable release-session-table" data-testid="release-timeline-sessions">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ tr("replay.trace_id") }}</th>
|
||||
<th>{{ trFallback("replay.user", "User") }}</th>
|
||||
<th>{{ trFallback("replay.device", "Device") }}</th>
|
||||
<th>{{ tr("replay.channel") }}</th>
|
||||
<th>{{ trFallback("replay.release", "Release") }}</th>
|
||||
<th>{{ trFallback("replay.events", "Events") }}</th>
|
||||
<th>{{ trFallback("replay.last_seen", "Last seen") }}</th>
|
||||
<th>{{ trFallback("actions.inspect", "Inspect") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="session in sessionRows" :key="session.trace_id">
|
||||
<td><code>{{ shortValue(session.trace_id) }}</code></td>
|
||||
<td>{{ userLabel(session) }}</td>
|
||||
<td>{{ deviceLabel(session) }}</td>
|
||||
<td>{{ session.channel_slug || "--" }}</td>
|
||||
<td>{{ releaseLabel(session, "frontend") }} / {{ releaseLabel(session, "api") }}</td>
|
||||
<td>
|
||||
{{ session.event_count || 0 }}
|
||||
<b-tag v-if="session.error_count" type="is-danger" size="is-small">{{ session.error_count }}</b-tag>
|
||||
<b-tag v-if="session.error_report_count" type="is-warning" size="is-small">
|
||||
{{ session.error_report_count }}
|
||||
</b-tag>
|
||||
</td>
|
||||
<td>{{ formatDate(session.last_event_at || session.last_seen_at) }}</td>
|
||||
<td class="release-action-cell">
|
||||
<ActionSettingsWheelButton
|
||||
class="release-session-actions"
|
||||
:data-testid="`release-session-actions-${session.trace_id}`"
|
||||
>
|
||||
<template #actions>
|
||||
<ActionSettingsWheelItem
|
||||
icon="fas fa-search"
|
||||
:label="trFallback('actions.inspect', 'Inspect')"
|
||||
:click-action="() => inspectSession(session)"
|
||||
:disabled="busy === `session:${session.trace_id}`"
|
||||
/>
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="sessionRows.length === 0">
|
||||
<td colspan="8">{{ tr("replay.no_timeline_events") }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="selectedDetail"
|
||||
class="modal is-active release-inspector-modal"
|
||||
data-testid="release-timeline-session-modal"
|
||||
>
|
||||
<div class="modal-background release-inspector-modal__backdrop" @click="closeDetailModal"></div>
|
||||
<section class="release-inspector-modal__card" data-testid="release-timeline-session-detail">
|
||||
<header class="release-inspector-modal__header">
|
||||
<div>
|
||||
<p class="release-inspector-modal__eyebrow">{{ trFallback("replay.debug_inspection", "Debug inspection") }}</p>
|
||||
<h3>{{ userLabel(selectedSession) }}</h3>
|
||||
<p class="release-inspector-modal__trace"><code>{{ selectedSession.trace_id }}</code></p>
|
||||
</div>
|
||||
<div class="release-inspector-modal__meta">
|
||||
<b-tag :class="statusClass(selectedSession.error_count ? 'error' : 'info')">
|
||||
{{ selectedSession.error_count ? "errors" : "info" }}
|
||||
</b-tag>
|
||||
<button class="delete" type="button" aria-label="close" @click="closeDetailModal"></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="release-inspector-modal__summary">
|
||||
<span>{{ selectedSession.channel_slug || "--" }}</span>
|
||||
<span>{{ deviceLabel(selectedSession) }}</span>
|
||||
<span>{{ releaseLabel(selectedSession, "frontend") }} / {{ releaseLabel(selectedSession, "api") }}</span>
|
||||
<span>{{ selectedSession.event_count || 0 }} {{ trFallback("replay.events", "Events") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="release-inspector-modal__tabs" role="tablist">
|
||||
<button class="button is-small" :class="{ 'is-dark': selectedDetailTab === 'events' }" type="button" @click="selectedDetailTab = 'events'">
|
||||
{{ trFallback("replay.events", "Events") }}
|
||||
</button>
|
||||
<button class="button is-small" :class="{ 'is-dark': selectedDetailTab === 'release' }" type="button" @click="selectedDetailTab = 'release'">
|
||||
{{ trFallback("replay.release", "Release") }}
|
||||
</button>
|
||||
<button class="button is-small" :class="{ 'is-dark': selectedDetailTab === 'device' }" type="button" @click="selectedDetailTab = 'device'">
|
||||
{{ trFallback("replay.device", "Device") }}
|
||||
</button>
|
||||
<button class="button is-small" :class="{ 'is-dark': selectedDetailTab === 'reports' }" type="button" @click="selectedDetailTab = 'reports'">
|
||||
{{ trFallback("replay.error_reports", "Error reports") }}
|
||||
</button>
|
||||
<button class="button is-small" type="button" disabled>
|
||||
{{ trFallback("replay.visual_replay", "Visual replay") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="release-inspector-modal__body">
|
||||
<div v-if="selectedDetailTab === 'events'" class="release-detail-panel">
|
||||
<ol class="release-timeline" data-testid="release-timeline-events">
|
||||
<li v-for="event in detailEvents" :key="event.id">
|
||||
<time>{{ formatDate(event.occurred_at) }}</time>
|
||||
<strong>{{ event.event_type }}</strong>
|
||||
<b-tag class="ml-1" :class="statusClass(event.severity)">{{ valueLabel("severity", event.severity) }}</b-tag>
|
||||
<div class="release-muted">
|
||||
{{ event.channel_slug || "--" }} / {{ event.module_key || "--" }} /
|
||||
{{ event.route_path || event.component || "--" }}
|
||||
</div>
|
||||
<details>
|
||||
<summary>{{ trFallback("replay.payload", "Payload") }}</summary>
|
||||
<pre>{{ jsonPreview(event.payload) }}</pre>
|
||||
</details>
|
||||
</li>
|
||||
<li v-if="detailEvents.length === 0">{{ tr("replay.no_timeline_events") }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div v-else-if="selectedDetailTab === 'release'" class="release-detail-panel release-kv-grid">
|
||||
<span>Channel</span><strong>{{ selectedSession.channel_slug || "--" }}</strong>
|
||||
<span>Frontend</span><strong>{{ releaseApp("frontend").version_label || releaseLabel(selectedSession, "frontend") }}</strong>
|
||||
<span>Frontend commit</span><strong>{{ releaseApp("frontend").commit_sha || "--" }}</strong>
|
||||
<span>Frontend version ref</span><strong>{{ versionReferenceLabel("frontend") }}</strong>
|
||||
<span>Frontend deployment</span><strong>{{ deploymentReferenceLabel("frontend") }}</strong>
|
||||
<span>API</span><strong>{{ releaseApp("api").version_label || releaseLabel(selectedSession, "api") }}</strong>
|
||||
<span>API commit</span><strong>{{ releaseApp("api").commit_sha || "--" }}</strong>
|
||||
<span>API version ref</span><strong>{{ versionReferenceLabel("api") }}</strong>
|
||||
<span>API deployment</span><strong>{{ deploymentReferenceLabel("api") }}</strong>
|
||||
<span>Bundle</span><strong>{{ bundleReferenceLabel() }}</strong>
|
||||
<span>Route</span><strong>{{ selectedSession.last_route_path || "--" }}</strong>
|
||||
</div>
|
||||
|
||||
<div v-else-if="selectedDetailTab === 'device'" class="release-detail-panel release-kv-grid">
|
||||
<span>Type</span><strong>{{ selectedSession.device?.type || "--" }}</strong>
|
||||
<span>Browser</span><strong>{{ selectedSession.device?.browser_name || "--" }} {{ selectedSession.device?.browser_version || "" }}</strong>
|
||||
<span>OS</span><strong>{{ selectedSession.device?.os_name || "--" }} {{ selectedSession.device?.os_version || "" }}</strong>
|
||||
<span>Viewport</span><strong>{{ viewportLabel(selectedSession) }}</strong>
|
||||
<span>User agent</span><strong class="release-break-word">{{ selectedSession.device?.user_agent || "--" }}</strong>
|
||||
</div>
|
||||
|
||||
<div v-else-if="selectedDetailTab === 'reports'" class="release-detail-panel">
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Status</th>
|
||||
<th>Reporter</th>
|
||||
<th>Route</th>
|
||||
<th>Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="report in detailErrorReports" :key="report.id">
|
||||
<td>
|
||||
<router-link to="/superuser/error-reports">#{{ report.id }}</router-link>
|
||||
</td>
|
||||
<td>{{ report.status }}</td>
|
||||
<td>{{ report.reporter?.name || report.reporter?.email || report.reporter?.type || "--" }}</td>
|
||||
<td>{{ report.route_path || "--" }}</td>
|
||||
<td>{{ formatDate(report.created_at) }}</td>
|
||||
</tr>
|
||||
<tr v-if="detailErrorReports.length === 0">
|
||||
<td colspan="5">{{ trFallback("replay.no_error_reports", "No linked error reports.") }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p class="release-muted mt-2">
|
||||
{{ trFallback("replay.visual_replay_disabled", "Visual replay requires a future visual_redacted capture mode.") }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<ol v-if="!selectedDetail" class="release-timeline" data-testid="release-timeline-events">
|
||||
<li v-for="event in timelineEvents" :key="event.id">
|
||||
<time>{{ formatDate(event.occurred_at) }}</time>
|
||||
<strong>{{ event.event_type }}</strong>
|
||||
<b-tag class="ml-1" :class="statusClass(event.severity)">{{ valueLabel("severity", event.severity) }}</b-tag>
|
||||
<div class="release-muted">
|
||||
{{ event.channel_slug || "--" }} / {{ event.module_key || "--" }} /
|
||||
{{ event.route_path || event.component || "--" }}
|
||||
</div>
|
||||
<details>
|
||||
<summary>{{ trFallback("replay.payload", "Payload") }}</summary>
|
||||
<pre>{{ jsonPreview(event.payload) }}</pre>
|
||||
</details>
|
||||
</li>
|
||||
<li v-if="timelineEvents.length === 0">{{ tr("replay.no_timeline_events") }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.release-replay-inspector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.release-session-table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.release-action-cell {
|
||||
text-align: right;
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.release-inspector-modal {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
.release-inspector-modal__backdrop {
|
||||
background: rgba(15, 23, 42, 0.42);
|
||||
}
|
||||
|
||||
.release-inspector-modal__card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #d8dee8;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 28px 90px rgba(15, 23, 42, 0.34);
|
||||
color: #111827;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: min(86vh, 900px);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: min(1120px, calc(100vw - 2rem));
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.release-inspector-modal__header {
|
||||
align-items: flex-start;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
padding: 1.25rem 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.release-inspector-modal__header h3 {
|
||||
color: #111827;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.release-inspector-modal__eyebrow {
|
||||
color: #6b7280;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
margin-bottom: 0.25rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.release-inspector-modal__trace {
|
||||
margin: 0.4rem 0 0;
|
||||
}
|
||||
|
||||
.release-inspector-modal__meta {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.release-inspector-modal__summary {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
}
|
||||
|
||||
.release-inspector-modal__summary span {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 999px;
|
||||
color: #374151;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.65rem;
|
||||
}
|
||||
|
||||
.release-inspector-modal__tabs {
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
padding: 0.9rem 1.5rem;
|
||||
}
|
||||
|
||||
.release-inspector-modal__body {
|
||||
background: #ffffff;
|
||||
overflow: auto;
|
||||
padding: 1.25rem 1.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.release-detail-panel {
|
||||
background: #ffffff;
|
||||
border: 1px solid #d8dee8;
|
||||
border-radius: 6px;
|
||||
color: #111827;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.release-kv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(9rem, 12rem) minmax(0, 1fr);
|
||||
gap: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.release-kv-grid span {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.release-break-word {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.release-timeline pre {
|
||||
background: #111827;
|
||||
border-radius: 6px;
|
||||
color: #f9fafb;
|
||||
font-size: 0.78rem;
|
||||
margin-top: 0.5rem;
|
||||
max-height: 24rem;
|
||||
overflow: auto;
|
||||
padding: 0.75rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.release-inspector-modal__card {
|
||||
max-height: 92vh;
|
||||
width: calc(100vw - 1rem);
|
||||
}
|
||||
|
||||
.release-inspector-modal__header {
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.release-inspector-modal__summary,
|
||||
.release-inspector-modal__tabs,
|
||||
.release-inspector-modal__body {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.release-kv-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2441,14 +2441,24 @@
|
||||
"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_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_tooltip": "Indlæs release-data fra denne API",
|
||||
"use": "Brug",
|
||||
"reset_tooltip": "Vend tilbage til standard kontrol-API",
|
||||
"reset": "Nulstil",
|
||||
@@ -2457,15 +2467,15 @@
|
||||
"tabs": {
|
||||
"overview": {
|
||||
"label": "Oversigt",
|
||||
"description": "Kanalstatus, ops?tningsfremdrift og seneste release-tilstand."
|
||||
"description": "Kanalstatus, opsætningsfremdrift og seneste release-tilstand."
|
||||
},
|
||||
"channels": {
|
||||
"label": "Kanaler",
|
||||
"description": "Opret stabile, canary- og m?lrettede kanaler med rollout-gr?nser."
|
||||
"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."
|
||||
"description": "Fastgør brugere, subbrugere eller kunder til en bestemt release-kanal."
|
||||
},
|
||||
"deployments": {
|
||||
"label": "Udrulninger",
|
||||
@@ -2473,7 +2483,7 @@
|
||||
},
|
||||
"replay": {
|
||||
"label": "Replay",
|
||||
"description": "Aktiv?r m?lrettet opsamling og s?g i release-tidslinjeh?ndelser."
|
||||
"description": "Aktivér målrettet opsamling og søg i release-tidslinjehændelser."
|
||||
},
|
||||
"integrations": {
|
||||
"label": "Integrationer",
|
||||
@@ -2486,22 +2496,22 @@
|
||||
},
|
||||
"setup": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "GitHub- og Coolify-m?l",
|
||||
"targets": "GitHub- og Coolify-mål",
|
||||
"assignments": "Pilot-tildelinger",
|
||||
"deployments": "F?rste udrulning",
|
||||
"deployments": "Første udrulning",
|
||||
"replay": "Replay-opsamling"
|
||||
},
|
||||
"stats": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "M?l",
|
||||
"targets": "Mål",
|
||||
"deployments": "Udrulninger",
|
||||
"timeline_events": "Tidslinjeh?ndelser"
|
||||
"timeline_events": "Tidslinjehændelser"
|
||||
},
|
||||
"overview": {
|
||||
"title": "Oversigt",
|
||||
"subtitle": "Aktuel kanalstatus, release-aktivitet og modulstatus.",
|
||||
"guided_setup": "Guidet ops?tning",
|
||||
"next_step": "N?ste: {step}",
|
||||
"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."
|
||||
@@ -2515,14 +2525,14 @@
|
||||
"github_webhook_secret": "GitHub webhook-hemmelighed",
|
||||
"configured": "Konfigureret",
|
||||
"not_configured": "Ikke konfigureret",
|
||||
"loaded_from": "Indl?st fra {variable}",
|
||||
"loaded_from": "Indlæst fra {variable}",
|
||||
"set_below": "Angiv {variable} nedenfor",
|
||||
"private_repositories_prefix": "Private repositories l?ses med serverens milj?variabel",
|
||||
"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_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_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",
|
||||
@@ -2559,19 +2569,19 @@
|
||||
"kicker": "Release Manager",
|
||||
"title": "Release-kanalen er ikke klar",
|
||||
"summary_prefix": "Din konto er tildelt",
|
||||
"summary_suffix": ", men kanalen mangler den konfiguration, der skal bruges for at indl?se dens release-image.",
|
||||
"summary_suffix": ", men kanalen mangler den konfiguration, der skal bruges for at indlæse dens release-image.",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"api_url": "API-URL",
|
||||
"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",
|
||||
"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}",
|
||||
"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.",
|
||||
"frontend": "Frontend",
|
||||
@@ -2579,10 +2589,10 @@
|
||||
"frontend_version": "Frontend-version",
|
||||
"api_version": "API-version",
|
||||
"assigned_channel": "Tildelt kanal",
|
||||
"current_app_image": "Nuv?rende app-image",
|
||||
"current_api": "Nuv?rende API",
|
||||
"current_app_image": "Nuværende app-image",
|
||||
"current_api": "Nuværende API",
|
||||
"base_image": "Basis-image",
|
||||
"continue": "Forts?t"
|
||||
"continue": "Fortsæt"
|
||||
},
|
||||
"common": {
|
||||
"yes": "ja",
|
||||
@@ -2762,7 +2772,7 @@
|
||||
"repository_message": "Eksempel: truckwash/front-end-vue",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: main",
|
||||
"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.",
|
||||
|
||||
@@ -2551,27 +2551,37 @@
|
||||
"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.",
|
||||
"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_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",
|
||||
"reset_tooltip": "Zur Standard-Kontroll-API zurückkehren",
|
||||
"reset": "Zurücksetzen",
|
||||
"known_endpoints": "Bekannte Kontroll-API-Endpunkte"
|
||||
},
|
||||
"tabs": {
|
||||
"overview": {
|
||||
"label": "?bersicht",
|
||||
"label": "Übersicht",
|
||||
"description": "Kanalzustand, Einrichtungsfortschritt und aktueller Release-Status."
|
||||
},
|
||||
"channels": {
|
||||
"label": "Kan?le",
|
||||
"description": "Stabile, Canary- und Zielkan?le mit Rollout-Grenzen erstellen."
|
||||
"label": "Kanäle",
|
||||
"description": "Stabile, Canary- und Zielkanäle mit Rollout-Grenzen erstellen."
|
||||
},
|
||||
"assignments": {
|
||||
"label": "Zuweisungen",
|
||||
@@ -2595,30 +2605,30 @@
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"channels": "Kan?le",
|
||||
"channels": "Kanäle",
|
||||
"targets": "GitHub- und Coolify-Ziele",
|
||||
"assignments": "Pilot-Zuweisungen",
|
||||
"deployments": "Erste Bereitstellung",
|
||||
"replay": "Replay-Erfassung"
|
||||
},
|
||||
"stats": {
|
||||
"channels": "Kan?le",
|
||||
"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.",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -2635,7 +2645,7 @@
|
||||
"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",
|
||||
"back_to_integrations": "Zurück zu Integrationen",
|
||||
"guide": {
|
||||
"steps": {
|
||||
"github_token": "GitHub-Token",
|
||||
@@ -2672,10 +2682,10 @@
|
||||
"summary_suffix": ", aber diesem Kanal fehlt die Konfiguration, um sein Release-Image zu laden.",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"api_url": "API-URL",
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
@@ -2683,7 +2693,7 @@
|
||||
"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.",
|
||||
"summary_suffix": "zugewiesen. Dieses Gerät merkt sich, dass du diesen Hinweis gesehen hast.",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"frontend_version": "Frontend-Version",
|
||||
@@ -2872,7 +2882,7 @@
|
||||
"repository_message": "Example: truckwash/front-end-vue",
|
||||
"repository_placeholder": "owner/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Example: main",
|
||||
"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.",
|
||||
|
||||
@@ -2275,6 +2275,16 @@
|
||||
"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.",
|
||||
@@ -2596,7 +2606,7 @@
|
||||
"repository_message": "Example: truckwash/front-end-vue",
|
||||
"repository_placeholder": "owner/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Example: main",
|
||||
"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.",
|
||||
|
||||
@@ -1500,6 +1500,16 @@
|
||||
"release_manager": {
|
||||
"title": "@:{'templates.generated.compat.configuration.release_manager.title'}",
|
||||
"subtitle": "@:{'templates.generated.compat.configuration.release_manager.subtitle'}",
|
||||
"channel_names": {
|
||||
"stable": "@:{'templates.generated.compat.configuration.release_manager.channel_names.stable'}",
|
||||
"canary": "@:{'templates.generated.compat.configuration.release_manager.channel_names.canary'}",
|
||||
"internal": "@:{'templates.generated.compat.configuration.release_manager.channel_names.internal'}"
|
||||
},
|
||||
"channel_descriptions": {
|
||||
"stable": "@:{'templates.generated.compat.configuration.release_manager.channel_descriptions.stable'}",
|
||||
"canary": "@:{'templates.generated.compat.configuration.release_manager.channel_descriptions.canary'}",
|
||||
"internal": "@:{'templates.generated.compat.configuration.release_manager.channel_descriptions.internal'}"
|
||||
},
|
||||
"control_api": {
|
||||
"title": "@:{'templates.generated.compat.configuration.release_manager.control_api.title'}",
|
||||
"tooltip": "@:{'templates.generated.compat.configuration.release_manager.control_api.tooltip'}",
|
||||
|
||||
@@ -2552,16 +2552,26 @@
|
||||
"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_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_tooltip": "Gå tilbake til standard kontroll-API",
|
||||
"reset": "Tilbakestill",
|
||||
"known_endpoints": "Kjente kontroll-API-endepunkter"
|
||||
},
|
||||
@@ -2572,7 +2582,7 @@
|
||||
},
|
||||
"channels": {
|
||||
"label": "Kanaler",
|
||||
"description": "Opprett stabile, canary- og m?lrettede kanaler med rollout-grenser."
|
||||
"description": "Opprett stabile, canary- og målrettede kanaler med rollout-grenser."
|
||||
},
|
||||
"assignments": {
|
||||
"label": "Tildelinger",
|
||||
@@ -2584,7 +2594,7 @@
|
||||
},
|
||||
"replay": {
|
||||
"label": "Replay",
|
||||
"description": "Aktiver m?lrettet innsamling og s?k i release-tidslinjehendelser."
|
||||
"description": "Aktiver målrettet innsamling og søk i release-tidslinjehendelser."
|
||||
},
|
||||
"integrations": {
|
||||
"label": "Integrasjoner",
|
||||
@@ -2597,14 +2607,14 @@
|
||||
},
|
||||
"setup": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "GitHub- og Coolify-m?l",
|
||||
"targets": "GitHub- og Coolify-mål",
|
||||
"assignments": "Pilottildelinger",
|
||||
"deployments": "F?rste utrulling",
|
||||
"deployments": "Første utrulling",
|
||||
"replay": "Replay-innsamling"
|
||||
},
|
||||
"stats": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "M?l",
|
||||
"targets": "Mål",
|
||||
"deployments": "Utrullinger",
|
||||
"timeline_events": "Tidslinjehendelser"
|
||||
},
|
||||
@@ -2628,12 +2638,12 @@
|
||||
"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_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_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_message": "Valgfritt; la stå tomt for å beholde eksisterende hemmelighet.",
|
||||
"webhook_secret_placeholder": "Webhook HMAC-hemmelighet",
|
||||
"save": "Lagre innstillinger",
|
||||
"back_to_integrations": "Tilbake til integrasjoner",
|
||||
@@ -2670,11 +2680,11 @@
|
||||
"kicker": "Release Manager",
|
||||
"title": "Release-kanalen er ikke klar",
|
||||
"summary_prefix": "Kontoen din er tildelt",
|
||||
"summary_suffix": ", men kanalen mangler konfigurasjonen som trengs for ? laste release-imaget.",
|
||||
"summary_suffix": ", men kanalen mangler konfigurasjonen som trengs for å laste release-imaget.",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"api_url": "API-URL",
|
||||
"checking_again": "Sjekker igjen om {seconds}s",
|
||||
"refresh_error": "Release-status kunne ikke oppdateres. Neste automatiske sjekk pr?ver igjen.",
|
||||
"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",
|
||||
@@ -2682,7 +2692,7 @@
|
||||
},
|
||||
"channel_switched": {
|
||||
"kicker": "Release Manager",
|
||||
"title": "Du er n? p? {channel}",
|
||||
"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.",
|
||||
"frontend": "Frontend",
|
||||
@@ -2690,8 +2700,8 @@
|
||||
"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",
|
||||
"current_app_image": "Nåværende app-image",
|
||||
"current_api": "Nåværende API",
|
||||
"base_image": "Basis-image",
|
||||
"continue": "Fortsett"
|
||||
},
|
||||
@@ -2873,7 +2883,7 @@
|
||||
"repository_message": "Eksempel: truckwash/front-end-vue",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: main",
|
||||
"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.",
|
||||
|
||||
@@ -2602,31 +2602,41 @@
|
||||
"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.",
|
||||
"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_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"
|
||||
"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."
|
||||
"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."
|
||||
"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."
|
||||
"description": "Koppla användare, underanvändare eller kunder till en specifik release-kanal."
|
||||
},
|
||||
"deployments": {
|
||||
"label": "Utrullningar",
|
||||
@@ -2634,58 +2644,58 @@
|
||||
},
|
||||
"replay": {
|
||||
"label": "Replay",
|
||||
"description": "Aktivera riktad insamling och s?k i release-tidslinjeh?ndelser."
|
||||
"description": "Aktivera riktad insamling och sök i release-tidslinjehändelser."
|
||||
},
|
||||
"integrations": {
|
||||
"label": "Integrationer",
|
||||
"description": "Anslut GitHub-repositories, branches och Coolify-tj?nster."
|
||||
"description": "Anslut GitHub-repositories, branches och Coolify-tjänster."
|
||||
},
|
||||
"settings": {
|
||||
"label": "Inst?llningar",
|
||||
"description": "Konfigurera Release Managers GitHub-?tkomst och webhook-inst?llningar."
|
||||
"label": "Inställningar",
|
||||
"description": "Konfigurera Release Managers GitHub-åtkomst och webhook-inställningar."
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "GitHub- och Coolify-m?l",
|
||||
"targets": "GitHub- och Coolify-mål",
|
||||
"assignments": "Pilottilldelningar",
|
||||
"deployments": "F?rsta utrullningen",
|
||||
"deployments": "Första utrullningen",
|
||||
"replay": "Replay-insamling"
|
||||
},
|
||||
"stats": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "M?l",
|
||||
"targets": "Mål",
|
||||
"deployments": "Utrullningar",
|
||||
"timeline_events": "Tidslinjeh?ndelser"
|
||||
"timeline_events": "Tidslinjehändelser"
|
||||
},
|
||||
"overview": {
|
||||
"title": "?versikt",
|
||||
"subtitle": "Aktuell kanalh?lsa, release-aktivitet och modulstatus.",
|
||||
"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.",
|
||||
"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.",
|
||||
"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}",
|
||||
"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_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.",
|
||||
"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",
|
||||
"save": "Spara inställningar",
|
||||
"back_to_integrations": "Tillbaka till integrationer",
|
||||
"guide": {
|
||||
"steps": {
|
||||
@@ -2718,13 +2728,13 @@
|
||||
},
|
||||
"channel_unavailable": {
|
||||
"kicker": "Release Manager",
|
||||
"title": "Release-kanalen ?r inte klar",
|
||||
"summary_prefix": "Ditt konto ?r tilldelat",
|
||||
"summary_suffix": ", men kanalen saknar konfigurationen som beh?vs f?r att l?sa in dess release-image.",
|
||||
"title": "Release-kanalen är inte klar",
|
||||
"summary_prefix": "Ditt konto är tilldelat",
|
||||
"summary_suffix": ", men kanalen saknar konfigurationen som behövs för att läsa in dess release-image.",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"api_url": "API-URL",
|
||||
"checking_again": "Kontrollerar igen om {seconds}s",
|
||||
"refresh_error": "Release-status kunde inte uppdateras. N?sta automatiska kontroll f?rs?ker igen.",
|
||||
"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",
|
||||
@@ -2732,9 +2742,9 @@
|
||||
},
|
||||
"channel_switched": {
|
||||
"kicker": "Release Manager",
|
||||
"title": "Du ?r nu p? {channel}",
|
||||
"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.",
|
||||
"summary_suffix": "Den här enheten kommer ihåg att du har sett detta meddelande.",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"frontend_version": "Frontend-version",
|
||||
@@ -2743,7 +2753,7 @@
|
||||
"current_app_image": "Nuvarande app-image",
|
||||
"current_api": "Nuvarande API",
|
||||
"base_image": "Bas-image",
|
||||
"continue": "Forts?tt"
|
||||
"continue": "Fortsätt"
|
||||
},
|
||||
"common": {
|
||||
"yes": "ja",
|
||||
@@ -2923,7 +2933,7 @@
|
||||
"repository_message": "Eksempel: truckwash/front-end-vue",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: main",
|
||||
"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.",
|
||||
|
||||
+34
-24
@@ -1499,14 +1499,24 @@
|
||||
"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_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_tooltip": "Indlæs release-data fra denne API",
|
||||
"use": "Brug",
|
||||
"reset_tooltip": "Vend tilbage til standard kontrol-API",
|
||||
"reset": "Nulstil",
|
||||
@@ -1515,15 +1525,15 @@
|
||||
"tabs": {
|
||||
"overview": {
|
||||
"label": "Oversigt",
|
||||
"description": "Kanalstatus, ops?tningsfremdrift og seneste release-tilstand."
|
||||
"description": "Kanalstatus, opsætningsfremdrift og seneste release-tilstand."
|
||||
},
|
||||
"channels": {
|
||||
"label": "Kanaler",
|
||||
"description": "Opret stabile, canary- og m?lrettede kanaler med rollout-gr?nser."
|
||||
"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."
|
||||
"description": "Fastgør brugere, subbrugere eller kunder til en bestemt release-kanal."
|
||||
},
|
||||
"deployments": {
|
||||
"label": "Udrulninger",
|
||||
@@ -1531,7 +1541,7 @@
|
||||
},
|
||||
"replay": {
|
||||
"label": "Replay",
|
||||
"description": "Aktiv?r m?lrettet opsamling og s?g i release-tidslinjeh?ndelser."
|
||||
"description": "Aktivér målrettet opsamling og søg i release-tidslinjehændelser."
|
||||
},
|
||||
"integrations": {
|
||||
"label": "Integrationer",
|
||||
@@ -1544,22 +1554,22 @@
|
||||
},
|
||||
"setup": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "GitHub- og Coolify-m?l",
|
||||
"targets": "GitHub- og Coolify-mål",
|
||||
"assignments": "Pilot-tildelinger",
|
||||
"deployments": "F?rste udrulning",
|
||||
"deployments": "Første udrulning",
|
||||
"replay": "Replay-opsamling"
|
||||
},
|
||||
"stats": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "M?l",
|
||||
"targets": "Mål",
|
||||
"deployments": "Udrulninger",
|
||||
"timeline_events": "Tidslinjeh?ndelser"
|
||||
"timeline_events": "Tidslinjehændelser"
|
||||
},
|
||||
"overview": {
|
||||
"title": "Oversigt",
|
||||
"subtitle": "Aktuel kanalstatus, release-aktivitet og modulstatus.",
|
||||
"guided_setup": "Guidet ops?tning",
|
||||
"next_step": "N?ste: {step}",
|
||||
"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."
|
||||
@@ -1573,14 +1583,14 @@
|
||||
"github_webhook_secret": "GitHub webhook-hemmelighed",
|
||||
"configured": "Konfigureret",
|
||||
"not_configured": "Ikke konfigureret",
|
||||
"loaded_from": "Indl?st fra {variable}",
|
||||
"loaded_from": "Indlæst fra {variable}",
|
||||
"set_below": "Angiv {variable} nedenfor",
|
||||
"private_repositories_prefix": "Private repositories l?ses med serverens milj?variabel",
|
||||
"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_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_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",
|
||||
@@ -1617,19 +1627,19 @@
|
||||
"kicker": "Release Manager",
|
||||
"title": "Release-kanalen er ikke klar",
|
||||
"summary_prefix": "Din konto er tildelt",
|
||||
"summary_suffix": ", men kanalen mangler den konfiguration, der skal bruges for at indl?se dens release-image.",
|
||||
"summary_suffix": ", men kanalen mangler den konfiguration, der skal bruges for at indlæse dens release-image.",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"api_url": "API-URL",
|
||||
"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",
|
||||
"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}",
|
||||
"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.",
|
||||
"frontend": "Frontend",
|
||||
@@ -1637,10 +1647,10 @@
|
||||
"frontend_version": "Frontend-version",
|
||||
"api_version": "API-version",
|
||||
"assigned_channel": "Tildelt kanal",
|
||||
"current_app_image": "Nuv?rende app-image",
|
||||
"current_api": "Nuv?rende API",
|
||||
"current_app_image": "Nuværende app-image",
|
||||
"current_api": "Nuværende API",
|
||||
"base_image": "Basis-image",
|
||||
"continue": "Forts?t"
|
||||
"continue": "Fortsæt"
|
||||
},
|
||||
"common": {
|
||||
"yes": "ja",
|
||||
@@ -1820,7 +1830,7 @@
|
||||
"repository_message": "Eksempel: truckwash/front-end-vue",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: main",
|
||||
"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.",
|
||||
|
||||
+32
-22
@@ -1499,27 +1499,37 @@
|
||||
"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.",
|
||||
"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_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",
|
||||
"reset_tooltip": "Zur Standard-Kontroll-API zurückkehren",
|
||||
"reset": "Zurücksetzen",
|
||||
"known_endpoints": "Bekannte Kontroll-API-Endpunkte"
|
||||
},
|
||||
"tabs": {
|
||||
"overview": {
|
||||
"label": "?bersicht",
|
||||
"label": "Übersicht",
|
||||
"description": "Kanalzustand, Einrichtungsfortschritt und aktueller Release-Status."
|
||||
},
|
||||
"channels": {
|
||||
"label": "Kan?le",
|
||||
"description": "Stabile, Canary- und Zielkan?le mit Rollout-Grenzen erstellen."
|
||||
"label": "Kanäle",
|
||||
"description": "Stabile, Canary- und Zielkanäle mit Rollout-Grenzen erstellen."
|
||||
},
|
||||
"assignments": {
|
||||
"label": "Zuweisungen",
|
||||
@@ -1543,30 +1553,30 @@
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"channels": "Kan?le",
|
||||
"channels": "Kanäle",
|
||||
"targets": "GitHub- und Coolify-Ziele",
|
||||
"assignments": "Pilot-Zuweisungen",
|
||||
"deployments": "Erste Bereitstellung",
|
||||
"replay": "Replay-Erfassung"
|
||||
},
|
||||
"stats": {
|
||||
"channels": "Kan?le",
|
||||
"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.",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -1583,7 +1593,7 @@
|
||||
"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",
|
||||
"back_to_integrations": "Zurück zu Integrationen",
|
||||
"guide": {
|
||||
"steps": {
|
||||
"github_token": "GitHub-Token",
|
||||
@@ -1620,10 +1630,10 @@
|
||||
"summary_suffix": ", aber diesem Kanal fehlt die Konfiguration, um sein Release-Image zu laden.",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"api_url": "API-URL",
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
@@ -1631,7 +1641,7 @@
|
||||
"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.",
|
||||
"summary_suffix": "zugewiesen. Dieses Gerät merkt sich, dass du diesen Hinweis gesehen hast.",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"frontend_version": "Frontend-Version",
|
||||
@@ -1820,7 +1830,7 @@
|
||||
"repository_message": "Example: truckwash/front-end-vue",
|
||||
"repository_placeholder": "owner/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Example: main",
|
||||
"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.",
|
||||
|
||||
@@ -1499,6 +1499,16 @@
|
||||
"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.",
|
||||
@@ -1820,7 +1830,7 @@
|
||||
"repository_message": "Example: truckwash/front-end-vue",
|
||||
"repository_placeholder": "owner/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Example: main",
|
||||
"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.",
|
||||
|
||||
+26
-16
@@ -1499,16 +1499,26 @@
|
||||
"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_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_tooltip": "Gå tilbake til standard kontroll-API",
|
||||
"reset": "Tilbakestill",
|
||||
"known_endpoints": "Kjente kontroll-API-endepunkter"
|
||||
},
|
||||
@@ -1519,7 +1529,7 @@
|
||||
},
|
||||
"channels": {
|
||||
"label": "Kanaler",
|
||||
"description": "Opprett stabile, canary- og m?lrettede kanaler med rollout-grenser."
|
||||
"description": "Opprett stabile, canary- og målrettede kanaler med rollout-grenser."
|
||||
},
|
||||
"assignments": {
|
||||
"label": "Tildelinger",
|
||||
@@ -1531,7 +1541,7 @@
|
||||
},
|
||||
"replay": {
|
||||
"label": "Replay",
|
||||
"description": "Aktiver m?lrettet innsamling og s?k i release-tidslinjehendelser."
|
||||
"description": "Aktiver målrettet innsamling og søk i release-tidslinjehendelser."
|
||||
},
|
||||
"integrations": {
|
||||
"label": "Integrasjoner",
|
||||
@@ -1544,14 +1554,14 @@
|
||||
},
|
||||
"setup": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "GitHub- og Coolify-m?l",
|
||||
"targets": "GitHub- og Coolify-mål",
|
||||
"assignments": "Pilottildelinger",
|
||||
"deployments": "F?rste utrulling",
|
||||
"deployments": "Første utrulling",
|
||||
"replay": "Replay-innsamling"
|
||||
},
|
||||
"stats": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "M?l",
|
||||
"targets": "Mål",
|
||||
"deployments": "Utrullinger",
|
||||
"timeline_events": "Tidslinjehendelser"
|
||||
},
|
||||
@@ -1575,12 +1585,12 @@
|
||||
"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_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_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_message": "Valgfritt; la stå tomt for å beholde eksisterende hemmelighet.",
|
||||
"webhook_secret_placeholder": "Webhook HMAC-hemmelighet",
|
||||
"save": "Lagre innstillinger",
|
||||
"back_to_integrations": "Tilbake til integrasjoner",
|
||||
@@ -1617,11 +1627,11 @@
|
||||
"kicker": "Release Manager",
|
||||
"title": "Release-kanalen er ikke klar",
|
||||
"summary_prefix": "Kontoen din er tildelt",
|
||||
"summary_suffix": ", men kanalen mangler konfigurasjonen som trengs for ? laste release-imaget.",
|
||||
"summary_suffix": ", men kanalen mangler konfigurasjonen som trengs for å laste release-imaget.",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"api_url": "API-URL",
|
||||
"checking_again": "Sjekker igjen om {seconds}s",
|
||||
"refresh_error": "Release-status kunne ikke oppdateres. Neste automatiske sjekk pr?ver igjen.",
|
||||
"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",
|
||||
@@ -1629,7 +1639,7 @@
|
||||
},
|
||||
"channel_switched": {
|
||||
"kicker": "Release Manager",
|
||||
"title": "Du er n? p? {channel}",
|
||||
"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.",
|
||||
"frontend": "Frontend",
|
||||
@@ -1637,8 +1647,8 @@
|
||||
"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",
|
||||
"current_app_image": "Nåværende app-image",
|
||||
"current_api": "Nåværende API",
|
||||
"base_image": "Basis-image",
|
||||
"continue": "Fortsett"
|
||||
},
|
||||
@@ -1820,7 +1830,7 @@
|
||||
"repository_message": "Eksempel: truckwash/front-end-vue",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: main",
|
||||
"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.",
|
||||
|
||||
+50
-40
@@ -1499,31 +1499,41 @@
|
||||
"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.",
|
||||
"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_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"
|
||||
"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."
|
||||
"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."
|
||||
"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."
|
||||
"description": "Koppla användare, underanvändare eller kunder till en specifik release-kanal."
|
||||
},
|
||||
"deployments": {
|
||||
"label": "Utrullningar",
|
||||
@@ -1531,58 +1541,58 @@
|
||||
},
|
||||
"replay": {
|
||||
"label": "Replay",
|
||||
"description": "Aktivera riktad insamling och s?k i release-tidslinjeh?ndelser."
|
||||
"description": "Aktivera riktad insamling och sök i release-tidslinjehändelser."
|
||||
},
|
||||
"integrations": {
|
||||
"label": "Integrationer",
|
||||
"description": "Anslut GitHub-repositories, branches och Coolify-tj?nster."
|
||||
"description": "Anslut GitHub-repositories, branches och Coolify-tjänster."
|
||||
},
|
||||
"settings": {
|
||||
"label": "Inst?llningar",
|
||||
"description": "Konfigurera Release Managers GitHub-?tkomst och webhook-inst?llningar."
|
||||
"label": "Inställningar",
|
||||
"description": "Konfigurera Release Managers GitHub-åtkomst och webhook-inställningar."
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "GitHub- och Coolify-m?l",
|
||||
"targets": "GitHub- och Coolify-mål",
|
||||
"assignments": "Pilottilldelningar",
|
||||
"deployments": "F?rsta utrullningen",
|
||||
"deployments": "Första utrullningen",
|
||||
"replay": "Replay-insamling"
|
||||
},
|
||||
"stats": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "M?l",
|
||||
"targets": "Mål",
|
||||
"deployments": "Utrullningar",
|
||||
"timeline_events": "Tidslinjeh?ndelser"
|
||||
"timeline_events": "Tidslinjehändelser"
|
||||
},
|
||||
"overview": {
|
||||
"title": "?versikt",
|
||||
"subtitle": "Aktuell kanalh?lsa, release-aktivitet och modulstatus.",
|
||||
"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.",
|
||||
"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.",
|
||||
"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}",
|
||||
"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_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.",
|
||||
"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",
|
||||
"save": "Spara inställningar",
|
||||
"back_to_integrations": "Tillbaka till integrationer",
|
||||
"guide": {
|
||||
"steps": {
|
||||
@@ -1615,13 +1625,13 @@
|
||||
},
|
||||
"channel_unavailable": {
|
||||
"kicker": "Release Manager",
|
||||
"title": "Release-kanalen ?r inte klar",
|
||||
"summary_prefix": "Ditt konto ?r tilldelat",
|
||||
"summary_suffix": ", men kanalen saknar konfigurationen som beh?vs f?r att l?sa in dess release-image.",
|
||||
"title": "Release-kanalen är inte klar",
|
||||
"summary_prefix": "Ditt konto är tilldelat",
|
||||
"summary_suffix": ", men kanalen saknar konfigurationen som behövs för att läsa in dess release-image.",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"api_url": "API-URL",
|
||||
"checking_again": "Kontrollerar igen om {seconds}s",
|
||||
"refresh_error": "Release-status kunde inte uppdateras. N?sta automatiska kontroll f?rs?ker igen.",
|
||||
"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",
|
||||
@@ -1629,9 +1639,9 @@
|
||||
},
|
||||
"channel_switched": {
|
||||
"kicker": "Release Manager",
|
||||
"title": "Du ?r nu p? {channel}",
|
||||
"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.",
|
||||
"summary_suffix": "Den här enheten kommer ihåg att du har sett detta meddelande.",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"frontend_version": "Frontend-version",
|
||||
@@ -1640,7 +1650,7 @@
|
||||
"current_app_image": "Nuvarande app-image",
|
||||
"current_api": "Nuvarande API",
|
||||
"base_image": "Bas-image",
|
||||
"continue": "Forts?tt"
|
||||
"continue": "Fortsätt"
|
||||
},
|
||||
"common": {
|
||||
"yes": "ja",
|
||||
@@ -1820,7 +1830,7 @@
|
||||
"repository_message": "Eksempel: truckwash/front-end-vue",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: main",
|
||||
"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.",
|
||||
|
||||
@@ -302,14 +302,24 @@
|
||||
"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_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_tooltip": "Indlæs release-data fra denne API",
|
||||
"use": "Brug",
|
||||
"reset_tooltip": "Vend tilbage til standard kontrol-API",
|
||||
"reset": "Nulstil",
|
||||
@@ -318,15 +328,15 @@
|
||||
"tabs": {
|
||||
"overview": {
|
||||
"label": "Oversigt",
|
||||
"description": "Kanalstatus, ops?tningsfremdrift og seneste release-tilstand."
|
||||
"description": "Kanalstatus, opsætningsfremdrift og seneste release-tilstand."
|
||||
},
|
||||
"channels": {
|
||||
"label": "Kanaler",
|
||||
"description": "Opret stabile, canary- og m?lrettede kanaler med rollout-gr?nser."
|
||||
"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."
|
||||
"description": "Fastgør brugere, subbrugere eller kunder til en bestemt release-kanal."
|
||||
},
|
||||
"deployments": {
|
||||
"label": "Udrulninger",
|
||||
@@ -334,7 +344,7 @@
|
||||
},
|
||||
"replay": {
|
||||
"label": "Replay",
|
||||
"description": "Aktiv?r m?lrettet opsamling og s?g i release-tidslinjeh?ndelser."
|
||||
"description": "Aktivér målrettet opsamling og søg i release-tidslinjehændelser."
|
||||
},
|
||||
"integrations": {
|
||||
"label": "Integrationer",
|
||||
@@ -347,22 +357,22 @@
|
||||
},
|
||||
"setup": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "GitHub- og Coolify-m?l",
|
||||
"targets": "GitHub- og Coolify-mål",
|
||||
"assignments": "Pilot-tildelinger",
|
||||
"deployments": "F?rste udrulning",
|
||||
"deployments": "Første udrulning",
|
||||
"replay": "Replay-opsamling"
|
||||
},
|
||||
"stats": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "M?l",
|
||||
"targets": "Mål",
|
||||
"deployments": "Udrulninger",
|
||||
"timeline_events": "Tidslinjeh?ndelser"
|
||||
"timeline_events": "Tidslinjehændelser"
|
||||
},
|
||||
"overview": {
|
||||
"title": "Oversigt",
|
||||
"subtitle": "Aktuel kanalstatus, release-aktivitet og modulstatus.",
|
||||
"guided_setup": "Guidet ops?tning",
|
||||
"next_step": "N?ste: {step}",
|
||||
"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."
|
||||
@@ -376,14 +386,14 @@
|
||||
"github_webhook_secret": "GitHub webhook-hemmelighed",
|
||||
"configured": "Konfigureret",
|
||||
"not_configured": "Ikke konfigureret",
|
||||
"loaded_from": "Indl?st fra {variable}",
|
||||
"loaded_from": "Indlæst fra {variable}",
|
||||
"set_below": "Angiv {variable} nedenfor",
|
||||
"private_repositories_prefix": "Private repositories l?ses med serverens milj?variabel",
|
||||
"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_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_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",
|
||||
@@ -420,19 +430,19 @@
|
||||
"kicker": "Release Manager",
|
||||
"title": "Release-kanalen er ikke klar",
|
||||
"summary_prefix": "Din konto er tildelt",
|
||||
"summary_suffix": ", men kanalen mangler den konfiguration, der skal bruges for at indl?se dens release-image.",
|
||||
"summary_suffix": ", men kanalen mangler den konfiguration, der skal bruges for at indlæse dens release-image.",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"api_url": "API-URL",
|
||||
"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",
|
||||
"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}",
|
||||
"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.",
|
||||
"frontend": "Frontend",
|
||||
@@ -440,10 +450,10 @@
|
||||
"frontend_version": "Frontend-version",
|
||||
"api_version": "API-version",
|
||||
"assigned_channel": "Tildelt kanal",
|
||||
"current_app_image": "Nuv?rende app-image",
|
||||
"current_api": "Nuv?rende API",
|
||||
"current_app_image": "Nuværende app-image",
|
||||
"current_api": "Nuværende API",
|
||||
"base_image": "Basis-image",
|
||||
"continue": "Forts?t"
|
||||
"continue": "Fortsæt"
|
||||
},
|
||||
"common": {
|
||||
"yes": "ja",
|
||||
@@ -623,7 +633,7 @@
|
||||
"repository_message": "Eksempel: truckwash/front-end-vue",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: main",
|
||||
"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.",
|
||||
|
||||
@@ -302,27 +302,37 @@
|
||||
"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.",
|
||||
"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_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",
|
||||
"reset_tooltip": "Zur Standard-Kontroll-API zurückkehren",
|
||||
"reset": "Zurücksetzen",
|
||||
"known_endpoints": "Bekannte Kontroll-API-Endpunkte"
|
||||
},
|
||||
"tabs": {
|
||||
"overview": {
|
||||
"label": "?bersicht",
|
||||
"label": "Übersicht",
|
||||
"description": "Kanalzustand, Einrichtungsfortschritt und aktueller Release-Status."
|
||||
},
|
||||
"channels": {
|
||||
"label": "Kan?le",
|
||||
"description": "Stabile, Canary- und Zielkan?le mit Rollout-Grenzen erstellen."
|
||||
"label": "Kanäle",
|
||||
"description": "Stabile, Canary- und Zielkanäle mit Rollout-Grenzen erstellen."
|
||||
},
|
||||
"assignments": {
|
||||
"label": "Zuweisungen",
|
||||
@@ -346,30 +356,30 @@
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"channels": "Kan?le",
|
||||
"channels": "Kanäle",
|
||||
"targets": "GitHub- und Coolify-Ziele",
|
||||
"assignments": "Pilot-Zuweisungen",
|
||||
"deployments": "Erste Bereitstellung",
|
||||
"replay": "Replay-Erfassung"
|
||||
},
|
||||
"stats": {
|
||||
"channels": "Kan?le",
|
||||
"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.",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -386,7 +396,7 @@
|
||||
"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",
|
||||
"back_to_integrations": "Zurück zu Integrationen",
|
||||
"guide": {
|
||||
"steps": {
|
||||
"github_token": "GitHub-Token",
|
||||
@@ -423,10 +433,10 @@
|
||||
"summary_suffix": ", aber diesem Kanal fehlt die Konfiguration, um sein Release-Image zu laden.",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"api_url": "API-URL",
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
@@ -434,7 +444,7 @@
|
||||
"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.",
|
||||
"summary_suffix": "zugewiesen. Dieses Gerät merkt sich, dass du diesen Hinweis gesehen hast.",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"frontend_version": "Frontend-Version",
|
||||
@@ -623,7 +633,7 @@
|
||||
"repository_message": "Example: truckwash/front-end-vue",
|
||||
"repository_placeholder": "owner/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Example: main",
|
||||
"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.",
|
||||
|
||||
@@ -302,6 +302,16 @@
|
||||
"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.",
|
||||
@@ -623,7 +633,7 @@
|
||||
"repository_message": "Example: truckwash/front-end-vue",
|
||||
"repository_placeholder": "owner/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Example: main",
|
||||
"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.",
|
||||
|
||||
@@ -355,6 +355,16 @@
|
||||
"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'}",
|
||||
|
||||
@@ -302,16 +302,26 @@
|
||||
"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_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_tooltip": "Gå tilbake til standard kontroll-API",
|
||||
"reset": "Tilbakestill",
|
||||
"known_endpoints": "Kjente kontroll-API-endepunkter"
|
||||
},
|
||||
@@ -322,7 +332,7 @@
|
||||
},
|
||||
"channels": {
|
||||
"label": "Kanaler",
|
||||
"description": "Opprett stabile, canary- og m?lrettede kanaler med rollout-grenser."
|
||||
"description": "Opprett stabile, canary- og målrettede kanaler med rollout-grenser."
|
||||
},
|
||||
"assignments": {
|
||||
"label": "Tildelinger",
|
||||
@@ -334,7 +344,7 @@
|
||||
},
|
||||
"replay": {
|
||||
"label": "Replay",
|
||||
"description": "Aktiver m?lrettet innsamling og s?k i release-tidslinjehendelser."
|
||||
"description": "Aktiver målrettet innsamling og søk i release-tidslinjehendelser."
|
||||
},
|
||||
"integrations": {
|
||||
"label": "Integrasjoner",
|
||||
@@ -347,14 +357,14 @@
|
||||
},
|
||||
"setup": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "GitHub- og Coolify-m?l",
|
||||
"targets": "GitHub- og Coolify-mål",
|
||||
"assignments": "Pilottildelinger",
|
||||
"deployments": "F?rste utrulling",
|
||||
"deployments": "Første utrulling",
|
||||
"replay": "Replay-innsamling"
|
||||
},
|
||||
"stats": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "M?l",
|
||||
"targets": "Mål",
|
||||
"deployments": "Utrullinger",
|
||||
"timeline_events": "Tidslinjehendelser"
|
||||
},
|
||||
@@ -378,12 +388,12 @@
|
||||
"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_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_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_message": "Valgfritt; la stå tomt for å beholde eksisterende hemmelighet.",
|
||||
"webhook_secret_placeholder": "Webhook HMAC-hemmelighet",
|
||||
"save": "Lagre innstillinger",
|
||||
"back_to_integrations": "Tilbake til integrasjoner",
|
||||
@@ -420,11 +430,11 @@
|
||||
"kicker": "Release Manager",
|
||||
"title": "Release-kanalen er ikke klar",
|
||||
"summary_prefix": "Kontoen din er tildelt",
|
||||
"summary_suffix": ", men kanalen mangler konfigurasjonen som trengs for ? laste release-imaget.",
|
||||
"summary_suffix": ", men kanalen mangler konfigurasjonen som trengs for å laste release-imaget.",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"api_url": "API-URL",
|
||||
"checking_again": "Sjekker igjen om {seconds}s",
|
||||
"refresh_error": "Release-status kunne ikke oppdateres. Neste automatiske sjekk pr?ver igjen.",
|
||||
"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",
|
||||
@@ -432,7 +442,7 @@
|
||||
},
|
||||
"channel_switched": {
|
||||
"kicker": "Release Manager",
|
||||
"title": "Du er n? p? {channel}",
|
||||
"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.",
|
||||
"frontend": "Frontend",
|
||||
@@ -440,8 +450,8 @@
|
||||
"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",
|
||||
"current_app_image": "Nåværende app-image",
|
||||
"current_api": "Nåværende API",
|
||||
"base_image": "Basis-image",
|
||||
"continue": "Fortsett"
|
||||
},
|
||||
@@ -623,7 +633,7 @@
|
||||
"repository_message": "Eksempel: truckwash/front-end-vue",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: main",
|
||||
"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.",
|
||||
|
||||
@@ -302,31 +302,41 @@
|
||||
"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.",
|
||||
"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_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"
|
||||
"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."
|
||||
"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."
|
||||
"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."
|
||||
"description": "Koppla användare, underanvändare eller kunder till en specifik release-kanal."
|
||||
},
|
||||
"deployments": {
|
||||
"label": "Utrullningar",
|
||||
@@ -334,58 +344,58 @@
|
||||
},
|
||||
"replay": {
|
||||
"label": "Replay",
|
||||
"description": "Aktivera riktad insamling och s?k i release-tidslinjeh?ndelser."
|
||||
"description": "Aktivera riktad insamling och sök i release-tidslinjehändelser."
|
||||
},
|
||||
"integrations": {
|
||||
"label": "Integrationer",
|
||||
"description": "Anslut GitHub-repositories, branches och Coolify-tj?nster."
|
||||
"description": "Anslut GitHub-repositories, branches och Coolify-tjänster."
|
||||
},
|
||||
"settings": {
|
||||
"label": "Inst?llningar",
|
||||
"description": "Konfigurera Release Managers GitHub-?tkomst och webhook-inst?llningar."
|
||||
"label": "Inställningar",
|
||||
"description": "Konfigurera Release Managers GitHub-åtkomst och webhook-inställningar."
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "GitHub- och Coolify-m?l",
|
||||
"targets": "GitHub- och Coolify-mål",
|
||||
"assignments": "Pilottilldelningar",
|
||||
"deployments": "F?rsta utrullningen",
|
||||
"deployments": "Första utrullningen",
|
||||
"replay": "Replay-insamling"
|
||||
},
|
||||
"stats": {
|
||||
"channels": "Kanaler",
|
||||
"targets": "M?l",
|
||||
"targets": "Mål",
|
||||
"deployments": "Utrullningar",
|
||||
"timeline_events": "Tidslinjeh?ndelser"
|
||||
"timeline_events": "Tidslinjehändelser"
|
||||
},
|
||||
"overview": {
|
||||
"title": "?versikt",
|
||||
"subtitle": "Aktuell kanalh?lsa, release-aktivitet och modulstatus.",
|
||||
"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.",
|
||||
"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.",
|
||||
"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}",
|
||||
"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_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.",
|
||||
"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",
|
||||
"save": "Spara inställningar",
|
||||
"back_to_integrations": "Tillbaka till integrationer",
|
||||
"guide": {
|
||||
"steps": {
|
||||
@@ -418,13 +428,13 @@
|
||||
},
|
||||
"channel_unavailable": {
|
||||
"kicker": "Release Manager",
|
||||
"title": "Release-kanalen ?r inte klar",
|
||||
"summary_prefix": "Ditt konto ?r tilldelat",
|
||||
"summary_suffix": ", men kanalen saknar konfigurationen som beh?vs f?r att l?sa in dess release-image.",
|
||||
"title": "Release-kanalen är inte klar",
|
||||
"summary_prefix": "Ditt konto är tilldelat",
|
||||
"summary_suffix": ", men kanalen saknar konfigurationen som behövs för att läsa in dess release-image.",
|
||||
"frontend_url": "Frontend-URL",
|
||||
"api_url": "API-URL",
|
||||
"checking_again": "Kontrollerar igen om {seconds}s",
|
||||
"refresh_error": "Release-status kunde inte uppdateras. N?sta automatiska kontroll f?rs?ker igen.",
|
||||
"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",
|
||||
@@ -432,9 +442,9 @@
|
||||
},
|
||||
"channel_switched": {
|
||||
"kicker": "Release Manager",
|
||||
"title": "Du ?r nu p? {channel}",
|
||||
"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.",
|
||||
"summary_suffix": "Den här enheten kommer ihåg att du har sett detta meddelande.",
|
||||
"frontend": "Frontend",
|
||||
"api": "API",
|
||||
"frontend_version": "Frontend-version",
|
||||
@@ -443,7 +453,7 @@
|
||||
"current_app_image": "Nuvarande app-image",
|
||||
"current_api": "Nuvarande API",
|
||||
"base_image": "Bas-image",
|
||||
"continue": "Forts?tt"
|
||||
"continue": "Fortsätt"
|
||||
},
|
||||
"common": {
|
||||
"yes": "ja",
|
||||
@@ -623,7 +633,7 @@
|
||||
"repository_message": "Eksempel: truckwash/front-end-vue",
|
||||
"repository_placeholder": "ejer/repo",
|
||||
"branch": "Branch",
|
||||
"branch_message": "Eksempel: main",
|
||||
"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.",
|
||||
|
||||
@@ -221,6 +221,98 @@ export const getRecentFrontendFailureEvents = () =>
|
||||
payload: redactReleasePayload(event.payload),
|
||||
}));
|
||||
|
||||
const browserInfoFromUserAgent = (userAgent = "") => {
|
||||
const ua = String(userAgent || "");
|
||||
const matchers = [
|
||||
["Edge", /Edg\/([\d.]+)/],
|
||||
["Chrome", /Chrome\/([\d.]+)/],
|
||||
["Firefox", /Firefox\/([\d.]+)/],
|
||||
["Safari", /Version\/([\d.]+).*Safari/],
|
||||
];
|
||||
for (const [name, pattern] of matchers) {
|
||||
const match = ua.match(pattern);
|
||||
if (match) {
|
||||
return { name, version: match[1] || null };
|
||||
}
|
||||
}
|
||||
return { name: "Unknown", version: null };
|
||||
};
|
||||
|
||||
const osInfoFromUserAgent = (userAgent = "") => {
|
||||
const ua = String(userAgent || "");
|
||||
const matchers = [
|
||||
["Windows", /Windows NT ([\d.]+)/],
|
||||
["Android", /Android ([\d.]+)/],
|
||||
["iOS", /(?:iPhone|iPad).*OS ([\d_]+)/],
|
||||
["macOS", /Mac OS X ([\d_]+)/],
|
||||
["Linux", /Linux/],
|
||||
];
|
||||
for (const [name, pattern] of matchers) {
|
||||
const match = ua.match(pattern);
|
||||
if (match) {
|
||||
return { name, version: match[1] ? String(match[1]).replace(/_/g, ".") : null };
|
||||
}
|
||||
}
|
||||
return { name: "Unknown", version: null };
|
||||
};
|
||||
|
||||
const currentDeviceType = () => {
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
}
|
||||
const width = Number(window.innerWidth || 0);
|
||||
if (width > 0 && width < 769) {
|
||||
return "mobile";
|
||||
}
|
||||
if (width >= 769 && width < 1024) {
|
||||
return "tablet";
|
||||
}
|
||||
return "desktop";
|
||||
};
|
||||
|
||||
const versionLabel = (version) => version?.version_label || version?.label || null;
|
||||
const commitSha = (version) => version?.commit_sha || version?.commit || null;
|
||||
|
||||
export const buildReleaseTimelineContext = () => {
|
||||
const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "";
|
||||
const browser = browserInfoFromUserAgent(userAgent);
|
||||
const os = osInfoFromUserAgent(userAgent);
|
||||
const frontendVersion = releaseRuntimeStateMutable.versions?.frontend || {};
|
||||
const apiVersion = releaseRuntimeStateMutable.versions?.api || {};
|
||||
const fallbackFrontendVersion = import.meta.env.VITE_COMMIT_HASH || "unknown";
|
||||
|
||||
return {
|
||||
trace_id: releaseRuntimeStateMutable.traceId,
|
||||
channel_slug: releaseRuntimeStateMutable.channel?.slug || null,
|
||||
route_path: typeof window !== "undefined" ? window.location.pathname : null,
|
||||
device: {
|
||||
type: currentDeviceType(),
|
||||
},
|
||||
browser,
|
||||
os,
|
||||
viewport:
|
||||
typeof window !== "undefined"
|
||||
? {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
device_pixel_ratio: window.devicePixelRatio || 1,
|
||||
}
|
||||
: null,
|
||||
frontend: {
|
||||
version_label: versionLabel(frontendVersion) || fallbackFrontendVersion,
|
||||
commit_sha: commitSha(frontendVersion) || fallbackFrontendVersion,
|
||||
},
|
||||
api: {
|
||||
version_label: versionLabel(apiVersion),
|
||||
commit_sha: commitSha(apiVersion),
|
||||
},
|
||||
frontend_version: versionLabel(frontendVersion) || fallbackFrontendVersion,
|
||||
frontend_commit_sha: commitSha(frontendVersion) || fallbackFrontendVersion,
|
||||
api_version: versionLabel(apiVersion),
|
||||
api_commit_sha: commitSha(apiVersion),
|
||||
};
|
||||
};
|
||||
|
||||
const scheduleReleaseTimelineFlush = () => {
|
||||
if (flushTimer !== null || typeof window === "undefined") {
|
||||
return;
|
||||
@@ -242,11 +334,7 @@ export const flushReleaseTimelineEvents = async () => {
|
||||
|
||||
const body = {
|
||||
events,
|
||||
context: {
|
||||
trace_id: releaseRuntimeStateMutable.traceId,
|
||||
channel_slug: releaseRuntimeStateMutable.channel?.slug || null,
|
||||
frontend_version: import.meta.env.VITE_COMMIT_HASH || "unknown",
|
||||
},
|
||||
context: buildReleaseTimelineContext(),
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -190,6 +190,9 @@ export const listReleaseServiceSets = () => requestReleaseManager("/superuser/re
|
||||
export const createReleaseServiceSet = (payload) =>
|
||||
requestReleaseManager("/superuser/releases/service-sets", "POST", payload);
|
||||
|
||||
export const completeReleaseServiceSetIsolatedDataServices = (id, payload = {}) =>
|
||||
requestReleaseManager(`/superuser/releases/service-sets/${id}/isolated-data-services`, "POST", payload);
|
||||
|
||||
export const listReleaseBundles = (limit = 50) =>
|
||||
requestReleaseManager("/superuser/releases/bundles", "GET", { limit });
|
||||
|
||||
@@ -228,3 +231,9 @@ export const setReleaseReplayTarget = (payload) =>
|
||||
|
||||
export const searchReleaseTimeline = (filters = {}) =>
|
||||
requestReleaseManager("/superuser/releases/timeline", "GET", filters);
|
||||
|
||||
export const listReleaseTimelineSessions = (filters = {}) =>
|
||||
requestReleaseManager("/superuser/releases/timeline/sessions", "GET", filters);
|
||||
|
||||
export const getReleaseTimelineSession = (traceId) =>
|
||||
requestReleaseManager(`/superuser/releases/timeline/sessions/${encodeURIComponent(traceId)}`, "GET", {});
|
||||
|
||||
+185
-195
@@ -8,6 +8,7 @@ import PageTitle from "@/components/global/PageTitle.vue";
|
||||
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
|
||||
import ConfigurationSubPageWrapper from "@/views/dashboards/superUserDashboard/configuration/ConfigurationSubPageWrapper.vue";
|
||||
import ConfigurationError from "@/components/displays/superuser/configuration/ConfigurationError.vue";
|
||||
import ReleaseReplayInspector from "@/components/release/ReleaseReplayInspector.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import {
|
||||
isSearching as isSearchingCustomers,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
} from "@/components/search/economic/customerSearch.vue";
|
||||
import {
|
||||
clearReleaseManagerControlApiUrl,
|
||||
completeReleaseServiceSetIsolatedDataServices,
|
||||
createReleaseAssignment,
|
||||
createReleaseBundle,
|
||||
createReleaseChannel,
|
||||
@@ -33,8 +35,6 @@ import {
|
||||
promoteReleaseDeployment,
|
||||
rollbackReleaseChannel,
|
||||
saveReleaseDeploymentTarget,
|
||||
searchReleaseTimeline,
|
||||
setReleaseReplayTarget,
|
||||
releaseManagerControlApiCandidates,
|
||||
startReleaseDeployment,
|
||||
setReleaseManagerControlApiUrl,
|
||||
@@ -50,6 +50,7 @@ const trFallback = (key, fallback, params = {}) => {
|
||||
const translated = t(fullKey, params);
|
||||
return translated === fullKey ? fallback : translated;
|
||||
};
|
||||
const DEFAULT_RELEASE_BRANCH = "master";
|
||||
const valueLabel = (group, value) => trFallback(`values.${group}.${value}`, String(value || ""));
|
||||
const readableValue = (value) => String(value || "").replace(/_/g, " ");
|
||||
const statusLabel = (value) => trFallback(`values.status.${value}`, readableValue(value));
|
||||
@@ -114,7 +115,6 @@ const summary = ref({
|
||||
module_health: [],
|
||||
suggestions: {},
|
||||
});
|
||||
const timelineEvents = ref([]);
|
||||
const errors = ref([]);
|
||||
const busy = ref(null);
|
||||
const releaseConfig = ref({});
|
||||
@@ -178,10 +178,12 @@ const targetForm = reactive({
|
||||
channel_id: null,
|
||||
app: "frontend",
|
||||
repository: "",
|
||||
branch: "main",
|
||||
branch: DEFAULT_RELEASE_BRANCH,
|
||||
coolify_instance_id: "",
|
||||
coolify_project_uuid: "",
|
||||
coolify_service_uuid: "",
|
||||
coolify_github_app_uuid: "",
|
||||
coolify_build_pack: "nixpacks",
|
||||
health_url: "",
|
||||
auto_deploy: true,
|
||||
coolify_auto_create: false,
|
||||
@@ -195,7 +197,7 @@ const deploymentForm = reactive({
|
||||
channel_id: null,
|
||||
app: "frontend",
|
||||
repository: "",
|
||||
branch: "main",
|
||||
branch: DEFAULT_RELEASE_BRANCH,
|
||||
commit_mode: "latest",
|
||||
commit_sha: "",
|
||||
version_label: "",
|
||||
@@ -207,34 +209,18 @@ const bundleForm = reactive({
|
||||
service_set_name: "",
|
||||
channel_id: null,
|
||||
frontend_repository: "",
|
||||
frontend_branch: "main",
|
||||
frontend_branch: DEFAULT_RELEASE_BRANCH,
|
||||
frontend_commit_mode: "latest",
|
||||
frontend_commit_sha: "",
|
||||
frontend_image: "",
|
||||
api_repository: "",
|
||||
api_branch: "main",
|
||||
api_branch: DEFAULT_RELEASE_BRANCH,
|
||||
api_commit_mode: "latest",
|
||||
api_commit_sha: "",
|
||||
api_image: "",
|
||||
version_label: "",
|
||||
});
|
||||
|
||||
const replayForm = reactive({
|
||||
target_type: "user",
|
||||
target_id: "",
|
||||
channel_id: null,
|
||||
capture_level: "full_redacted",
|
||||
expires_at: "",
|
||||
});
|
||||
|
||||
const timelineFilters = reactive({
|
||||
trace_id: "",
|
||||
channel_slug: "",
|
||||
principal_type: "",
|
||||
principal_id: "",
|
||||
module_key: "",
|
||||
severity: "",
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
const canView = computed(
|
||||
() => SessionUser.canAccessSuperUser() || SessionUser.hasPermission("superuser_release_manager_view")
|
||||
);
|
||||
@@ -269,7 +255,7 @@ const repositorySuggestions = computed(() =>
|
||||
);
|
||||
const branchSuggestions = computed(() =>
|
||||
uniqueStrings([
|
||||
...(Array.isArray(suggestions.value.branches) ? suggestions.value.branches : ["main"]),
|
||||
...(Array.isArray(suggestions.value.branches) ? suggestions.value.branches : [DEFAULT_RELEASE_BRANCH]),
|
||||
...githubBranches.value.map((branch) => branch?.name || "").filter(Boolean),
|
||||
])
|
||||
);
|
||||
@@ -388,7 +374,7 @@ const datasetModeMessage = computed(() =>
|
||||
isIsolatedStackMode.value
|
||||
? trFallback(
|
||||
"bundles.isolated_stack_message",
|
||||
"Creates new Coolify frontend and API services and leaves data services unattached so production data is not used."
|
||||
"Creates new Coolify frontend, API, database, Redis, and MinIO services without attaching production data."
|
||||
)
|
||||
: trFallback(
|
||||
"bundles.dataset_mode_message",
|
||||
@@ -400,7 +386,7 @@ const bundleReviewStack = computed(() => [
|
||||
key: "frontend",
|
||||
label: trFallback("bundles.stack.frontend", "Frontend"),
|
||||
value: bundleForm.frontend_repository || frontendBundleSourceTarget.value?.repository || "--",
|
||||
detail: `${bundleForm.frontend_branch || frontendBundleSourceTarget.value?.branch || "main"} ${
|
||||
detail: `${bundleForm.frontend_branch || frontendBundleSourceTarget.value?.branch || DEFAULT_RELEASE_BRANCH} ${
|
||||
bundleForm.frontend_commit_mode === "specific" ? bundleForm.frontend_commit_sha : trFallback("bundles.latest", "latest")
|
||||
}`,
|
||||
status: isIsolatedStackMode.value
|
||||
@@ -413,7 +399,7 @@ const bundleReviewStack = computed(() => [
|
||||
key: "api",
|
||||
label: trFallback("bundles.stack.api", "PHP backend"),
|
||||
value: bundleForm.api_repository || apiBundleSourceTarget.value?.repository || "--",
|
||||
detail: `${bundleForm.api_branch || apiBundleSourceTarget.value?.branch || "main"} ${
|
||||
detail: `${bundleForm.api_branch || apiBundleSourceTarget.value?.branch || DEFAULT_RELEASE_BRANCH} ${
|
||||
bundleForm.api_commit_mode === "specific" ? bundleForm.api_commit_sha : trFallback("bundles.latest", "latest")
|
||||
}`,
|
||||
status: isIsolatedStackMode.value
|
||||
@@ -452,7 +438,10 @@ const canContinueReleaseFlow = computed(() => {
|
||||
String(bundleForm.frontend_commit_sha || "").trim() !== "") &&
|
||||
(!bundleForm.api_commit_mode ||
|
||||
bundleForm.api_commit_mode === "latest" ||
|
||||
String(bundleForm.api_commit_sha || "").trim() !== "")
|
||||
String(bundleForm.api_commit_sha || "").trim() !== "") &&
|
||||
(!isIsolatedStackMode.value ||
|
||||
((Boolean(isolatedStackImage("frontend")) || Boolean(isolatedStackGithubAppUuid("frontend"))) &&
|
||||
(Boolean(isolatedStackImage("api")) || Boolean(isolatedStackGithubAppUuid("api")))))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
@@ -884,14 +873,6 @@ function controlApiOptions() {
|
||||
});
|
||||
}
|
||||
|
||||
function moduleKeyOptions() {
|
||||
return autocompleteOptions(moduleKeySuggestions.value, timelineFilters.module_key, {
|
||||
category: tr("autocomplete.module"),
|
||||
description: tr("autocomplete.timeline_module_key"),
|
||||
icon: "fas fa-puzzle-piece",
|
||||
});
|
||||
}
|
||||
|
||||
function selectChannelUrl(kind, option) {
|
||||
if (!option) {
|
||||
return;
|
||||
@@ -944,13 +925,6 @@ function selectControlApi(option) {
|
||||
controlApiInput.value = optionValue(option);
|
||||
}
|
||||
|
||||
function selectModuleKey(option) {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
timelineFilters.module_key = optionValue(option);
|
||||
}
|
||||
|
||||
function selectCoolifyService(service) {
|
||||
if (!service) {
|
||||
return;
|
||||
@@ -982,7 +956,7 @@ function prepareCoolifySslTarget(app, url) {
|
||||
repositorySuggestions.value.filter((repository) => /front|vue|web/i.test(repository)),
|
||||
targetForm.repository
|
||||
),
|
||||
branch: targetForm.branch || firstAvailable(branchSuggestions.value, "main"),
|
||||
branch: targetForm.branch || firstAvailable(branchSuggestions.value, DEFAULT_RELEASE_BRANCH),
|
||||
coolify_instance_id: targetForm.coolify_instance_id || coolifyInstances.value[0]?.id || "",
|
||||
coolify_project_uuid: targetForm.coolify_project_uuid || defaultCoolifyProjectUuid(targetForm.coolify_instance_id || coolifyInstances.value[0]?.id),
|
||||
health_url: healthUrlForBaseUrl(app, baseUrl),
|
||||
@@ -1027,10 +1001,12 @@ function applyTargetPreset(preset) {
|
||||
channel_id: targetForm.channel_id || defaultChannel.value?.id || null,
|
||||
app: preset.app || "frontend",
|
||||
repository: preset.repository || firstAvailable(repositorySuggestions.value),
|
||||
branch: preset.branch || firstAvailable(branchSuggestions.value, "main"),
|
||||
branch: preset.branch || firstAvailable(branchSuggestions.value, DEFAULT_RELEASE_BRANCH),
|
||||
coolify_instance_id: targetForm.coolify_instance_id || coolifyInstances.value[0]?.id || "",
|
||||
coolify_project_uuid: preset.coolify_project_uuid || targetForm.coolify_project_uuid || defaultCoolifyProjectUuid(targetForm.coolify_instance_id || coolifyInstances.value[0]?.id),
|
||||
coolify_service_uuid: preset.coolify_service_uuid || firstAvailable(serviceUuidSuggestions.value),
|
||||
coolify_github_app_uuid: preset.coolify_github_app_uuid || targetForm.coolify_github_app_uuid || "",
|
||||
coolify_build_pack: preset.coolify_build_pack || targetForm.coolify_build_pack || "nixpacks",
|
||||
health_url: preset.health_url || suggestedHealthUrl(preset.app || "frontend"),
|
||||
auto_deploy: preset.auto_deploy !== false,
|
||||
coolify_auto_create: Boolean(preset.coolify_auto_create),
|
||||
@@ -1094,10 +1070,16 @@ function applyBundleDefaults() {
|
||||
firstAvailable(repositorySuggestions.value.filter((repository) => /api|backend|php/i.test(repository)));
|
||||
}
|
||||
if (!bundleForm.frontend_branch) {
|
||||
bundleForm.frontend_branch = selectedServiceSet.value?.targets?.frontend?.branch || "main";
|
||||
bundleForm.frontend_branch = selectedServiceSet.value?.targets?.frontend?.branch || DEFAULT_RELEASE_BRANCH;
|
||||
}
|
||||
if (!bundleForm.api_branch) {
|
||||
bundleForm.api_branch = selectedServiceSet.value?.targets?.api?.branch || "main";
|
||||
bundleForm.api_branch = selectedServiceSet.value?.targets?.api?.branch || DEFAULT_RELEASE_BRANCH;
|
||||
}
|
||||
if (isIsolatedStackMode.value && !bundleForm.frontend_image) {
|
||||
bundleForm.frontend_image = firstImageFromContext(frontendBundleSourceTarget.value?.deploy_context || {});
|
||||
}
|
||||
if (isIsolatedStackMode.value && !bundleForm.api_image) {
|
||||
bundleForm.api_image = firstImageFromContext(apiBundleSourceTarget.value?.deploy_context || {});
|
||||
}
|
||||
if (!bundleForm.version_label && bundleForm.channel_id) {
|
||||
const channelSlug = channels.value.find((channel) => Number(channel.id) === Number(bundleForm.channel_id))?.slug || "release";
|
||||
@@ -1124,9 +1106,6 @@ async function load() {
|
||||
bundleForm.channel_id = channels.value[0].id;
|
||||
}
|
||||
applyBundleDefaults();
|
||||
if (!replayForm.channel_id && channels.value[0]) {
|
||||
replayForm.channel_id = channels.value[0].id;
|
||||
}
|
||||
} catch (error) {
|
||||
errors.value.push(error);
|
||||
} finally {
|
||||
@@ -1278,6 +1257,17 @@ function serviceSetStackItem(set, item) {
|
||||
return set?.stack?.[item] || null;
|
||||
}
|
||||
|
||||
function missingIsolatedDataServices(set) {
|
||||
if (set?.mode !== "isolated_stack") {
|
||||
return [];
|
||||
}
|
||||
return ["database", "redis", "minio"].filter((kind) => !serviceSetStackItem(set, kind));
|
||||
}
|
||||
|
||||
function canCompleteIsolatedDataServices(set) {
|
||||
return canDeploy.value && missingIsolatedDataServices(set).length > 0 && serviceSetStackItem(set, "frontend") && serviceSetStackItem(set, "api");
|
||||
}
|
||||
|
||||
function bundleApp(bundle, app) {
|
||||
return bundle?.apps?.[app] || {};
|
||||
}
|
||||
@@ -1320,11 +1310,76 @@ function isolatedStackProjectUuid(instanceId, sourceTarget) {
|
||||
return sourceTarget?.deploy_context?.coolify_project_uuid || defaultCoolifyProjectUuid(instanceId);
|
||||
}
|
||||
|
||||
function safeEnvironmentSlug(value) {
|
||||
return String(value || "")
|
||||
.toLowerCase()
|
||||
.replace(/^refs\/heads\//, "")
|
||||
.replace(/[^a-z0-9_-]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
.slice(0, 64);
|
||||
}
|
||||
|
||||
function releaseCoolifyEnvironmentName(channelSlug, branch) {
|
||||
const channel = safeEnvironmentSlug(channelSlug);
|
||||
if (!channel || ["stable", "production", "prod", "beta"].includes(channel)) {
|
||||
return "";
|
||||
}
|
||||
const branchSlug = safeEnvironmentSlug(branch);
|
||||
if (branchSlug && !["main", "master"].includes(branchSlug)) {
|
||||
return branchSlug;
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
function firstImageFromContext(context = {}) {
|
||||
for (const key of ["image", "docker_image", "coolify_image", "coolify_docker_image", "registry_image"]) {
|
||||
const value = context[key];
|
||||
if (typeof value === "string" && value.trim() !== "") {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function isolatedStackImage(app) {
|
||||
const formImage = String(bundleForm[`${app}_image`] || "").trim();
|
||||
return formImage || firstImageFromContext(isolatedStackSourceTarget(app)?.deploy_context || {});
|
||||
}
|
||||
|
||||
function isolatedStackImageContext(app) {
|
||||
const image = isolatedStackImage(app);
|
||||
return image ? { image } : {};
|
||||
}
|
||||
|
||||
function isolatedStackGithubAppUuid(app) {
|
||||
const context = isolatedStackSourceTarget(app)?.deploy_context || {};
|
||||
return String(
|
||||
context.coolify_github_app_uuid ||
|
||||
context.github_app_uuid ||
|
||||
context.coolify_git_app_uuid ||
|
||||
context.git_app_uuid ||
|
||||
""
|
||||
).trim();
|
||||
}
|
||||
|
||||
function isolatedStackPullContext(app) {
|
||||
const context = isolatedStackSourceTarget(app)?.deploy_context || {};
|
||||
const githubAppUuid = isolatedStackGithubAppUuid(app);
|
||||
if (!githubAppUuid) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
coolify_github_app_uuid: githubAppUuid,
|
||||
coolify_build_pack: String(context.coolify_build_pack || context.build_pack || (app === "api" ? "dockerfile" : "nixpacks")).trim(),
|
||||
};
|
||||
}
|
||||
|
||||
async function createIsolatedStackDeploymentTarget(app) {
|
||||
const sourceTarget = isolatedStackSourceTarget(app);
|
||||
const instanceId = Number(sourceTarget?.coolify_instance_id || coolifyInstances.value[0]?.id || 0) || null;
|
||||
const repository = bundleForm[`${app}_repository`] || sourceTarget?.repository || "";
|
||||
const branch = bundleForm[`${app}_branch`] || sourceTarget?.branch || "main";
|
||||
const branch = bundleForm[`${app}_branch`] || sourceTarget?.branch || DEFAULT_RELEASE_BRANCH;
|
||||
if (!instanceId) {
|
||||
throw new Error(
|
||||
trFallback("errors.isolated_stack_coolify_required", "Select or configure a Coolify instance before creating an isolated stack.")
|
||||
@@ -1347,12 +1402,16 @@ async function createIsolatedStackDeploymentTarget(app) {
|
||||
health_url: "",
|
||||
auto_deploy: true,
|
||||
deploy_context: {
|
||||
...isolatedStackPullContext(app),
|
||||
...isolatedStackImageContext(app),
|
||||
coolify_auto_create: true,
|
||||
coolify_enable_ssl: false,
|
||||
coolify_domain: "",
|
||||
coolify_public_url: "",
|
||||
coolify_deploy_now: true,
|
||||
coolify_project_uuid: isolatedStackProjectUuid(instanceId, sourceTarget),
|
||||
coolify_environment_uuid: "",
|
||||
coolify_environment_name: releaseCoolifyEnvironmentName(selectedBundleChannel()?.slug || "", branch),
|
||||
coolify_service_name: isolatedStackServiceName(app),
|
||||
isolated_stack: true,
|
||||
production_data_attached: false,
|
||||
@@ -1366,6 +1425,15 @@ async function createIsolatedStackDeploymentTarget(app) {
|
||||
return savedTarget;
|
||||
}
|
||||
|
||||
async function completeIsolatedDataServices(set) {
|
||||
await run(`service-set:${set.id}:isolated-data`, async () => {
|
||||
await completeReleaseServiceSetIsolatedDataServices(set.id, {
|
||||
deploy_data_targets: true,
|
||||
});
|
||||
await load();
|
||||
});
|
||||
}
|
||||
|
||||
function selectBundleField(app, field, option) {
|
||||
if (!option) {
|
||||
return;
|
||||
@@ -1430,6 +1498,8 @@ async function serviceSetIdForBundle() {
|
||||
}`,
|
||||
frontend_target_id: isolatedTargets?.frontend?.id || frontendBundleTarget.value?.id || null,
|
||||
api_target_id: isolatedTargets?.api?.id || apiBundleTarget.value?.id || null,
|
||||
create_data_targets: isIsolatedStackMode.value,
|
||||
deploy_data_targets: isIsolatedStackMode.value,
|
||||
metadata: isIsolatedStackMode.value
|
||||
? {
|
||||
isolated_stack: true,
|
||||
@@ -1573,6 +1643,8 @@ async function saveTarget() {
|
||||
coolify_public_url: coolifyDomain ? `https://${coolifyDomain}` : "",
|
||||
coolify_deploy_now: Boolean(targetForm.coolify_deploy_now),
|
||||
coolify_project_uuid: targetForm.coolify_project_uuid || "",
|
||||
coolify_github_app_uuid: String(targetForm.coolify_github_app_uuid || "").trim(),
|
||||
coolify_build_pack: String(targetForm.coolify_build_pack || "").trim() || "nixpacks",
|
||||
};
|
||||
const savedResponse = await saveReleaseDeploymentTarget({
|
||||
...targetForm,
|
||||
@@ -1598,10 +1670,12 @@ async function saveTarget() {
|
||||
id: null,
|
||||
app: "frontend",
|
||||
repository: "",
|
||||
branch: "main",
|
||||
branch: DEFAULT_RELEASE_BRANCH,
|
||||
coolify_instance_id: "",
|
||||
coolify_project_uuid: "",
|
||||
coolify_service_uuid: "",
|
||||
coolify_github_app_uuid: "",
|
||||
coolify_build_pack: "nixpacks",
|
||||
health_url: "",
|
||||
auto_deploy: true,
|
||||
coolify_auto_create: false,
|
||||
@@ -1659,20 +1733,6 @@ async function rollbackChannel(channel) {
|
||||
});
|
||||
}
|
||||
|
||||
async function enableReplayTarget() {
|
||||
await run("replay:target", async () => {
|
||||
await setReleaseReplayTarget({ ...replayForm, enabled: true });
|
||||
Object.assign(replayForm, { target_id: "", expires_at: "" });
|
||||
await load();
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTimeline() {
|
||||
await run("timeline:search", async () => {
|
||||
timelineEvents.value = responseData(await searchReleaseTimeline({ ...timelineFilters }), []);
|
||||
});
|
||||
}
|
||||
|
||||
async function run(key, callback) {
|
||||
busy.value = key;
|
||||
errors.value = [];
|
||||
@@ -1830,6 +1890,7 @@ watch(
|
||||
const channelSlug = channels.value.find((channel) => Number(channel.id) === Number(bundleForm.channel_id))?.slug || "release";
|
||||
bundleForm.service_set_name = `${channelSlug}-${mode.replace(/_/g, "-")}-${new Date().toISOString().slice(0, 10)}`;
|
||||
}
|
||||
applyBundleDefaults();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1866,7 +1927,6 @@ onMounted(async () => {
|
||||
await load();
|
||||
await loadReleaseConfig();
|
||||
await loadGithubRepositories();
|
||||
await loadTimeline();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2461,6 +2521,17 @@ onMounted(async () => {
|
||||
>
|
||||
{{ trFallback("bundles.clone", "Clone") }}
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="canCompleteIsolatedDataServices(set)"
|
||||
size="is-small"
|
||||
icon-left="database"
|
||||
icon-pack="fas"
|
||||
:loading="busy === `service-set:${set.id}:isolated-data`"
|
||||
:data-testid="`release-service-set-complete-isolated-data-${set.id}`"
|
||||
@click="completeIsolatedDataServices(set)"
|
||||
>
|
||||
{{ trFallback("bundles.add_data_services", "Add data services") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
@@ -2479,7 +2550,7 @@ onMounted(async () => {
|
||||
:label="trFallback('bundles.channel', 'Channel')"
|
||||
:message="trFallback('bundles.channel_message', 'The channel that will receive this full-stack bundle.')"
|
||||
>
|
||||
<b-select v-model.number="bundleForm.channel_id" expanded required>
|
||||
<b-select v-model.number="bundleForm.channel_id" data-testid="release-bundle-channel" expanded required>
|
||||
<option v-for="channel in channels" :key="channel.id" :value="channel.id">{{ channel.slug }}</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
@@ -2547,13 +2618,20 @@ onMounted(async () => {
|
||||
v-model="bundleForm.frontend_branch"
|
||||
:data="branchOptions(bundleForm.frontend_branch)"
|
||||
field="value"
|
||||
placeholder="main"
|
||||
:placeholder="DEFAULT_RELEASE_BRANCH"
|
||||
open-on-focus
|
||||
keep-first
|
||||
expanded
|
||||
@select="(option) => selectBundleField('frontend', 'branch', option)"
|
||||
/>
|
||||
</b-field>
|
||||
<b-field
|
||||
v-if="isIsolatedStackMode"
|
||||
:label="trFallback('bundles.container_image', 'Container image')"
|
||||
:message="trFallback('bundles.container_image_message', 'Image must be pullable by the selected Coolify server.')"
|
||||
>
|
||||
<b-input v-model="bundleForm.frontend_image" placeholder="ghcr.io/org/frontend:tag" />
|
||||
</b-field>
|
||||
<b-field :label="tr('deployments.commit_selection')" :message="tr('deployments.commit_selection_message')">
|
||||
<b-select v-model="bundleForm.frontend_commit_mode" expanded>
|
||||
<option value="latest">{{ tr("deployments.latest_from_branch") }}</option>
|
||||
@@ -2588,13 +2666,20 @@ onMounted(async () => {
|
||||
v-model="bundleForm.api_branch"
|
||||
:data="branchOptions(bundleForm.api_branch)"
|
||||
field="value"
|
||||
placeholder="main"
|
||||
:placeholder="DEFAULT_RELEASE_BRANCH"
|
||||
open-on-focus
|
||||
keep-first
|
||||
expanded
|
||||
@select="(option) => selectBundleField('api', 'branch', option)"
|
||||
/>
|
||||
</b-field>
|
||||
<b-field
|
||||
v-if="isIsolatedStackMode"
|
||||
:label="trFallback('bundles.container_image', 'Container image')"
|
||||
:message="trFallback('bundles.container_image_message', 'Image must be pullable by the selected Coolify server.')"
|
||||
>
|
||||
<b-input v-model="bundleForm.api_image" placeholder="ghcr.io/org/api:tag" />
|
||||
</b-field>
|
||||
<b-field :label="tr('deployments.commit_selection')" :message="tr('deployments.commit_selection_message')">
|
||||
<b-select v-model="bundleForm.api_commit_mode" expanded>
|
||||
<option value="latest">{{ tr("deployments.latest_from_branch") }}</option>
|
||||
@@ -3002,128 +3087,12 @@ onMounted(async () => {
|
||||
icon="fas fa-clock-rotate-left"
|
||||
default-expanded
|
||||
>
|
||||
<div class="release-chip-row" data-testid="release-replay-target-suggestions">
|
||||
<b-button
|
||||
v-for="channel in channels"
|
||||
:key="channel.id"
|
||||
size="is-small"
|
||||
type="is-light"
|
||||
icon-left="circle"
|
||||
icon-pack="fas"
|
||||
@click="
|
||||
Object.assign(replayForm, { target_type: 'channel', target_id: channel.slug, channel_id: channel.id })
|
||||
"
|
||||
>
|
||||
{{ tr("replay.capture_channel", { channel: channel.slug }) }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<form class="release-form" data-testid="release-replay-form" @submit.prevent="enableReplayTarget">
|
||||
<b-field :label="tr('replay.target_type')">
|
||||
<b-select v-model="replayForm.target_type" expanded>
|
||||
<option value="user">{{ valueLabel("subject_type", "user") }}</option>
|
||||
<option value="subuser">{{ valueLabel("subject_type", "subuser") }}</option>
|
||||
<option value="customer">{{ valueLabel("subject_type", "customer") }}</option>
|
||||
<option value="channel">{{ valueLabel("subject_type", "channel") }}</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.target')" :message="tr('replay.target_message')">
|
||||
<b-input v-model="replayForm.target_id" :placeholder="tr('replay.target_placeholder')" />
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.channel_scope')" :message="tr('replay.channel_scope_message')">
|
||||
<b-select v-model.number="replayForm.channel_id" expanded>
|
||||
<option :value="null">{{ tr("replay.no_channel_scope") }}</option>
|
||||
<option v-for="channel in channels" :key="channel.id" :value="channel.id">{{ channel.slug }}</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.capture_level')">
|
||||
<b-select v-model="replayForm.capture_level" expanded>
|
||||
<option value="full_redacted">{{ valueLabel("capture_level", "full_redacted") }}</option>
|
||||
<option value="metadata">{{ valueLabel("capture_level", "metadata") }}</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.expires')" :message="tr('replay.expires_message')">
|
||||
<b-input v-model="replayForm.expires_at" type="datetime-local" />
|
||||
</b-field>
|
||||
<div class="release-form-actions">
|
||||
<b-button
|
||||
type="is-dark"
|
||||
native-type="submit"
|
||||
icon-left="play"
|
||||
icon-pack="fas"
|
||||
:disabled="!canReplay"
|
||||
:loading="busy === 'replay:target'"
|
||||
>
|
||||
{{ tr("actions.enable") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form class="release-form mt-3" data-testid="release-timeline-filter-form" @submit.prevent="loadTimeline">
|
||||
<b-field :label="tr('replay.trace_id')" :message="tr('replay.trace_id_message')">
|
||||
<b-input v-model="timelineFilters.trace_id" :placeholder="tr('replay.trace_id_placeholder')" />
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.channel')">
|
||||
<b-input v-model="timelineFilters.channel_slug" :placeholder="tr('replay.channel_placeholder')" />
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.principal_id')">
|
||||
<b-input v-model="timelineFilters.principal_id" :placeholder="tr('replay.principal_id_placeholder')" />
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.module')">
|
||||
<BAutocomplete
|
||||
v-model="timelineFilters.module_key"
|
||||
:data="moduleKeyOptions()"
|
||||
field="value"
|
||||
:placeholder="tr('replay.module_placeholder')"
|
||||
open-on-focus
|
||||
keep-first
|
||||
expanded
|
||||
@select="selectModuleKey"
|
||||
>
|
||||
<template #default="slotProps">
|
||||
<div class="release-autocomplete-option">
|
||||
<i :class="slotProps.option.icon" aria-hidden="true"></i>
|
||||
<span>
|
||||
<strong>{{ slotProps.option.title }}</strong>
|
||||
<small>{{ slotProps.option.description }}</small>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</BAutocomplete>
|
||||
</b-field>
|
||||
<b-field :label="tr('replay.severity')">
|
||||
<b-select v-model="timelineFilters.severity" expanded>
|
||||
<option value="">{{ tr("replay.any_severity") }}</option>
|
||||
<option value="error">{{ valueLabel("severity", "error") }}</option>
|
||||
<option value="warning">{{ valueLabel("severity", "warning") }}</option>
|
||||
<option value="info">{{ valueLabel("severity", "info") }}</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<div class="release-form-actions">
|
||||
<b-button
|
||||
native-type="submit"
|
||||
icon-left="search"
|
||||
icon-pack="fas"
|
||||
:disabled="!canReplay"
|
||||
:loading="busy === 'timeline:search'"
|
||||
>
|
||||
{{ tr("actions.search") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ol class="release-timeline" data-testid="release-timeline-events">
|
||||
<li v-for="event in timelineEvents" :key="event.id">
|
||||
<time>{{ event.occurred_at }}</time>
|
||||
<strong>{{ event.event_type }}</strong>
|
||||
<b-tag class="ml-1" :class="statusClass(event.severity)">{{ valueLabel("severity", event.severity) }}</b-tag>
|
||||
<div class="release-muted">
|
||||
{{ event.channel_slug || "--" }} / {{ event.module_key || "--" }} /
|
||||
{{ event.route_path || event.component || "--" }}
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="timelineEvents.length === 0">{{ tr("replay.no_timeline_events") }}</li>
|
||||
</ol>
|
||||
<ReleaseReplayInspector
|
||||
:channels="channels"
|
||||
:module-keys="moduleKeySuggestions"
|
||||
:can-replay="canReplay"
|
||||
@refresh-summary="load"
|
||||
/>
|
||||
</ConfigurationCategory>
|
||||
|
||||
<ConfigurationCategory
|
||||
@@ -3145,7 +3114,7 @@ onMounted(async () => {
|
||||
>
|
||||
<strong>{{ preset.label }}</strong>
|
||||
<span>{{ preset.repository || tr("integrations.choose_repository") }}</span>
|
||||
<small>{{ preset.branch || "main" }} / {{ preset.health_url || tr("integrations.add_health_url") }}</small>
|
||||
<small>{{ preset.branch || DEFAULT_RELEASE_BRANCH }} / {{ preset.health_url || tr("integrations.add_health_url") }}</small>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -3306,6 +3275,27 @@ onMounted(async () => {
|
||||
</template>
|
||||
</BAutocomplete>
|
||||
</b-field>
|
||||
<b-field
|
||||
:label="trFallback('integrations.coolify_github_app_uuid', 'Coolify GitHub App UUID')"
|
||||
:message="trFallback('integrations.coolify_github_app_uuid_message', 'Used when Release Manager creates a new Coolify application so repository pulls use the GitHub App token.')"
|
||||
>
|
||||
<b-input
|
||||
v-model="targetForm.coolify_github_app_uuid"
|
||||
placeholder="github-app-uuid"
|
||||
data-testid="release-target-coolify-github-app-uuid"
|
||||
/>
|
||||
</b-field>
|
||||
<b-field
|
||||
:label="trFallback('integrations.coolify_build_pack', 'Coolify build pack')"
|
||||
:message="trFallback('integrations.coolify_build_pack_message', 'Build pack for GitHub App based applications.')"
|
||||
>
|
||||
<b-select v-model="targetForm.coolify_build_pack" expanded data-testid="release-target-coolify-build-pack">
|
||||
<option value="nixpacks">nixpacks</option>
|
||||
<option value="dockerfile">dockerfile</option>
|
||||
<option value="dockercompose">dockercompose</option>
|
||||
<option value="static">static</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="tr('integrations.health_url')" :message="tr('integrations.health_url_message')">
|
||||
<BAutocomplete
|
||||
v-model="targetForm.health_url"
|
||||
|
||||
@@ -59,7 +59,7 @@ async function boot(page) {
|
||||
test("users assigned to a configured release channel see the switched notice once on the device", async ({ page }) => {
|
||||
await boot(page);
|
||||
|
||||
await page.goto("/user");
|
||||
await page.goto("/user", { waitUntil: "domcontentloaded" });
|
||||
const notice = page.getByTestId("release-channel-switched-page");
|
||||
await expect(notice).toBeVisible();
|
||||
await expect(notice).toContainText("You are now on Canary");
|
||||
@@ -70,6 +70,6 @@ test("users assigned to a configured release channel see the switched notice onc
|
||||
await page.getByTestId("release-channel-switched-continue").click();
|
||||
await expect(notice).toHaveCount(0);
|
||||
|
||||
await page.goto("/user");
|
||||
await page.goto("/user", { waitUntil: "domcontentloaded" });
|
||||
await expect(page.getByTestId("release-channel-switched-page")).toHaveCount(0);
|
||||
});
|
||||
|
||||
@@ -52,11 +52,11 @@ const availableRuntime = (frontendBaseUrl) => ({
|
||||
},
|
||||
});
|
||||
|
||||
async function boot(page, runtime = unavailableRuntime) {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("locale", "en");
|
||||
async function boot(page, runtime = unavailableRuntime, locale = "en") {
|
||||
await page.addInitScript((selectedLocale) => {
|
||||
window.localStorage.setItem("locale", selectedLocale);
|
||||
window.localStorage.removeItem("release_channel_unavailable_ignore_until");
|
||||
});
|
||||
}, locale);
|
||||
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
@@ -76,7 +76,7 @@ test("users assigned to an unconfigured release channel can ignore the guard tem
|
||||
await route.fulfill(json({ data: unavailableRuntime }));
|
||||
});
|
||||
|
||||
await page.goto("/user");
|
||||
await page.goto("/user", { waitUntil: "domcontentloaded" });
|
||||
const guard = page.getByTestId("release-channel-unavailable-page");
|
||||
await expect(guard).toBeVisible();
|
||||
await expect(guard).toContainText("Release channel is not ready");
|
||||
@@ -90,6 +90,33 @@ test("users assigned to an unconfigured release channel can ignore the guard tem
|
||||
await page.evaluate(() => window.localStorage.removeItem("release_channel_unavailable_ignore_until"));
|
||||
});
|
||||
|
||||
test("predefined release channel text is localized on the guard page", async ({ page }) => {
|
||||
const internalRuntime = {
|
||||
...unavailableRuntime,
|
||||
channel: {
|
||||
...unavailableRuntime.channel,
|
||||
slug: "internal",
|
||||
name: "Internal",
|
||||
description: "Internal staff and superuser validation channel.",
|
||||
},
|
||||
};
|
||||
|
||||
await boot(page, internalRuntime, "da");
|
||||
await page.route("**/release/runtime", async (route) => {
|
||||
await route.fulfill(json({ data: internalRuntime }));
|
||||
});
|
||||
|
||||
await page.goto("/user", { waitUntil: "domcontentloaded" });
|
||||
const guard = page.getByTestId("release-channel-unavailable-page");
|
||||
await expect(guard).toBeVisible();
|
||||
await expect(guard).toContainText("Release-kanalen er ikke klar");
|
||||
await expect(guard).toContainText("Intern");
|
||||
await expect(guard).toContainText("Intern kanal til medarbejdere");
|
||||
await expect(guard).not.toContainText("Internal staff");
|
||||
await expect(guard).not.toContainText(/\binternal\b/);
|
||||
await expect(guard).not.toContainText(/\p{L}\?\p{L}|\?\p{L}/u);
|
||||
});
|
||||
|
||||
test("users assigned to an unconfigured release channel can check again when it becomes ready", async ({ page }) => {
|
||||
await boot(page);
|
||||
let releaseRuntime = unavailableRuntime;
|
||||
@@ -98,7 +125,7 @@ test("users assigned to an unconfigured release channel can check again when it
|
||||
await route.fulfill(json({ data: releaseRuntime }));
|
||||
});
|
||||
|
||||
await page.goto("/user");
|
||||
await page.goto("/user", { waitUntil: "domcontentloaded" });
|
||||
const guard = page.getByTestId("release-channel-unavailable-page");
|
||||
await expect(guard).toBeVisible();
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { mockApi, seedAuthenticatedState } from "./support/network.js";
|
||||
|
||||
test.describe.configure({ mode: "serial", timeout: 180_000 });
|
||||
|
||||
const DEFAULT_RELEASE_BRANCH = "master";
|
||||
|
||||
const permissions = [
|
||||
"superuser",
|
||||
"user",
|
||||
@@ -27,6 +31,7 @@ function createReleaseState() {
|
||||
nextDeploymentId: 3,
|
||||
nextServiceSetId: 2,
|
||||
nextBundleId: 1,
|
||||
nextCoolifyTargetId: 20,
|
||||
channels: [
|
||||
{
|
||||
id: 1,
|
||||
@@ -85,7 +90,7 @@ function createReleaseState() {
|
||||
channel_slug: "canary",
|
||||
app: "frontend",
|
||||
repository: "truckwash/front-end-vue",
|
||||
branch: "main",
|
||||
branch: DEFAULT_RELEASE_BRANCH,
|
||||
coolify_instance_id: 3,
|
||||
coolify_instance_label: "Production Coolify",
|
||||
coolify_service_uuid: "service-canary",
|
||||
@@ -109,7 +114,7 @@ function createReleaseState() {
|
||||
channel_slug: "canary",
|
||||
app: "frontend",
|
||||
repository: "truckwash/front-end-vue",
|
||||
branch: "main",
|
||||
branch: DEFAULT_RELEASE_BRANCH,
|
||||
},
|
||||
api: null,
|
||||
},
|
||||
@@ -172,7 +177,7 @@ function createReleaseState() {
|
||||
channel_slug: "canary",
|
||||
app: "frontend",
|
||||
repository: "truckwash/front-end-vue",
|
||||
branch: "main",
|
||||
branch: DEFAULT_RELEASE_BRANCH,
|
||||
commit_sha: "c0ffee",
|
||||
version_label: "frontend-canary",
|
||||
status: "deployed",
|
||||
@@ -181,6 +186,49 @@ function createReleaseState() {
|
||||
},
|
||||
],
|
||||
replayTargets: [],
|
||||
timelineSessions: [
|
||||
{
|
||||
id: 7,
|
||||
trace_id: "trace-canary-1",
|
||||
principal_type: "user",
|
||||
principal_id: "42",
|
||||
customer_number: 424242,
|
||||
user: {
|
||||
type: "user",
|
||||
id: "42",
|
||||
customer_number: 424242,
|
||||
name: "Acme Dispatcher",
|
||||
email: "dispatcher@example.test",
|
||||
label: "Acme Dispatcher",
|
||||
},
|
||||
channel_id: 2,
|
||||
channel_slug: "canary",
|
||||
release: {
|
||||
frontend: { version_label: "frontend-canary", commit_sha: "c0ffee" },
|
||||
api: { version_label: "api-canary", commit_sha: "def456" },
|
||||
},
|
||||
device: {
|
||||
type: "desktop",
|
||||
browser_name: "Chrome",
|
||||
browser_version: "125.0.0",
|
||||
os_name: "Windows",
|
||||
os_version: "11",
|
||||
viewport_width: 1440,
|
||||
viewport_height: 900,
|
||||
device_pixel_ratio: 1,
|
||||
user_agent: "Mozilla/5.0 Chrome/125.0.0",
|
||||
},
|
||||
last_route_path: "/superuser/configuration/releases",
|
||||
event_count: 1,
|
||||
error_count: 1,
|
||||
error_report_count: 1,
|
||||
module_keys: ["requestqueue"],
|
||||
first_event_at: "2026-05-19T08:10:00.000Z",
|
||||
last_event_at: "2026-05-19T08:10:00.000Z",
|
||||
created_at: "2026-05-19T08:09:00.000Z",
|
||||
last_seen_at: "2026-05-19T08:10:00.000Z",
|
||||
},
|
||||
],
|
||||
timelineEvents: [
|
||||
{
|
||||
id: 99,
|
||||
@@ -192,6 +240,10 @@ function createReleaseState() {
|
||||
route_path: "/superuser/configuration/releases",
|
||||
component: null,
|
||||
occurred_at: "2026-05-19T08:10:00.000Z",
|
||||
payload: {
|
||||
request: { url: "/superuser/releases", headers: { Authorization: "[redacted]" } },
|
||||
response: { status: 500 },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -225,13 +277,13 @@ function summary(state) {
|
||||
},
|
||||
],
|
||||
timeline: {
|
||||
sessions: 1,
|
||||
sessions: state.timelineSessions.length,
|
||||
events: state.timelineEvents.length,
|
||||
},
|
||||
module_keys: ["requestqueue", "frontend", "auth", "coolify"],
|
||||
suggestions: {
|
||||
repositories: ["truckwash/front-end-vue", "truckwash/backend-php"],
|
||||
branches: ["main", "release/canary", "release/beta"],
|
||||
branches: [DEFAULT_RELEASE_BRANCH, "release/canary", "release/beta"],
|
||||
frontend_base_urls: ["http://localhost:5173", "https://canary.example.test", "https://app.example.test"],
|
||||
api_base_urls: ["http://api.truckwash.io:4433", "https://api-canary.example.test", "https://api.example.test"],
|
||||
health_urls: [
|
||||
@@ -281,7 +333,7 @@ function summary(state) {
|
||||
label: "Frontend target",
|
||||
app: "frontend",
|
||||
repository: "truckwash/front-end-vue",
|
||||
branch: "main",
|
||||
branch: DEFAULT_RELEASE_BRANCH,
|
||||
health_url: "https://canary.example.test/health",
|
||||
auto_deploy: true,
|
||||
},
|
||||
@@ -302,6 +354,44 @@ function channelSlug(state, id) {
|
||||
return state.channels.find((channel) => Number(channel.id) === Number(id))?.slug || "stable";
|
||||
}
|
||||
|
||||
function isolatedDataService(state, kind, stackName = "isolated-stack") {
|
||||
const id = state.nextCoolifyTargetId++;
|
||||
return {
|
||||
id,
|
||||
kind,
|
||||
label: `release-${stackName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${kind}`,
|
||||
role: "replica",
|
||||
resource_name: `release-${stackName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${kind}-${id}`,
|
||||
resource_uuid: `isolated-${kind}-${id}`,
|
||||
deployment_status: "deploying",
|
||||
availability_state: "degraded",
|
||||
options: {
|
||||
isolated_stack: true,
|
||||
skip_replication_provisioning: true,
|
||||
production_data_attached: false,
|
||||
},
|
||||
replication: {
|
||||
id: id + 1000,
|
||||
label: `release-${stackName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${kind}`,
|
||||
status: "unknown",
|
||||
role: "replica",
|
||||
source_host_id: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function completeIsolatedDataServices(state, serviceSet) {
|
||||
serviceSet.data_services = serviceSet.data_services || {};
|
||||
for (const kind of ["database", "redis", "minio"]) {
|
||||
if (!serviceSet.data_services[kind]) {
|
||||
serviceSet.data_services[kind] = isolatedDataService(state, kind, serviceSet.name || "isolated-stack");
|
||||
}
|
||||
}
|
||||
serviceSet.status = "isolated_stack";
|
||||
serviceSet.health = { status: "isolated_stack", stack_complete: true };
|
||||
return serviceSet;
|
||||
}
|
||||
|
||||
async function boot(page, state = createReleaseState()) {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("locale", "en");
|
||||
@@ -404,7 +494,7 @@ async function installReleaseMocks(page, state) {
|
||||
full_name: "truckwash/front-end-vue",
|
||||
name: "front-end-vue",
|
||||
private: true,
|
||||
default_branch: "main",
|
||||
default_branch: DEFAULT_RELEASE_BRANCH,
|
||||
description: "Truckwash dashboard frontend",
|
||||
},
|
||||
{
|
||||
@@ -429,7 +519,7 @@ async function installReleaseMocks(page, state) {
|
||||
token_configured: true,
|
||||
repository,
|
||||
branches: [
|
||||
{ name: "main", commit_sha: "c0ffee", protected: true },
|
||||
{ name: DEFAULT_RELEASE_BRANCH, commit_sha: "c0ffee", protected: true },
|
||||
{ name: "release/canary", commit_sha: "feedface", protected: false },
|
||||
],
|
||||
},
|
||||
@@ -440,7 +530,7 @@ async function installReleaseMocks(page, state) {
|
||||
|
||||
if (pathname.endsWith("/superuser/releases/github/commits") && method === "GET") {
|
||||
const repository = url.searchParams.get("repository") || "truckwash/front-end-vue";
|
||||
const branch = url.searchParams.get("branch") || "main";
|
||||
const branch = url.searchParams.get("branch") || DEFAULT_RELEASE_BRANCH;
|
||||
await route.fulfill(
|
||||
json({
|
||||
data: {
|
||||
@@ -480,7 +570,7 @@ async function installReleaseMocks(page, state) {
|
||||
token_configured: true,
|
||||
message: "Repository and branch are accessible with the configured GitHub token.",
|
||||
repository: payload.repository,
|
||||
branch: payload.branch || "main",
|
||||
branch: payload.branch || DEFAULT_RELEASE_BRANCH,
|
||||
commit_mode: payload.commit_mode || "latest",
|
||||
commit_sha: payload.commit_mode === "specific" ? payload.commit_sha : latestSha,
|
||||
latest_commit_sha: latestSha,
|
||||
@@ -524,7 +614,7 @@ async function installReleaseMocks(page, state) {
|
||||
channel_slug: channelSlug(state, channelId),
|
||||
app: "api",
|
||||
repository: "truckwash/backend-php",
|
||||
branch: "main",
|
||||
branch: DEFAULT_RELEASE_BRANCH,
|
||||
commit_sha: "rollback",
|
||||
version_label: "rollback",
|
||||
status: "rolled_back",
|
||||
@@ -611,11 +701,26 @@ async function installReleaseMocks(page, state) {
|
||||
},
|
||||
attached_bundles: [],
|
||||
};
|
||||
if (payload.mode === "isolated_stack" && payload.create_data_targets !== false) {
|
||||
completeIsolatedDataServices(state, serviceSet);
|
||||
}
|
||||
state.serviceSets.unshift(serviceSet);
|
||||
await route.fulfill(json({ data: summary(state).service_sets.find((entry) => entry.id === serviceSet.id) }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (/\/superuser\/releases\/service-sets\/\d+\/isolated-data-services$/.test(pathname) && method === "POST") {
|
||||
const id = Number(pathname.match(/service-sets\/(\d+)\/isolated-data-services/)?.[1] || 0);
|
||||
const serviceSet = state.serviceSets.find((entry) => Number(entry.id) === id);
|
||||
if (!serviceSet || serviceSet.mode !== "isolated_stack") {
|
||||
await route.fulfill(json({ message: "Only isolated stacks can add data services." }, 400));
|
||||
return;
|
||||
}
|
||||
completeIsolatedDataServices(state, serviceSet);
|
||||
await route.fulfill(json({ data: summary(state).service_sets.find((entry) => entry.id === serviceSet.id) }, 202));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/superuser/releases/bundles") && method === "GET") {
|
||||
await route.fulfill(json({ data: state.bundles }));
|
||||
return;
|
||||
@@ -637,7 +742,7 @@ async function installReleaseMocks(page, state) {
|
||||
version_id: state.nextBundleId * 10 + 1,
|
||||
deployment_id: null,
|
||||
repository: payload.frontend?.repository || "truckwash/front-end-vue",
|
||||
branch: payload.frontend?.branch || "main",
|
||||
branch: payload.frontend?.branch || DEFAULT_RELEASE_BRANCH,
|
||||
commit_sha: payload.frontend?.commit_mode === "specific" ? payload.frontend?.commit_sha : "c0ffee",
|
||||
},
|
||||
api: {
|
||||
@@ -706,7 +811,7 @@ async function installReleaseMocks(page, state) {
|
||||
channel_slug: channelSlug(state, channelId),
|
||||
app: payload.app || target?.app || "frontend",
|
||||
repository: payload.repository || target?.repository || "truckwash/front-end-vue",
|
||||
branch: payload.branch || target?.branch || "main",
|
||||
branch: payload.branch || target?.branch || DEFAULT_RELEASE_BRANCH,
|
||||
commit_sha: payload.commit_sha || "latestcommit",
|
||||
version_label: payload.version_label || "manual-release",
|
||||
status: "deploying",
|
||||
@@ -739,6 +844,53 @@ async function installReleaseMocks(page, state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/superuser/releases/timeline/sessions") && method === "GET") {
|
||||
await route.fulfill(json({ data: state.timelineSessions }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (/\/superuser\/releases\/timeline\/sessions\/[^/]+$/.test(pathname) && method === "GET") {
|
||||
const traceId = decodeURIComponent(pathname.split("/").pop() || "");
|
||||
const session = state.timelineSessions.find((entry) => entry.trace_id === traceId);
|
||||
await route.fulfill(
|
||||
json({
|
||||
data: {
|
||||
session,
|
||||
events: state.timelineEvents.filter((event) => event.trace_id === traceId),
|
||||
error_reports: [
|
||||
{
|
||||
id: 17,
|
||||
status: "open",
|
||||
reporter: { type: "user", user_id: 42, name: "Acme Dispatcher" },
|
||||
route_path: "/superuser/configuration/releases",
|
||||
release_trace_id: traceId,
|
||||
frontend_version: "frontend-canary",
|
||||
api_version: "api-canary",
|
||||
request_error_count: 1,
|
||||
vue_error_count: 0,
|
||||
created_at: "2026-05-19T08:11:00.000Z",
|
||||
},
|
||||
],
|
||||
release: {
|
||||
channel: state.channels.find((channel) => channel.slug === session?.channel_slug) || null,
|
||||
frontend: {
|
||||
...(session?.release?.frontend || {}),
|
||||
version: { id: 12, app: "frontend", status: "deployed", version_label: "frontend-canary" },
|
||||
deployment: { id: 21, status: "deployed", bundle_id: 31 },
|
||||
},
|
||||
api: {
|
||||
...(session?.release?.api || {}),
|
||||
version: { id: 13, app: "api", status: "deployed", version_label: "api-canary" },
|
||||
deployment: { id: 22, status: "deployed", bundle_id: 31 },
|
||||
},
|
||||
bundle: { id: 31, version_label: "canary-bundle", status: "deployed" },
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/superuser/releases/timeline") && method === "GET") {
|
||||
await route.fulfill(json({ data: state.timelineEvents }));
|
||||
return;
|
||||
@@ -765,7 +917,7 @@ async function selectReleaseTab(page, name) {
|
||||
|
||||
test("superusers manage release channels, assignments, deployments, and replay", async ({ page }) => {
|
||||
await boot(page);
|
||||
await page.goto("/superuser/configuration/releases");
|
||||
await page.goto("/superuser/configuration/releases", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await expect(page.getByTestId("release-manager-page")).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByRole("link", { name: /Release Manager/i }).first()).toBeVisible();
|
||||
@@ -855,6 +1007,8 @@ test("superusers manage release channels, assignments, deployments, and replay",
|
||||
await bundleFlow.getByRole("button", { name: "Next" }).click();
|
||||
await expect(bundleFlow.getByPlaceholder("owner/frontend")).toHaveValue("truckwash/front-end-vue");
|
||||
await expect(bundleFlow.getByPlaceholder("owner/backend-php")).toHaveValue("truckwash/backend-php");
|
||||
await expect(bundleFlow.getByPlaceholder(DEFAULT_RELEASE_BRANCH).first()).toHaveValue(DEFAULT_RELEASE_BRANCH);
|
||||
await expect(bundleFlow.getByPlaceholder(DEFAULT_RELEASE_BRANCH).nth(1)).toHaveValue(DEFAULT_RELEASE_BRANCH);
|
||||
await bundleFlow.getByRole("button", { name: "Next" }).click();
|
||||
await expect(page.getByTestId("release-bundle-review-stack")).toContainText("Frontend");
|
||||
await expect(page.getByTestId("release-bundle-review-stack")).toContainText("PHP backend");
|
||||
@@ -885,25 +1039,70 @@ test("superusers manage release channels, assignments, deployments, and replay",
|
||||
await page.getByTestId("release-replay-form").getByRole("button", { name: "Enable" }).click();
|
||||
await page.getByTestId("release-timeline-filter-form").getByPlaceholder("Trace id").fill("trace-canary-1");
|
||||
await page.getByTestId("release-timeline-filter-form").getByRole("button", { name: "Search" }).click();
|
||||
await expect(page.getByTestId("release-timeline-sessions")).toContainText("Acme Dispatcher");
|
||||
await expect(page.getByTestId("release-timeline-sessions")).toContainText("desktop / Chrome / Windows");
|
||||
await expect(page.getByTestId("release-timeline-events")).toContainText("request_failed");
|
||||
const sessionActions = page.getByTestId("release-session-actions-trace-canary-1");
|
||||
await sessionActions.getByRole("button").first().click();
|
||||
await sessionActions.getByRole("button", { name: "Inspect" }).click();
|
||||
await expect(page.getByTestId("release-timeline-session-modal")).toBeVisible();
|
||||
await page.getByTestId("release-timeline-session-detail").getByRole("button", { name: "Release" }).click();
|
||||
await expect(page.getByTestId("release-timeline-session-detail")).toContainText("frontend-canary");
|
||||
await expect(page.getByTestId("release-timeline-session-detail")).toContainText("#21 deployed");
|
||||
await expect(page.getByTestId("release-timeline-session-detail")).toContainText("#31 canary-bundle");
|
||||
await page.getByTestId("release-timeline-session-detail").getByRole("button", { name: "Device" }).click();
|
||||
await expect(page.getByTestId("release-timeline-session-detail")).toContainText("Chrome");
|
||||
await page.getByTestId("release-timeline-session-detail").getByRole("button", { name: "Error reports" }).click();
|
||||
await expect(page.getByTestId("release-timeline-session-detail")).toContainText("#17");
|
||||
await expect(page.getByTestId("release-timeline-session-detail")).toContainText("Visual replay");
|
||||
});
|
||||
|
||||
test("isolated stack mode creates fresh Coolify app targets without attaching production data", async ({ page }) => {
|
||||
test("isolated stack mode creates fresh Coolify app and data targets without attaching production data", async ({
|
||||
page,
|
||||
}) => {
|
||||
const state = await boot(page);
|
||||
await page.goto("/superuser/configuration/releases");
|
||||
state.targets[0].deploy_context = {
|
||||
coolify_project_uuid: "project-internal",
|
||||
coolify_github_app_uuid: "github-app-copenhagentruckwash-github",
|
||||
coolify_build_pack: "nixpacks",
|
||||
};
|
||||
state.targets.push({
|
||||
id: state.nextTargetId++,
|
||||
channel_id: 2,
|
||||
channel_slug: "canary",
|
||||
app: "api",
|
||||
repository: "truckwash/backend-php",
|
||||
branch: "release/canary",
|
||||
coolify_instance_id: 3,
|
||||
coolify_instance_label: "Production Coolify",
|
||||
coolify_service_uuid: "api-canary-service",
|
||||
health_url: "https://api-canary.example.test/ping",
|
||||
auto_deploy: true,
|
||||
deploy_context: {
|
||||
coolify_project_uuid: "project-internal",
|
||||
coolify_github_app_uuid: "github-app-copenhagentruckwash-github",
|
||||
coolify_build_pack: "dockerfile",
|
||||
},
|
||||
});
|
||||
await page.goto("/superuser/configuration/releases", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await selectReleaseTab(page, "Deployments");
|
||||
await expandActiveReleaseCategory(page);
|
||||
|
||||
const bundleFlow = page.getByTestId("release-bundle-flow");
|
||||
const bundleChannel = bundleFlow.getByTestId("release-bundle-channel");
|
||||
await bundleChannel.selectOption("2");
|
||||
await expect(bundleChannel).toHaveValue("2");
|
||||
await bundleFlow.getByTestId("release-bundle-dataset-mode").selectOption("isolated_stack");
|
||||
await expect(bundleFlow).toContainText("production data is not used");
|
||||
await bundleChannel.selectOption("2");
|
||||
await expect(bundleChannel).toHaveValue("2");
|
||||
await expect(bundleFlow).toContainText("without attaching production data");
|
||||
await expect(bundleFlow.getByTestId("release-bundle-source-service-set")).toBeDisabled();
|
||||
await bundleFlow.getByPlaceholder("canary fresh data").fill("Internal safe stack");
|
||||
|
||||
await bundleFlow.getByRole("button", { name: "Next" }).click();
|
||||
await expect(bundleFlow.getByPlaceholder("owner/frontend")).toHaveValue("truckwash/front-end-vue");
|
||||
await expect(bundleFlow.getByPlaceholder("owner/backend-php")).toHaveValue("truckwash/backend-php");
|
||||
await expect(bundleFlow.getByPlaceholder("owner/frontend", { exact: true })).toHaveValue("truckwash/front-end-vue");
|
||||
await expect(bundleFlow.getByPlaceholder("owner/backend-php", { exact: true })).toHaveValue("truckwash/backend-php");
|
||||
|
||||
await bundleFlow.getByRole("button", { name: "Next" }).click();
|
||||
await expect(page.getByTestId("release-bundle-review-stack")).toContainText("new isolated service");
|
||||
@@ -920,10 +1119,69 @@ test("isolated stack mode creates fresh Coolify app targets without attaching pr
|
||||
expect(target.deploy_context.coolify_auto_create).toBe(true);
|
||||
expect(target.deploy_context.coolify_enable_ssl).toBe(false);
|
||||
expect(target.deploy_context.production_data_attached).toBe(false);
|
||||
expect(target.deploy_context.coolify_github_app_uuid).toBe("github-app-copenhagentruckwash-github");
|
||||
expect(target.deploy_context.coolify_build_pack).toBe(target.app === "api" ? "dockerfile" : "nixpacks");
|
||||
expect(target.deploy_context.image || "").toBe("");
|
||||
expect(target.deploy_context.coolify_environment_name).toBe("canary");
|
||||
expect(target.deploy_context.coolify_service_name).toMatch(/^release-internal-safe-stack-(api|frontend)$/);
|
||||
}
|
||||
|
||||
const isolatedSet = state.serviceSets.find((set) => set.mode === "isolated_stack");
|
||||
expect(isolatedSet.source_service_set_id).toBeNull();
|
||||
expect(isolatedSet.data_services).toEqual({ database: null, redis: null, minio: null });
|
||||
expect(Object.keys(isolatedSet.data_services).sort()).toEqual(["database", "minio", "redis"]);
|
||||
for (const service of Object.values(isolatedSet.data_services)) {
|
||||
expect(service.options.isolated_stack).toBe(true);
|
||||
expect(service.options.production_data_attached).toBe(false);
|
||||
expect(service.options.skip_replication_provisioning).toBe(true);
|
||||
expect(service.replication.source_host_id).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
test("existing isolated stacks can add missing data services safely", async ({ page }) => {
|
||||
const state = createReleaseState();
|
||||
state.serviceSets.unshift({
|
||||
id: state.nextServiceSetId++,
|
||||
channel_id: 2,
|
||||
channel_slug: "canary",
|
||||
name: "Internal isolated stack",
|
||||
slug: "internal-isolated-stack",
|
||||
mode: "isolated_stack",
|
||||
status: "needs_isolated_targets",
|
||||
targets: {
|
||||
frontend: {
|
||||
id: 101,
|
||||
channel_id: 2,
|
||||
channel_slug: "canary",
|
||||
app: "frontend",
|
||||
repository: "truckwash/front-end-vue",
|
||||
branch: DEFAULT_RELEASE_BRANCH,
|
||||
deploy_context: { isolated_stack: true, coolify_auto_create: true, production_data_attached: false },
|
||||
},
|
||||
api: {
|
||||
id: 102,
|
||||
channel_id: 2,
|
||||
channel_slug: "canary",
|
||||
app: "api",
|
||||
repository: "truckwash/backend-php",
|
||||
branch: "release/canary",
|
||||
deploy_context: { isolated_stack: true, coolify_auto_create: true, production_data_attached: false },
|
||||
},
|
||||
},
|
||||
data_services: { database: null, redis: null, minio: null },
|
||||
attached_bundles: [],
|
||||
});
|
||||
|
||||
await boot(page, state);
|
||||
await page.goto("/superuser/configuration/releases", { waitUntil: "domcontentloaded" });
|
||||
await selectReleaseTab(page, "Deployments");
|
||||
await expandActiveReleaseCategory(page);
|
||||
|
||||
await expect(page.getByTestId("release-service-set-cards")).toContainText("Internal isolated stack");
|
||||
await page.getByTestId("release-service-set-complete-isolated-data-2").click();
|
||||
await expect(page.getByTestId("release-service-set-complete-isolated-data-2")).toHaveCount(0);
|
||||
|
||||
const serviceSet = state.serviceSets.find((set) => set.slug === "internal-isolated-stack");
|
||||
expect(serviceSet.data_services.database.options.production_data_attached).toBe(false);
|
||||
expect(serviceSet.data_services.redis.options.skip_replication_provisioning).toBe(true);
|
||||
expect(serviceSet.data_services.minio.options.isolated_stack).toBe(true);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readJsonFile } from "./helpers/readJsonFile";
|
||||
|
||||
const root = process.cwd();
|
||||
const activeLocales = ["da", "en", "sv", "de", "no"];
|
||||
const channelKeys = ["stable", "canary", "internal"];
|
||||
|
||||
const suspiciousTranslationArtifact =
|
||||
/(?:\p{L}\?\p{L}|(?:^|[\s([{])\?\p{L}|\p{L}\?@:\{|\u00c3|\u00c2|\ufffd|\u00ef\u00bf\u00bd)/u;
|
||||
|
||||
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
|
||||
const mergeMessages = (sharedMessages = {}, localeMessages = {}) => {
|
||||
const mergedMessages = { ...sharedMessages };
|
||||
|
||||
for (const [key, value] of Object.entries(localeMessages ?? {})) {
|
||||
if (isPlainObject(value) && isPlainObject(mergedMessages[key])) {
|
||||
mergedMessages[key] = mergeMessages(mergedMessages[key], value);
|
||||
} else {
|
||||
mergedMessages[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return mergedMessages;
|
||||
};
|
||||
|
||||
const flattenStrings = (value, prefix = "") => {
|
||||
if (typeof value === "string") {
|
||||
return [{ key: prefix, value }];
|
||||
}
|
||||
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(value).flatMap(([key, nestedValue]) =>
|
||||
flattenStrings(nestedValue, prefix ? `${prefix}.${key}` : key)
|
||||
);
|
||||
};
|
||||
|
||||
const loadReleaseManagerCatalogs = () =>
|
||||
activeLocales.flatMap((locale) => {
|
||||
const runtimeV1 = readJsonFile(join(root, `src/i18n/locales/${locale}.json`)).configuration.release_manager;
|
||||
const source = readJsonFile(join(root, `src/i18n/source/${locale}/phrases/compat/configuration/index.json`)).compat
|
||||
.configuration.release_manager;
|
||||
const localeV2 = readJsonFile(join(root, `src/i18n/generated/${locale}-v2.json`));
|
||||
const generatedV2 = localeV2.templates.generated.compat.configuration.release_manager;
|
||||
const globalV2 = readJsonFile(join(root, "src/i18n/generated/global-v2.json"));
|
||||
const runtimeV2 = mergeMessages(mergeMessages(globalV2.shared, globalV2.locales?.[locale]), localeV2).configuration
|
||||
.release_manager;
|
||||
|
||||
return [
|
||||
[`${locale} v1`, runtimeV1],
|
||||
[`${locale} source`, source],
|
||||
[`${locale} generated v2`, generatedV2],
|
||||
[`${locale} runtime v2`, runtimeV2],
|
||||
];
|
||||
});
|
||||
|
||||
describe("release manager i18n", () => {
|
||||
it("does not contain replacement-character translation artifacts", () => {
|
||||
const failures = [];
|
||||
|
||||
for (const [name, releaseManager] of loadReleaseManagerCatalogs()) {
|
||||
for (const { key, value } of flattenStrings(releaseManager)) {
|
||||
if (suspiciousTranslationArtifact.test(value)) {
|
||||
failures.push(`${name}: ${key} = ${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(failures).toEqual([]);
|
||||
});
|
||||
|
||||
it("defines localized names and descriptions for built-in release channels", () => {
|
||||
for (const [name, releaseManager] of loadReleaseManagerCatalogs()) {
|
||||
expect(Object.keys(releaseManager.channel_names ?? {}), `${name} channel_names`).toEqual(channelKeys);
|
||||
expect(Object.keys(releaseManager.channel_descriptions ?? {}), `${name} channel_descriptions`).toEqual(
|
||||
channelKeys
|
||||
);
|
||||
|
||||
for (const key of channelKeys) {
|
||||
expect(releaseManager.channel_names[key], `${name} channel_names.${key}`).toEqual(expect.any(String));
|
||||
expect(releaseManager.channel_descriptions[key], `${name} channel_descriptions.${key}`).toEqual(
|
||||
expect.any(String)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("uses Danish copy for the internal channel guard", () => {
|
||||
const danishReleaseManager = readJsonFile(join(root, "src/i18n/locales/da.json")).configuration.release_manager;
|
||||
|
||||
expect(danishReleaseManager.channel_names.internal).toBe("Intern");
|
||||
expect(danishReleaseManager.channel_descriptions.internal).toContain("Intern kanal");
|
||||
expect(danishReleaseManager.channel_descriptions.internal).not.toBe(
|
||||
"Internal staff and superuser validation channel."
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
__resetReleaseTimelineForTests,
|
||||
__setReleaseTimelineTransportForTests,
|
||||
buildReleaseTimelineContext,
|
||||
configureReleaseRuntime,
|
||||
flushReleaseTimelineEvents,
|
||||
getRecentFrontendFailureEvents,
|
||||
@@ -128,6 +129,29 @@ describe("release timeline runtime", () => {
|
||||
|
||||
expect(sentBodies).toHaveLength(1);
|
||||
expect(sentBodies[0].context.channel_slug).toBe("beta");
|
||||
expect(sentBodies[0].context.device.type).toBe("desktop");
|
||||
expect(sentBodies[0].context.frontend.version_label).toBeTruthy();
|
||||
expect(sentBodies[0].events[0].type).toBe("route_change");
|
||||
});
|
||||
|
||||
it("builds device and release context for replay sessions", () => {
|
||||
configureReleaseRuntime({
|
||||
trace_id: "trace-context",
|
||||
channel: { slug: "canary" },
|
||||
versions: {
|
||||
frontend: { version_label: "frontend-canary", commit_sha: "c0ffee" },
|
||||
api: { version_label: "api-canary", commit_sha: "def456" },
|
||||
},
|
||||
capture_policy: { enabled: true, capture_level: "full_redacted" },
|
||||
});
|
||||
|
||||
const context = buildReleaseTimelineContext();
|
||||
|
||||
expect(context.trace_id).toBe("trace-context");
|
||||
expect(context.channel_slug).toBe("canary");
|
||||
expect(context.device.type).toBe("desktop");
|
||||
expect(context.frontend.version_label).toBe("frontend-canary");
|
||||
expect(context.api.version_label).toBe("api-canary");
|
||||
expect(context.viewport.width).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user