Stabilize customer booking and i18n checks

This commit is contained in:
Jeppe B
2026-07-14 10:34:29 +02:00
parent 6ccfea9eb4
commit 26266e724b
3 changed files with 83 additions and 34 deletions
@@ -155,9 +155,9 @@ async function deployWorkers() {
try {
await deployCronWorkers({});
await loadWorkers();
queuedMessage.value = t(
updatingDeployment ? "cron.messages.worker_update_queued" : "cron.messages.worker_deploy_queued"
);
queuedMessage.value = updatingDeployment
? t("cron.messages.worker_update_queued")
: t("cron.messages.worker_deploy_queued");
} catch (error) {
errorMessage.value = parseError(error);
} finally {
@@ -62,6 +62,20 @@ const selectedIncident = ref(null);
const incidentNote = ref("");
const incidentFilters = ref({ status: "open", search: "" });
const firewallDraft = ref(defaultFirewallDraft());
const securityRuleLabels = {
bookings_created: () => t("security.rules.bookings_created"),
failed_login_attempts: () => t("security.rules.failed_login_attempts"),
requests_per_customer: () => t("security.rules.requests_per_customer"),
requests_per_ip: () => t("security.rules.requests_per_ip"),
vehicles_created: () => t("security.rules.vehicles_created"),
};
const securityRuleDescriptions = {
bookings_created: () => t("security.rule_descriptions.bookings_created"),
failed_login_attempts: () => t("security.rule_descriptions.failed_login_attempts"),
requests_per_customer: () => t("security.rule_descriptions.requests_per_customer"),
requests_per_ip: () => t("security.rule_descriptions.requests_per_ip"),
vehicles_created: () => t("security.rule_descriptions.vehicles_created"),
};
const responseData = (response, fallback) => response?.data?.data ?? fallback;
const parseError = (error) =>
@@ -108,9 +122,7 @@ function normalizeRuleDraft(rule) {
enabled: Boolean(rule.enabled),
threshold_count: Number(rule.threshold_count || 1),
window_seconds: Number(rule.window_seconds || 60),
exemptionsText: Array.isArray(rule.exempt_permission_nodes)
? rule.exempt_permission_nodes.join(", ")
: "",
exemptionsText: Array.isArray(rule.exempt_permission_nodes) ? rule.exempt_permission_nodes.join(", ") : "",
};
}
@@ -136,15 +148,11 @@ function formatDate(value) {
}
function ruleLabel(rule) {
const key = `security.rules.${rule.rule_key}`;
const value = t(key);
return value === key ? rule.label || rule.rule_key : value;
return securityRuleLabels[rule.rule_key]?.() || rule.label || rule.rule_key;
}
function ruleDescription(rule) {
const key = `security.rule_descriptions.${rule.rule_key}`;
const value = t(key);
return value === key ? rule.description || "" : value;
return securityRuleDescriptions[rule.rule_key]?.() || rule.description || "";
}
function statusClass(status) {
@@ -468,8 +476,12 @@ onMounted(() => {
<tbody>
<tr v-for="incident in summary?.incidents?.recent || []" :key="incident.id">
<td>{{ incident.title }}</td>
<td><span class="tag" :class="statusClass(incident.status)">{{ incident.status }}</span></td>
<td><span class="tag" :class="severityClass(incident.severity)">{{ incident.severity }}</span></td>
<td>
<span class="tag" :class="statusClass(incident.status)">{{ incident.status }}</span>
</td>
<td>
<span class="tag" :class="severityClass(incident.severity)">{{ incident.severity }}</span>
</td>
<td>{{ formatDate(incident.last_seen_at) }}</td>
</tr>
</tbody>
@@ -481,13 +493,7 @@ onMounted(() => {
<section v-else-if="activePage === 'firewall'" class="security-section" data-testid="security-firewall">
<div class="security-toolbar">
<h2>{{ t("security.firewall.management") }}</h2>
<b-button
type="is-light"
icon-left="sync-alt"
icon-pack="fas"
:loading="loading"
@click="loadPage"
>
<b-button type="is-light" icon-left="sync-alt" icon-pack="fas" :loading="loading" @click="loadPage">
{{ t("security.actions.refresh") }}
</b-button>
</div>
@@ -602,7 +608,12 @@ onMounted(() => {
<tbody>
<tr v-for="rule in firewallRules" :key="rule.id">
<td>
<span class="tag" :class="rule.action === 'block' ? 'is-danger' : rule.action === 'watch' ? 'is-warning' : 'is-success'">
<span
class="tag"
:class="
rule.action === 'block' ? 'is-danger' : rule.action === 'watch' ? 'is-warning' : 'is-success'
"
>
{{ rule.action }}
</span>
<span v-if="!rule.enabled" class="tag is-light ml-1">{{ t("security.firewall.disabled") }}</span>
@@ -709,13 +720,7 @@ onMounted(() => {
<section v-else class="security-section" data-testid="security-incidents">
<div class="security-toolbar">
<h2>{{ t("security.incidents.management") }}</h2>
<b-button
type="is-light"
icon-left="sync-alt"
icon-pack="fas"
:loading="loading"
@click="loadIncidents"
>
<b-button type="is-light" icon-left="sync-alt" icon-pack="fas" :loading="loading" @click="loadIncidents">
{{ t("security.actions.refresh") }}
</b-button>
</div>
@@ -761,8 +766,12 @@ onMounted(() => {
@click="selectIncident(incident)"
>
<td>{{ incident.title }}</td>
<td><span class="tag" :class="statusClass(incident.status)">{{ incident.status }}</span></td>
<td><span class="tag" :class="severityClass(incident.severity)">{{ incident.severity }}</span></td>
<td>
<span class="tag" :class="statusClass(incident.status)">{{ incident.status }}</span>
</td>
<td>
<span class="tag" :class="severityClass(incident.severity)">{{ incident.severity }}</span>
</td>
<td>{{ incident.occurrence_count }}</td>
<td>{{ formatDate(incident.last_seen_at) }}</td>
</tr>
@@ -799,7 +808,9 @@ onMounted(() => {
</div>
<div>
<dt>{{ t("security.incidents.route") }}</dt>
<dd>{{ selectedIncident.route_path || selectedIncident.route_template || t("security.empty_value") }}</dd>
<dd>
{{ selectedIncident.route_path || selectedIncident.route_template || t("security.empty_value") }}
</dd>
</div>
</dl>
+40 -2
View File
@@ -90,6 +90,35 @@ async function clickDesktopNext(page: Page) {
throw new Error("Could not find a visible desktop next-step button in the booking flow.");
}
async function isDesktopProductSelectionVisible(page: Page) {
const productSelectionLocators = [
page.locator('[data-testid^="pos-product-card-"]:visible').first(),
page.getByTestId("pos-product-categories-loading"),
page.getByTestId("pos-products-loading"),
page.locator('[data-testid^="pos-product-category-tab-"]:visible').first(),
page.locator('[data-testid="pos-product-category-select"]:visible').first(),
];
for (const locator of productSelectionLocators) {
if (await locator.isVisible().catch(() => false)) {
return true;
}
}
return false;
}
async function waitForDesktopProductSelection(page: Page) {
return expect
.poll(async () => isDesktopProductSelectionVisible(page), {
intervals: [50, 100, 250],
timeout: 1_500,
})
.toBe(true)
.then(() => true)
.catch(() => false);
}
async function ensureVehicleInput(page: Page) {
const reg1Input = page.locator('input[id="reg1-input"]:visible').first();
@@ -139,6 +168,15 @@ async function ensureVehicleTypeSelection(page: Page) {
.toBeGreaterThan(0);
}
async function confirmVehicleRegistrationInput(page: Page, reg1Input: ReturnType<Page["locator"]>) {
if (!(await reg1Input.isVisible().catch(() => false))) {
return true;
}
await reg1Input.press("Enter");
return !(await reg1Input.isVisible().catch(() => false));
}
export async function fillBookingField(page: Page, toggleTestId: string, inputTestId: string, value: string) {
const input = page.locator(`[data-testid="${inputTestId}"]:visible`).first();
const toggle = page.locator(`[data-testid="${toggleTestId}"]:visible`).first();
@@ -188,7 +226,7 @@ export async function goToBookingProductSelectionStepWithOptions(
const reg1Input = await ensureVehicleInput(page);
await reg1Input.fill(registrationNumber);
await reg1Input.press("Enter");
const vehicleInputClosed = await confirmVehicleRegistrationInput(page, reg1Input);
if (!mobileWizard) {
await ensureVehicleTypeSelection(page);
@@ -197,7 +235,7 @@ export async function goToBookingProductSelectionStepWithOptions(
if (mobileWizard) {
await expect(page.getByTestId("booking-mobile-next")).toBeEnabled();
await page.getByTestId("booking-mobile-next").click();
} else {
} else if (!vehicleInputClosed && !(await waitForDesktopProductSelection(page))) {
await clickDesktopNext(page);
}