arden scrape safety telemetry and polish run state UX
This commit is contained in:
parent
110e246a1c
commit
d7404abf18
33 changed files with 2074 additions and 96 deletions
|
|
@ -3,7 +3,9 @@ import { computed } from "vue";
|
|||
import { useRouter } from "vue-router";
|
||||
|
||||
import RunStatusBadge from "@/components/patterns/RunStatusBadge.vue";
|
||||
import ScrapeSafetyBadge from "@/components/patterns/ScrapeSafetyBadge.vue";
|
||||
import AppButton from "@/components/ui/AppButton.vue";
|
||||
import { formatCooldownCountdown } from "@/features/safety";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useRunStatusStore } from "@/stores/run_status";
|
||||
import { useUserSettingsStore } from "@/stores/user_settings";
|
||||
|
|
@ -31,21 +33,34 @@ const links = computed(() => {
|
|||
return userSettings.isPageVisible(item.id);
|
||||
});
|
||||
});
|
||||
const navRunStatus = computed(() => {
|
||||
const navSafetyText = computed(() => {
|
||||
if (!userSettings.manualRunAllowed) {
|
||||
return "Manual checks disabled";
|
||||
}
|
||||
if (runStatus.safetyState.cooldown_active) {
|
||||
return `Cooldown: ${formatCooldownCountdown(runStatus.safetyState.cooldown_remaining_seconds)}`;
|
||||
}
|
||||
return "Safety ready";
|
||||
});
|
||||
const navRunStateStatus = computed(() => {
|
||||
if (runStatus.isSubmitting && !runStatus.isLikelyRunning) {
|
||||
return "starting";
|
||||
}
|
||||
if (runStatus.isLikelyRunning) {
|
||||
return "running";
|
||||
}
|
||||
return "idle";
|
||||
});
|
||||
const navRunText = computed(() => {
|
||||
const label = navRunStatus.value
|
||||
.replaceAll("_", " ")
|
||||
.split(" ")
|
||||
const navRunStateLabel = computed(() =>
|
||||
navRunStateStatus.value
|
||||
.split("_")
|
||||
.filter((segment) => segment.length > 0)
|
||||
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
||||
.join(" ");
|
||||
return `State: ${label}`;
|
||||
});
|
||||
.join(" "),
|
||||
);
|
||||
const showSafetyRow = computed(
|
||||
() => runStatus.safetyState.cooldown_active || !userSettings.manualRunAllowed,
|
||||
);
|
||||
|
||||
async function onLogout(): Promise<void> {
|
||||
await auth.logout();
|
||||
|
|
@ -71,12 +86,18 @@ async function onLogout(): Promise<void> {
|
|||
</nav>
|
||||
|
||||
<div class="mt-auto grid gap-2 border-t border-stroke-subtle pt-3">
|
||||
<div
|
||||
v-if="auth.isAdmin"
|
||||
class="flex items-center justify-between gap-2 rounded-lg border border-stroke-default bg-surface-card-muted px-2.5 py-2 text-xs text-secondary"
|
||||
>
|
||||
<span class="truncate">{{ navRunText }}</span>
|
||||
<RunStatusBadge :status="navRunStatus" />
|
||||
<div class="grid gap-1 rounded-lg border border-stroke-default bg-surface-card-muted px-2.5 py-2 text-xs text-secondary">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="truncate">State: {{ navRunStateLabel }}</span>
|
||||
<RunStatusBadge :status="navRunStateStatus" />
|
||||
</div>
|
||||
<div
|
||||
v-if="showSafetyRow"
|
||||
class="flex items-center justify-between gap-2 border-t border-stroke-subtle pt-1"
|
||||
>
|
||||
<span class="truncate">{{ navSafetyText }}</span>
|
||||
<ScrapeSafetyBadge :state="runStatus.safetyState" />
|
||||
</div>
|
||||
</div>
|
||||
<AppButton variant="ghost" class="w-full justify-start" @click="onLogout">Logout</AppButton>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ const toneClass = computed(() => {
|
|||
if (props.status === "running") {
|
||||
return "border-state-info-border bg-state-info-bg text-state-info-text";
|
||||
}
|
||||
if (props.status === "starting") {
|
||||
return "border-state-info-border bg-state-info-bg text-state-info-text";
|
||||
}
|
||||
return "border-stroke-default bg-surface-card-muted text-ink-secondary";
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
27
frontend/src/components/patterns/ScrapeSafetyBadge.vue
Normal file
27
frontend/src/components/patterns/ScrapeSafetyBadge.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
import { type ScrapeSafetyState } from "@/features/safety";
|
||||
|
||||
const props = defineProps<{
|
||||
state: ScrapeSafetyState;
|
||||
}>();
|
||||
|
||||
const label = computed(() => (props.state.cooldown_active ? "Safety cooldown" : "Safety ready"));
|
||||
|
||||
const toneClass = computed(() => {
|
||||
if (!props.state.cooldown_active) {
|
||||
return "border-state-success-border bg-state-success-bg text-state-success-text";
|
||||
}
|
||||
if (props.state.cooldown_reason === "blocked_failure_threshold_exceeded") {
|
||||
return "border-state-danger-border bg-state-danger-bg text-state-danger-text";
|
||||
}
|
||||
return "border-state-warning-border bg-state-warning-bg text-state-warning-text";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold" :class="toneClass">
|
||||
{{ label }}
|
||||
</span>
|
||||
</template>
|
||||
102
frontend/src/components/patterns/ScrapeSafetyBanner.vue
Normal file
102
frontend/src/components/patterns/ScrapeSafetyBanner.vue
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
|
||||
import AppAlert from "@/components/ui/AppAlert.vue";
|
||||
import {
|
||||
formatCooldownCountdown,
|
||||
type ScrapeSafetyState,
|
||||
} from "@/features/safety";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
safetyState: ScrapeSafetyState;
|
||||
manualRunAllowed?: boolean;
|
||||
}>(),
|
||||
{
|
||||
manualRunAllowed: true,
|
||||
},
|
||||
);
|
||||
|
||||
const now = ref(Date.now());
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const cooldownUntilMs = computed(() => {
|
||||
if (!props.safetyState.cooldown_until) {
|
||||
return null;
|
||||
}
|
||||
const parsed = new Date(props.safetyState.cooldown_until).getTime();
|
||||
if (Number.isNaN(parsed)) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
});
|
||||
|
||||
const cooldownRemainingSeconds = computed(() => {
|
||||
if (!props.safetyState.cooldown_active) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (cooldownUntilMs.value !== null) {
|
||||
return Math.max(0, Math.floor((cooldownUntilMs.value - now.value) / 1000));
|
||||
}
|
||||
|
||||
return Math.max(0, props.safetyState.cooldown_remaining_seconds || 0);
|
||||
});
|
||||
|
||||
const isCooldownBlocked = computed(() => props.safetyState.cooldown_active && cooldownRemainingSeconds.value > 0);
|
||||
const isPolicyBlocked = computed(() => !props.manualRunAllowed);
|
||||
|
||||
const isVisible = computed(() => isPolicyBlocked.value || isCooldownBlocked.value);
|
||||
const tone = computed(() => {
|
||||
if (isPolicyBlocked.value) {
|
||||
return "warning" as const;
|
||||
}
|
||||
if (props.safetyState.cooldown_reason === "blocked_failure_threshold_exceeded") {
|
||||
return "danger" as const;
|
||||
}
|
||||
return "warning" as const;
|
||||
});
|
||||
|
||||
const title = computed(() => {
|
||||
if (isPolicyBlocked.value) {
|
||||
return "Manual checks disabled by server policy";
|
||||
}
|
||||
return props.safetyState.cooldown_reason_label || "Safety cooldown active";
|
||||
});
|
||||
|
||||
const detailText = computed(() => {
|
||||
if (isPolicyBlocked.value) {
|
||||
return "This server currently disallows manual checks. Automatic checks may still run if enabled by policy.";
|
||||
}
|
||||
const countdown = formatCooldownCountdown(cooldownRemainingSeconds.value);
|
||||
return `Manual and scheduled checks are paused for ${countdown}.`;
|
||||
});
|
||||
|
||||
const actionText = computed(() => {
|
||||
if (isPolicyBlocked.value) {
|
||||
return "Ask an admin to enable manual checks in server environment policy.";
|
||||
}
|
||||
return props.safetyState.recommended_action;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
timer = setInterval(() => {
|
||||
now.value = Date.now();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer !== null) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppAlert v-if="isVisible" :tone="tone">
|
||||
<template #title>{{ title }}</template>
|
||||
<p>{{ detailText }}</p>
|
||||
<p v-if="actionText" class="text-secondary">{{ actionText }}</p>
|
||||
</AppAlert>
|
||||
</template>
|
||||
Loading…
Add table
Add a link
Reference in a new issue