Release v0.1.2

This commit is contained in:
2026-06-25 19:58:20 +02:00
parent 39ad3500e2
commit 23318c709a
98 changed files with 3432 additions and 2339 deletions

View File

@@ -1,14 +1,16 @@
import { useEffect, useMemo, useState } from "react";
import { ExternalLink } from "lucide-react";
import { useNavigate } from "react-router-dom";
import type { ApiSettings, CampaignListItem } from "../../types";
import { getCampaignSummary, listCampaigns, retryCampaignJobs, sendUnattemptedCampaignJobs, type CampaignSummary } from "../../api/campaigns";
import Button from "../../components/Button";
import Card from "../../components/Card";
import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import DataGrid, { type DataGridColumn } from "../../components/table/DataGrid";
import DismissibleAlert from "../../components/DismissibleAlert";
import LoadingFrame from "../../components/LoadingFrame";
import PageTitle from "../../components/PageTitle";
import StatusBadge from "../../components/StatusBadge";
import { DismissibleAlert } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { MetricCard } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import { StatusBadge } from "@govoplan/core-webui";
import { asRecord, formatDateTime, humanize } from "../campaigns/utils/campaignView";
type OperatorRow = {
@@ -92,11 +94,13 @@ export default function OperatorQueuePage({ settings }: { settings: ApiSettings
{
id: "actions",
header: "Actions",
width: 320,
width: 270,
sticky: "end",
render: (row) => (
<div className="button-row compact-actions">
<Button onClick={() => navigate(`/campaigns/${row.campaign.id}/report`)}>Open queue</Button>
<Button className="admin-icon-button" onClick={() => navigate(`/campaigns/${row.campaign.id}/report`)} aria-label={`Open queue for ${row.campaign.name}`} title={`Open queue for ${row.campaign.name}`}>
<ExternalLink />
</Button>
<Button onClick={() => void runAction(row, "retry")} disabled={row.failed <= 0 || Boolean(busy)}>Retry</Button>
<Button onClick={() => void runAction(row, "unattempted")} disabled={row.notAttempted <= 0 || Boolean(busy)}>Queue unsent</Button>
</div>
@@ -119,15 +123,16 @@ export default function OperatorQueuePage({ settings }: { settings: ApiSettings
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{message && <DismissibleAlert tone="success" resetKey={message} floating>{message}</DismissibleAlert>}
<div className="dashboard-grid settings-dashboard-grid">
<Card title="Queue pressure"><MetricGrid items={[
["Failed", totals.failed],
["Outcome unknown", totals.outcomeUnknown],
["Unattempted", totals.notAttempted],
["Queued/active", totals.queuedOrActive],
["IMAP failed", totals.imapFailed],
]} /></Card>
</div>
<section className="queue-pressure-section" aria-labelledby="operator-queue-pressure-title">
<h2 id="operator-queue-pressure-title" className="queue-pressure-heading">Queue pressure</h2>
<QueuePressureGrid items={[
{ label: "Failed", value: totals.failed, tone: "danger" },
{ label: "Outcome unknown", value: totals.outcomeUnknown, tone: "warning" },
{ label: "Unattempted", value: totals.notAttempted, tone: "info" },
{ label: "Queued/active", value: totals.queuedOrActive, tone: "neutral" },
{ label: "IMAP failed", value: totals.imapFailed, tone: "warning" },
]} />
</section>
<LoadingFrame loading={loading} label="Loading operator queue…">
<Card title="Campaign queues">
@@ -164,14 +169,11 @@ function numberValue(value: unknown): number {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
function MetricGrid({ items }: { items: Array<[string, number]> }) {
function QueuePressureGrid({ items }: { items: Array<{ label: string; value: number; tone: "neutral" | "good" | "warning" | "danger" | "info" }> }) {
return (
<div className="status-grid">
{items.map(([label, value]) => (
<div className="status-card" key={label}>
<span>{label}</span>
<strong>{value}</strong>
</div>
<div className="metric-grid queue-pressure-grid">
{items.map((item) => (
<MetricCard key={item.label} label={item.label} value={item.value} tone={item.tone} />
))}
</div>
);