@kingsleyweb/bun-ui 0.0.0-stage → 3.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +80 -2
- package/app/docs/App.tsx +72 -0
- package/app/docs/api/client.ts +303 -0
- package/app/docs/api/contract.ts +24 -0
- package/app/docs/api/docs.ts +38 -0
- package/app/docs/api/errors.ts +144 -0
- package/app/docs/boot.tsx +58 -0
- package/app/docs/components/Badge.tsx +65 -0
- package/app/docs/components/Button.tsx +37 -0
- package/app/docs/components/Card.tsx +41 -0
- package/app/docs/components/ConfirmDialog.tsx +173 -0
- package/app/docs/components/CopyButton.tsx +68 -0
- package/app/docs/components/Dialog.tsx +230 -0
- package/app/docs/components/EmptyState.tsx +34 -0
- package/app/docs/components/ErrorView.tsx +70 -0
- package/app/docs/components/Field.tsx +93 -0
- package/app/docs/components/JsonEditor.tsx +151 -0
- package/app/docs/components/JsonView.tsx +338 -0
- package/app/docs/components/KeyValue.tsx +63 -0
- package/app/docs/components/ProblemBanner.tsx +116 -0
- package/app/docs/components/ScreenErrorBoundary.tsx +141 -0
- package/app/docs/components/Spinner.tsx +33 -0
- package/app/docs/components/Table.tsx +33 -0
- package/app/docs/components/Tabs.tsx +226 -0
- package/app/docs/components/classNames.ts +4 -0
- package/app/docs/components/components.css +691 -0
- package/app/docs/components/copy.ts +89 -0
- package/app/docs/components/fieldContext.ts +29 -0
- package/app/docs/components/inputValues.ts +142 -0
- package/app/docs/components/inputs.tsx +387 -0
- package/app/docs/components/jsonParse.ts +338 -0
- package/app/docs/components/listKeys.ts +30 -0
- package/app/docs/config.ts +86 -0
- package/app/docs/context.ts +33 -0
- package/app/docs/embed.ts +74 -0
- package/app/docs/env.d.ts +2 -0
- package/app/docs/format.ts +9 -0
- package/app/docs/hooks/useUrlTab.ts +16 -0
- package/app/docs/main.tsx +11 -0
- package/app/docs/queryClient.ts +54 -0
- package/app/docs/router.tsx +194 -0
- package/app/docs/routing.ts +194 -0
- package/app/docs/screens/docs/document.ts +27 -0
- package/app/docs/screens/docs/http/OperationView.tsx +407 -0
- package/app/docs/screens/docs/http/Sidebar.tsx +190 -0
- package/app/docs/screens/docs/http/TryItPanel.tsx +603 -0
- package/app/docs/screens/docs/http/http.css +405 -0
- package/app/docs/screens/docs/http/index.tsx +395 -0
- package/app/docs/screens/docs/http/model.ts +604 -0
- package/app/docs/screens/docs/http/tryIt.ts +455 -0
- package/app/docs/screens/docs/permissions.ts +6 -0
- package/app/docs/screens/docs/schema/Prose.tsx +43 -0
- package/app/docs/screens/docs/schema/SchemaTree.tsx +319 -0
- package/app/docs/screens/docs/schema/index.ts +30 -0
- package/app/docs/screens/docs/schema/resolve.ts +466 -0
- package/app/docs/screens/docs/schema/schema.css +137 -0
- package/app/docs/screens/docs/schema/skeleton.ts +92 -0
- package/app/docs/screens/docs/viewer.tsx +154 -0
- package/app/docs/screens/docs/ws/InlineText.tsx +25 -0
- package/app/docs/screens/docs/ws/PayloadSchema.tsx +39 -0
- package/app/docs/screens/docs/ws/format.ts +82 -0
- package/app/docs/screens/docs/ws/index.tsx +286 -0
- package/app/docs/screens/docs/ws/items.tsx +786 -0
- package/app/docs/screens/docs/ws/model.ts +1178 -0
- package/app/docs/screens/docs/ws/panels.tsx +440 -0
- package/app/docs/screens/docs/ws/ws.css +231 -0
- package/app/docs/shortcuts.ts +94 -0
- package/app/docs/styles/base.css +847 -0
- package/app/docs/styles/tokens.css +143 -0
- package/app/docs/theme.ts +56 -0
- package/app/kit/unauthorized.ts +222 -0
- package/dist/docs/assets/main-0rchxtv5.js +18 -0
- package/dist/docs/assets/main-0rchxtv5.js.map +99 -0
- package/dist/docs/assets/main-cqgyvs86.css +1 -0
- package/dist/docs/manifest.json +26 -0
- package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js +6 -0
- package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js.map +11 -0
- package/dist/jobs/assets/AddFlowDialog-n7gtc84v.css +1 -0
- package/dist/jobs/assets/added-cbwsxv3e.js +4 -0
- package/dist/jobs/assets/added-cbwsxv3e.js.map +11 -0
- package/dist/jobs/assets/bodies-yyw5hmrp.js +6 -0
- package/dist/jobs/assets/bodies-yyw5hmrp.js.map +10 -0
- package/dist/jobs/assets/channel-tgkvqm8y.js +4 -0
- package/dist/jobs/assets/channel-tgkvqm8y.js.map +10 -0
- package/dist/jobs/assets/channelChecks-s4yz4yhw.js +4 -0
- package/dist/jobs/assets/channelChecks-s4yz4yhw.js.map +10 -0
- package/dist/jobs/assets/index-02427smp.css +1 -0
- package/dist/jobs/assets/index-09z2cjft.js +4 -0
- package/dist/jobs/assets/index-09z2cjft.js.map +16 -0
- package/dist/jobs/assets/index-18bn0gvy.css +1 -0
- package/dist/jobs/assets/index-47pxmyxn.js +4 -0
- package/dist/jobs/assets/index-47pxmyxn.js.map +10 -0
- package/dist/jobs/assets/index-4e55z986.js +4 -0
- package/dist/jobs/assets/index-4e55z986.js.map +11 -0
- package/dist/jobs/assets/index-5c933424.js +4 -0
- package/dist/jobs/assets/index-5c933424.js.map +18 -0
- package/dist/jobs/assets/index-5kbyqspm.css +1 -0
- package/dist/jobs/assets/index-5ng37hk6.css +1 -0
- package/dist/jobs/assets/index-5w8w7fvc.css +1 -0
- package/dist/jobs/assets/index-71qs5qyx.css +1 -0
- package/dist/jobs/assets/index-btpg4r8b.css +1 -0
- package/dist/jobs/assets/index-d5r5n0mm.js +4 -0
- package/dist/jobs/assets/index-d5r5n0mm.js.map +23 -0
- package/dist/jobs/assets/index-e0yy2skq.css +1 -0
- package/dist/jobs/assets/index-hs60b25v.js +10 -0
- package/dist/jobs/assets/index-hs60b25v.js.map +62 -0
- package/dist/jobs/assets/index-k2smrr70.js +4 -0
- package/dist/jobs/assets/index-k2smrr70.js.map +9 -0
- package/dist/jobs/assets/index-q24jw3w9.js +4 -0
- package/dist/jobs/assets/index-q24jw3w9.js.map +32 -0
- package/dist/jobs/assets/index-qfb80xen.css +1 -0
- package/dist/jobs/assets/index-rtynx6wj.js +4 -0
- package/dist/jobs/assets/index-rtynx6wj.js.map +10 -0
- package/dist/jobs/assets/live-6k7s6358.js +4 -0
- package/dist/jobs/assets/live-6k7s6358.js.map +10 -0
- package/dist/jobs/assets/main-0jmeh6mq.js +6 -0
- package/dist/jobs/assets/main-0jmeh6mq.js.map +12 -0
- package/dist/jobs/assets/main-3yj6bv90.js +5 -0
- package/dist/jobs/assets/main-3yj6bv90.js.map +10 -0
- package/dist/jobs/assets/main-4g69fn3x.js +5 -0
- package/dist/jobs/assets/main-4g69fn3x.js.map +10 -0
- package/dist/jobs/assets/main-4gte3eqq.js +5 -0
- package/dist/jobs/assets/main-4gte3eqq.js.map +10 -0
- package/dist/jobs/assets/main-4j1fcevh.js +5 -0
- package/dist/jobs/assets/main-4j1fcevh.js.map +10 -0
- package/dist/jobs/assets/main-54qwkz28.css +1 -0
- package/dist/jobs/assets/main-5grjvsy7.js +5 -0
- package/dist/jobs/assets/main-5grjvsy7.js.map +11 -0
- package/dist/jobs/assets/main-5zbx60gq.js +5 -0
- package/dist/jobs/assets/main-5zbx60gq.js.map +11 -0
- package/dist/jobs/assets/main-9prgjhg4.js +5 -0
- package/dist/jobs/assets/main-9prgjhg4.js.map +10 -0
- package/dist/jobs/assets/main-a250ffk7.js +17 -0
- package/dist/jobs/assets/main-a250ffk7.js.map +115 -0
- package/dist/jobs/assets/main-d6ge7w08.js +5 -0
- package/dist/jobs/assets/main-d6ge7w08.js.map +10 -0
- package/dist/jobs/assets/main-gngzht43.js +5 -0
- package/dist/jobs/assets/main-gngzht43.js.map +10 -0
- package/dist/jobs/assets/main-hg0hrg8j.js +5 -0
- package/dist/jobs/assets/main-hg0hrg8j.js.map +10 -0
- package/dist/jobs/assets/main-hm1ng3yg.js +5 -0
- package/dist/jobs/assets/main-hm1ng3yg.js.map +12 -0
- package/dist/jobs/assets/main-jkvd92z9.js +5 -0
- package/dist/jobs/assets/main-jkvd92z9.js.map +10 -0
- package/dist/jobs/assets/main-jv5xqd13.js +5 -0
- package/dist/jobs/assets/main-jv5xqd13.js.map +21 -0
- package/dist/jobs/assets/main-jxre7w93.js +5 -0
- package/dist/jobs/assets/main-jxre7w93.js.map +23 -0
- package/dist/jobs/assets/main-kwmqkjfx.js +5 -0
- package/dist/jobs/assets/main-kwmqkjfx.js.map +11 -0
- package/dist/jobs/assets/main-n9cf6bxm.js +5 -0
- package/dist/jobs/assets/main-n9cf6bxm.js.map +11 -0
- package/dist/jobs/assets/main-pegp0ygb.js +5 -0
- package/dist/jobs/assets/main-pegp0ygb.js.map +10 -0
- package/dist/jobs/assets/main-qr573853.js +5 -0
- package/dist/jobs/assets/main-qr573853.js.map +12 -0
- package/dist/jobs/assets/main-rq97rdr4.js +4 -0
- package/dist/jobs/assets/main-rq97rdr4.js.map +22 -0
- package/dist/jobs/assets/main-szjdmydc.js +5 -0
- package/dist/jobs/assets/main-szjdmydc.js.map +13 -0
- package/dist/jobs/assets/main-tnbhrm9n.js +7 -0
- package/dist/jobs/assets/main-tnbhrm9n.js.map +11 -0
- package/dist/jobs/assets/main-ts19j4n0.js +5 -0
- package/dist/jobs/assets/main-ts19j4n0.js.map +25 -0
- package/dist/jobs/assets/main-w93x1dzs.js +5 -0
- package/dist/jobs/assets/main-w93x1dzs.js.map +10 -0
- package/dist/jobs/assets/sections-0ksdxs8m.css +1 -0
- package/dist/jobs/assets/sections-0y7w4fb4.js +4 -0
- package/dist/jobs/assets/sections-0y7w4fb4.js.map +10 -0
- package/dist/jobs/assets/sse-mn0t0fh7.js +5 -0
- package/dist/jobs/assets/sse-mn0t0fh7.js.map +11 -0
- package/dist/jobs/assets/sseChecks-t7tcm7ra.js +4 -0
- package/dist/jobs/assets/sseChecks-t7tcm7ra.js.map +10 -0
- package/dist/jobs/assets/validation-ysjfy072.js +4 -0
- package/dist/jobs/assets/validation-ysjfy072.js.map +9 -0
- package/dist/jobs/manifest.json +511 -0
- package/dts/docs/assets.d.ts +144 -0
- package/dts/docs/assets.d.ts.map +1 -0
- package/dts/docs/browser/config.d.ts +61 -0
- package/dts/docs/browser/config.d.ts.map +1 -0
- package/dts/docs/browser/index.d.ts +12 -0
- package/dts/docs/browser/index.d.ts.map +1 -0
- package/dts/docs/docsUi.d.ts +42 -0
- package/dts/docs/docsUi.d.ts.map +1 -0
- package/dts/docs/hosted.d.ts +22 -0
- package/dts/docs/hosted.d.ts.map +1 -0
- package/dts/docs/index.d.ts +18 -0
- package/dts/docs/index.d.ts.map +1 -0
- package/dts/docs/shell.d.ts +57 -0
- package/dts/docs/shell.d.ts.map +1 -0
- package/dts/docs/types.d.ts +137 -0
- package/dts/docs/types.d.ts.map +1 -0
- package/dts/index.d.ts +21 -0
- package/dts/index.d.ts.map +1 -0
- package/dts/jobs/assets.d.ts +157 -0
- package/dts/jobs/assets.d.ts.map +1 -0
- package/dts/jobs/browser/config.d.ts +67 -0
- package/dts/jobs/browser/config.d.ts.map +1 -0
- package/dts/jobs/browser/index.d.ts +10 -0
- package/dts/jobs/browser/index.d.ts.map +1 -0
- package/dts/jobs/browser/unauthorized.d.ts +9 -0
- package/dts/jobs/browser/unauthorized.d.ts.map +1 -0
- package/dts/jobs/index.d.ts +24 -0
- package/dts/jobs/index.d.ts.map +1 -0
- package/dts/jobs/jobsUi.d.ts +63 -0
- package/dts/jobs/jobsUi.d.ts.map +1 -0
- package/dts/jobs/shell.d.ts +77 -0
- package/dts/jobs/shell.d.ts.map +1 -0
- package/dts/jobs/types.d.ts +146 -0
- package/dts/jobs/types.d.ts.map +1 -0
- package/dts/kit/browser/shared.d.ts +10 -0
- package/dts/kit/browser/shared.d.ts.map +1 -0
- package/dts/kit/browser/unauthorized.d.ts +74 -0
- package/dts/kit/browser/unauthorized.d.ts.map +1 -0
- package/dts/kit/unauthorized.d.ts +47 -0
- package/dts/kit/unauthorized.d.ts.map +1 -0
- package/guides/docs.md +170 -0
- package/guides/jobs.md +1120 -0
- package/lib/docs/assets.ts +427 -0
- package/lib/docs/browser/config.ts +66 -0
- package/lib/docs/browser/index.ts +24 -0
- package/lib/docs/docsUi.ts +623 -0
- package/lib/docs/hosted.ts +31 -0
- package/lib/docs/index.ts +37 -0
- package/lib/docs/shell.ts +156 -0
- package/lib/docs/types.ts +149 -0
- package/lib/index.ts +27 -0
- package/lib/jobs/assets.ts +448 -0
- package/lib/jobs/browser/config.ts +65 -0
- package/lib/jobs/browser/index.ts +9 -0
- package/lib/jobs/browser/unauthorized.ts +11 -0
- package/lib/jobs/index.ts +41 -0
- package/lib/jobs/jobsUi.ts +709 -0
- package/lib/jobs/shell.ts +205 -0
- package/lib/jobs/types.ts +162 -0
- package/lib/kit/browser/shared.ts +11 -0
- package/lib/kit/browser/unauthorized.ts +114 -0
- package/lib/kit/unauthorized.ts +222 -0
- package/package.json +198 -4
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["app/jobs/screens/queues/panels/DemandPanel.tsx", "app/jobs/screens/queues/panels/jobDefaultsGate.ts", "app/jobs/screens/queues/panels/limitsDraft.ts", "app/jobs/screens/queues/panels/LimitsPanel.tsx", "app/jobs/screens/queues/panels/RepeatablesPanel.tsx", "app/jobs/screens/queues/panels/summonPolicyText.ts", "app/jobs/screens/queues/panels/SummonPanel.tsx", "app/jobs/screens/queues/panels/throughput.ts", "app/jobs/screens/queues/panels/ThroughputPanel.tsx", "app/jobs/screens/workers/sweeps.ts", "app/jobs/screens/queues/panels/WorkersPanel.tsx", "app/jobs/screens/queues/QueueActions.tsx", "app/jobs/screens/queues/QueueScreen.tsx", "app/jobs/screens/queues/QueuesListScreen.tsx"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type { QueueDemandDto } from \"../../../api/types\";\nimport { keepPreviousData, useQuery } from \"@tanstack/react-query\";\nimport { demandKeys, demandUrls, getQueueDemand } from \"../../../api/demand\";\nimport { CopyButton } from \"../../../components/CopyButton\";\nimport { KeyValue } from \"../../../components/KeyValue\";\nimport { ProblemBanner } from \"../../../components/ProblemBanner\";\nimport { RelativeTime } from \"../../../components/RelativeTime\";\nimport { Spinner } from \"../../../components/Spinner\";\nimport { useApiClient } from \"../../../context\";\nimport { formatNumber } from \"../../../format\";\nimport { useRefreshInterval } from \"../live\";\nimport {\n DEMAND_FIGURES,\n demandFigureHint,\n formatDemandFigure,\n qualify,\n} from \"./demand\";\n\n/** Props of {@link DemandPanel}. */\nexport interface DemandPanelProps {\n /** The queue. */\n queue: string;\n}\n\n/**\n * `GET /queues/:queue/demand`: the figures a scaler polls to decide how many\n * workers to run, as this API answers them now, and the URLs it would poll.\n *\n * Every figure is written as the answer qualifies it (see `./demand`): \"≥\"\n * when a count reached the cap, \"≈\" when the backend's fallback produced it.\n * A paused queue reads demand and outstanding as 0 over its unchanged\n * backlog, which a note says, so a 0 is not mistaken for an empty queue.\n */\nexport function DemandPanel({ queue }: DemandPanelProps) {\n const api = useApiClient();\n const refetchInterval = useRefreshInterval(\"demand\");\n const demand = useQuery({\n queryKey: demandKeys.queue(queue),\n queryFn: ({ signal }) => getQueueDemand(api, queue, signal),\n refetchInterval,\n placeholderData: keepPreviousData,\n });\n if (demand.isPending) {\n return (\n <Spinner\n label=\"Loading demand\"\n showLabel\n />\n );\n }\n if (demand.isError) {\n return (\n <ProblemBanner\n error={demand.error}\n title=\"Could not load demand\"\n onRetry={() => void demand.refetch()}\n />\n );\n }\n return (\n <DemandReading\n reading={demand.data}\n urls={demandUrls(api.base, queue, window.location.origin)}\n />\n );\n}\n\n/** Props of {@link DemandReading}. */\ninterface DemandReadingProps {\n /** The answer. */\n reading: QueueDemandDto;\n /** Where a scaler reads it. */\n urls: { json: string; prometheus: string };\n}\n\n/** One demand answer, its qualifications, and the scaler URLs. */\nfunction DemandReading({ reading, urls }: DemandReadingProps) {\n const approximate = DEMAND_FIGURES.some(\n ({ figure }) => qualify(reading, figure).approximate,\n );\n return (\n <div\n className=\"demand-panel\"\n data-testid=\"queue-demand\"\n >\n {reading.paused && (\n <p\n className=\"notice\"\n role=\"note\"\n data-testid=\"demand-paused\"\n >\n The queue is paused, so it demands nothing: demand and outstanding\n read 0 whatever is waiting, and a scaler polling them scales to zero.\n </p>\n )}\n {reading.capped && (\n <p\n className=\"notice\"\n role=\"note\"\n data-testid=\"demand-capped\"\n >\n At least these many: a count reached the backend's cap, so every\n figure is a lower bound (≥).\n </p>\n )}\n {approximate && (\n <p\n className=\"notice\"\n role=\"note\"\n data-testid=\"demand-approximate\"\n >\n Approximate (≈): this backend cannot count demand directly, so due now\n is only 1 or 0, stalled counts every active job while no worker is\n live, and the next due time is known only while nothing is due. Right\n as a trigger, not as a count.\n </p>\n )}\n <KeyValue\n items={[\n ...DEMAND_FIGURES.map(({ figure, label, hint }) => ({\n key: figure,\n label,\n value: (\n <span\n className=\"num\"\n title={demandFigureHint(reading, figure)}\n data-testid={`demand-${figure}`}\n >\n {formatDemandFigure(reading, figure)}\n </span>\n ),\n hint,\n })),\n {\n key: \"workers\",\n label: \"Workers\",\n value: (\n <span\n className=\"num\"\n data-testid=\"demand-workers\"\n >\n {formatNumber(reading.workers)}\n </span>\n ),\n hint: \"Live workers on the queue, from their heartbeats, paused ones included. 0 on a backend that keeps no worker records.\",\n },\n {\n key: \"nextDueAt\",\n label: \"Next due\",\n value: (\n <span data-testid=\"demand-next-due\">\n {reading.nextDueAt === null ? (\n \"None scheduled\"\n ) : (\n <RelativeTime\n value={reading.nextDueAt}\n hint={\n reading.exact\n ? undefined\n : \"Approximate: known only while nothing is due.\"\n }\n />\n )}\n </span>\n ),\n hint: \"The earliest delayed job or retry still in the future.\",\n },\n {\n key: \"at\",\n label: \"Read\",\n value: <RelativeTime value={reading.at} />,\n },\n ]}\n />\n <div className=\"demand-urls\">\n <p className=\"muted\">\n A scaler reads the same figures from these URLs (JSON, and the\n Prometheus text format), with the credentials this API asks for.\n </p>\n <DemandUrl\n label=\"JSON\"\n url={urls.json}\n testId=\"demand-url-json\"\n />\n <DemandUrl\n label=\"Prometheus\"\n url={urls.prometheus}\n testId=\"demand-url-prometheus\"\n />\n </div>\n </div>\n );\n}\n\n/** One scaler URL with a copy button. */\nfunction DemandUrl({\n label,\n url,\n testId,\n}: {\n /** What format it serves. */\n label: string;\n /** The absolute URL. */\n url: string;\n /** Its test id. */\n testId: string;\n}) {\n return (\n <div className=\"demand-url\">\n <span className=\"demand-url-label\">{label}</span>\n <code data-testid={testId}>{url}</code>\n <CopyButton\n text={url}\n ariaLabel={`Copy the ${label} demand URL`}\n />\n </div>\n );\n}\n",
|
|
6
|
+
"import { useCan, useMeta } from \"../../../meta/hooks\";\nimport { useCanMutate } from \"../gating\";\n\n/** What the queue screen needs to know about the Job defaults panel. */\nexport interface JobDefaultsGate {\n /** Whether the panel is offered at all. */\n shown: boolean;\n /** Whether its Settings… editor is offered: mutation `queues.defaults`. */\n canEdit: boolean;\n /** Whether its Apply to pending jobs… action is offered: mutation `queues.applyDefaults`, separate from editing. */\n canApply: boolean;\n}\n\n/**\n * Whether the queue's job defaults can be shown, edited and applied.\n *\n * `features.jobDefaults` says the API serves the job-defaults routes (every\n * built-in backend; false in `runner` mode), and `features.jobDefaultsApply`\n * the apply route. Reading needs `queues.read`, as the limits do; editing\n * (`queues.defaults`) and applying (`queues.applyDefaults`) are separate\n * opt-in mutations, so someone may edit without being allowed to rewrite the\n * backlog. Where the flag is off nothing is requested.\n */\nexport function useJobDefaultsGate(): JobDefaultsGate {\n const meta = useMeta();\n const canRead = useCan(\"queues.read\");\n const canMutate = useCanMutate();\n const shown = meta.features.jobDefaults && canRead;\n return {\n shown,\n canEdit: shown && canMutate(\"queues.defaults\"),\n canApply:\n shown &&\n meta.features.jobDefaultsApply &&\n canMutate(\"queues.applyDefaults\"),\n };\n}\n",
|
|
7
|
+
"import type {\n QueueLimitsBody,\n QueueLimitsDto,\n RateInput,\n} from \"../../../api/types\";\nimport { parseWindow } from \"../duration\";\n\n/** One rate + concurrency row of the editor, as typed. */\nexport interface LimitsDraftRow {\n /** Most jobs per window. */\n rateMax: number | undefined;\n /** The window: digits are ms, anything else a phrase such as \"1 minute\". */\n rateDuration: string;\n /** Jobs at once. */\n concurrency: number | undefined;\n}\n\n/** A per-name row of the editor. */\nexport interface LimitsDraftName extends LimitsDraftRow {\n /** Stable key for React; not sent. */\n key: number;\n /** The job name. */\n name: string;\n}\n\n/** The editor's state. */\nexport interface LimitsDraft {\n /** Queue-wide limits. */\n queue: LimitsDraftRow;\n /** Per-name overrides. */\n names: LimitsDraftName[];\n}\n\n/** An editor row from stored limits. */\nfunction rowFrom(\n limits:\n | {\n rate?: { max: number; duration: number };\n concurrency?: number;\n }\n | undefined,\n): LimitsDraftRow {\n return {\n rateMax: limits?.rate?.max,\n rateDuration: limits?.rate ? String(limits.rate.duration) : \"\",\n concurrency: limits?.concurrency,\n };\n}\n\n/** The editor's state from stored limits (`null` = none). */\nexport function draftFromLimits(limits: QueueLimitsDto | null): LimitsDraft {\n return {\n queue: rowFrom(limits ?? undefined),\n names: Object.entries(limits?.names ?? {}).map(([name, row], index) => ({\n key: index,\n name,\n ...rowFrom(row),\n })),\n };\n}\n\n/** One row as the body takes it; a rate needs both parts, so half a rate is sent as typed for the API to refuse. */\nfunction rowBody(row: LimitsDraftRow): {\n rate?: RateInput;\n concurrency?: number;\n} {\n const out: { rate?: RateInput; concurrency?: number } = {};\n const duration = parseWindow(row.rateDuration);\n if (row.rateMax !== undefined || duration !== undefined) {\n out.rate = {\n max: row.rateMax ?? 0,\n duration: duration ?? 0,\n };\n }\n if (row.concurrency !== undefined) {\n out.concurrency = row.concurrency;\n }\n return out;\n}\n\n/** The `PUT` body from the editor's state; rows with no name are dropped. */\nexport function limitsBodyFromDraft(draft: LimitsDraft): QueueLimitsBody {\n const body: QueueLimitsBody = rowBody(draft.queue);\n const names: NonNullable<QueueLimitsBody[\"names\"]> = {};\n for (const row of draft.names) {\n const name = row.name.trim();\n if (name) {\n names[name] = rowBody(row);\n }\n }\n if (Object.keys(names).length > 0) {\n body.names = names;\n }\n return body;\n}\n",
|
|
8
|
+
"import type { FormEvent } from \"react\";\nimport type { QueueLimitsBody, QueueLimitsDto } from \"../../../api/types\";\nimport type { LimitsDraftRow } from \"./limitsDraft\";\nimport { useState } from \"react\";\nimport { isApiError } from \"../../../api/errors\";\nimport {\n mutationInvalidations,\n removeQueueLimits,\n setQueueLimits,\n} from \"../../../api/queues\";\nimport { Button } from \"../../../components/Button\";\nimport { ConfirmDialog } from \"../../../components/ConfirmDialog\";\nimport { Field } from \"../../../components/Field\";\nimport { NumberInput, TextInput } from \"../../../components/inputs\";\nimport { ProblemBanner } from \"../../../components/ProblemBanner\";\nimport { useApiClient } from \"../../../context\";\nimport { useApiMutation } from \"../../../hooks/useApiMutation\";\nimport { formatMs } from \"../duration\";\nimport { draftFromLimits, limitsBodyFromDraft } from \"./limitsDraft\";\n\n/** Props of {@link LimitsPanel}. */\nexport interface LimitsPanelProps {\n /** The queue. */\n queue: string;\n /** Its stored limits (`null` for none). */\n limits: QueueLimitsDto | null;\n /** Whether the caller may change them (`queues.limits`, not read-only). */\n editable: boolean;\n}\n\n/** The queue's limits: a read-only summary, or an editor when permitted. */\nexport function LimitsPanel({ queue, limits, editable }: LimitsPanelProps) {\n if (!editable) {\n return <LimitsSummary limits={limits} />;\n }\n // Remounts when the stored limits change, so the form starts from them.\n return (\n <LimitsEditor\n key={JSON.stringify(limits)}\n queue={queue}\n limits={limits}\n />\n );\n}\n\n/** The stored limits as text. */\nfunction LimitsSummary({ limits }: { limits: QueueLimitsDto | null }) {\n if (!limits || Object.keys(limits).length === 0) {\n return <p className=\"muted\">No limits are set on this queue.</p>;\n }\n const describe = (row: {\n rate?: { max: number; duration: number };\n concurrency?: number;\n }) =>\n [\n row.rate && `${row.rate.max} per ${formatMs(row.rate.duration)}`,\n row.concurrency !== undefined && `${row.concurrency} at once`,\n ]\n .filter(Boolean)\n .join(\", \") || \"none\";\n return (\n <dl className=\"limits-summary\">\n <dt>Queue</dt>\n <dd>{describe(limits)}</dd>\n {Object.entries(limits.names ?? {}).map(([name, row]) => (\n <div key={name}>\n <dt>{name}</dt>\n <dd>{describe(row)}</dd>\n </div>\n ))}\n </dl>\n );\n}\n\n/** Props of {@link LimitsRowFields}. */\ninterface LimitsRowFieldsProps {\n /** The row. */\n row: LimitsDraftRow;\n /** Replaces the row. */\n onChange: (row: LimitsDraftRow) => void;\n /** The field-error key prefix, e.g. `\"\"` or `\"names.email.\"`. */\n prefix: string;\n /** Field errors from the last save. */\n errors: Record<string, string>;\n /** Label prefix, e.g. \"Queue\" or the job name. */\n label: string;\n /**\n * What the row limits, as the hints put it: `\"queue\"` for the queue-wide\n * row, or the job name a per-name row applies to (`\"\"` while it is unnamed).\n */\n scope: { kind: \"queue\" } | { kind: \"name\"; name: string };\n}\n\n/**\n * Rate max, rate window and concurrency inputs for one row, each with a hint\n * saying what it limits. The limits are the QUEUE's, enforced across every\n * worker in every process — not a worker's own settings — so the hints say\n * that, and that an empty field means no limit.\n */\nfunction LimitsRowFields({\n row,\n onChange,\n prefix,\n errors,\n label,\n scope,\n}: LimitsRowFieldsProps) {\n const error = (key: string) => errors[`${prefix}${key}`];\n const which =\n scope.kind === \"queue\"\n ? \"jobs on this queue\"\n : scope.name\n ? `jobs named “${scope.name}”`\n : \"jobs with this name\";\n return (\n <div className=\"field-row limits-row\">\n <Field\n label={`${label}: rate max`}\n hint={`The most ${which} that may START in one rate window, across every worker. Set it with the window beside it; leave both empty for no rate limit.`}\n error={error(\"rate.max\") ?? error(\"rate\")}\n >\n <NumberInput\n value={row.rateMax}\n onChange={(rateMax) => onChange({ ...row, rateMax })}\n min={1}\n />\n </Field>\n <Field\n label={`${label}: rate window`}\n hint=\"The length of the rate window: milliseconds (60000), or a duration such as “1 minute” or “30 seconds”. Starts are counted over this window.\"\n error={error(\"rate.duration\")}\n >\n <TextInput\n value={row.rateDuration}\n onChange={(rateDuration) => onChange({ ...row, rateDuration })}\n maxLength={100}\n />\n </Field>\n <Field\n label={`${label}: concurrency`}\n hint={`The most ${which} that may RUN at once, counted across every worker — on top of each worker's own concurrency, which still applies. A worker that dies holds its share until its lock expires. Empty means no limit.`}\n error={error(\"concurrency\")}\n >\n <NumberInput\n value={row.concurrency}\n onChange={(concurrency) => onChange({ ...row, concurrency })}\n min={1}\n />\n </Field>\n </div>\n );\n}\n\n/** The editor: `PUT` the form, or `PUT null` to remove every limit. */\nfunction LimitsEditor({\n queue,\n limits,\n}: {\n queue: string;\n limits: QueueLimitsDto | null;\n}) {\n const api = useApiClient();\n const [draft, setDraft] = useState(() => draftFromLimits(limits));\n const [confirmRemove, setConfirmRemove] = useState(false);\n const save = useApiMutation({\n mutationFn: (body: QueueLimitsBody | null) =>\n body === null\n ? removeQueueLimits(api, queue)\n : setQueueLimits(api, queue, body),\n successMessage: (_result, body) =>\n body === null\n ? `Removed the limits of ${queue}`\n : `Saved the limits of ${queue}`,\n invalidate: mutationInvalidations(queue),\n toastErrors: false,\n });\n const submit = (event?: FormEvent<HTMLFormElement>) => {\n event?.preventDefault();\n save.mutate(limitsBodyFromDraft(draft));\n };\n const contended =\n isApiError(save.error) && save.error.code === \"LIMITS_CONTENDED\";\n const bannerError =\n save.error &&\n (Object.keys(save.fieldErrors).length === 0 ||\n save.unplacedIssues.length > 0)\n ? save.error\n : null;\n const nextKey = draft.names.reduce(\n (max, row) => Math.max(max, row.key + 1),\n 0,\n );\n\n return (\n <>\n <form\n className=\"limits-editor\"\n aria-label=\"Queue limits\"\n onSubmit={submit}\n >\n <LimitsRowFields\n row={draft.queue}\n onChange={(row) => setDraft({ ...draft, queue: row })}\n prefix=\"\"\n errors={save.fieldErrors}\n label=\"Queue\"\n scope={{ kind: \"queue\" }}\n />\n <fieldset className=\"limits-names\">\n <legend>Per-name limits</legend>\n {draft.names.length === 0 && <p className=\"muted\">None.</p>}\n {draft.names.map((row, index) => (\n <div\n key={row.key}\n className=\"limits-name\"\n >\n <Field\n label={`Name ${index + 1}`}\n hint=\"The job name these limits apply to, exactly as jobs are added with it (e.g. “send-email”). They apply on top of the queue's own. A name at its limit is skipped, not waited behind, so other names keep running.\"\n >\n <TextInput\n value={row.name}\n onChange={(name) =>\n setDraft({\n ...draft,\n names: draft.names.map((item) =>\n item.key === row.key ? { ...item, name } : item,\n ),\n })\n }\n maxLength={200}\n />\n </Field>\n <LimitsRowFields\n row={row}\n onChange={(next) =>\n setDraft({\n ...draft,\n names: draft.names.map((item) =>\n item.key === row.key ? { ...item, ...next } : item,\n ),\n })\n }\n prefix={`names.${row.name.trim()}.`}\n errors={save.fieldErrors}\n label={row.name.trim() || `Name ${index + 1}`}\n scope={{ kind: \"name\", name: row.name.trim() }}\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() =>\n setDraft({\n ...draft,\n names: draft.names.filter((item) => item.key !== row.key),\n })\n }\n >\n Remove {row.name.trim() || `name ${index + 1}`}\n </Button>\n </div>\n ))}\n <Button\n size=\"sm\"\n onClick={() =>\n setDraft({\n ...draft,\n names: [\n ...draft.names,\n {\n key: nextKey,\n name: \"\",\n rateMax: undefined,\n rateDuration: \"\",\n concurrency: undefined,\n },\n ],\n })\n }\n >\n Add a name\n </Button>\n </fieldset>\n {bannerError !== null && (\n <ProblemBanner\n error={bannerError}\n title={\n contended\n ? \"The limits changed while saving. Retry to save again.\"\n : \"Could not save the limits\"\n }\n onRetry={contended ? () => submit() : undefined}\n />\n )}\n <div className=\"form-actions\">\n <Button\n type=\"submit\"\n variant=\"primary\"\n disabled={save.isPending}\n aria-busy={save.isPending || undefined}\n >\n {save.isPending ? \"Saving…\" : \"Save limits\"}\n </Button>\n {limits !== null && (\n <Button\n variant=\"danger\"\n onClick={() => setConfirmRemove(true)}\n disabled={save.isPending}\n >\n Remove all limits…\n </Button>\n )}\n </div>\n </form>\n <ConfirmDialog\n open={confirmRemove}\n onClose={() => setConfirmRemove(false)}\n title={`Remove every limit on ${queue}?`}\n description=\"Workers stop rate-limiting and capping this queue once they refresh their limits.\"\n variant=\"danger\"\n confirmLabel=\"Remove limits\"\n onConfirm={() => save.mutateAsync(null)}\n />\n </>\n );\n}\n",
|
|
9
|
+
"import type { RepeatableDto } from \"../../../api/types\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useState } from \"react\";\nimport { ApiError, isApiError } from \"../../../api/errors\";\nimport {\n disableRepeatable,\n enableRepeatable,\n listRepeatables,\n mutationInvalidations,\n queueKeys,\n removeRepeatable,\n} from \"../../../api/queues\";\nimport { Badge } from \"../../../components/Badge\";\nimport { Button } from \"../../../components/Button\";\nimport { ConfirmDialog } from \"../../../components/ConfirmDialog\";\nimport { EmptyState } from \"../../../components/EmptyState\";\nimport { JsonView } from \"../../../components/JsonView\";\nimport { Pager } from \"../../../components/Pager\";\nimport { ProblemBanner } from \"../../../components/ProblemBanner\";\nimport { RelativeTime } from \"../../../components/RelativeTime\";\nimport { Spinner } from \"../../../components/Spinner\";\nimport { Table } from \"../../../components/Table\";\nimport { useClientPage } from \"../../../components/useClientPage\";\nimport { useApiClient } from \"../../../context\";\nimport { formatNumber } from \"../../../format\";\nimport { useApiMutation } from \"../../../hooks/useApiMutation\";\nimport { useRefreshInterval } from \"../live\";\nimport { describeSchedule } from \"../queueFormat\";\n\n/**\n * Repeat series per page.\n *\n * Twenty: a series' row carries a schedule, a next run, its counts and a JSON\n * tree of its data, so it is a tall row inside a detail panel of the queue\n * screen — fewer than a bare list would take. A queue seldom has more, so the\n * panel usually shows no pager at all.\n */\nconst REPEATABLE_PAGE_SIZE = 20;\n\n/** Props of {@link RepeatablesPanel}. */\nexport interface RepeatablesPanelProps {\n /** The queue whose repeat series are listed. */\n queue: string;\n /** Whether the caller may remove a series (`repeatables.remove`, not read-only). */\n canRemove: boolean;\n /** Whether the caller may disable a series (`repeatables.disable`, not read-only); offered on enabled ones. Defaults to `false`. */\n canDisable?: boolean;\n /** Whether the caller may enable a series (`repeatables.enable`, not read-only); offered on disabled ones. Defaults to `false`. */\n canEnable?: boolean;\n}\n\n/** A disable or enable of one series. */\nexport type RepeatableToggle = \"disable\" | \"enable\";\n\n/**\n * A failed disable or enable, with a 404 `REPEATABLE_NOT_FOUND` explained in\n * plain words. Anything else is returned unchanged.\n */\nfunction explainToggleError(error: unknown, key: string): unknown {\n if (!isApiError(error) || error.code !== \"REPEATABLE_NOT_FOUND\") {\n return error;\n }\n return new ApiError({\n kind: error.kind,\n status: error.status,\n code: error.code,\n title: error.title,\n detail: `The repeat series “${key}” no longer exists: somebody removed it, or it ended. Refresh to see the series there are now.`,\n type: error.type,\n issues: error.issues,\n context: error.context,\n instance: error.instance,\n cause: error,\n });\n}\n\n/** `GET /queues/:queue/repeatables?include=data`, with remove, disable and enable. */\nexport function RepeatablesPanel({\n queue,\n canRemove,\n canDisable = false,\n canEnable = false,\n}: RepeatablesPanelProps) {\n const api = useApiClient();\n const [removing, setRemoving] = useState<RepeatableDto | null>(null);\n const refetchInterval = useRefreshInterval(\"repeatables\");\n const repeatables = useQuery({\n queryKey: queueKeys.repeatables(queue),\n queryFn: ({ signal }) => listRepeatables(api, queue, signal),\n refetchInterval,\n });\n const remove = useApiMutation({\n mutationFn: (key: string) => removeRepeatable(api, queue, key),\n successMessage: (_result, key) => `Removed the repeat series ${key}`,\n invalidate: mutationInvalidations(queue),\n toastErrors: false,\n });\n const toggle = useApiMutation({\n mutationFn: async ({\n key,\n to,\n }: {\n key: string;\n to: RepeatableToggle;\n }): Promise<unknown> => {\n try {\n return to === \"disable\"\n ? await disableRepeatable(api, queue, key)\n : await enableRepeatable(api, queue, key);\n } catch (error) {\n throw explainToggleError(error, key);\n }\n },\n successMessage: (_result, { key, to }) =>\n to === \"disable\"\n ? `Disabled the repeat series ${key}: it schedules nothing until enabled`\n : `Enabled the repeat series ${key}: its next occurrence is scheduled from now`,\n errorTitle: \"Could not change the repeat series\",\n invalidate: mutationInvalidations(queue),\n });\n // Cut before the guards below: the page is a hook, and the panel returns\n // early while the list loads or fails.\n const items = repeatables.data?.items ?? [];\n const page = useClientPage(items, REPEATABLE_PAGE_SIZE);\n if (repeatables.isPending) {\n return (\n <Spinner\n label=\"Loading repeatables\"\n showLabel\n />\n );\n }\n if (repeatables.isError) {\n return (\n <ProblemBanner\n error={repeatables.error}\n title=\"Could not load repeatables\"\n onRetry={() => void repeatables.refetch()}\n />\n );\n }\n // An actions column only when some row has a button in it. Decided over\n // every series, not the page, so a column cannot appear on one page and\n // vanish on the next.\n const hasActions =\n canRemove ||\n items.some((series) => (series.disabled ? canEnable : canDisable));\n if (items.length === 0) {\n return (\n <EmptyState\n title=\"No repeatables\"\n description=\"No repeat series is scheduled on this queue.\"\n />\n );\n }\n return (\n <>\n <Table label={`Repeatables of ${queue}`}>\n <thead>\n <tr>\n <th scope=\"col\">Key</th>\n <th scope=\"col\">Name</th>\n <th scope=\"col\">Schedule</th>\n <th scope=\"col\">Next run</th>\n <th\n scope=\"col\"\n className=\"num\"\n >\n Runs\n </th>\n <th scope=\"col\">Data</th>\n {hasActions && (\n <th scope=\"col\">\n <span className=\"visually-hidden\">Actions</span>\n </th>\n )}\n </tr>\n </thead>\n <tbody>\n {page.rows.map((series) => (\n <tr\n key={series.key}\n data-testid={`repeatable-row-${series.key}`}\n >\n <th scope=\"row\">\n <code>{series.key}</code>\n {series.disabled && (\n <>\n {\" \"}\n <Badge\n tone=\"warning\"\n title=\"Schedules nothing until it is enabled\"\n >\n Disabled\n </Badge>\n </>\n )}\n </th>\n <td>{series.name}</td>\n <td>{describeSchedule(series)}</td>\n <td data-testid={`repeatable-next-${series.key}`}>\n {series.disabled ? (\n \"paused (disabled)\"\n ) : (\n <RelativeTime value={series.nextRunAt} />\n )}\n </td>\n <td className=\"num\">\n {formatNumber(series.count)}\n {series.limit !== undefined &&\n ` / ${formatNumber(series.limit)}`}\n </td>\n <td className=\"repeatable-data\">\n {series.data === undefined ? (\n \"—\"\n ) : (\n <JsonView\n value={series.data}\n label={`Data of ${series.key}`}\n expandDepth={0}\n />\n )}\n </td>\n {hasActions && (\n <td>\n <div className=\"repeatable-actions\">\n {canDisable && !series.disabled && (\n <ToggleButton\n series={series}\n to=\"disable\"\n pending={\n toggle.isPending &&\n toggle.variables?.key === series.key\n }\n onClick={() =>\n toggle.mutate({ key: series.key, to: \"disable\" })\n }\n />\n )}\n {canEnable && series.disabled && (\n <ToggleButton\n series={series}\n to=\"enable\"\n pending={\n toggle.isPending &&\n toggle.variables?.key === series.key\n }\n onClick={() =>\n toggle.mutate({ key: series.key, to: \"enable\" })\n }\n />\n )}\n {canRemove && (\n <Button\n size=\"sm\"\n variant=\"danger\"\n aria-label={`Remove repeatable ${series.key}`}\n onClick={() => setRemoving(series)}\n >\n Remove…\n </Button>\n )}\n </div>\n </td>\n )}\n </tr>\n ))}\n </tbody>\n </Table>\n {page.paged && (\n <Pager\n label=\"Repeatable pages\"\n offset={page.offset}\n limit={page.limit}\n total={page.total}\n itemCount={page.rows.length}\n onChange={page.onChange}\n />\n )}\n <ConfirmDialog\n open={removing !== null}\n onClose={() => setRemoving(null)}\n title={`Remove the repeat series ${removing?.key ?? \"\"}?`}\n description=\"No further occurrences are scheduled. Jobs it already produced stay.\"\n variant=\"danger\"\n confirmLabel=\"Remove\"\n onConfirm={() =>\n removing ? remove.mutateAsync(removing.key) : undefined\n }\n />\n </>\n );\n}\n\n/** Props of {@link ToggleButton}. */\ninterface ToggleButtonProps {\n /** The series the button acts on. */\n series: RepeatableDto;\n /** What it does. */\n to: RepeatableToggle;\n /** Whether this series' request is in flight. */\n pending: boolean;\n /** Sends the request. */\n onClick: () => void;\n}\n\n/** A series' Disable or Enable button: idempotent, so it asks no confirmation. */\nfunction ToggleButton({ series, to, pending, onClick }: ToggleButtonProps) {\n const label = to === \"disable\" ? \"Disable\" : \"Enable\";\n return (\n <Button\n size=\"sm\"\n aria-label={`${label} repeatable ${series.key}`}\n disabled={pending}\n aria-busy={pending || undefined}\n onClick={onClick}\n >\n {pending ? `${to === \"disable\" ? \"Disabling\" : \"Enabling\"}…` : label}\n </Button>\n );\n}\n",
|
|
10
|
+
"import type { SummonPolicyKey } from \"../../../api/contract\";\nimport type {\n SummonCapabilitiesDto,\n SummonPolicyDto,\n SummonStatusDto,\n SummonTriggerDto,\n} from \"../../../api/types\";\nimport { displayText, formatNumber, plural } from \"../../../format\";\nimport { formatMs } from \"../duration\";\n\n/**\n * A summon controller's policy in plain words: the rows of the Summon\n * panel's Policy block (`status.policy`, advertised by\n * `/meta.features.summonPolicy`), each value marked \"default\" where the\n * policy did not set it.\n *\n * Every value names the `SUMMON_POLICY_KEYS` paths it shows, so whether it\n * is a default is one lookup in `policy.explicit`. Values the policy cannot\n * set — the platform's shutdown and lifetime cap, from the summoner's\n * capabilities — name none and are never marked.\n */\n\n/** What the Summon panel shows of a status's policy. */\nexport type SummonPolicyVisibility =\n /** The Policy block. */\n | \"shown\"\n /** One line: the controller runs in another process, which alone knows its policy. */\n | \"elsewhere\"\n /** Nothing: an API without the field, or a local status without a policy. */\n | \"none\";\n\n/** The line shown in place of the Policy block for a status read from storage (`local: false`). */\nexport const SUMMON_POLICY_ELSEWHERE =\n \"Its policy is known only where its controller runs.\";\n\n/** The Boot budget row's value while the summoner is not ready and the policy set none. */\nexport const PROVIDER_DEFAULT_BOOT_BUDGET = \"(provider default)\";\n\n/** The Workers row's hint: which values it shows depends on readiness, and it does not say which. */\nexport const SUMMON_POLICY_WORKERS_HINT =\n \"While the summoner is not ready, the configured values; once it is, they may be lowered to a wake pool's size.\";\n\n/** The tooltip of a value's \"default\" tag. */\nexport const SUMMON_POLICY_DEFAULT_HINT =\n \"The policy does not set this: the default applies.\";\n\n/** The tooltip of a per-queue value's \"override\" tag. */\nexport const SUMMON_POLICY_OVERRIDE_HINT =\n \"Set for this queue by the unit's overrides.\";\n\n/**\n * Whether the Summon panel shows the policy: only where the API says it\n * sends one (`features.summonPolicy`, compared with `true` so an older API's\n * missing flag is off). A status read from storage (`local: false`) never\n * has one, which the panel says in a line; a local status without one shows\n * nothing.\n */\nexport function summonPolicyVisibility(\n featureOn: boolean,\n status: Pick<SummonStatusDto, \"local\" | \"policy\">,\n): SummonPolicyVisibility {\n if (!featureOn) {\n return \"none\";\n }\n if (!status.local) {\n return \"elsewhere\";\n }\n return status.policy === undefined ? \"none\" : \"shown\";\n}\n\n/** One piece of a policy row's value, with the policy keys it shows. */\nexport interface SummonPolicyPart {\n /** What is printed before it, after the previous piece: `\"\"` for the first. */\n joiner: string;\n /** The piece in words; for {@link names}, what an empty list reads. */\n text: string;\n /**\n * The `SUMMON_POLICY_KEYS` paths it shows. Empty for a value no policy\n * key sets (the platform's, from the summoner's capabilities, or a\n * trigger kind this build does not know), which is never tagged.\n */\n keys: readonly SummonPolicyKey[];\n /** Whether it is shown muted: a trigger that is off. */\n muted: boolean;\n /** Names shown as code chips in place of {@link text} when there are any (the env names). */\n names?: readonly string[];\n /** Whether it carries its own \"default\" tag (see {@link SummonPolicyRow.tag}). */\n tag: boolean;\n}\n\n/** One row of the Policy block. */\nexport interface SummonPolicyRow {\n /** Stable id: the row's `data-testid` is `summon-policy-<id>`. */\n id: string;\n /** The row's label. */\n label: string;\n /** Its value, piece by piece. */\n parts: SummonPolicyPart[];\n /** A note under the value. */\n hint?: string;\n /**\n * Whether the row carries one \"default\" tag after its whole value: when\n * every piece shows policy keys and none of them was set. Otherwise each\n * piece none of whose keys was set carries its own ({@link SummonPolicyPart.tag}).\n */\n tag: boolean;\n}\n\n/** A piece before its tag is decided. */\ntype Piece = Omit<SummonPolicyPart, \"tag\" | \"muted\"> & { muted?: boolean };\n\n/** A piece: `text` showing `keys`, after `joiner`. */\nfunction piece(\n text: string,\n keys: readonly SummonPolicyKey[],\n joiner = \"\",\n muted = false,\n): Piece {\n return { joiner, text, keys, muted };\n}\n\n/** Each known trigger kind's label. */\nconst TRIGGER_LABELS: Readonly<Record<SummonTriggerDto[\"kind\"], string>> = {\n onAdd: \"On add\",\n events: \"Events\",\n poll: \"Poll\",\n};\n\n/** Each trigger kind's policy key. */\nconst TRIGGER_KEYS: Readonly<\n Record<SummonTriggerDto[\"kind\"], SummonPolicyKey>\n> = {\n onAdd: \"triggers.onAdd\",\n events: \"triggers.events\",\n poll: \"triggers.poll\",\n};\n\n/** Each `servedBy` in words. */\nconst SERVED_BY: Readonly<Record<SummonPolicyDto[\"servedBy\"], string>> = {\n \"any-worker\": \"any live worker\",\n \"summoned-only\": \"summoned workers only\",\n};\n\n/** Whether `key` is an own key of `table` (so `\"toString\"` is not). */\nfunction known<K extends string>(\n table: Readonly<Record<K, unknown>>,\n key: unknown,\n): key is K {\n return typeof key === \"string\" && Object.hasOwn(table, key);\n}\n\n/** A trigger of a kind this build does not know, as raw text: `webhook (on: true, every: 5000)`. */\nfunction unknownTrigger(trigger: SummonTriggerDto): string {\n const fields = Object.entries(trigger)\n .filter(([key]) => key !== \"kind\")\n .map(([key, value]) => `${key}: ${displayText(value)}`);\n const kind = displayText(trigger.kind);\n return fields.length === 0 ? kind : `${kind} (${fields.join(\", \")})`;\n}\n\n/**\n * The triggers, in the order given: \"On add · Events (coalesced 250ms) ·\n * Poll every 10s\". One that is off reads \"<label> off\", muted; a kind this\n * build does not know, raw text, untagged.\n *\n * The debounce coalesces add and event triggers alike, so it is printed\n * once, after the last trigger that is on and carries it, as its own piece\n * (`triggers.debounce`); a trigger with a different debounce gets its own.\n * With neither on, it is not printed: it changes nothing.\n */\nfunction triggerPieces(triggers: readonly SummonTriggerDto[]): Piece[] {\n const pieces: Piece[] = [];\n triggers.forEach((trigger, index) => {\n const joiner = pieces.length === 0 ? \"\" : \" · \";\n if (!known(TRIGGER_LABELS, trigger.kind)) {\n pieces.push(\n piece(unknownTrigger(trigger), [], joiner, trigger.on === false),\n );\n return;\n }\n const label = TRIGGER_LABELS[trigger.kind];\n const key = TRIGGER_KEYS[trigger.kind];\n if (trigger.on !== true) {\n pieces.push(piece(`${label} off`, [key], joiner, true));\n return;\n }\n if (trigger.kind === \"poll\") {\n pieces.push(\n piece(\n typeof trigger.every === \"number\"\n ? `${label} every ${formatMs(trigger.every)}`\n : label,\n [key],\n joiner,\n ),\n );\n return;\n }\n pieces.push(piece(label, [key], joiner));\n const debounce = trigger.debounce;\n const repeated = triggers\n .slice(index + 1)\n .some(\n (later) =>\n later.on === true &&\n (later.kind === \"onAdd\" || later.kind === \"events\") &&\n later.debounce === debounce,\n );\n if (typeof debounce === \"number\" && !repeated) {\n pieces.push(\n piece(`(coalesced ${formatMs(debounce)})`, [\"triggers.debounce\"], \" \"),\n );\n }\n });\n return pieces;\n}\n\n/** `jobsPerWorker` in words: \"2 jobs per worker\", or `null` \"one regardless of depth\". */\nexport function jobsPerWorkerText(value: number | null): string {\n return value === null\n ? \"one regardless of depth\"\n : `${plural(value, \"job\")} per worker`;\n}\n\n/** The platform's shutdown in words: \"SIGTERM, 3s grace (up to 1m)\", or \"no stop signal (in-invocation)\". */\nexport function shutdownText(\n shutdown: SummonCapabilitiesDto[\"shutdown\"],\n): string {\n if (shutdown.signal === \"none\") {\n return \"no stop signal (in-invocation)\";\n }\n const upTo =\n typeof shutdown.graceMaxMs === \"number\"\n ? ` (up to ${formatMs(shutdown.graceMaxMs)})`\n : \"\";\n return `${displayText(shutdown.signal)}, ${formatMs(shutdown.graceMs)} grace${upTo}`;\n}\n\n/** A row before its tags are decided. */\ninterface Draft {\n /** {@link SummonPolicyRow.id}. */\n id: string;\n /** {@link SummonPolicyRow.label}. */\n label: string;\n /** Its pieces. */\n pieces: Piece[];\n /** {@link SummonPolicyRow.hint}. */\n hint?: string;\n}\n\n/** Decides a row's tags against the keys the policy set. */\nfunction tagged(draft: Draft, explicit: ReadonlySet<string>): SummonPolicyRow {\n const defaulted = (keys: readonly SummonPolicyKey[]) =>\n keys.length > 0 && keys.every((key) => !explicit.has(key));\n const whole =\n draft.pieces.length > 0 &&\n draft.pieces.every((part) => defaulted(part.keys));\n return {\n id: draft.id,\n label: draft.label,\n ...(draft.hint === undefined ? {} : { hint: draft.hint }),\n tag: whole,\n parts: draft.pieces.map((part) => ({\n ...part,\n muted: part.muted === true,\n tag: !whole && defaulted(part.keys),\n })),\n };\n}\n\n/**\n * The Policy block's rows, in the panel's order, each value marked\n * \"default\" where the policy did not set it (`policy.explicit`).\n *\n * - **Workers** shows `maxWorkers`, `jobsPerWorker` and `maxPending`, with a\n * hint that they are the configured values or a wake pool's clamp\n * depending on readiness.\n * - **Group circuit** only where `circuit.group` is present: a per-queue\n * member of a group with a shared circuit, never a shared unit.\n * - **Boot budget** reads {@link PROVIDER_DEFAULT_BOOT_BUDGET} when absent\n * (the summoner is not ready and the policy set none), never a number.\n * - **Longest life** adds the platform's cap and **Shutdown** is the\n * platform's own, both from `capabilities` and untagged; without\n * `capabilities` (not ready) there is no Shutdown row.\n * - **Scale down** only for a scale-style summoner.\n *\n * The per-queue capacities of a shared unit are {@link summonPolicyPerQueue};\n * the budget stays in the status's own Budget row ({@link summonBudgetDefaulted}).\n */\nexport function summonPolicyRows(\n policy: SummonPolicyDto,\n capabilities: SummonCapabilitiesDto | undefined,\n): SummonPolicyRow[] {\n const explicit = new Set<string>(\n Array.isArray(policy.explicit) ? policy.explicit : [],\n );\n const group = policy.circuit.group;\n const rows: (Draft | false)[] = [\n {\n id: \"triggers\",\n label: \"Triggers\",\n pieces: triggerPieces(policy.triggers),\n hint: \"What makes the controller check for work to summon for.\",\n },\n {\n id: \"workers\",\n label: \"Workers\",\n pieces: [\n piece(`at most ${formatNumber(policy.maxWorkers)}`, [\"maxWorkers\"]),\n piece(\n jobsPerWorkerText(policy.jobsPerWorker),\n [\"jobsPerWorker\"],\n \" · \",\n ),\n piece(\n `at most ${formatNumber(policy.maxPending)} starting`,\n [\"maxPending\"],\n \" · \",\n ),\n ],\n hint: SUMMON_POLICY_WORKERS_HINT,\n },\n {\n id: \"cooldown\",\n label: \"Cooldown\",\n pieces: [piece(formatMs(policy.cooldown), [\"cooldown\"])],\n hint: \"The least time between two attempts.\",\n },\n {\n id: \"backoff\",\n label: \"Backoff\",\n pieces: [\n piece(formatMs(policy.backoff.initial), [\"backoff.initial\"]),\n piece(\n `doubling to ${formatMs(policy.backoff.max)}`,\n [\"backoff.max\"],\n \", \",\n ),\n ],\n hint: \"The wait after a failed or lost attempt, doubling with each one in a row.\",\n },\n {\n id: \"circuit\",\n label: \"Circuit\",\n pieces: [\n piece(`opens after ${plural(policy.circuit.failures, \"failure\")}`, [\n \"circuit.failures\",\n ]),\n piece(\n `for ${formatMs(policy.circuit.resetAfter)}`,\n [\"circuit.resetAfter\"],\n \", \",\n ),\n ],\n hint: \"Failed or lost attempts in a row that stop summoning, and for how long before one trial attempt.\",\n },\n group !== undefined && {\n id: \"group-circuit\",\n label: \"Group circuit\",\n pieces: [\n piece(\n `opens after ${plural(group.failures, \"failure\")} across the group`,\n [\"group.circuit.failures\"],\n ),\n piece(\n `for ${formatMs(group.resetAfter)}`,\n [\"group.circuit.resetAfter\"],\n \", \",\n ),\n ],\n hint: \"The summon group's shared circuit: failures of this summoner's kind on any of its queues.\",\n },\n {\n id: \"boot-budget\",\n label: \"Boot budget\",\n pieces: [\n typeof policy.bootBudget === \"number\"\n ? piece(formatMs(policy.bootBudget), [\"bootBudget\"])\n : // Not known until the summoner is ready, and not the policy's.\n piece(PROVIDER_DEFAULT_BOOT_BUDGET, []),\n ],\n hint: \"How long an attempt counts as a worker on its way before it is lost.\",\n },\n {\n id: \"lifetime\",\n label: \"Longest life\",\n pieces: [\n piece(formatMs(policy.maxLifetime), [\"maxLifetime\"]),\n ...(typeof capabilities?.maxLifetimeMs === \"number\"\n ? [\n piece(\n `platform cap ${formatMs(capabilities.maxLifetimeMs)}`,\n [],\n \" · \",\n ),\n ]\n : []),\n ],\n hint: \"The longest a summoned worker may live.\",\n },\n capabilities !== undefined && {\n id: \"shutdown\",\n label: \"Shutdown\",\n pieces: [piece(shutdownText(capabilities.shutdown), [])],\n hint: \"What the platform sends to stop a unit, and how long it waits: declared by the provider, not set by the policy.\",\n },\n capabilities?.style === \"scale\" && {\n id: \"scale-down\",\n label: \"Scale down\",\n pieces: [\n piece(`to zero after ${formatMs(policy.scaleDown.after)} idle`, [\n \"scaleDown.after\",\n ]),\n ],\n },\n {\n id: \"summon-timeout\",\n label: \"Summon timeout\",\n pieces: [piece(formatMs(policy.summonTimeout), [\"summonTimeout\"])],\n hint: \"How long one call to the summoner may take.\",\n },\n {\n id: \"served-by\",\n label: \"Served by\",\n pieces: [\n piece(\n known(SERVED_BY, policy.servedBy)\n ? SERVED_BY[policy.servedBy]\n : displayText(policy.servedBy),\n [\"servedBy\"],\n ),\n ],\n hint: \"Which live workers count as serving the queue.\",\n },\n {\n id: \"env\",\n label: \"Env\",\n pieces: [\n {\n ...piece(\"none\", [\"env\"]),\n names: policy.env.map((name) => displayText(name)),\n },\n ],\n hint: \"The names the policy adds to every summoned worker's environment. Their values are never sent.\",\n },\n {\n id: \"from-summoned\",\n label: \"From summoned workers\",\n pieces: [piece(policy.fromSummoned ? \"yes\" : \"no\", [\"fromSummoned\"])],\n hint: \"Whether this controller may run in a summoned process or a runner child.\",\n },\n {\n id: \"failure-hook\",\n label: \"Failure hook\",\n pieces: [\n piece(policy.onSummonFailed ? \"set\" : \"not set\", [\"onSummonFailed\"]),\n ],\n hint: \"Whether an onSummonFailed hook is called on failed attempts.\",\n },\n ];\n return rows\n .filter((row): row is Draft => row !== false)\n .map((row) => tagged(row, explicit));\n}\n\n/** One queue of a shared unit's per-queue capacities. */\nexport interface SummonPolicyQueueRow {\n /** The queue, made safe as text. */\n queue: string;\n /** Its jobs per worker in words ({@link jobsPerWorkerText}). */\n jobsPerWorker: string;\n /** Its most workers. */\n maxWorkers: string;\n /** Whether the unit's overrides set its `jobsPerWorker`. */\n jobsPerWorkerOverride: boolean;\n /** Whether the unit's overrides set its `maxWorkers`. */\n maxWorkersOverride: boolean;\n}\n\n/**\n * A shared unit's per-queue capacities (`policy.perQueue`), in the order the\n * API sends them, each value marked where the unit's overrides set it.\n * `undefined` for a controller of one queue, which has none.\n */\nexport function summonPolicyPerQueue(\n policy: Pick<SummonPolicyDto, \"perQueue\">,\n): SummonPolicyQueueRow[] | undefined {\n if (policy.perQueue === undefined) {\n return undefined;\n }\n return Object.entries(policy.perQueue).map(([queue, capacity]) => {\n const set = Array.isArray(capacity.explicit) ? capacity.explicit : [];\n return {\n queue: displayText(queue),\n jobsPerWorker: jobsPerWorkerText(capacity.jobsPerWorker),\n maxWorkers: formatNumber(capacity.maxWorkers),\n jobsPerWorkerOverride: set.includes(\"jobsPerWorker\"),\n maxWorkersOverride: set.includes(\"maxWorkers\"),\n };\n });\n}\n\n/**\n * Whether the status's existing Budget row is marked \"default\": the policy\n * gave no `budget` at all (`budget` is listed whenever it gave one, `false`\n * included). Only where the policy is shown; without one, nothing is known.\n */\nexport function summonBudgetDefaulted(\n policy: Pick<SummonPolicyDto, \"explicit\"> | undefined,\n): boolean {\n return (\n policy !== undefined &&\n Array.isArray(policy.explicit) &&\n !policy.explicit.includes(\"budget\")\n );\n}\n",
|
|
11
|
+
"import type {\n SummonCapabilitiesDto,\n SummonPolicyDto,\n SummonStatusDto,\n} from \"../../../api/types\";\nimport type { SummonPolicyPart, SummonPolicyRow } from \"./summonPolicyText\";\nimport { keepPreviousData, useQueries, useQuery } from \"@tanstack/react-query\";\nimport { useState } from \"react\";\nimport { demandKeys } from \"../../../api/demand\";\nimport { isApiError } from \"../../../api/errors\";\nimport { listProviders, providerKeys } from \"../../../api/providers\";\nimport { queryKeys } from \"../../../api/queryKeys\";\nimport { queueKeys } from \"../../../api/queues\";\nimport {\n getSummonStatus,\n resetSummon,\n summonKeys,\n summonNow,\n} from \"../../../api/summon\";\nimport { Badge } from \"../../../components/Badge\";\nimport { Button } from \"../../../components/Button\";\nimport { ConfirmDialog } from \"../../../components/ConfirmDialog\";\nimport { EmptyState } from \"../../../components/EmptyState\";\nimport { Checkbox } from \"../../../components/inputs\";\nimport { KeyValue } from \"../../../components/KeyValue\";\nimport { ProblemBanner } from \"../../../components/ProblemBanner\";\nimport { RelativeTime } from \"../../../components/RelativeTime\";\nimport { Spinner } from \"../../../components/Spinner\";\nimport { Table } from \"../../../components/Table\";\nimport { useToast } from \"../../../components/toast\";\nimport { useApiClient } from \"../../../context\";\nimport { displayText, formatNumber, plural } from \"../../../format\";\nimport { useApiMutation } from \"../../../hooks/useApiMutation\";\nimport { canPerform, useCan, useFeature, useMeta } from \"../../../meta/hooks\";\nimport { Link } from \"../../../router\";\nimport { providerReadiness } from \"../../providers/providerText\";\nimport { TestConnection } from \"../../providers/TestConnection\";\nimport { summonTabPath } from \"../../summoning/paths\";\nimport { formatMs } from \"../duration\";\nimport { useCanMutate } from \"../gating\";\nimport { useRefreshInterval } from \"../live\";\nimport { budgetUsedText } from \"./budgetText\";\nimport { SummonBudget } from \"./SummonBudget\";\nimport { SummonGroupCard } from \"./SummonGroupCard\";\nimport {\n SUMMON_POLICY_DEFAULT_HINT,\n SUMMON_POLICY_ELSEWHERE,\n SUMMON_POLICY_OVERRIDE_HINT,\n summonBudgetDefaulted,\n summonPolicyPerQueue,\n summonPolicyRows,\n summonPolicyVisibility,\n} from \"./summonPolicyText\";\nimport {\n attemptCoverage,\n COVERAGE_HEADER,\n COVERAGE_NONE,\n SHARED_UNIT_FORBIDDEN,\n showCoverage,\n SUMMON_PARTIAL_HINT,\n summonActionsOffered,\n summonCheckSummary,\n summonInert,\n summonLastDetail,\n summonLastMissing,\n summonOutcome,\n summonPartialText,\n summonStateKnown,\n summonStyle,\n} from \"./summonText\";\n\n/** Props of {@link SummonPanel}. */\nexport interface SummonPanelProps {\n /** The queue. */\n queue: string;\n}\n\n/**\n * A queue's summoning (`GET /queues/:queue/summon`): who summons its workers,\n * what is on its way, what holds the next attempt back, the summon group it\n * is in (when it is in one), and the two opt-in actions — \"summon now\" and\n * reset (`queues.summon`).\n *\n * Shown only where the status answers 200: the queue screen reads it before\n * offering the tab, and a queue with no summoner in the API's process (409\n * `SUMMON_NOT_CONFIGURED`) has no tab at all.\n *\n * A shared summon unit (a status with `queues`) is one process serving\n * several queues: the panel names them, warns when another group's units\n * were seen on them, and offers the actions only where the caller may\n * summon every one of them ({@link useUnitCanSummon}).\n *\n * Where the API sends it (`features.summonPolicy`), the controller's policy\n * follows the summoner ({@link SummonPolicy}); a status read from another\n * process's controller has none, and says so in a line.\n *\n * `local` is always `true` today. A `false` answer — a controller in another\n * process, read from the store — is shown read-only: no actions, since both\n * need a controller in the API's own process.\n */\nexport function SummonPanel({ queue }: SummonPanelProps) {\n const api = useApiClient();\n const canMutate = useCanMutate();\n const refetchInterval = useRefreshInterval(\"summon\");\n const [confirming, setConfirming] = useState<\"summon\" | \"reset\" | null>(null);\n const [force, setForce] = useState(true);\n // Off each time the dialog opens: clearing the budget is never carried over.\n const [clearBudget, setClearBudget] = useState(false);\n // The API takes `{ budget: true }` (and serves `GET /summon`) only where it says so.\n const canClearBudget = useFeature(\"summonResetBudget\") === true;\n // The status carries the controller's policy only where the API says so.\n const policyFeature = useFeature(\"summonPolicy\") === true;\n const status = useQuery({\n queryKey: summonKeys.status(api, queue),\n queryFn: ({ signal }) => getSummonStatus(api, queue, signal),\n refetchInterval,\n placeholderData: keepPreviousData,\n });\n const toast = useToast();\n const unitCanSummon = useUnitCanSummon(status.data?.queues);\n // A shared unit's actions need queues.summon on every queue it serves,\n // hidden ones included, and the 403 names none of them: say so.\n const onActionError = (error: Error) => {\n if (\n status.data?.queues !== undefined &&\n isApiError(error) &&\n error.status === 403\n ) {\n toast.error(SHARED_UNIT_FORBIDDEN, {\n description: error.detail ?? error.title,\n });\n }\n };\n const invalidate = [\n ...summonKeys.changed(api, queue),\n demandKeys.queue(queue),\n queueKeys.workers(queue),\n ];\n const summon = useApiMutation({\n mutationFn: (skipCooldown: boolean) => summonNow(api, queue, skipCooldown),\n successMessage: (result) => summonCheckSummary(result),\n invalidate,\n toastErrors: false,\n onError: onActionError,\n });\n const reset = useApiMutation({\n mutationFn: (budget: boolean) =>\n resetSummon(api, queue, budget ? { budget: true } : {}),\n successMessage: (_result, budget) =>\n budget\n ? `Reset the summon state of ${queue}: failures, backoff, circuit and budget usage cleared`\n : `Reset the summon state of ${queue}: failures, backoff and circuit cleared`,\n invalidate,\n toastErrors: false,\n onError: onActionError,\n });\n\n if (status.isPending) {\n return (\n <Spinner\n label=\"Loading summon status\"\n showLabel\n />\n );\n }\n if (status.isError) {\n return (\n <ProblemBanner\n error={status.error}\n title=\"Could not load the summon status\"\n onRetry={() => void status.refetch()}\n />\n );\n }\n if (status.data === null) {\n return (\n <EmptyState\n title=\"No summoner here\"\n description=\"No summon controller for this queue runs in this API's process.\"\n />\n );\n }\n const data = status.data;\n const policyView = summonPolicyVisibility(policyFeature, data);\n const actionable = summonActionsOffered(\n data,\n // A single queue's own map decides, as it always has; a shared unit's,\n // every member's map.\n unitCanSummon ?? canMutate(\"queues.summon\"),\n );\n return (\n <div\n className=\"summon-panel\"\n data-testid=\"queue-summon\"\n >\n {data.inert && (\n <p\n className=\"notice\"\n role=\"note\"\n data-testid=\"summon-inert\"\n >\n {summonInert(data.inertReason)}\n </p>\n )}\n {!data.local && (\n <p\n className=\"notice\"\n role=\"note\"\n data-testid=\"summon-stored\"\n >\n This queue's controller runs in another process: its state is read\n from the store, and it can be summoned or reset only from there.\n </p>\n )}\n {data.overlaps !== undefined && data.overlaps.length > 0 && (\n <p\n className=\"notice\"\n role=\"note\"\n data-testid=\"summon-overlaps\"\n >\n {data.queues === undefined\n ? `Workers summoned for another summon group (${data.overlaps.map(displayText).join(\", \")}) were seen on this queue at the last check: two units may be serving the same queue.`\n : `Workers summoned for another summon group (${data.overlaps.map(displayText).join(\", \")}) were seen on this unit's queues at the last check: two units may be serving the same queues.`}\n </p>\n )}\n <SummonState\n status={data}\n queue={queue}\n policy={policyView === \"shown\" ? data.policy : undefined}\n />\n {data.group !== undefined && (\n <SummonGroupCard\n group={data.group}\n queue={queue}\n kind={data.summoner?.provider.kind}\n />\n )}\n {data.summoner && <Summoner summoner={data.summoner} />}\n {policyView === \"shown\" && data.policy !== undefined && (\n <SummonPolicy\n policy={data.policy}\n capabilities={data.summoner?.capabilities}\n queue={queue}\n />\n )}\n {policyView === \"elsewhere\" && (\n <p\n className=\"muted\"\n data-testid=\"summon-policy-elsewhere\"\n >\n {SUMMON_POLICY_ELSEWHERE}\n </p>\n )}\n {summonStateKnown(data) && data.pending.length > 0 && (\n <PendingTable\n queue={queue}\n queues={data.queues}\n pending={data.pending}\n />\n )}\n {actionable && (\n <div\n className=\"summon-actions\"\n role=\"group\"\n aria-label=\"Summon actions\"\n >\n <Button\n variant=\"primary\"\n onClick={() => setConfirming(\"summon\")}\n disabled={summon.isPending}\n >\n Summon now…\n </Button>\n <Button\n onClick={() => {\n setClearBudget(false);\n setConfirming(\"reset\");\n }}\n disabled={reset.isPending}\n >\n Reset…\n </Button>\n </div>\n )}\n <ConfirmDialog\n open={confirming === \"summon\"}\n onClose={() => setConfirming(null)}\n title={`Summon a worker for ${queue} now?`}\n description=\"Runs one check of the queue's summon controller now. The circuit, the budget, the attempts already on their way and the guard against two controllers summoning at once all still apply.\"\n confirmLabel=\"Summon now\"\n onConfirm={() => summon.mutateAsync(force)}\n >\n <Checkbox\n checked={force}\n onChange={setForce}\n label=\"Skip the cooldown\"\n hint=\"Summon even if the last attempt was recent. Nothing else is skipped.\"\n />\n </ConfirmDialog>\n <ConfirmDialog\n open={confirming === \"reset\"}\n onClose={() => setConfirming(null)}\n title={`Reset the summon state of ${queue}?`}\n description=\"Clears the consecutive failures, the backoff and an open circuit, so the next check may summon. Attempts already on their way are kept.\"\n variant=\"danger\"\n confirmLabel=\"Reset\"\n onConfirm={() => reset.mutateAsync(canClearBudget && clearBudget)}\n >\n {canClearBudget && (\n <Checkbox\n checked={clearBudget}\n onChange={setClearBudget}\n label=\"Also clear budget usage\"\n hint=\"Sets the attempts counted this UTC hour and today back to 0, so the budget allows its full limits again. Every controller on the queue shares the counts.\"\n />\n )}\n </ConfirmDialog>\n </div>\n );\n}\n\n/**\n * Whether the caller may summon or reset a shared unit (`queues`): only with\n * `queues.summon` on **every** queue the status lists, each judged by its own\n * targeted map (`GET /meta/permissions?queue=<member>`, the request the\n * queue screen's own scope makes, so its own queue's is shared with it), and\n * not read-only. `false` while any map is loading or after one failed: the\n * server refuses the action on any member, so a guess would offer a button\n * that fails. Members hidden from the caller cannot be asked about; the\n * server's 403 covers them.\n *\n * `undefined` for a single queue's status (`queues` absent), which reads no\n * other map: its own scope decides, as before.\n */\nfunction useUnitCanSummon(\n queues: readonly string[] | undefined,\n): boolean | undefined {\n const api = useApiClient();\n const { readOnly } = useMeta();\n const maps = useQueries({\n queries: (queues ?? []).map((member) => ({\n queryKey: queryKeys.permissions({ queue: member }),\n queryFn: ({ signal }: { signal: AbortSignal }) =>\n api.getPermissions({ queue: member }, signal),\n })),\n });\n if (queues === undefined) {\n return undefined;\n }\n return (\n !readOnly &&\n maps.length > 0 &&\n maps.every(\n (map) => map.data !== undefined && canPerform(map.data, \"queues.summon\"),\n )\n );\n}\n\n/** Props of {@link SummonState}. */\ninterface SummonStateProps {\n /** The status. */\n status: SummonStatusDto;\n /** The queue whose Summon tab this is: marked among a shared unit's queues. */\n queue: string;\n /**\n * The controller's policy where the panel shows it, so the Budget row can\n * say when the budget is the default; `undefined` otherwise.\n */\n policy: SummonPolicyDto | undefined;\n}\n\n/**\n * What holds the next attempt back, and the last thing that happened. For\n * summon state the server could not read ({@link summonStateKnown}), only\n * the last outcome: the rest would be placeholders. For a shared unit, first\n * the queues it serves, and a last attempt that registered on only some of\n * them says which it missed ({@link summonLastMissing}) in place of its raw\n * `partial:<queues>` detail.\n */\nfunction SummonState({ status, queue, policy }: SummonStateProps) {\n const last =\n status.last === undefined ? undefined : summonOutcome(status.last.outcome);\n const missing =\n status.last === undefined ? [] : summonLastMissing(status.last);\n const detail =\n status.last === undefined ? undefined : summonLastDetail(status.last);\n const known = summonStateKnown(status);\n return (\n <KeyValue\n items={[\n status.queues !== undefined && {\n key: \"unit\",\n label: \"Shared unit\",\n value: (\n <UnitQueues\n queue={queue}\n queues={status.queues}\n />\n ),\n hint: \"One summoned process serves these queues, and this status is the unit's. Only the queues you may read are listed.\",\n },\n {\n key: \"last\",\n label: \"Last outcome\",\n value:\n status.last === undefined || last === undefined ? (\n <span data-testid=\"summon-last\">None yet</span>\n ) : (\n <span data-testid=\"summon-last\">\n <Badge\n tone={last.tone}\n title={last.hint}\n >\n {last.label}\n </Badge>{\" \"}\n <RelativeTime value={status.last.at} />\n {detail !== undefined && (\n <span className=\"muted\"> — {detail}</span>\n )}\n {missing.length > 0 && (\n <span\n className=\"summon-last-missing\"\n data-testid=\"summon-last-missing\"\n >\n {summonPartialText(missing)}\n </span>\n )}\n </span>\n ),\n hint:\n status.last !== undefined && missing.length > 0\n ? SUMMON_PARTIAL_HINT\n : undefined,\n },\n known && {\n key: \"pending\",\n label: \"On their way\",\n value: (\n <span data-testid=\"summon-pending-count\">\n {formatNumber(status.pending.length)}\n </span>\n ),\n hint: \"Summon attempts still counting as workers on their way.\",\n },\n known && {\n key: \"failures\",\n label: \"Consecutive failures\",\n value: (\n <span data-testid=\"summon-failures\">\n {formatNumber(status.failures)}\n </span>\n ),\n hint: \"Failed, unavailable or lost attempts in a row, towards the circuit. A throttled call is not counted, and an auth or misconfiguration error counts enough to open it at once. A registration or a reset clears them.\",\n },\n known &&\n status.backoffUntil !== undefined && {\n key: \"backoff\",\n label: \"Backing off until\",\n value: (\n <span data-testid=\"summon-backoff\">\n <RelativeTime value={status.backoffUntil} />\n </span>\n ),\n },\n known &&\n status.circuitOpenUntil !== undefined && {\n key: \"circuit\",\n label: \"Circuit open until\",\n value: (\n <span data-testid=\"summon-circuit\">\n <RelativeTime value={status.circuitOpenUntil} />\n </span>\n ),\n hint: \"After repeated failures nothing is summoned until then, apart from one trial attempt.\",\n },\n known &&\n status.budget !== undefined && {\n key: \"budget\",\n label: \"Budget\",\n value: (\n <>\n <SummonBudget\n budget={status.budget}\n testId=\"summon-budget\"\n // Its own budget off, the group's applies to it, unless the\n // group's is off too: then no limit applies at all.\n group={\n status.group !== undefined &&\n status.group.budget.off !== true\n }\n />\n {summonBudgetDefaulted(policy) && (\n <>\n {\" \"}\n <DefaultTag />\n </>\n )}\n </>\n ),\n // The row shows what is left; the counts used stay in view here.\n hint: budgetUsedText(status.budget),\n },\n ]}\n />\n );\n}\n\n/** Props of {@link UnitQueues}. */\ninterface UnitQueuesProps {\n /** The queue whose Summon tab this is: marked in the list. */\n queue: string;\n /** The unit's queues the caller may read (`status.queues`). */\n queues: readonly string[];\n}\n\n/**\n * A shared unit's queues in a line, \"One process for 3 queues: renders,\n * thumbs, previews\", each linked to its Summon tab and this one marked.\n * The count is of the queues listed: ones hidden from the caller are not\n * in it, so it never claims the unit's total.\n */\nfunction UnitQueues({ queue, queues }: UnitQueuesProps) {\n return (\n <span data-testid=\"summon-unit\">\n One process for {plural(queues.length, \"queue\")}:{\" \"}\n {queues.map((member, index) => {\n const current = member === queue;\n return (\n <span key={member}>\n {index > 0 && \", \"}\n <Link\n to={summonTabPath(member)}\n title={`The Summon tab of ${member}`}\n >\n {displayText(member)}\n </Link>\n {current && <span className=\"muted\"> (this queue)</span>}\n </span>\n );\n })}\n </span>\n );\n}\n\n/** The summoner: who provides it, what it declares, and its facts. */\nfunction Summoner({\n summoner,\n}: {\n /** The status's summoner. */\n summoner: NonNullable<SummonStatusDto[\"summoner\"]>;\n}) {\n const { provider, capabilities, facts } = summoner;\n const factEntries = Object.entries(facts);\n const readiness = providerReadiness(summoner.readiness);\n const preflight = useProviderPreflight(summoner.providerId);\n const canMutate = useCanMutate();\n const name = displayText(provider.displayName ?? provider.kind);\n return (\n <div\n className=\"summon-summoner\"\n data-testid=\"summon-summoner\"\n >\n <h3 className=\"summon-heading\">Summoner</h3>\n <KeyValue\n items={[\n {\n key: \"provider\",\n label: \"Provider\",\n value: (\n <span data-testid=\"summon-provider\">\n {displayText(provider.displayName ?? provider.kind)}{\" \"}\n <span className=\"muted\">\n {displayText(provider.name)} {displayText(provider.version)}\n </span>\n </span>\n ),\n },\n {\n key: \"readiness\",\n label: \"Readiness\",\n value: (\n <Badge\n tone={readiness.tone}\n title={readiness.hint}\n testId=\"summon-readiness\"\n >\n {readiness.label}\n </Badge>\n ),\n hint: readiness.hint,\n },\n // Declared once the summoner is ready: unknown before.\n ...(capabilities === undefined\n ? []\n : [\n {\n key: \"style\",\n label: \"Style\",\n value: summonStyle(capabilities.style),\n },\n {\n key: \"boot\",\n // The provider's default: the policy's own is in the Policy block.\n label: \"Provider default boot budget\",\n value: formatMs(capabilities.bootBudgetMs),\n hint: \"How long an attempt counts as a worker on its way before it is lost, unless the policy sets its own.\",\n },\n {\n key: \"lifetime\",\n label: \"Longest life\",\n value:\n capabilities.maxLifetimeMs === null\n ? \"No platform limit known\"\n : formatMs(capabilities.maxLifetimeMs),\n },\n ]),\n ...factEntries.map(([key, value]) => ({\n key: `fact-${key}`,\n label: displayText(key),\n value: <code>{displayText(value)}</code>,\n })),\n ]}\n />\n {summoner.providerId !== undefined &&\n preflight &&\n canMutate(\"providers.validate\") && (\n <TestConnection\n providerId={summoner.providerId}\n label={name}\n />\n )}\n </div>\n );\n}\n\n/** Props of {@link SummonPolicy}. */\ninterface SummonPolicyProps {\n /** The status's policy. */\n policy: SummonPolicyDto;\n /** The summoner's capabilities, once it is ready: the platform's shutdown, lifetime cap and style. */\n capabilities: SummonCapabilitiesDto | undefined;\n /** The queue, for the per-queue table's name. */\n queue: string;\n}\n\n/**\n * The policy the queue's controller runs with (`status.policy`), every value\n * in words and marked \"default\" where the policy did not set it\n * ({@link summonPolicyRows}); a shared unit's per-queue capacities as a\n * table after Workers, each value the unit's overrides set marked\n * \"override\". The platform's shutdown is the summoner's, not the policy's.\n */\nfunction SummonPolicy({ policy, capabilities, queue }: SummonPolicyProps) {\n const rows = summonPolicyRows(policy, capabilities);\n const perQueue = summonPolicyPerQueue(policy);\n const items = rows.flatMap((row) => [\n {\n key: row.id,\n label: row.label,\n value: <PolicyValue row={row} />,\n hint: row.hint,\n },\n ...(row.id === \"workers\" && perQueue !== undefined\n ? [\n {\n key: \"per-queue\",\n label: \"Per queue\",\n value: (\n <PerQueueTable\n queue={queue}\n rows={perQueue}\n />\n ),\n hint: \"Each of the unit's queues' own capacity: what its demand alone may ask for.\",\n },\n ]\n : []),\n ]);\n return (\n <div\n className=\"summon-policy\"\n data-testid=\"summon-policy\"\n >\n <h3 className=\"summon-heading\">Policy</h3>\n <KeyValue items={items} />\n </div>\n );\n}\n\n/** A policy row's value: its pieces, each muted or tagged as the row says. */\nfunction PolicyValue({\n row,\n}: {\n /** The row. */\n row: SummonPolicyRow;\n}) {\n return (\n <span data-testid={`summon-policy-${row.id}`}>\n {row.parts.map((part, index) => (\n <span\n // The pieces are fixed for a row: their position is their identity.\n // eslint-disable-next-line react/no-array-index-key\n key={index}\n >\n {part.joiner}\n <PolicyPart part={part} />\n {part.tag && (\n <>\n {\" \"}\n <DefaultTag />\n </>\n )}\n </span>\n ))}\n {row.tag && (\n <>\n {\" \"}\n <DefaultTag />\n </>\n )}\n </span>\n );\n}\n\n/** One piece of a policy value: its names as code chips, or its text. */\nfunction PolicyPart({\n part,\n}: {\n /** The piece. */\n part: SummonPolicyPart;\n}) {\n if (part.names !== undefined && part.names.length > 0) {\n return (\n <span className=\"summon-policy-names\">\n {part.names.map((name) => (\n <code key={name}>{name}</code>\n ))}\n </span>\n );\n }\n return <span className={part.muted ? \"muted\" : undefined}>{part.text}</span>;\n}\n\n/** The muted \"default\" tag of a value the policy did not set. */\nfunction DefaultTag() {\n return (\n <span\n className=\"summon-policy-tag muted\"\n title={SUMMON_POLICY_DEFAULT_HINT}\n data-testid=\"summon-policy-default\"\n >\n default\n </span>\n );\n}\n\n/** A shared unit's per-queue capacities, each override marked. */\nfunction PerQueueTable({\n queue,\n rows,\n}: {\n /** The queue whose tab this is, for the table's name. */\n queue: string;\n /** The rows ({@link summonPolicyPerQueue}). */\n rows: NonNullable<ReturnType<typeof summonPolicyPerQueue>>;\n}) {\n return (\n <div data-testid=\"summon-policy-per-queue\">\n <Table label={`Per-queue capacity of the unit serving ${queue}`}>\n <thead>\n <tr>\n <th scope=\"col\">Queue</th>\n <th scope=\"col\">Jobs per worker</th>\n <th\n scope=\"col\"\n className=\"num\"\n >\n Max workers\n </th>\n </tr>\n </thead>\n <tbody>\n {rows.map((row) => (\n <tr\n key={row.queue}\n data-testid={`summon-policy-per-queue-${row.queue}`}\n >\n <th scope=\"row\">{row.queue}</th>\n <td>\n {row.jobsPerWorker}\n {row.jobsPerWorkerOverride && <OverrideTag />}\n </td>\n <td className=\"num\">\n {row.maxWorkers}\n {row.maxWorkersOverride && <OverrideTag />}\n </td>\n </tr>\n ))}\n </tbody>\n </Table>\n </div>\n );\n}\n\n/** The \"override\" tag of a per-queue value the unit's overrides set. */\nfunction OverrideTag() {\n return (\n <>\n {\" \"}\n <Badge\n tone=\"accent\"\n title={SUMMON_POLICY_OVERRIDE_HINT}\n testId=\"summon-policy-override\"\n >\n override\n </Badge>\n </>\n );\n}\n\n/**\n * Whether the summoner's provider has a preflight, so \"Test connection\"\n * checks the platform: `GET /providers` says so, read only where the API\n * serves it (`features.providers`) and the caller may read providers\n * (`providers.read`). Without either, the answer is no and nothing is read.\n */\nfunction useProviderPreflight(providerId: string | undefined): boolean {\n const api = useApiClient();\n const meta = useMeta();\n const canRead = useCan(\"providers.read\");\n const enabled =\n providerId !== undefined && meta.features.providers && canRead;\n const list = useQuery({\n queryKey: providerKeys.list,\n queryFn: ({ signal }) => listProviders(api, signal),\n enabled,\n staleTime: 30_000,\n });\n return (\n enabled &&\n list.data?.providers.some(\n (item) => item.id === providerId && item.preflight,\n ) === true\n );\n}\n\n/**\n * The attempts on their way. A shared unit's carry their coverage, how far\n * each has got on each of the unit's queues: a Coverage column, where at\n * least one attempt has it.\n */\nfunction PendingTable({\n queue,\n queues,\n pending,\n}: {\n /** The queue, for the table's name; marked in a Coverage cell. */\n queue: string;\n /** A shared unit's queues (`status.queues`), the order a Coverage cell lists them in; `undefined` for one queue. */\n queues: readonly string[] | undefined;\n /** The attempts, oldest first. */\n pending: SummonStatusDto[\"pending\"];\n}) {\n // Only when the API sent some (`serialize.exposeSummonHandles`): nothing\n // marks handles withheld.\n const showHandles = pending.some((attempt) => attempt.handles !== undefined);\n const coverage = showCoverage(pending);\n return (\n <Table label={`Summon attempts on their way for ${queue}`}>\n <thead>\n <tr>\n <th scope=\"col\">Attempt</th>\n <th scope=\"col\">Started</th>\n <th scope=\"col\">Lost unless a worker registers</th>\n <th\n scope=\"col\"\n className=\"num\"\n >\n Workers\n </th>\n <th scope=\"col\">Summoner</th>\n {coverage && <th scope=\"col\">{COVERAGE_HEADER}</th>}\n {showHandles && <th scope=\"col\">Handles</th>}\n </tr>\n </thead>\n <tbody>\n {pending.map((attempt) => (\n <tr\n key={attempt.id}\n data-testid={`summon-pending-${attempt.id}`}\n >\n <th scope=\"row\">\n <code>{displayText(attempt.id)}</code>\n </th>\n <td>\n <RelativeTime value={attempt.at} />\n </td>\n <td>\n <RelativeTime value={attempt.until} />\n </td>\n <td className=\"num\">{formatNumber(attempt.count)}</td>\n <td>{displayText(attempt.kind)}</td>\n {coverage && (\n <td>\n <CoverageCell\n attempt={attempt.id}\n badges={attemptCoverage(attempt.coverage, queues, queue)}\n />\n </td>\n )}\n {showHandles && (\n <td>\n {attempt.handles?.map((handle) => (\n <code\n key={handle}\n className=\"worker-target-file\"\n >\n {displayText(handle)}\n </code>\n ))}\n </td>\n )}\n </tr>\n ))}\n </tbody>\n </Table>\n );\n}\n\n/** Props of {@link CoverageCell}. */\ninterface CoverageCellProps {\n /** The attempt's id, naming each badge's `data-testid`. */\n attempt: string;\n /** Its coverage's badges ({@link attemptCoverage}); `undefined` for an attempt without. */\n badges: ReturnType<typeof attemptCoverage>;\n}\n\n/**\n * One attempt's coverage: a badge per queue — \"✓ renders\" registered,\n * \"renders pending\", \"✗ renders missing\" — this tab's queue marked; \"—\" for\n * an attempt without (one queue's, or a status whose reads of it failed).\n */\nfunction CoverageCell({ attempt, badges }: CoverageCellProps) {\n if (badges === undefined || badges.length === 0) {\n return <span className=\"muted\">{COVERAGE_NONE}</span>;\n }\n return (\n <ul className=\"summon-coverage\">\n {badges.map((badge) => (\n <li key={badge.queue}>\n <Badge\n tone={badge.tone}\n title={badge.title}\n testId={`summon-coverage-${attempt}-${badge.queue}`}\n >\n {badge.label}\n </Badge>\n {badge.current && <span className=\"muted\"> (this queue)</span>}\n </li>\n ))}\n </ul>\n );\n}\n",
|
|
12
|
+
"/** The windows offered, in minutes (the API allows 1..1440). */\nexport const THROUGHPUT_WINDOWS = [15, 60, 360, 1440] as const;\n\n/** One window. */\nexport type ThroughputWindow = (typeof THROUGHPUT_WINDOWS)[number];\n\n/** The default window. */\nexport const DEFAULT_WINDOW: ThroughputWindow = 60;\n\n/** Labels of the windows. */\nexport const WINDOW_LABELS: Readonly<Record<ThroughputWindow, string>> = {\n 15: \"Last 15 minutes\",\n 60: \"Last hour\",\n 360: \"Last 6 hours\",\n 1440: \"Last 24 hours\",\n};\n\n/** Height of the plot, in viewBox units. */\nexport const PLOT_HEIGHT = 120;\n\n/** Gap between stacked segments, in viewBox units. */\nexport const SEGMENT_GAP = 2;\n\n/** Formats a bucket's minute in the browser's time zone. */\nconst minuteFormat = new Intl.DateTimeFormat(undefined, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n});\n\n/** `\"12:04\"` for a bucket start. */\nexport function formatMinute(at: number): string {\n return minuteFormat.format(new Date(at));\n}\n\n/** Reads the window from its URL value, falling back to the default. */\nexport function parseWindowParam(raw: string | null): ThroughputWindow {\n const value = Number(raw);\n return (\n THROUGHPUT_WINDOWS.find((window) => window === value) ?? DEFAULT_WINDOW\n );\n}\n",
|
|
13
|
+
"import type { QueueThroughputDto } from \"../../../api/types\";\nimport { keepPreviousData, useQuery } from \"@tanstack/react-query\";\nimport { useId } from \"react\";\nimport { getThroughput, queueKeys } from \"../../../api/queues\";\nimport { Select } from \"../../../components/inputs\";\nimport { ProblemBanner } from \"../../../components/ProblemBanner\";\nimport { Spinner } from \"../../../components/Spinner\";\nimport { useApiClient } from \"../../../context\";\nimport { formatNumber } from \"../../../format\";\nimport { useRefreshInterval } from \"../live\";\nimport { useUrlParams } from \"../urlState\";\nimport {\n DEFAULT_WINDOW,\n formatMinute,\n parseWindowParam,\n PLOT_HEIGHT,\n SEGMENT_GAP,\n THROUGHPUT_WINDOWS,\n WINDOW_LABELS,\n} from \"./throughput\";\n\n/** Props of {@link ThroughputPanel}. */\nexport interface ThroughputPanelProps {\n /** The queue charted. */\n queue: string;\n}\n\n/** `GET /queues/:queue/throughput`: completed and failed per minute, with a window picker (in the URL as `window`). */\nexport function ThroughputPanel({ queue }: ThroughputPanelProps) {\n const api = useApiClient();\n const [params, update] = useUrlParams();\n const minutes = parseWindowParam(params.get(\"window\"));\n const selectId = useId();\n const refetchInterval = useRefreshInterval(\"throughput\");\n const throughput = useQuery({\n queryKey: queueKeys.throughput(queue, minutes),\n queryFn: ({ signal }) => getThroughput(api, queue, minutes, signal),\n refetchInterval,\n placeholderData: keepPreviousData,\n });\n return (\n <div className=\"throughput-panel\">\n <div className=\"throughput-controls\">\n <label htmlFor={selectId}>Window</label>\n <Select\n id={selectId}\n options={THROUGHPUT_WINDOWS.map((window) => ({\n value: String(window),\n label: WINDOW_LABELS[window],\n }))}\n value={String(minutes)}\n onChange={(value) =>\n update({ window: value === String(DEFAULT_WINDOW) ? null : value })\n }\n />\n </div>\n {throughput.isPending ? (\n <Spinner\n label=\"Loading throughput\"\n showLabel\n />\n ) : throughput.isError ? (\n <ProblemBanner\n error={throughput.error}\n title=\"Could not load throughput\"\n onRetry={() => void throughput.refetch()}\n />\n ) : (\n <ThroughputChart\n data={throughput.data}\n label={`${queue}: jobs per minute, ${WINDOW_LABELS[minutes].toLowerCase()}`}\n />\n )}\n </div>\n );\n}\n\n/** Props of {@link ThroughputChart}. */\nexport interface ThroughputChartProps {\n /** The buckets, oldest first. */\n data: QueueThroughputDto;\n /** Accessible name of the chart. */\n label: string;\n}\n\n/**\n * Stacked bars, completed below failed, one per minute. The SVG is a picture\n * (`role=\"img\"` with a summary); the numbers are in a visually hidden table\n * beside it, and each bar carries a `<title>` for hover.\n */\nexport function ThroughputChart({ data, label }: ThroughputChartProps) {\n const { buckets } = data;\n const peak = Math.max(1, ...buckets.map((b) => b.completed + b.failed));\n const width = Math.max(1, buckets.length) * 10;\n const scale = (value: number) => (value / peak) * PLOT_HEIGHT;\n const summary = `${label}: ${formatNumber(data.completed)} completed, ${formatNumber(data.failed)} failed, peak ${formatNumber(peak)} per minute.`;\n return (\n <figure className=\"throughput-chart\">\n <div\n className=\"throughput-legend\"\n aria-hidden=\"true\"\n >\n <span>\n <span className=\"swatch swatch-completed\" /> Completed{\" \"}\n {formatNumber(data.completed)}\n </span>\n <span>\n <span className=\"swatch swatch-failed\" /> Failed{\" \"}\n {formatNumber(data.failed)}\n </span>\n <span className=\"muted\">Peak {formatNumber(peak)}/min</span>\n </div>\n <svg\n role=\"img\"\n aria-label={summary}\n viewBox={`0 0 ${width} ${PLOT_HEIGHT}`}\n preserveAspectRatio=\"none\"\n className=\"throughput-svg\"\n >\n <line\n x1={0}\n x2={width}\n y1={PLOT_HEIGHT}\n y2={PLOT_HEIGHT}\n className=\"throughput-baseline\"\n />\n {buckets.map((bucket, index) => {\n const x = index * 10 + 1;\n const completed = scale(bucket.completed);\n const failed = scale(bucket.failed);\n const gap = completed > 0 && failed > 0 ? SEGMENT_GAP : 0;\n return (\n <g key={bucket.at}>\n <title>{`${formatMinute(bucket.at)}: ${bucket.completed} completed, ${bucket.failed} failed`}</title>\n <rect\n x={x - 1}\n y={0}\n width={10}\n height={PLOT_HEIGHT}\n className=\"throughput-hit\"\n />\n {completed > 0 && (\n <rect\n x={x}\n y={PLOT_HEIGHT - completed}\n width={8}\n height={completed}\n rx={1}\n className=\"bar-completed\"\n />\n )}\n {failed > 0 && (\n <rect\n x={x}\n y={PLOT_HEIGHT - completed - gap - failed}\n width={8}\n height={failed}\n rx={1}\n className=\"bar-failed\"\n />\n )}\n </g>\n );\n })}\n </svg>\n {buckets.length > 0 && (\n <figcaption\n className=\"throughput-axis\"\n aria-hidden=\"true\"\n >\n <span>{formatMinute(buckets[0].at)}</span>\n <span>{formatMinute(buckets.at(-1)!.at)}</span>\n </figcaption>\n )}\n <table className=\"visually-hidden\">\n <caption>{label}</caption>\n <thead>\n <tr>\n <th scope=\"col\">Minute</th>\n <th scope=\"col\">Completed</th>\n <th scope=\"col\">Failed</th>\n </tr>\n </thead>\n <tbody>\n {buckets.map((bucket) => (\n <tr key={bucket.at}>\n <th scope=\"row\">{formatMinute(bucket.at)}</th>\n <td>{bucket.completed}</td>\n <td>{bucket.failed}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </figure>\n );\n}\n",
|
|
14
|
+
"import type { WorkerDto } from \"../../api/types\";\n\n/**\n * Whether a queue's live workers leave its housekeeping to nobody.\n *\n * `WorkerDto.sweeps` says whether a worker takes part in the queue's minute\n * pass — pruning expired results, healing repeat series, sweeping stale queue\n * state — which its `maintenance` option decides. It says nothing about\n * liveness: promoting delayed jobs, recovering stalled ones and healing flows\n * happen on every worker and cannot be turned off, so a queue nobody sweeps\n * still runs. It only accumulates what nobody tidies.\n *\n * **The verb is queue-level on purpose.** Once the pass is leased (bun-jobs\n * #108), one `sweeps: true` worker holds the lease per pass and the rest stand\n * down, so saying a *worker* \"runs the sweeps\" is wrong most of the time —\n * `true` is a capability, never a live activity, and a per-worker label must\n * read \"takes part in housekeeping\". Of a *queue* the plainer verb stays\n * exactly true: if nobody arms the timer, nobody contends, and nothing is\n * swept. That is why the note below says \"no live worker … runs housekeeping\"\n * rather than making an operator parse a lease to learn their expired jobs are\n * piling up.\n *\n * **Absent is not `false`.** A worker too old to report the field has said\n * nothing, so a queue whose live workers all omit it must not be warned\n * about: otherwise every fleet mid-upgrade reads as broken the moment this UI\n * is deployed against older workers.\n */\n\n/** What the panel says when a queue's housekeeping has no taker. */\nexport interface SweepWarning {\n /** The sentence to show. */\n message: string;\n /**\n * Whether some live worker is too old to say. The warning is then a\n * possibility rather than a certainty: an older worker may be sweeping\n * unseen.\n */\n uncertain: boolean;\n}\n\n/** Said whatever the certainty: the queue is untidy, not stuck. */\nconst CONSEQUENCE =\n \"Jobs still run — delayed jobs are promoted and stalled ones recovered — but expired results, repeat series and stale queue state are not swept.\";\n\n/**\n * The warning for a queue's live workers, or `null` when there is nothing to\n * say — some worker sweeps, nobody has reported either way, or there are no\n * workers at all.\n *\n * @param workers The queue's live workers, as `GET /queues/:queue/workers`\n * returns them.\n */\nexport function sweepWarning(\n workers: readonly WorkerDto[],\n): SweepWarning | null {\n const reported = workers.filter((worker) => worker.sweeps !== undefined);\n // Nobody has said anything: an older fleet, not a misconfigured one.\n if (reported.length === 0) {\n return null;\n }\n if (reported.some((worker) => worker.sweeps === true)) {\n return null;\n }\n const uncertain = reported.length < workers.length;\n return {\n message: uncertain\n ? `No live worker on this queue reports that it runs housekeeping, and some are too old to say — so there may be nobody doing it. ${CONSEQUENCE}`\n : `No live worker on this queue runs housekeeping. ${CONSEQUENCE}`,\n uncertain,\n };\n}\n",
|
|
15
|
+
"import { useQuery } from \"@tanstack/react-query\";\nimport { listQueueWorkers, queueKeys } from \"../../../api/queues\";\nimport { EmptyState } from \"../../../components/EmptyState\";\nimport { ProblemBanner } from \"../../../components/ProblemBanner\";\nimport { Spinner } from \"../../../components/Spinner\";\nimport { useApiClient } from \"../../../context\";\nimport { useCan } from \"../../../meta/hooks\";\nimport { useQueueWorkersLive } from \"../../workers/live\";\nimport { useWorkerPagesRouted } from \"../../workers/routed\";\nimport { sweepWarning } from \"../../workers/sweeps\";\nimport { WorkerTable } from \"../../workers/WorkerTable\";\nimport { useRefreshInterval } from \"../live\";\n\n/**\n * Workers per page in a queue's panel.\n *\n * The same 25 as a server's section on the Workers page: it is the same kind\n * of list — live workers, one row each — so it pages the same way wherever it\n * is read. One queue rarely has that many workers, so the panel usually shows\n * no pager.\n */\nconst WORKER_PAGE_SIZE = 25;\n\n/** Props of {@link WorkersPanel}. */\nexport interface WorkersPanelProps {\n /** The queue whose live workers are listed. */\n queue: string;\n}\n\n/** `GET /queues/:queue/workers`: live workers, with host/pid when the API exposes them. */\nexport function WorkersPanel({ queue }: WorkersPanelProps) {\n const api = useApiClient();\n const refetchInterval = useRefreshInterval(\"workers\");\n // The server authorizes a worker event as its queue's, so the subscription\n // follows the same permission the queue screen's own subscription does.\n useQueueWorkersLive(queue, useCan(\"queues.read\"));\n const linkKeys = useWorkerPagesRouted();\n const workers = useQuery({\n queryKey: queueKeys.workers(queue),\n queryFn: ({ signal }) => listQueueWorkers(api, queue, signal),\n refetchInterval,\n });\n if (workers.isPending) {\n return (\n <Spinner\n label=\"Loading workers\"\n showLabel\n />\n );\n }\n if (workers.isError) {\n return (\n <ProblemBanner\n error={workers.error}\n title=\"Could not load workers\"\n onRetry={() => void workers.refetch()}\n />\n );\n }\n const items = workers.data.items;\n if (items.length === 0) {\n return (\n <EmptyState\n title=\"No live workers\"\n description=\"No worker has reported for this queue recently.\"\n />\n );\n }\n // A queue's panel names the queue already; hosts show when the API exposes them.\n const showHost = items.some(\n (worker) => worker.host !== undefined || worker.pid !== undefined,\n );\n const untidy = sweepWarning(items);\n return (\n <>\n {untidy && (\n <p\n className=\"workers-sweep-note\"\n role=\"note\"\n data-testid=\"sweep-warning\"\n data-uncertain={untidy.uncertain ? \"true\" : \"false\"}\n >\n {untidy.message}\n </p>\n )}\n <WorkerTable\n workers={items}\n label={`Workers of ${queue}`}\n showQueue={false}\n showHost={showHost}\n linkKeys={linkKeys}\n pageSize={WORKER_PAGE_SIZE}\n />\n </>\n );\n}\n",
|
|
16
|
+
"import type { CleanQueueBody, RetryAllBody } from \"../../api/types\";\nimport type { DurationUnit } from \"./duration\";\nimport { useState } from \"react\";\nimport { isApiError } from \"../../api/errors\";\nimport {\n cleanQueue,\n drainQueue,\n mutationInvalidations,\n pauseQueue,\n resumeQueue,\n retryAllJobs,\n} from \"../../api/queues\";\nimport { Button } from \"../../components/Button\";\nimport { ConfirmDialog } from \"../../components/ConfirmDialog\";\nimport { Field } from \"../../components/Field\";\nimport {\n Checkbox,\n NumberInput,\n Select,\n TextInput,\n} from \"../../components/inputs\";\nimport { useToast } from \"../../components/toast\";\nimport { useApiClient } from \"../../context\";\nimport { formatNumber, plural, STATE_LABELS } from \"../../format\";\nimport { useApiMutation } from \"../../hooks/useApiMutation\";\nimport { useMeta } from \"../../meta/hooks\";\nimport { toMs } from \"./duration\";\nimport { useCanMutate } from \"./gating\";\nimport { listIds } from \"./queueFormat\";\n\n/** The states `clean` accepts. */\nconst CLEAN_STATES: readonly CleanQueueBody[\"state\"][] = [\n \"completed\",\n \"failed\",\n \"dead\",\n \"waiting\",\n \"delayed\",\n \"waiting-children\",\n];\n\n/** The states `retry-all` accepts. */\nconst RETRY_ALL_STATES: readonly RetryAllBody[\"state\"][] = [\n \"failed\",\n \"dead\",\n \"completed\",\n];\n\n/** Units offered for the clean age. */\nconst UNIT_OPTIONS: readonly { value: DurationUnit; label: string }[] = [\n { value: \"minutes\", label: \"minutes\" },\n { value: \"hours\", label: \"hours\" },\n { value: \"days\", label: \"days\" },\n];\n\n/** Which dialog is open. */\ntype OpenDialog = \"drain\" | \"clean\" | \"retryAll\" | null;\n\n/** Props of {@link QueueActions}. */\nexport interface QueueActionsProps {\n /** The queue acted on. */\n queue: string;\n /** Whether it is paused (picks Pause or Resume); `undefined` while unknown. */\n paused: boolean | undefined;\n}\n\n/**\n * The queue-wide actions: pause/resume, drain, clean and retry-all, each\n * present only when permitted and the API is not read-only. The destructive\n * ones confirm by typing the queue name.\n */\nexport function QueueActions({ queue, paused }: QueueActionsProps) {\n const api = useApiClient();\n const canMutate = useCanMutate();\n const [open, setOpen] = useState<OpenDialog>(null);\n const close = () => setOpen(null);\n const invalidate = mutationInvalidations(queue);\n\n const pause = useApiMutation({\n mutationFn: () => pauseQueue(api, queue),\n successMessage: `Paused ${queue}`,\n errorTitle: `Could not pause ${queue}`,\n invalidate,\n });\n const resume = useApiMutation({\n mutationFn: () => resumeQueue(api, queue),\n successMessage: `Resumed ${queue}`,\n errorTitle: `Could not resume ${queue}`,\n invalidate,\n });\n\n const showPause = paused === false && canMutate(\"queues.pause\");\n const showResume = paused === true && canMutate(\"queues.resume\");\n const showDrain = canMutate(\"queues.drain\");\n const showClean = canMutate(\"queues.clean\");\n const showRetryAll = canMutate(\"jobs.retryAll\");\n if (!showPause && !showResume && !showDrain && !showClean && !showRetryAll) {\n return null;\n }\n\n return (\n <div\n className=\"queue-actions\"\n role=\"group\"\n aria-label=\"Queue actions\"\n >\n {showPause && (\n <Button\n onClick={() => pause.mutate()}\n disabled={pause.isPending}\n aria-busy={pause.isPending || undefined}\n >\n Pause\n </Button>\n )}\n {showResume && (\n <Button\n variant=\"primary\"\n onClick={() => resume.mutate()}\n disabled={resume.isPending}\n aria-busy={resume.isPending || undefined}\n >\n Resume\n </Button>\n )}\n {showRetryAll && (\n <Button onClick={() => setOpen(\"retryAll\")}>Retry all…</Button>\n )}\n {showClean && (\n <Button\n variant=\"danger\"\n onClick={() => setOpen(\"clean\")}\n >\n Clean…\n </Button>\n )}\n {showDrain && (\n <Button\n variant=\"danger\"\n onClick={() => setOpen(\"drain\")}\n >\n Drain…\n </Button>\n )}\n {open === \"drain\" && (\n <DrainDialog\n queue={queue}\n onClose={close}\n />\n )}\n {open === \"clean\" && (\n <CleanDialog\n queue={queue}\n onClose={close}\n />\n )}\n {open === \"retryAll\" && (\n <RetryAllDialog\n queue={queue}\n onClose={close}\n />\n )}\n </div>\n );\n}\n\n/** Props of one action dialog. */\ninterface ActionDialogProps {\n /** The queue acted on. */\n queue: string;\n /** Closes the dialog. */\n onClose: () => void;\n}\n\n/** `POST /queues/:queue/drain`, with the `delayed` option. */\nfunction DrainDialog({ queue, onClose }: ActionDialogProps) {\n const api = useApiClient();\n const [delayed, setDelayed] = useState(false);\n const drain = useApiMutation({\n mutationFn: (withDelayed: boolean) => drainQueue(api, queue, withDelayed),\n successMessage: (result) =>\n `Drained ${plural(result.count, \"job\")} from ${queue}`,\n invalidate: mutationInvalidations(queue),\n toastErrors: false,\n });\n return (\n <ConfirmDialog\n open\n onClose={onClose}\n title={`Drain ${queue}?`}\n description=\"Removes every waiting job (and, if chosen, every delayed one). Running jobs are not touched. This cannot be undone.\"\n variant=\"danger\"\n confirmLabel=\"Drain\"\n pendingLabel=\"Draining…\"\n confirmText={queue}\n onConfirm={() => drain.mutateAsync(delayed)}\n >\n <Checkbox\n checked={delayed}\n onChange={setDelayed}\n label=\"Also remove delayed jobs\"\n />\n </ConfirmDialog>\n );\n}\n\n/** `POST /queues/:queue/clean`: a state, an age and a limit. */\nfunction CleanDialog({ queue, onClose }: ActionDialogProps) {\n const api = useApiClient();\n const { limits } = useMeta();\n const toast = useToast();\n const [state, setState] = useState<CleanQueueBody[\"state\"]>(\"completed\");\n const [age, setAge] = useState<number | undefined>(1);\n const [unit, setUnit] = useState<DurationUnit>(\"days\");\n const [limit, setLimit] = useState<number | undefined>(limits.defaultClean);\n const clean = useApiMutation({\n mutationFn: (body: CleanQueueBody) => cleanQueue(api, queue, body),\n onSuccess: (result) => {\n toast.success(`Cleaned ${plural(result.count, \"job\")} from ${queue}`, {\n description:\n result.ids.length > 0 ? `Removed: ${listIds(result.ids)}` : undefined,\n });\n },\n invalidate: mutationInvalidations(queue),\n toastErrors: false,\n });\n const olderThan = toMs(age, unit);\n const limitValid =\n limit !== undefined &&\n Number.isInteger(limit) &&\n limit >= 1 &&\n limit <= limits.maxClean;\n const fieldError = (key: string) => clean.fieldErrors[key];\n return (\n <ConfirmDialog\n open\n onClose={onClose}\n title={`Clean ${queue}?`}\n description=\"Removes jobs in one state that are older than the age you give, up to the limit. This cannot be undone.\"\n variant=\"danger\"\n confirmLabel=\"Clean\"\n pendingLabel=\"Cleaning…\"\n confirmText={queue}\n confirmDisabled={olderThan === undefined || !limitValid}\n onConfirm={() =>\n clean.mutateAsync({ state, olderThan: olderThan ?? 0, limit })\n }\n >\n <Field\n label=\"State\"\n error={fieldError(\"state\")}\n >\n <Select\n options={CLEAN_STATES.map((value) => ({\n value,\n label: STATE_LABELS[value],\n }))}\n value={state}\n onChange={setState}\n />\n </Field>\n <div className=\"field-row\">\n <Field\n label=\"Older than\"\n error={fieldError(\"olderThan\")}\n required\n >\n <NumberInput\n value={age}\n onChange={setAge}\n min={0}\n step=\"any\"\n />\n </Field>\n <Field label=\"Unit\">\n <Select\n options={UNIT_OPTIONS}\n value={unit}\n onChange={setUnit}\n />\n </Field>\n </div>\n <Field\n label=\"Limit\"\n hint={`At most ${formatNumber(limits.maxClean)}.`}\n error={\n fieldError(\"limit\") ??\n (limitValid\n ? undefined\n : `Between 1 and ${formatNumber(limits.maxClean)}.`)\n }\n >\n <NumberInput\n value={limit}\n onChange={setLimit}\n min={1}\n max={limits.maxClean}\n />\n </Field>\n </ConfirmDialog>\n );\n}\n\n/** `POST /queues/:queue/jobs/retry-all`: synchronous, so the dialog stays pending until it answers. */\nfunction RetryAllDialog({ queue, onClose }: ActionDialogProps) {\n const api = useApiClient();\n const { limits } = useMeta();\n const toast = useToast();\n const [state, setState] = useState<RetryAllBody[\"state\"]>(\"failed\");\n const [name, setName] = useState(\"\");\n const [reason, setReason] = useState(\"\");\n const [limit, setLimit] = useState<number | undefined>(limits.maxRetryAll);\n const [resetAttempts, setResetAttempts] = useState(true);\n const retryAll = useApiMutation({\n mutationFn: (body: RetryAllBody) => retryAllJobs(api, queue, body),\n onSuccess: (result) => {\n toast.success(`Retried ${plural(result.count, \"job\")} in ${queue}`, {\n description:\n result.ids.length === 0\n ? undefined\n : `${listIds(result.ids)}${result.truncated ? ` (ids of the first ${limits.maxRetryAllIds} only)` : \"\"}`,\n });\n },\n invalidate: mutationInvalidations(queue),\n toastErrors: false,\n });\n const limitValid =\n limit !== undefined &&\n Number.isInteger(limit) &&\n limit >= 1 &&\n limit <= limits.maxRetryAll;\n const inProgress =\n isApiError(retryAll.error) &&\n retryAll.error.code === \"OPERATION_IN_PROGRESS\";\n const fieldError = (key: string) => retryAll.fieldErrors[key];\n return (\n <ConfirmDialog\n open\n onClose={onClose}\n title={`Retry every ${STATE_LABELS[state].toLowerCase()} job in ${queue}?`}\n description=\"Moves matching jobs back to waiting. It runs to completion before answering, which can take a while on a large queue.\"\n confirmLabel=\"Retry all\"\n pendingLabel=\"Retrying… this can take a while\"\n confirmText={queue}\n confirmDisabled={!limitValid}\n onConfirm={() =>\n retryAll.mutateAsync({\n state,\n name: name.trim() || undefined,\n reason: reason.trim() || undefined,\n limit,\n resetAttempts,\n })\n }\n >\n <Field\n label=\"State\"\n error={fieldError(\"state\")}\n >\n <Select\n options={RETRY_ALL_STATES.map((value) => ({\n value,\n label: STATE_LABELS[value],\n }))}\n value={state}\n onChange={setState}\n />\n </Field>\n <Field\n label=\"Job name\"\n hint=\"Optional: only jobs with exactly this name.\"\n error={fieldError(\"name\")}\n >\n <TextInput\n value={name}\n onChange={setName}\n maxLength={200}\n />\n </Field>\n <Field\n label=\"Failure reason contains\"\n hint=\"Optional: a plain substring of “ErrorName: message”.\"\n error={fieldError(\"reason\")}\n >\n <TextInput\n value={reason}\n onChange={setReason}\n maxLength={500}\n />\n </Field>\n <Field\n label=\"Limit\"\n hint={`At most ${formatNumber(limits.maxRetryAll)}.`}\n error={\n fieldError(\"limit\") ??\n (limitValid\n ? undefined\n : `Between 1 and ${formatNumber(limits.maxRetryAll)}.`)\n }\n >\n <NumberInput\n value={limit}\n onChange={setLimit}\n min={1}\n max={limits.maxRetryAll}\n />\n </Field>\n <Checkbox\n checked={resetAttempts}\n onChange={setResetAttempts}\n label=\"Reset attempts\"\n />\n {inProgress && (\n <p\n className=\"notice\"\n role=\"note\"\n data-testid=\"retry-all-in-progress\"\n >\n A retry-all is already running on this queue. Only one runs per queue\n at a time; wait for it to finish, then try again.\n </p>\n )}\n </ConfirmDialog>\n );\n}\n",
|
|
17
|
+
"import type { ReactNode } from \"react\";\nimport type { QueueLimitsDto } from \"../../api/types\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useRef, useState } from \"react\";\nimport { JOB_STATES } from \"../../api/contract\";\nimport { getQueue, getQueueCounts, queueKeys } from \"../../api/queues\";\nimport { getSummonStatus, summonKeys } from \"../../api/summon\";\nimport { Badge } from \"../../components/Badge\";\nimport { Button } from \"../../components/Button\";\nimport { Card } from \"../../components/Card\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { ErrorView } from \"../../components/ErrorView\";\nimport { RelativeTime } from \"../../components/RelativeTime\";\nimport { Spinner } from \"../../components/Spinner\";\nimport { UrlTabs } from \"../../components/Tabs\";\nimport { useApiClient } from \"../../context\";\nimport { formatNumber } from \"../../format\";\nimport { useUrlTab } from \"../../hooks/useUrlTab\";\nimport { useCan, useMeta, usePermissionsSettled } from \"../../meta/hooks\";\nimport { Link } from \"../../router\";\nimport { useParams } from \"../../routing\";\nimport { AddFlowDialog, AddJobDialog } from \"../job\";\nimport { JobDefaultsPanel } from \"../lazy\";\nimport { canAddJobs, useCanMutate } from \"./gating\";\nimport { JobsTable } from \"./JobsTable\";\nimport { useQueueLive, useRefreshInterval } from \"./live\";\nimport { DemandPanel } from \"./panels/DemandPanel\";\nimport { useJobDefaultsGate } from \"./panels/jobDefaultsGate\";\nimport { LimitsPanel } from \"./panels/LimitsPanel\";\nimport { RepeatablesPanel } from \"./panels/RepeatablesPanel\";\nimport { SummonPanel } from \"./panels/SummonPanel\";\nimport { ThroughputPanel } from \"./panels/ThroughputPanel\";\nimport { WorkersPanel } from \"./panels/WorkersPanel\";\nimport { QueueActions } from \"./QueueActions\";\nimport \"./queues.css\";\n\n/** A panel below the jobs table. */\nexport type PanelId =\n | \"limits\"\n | \"job-defaults\"\n | \"workers\"\n | \"demand\"\n | \"summon\"\n | \"throughput\"\n | \"repeatables\";\n\n/** `/queues/:queue`: header and actions, the jobs table, and the panels. */\nexport function QueueScreen() {\n const { queue = \"\" } = useParams<{ queue: string }>();\n const api = useApiClient();\n const meta = useMeta();\n const canRead = useCan(\"queues.read\");\n const canListJobs = useCan(\"jobs.list\");\n const canAddAction = useCan(\"jobs.add\");\n const canRepeatables = useCan(\"repeatables.list\");\n const settled = usePermissionsSettled();\n const detailInterval = useRefreshInterval(\"detail\");\n const countsInterval = useRefreshInterval(\"counts\");\n // Subscribes once the queue's own permissions have answered, and only\n // while its detail is readable.\n useQueueLive(queue, {\n enabled: canRead && settled,\n jobs: canListJobs,\n repeatables: canRepeatables,\n });\n const [adding, setAdding] = useState(false);\n const addButtonRef = useRef<HTMLButtonElement>(null);\n const [addingFlow, setAddingFlow] = useState(false);\n const addFlowButtonRef = useRef<HTMLButtonElement>(null);\n\n const detail = useQuery({\n queryKey: queueKeys.detail(queue),\n queryFn: ({ signal }) => getQueue(api, queue, signal),\n refetchInterval: detailInterval,\n enabled: canRead,\n });\n const counts = useQuery({\n queryKey: queueKeys.counts(queue),\n queryFn: ({ signal }) => getQueueCounts(api, queue, signal),\n refetchInterval: countsInterval,\n enabled: canRead,\n });\n\n if (canRead && detail.isError && !detail.data) {\n return (\n <div className=\"screen\">\n <ErrorView\n headingLevel={1}\n error={detail.error}\n title={`Could not load the queue ${queue}`}\n onRetry={() => void detail.refetch()}\n />\n </div>\n );\n }\n\n const canAdd = canAddJobs(meta, canAddAction);\n // `features.addFlow` is the backend's capability alone: `jobs.add` (opt-in,\n // pruned under readOnly) is what `canAddJobs` checks.\n const canAddFlow = canAdd && meta.features.addFlow;\n const total = counts.data\n ? JOB_STATES.reduce((sum, state) => sum + counts.data[state], 0)\n : detail.data?.total;\n\n return (\n <div\n className=\"screen queue-screen\"\n data-testid=\"queue-screen\"\n >\n <nav\n aria-label=\"Breadcrumb\"\n className=\"breadcrumb\"\n >\n <Link to=\"/queues\">Queues</Link>\n <span aria-hidden=\"true\"> / </span>\n </nav>\n <header className=\"queue-header\">\n <div className=\"queue-heading\">\n <h1 className=\"screen-title\">{queue}</h1>\n {detail.data?.paused && <Badge tone=\"warning\">Paused</Badge>}\n {total !== undefined && (\n <span\n className=\"queue-total\"\n data-testid=\"queue-total\"\n >\n {formatNumber(total)} jobs\n </span>\n )}\n {counts.dataUpdatedAt > 0 && (\n <span className=\"muted queue-updated\">\n Updated <RelativeTime value={counts.dataUpdatedAt} />\n </span>\n )}\n </div>\n <div className=\"queue-toolbar\">\n {canAdd && (\n <Button\n ref={addButtonRef}\n variant=\"primary\"\n onClick={() => setAdding(true)}\n >\n Add job\n </Button>\n )}\n {canAddFlow && (\n <Button\n ref={addFlowButtonRef}\n onClick={() => setAddingFlow(true)}\n >\n Add flow\n </Button>\n )}\n <QueueActions\n queue={queue}\n paused={detail.data?.paused}\n />\n </div>\n </header>\n {canAdd && (\n <AddJobDialog\n queue={queue}\n open={adding}\n onClose={() => {\n setAdding(false);\n addButtonRef.current?.focus();\n }}\n />\n )}\n {canAddFlow && addingFlow && (\n <AddFlowDialog\n queue={queue}\n open\n onClose={() => {\n setAddingFlow(false);\n addFlowButtonRef.current?.focus();\n }}\n />\n )}\n {canListJobs ? (\n <Card title=\"Jobs\">\n <JobsTable\n queue={queue}\n counts={counts.data}\n />\n </Card>\n ) : (\n <EmptyState\n title=\"Jobs hidden\"\n description=\"You may not list this queue's jobs.\"\n />\n )}\n <QueuePanels\n queue={queue}\n limits={detail.data ? detail.data.limits : undefined}\n detailLoading={canRead && detail.isPending}\n />\n </div>\n );\n}\n\n/** Props of {@link QueuePanels}. */\ninterface QueuePanelsProps {\n /** The queue. */\n queue: string;\n /** `QueueDetailDto.limits`: `undefined` when the backend cannot store limits (the key is absent) or the detail is not loaded. */\n limits: QueueLimitsDto | null | undefined;\n /** Whether the detail is still loading (the limits panel waits for it). */\n detailLoading: boolean;\n}\n\n/** The panels the backend and the caller allow; a pruned one is absent, not disabled. */\nfunction QueuePanels({ queue, limits, detailLoading }: QueuePanelsProps) {\n const meta = useMeta();\n const canMutate = useCanMutate();\n const canWorkers = useCan(\"workers.list\");\n const canRead = useCan(\"queues.read\");\n const canMetrics = useCan(\"metrics.read\");\n const canRepeatables = useCan(\"repeatables.list\");\n const jobDefaults = useJobDefaultsGate();\n const api = useApiClient();\n // `/meta` says nothing about summoning: the status read itself decides. A\n // queue with no summoner in the API's process reads as `null` (409\n // `SUMMON_NOT_CONFIGURED`) and gets no tab; the panel reuses this read.\n const summon = useQuery({\n queryKey: summonKeys.status(api, queue),\n queryFn: ({ signal }) => getSummonStatus(api, queue, signal),\n enabled: canRead,\n retry: false,\n });\n\n const panels: { value: PanelId; label: string; render: () => ReactNode }[] =\n [];\n if (meta.features.limits && (limits !== undefined || detailLoading)) {\n panels.push({\n value: \"limits\",\n label: \"Limits\",\n render: () =>\n limits === undefined ? (\n <Spinner\n label=\"Loading limits\"\n showLabel\n />\n ) : (\n <LimitsPanel\n queue={queue}\n limits={limits}\n editable={canMutate(\"queues.limits\")}\n />\n ),\n });\n }\n if (jobDefaults.shown) {\n panels.push({\n value: \"job-defaults\",\n label: \"Job defaults\",\n render: () => (\n <JobDefaultsPanel\n queue={queue}\n canEdit={jobDefaults.canEdit}\n canApply={jobDefaults.canApply}\n />\n ),\n });\n }\n if (meta.features.workers && canWorkers) {\n panels.push({\n value: \"workers\",\n label: \"Workers\",\n render: () => <WorkersPanel queue={queue} />,\n });\n }\n // Every backend serves it (`features.demand` is false only in `runner`\n // mode); whether its figures are exact is in each answer.\n if (meta.features.demand && canRead) {\n panels.push({\n value: \"demand\",\n label: \"Demand\",\n render: () => <DemandPanel queue={queue} />,\n });\n }\n // Shown once it answered with a status, and on a failure other than \"no\n // summoner here\", which the panel then explains.\n if (canRead && (summon.isError || (summon.data ?? null) !== null)) {\n panels.push({\n value: \"summon\",\n label: \"Summon\",\n render: () => <SummonPanel queue={queue} />,\n });\n }\n if (meta.features.throughput && canMetrics) {\n panels.push({\n value: \"throughput\",\n label: \"Throughput\",\n render: () => <ThroughputPanel queue={queue} />,\n });\n }\n if (canRepeatables) {\n panels.push({\n value: \"repeatables\",\n label: \"Repeatables\",\n render: () => (\n <RepeatablesPanel\n queue={queue}\n canRemove={canMutate(\"repeatables.remove\")}\n canDisable={canMutate(\"repeatables.disable\")}\n canEnable={canMutate(\"repeatables.enable\")}\n />\n ),\n });\n }\n const first = panels[0]?.value ?? \"limits\";\n const selected = useUrlTab<PanelId>(\"panel\", panels, first);\n if (panels.length === 0) {\n return null;\n }\n const current = panels.find((panel) => panel.value === selected) ?? panels[0];\n return (\n <Card\n title=\"Details\"\n className=\"queue-panels\"\n >\n <UrlTabs\n tabs={panels.map(({ value, label }) => ({ value, label }))}\n param=\"panel\"\n defaultValue={first}\n label=\"Queue details\"\n >\n {current.render()}\n </UrlTabs>\n </Card>\n );\n}\n",
|
|
18
|
+
"import { keepPreviousData, useQuery } from \"@tanstack/react-query\";\nimport { useDeferredValue, useId } from \"react\";\nimport { listQueuesPage, queueKeys } from \"../../api/queues\";\nimport { Card } from \"../../components/Card\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { ErrorView } from \"../../components/ErrorView\";\nimport { Pager } from \"../../components/Pager\";\nimport { Spinner } from \"../../components/Spinner\";\nimport { useApiClient } from \"../../context\";\nimport { SEARCH_SHORTCUT } from \"../../layout/shortcuts\";\nimport { useCan, useMeta } from \"../../meta/hooks\";\nimport { QueueTable } from \"../Overview\";\nimport { useQueueListLive, useRefreshInterval } from \"./live\";\nimport { clampLimit, intParam, useUrlParams } from \"./urlState\";\nimport \"./queues.css\";\n\n/** `/queues`: every queue, searchable (case-insensitive) and paged; search and page live in the URL. */\nexport function QueuesListScreen() {\n const api = useApiClient();\n const { limits } = useMeta();\n const canList = useCan(\"queues.list\");\n const [params, update] = useUrlParams();\n const searchId = useId();\n\n const search = params.get(\"search\") ?? \"\";\n const deferredSearch = useDeferredValue(search);\n const limit = clampLimit(\n intParam(\n params,\n \"limit\",\n Math.min(limits.defaultPageSize, limits.maxQueues),\n ),\n limits.maxQueues,\n );\n const offset = intParam(params, \"offset\", 0);\n const query = { search: deferredSearch, offset, limit };\n const refetchInterval = useRefreshInterval(\"list\");\n useQueueListLive(canList);\n\n const queues = useQuery({\n queryKey: queueKeys.page(query),\n queryFn: ({ signal }) => listQueuesPage(api, query, signal),\n refetchInterval,\n placeholderData: keepPreviousData,\n enabled: canList,\n });\n\n if (!canList) {\n return (\n <div className=\"screen\">\n <h1 className=\"screen-title\">Queues</h1>\n <EmptyState\n title=\"Nothing to show\"\n description=\"You may not list queues on this API.\"\n />\n </div>\n );\n }\n\n return (\n <div\n className=\"screen\"\n data-testid=\"queues-list\"\n >\n <h1 className=\"screen-title\">Queues</h1>\n <Card\n title=\"Queues\"\n actions={\n <div className=\"search\">\n <label\n htmlFor={searchId}\n className=\"visually-hidden\"\n >\n Search queues by name\n </label>\n <input\n id={searchId}\n type=\"search\"\n className=\"input\"\n placeholder=\"Search queues\"\n {...SEARCH_SHORTCUT}\n value={search}\n maxLength={200}\n onChange={(event) =>\n update({ search: event.target.value, offset: null })\n }\n />\n </div>\n }\n >\n {queues.isPending ? (\n <Spinner\n label=\"Loading queues\"\n showLabel\n />\n ) : queues.isError ? (\n <ErrorView\n error={queues.error}\n title=\"Could not load queues\"\n onRetry={() => void queues.refetch()}\n />\n ) : queues.data.items.length === 0 && offset === 0 ? (\n deferredSearch ? (\n <EmptyState\n title=\"No queues match\"\n description={`No queue name contains “${deferredSearch}”.`}\n />\n ) : (\n <EmptyState\n title=\"No queues yet\"\n description=\"Queues appear here once a job is added to one.\"\n />\n )\n ) : (\n <>\n <QueueTable\n items={queues.data.items}\n sparklines={false}\n />\n <Pager\n label=\"Queue pages\"\n offset={offset}\n limit={limit}\n total={queues.data.page.total}\n itemCount={queues.data.items.length}\n hasMore={queues.data.page.hasMore}\n maxPageSize={limits.maxQueues}\n onChange={(next) =>\n update({\n offset: next.offset > 0 ? String(next.offset) : null,\n limit: String(next.limit),\n })\n }\n />\n </>\n )}\n </Card>\n </div>\n );\n}\n"
|
|
19
|
+
],
|
|
20
|
+
"mappings": "8/BAiCO,SAAS,EAAW,EAAG,SAA2B,CACvD,IAAM,EAAM,EAAa,EACnB,EAAkB,GAAmB,QAAQ,EAC7C,EAAS,EAAS,CACtB,SAAU,GAAW,MAAM,CAAK,EAChC,QAAS,EAAG,YAAa,GAAe,EAAK,EAAO,CAAM,EAC1D,kBACA,gBAAiB,CACnB,CAAC,EACD,GAAI,EAAO,UACT,OACE,EAAC,EAAD,CACE,MAAM,iBACN,UAAS,GACX,EAGJ,GAAI,EAAO,QACT,OACE,EAAC,EAAD,CACE,MAAO,EAAO,MACd,MAAM,wBACN,QAAS,IAAM,KAAK,EAAO,QAAQ,EACrC,EAGJ,OACE,EAAC,GAAD,CACE,QAAS,EAAO,KAChB,KAAM,GAAW,EAAI,KAAM,EAAO,OAAO,SAAS,MAAM,EAC1D,EAaJ,SAAS,EAAa,EAAG,UAAS,QAA4B,CAC5D,IAAM,EAAc,GAAe,KACjC,EAAG,YAAa,GAAQ,EAAS,CAAM,EAAE,WAC3C,EACA,OACE,EAAC,MAAD,CACE,UAAU,eACV,cAAY,eAFd,SA6GE,CAzGC,EAAQ,QACP,EAAC,IAAD,CACE,UAAU,SACV,KAAK,OACL,cAAY,gBAHd,oJAOE,EAEH,EAAQ,QACP,EAAC,IAAD,CACE,UAAU,SACV,KAAK,OACL,cAAY,gBAHd,yGAOE,EAEH,GACC,EAAC,IAAD,CACE,UAAU,SACV,KAAK,OACL,cAAY,qBAHd,yPASE,EAEJ,EAAC,EAAD,CACE,MAAO,CACL,GAAG,GAAe,IAAI,EAAG,SAAQ,QAAO,WAAY,CAClD,IAAK,EACL,QACA,MACE,EAAC,OAAD,CACE,UAAU,MACV,MAAO,GAAiB,EAAS,CAAM,EACvC,cAAa,UAAU,IAHzB,SAKG,GAAmB,EAAS,CAAM,EACnC,EAEJ,MACF,EAAE,EACF,CACE,IAAK,UACL,MAAO,UACP,MACE,EAAC,OAAD,CACE,UAAU,MACV,cAAY,iBAFd,SAIG,EAAa,EAAQ,OAAO,EAC7B,EAEJ,KAAM,sHACR,EACA,CACE,IAAK,YACL,MAAO,WACP,MACE,EAAC,OAAD,CAAM,cAAY,kBAAlB,SACG,EAAQ,YAAc,KACrB,iBAEA,EAAC,EAAD,CACE,MAAO,EAAQ,UACf,KACE,EAAQ,MACJ,OACA,gDAER,EAEF,EAEJ,KAAM,wDACR,EACA,CACE,IAAK,KACL,MAAO,OACP,MAAO,EAAC,EAAD,CAAc,MAAO,EAAQ,GAAI,CAC1C,CACF,EACF,EACA,EAAC,MAAD,CAAK,UAAU,cAAf,SAeE,CAdA,EAAC,IAAD,CAAG,UAAU,QAAb,2IAGE,EACF,EAAC,GAAD,CACE,MAAM,OACN,IAAK,EAAK,KACV,OAAO,kBACT,EACA,EAAC,GAAD,CACE,MAAM,aACN,IAAK,EAAK,WACV,OAAO,wBACT,GACA,GACF,EAKN,SAAS,EAAS,EAChB,QACA,MACA,UAQC,CACD,OACE,EAAC,MAAD,CAAK,UAAU,aAAf,SAOE,CANA,EAAC,OAAD,CAAM,UAAU,mBAAhB,SAAoC,EAAQ,EAC5C,EAAC,OAAD,CAAM,cAAa,EAAnB,SAA4B,EAAM,EAClC,EAAC,GAAD,CACE,KAAM,EACN,UAAW,YAAY,eACzB,GACA,EChMC,SAAS,EAAkB,EAAoB,CACpD,IAAM,EAAO,EAAQ,EACf,EAAU,EAAO,aAAa,EAC9B,EAAY,GAAa,EACzB,EAAQ,EAAK,SAAS,aAAe,EAC3C,MAAO,CACL,QACA,QAAS,GAAS,EAAU,iBAAiB,EAC7C,SACE,GACA,EAAK,SAAS,kBACd,EAAU,sBAAsB,CACpC,ECDF,SAAS,EAAO,CACd,EAMgB,CAChB,MAAO,CACL,QAAS,GAAQ,MAAM,IACvB,aAAc,GAAQ,KAAO,OAAO,EAAO,KAAK,QAAQ,EAAI,GAC5D,YAAa,GAAQ,WACvB,EAIK,SAAS,EAAe,CAAC,EAA4C,CAC1E,MAAO,CACL,MAAO,GAAQ,GAAU,MAAS,EAClC,MAAO,OAAO,QAAQ,GAAQ,OAAS,CAAC,CAAC,EAAE,IAAI,EAAE,EAAM,GAAM,KAAW,CACtE,IAAK,EACL,UACG,GAAQ,CAAG,CAChB,EAAE,CACJ,EAIF,SAAS,EAAO,CAAC,EAGf,CACA,IAAM,EAAkD,CAAC,EACnD,EAAW,GAAY,EAAI,YAAY,EAC7C,GAAI,EAAI,UAAY,QAAa,IAAa,OAC5C,EAAI,KAAO,CACT,IAAK,EAAI,SAAW,EACpB,SAAU,GAAY,CACxB,EAEF,GAAI,EAAI,cAAgB,OACtB,EAAI,YAAc,EAAI,YAExB,OAAO,EAIF,SAAS,EAAmB,CAAC,EAAqC,CACvE,IAAM,EAAwB,GAAQ,EAAM,KAAK,EAC3C,EAA+C,CAAC,EACtD,QAAW,KAAO,EAAM,MAAO,CAC7B,IAAM,EAAO,EAAI,KAAK,KAAK,EAC3B,GAAI,EACF,EAAM,GAAQ,GAAQ,CAAG,EAG7B,GAAI,OAAO,KAAK,CAAK,EAAE,OAAS,EAC9B,EAAK,MAAQ,EAEf,OAAO,EC9DF,SAAS,EAAW,EAAG,QAAO,SAAQ,YAA8B,CACzE,GAAI,CAAC,EACH,OAAO,EAAC,GAAD,CAAe,OAAQ,EAAQ,EAGxC,OACE,EAAC,GAAD,CAEE,MAAO,EACP,OAAQ,GAFH,KAAK,UAAU,CAAM,CAG5B,EAKJ,SAAS,EAAa,EAAG,UAA6C,CACpE,GAAI,CAAC,GAAU,OAAO,KAAK,CAAM,EAAE,SAAW,EAC5C,OAAO,EAAC,IAAD,CAAG,UAAU,QAAb,4CAAuD,EAEhE,IAAM,EAAW,CAAC,IAIhB,CACE,EAAI,MAAQ,GAAG,EAAI,KAAK,WAAW,EAAS,EAAI,KAAK,QAAQ,IAC7D,EAAI,cAAgB,QAAa,GAAG,EAAI,qBAC1C,EACG,OAAO,OAAO,EACd,KAAK,IAAI,GAAK,OACnB,OACE,EAAC,KAAD,CAAI,UAAU,iBAAd,SASE,CARA,EAAC,KAAD,kBAAW,EACX,EAAC,KAAD,UAAK,EAAS,CAAM,EAAI,EACvB,OAAO,QAAQ,EAAO,OAAS,CAAC,CAAC,EAAE,IAAI,EAAE,EAAM,KAC9C,EAAC,MAAD,UAGE,CAFA,EAAC,KAAD,UAAK,EAAO,EACZ,EAAC,KAAD,UAAK,EAAS,CAAG,EAAI,IAFb,CAGR,CACH,GACD,EA6BN,SAAS,EAAe,EACtB,MACA,WACA,SACA,SACA,QACA,SACuB,CACvB,IAAM,EAAQ,CAAC,IAAgB,EAAO,GAAG,IAAS,KAC5C,EACJ,EAAM,OAAS,QACX,qBACA,EAAM,KACJ,eAAe,EAAM,QACrB,sBACR,OACE,EAAC,MAAD,CAAK,UAAU,uBAAf,SAkCE,CAjCA,EAAC,EAAD,CACE,MAAO,GAAG,cACV,KAAM,YAAY,kIAClB,MAAO,EAAM,UAAU,GAAK,EAAM,MAAM,EAH1C,SAKE,EAAC,EAAD,CACE,MAAO,EAAI,QACX,SAAU,CAAC,IAAY,EAAS,IAAK,EAAK,SAAQ,CAAC,EACnD,IAAK,EACP,EACA,EACF,EAAC,EAAD,CACE,MAAO,GAAG,iBACV,KAAK,8IACL,MAAO,EAAM,eAAe,EAH9B,SAKE,EAAC,EAAD,CACE,MAAO,EAAI,aACX,SAAU,CAAC,IAAiB,EAAS,IAAK,EAAK,cAAa,CAAC,EAC7D,UAAW,IACb,EACA,EACF,EAAC,EAAD,CACE,MAAO,GAAG,iBACV,KAAM,YAAY,uMAClB,MAAO,EAAM,aAAa,EAH5B,SAKE,EAAC,EAAD,CACE,MAAO,EAAI,YACX,SAAU,CAAC,IAAgB,EAAS,IAAK,EAAK,aAAY,CAAC,EAC3D,IAAK,EACP,EACA,GACF,EAKN,SAAS,EAAY,EACnB,QACA,UAIC,CACD,IAAM,EAAM,EAAa,GAClB,EAAO,GAAY,EAAS,IAAM,GAAgB,CAAM,CAAC,GACzD,EAAe,GAAoB,EAAS,EAAK,EAClD,EAAO,EAAe,CAC1B,WAAY,CAAC,IACX,IAAS,KACL,GAAkB,EAAK,CAAK,EAC5B,GAAe,EAAK,EAAO,CAAI,EACrC,eAAgB,CAAC,EAAS,KACxB,KAAS,KACL,yBAAyB,IACzB,uBAAuB,IAC7B,WAAY,GAAsB,CAAK,EACvC,YAAa,EACf,CAAC,EACK,EAAS,CAAC,IAAuC,CACrD,GAAO,eAAe,EACtB,EAAK,OAAO,GAAoB,CAAK,CAAC,GAElC,EACJ,EAAW,EAAK,KAAK,GAAK,EAAK,MAAM,OAAS,mBAC1C,GACJ,EAAK,QACJ,OAAO,KAAK,EAAK,WAAW,EAAE,SAAW,GACxC,EAAK,eAAe,OAAS,GAC3B,EAAK,MACL,KACA,EAAU,EAAM,MAAM,OAC1B,CAAC,EAAK,KAAQ,KAAK,IAAI,EAAK,GAAI,IAAM,CAAC,EACvC,CACF,EAEA,OACE,cAiIE,CAhIA,EAAC,OAAD,CACE,UAAU,gBACV,aAAW,eACX,SAAU,EAHZ,SAsHE,CAjHA,EAAC,GAAD,CACE,IAAK,EAAM,MACX,SAAU,CAAC,IAAQ,EAAS,IAAK,EAAO,MAAO,CAAI,CAAC,EACpD,OAAO,GACP,OAAQ,EAAK,YACb,MAAM,QACN,MAAO,CAAE,KAAM,OAAQ,EACzB,EACA,EAAC,WAAD,CAAU,UAAU,eAApB,SA0EE,CAzEA,EAAC,SAAD,4BAAyB,EACxB,EAAM,MAAM,SAAW,GAAK,EAAC,IAAD,CAAG,UAAU,QAAb,iBAA4B,EACxD,EAAM,MAAM,IAAI,CAAC,EAAK,KACrB,EAAC,MAAD,CAEE,UAAU,cAFZ,SAgDE,CA5CA,EAAC,EAAD,CACE,MAAO,QAAQ,GAAQ,IACvB,KAAK,mNAFP,SAIE,EAAC,EAAD,CACE,MAAO,EAAI,KACX,SAAU,CAAC,IACT,EAAS,IACJ,EACH,MAAO,EAAM,MAAM,IAAI,CAAC,KACtB,GAAK,MAAQ,EAAI,IAAM,IAAK,GAAM,MAAK,EAAI,EAC7C,CACF,CAAC,EAEH,UAAW,IACb,EACA,EACF,EAAC,GAAD,CACE,IAAK,EACL,SAAU,CAAC,IACT,EAAS,IACJ,EACH,MAAO,EAAM,MAAM,IAAI,CAAC,KACtB,GAAK,MAAQ,EAAI,IAAM,IAAK,MAAS,CAAK,EAAI,EAChD,CACF,CAAC,EAEH,OAAQ,SAAS,EAAI,KAAK,KAAK,KAC/B,OAAQ,EAAK,YACb,MAAO,EAAI,KAAK,KAAK,GAAK,QAAQ,GAAQ,IAC1C,MAAO,CAAE,KAAM,OAAQ,KAAM,EAAI,KAAK,KAAK,CAAE,EAC/C,EACA,EAAC,EAAD,CACE,KAAK,KACL,QAAQ,QACR,QAAS,IACP,EAAS,IACJ,EACH,MAAO,EAAM,MAAM,OAAO,CAAC,IAAS,EAAK,MAAQ,EAAI,GAAG,CAC1D,CAAC,EAPL,SAWE,CAXF,UAUU,EAAI,KAAK,KAAK,GAAK,QAAQ,GAAQ,KAC3C,IA9CG,EAAI,GA+CT,CACH,EACD,EAAC,EAAD,CACE,KAAK,KACL,QAAS,IACP,EAAS,IACJ,EACH,MAAO,CACL,GAAG,EAAM,MACT,CACE,IAAK,EACL,KAAM,GACN,QAAS,OACT,aAAc,GACd,YAAa,MACf,CACF,CACF,CAAC,EAfL,sBAmBE,GACF,EACD,KAAgB,MACf,EAAC,EAAD,CACE,MAAO,GACP,MACE,EACI,wDACA,4BAEN,QAAS,EAAY,IAAM,EAAO,EAAI,OACxC,EAEF,EAAC,MAAD,CAAK,UAAU,eAAf,SAkBE,CAjBA,EAAC,EAAD,CACE,KAAK,SACL,QAAQ,UACR,SAAU,EAAK,UACf,YAAW,EAAK,WAAa,OAJ/B,SAMG,EAAK,UAAY,UAAY,cAC9B,EACD,IAAW,MACV,EAAC,EAAD,CACE,QAAQ,SACR,QAAS,IAAM,EAAiB,EAAI,EACpC,SAAU,EAAK,UAHjB,8BAME,GAEJ,GACF,EACF,EAAC,EAAD,CACE,KAAM,EACN,QAAS,IAAM,EAAiB,EAAK,EACrC,MAAO,yBAAyB,KAChC,YAAY,oFACZ,QAAQ,SACR,aAAa,gBACb,UAAW,IAAM,EAAK,YAAY,IAAI,EACxC,GACA,EC9RN,IAAM,GAAuB,GAqB7B,SAAS,EAAkB,CAAC,EAAgB,EAAsB,CAChE,GAAI,CAAC,EAAW,CAAK,GAAK,EAAM,OAAS,uBACvC,OAAO,EAET,OAAO,IAAI,EAAS,CAClB,KAAM,EAAM,KACZ,OAAQ,EAAM,OACd,KAAM,EAAM,KACZ,MAAO,EAAM,MACb,OAAQ,sBAAsB,kGAC9B,KAAM,EAAM,KACZ,OAAQ,EAAM,OACd,QAAS,EAAM,QACf,SAAU,EAAM,SAChB,MAAO,CACT,CAAC,EAII,SAAS,EAAgB,EAC9B,QACA,YACA,aAAa,GACb,YAAY,IACY,CACxB,IAAM,EAAM,EAAa,GAClB,EAAU,GAAe,EAA+B,IAAI,EAC7D,EAAkB,GAAmB,aAAa,EAClD,EAAc,EAAS,CAC3B,SAAU,GAAU,YAAY,CAAK,EACrC,QAAS,EAAG,YAAa,GAAgB,EAAK,EAAO,CAAM,EAC3D,iBACF,CAAC,EACK,EAAS,EAAe,CAC5B,WAAY,CAAC,IAAgB,GAAiB,EAAK,EAAO,CAAG,EAC7D,eAAgB,CAAC,EAAS,KAAQ,6BAA6B,KAC/D,WAAY,GAAsB,CAAK,EACvC,YAAa,EACf,CAAC,EACK,GAAS,EAAe,CAC5B,WAAY,OACV,MACA,SAIsB,CACtB,GAAI,CACF,OAAO,KAAO,UACV,MAAM,GAAkB,EAAK,EAAO,CAAG,EACvC,MAAM,GAAiB,EAAK,EAAO,CAAG,EAC1C,MAAO,GAAO,CACd,MAAM,GAAmB,GAAO,CAAG,IAGvC,eAAgB,CAAC,GAAW,OAAK,SAC/B,KAAO,UACH,8BAA8B,yCAC9B,6BAA6B,gDACnC,WAAY,qCACZ,WAAY,GAAsB,CAAK,CACzC,CAAC,EAGK,EAAQ,EAAY,MAAM,OAAS,CAAC,EACpC,EAAO,GAAc,EAAO,EAAoB,EACtD,GAAI,EAAY,UACd,OACE,EAAC,EAAD,CACE,MAAM,sBACN,UAAS,GACX,EAGJ,GAAI,EAAY,QACd,OACE,EAAC,EAAD,CACE,MAAO,EAAY,MACnB,MAAM,6BACN,QAAS,IAAM,KAAK,EAAY,QAAQ,EAC1C,EAMJ,IAAM,GACJ,GACA,EAAM,KAAK,CAAC,IAAY,EAAO,SAAW,EAAY,CAAW,EACnE,GAAI,EAAM,SAAW,EACnB,OACE,EAAC,EAAD,CACE,MAAM,iBACN,YAAY,+CACd,EAGJ,OACE,cAsIE,CArIA,EAAC,EAAD,CAAO,MAAO,kBAAkB,IAAhC,SA+GE,CA9GA,EAAC,QAAD,UACE,EAAC,KAAD,UAiBE,CAhBA,EAAC,KAAD,CAAI,MAAM,MAAV,eAAqB,EACrB,EAAC,KAAD,CAAI,MAAM,MAAV,gBAAsB,EACtB,EAAC,KAAD,CAAI,MAAM,MAAV,oBAA0B,EAC1B,EAAC,KAAD,CAAI,MAAM,MAAV,oBAA0B,EAC1B,EAAC,KAAD,CACE,MAAM,MACN,UAAU,MAFZ,gBAKE,EACF,EAAC,KAAD,CAAI,MAAM,MAAV,gBAAsB,EACrB,IACC,EAAC,KAAD,CAAI,MAAM,MAAV,SACE,EAAC,OAAD,CAAM,UAAU,kBAAhB,mBAA2C,EAC3C,GAEJ,EACF,EACF,EAAC,QAAD,UACG,EAAK,KAAK,IAAI,CAAC,IACd,EAAC,KAAD,CAEE,cAAa,kBAAkB,EAAO,MAFxC,SAqFE,CAjFA,EAAC,KAAD,CAAI,MAAM,MAAV,SAaE,CAZA,EAAC,OAAD,UAAO,EAAO,IAAM,EACnB,EAAO,UACN,cAQE,CAPC,IACD,EAAC,EAAD,CACE,KAAK,UACL,MAAM,wCAFR,oBAKE,GACF,GAEJ,EACF,EAAC,KAAD,UAAK,EAAO,KAAO,EACnB,EAAC,KAAD,UAAK,GAAiB,CAAM,EAAI,EAChC,EAAC,KAAD,CAAI,cAAa,mBAAmB,EAAO,MAA3C,SACG,EAAO,SACN,oBAEA,EAAC,EAAD,CAAc,MAAO,EAAO,UAAW,EAEzC,EACF,EAAC,KAAD,CAAI,UAAU,MAAd,SAIE,CAHC,EAAa,EAAO,KAAK,EACzB,EAAO,QAAU,QAChB,MAAM,EAAa,EAAO,KAAK,KACjC,EACF,EAAC,KAAD,CAAI,UAAU,kBAAd,SACG,EAAO,OAAS,OACf,IAEA,EAAC,GAAD,CACE,MAAO,EAAO,KACd,MAAO,WAAW,EAAO,MACzB,YAAa,EACf,EAEF,EACD,IACC,EAAC,KAAD,UACE,EAAC,MAAD,CAAK,UAAU,qBAAf,SAqCE,CApCC,GAAc,CAAC,EAAO,UACrB,EAAC,GAAD,CACE,OAAQ,EACR,GAAG,UACH,QACE,GAAO,WACP,GAAO,WAAW,MAAQ,EAAO,IAEnC,QAAS,IACP,GAAO,OAAO,CAAE,IAAK,EAAO,IAAK,GAAI,SAAU,CAAC,EAEpD,EAED,GAAa,EAAO,UACnB,EAAC,GAAD,CACE,OAAQ,EACR,GAAG,SACH,QACE,GAAO,WACP,GAAO,WAAW,MAAQ,EAAO,IAEnC,QAAS,IACP,GAAO,OAAO,CAAE,IAAK,EAAO,IAAK,GAAI,QAAS,CAAC,EAEnD,EAED,GACC,EAAC,EAAD,CACE,KAAK,KACL,QAAQ,SACR,aAAY,qBAAqB,EAAO,MACxC,QAAS,IAAM,EAAY,CAAM,EAJnC,mBAOE,GAEJ,EACF,IAlFC,EAAO,GAoFZ,CACH,EACD,GACF,EACD,EAAK,OACJ,EAAC,GAAD,CACE,MAAM,mBACN,OAAQ,EAAK,OACb,MAAO,EAAK,MACZ,MAAO,EAAK,MACZ,UAAW,EAAK,KAAK,OACrB,SAAU,EAAK,SACjB,EAEF,EAAC,EAAD,CACE,KAAM,IAAa,KACnB,QAAS,IAAM,EAAY,IAAI,EAC/B,MAAO,4BAA4B,GAAU,KAAO,MACpD,YAAY,uEACZ,QAAQ,SACR,aAAa,SACb,UAAW,IACT,EAAW,EAAO,YAAY,EAAS,GAAG,EAAI,OAElD,GACA,EAiBN,SAAS,EAAY,EAAG,SAAQ,KAAI,UAAS,WAA8B,CACzE,IAAM,EAAQ,IAAO,UAAY,UAAY,SAC7C,OACE,EAAC,EAAD,CACE,KAAK,KACL,aAAY,GAAG,gBAAoB,EAAO,MAC1C,SAAU,EACV,YAAW,GAAW,OACtB,QAAS,EALX,SAOG,EAAU,GAAG,IAAO,UAAY,YAAc,cAAgB,EAC/D,EC9RC,IAAM,GACX,sDAGW,GAA+B,qBAG/B,GACX,iHAGW,GACX,qDAGW,GACX,8CASK,SAAS,EAAsB,CACpC,EACA,EACwB,CACxB,GAAI,CAAC,EACH,MAAO,OAET,GAAI,CAAC,EAAO,MACV,MAAO,YAET,OAAO,EAAO,SAAW,OAAY,OAAS,QA6ChD,SAAS,EAAK,CACZ,EACA,EACA,EAAS,GACT,EAAQ,GACD,CACP,MAAO,CAAE,SAAQ,OAAM,OAAM,OAAM,EAIrC,IAAM,GAAqE,CACzE,MAAO,SACP,OAAQ,SACR,KAAM,MACR,EAGM,GAEF,CACF,MAAO,iBACP,OAAQ,kBACR,KAAM,eACR,EAGM,GAAmE,CACvE,aAAc,kBACd,gBAAiB,uBACnB,EAGA,SAAS,EAAuB,CAC9B,EACA,EACU,CACV,OAAO,OAAO,IAAQ,UAAY,OAAO,OAAO,EAAO,CAAG,EAI5D,SAAS,EAAc,CAAC,EAAmC,CACzD,IAAM,EAAS,OAAO,QAAQ,CAAO,EAClC,OAAO,EAAE,KAAS,IAAQ,MAAM,EAChC,IAAI,EAAE,EAAK,KAAW,GAAG,MAAQ,EAAY,CAAK,GAAG,EAClD,EAAO,EAAY,EAAQ,IAAI,EACrC,OAAO,EAAO,SAAW,EAAI,EAAO,GAAG,MAAS,EAAO,KAAK,IAAI,KAalE,SAAS,EAAa,CAAC,EAAgD,CACrE,IAAM,EAAkB,CAAC,EA2CzB,OA1CA,EAAS,QAAQ,CAAC,EAAS,IAAU,CACnC,IAAM,EAAS,EAAO,SAAW,EAAI,GAAK,MAC1C,GAAI,CAAC,GAAM,GAAgB,EAAQ,IAAI,EAAG,CACxC,EAAO,KACL,GAAM,GAAe,CAAO,EAAG,CAAC,EAAG,EAAQ,EAAQ,KAAO,EAAK,CACjE,EACA,OAEF,IAAM,EAAQ,GAAe,EAAQ,MAC/B,EAAM,GAAa,EAAQ,MACjC,GAAI,EAAQ,KAAO,GAAM,CACvB,EAAO,KAAK,GAAM,GAAG,QAAa,CAAC,CAAG,EAAG,EAAQ,EAAI,CAAC,EACtD,OAEF,GAAI,EAAQ,OAAS,OAAQ,CAC3B,EAAO,KACL,GACE,OAAO,EAAQ,QAAU,SACrB,GAAG,WAAe,EAAS,EAAQ,KAAK,IACxC,EACJ,CAAC,CAAG,EACJ,CACF,CACF,EACA,OAEF,EAAO,KAAK,GAAM,EAAO,CAAC,CAAG,EAAG,CAAM,CAAC,EACvC,IAAM,EAAW,EAAQ,SACnB,EAAW,EACd,MAAM,EAAQ,CAAC,EACf,KACC,CAAC,IACC,EAAM,KAAO,KACZ,EAAM,OAAS,SAAW,EAAM,OAAS,WAC1C,EAAM,WAAa,CACvB,EACF,GAAI,OAAO,IAAa,UAAY,CAAC,EACnC,EAAO,KACL,GAAM,cAAc,EAAS,CAAQ,KAAM,CAAC,mBAAmB,EAAG,GAAG,CACvE,EAEH,EACM,EAIF,SAAS,EAAiB,CAAC,EAA8B,CAC9D,OAAO,IAAU,KACb,0BACA,GAAG,EAAO,EAAO,KAAK,eAIrB,SAAS,EAAY,CAC1B,EACQ,CACR,GAAI,EAAS,SAAW,OACtB,MAAO,iCAET,IAAM,EACJ,OAAO,EAAS,aAAe,SAC3B,WAAW,EAAS,EAAS,UAAU,KACvC,GACN,MAAO,GAAG,EAAY,EAAS,MAAM,MAAM,EAAS,EAAS,OAAO,UAAU,IAgBhF,SAAS,EAAM,CAAC,EAAc,EAAgD,CAC5E,IAAM,EAAY,CAAC,IACjB,EAAK,OAAS,GAAK,EAAK,MAAM,CAAC,IAAQ,CAAC,EAAS,IAAI,CAAG,CAAC,EACrD,EACJ,EAAM,OAAO,OAAS,GACtB,EAAM,OAAO,MAAM,CAAC,IAAS,EAAU,EAAK,IAAI,CAAC,EACnD,MAAO,CACL,GAAI,EAAM,GACV,MAAO,EAAM,SACT,EAAM,OAAS,OAAY,CAAC,EAAI,CAAE,KAAM,EAAM,IAAK,EACvD,IAAK,EACL,MAAO,EAAM,OAAO,IAAI,CAAC,KAAU,IAC9B,EACH,MAAO,EAAK,QAAU,GACtB,IAAK,CAAC,GAAS,EAAU,EAAK,IAAI,CACpC,EAAE,CACJ,EAsBK,SAAS,EAAgB,CAC9B,EACA,EACmB,CACnB,IAAM,EAAW,IAAI,IACnB,MAAM,QAAQ,EAAO,QAAQ,EAAI,EAAO,SAAW,CAAC,CACtD,EACM,EAAQ,EAAO,QAAQ,MAoK7B,MAnKgC,CAC9B,CACE,GAAI,WACJ,MAAO,WACP,OAAQ,GAAc,EAAO,QAAQ,EACrC,KAAM,yDACR,EACA,CACE,GAAI,UACJ,MAAO,UACP,OAAQ,CACN,GAAM,WAAW,EAAa,EAAO,UAAU,IAAK,CAAC,YAAY,CAAC,EAClE,GACE,GAAkB,EAAO,aAAa,EACtC,CAAC,eAAe,EAChB,KACF,EACA,GACE,WAAW,EAAa,EAAO,UAAU,aACzC,CAAC,YAAY,EACb,KACF,CACF,EACA,KAAM,EACR,EACA,CACE,GAAI,WACJ,MAAO,WACP,OAAQ,CAAC,GAAM,EAAS,EAAO,QAAQ,EAAG,CAAC,UAAU,CAAC,CAAC,EACvD,KAAM,sCACR,EACA,CACE,GAAI,UACJ,MAAO,UACP,OAAQ,CACN,GAAM,EAAS,EAAO,QAAQ,OAAO,EAAG,CAAC,iBAAiB,CAAC,EAC3D,GACE,eAAe,EAAS,EAAO,QAAQ,GAAG,IAC1C,CAAC,aAAa,EACd,IACF,CACF,EACA,KAAM,2EACR,EACA,CACE,GAAI,UACJ,MAAO,UACP,OAAQ,CACN,GAAM,eAAe,EAAO,EAAO,QAAQ,SAAU,SAAS,IAAK,CACjE,kBACF,CAAC,EACD,GACE,OAAO,EAAS,EAAO,QAAQ,UAAU,IACzC,CAAC,oBAAoB,EACrB,IACF,CACF,EACA,KAAM,kGACR,EACA,IAAU,QAAa,CACrB,GAAI,gBACJ,MAAO,gBACP,OAAQ,CACN,GACE,eAAe,EAAO,EAAM,SAAU,SAAS,qBAC/C,CAAC,wBAAwB,CAC3B,EACA,GACE,OAAO,EAAS,EAAM,UAAU,IAChC,CAAC,0BAA0B,EAC3B,IACF,CACF,EACA,KAAM,2FACR,EACA,CACE,GAAI,cACJ,MAAO,cACP,OAAQ,CACN,OAAO,EAAO,aAAe,SACzB,GAAM,EAAS,EAAO,UAAU,EAAG,CAAC,YAAY,CAAC,EAEjD,GAAM,GAA8B,CAAC,CAAC,CAC5C,EACA,KAAM,sEACR,EACA,CACE,GAAI,WACJ,MAAO,eACP,OAAQ,CACN,GAAM,EAAS,EAAO,WAAW,EAAG,CAAC,aAAa,CAAC,EACnD,GAAI,OAAO,GAAc,gBAAkB,SACvC,CACE,GACE,gBAAgB,EAAS,EAAa,aAAa,IACnD,CAAC,EACD,KACF,CACF,EACA,CAAC,CACP,EACA,KAAM,yCACR,EACA,IAAiB,QAAa,CAC5B,GAAI,WACJ,MAAO,WACP,OAAQ,CAAC,GAAM,GAAa,EAAa,QAAQ,EAAG,CAAC,CAAC,CAAC,EACvD,KAAM,iHACR,EACA,GAAc,QAAU,SAAW,CACjC,GAAI,aACJ,MAAO,aACP,OAAQ,CACN,GAAM,iBAAiB,EAAS,EAAO,UAAU,KAAK,SAAU,CAC9D,iBACF,CAAC,CACH,CACF,EACA,CACE,GAAI,iBACJ,MAAO,iBACP,OAAQ,CAAC,GAAM,EAAS,EAAO,aAAa,EAAG,CAAC,eAAe,CAAC,CAAC,EACjE,KAAM,6CACR,EACA,CACE,GAAI,YACJ,MAAO,YACP,OAAQ,CACN,GACE,GAAM,GAAW,EAAO,QAAQ,EAC5B,GAAU,EAAO,UACjB,EAAY,EAAO,QAAQ,EAC/B,CAAC,UAAU,CACb,CACF,EACA,KAAM,gDACR,EACA,CACE,GAAI,MACJ,MAAO,MACP,OAAQ,CACN,IACK,GAAM,OAAQ,CAAC,KAAK,CAAC,EACxB,MAAO,EAAO,IAAI,IAAI,CAAC,IAAS,EAAY,CAAI,CAAC,CACnD,CACF,EACA,KAAM,gGACR,EACA,CACE,GAAI,gBACJ,MAAO,wBACP,OAAQ,CAAC,GAAM,EAAO,aAAe,MAAQ,KAAM,CAAC,cAAc,CAAC,CAAC,EACpE,KAAM,0EACR,EACA,CACE,GAAI,eACJ,MAAO,eACP,OAAQ,CACN,GAAM,EAAO,eAAiB,MAAQ,UAAW,CAAC,gBAAgB,CAAC,CACrE,EACA,KAAM,8DACR,CACF,EAEG,OAAO,CAAC,IAAsB,IAAQ,EAAK,EAC3C,IAAI,CAAC,IAAQ,GAAO,EAAK,CAAQ,CAAC,EAsBhC,SAAS,EAAoB,CAClC,EACoC,CACpC,GAAI,EAAO,WAAa,OACtB,OAEF,OAAO,OAAO,QAAQ,EAAO,QAAQ,EAAE,IAAI,EAAE,EAAO,KAAc,CAChE,IAAM,EAAM,MAAM,QAAQ,EAAS,QAAQ,EAAI,EAAS,SAAW,CAAC,EACpE,MAAO,CACL,MAAO,EAAY,CAAK,EACxB,cAAe,GAAkB,EAAS,aAAa,EACvD,WAAY,EAAa,EAAS,UAAU,EAC5C,sBAAuB,EAAI,SAAS,eAAe,EACnD,mBAAoB,EAAI,SAAS,YAAY,CAC/C,EACD,EAQI,SAAS,EAAqB,CACnC,EACS,CACT,OACE,IAAW,QACX,MAAM,QAAQ,EAAO,QAAQ,GAC7B,CAAC,EAAO,SAAS,SAAS,QAAQ,EC7Z/B,SAAS,EAAW,EAAG,SAA2B,CACvD,IAAM,EAAM,EAAa,EACnB,EAAY,GAAa,EACzB,EAAkB,GAAmB,QAAQ,GAC5C,EAAY,GAAiB,EAAoC,IAAI,GACrE,EAAO,GAAY,EAAS,EAAI,GAEhC,EAAa,GAAkB,EAAS,EAAK,EAE9C,GAAiB,GAAW,mBAAmB,IAAM,GAErD,EAAgB,GAAW,cAAc,IAAM,GAC/C,EAAS,EAAS,CACtB,SAAU,GAAW,OAAO,EAAK,CAAK,EACtC,QAAS,EAAG,aAAa,GAAgB,EAAK,EAAO,EAAM,EAC3D,kBACA,gBAAiB,CACnB,CAAC,EACK,GAAQ,GAAS,EACjB,EAAgB,GAAiB,EAAO,MAAM,MAAM,EAGpD,GAAgB,CAAC,KAAiB,CACtC,GACE,EAAO,MAAM,SAAW,QACxB,EAAW,EAAK,GAChB,GAAM,SAAW,IAEjB,GAAM,MAAM,GAAuB,CACjC,YAAa,GAAM,QAAU,GAAM,KACrC,CAAC,GAGC,GAAa,CACjB,GAAG,GAAW,QAAQ,EAAK,CAAK,EAChC,GAAW,MAAM,CAAK,EACtB,GAAU,QAAQ,CAAK,CACzB,EACM,GAAS,EAAe,CAC5B,WAAY,CAAC,KAA0B,GAAU,EAAK,EAAO,EAAY,EACzE,eAAgB,CAAC,KAAW,GAAmB,EAAM,EACrD,cACA,YAAa,GACb,QAAS,EACX,CAAC,EACK,GAAQ,EAAe,CAC3B,WAAY,CAAC,KACX,GAAY,EAAK,EAAO,GAAS,CAAE,OAAQ,EAAK,EAAI,CAAC,CAAC,EACxD,eAAgB,CAAC,GAAS,KACxB,GACI,6BAA6B,yDAC7B,6BAA6B,2CACnC,cACA,YAAa,GACb,QAAS,EACX,CAAC,EAED,GAAI,EAAO,UACT,OACE,EAAC,EAAD,CACE,MAAM,wBACN,UAAS,GACX,EAGJ,GAAI,EAAO,QACT,OACE,EAAC,EAAD,CACE,MAAO,EAAO,MACd,MAAM,mCACN,QAAS,IAAM,KAAK,EAAO,QAAQ,EACrC,EAGJ,GAAI,EAAO,OAAS,KAClB,OACE,EAAC,EAAD,CACE,MAAM,mBACN,YAAY,kEACd,EAGJ,IAAM,EAAO,EAAO,KACd,GAAa,GAAuB,EAAe,CAAI,EACvD,GAAa,GACjB,EAGA,GAAiB,EAAU,eAAe,CAC5C,EACA,OACE,EAAC,MAAD,CACE,UAAU,eACV,cAAY,eAFd,SA8HE,CA1HC,EAAK,OACJ,EAAC,IAAD,CACE,UAAU,SACV,KAAK,OACL,cAAY,eAHd,SAKG,GAAY,EAAK,WAAW,EAC7B,EAEH,CAAC,EAAK,OACL,EAAC,IAAD,CACE,UAAU,SACV,KAAK,OACL,cAAY,gBAHd,+IAOE,EAEH,EAAK,WAAa,QAAa,EAAK,SAAS,OAAS,GACrD,EAAC,IAAD,CACE,UAAU,SACV,KAAK,OACL,cAAY,kBAHd,SAKG,EAAK,SAAW,OACb,8CAA8C,EAAK,SAAS,IAAI,CAAW,EAAE,KAAK,IAAI,yFACtF,8CAA8C,EAAK,SAAS,IAAI,CAAW,EAAE,KAAK,IAAI,kGAC1F,EAEJ,EAAC,GAAD,CACE,OAAQ,EACR,MAAO,EACP,OAAQ,KAAe,QAAU,EAAK,OAAS,OACjD,EACC,EAAK,QAAU,QACd,EAAC,GAAD,CACE,MAAO,EAAK,MACZ,MAAO,EACP,KAAM,EAAK,UAAU,SAAS,KAChC,EAED,EAAK,UAAY,EAAC,GAAD,CAAU,SAAU,EAAK,SAAU,EACpD,KAAe,SAAW,EAAK,SAAW,QACzC,EAAC,GAAD,CACE,OAAQ,EAAK,OACb,aAAc,EAAK,UAAU,aAC7B,MAAO,EACT,EAED,KAAe,aACd,EAAC,IAAD,CACE,UAAU,QACV,cAAY,0BAFd,SAIG,GACD,EAEH,GAAiB,CAAI,GAAK,EAAK,QAAQ,OAAS,GAC/C,EAAC,GAAD,CACE,MAAO,EACP,OAAQ,EAAK,OACb,QAAS,EAAK,QAChB,EAED,IACC,EAAC,MAAD,CACE,UAAU,iBACV,KAAK,QACL,aAAW,iBAHb,SAqBE,CAhBA,EAAC,EAAD,CACE,QAAQ,UACR,QAAS,IAAM,EAAc,QAAQ,EACrC,SAAU,GAAO,UAHnB,uBAME,EACF,EAAC,EAAD,CACE,QAAS,IAAM,CACb,EAAe,EAAK,EACpB,EAAc,OAAO,GAEvB,SAAU,GAAM,UALlB,kBAQE,GACF,EAEJ,EAAC,EAAD,CACE,KAAM,IAAe,SACrB,QAAS,IAAM,EAAc,IAAI,EACjC,MAAO,uBAAuB,SAC9B,YAAY,2LACZ,aAAa,aACb,UAAW,IAAM,GAAO,YAAY,CAAK,EAN3C,SAQE,EAAC,EAAD,CACE,QAAS,EACT,SAAU,EACV,MAAM,oBACN,KAAK,uEACP,EACA,EACF,EAAC,EAAD,CACE,KAAM,IAAe,QACrB,QAAS,IAAM,EAAc,IAAI,EACjC,MAAO,6BAA6B,KACpC,YAAY,0IACZ,QAAQ,SACR,aAAa,QACb,UAAW,IAAM,GAAM,YAAY,IAAkB,CAAW,EAPlE,SASG,IACC,EAAC,EAAD,CACE,QAAS,EACT,SAAU,EACV,MAAM,0BACN,KAAK,4JACP,EAEF,GACF,EAiBN,SAAS,EAAgB,CACvB,EACqB,CACrB,IAAM,EAAM,EAAa,GACjB,YAAa,EAAQ,EACvB,EAAO,GAAW,CACtB,SAAU,GAAU,CAAC,GAAG,IAAI,CAAC,KAAY,CACvC,SAAU,EAAU,YAAY,CAAE,MAAO,CAAO,CAAC,EACjD,QAAS,EAAG,YACV,EAAI,eAAe,CAAE,MAAO,CAAO,EAAG,CAAM,CAChD,EAAE,CACJ,CAAC,EACD,GAAI,IAAW,OACb,OAEF,MACE,CAAC,GACD,EAAK,OAAS,GACd,EAAK,MACH,CAAC,IAAQ,EAAI,OAAS,QAAa,GAAW,EAAI,KAAM,eAAe,CACzE,EAyBJ,SAAS,EAAW,EAAG,SAAQ,QAAO,UAA4B,CAChE,IAAM,EACJ,EAAO,OAAS,OAAY,OAAY,GAAc,EAAO,KAAK,OAAO,EACrE,EACJ,EAAO,OAAS,OAAY,CAAC,EAAI,GAAkB,EAAO,IAAI,EAC1D,EACJ,EAAO,OAAS,OAAY,OAAY,GAAiB,EAAO,IAAI,EAChE,EAAQ,GAAiB,CAAM,EACrC,OACE,EAAC,EAAD,CACE,MAAO,CACL,EAAO,SAAW,QAAa,CAC7B,IAAK,OACL,MAAO,cACP,MACE,EAAC,GAAD,CACE,MAAO,EACP,OAAQ,EAAO,OACjB,EAEF,KAAM,mHACR,EACA,CACE,IAAK,OACL,MAAO,eACP,MACE,EAAO,OAAS,QAAa,IAAS,OACpC,EAAC,OAAD,CAAM,cAAY,cAAlB,oBAA0C,EAE1C,EAAC,OAAD,CAAM,cAAY,cAAlB,SAmBE,CAlBA,EAAC,EAAD,CACE,KAAM,EAAK,KACX,MAAO,EAAK,KAFd,SAIG,EAAK,MACN,EAAO,IACT,EAAC,EAAD,CAAc,MAAO,EAAO,KAAK,GAAI,EACpC,IAAW,QACV,EAAC,OAAD,CAAM,UAAU,QAAhB,SAAqC,CAArC,MAA4B,GAAS,EAEtC,EAAQ,OAAS,GAChB,EAAC,OAAD,CACE,UAAU,sBACV,cAAY,sBAFd,SAIG,GAAkB,CAAO,EAC1B,GAEJ,EAEN,KACE,EAAO,OAAS,QAAa,EAAQ,OAAS,EAC1C,GACA,MACR,EACA,GAAS,CACP,IAAK,UACL,MAAO,eACP,MACE,EAAC,OAAD,CAAM,cAAY,uBAAlB,SACG,EAAa,EAAO,QAAQ,MAAM,EACnC,EAEJ,KAAM,yDACR,EACA,GAAS,CACP,IAAK,WACL,MAAO,uBACP,MACE,EAAC,OAAD,CAAM,cAAY,kBAAlB,SACG,EAAa,EAAO,QAAQ,EAC7B,EAEJ,KAAM,qNACR,EACA,GACE,EAAO,eAAiB,QAAa,CACnC,IAAK,UACL,MAAO,oBACP,MACE,EAAC,OAAD,CAAM,cAAY,iBAAlB,SACE,EAAC,EAAD,CAAc,MAAO,EAAO,aAAc,EAC1C,CAEN,EACF,GACE,EAAO,mBAAqB,QAAa,CACvC,IAAK,UACL,MAAO,qBACP,MACE,EAAC,OAAD,CAAM,cAAY,iBAAlB,SACE,EAAC,EAAD,CAAc,MAAO,EAAO,iBAAkB,EAC9C,EAEJ,KAAM,uFACR,EACF,GACE,EAAO,SAAW,QAAa,CAC7B,IAAK,SACL,MAAO,SACP,MACE,cAiBE,CAhBA,EAAC,GAAD,CACE,OAAQ,EAAO,OACf,OAAO,gBAGP,MACE,EAAO,QAAU,QACjB,EAAO,MAAM,OAAO,MAAQ,GAEhC,EACC,GAAsB,CAAM,GAC3B,cAGE,CAFC,IACD,EAAC,GAAD,EAAY,GACZ,GAEJ,EAGJ,KAAM,GAAe,EAAO,MAAM,CACpC,CACJ,EACF,EAkBJ,SAAS,EAAU,EAAG,QAAO,UAA2B,CACtD,OACE,EAAC,OAAD,CAAM,cAAY,cAAlB,SAiBE,CAjBF,mBACmB,EAAO,EAAO,OAAQ,OAAO,EADhD,IACoD,IACjD,EAAO,IAAI,CAAC,EAAQ,IAAU,CAC7B,IAAM,EAAU,IAAW,EAC3B,OACE,EAAC,OAAD,UASE,CARC,EAAQ,GAAK,KACd,EAAC,EAAD,CACE,GAAI,GAAc,CAAM,EACxB,MAAO,qBAAqB,IAF9B,SAIG,EAAY,CAAM,EACnB,EACD,GAAW,EAAC,OAAD,CAAM,UAAU,QAAhB,yBAAuC,IAR1C,CAST,EAEL,GACD,EAKN,SAAS,EAAQ,EACf,YAIC,CACD,IAAQ,WAAU,eAAc,SAAU,EACpC,EAAc,OAAO,QAAQ,CAAK,EAClC,EAAY,GAAkB,EAAS,SAAS,EAChD,EAAY,GAAqB,EAAS,UAAU,EACpD,EAAY,GAAa,EACzB,EAAO,EAAY,EAAS,aAAe,EAAS,IAAI,EAC9D,OACE,EAAC,MAAD,CACE,UAAU,kBACV,cAAY,kBAFd,SAyEE,CArEA,EAAC,KAAD,CAAI,UAAU,iBAAd,oBAAyC,EACzC,EAAC,EAAD,CACE,MAAO,CACL,CACE,IAAK,WACL,MAAO,WACP,MACE,EAAC,OAAD,CAAM,cAAY,kBAAlB,SAKE,CAJC,EAAY,EAAS,aAAe,EAAS,IAAI,EAAG,IACrD,EAAC,OAAD,CAAM,UAAU,QAAhB,SAEE,CADC,EAAY,EAAS,IAAI,EAD5B,IACgC,EAAY,EAAS,OAAO,GAC1D,GACF,CAEN,EACA,CACE,IAAK,YACL,MAAO,YACP,MACE,EAAC,EAAD,CACE,KAAM,EAAU,KAChB,MAAO,EAAU,KACjB,OAAO,mBAHT,SAKG,EAAU,MACX,EAEJ,KAAM,EAAU,IAClB,EAEA,GAAI,IAAiB,OACjB,CAAC,EACD,CACE,CACE,IAAK,QACL,MAAO,QACP,MAAO,GAAY,EAAa,KAAK,CACvC,EACA,CACE,IAAK,OAEL,MAAO,+BACP,MAAO,EAAS,EAAa,YAAY,EACzC,KAAM,sGACR,EACA,CACE,IAAK,WACL,MAAO,eACP,MACE,EAAa,gBAAkB,KAC3B,0BACA,EAAS,EAAa,aAAa,CAC3C,CACF,EACJ,GAAG,EAAY,IAAI,EAAE,EAAK,OAAY,CACpC,IAAK,QAAQ,IACb,MAAO,EAAY,CAAG,EACtB,MAAO,EAAC,OAAD,UAAO,EAAY,EAAK,EAAI,CACrC,EAAE,CACJ,EACF,EACC,EAAS,aAAe,QACvB,GACA,EAAU,oBAAoB,GAC5B,EAAC,GAAD,CACE,WAAY,EAAS,WACrB,MAAO,EACT,GAEJ,EAqBN,SAAS,EAAY,EAAG,SAAQ,eAAc,SAA4B,CACxE,IAAM,EAAO,GAAiB,EAAQ,CAAY,EAC5C,EAAW,GAAqB,CAAM,EACtC,EAAQ,EAAK,QAAQ,CAAC,IAAQ,CAClC,CACE,IAAK,EAAI,GACT,MAAO,EAAI,MACX,MAAO,EAAC,GAAD,CAAa,IAAK,EAAK,EAC9B,KAAM,EAAI,IACZ,EACA,GAAI,EAAI,KAAO,WAAa,IAAa,OACrC,CACE,CACE,IAAK,YACL,MAAO,YACP,MACE,EAAC,GAAD,CACE,MAAO,EACP,KAAM,EACR,EAEF,KAAM,6EACR,CACF,EACA,CAAC,CACP,CAAC,EACD,OACE,EAAC,MAAD,CACE,UAAU,gBACV,cAAY,gBAFd,SAME,CAFA,EAAC,KAAD,CAAI,UAAU,iBAAd,kBAAuC,EACvC,EAAC,EAAD,CAAU,MAAO,EAAO,GACxB,EAKN,SAAS,EAAW,EAClB,OAIC,CACD,OACE,EAAC,OAAD,CAAM,cAAa,iBAAiB,EAAI,KAAxC,SAuBE,CAtBC,EAAI,MAAM,IAAI,CAAC,EAAM,IACpB,EAAC,OAAD,UAaE,CARC,EAAK,OACN,EAAC,GAAD,CAAY,KAAM,EAAM,EACvB,EAAK,KACJ,cAGE,CAFC,IACD,EAAC,GAAD,EAAY,GACZ,IARC,CAUL,CACH,EACA,EAAI,KACH,cAGE,CAFC,IACD,EAAC,GAAD,EAAY,GACZ,GAEJ,EAKN,SAAS,EAAU,EACjB,QAIC,CACD,GAAI,EAAK,QAAU,QAAa,EAAK,MAAM,OAAS,EAClD,OACE,EAAC,OAAD,CAAM,UAAU,sBAAhB,SACG,EAAK,MAAM,IAAI,CAAC,IACf,EAAC,OAAD,UAAkB,GAAP,CAAc,CAC1B,EACD,EAGN,OAAO,EAAC,OAAD,CAAM,UAAW,EAAK,MAAQ,QAAU,OAAxC,SAAoD,EAAK,KAAO,EAIzE,SAAS,EAAU,EAAG,CACpB,OACE,EAAC,OAAD,CACE,UAAU,0BACV,MAAO,GACP,cAAY,wBAHd,mBAME,EAKN,SAAS,EAAa,EACpB,QACA,QAMC,CACD,OACE,EAAC,MAAD,CAAK,cAAY,0BAAjB,SACE,EAAC,EAAD,CAAO,MAAO,0CAA0C,IAAxD,SA+BE,CA9BA,EAAC,QAAD,UACE,EAAC,KAAD,UASE,CARA,EAAC,KAAD,CAAI,MAAM,MAAV,iBAAuB,EACvB,EAAC,KAAD,CAAI,MAAM,MAAV,2BAAiC,EACjC,EAAC,KAAD,CACE,MAAM,MACN,UAAU,MAFZ,uBAKE,GACF,EACF,EACF,EAAC,QAAD,UACG,EAAK,IAAI,CAAC,IACT,EAAC,KAAD,CAEE,cAAa,2BAA2B,EAAI,QAF9C,SAaE,CATA,EAAC,KAAD,CAAI,MAAM,MAAV,SAAiB,EAAI,MAAQ,EAC7B,EAAC,KAAD,UAGE,CAFC,EAAI,cACJ,EAAI,uBAAyB,EAAC,GAAD,EAAa,GAC3C,EACF,EAAC,KAAD,CAAI,UAAU,MAAd,SAGE,CAFC,EAAI,WACJ,EAAI,oBAAsB,EAAC,GAAD,EAAa,GACxC,IAXG,EAAI,KAYT,CACH,EACD,GACF,EACF,EAKN,SAAS,EAAW,EAAG,CACrB,OACE,cASE,CARC,IACD,EAAC,EAAD,CACE,KAAK,SACL,MAAO,GACP,OAAO,yBAHT,oBAME,GACF,EAUN,SAAS,EAAoB,CAAC,EAAyC,CACrE,IAAM,EAAM,EAAa,EACnB,EAAO,EAAQ,EACf,EAAU,EAAO,gBAAgB,EACjC,EACJ,IAAe,QAAa,EAAK,SAAS,WAAa,EACnD,EAAO,EAAS,CACpB,SAAU,GAAa,KACvB,QAAS,EAAG,YAAa,GAAc,EAAK,CAAM,EAClD,UACA,UAAW,KACb,CAAC,EACD,OACE,GACA,EAAK,MAAM,UAAU,KACnB,CAAC,IAAS,EAAK,KAAO,GAAc,EAAK,SAC3C,IAAM,GASV,SAAS,EAAY,EACnB,QACA,SACA,WAQC,CAGD,IAAM,EAAc,EAAQ,KAAK,CAAC,IAAY,EAAQ,UAAY,MAAS,EACrE,EAAW,GAAa,CAAO,EACrC,OACE,EAAC,EAAD,CAAO,MAAO,oCAAoC,IAAlD,SAyDE,CAxDA,EAAC,QAAD,UACE,EAAC,KAAD,UAaE,CAZA,EAAC,KAAD,CAAI,MAAM,MAAV,mBAAyB,EACzB,EAAC,KAAD,CAAI,MAAM,MAAV,mBAAyB,EACzB,EAAC,KAAD,CAAI,MAAM,MAAV,0CAAgD,EAChD,EAAC,KAAD,CACE,MAAM,MACN,UAAU,MAFZ,mBAKE,EACF,EAAC,KAAD,CAAI,MAAM,MAAV,oBAA0B,EACzB,GAAY,EAAC,KAAD,CAAI,MAAM,MAAV,SAAiB,GAAkB,EAC/C,GAAe,EAAC,KAAD,CAAI,MAAM,MAAV,mBAAyB,GACzC,EACF,EACF,EAAC,QAAD,UACG,EAAQ,IAAI,CAAC,IACZ,EAAC,KAAD,CAEE,cAAa,kBAAkB,EAAQ,KAFzC,SAmCE,CA/BA,EAAC,KAAD,CAAI,MAAM,MAAV,SACE,EAAC,OAAD,UAAO,EAAY,EAAQ,EAAE,EAAI,EACjC,EACF,EAAC,KAAD,UACE,EAAC,EAAD,CAAc,MAAO,EAAQ,GAAI,EACjC,EACF,EAAC,KAAD,UACE,EAAC,EAAD,CAAc,MAAO,EAAQ,MAAO,EACpC,EACF,EAAC,KAAD,CAAI,UAAU,MAAd,SAAqB,EAAa,EAAQ,KAAK,EAAI,EACnD,EAAC,KAAD,UAAK,EAAY,EAAQ,IAAI,EAAI,EAChC,GACC,EAAC,KAAD,UACE,EAAC,GAAD,CACE,QAAS,EAAQ,GACjB,OAAQ,GAAgB,EAAQ,SAAU,EAAQ,CAAK,EACzD,EACA,EAEH,GACC,EAAC,KAAD,UACG,EAAQ,SAAS,IAAI,CAAC,IACrB,EAAC,OAAD,CAEE,UAAU,qBAFZ,SAIG,EAAY,CAAM,GAHd,CAIL,CACH,EACD,IAhCC,EAAQ,EAkCb,CACH,EACD,GACF,EAiBN,SAAS,EAAY,EAAG,UAAS,UAA6B,CAC5D,GAAI,IAAW,QAAa,EAAO,SAAW,EAC5C,OAAO,EAAC,OAAD,CAAM,UAAU,QAAhB,SAAyB,GAAgB,EAElD,OACE,EAAC,KAAD,CAAI,UAAU,kBAAd,SACG,EAAO,IAAI,CAAC,IACX,EAAC,KAAD,UASE,CARA,EAAC,EAAD,CACE,KAAM,EAAM,KACZ,MAAO,EAAM,MACb,OAAQ,mBAAmB,KAAW,EAAM,QAH9C,SAKG,EAAM,MACP,EACD,EAAM,SAAW,EAAC,OAAD,CAAM,UAAU,QAAhB,yBAAuC,IARlD,EAAM,KASb,CACH,EACD,EC/7BC,IAAM,GAAqB,CAAC,GAAI,GAAI,IAAK,IAAI,EAMvC,GAAmC,GAGnC,GAA4D,CACvE,GAAI,kBACJ,GAAI,YACJ,IAAK,eACL,KAAM,eACR,EAGa,GAAc,IAGd,GAAc,EAGrB,GAAe,IAAI,KAAK,eAAe,OAAW,CACtD,KAAM,UACN,OAAQ,SACV,CAAC,EAGM,SAAS,EAAY,CAAC,EAAoB,CAC/C,OAAO,GAAa,OAAO,IAAI,KAAK,CAAE,CAAC,EAIlC,SAAS,EAAgB,CAAC,EAAsC,CACrE,IAAM,EAAQ,OAAO,CAAG,EACxB,OACE,GAAmB,KAAK,CAAC,IAAW,IAAW,CAAK,GA/BR,GCqBzC,SAAS,EAAe,EAAG,SAA+B,CAC/D,IAAM,EAAM,EAAa,GAClB,EAAQ,GAAU,EAAa,EAChC,EAAU,GAAiB,EAAO,IAAI,QAAQ,CAAC,EAC/C,EAAW,EAAM,EACjB,EAAkB,GAAmB,YAAY,EACjD,EAAa,EAAS,CAC1B,SAAU,GAAU,WAAW,EAAO,CAAO,EAC7C,QAAS,EAAG,YAAa,GAAc,EAAK,EAAO,EAAS,CAAM,EAClE,kBACA,gBAAiB,CACnB,CAAC,EACD,OACE,EAAC,MAAD,CAAK,UAAU,mBAAf,SAgCE,CA/BA,EAAC,MAAD,CAAK,UAAU,sBAAf,SAaE,CAZA,EAAC,QAAD,CAAO,QAAS,EAAhB,kBAAkC,EAClC,EAAC,EAAD,CACE,GAAI,EACJ,QAAS,GAAmB,IAAI,CAAC,KAAY,CAC3C,MAAO,OAAO,CAAM,EACpB,MAAO,GAAc,EACvB,EAAE,EACF,MAAO,OAAO,CAAO,EACrB,SAAU,CAAC,IACT,EAAO,CAAE,OAAQ,IAAU,OAAO,EAAc,EAAI,KAAO,CAAM,CAAC,EAEtE,GACA,EACD,EAAW,UACV,EAAC,EAAD,CACE,MAAM,qBACN,UAAS,GACX,EACE,EAAW,QACb,EAAC,EAAD,CACE,MAAO,EAAW,MAClB,MAAM,4BACN,QAAS,IAAM,KAAK,EAAW,QAAQ,EACzC,EAEA,EAAC,GAAD,CACE,KAAM,EAAW,KACjB,MAAO,GAAG,uBAA2B,GAAc,GAAS,YAAY,IAC1E,GAEF,EAiBC,SAAS,EAAe,EAAG,OAAM,SAA+B,CACrE,IAAQ,WAAY,EACd,EAAO,KAAK,IAAI,EAAG,GAAG,EAAQ,IAAI,CAAC,IAAM,EAAE,UAAY,EAAE,MAAM,CAAC,EAChE,EAAQ,KAAK,IAAI,EAAG,EAAQ,MAAM,EAAI,GACtC,EAAQ,CAAC,IAAmB,EAAQ,EAAQ,GAC5C,EAAU,GAAG,MAAU,EAAa,EAAK,SAAS,gBAAgB,EAAa,EAAK,MAAM,kBAAkB,EAAa,CAAI,gBACnI,OACE,EAAC,SAAD,CAAQ,UAAU,mBAAlB,SAgGE,CA/FA,EAAC,MAAD,CACE,UAAU,oBACV,cAAY,OAFd,SAaE,CATA,EAAC,OAAD,UAGE,CAFA,EAAC,OAAD,CAAM,UAAU,0BAA0B,EAD5C,aACyD,IACtD,EAAa,EAAK,SAAS,GAC5B,EACF,EAAC,OAAD,UAGE,CAFA,EAAC,OAAD,CAAM,UAAU,uBAAuB,EADzC,UACmD,IAChD,EAAa,EAAK,MAAM,GACzB,EACF,EAAC,OAAD,CAAM,UAAU,QAAhB,SAAuD,CAAvD,QAA8B,EAAa,CAAI,EAA/C,QAAuD,GACvD,EACF,EAAC,MAAD,CACE,KAAK,MACL,aAAY,EACZ,QAAS,OAAO,KAAS,KACzB,oBAAoB,OACpB,UAAU,iBALZ,SAoDE,CA7CA,EAAC,OAAD,CACE,GAAI,EACJ,GAAI,EACJ,GAAI,GACJ,GAAI,GACJ,UAAU,sBACZ,EACC,EAAQ,IAAI,CAAC,EAAQ,IAAU,CAC9B,IAAM,EAAI,EAAQ,GAAK,EACjB,GAAY,EAAM,EAAO,SAAS,EAClC,EAAS,EAAM,EAAO,MAAM,EAC5B,EAAM,GAAY,GAAK,EAAS,EAAI,GAAc,EACxD,OACE,EAAC,IAAD,UA6BE,CA5BA,EAAC,QAAD,UAAQ,GAAG,GAAa,EAAO,EAAE,MAAM,EAAO,wBAAwB,EAAO,gBAAkB,EAC/F,EAAC,OAAD,CACE,EAAG,EAAI,EACP,EAAG,EACH,MAAO,GACP,OAAQ,GACR,UAAU,iBACZ,EACC,GAAY,GACX,EAAC,OAAD,CACE,EAAG,EACH,EAAG,GAAc,GACjB,MAAO,EACP,OAAQ,GACR,GAAI,EACJ,UAAU,gBACZ,EAED,EAAS,GACR,EAAC,OAAD,CACE,EAAG,EACH,EAAG,GAAc,GAAY,EAAM,EACnC,MAAO,EACP,OAAQ,EACR,GAAI,EACJ,UAAU,aACZ,IA3BI,EAAO,EA6Bb,EAEL,GACD,EACD,EAAQ,OAAS,GAChB,EAAC,aAAD,CACE,UAAU,kBACV,cAAY,OAFd,SAME,CAFA,EAAC,OAAD,UAAO,GAAa,EAAQ,GAAG,EAAE,EAAI,EACrC,EAAC,OAAD,UAAO,GAAa,EAAQ,GAAG,EAAE,EAAG,EAAE,EAAI,GAC1C,EAEJ,EAAC,QAAD,CAAO,UAAU,kBAAjB,SAkBE,CAjBA,EAAC,UAAD,UAAU,EAAQ,EAClB,EAAC,QAAD,UACE,EAAC,KAAD,UAIE,CAHA,EAAC,KAAD,CAAI,MAAM,MAAV,kBAAwB,EACxB,EAAC,KAAD,CAAI,MAAM,MAAV,qBAA2B,EAC3B,EAAC,KAAD,CAAI,MAAM,MAAV,kBAAwB,GACxB,EACF,EACF,EAAC,QAAD,UACG,EAAQ,IAAI,CAAC,IACZ,EAAC,KAAD,UAIE,CAHA,EAAC,KAAD,CAAI,MAAM,MAAV,SAAiB,GAAa,EAAO,EAAE,EAAI,EAC3C,EAAC,KAAD,UAAK,EAAO,UAAY,EACxB,EAAC,KAAD,UAAK,EAAO,OAAS,IAHd,EAAO,EAId,CACH,EACD,GACF,GACF,EC7IC,SAAS,EAAY,CAC1B,EACqB,CACrB,IAAM,EAAW,EAAQ,OAAO,CAAC,IAAW,EAAO,SAAW,MAAS,EAEvE,GAAI,EAAS,SAAW,EACtB,OAAO,KAET,GAAI,EAAS,KAAK,CAAC,IAAW,EAAO,SAAW,EAAI,EAClD,OAAO,KAET,IAAM,EAAY,EAAS,OAAS,EAAQ,OAC5C,MAAO,CACL,QAAS,EACL,kIAxBN,oJAyBM,mDAzBN,oJA0BE,WACF,EChDF,IAAM,GAAmB,GASlB,SAAS,EAAY,EAAG,SAA4B,CACzD,IAAM,EAAM,EAAa,EACnB,EAAkB,GAAmB,SAAS,EAGpD,GAAoB,EAAO,EAAO,aAAa,CAAC,EAChD,IAAM,EAAW,GAAqB,EAChC,EAAU,EAAS,CACvB,SAAU,GAAU,QAAQ,CAAK,EACjC,QAAS,EAAG,YAAa,GAAiB,EAAK,EAAO,CAAM,EAC5D,iBACF,CAAC,EACD,GAAI,EAAQ,UACV,OACE,EAAC,EAAD,CACE,MAAM,kBACN,UAAS,GACX,EAGJ,GAAI,EAAQ,QACV,OACE,EAAC,EAAD,CACE,MAAO,EAAQ,MACf,MAAM,yBACN,QAAS,IAAM,KAAK,EAAQ,QAAQ,EACtC,EAGJ,IAAM,EAAQ,EAAQ,KAAK,MAC3B,GAAI,EAAM,SAAW,EACnB,OACE,EAAC,EAAD,CACE,MAAM,kBACN,YAAY,kDACd,EAIJ,IAAM,EAAW,EAAM,KACrB,CAAC,IAAW,EAAO,OAAS,QAAa,EAAO,MAAQ,MAC1D,EACM,EAAS,GAAa,CAAK,EACjC,OACE,cAmBE,CAlBC,GACC,EAAC,IAAD,CACE,UAAU,qBACV,KAAK,OACL,cAAY,gBACZ,iBAAgB,EAAO,UAAY,OAAS,QAJ9C,SAMG,EAAO,QACR,EAEJ,EAAC,GAAD,CACE,QAAS,EACT,MAAO,cAAc,IACrB,UAAW,GACX,SAAU,EACV,SAAU,EACV,SAAU,GACZ,GACA,EC9DN,IAAM,GAAmD,CACvD,YACA,SACA,OACA,UACA,UACA,kBACF,EAGM,GAAqD,CACzD,SACA,OACA,WACF,EAGM,GAAkE,CACtE,CAAE,MAAO,UAAW,MAAO,SAAU,EACrC,CAAE,MAAO,QAAS,MAAO,OAAQ,EACjC,CAAE,MAAO,OAAQ,MAAO,MAAO,CACjC,EAkBO,SAAS,EAAY,EAAG,QAAO,UAA6B,CACjE,IAAM,EAAM,EAAa,EACnB,EAAY,GAAa,GACxB,EAAM,GAAW,EAAqB,IAAI,EAC3C,EAAQ,IAAM,EAAQ,IAAI,EAC1B,EAAa,GAAsB,CAAK,EAExC,EAAQ,EAAe,CAC3B,WAAY,IAAM,GAAW,EAAK,CAAK,EACvC,eAAgB,UAAU,IAC1B,WAAY,mBAAmB,IAC/B,YACF,CAAC,EACK,EAAS,EAAe,CAC5B,WAAY,IAAM,GAAY,EAAK,CAAK,EACxC,eAAgB,WAAW,IAC3B,WAAY,oBAAoB,IAChC,YACF,CAAC,EAEK,GAAY,IAAW,IAAS,EAAU,cAAc,EACxD,EAAa,IAAW,IAAQ,EAAU,eAAe,EACzD,EAAY,EAAU,cAAc,EACpC,GAAY,EAAU,cAAc,EACpC,EAAe,EAAU,eAAe,EAC9C,GAAI,CAAC,IAAa,CAAC,GAAc,CAAC,GAAa,CAAC,IAAa,CAAC,EAC5D,OAAO,KAGT,OACE,EAAC,MAAD,CACE,UAAU,gBACV,KAAK,QACL,aAAW,gBAHb,SA6DE,CAxDC,IACC,EAAC,EAAD,CACE,QAAS,IAAM,EAAM,OAAO,EAC5B,SAAU,EAAM,UAChB,YAAW,EAAM,WAAa,OAHhC,iBAME,EAEH,GACC,EAAC,EAAD,CACE,QAAQ,UACR,QAAS,IAAM,EAAO,OAAO,EAC7B,SAAU,EAAO,UACjB,YAAW,EAAO,WAAa,OAJjC,kBAOE,EAEH,GACC,EAAC,EAAD,CAAQ,QAAS,IAAM,EAAQ,UAAU,EAAzC,sBAAwD,EAEzD,IACC,EAAC,EAAD,CACE,QAAQ,SACR,QAAS,IAAM,EAAQ,OAAO,EAFhC,kBAKE,EAEH,GACC,EAAC,EAAD,CACE,QAAQ,SACR,QAAS,IAAM,EAAQ,OAAO,EAFhC,kBAKE,EAEH,IAAS,SACR,EAAC,GAAD,CACE,MAAO,EACP,QAAS,EACX,EAED,IAAS,SACR,EAAC,GAAD,CACE,MAAO,EACP,QAAS,EACX,EAED,IAAS,YACR,EAAC,GAAD,CACE,MAAO,EACP,QAAS,EACX,GAEF,EAaN,SAAS,EAAW,EAAG,QAAO,WAA8B,CAC1D,IAAM,EAAM,EAAa,GAClB,EAAS,GAAc,EAAS,EAAK,EACtC,EAAQ,EAAe,CAC3B,WAAY,CAAC,IAAyB,GAAW,EAAK,EAAO,CAAW,EACxE,eAAgB,CAAC,IACf,WAAW,EAAO,EAAO,MAAO,KAAK,UAAU,IACjD,WAAY,GAAsB,CAAK,EACvC,YAAa,EACf,CAAC,EACD,OACE,EAAC,EAAD,CACE,KAAI,GACJ,QAAS,EACT,MAAO,SAAS,KAChB,YAAY,sHACZ,QAAQ,SACR,aAAa,QACb,aAAa,YACb,YAAa,EACb,UAAW,IAAM,EAAM,YAAY,CAAO,EAT5C,SAWE,EAAC,EAAD,CACE,QAAS,EACT,SAAU,EACV,MAAM,2BACR,EACA,EAKN,SAAS,EAAW,EAAG,QAAO,WAA8B,CAC1D,IAAM,EAAM,EAAa,GACjB,UAAW,EAAQ,EACrB,EAAQ,GAAS,GAChB,EAAO,GAAY,EAAkC,WAAW,GAChE,EAAK,GAAU,EAA6B,CAAC,GAC7C,EAAM,IAAW,EAAuB,MAAM,GAC9C,EAAO,GAAY,EAA6B,EAAO,YAAY,EACpE,GAAQ,EAAe,CAC3B,WAAY,CAAC,KAAyB,GAAW,EAAK,EAAO,EAAI,EACjE,UAAW,CAAC,KAAW,CACrB,EAAM,QAAQ,WAAW,EAAO,GAAO,MAAO,KAAK,UAAU,IAAS,CACpE,YACE,GAAO,IAAI,OAAS,EAAI,YAAY,GAAQ,GAAO,GAAG,IAAM,MAChE,CAAC,GAEH,WAAY,GAAsB,CAAK,EACvC,YAAa,EACf,CAAC,EACK,EAAY,GAAK,EAAK,CAAI,EAC1B,GACJ,IAAU,QACV,OAAO,UAAU,CAAK,GACtB,GAAS,GACT,GAAS,EAAO,SACZ,GAAa,CAAC,KAAgB,GAAM,YAAY,IACtD,OACE,EAAC,EAAD,CACE,KAAI,GACJ,QAAS,EACT,MAAO,SAAS,KAChB,YAAY,0GACZ,QAAQ,SACR,aAAa,QACb,aAAa,YACb,YAAa,EACb,gBAAiB,IAAc,QAAa,CAAC,GAC7C,UAAW,IACT,GAAM,YAAY,CAAE,QAAO,UAAW,GAAa,EAAG,OAAM,CAAC,EAXjE,SAiEE,CAnDA,EAAC,EAAD,CACE,MAAM,QACN,MAAO,GAAW,OAAO,EAF3B,SAIE,EAAC,EAAD,CACE,QAAS,GAAa,IAAI,CAAC,MAAW,CACpC,SACA,MAAO,EAAa,GACtB,EAAE,EACF,MAAO,EACP,SAAU,EACZ,EACA,EACF,EAAC,MAAD,CAAK,UAAU,YAAf,SAoBE,CAnBA,EAAC,EAAD,CACE,MAAM,aACN,MAAO,GAAW,WAAW,EAC7B,SAAQ,GAHV,SAKE,EAAC,EAAD,CACE,MAAO,EACP,SAAU,EACV,IAAK,EACL,KAAK,MACP,EACA,EACF,EAAC,EAAD,CAAO,MAAM,OAAb,SACE,EAAC,EAAD,CACE,QAAS,GACT,MAAO,EACP,SAAU,GACZ,EACA,GACF,EACF,EAAC,EAAD,CACE,MAAM,QACN,KAAM,WAAW,EAAa,EAAO,QAAQ,KAC7C,MACE,GAAW,OAAO,IACjB,GACG,OACA,iBAAiB,EAAa,EAAO,QAAQ,MAPrD,SAUE,EAAC,EAAD,CACE,MAAO,EACP,SAAU,EACV,IAAK,EACL,IAAK,EAAO,SACd,EACA,GACF,EAKN,SAAS,EAAc,EAAG,QAAO,WAA8B,CAC7D,IAAM,EAAM,EAAa,GACjB,UAAW,EAAQ,EACrB,EAAQ,GAAS,GAChB,EAAO,GAAY,EAAgC,QAAQ,GAC3D,EAAM,GAAW,EAAS,EAAE,GAC5B,EAAQ,IAAa,EAAS,EAAE,GAChC,EAAO,GAAY,EAA6B,EAAO,WAAW,GAClE,GAAe,GAAoB,EAAS,EAAI,EACjD,GAAW,EAAe,CAC9B,WAAY,CAAC,IAAuB,GAAa,EAAK,EAAO,CAAI,EACjE,UAAW,CAAC,IAAW,CACrB,EAAM,QAAQ,WAAW,EAAO,EAAO,MAAO,KAAK,QAAQ,IAAS,CAClE,YACE,EAAO,IAAI,SAAW,EAClB,OACA,GAAG,GAAQ,EAAO,GAAG,IAAI,EAAO,UAAY,sBAAsB,EAAO,uBAAyB,IAC1G,CAAC,GAEH,WAAY,GAAsB,CAAK,EACvC,YAAa,EACf,CAAC,EACK,GACJ,IAAU,QACV,OAAO,UAAU,CAAK,GACtB,GAAS,GACT,GAAS,EAAO,YACZ,GACJ,EAAW,GAAS,KAAK,GACzB,GAAS,MAAM,OAAS,wBACpB,GAAa,CAAC,IAAgB,GAAS,YAAY,GACzD,OACE,EAAC,EAAD,CACE,KAAI,GACJ,QAAS,EACT,MAAO,eAAe,EAAa,GAAO,YAAY,YAAY,KAClE,YAAY,wHACZ,aAAa,YACb,aAAa,kCACb,YAAa,EACb,gBAAiB,CAAC,GAClB,UAAW,IACT,GAAS,YAAY,CACnB,QACA,KAAM,EAAK,KAAK,GAAK,OACrB,OAAQ,EAAO,KAAK,GAAK,OACzB,QACA,gBACF,CAAC,EAhBL,SAsFE,CAnEA,EAAC,EAAD,CACE,MAAM,QACN,MAAO,GAAW,OAAO,EAF3B,SAIE,EAAC,EAAD,CACE,QAAS,GAAiB,IAAI,CAAC,KAAW,CACxC,QACA,MAAO,EAAa,EACtB,EAAE,EACF,MAAO,EACP,SAAU,EACZ,EACA,EACF,EAAC,EAAD,CACE,MAAM,WACN,KAAK,8CACL,MAAO,GAAW,MAAM,EAH1B,SAKE,EAAC,EAAD,CACE,MAAO,EACP,SAAU,EACV,UAAW,IACb,EACA,EACF,EAAC,EAAD,CACE,MAAM,0BACN,KAAK,uDACL,MAAO,GAAW,QAAQ,EAH5B,SAKE,EAAC,EAAD,CACE,MAAO,EACP,SAAU,GACV,UAAW,IACb,EACA,EACF,EAAC,EAAD,CACE,MAAM,QACN,KAAM,WAAW,EAAa,EAAO,WAAW,KAChD,MACE,GAAW,OAAO,IACjB,GACG,OACA,iBAAiB,EAAa,EAAO,WAAW,MAPxD,SAUE,EAAC,EAAD,CACE,MAAO,EACP,SAAU,EACV,IAAK,EACL,IAAK,EAAO,YACd,EACA,EACF,EAAC,EAAD,CACE,QAAS,GACT,SAAU,EACV,MAAM,iBACR,EACC,IACC,EAAC,IAAD,CACE,UAAU,SACV,KAAK,OACL,cAAY,wBAHd,mIAOE,GAEJ,ECtXC,SAAS,EAAW,EAAG,CAC5B,IAAQ,QAAQ,IAAO,GAA6B,EAC9C,EAAM,EAAa,EACnB,EAAO,EAAQ,EACf,EAAU,EAAO,aAAa,EAC9B,EAAc,EAAO,WAAW,EAChC,EAAe,EAAO,UAAU,EAChC,EAAiB,EAAO,kBAAkB,EAC1C,EAAU,GAAsB,EAChC,EAAiB,GAAmB,QAAQ,EAC5C,EAAiB,GAAmB,QAAQ,EAGlD,GAAa,EAAO,CAClB,QAAS,GAAW,EACpB,KAAM,EACN,YAAa,CACf,CAAC,EACD,IAAO,GAAQ,GAAa,EAAS,EAAK,EACpC,EAAe,EAA0B,IAAI,GAC5C,GAAY,GAAiB,EAAS,EAAK,EAC5C,GAAmB,EAA0B,IAAI,EAEjD,GAAS,EAAS,CACtB,SAAU,GAAU,OAAO,CAAK,EAChC,QAAS,EAAG,aAAa,GAAS,EAAK,EAAO,EAAM,EACpD,gBAAiB,EACjB,QAAS,CACX,CAAC,EACK,GAAS,EAAS,CACtB,SAAU,GAAU,OAAO,CAAK,EAChC,QAAS,EAAG,aAAa,GAAe,EAAK,EAAO,EAAM,EAC1D,gBAAiB,EACjB,QAAS,CACX,CAAC,EAED,GAAI,GAAW,GAAO,SAAW,CAAC,GAAO,KACvC,OACE,EAAC,MAAD,CAAK,UAAU,SAAf,SACE,EAAC,EAAD,CACE,aAAc,EACd,MAAO,GAAO,MACd,MAAO,4BAA4B,IACnC,QAAS,IAAM,KAAK,GAAO,QAAQ,EACrC,EACA,EAIN,IAAM,GAAS,GAAW,EAAM,CAAY,EAGtC,EAAa,IAAU,EAAK,SAAS,QACrC,GAAQ,GAAO,KACjB,EAAW,OAAO,CAAC,GAAK,KAAU,GAAM,GAAO,KAAK,IAAQ,CAAC,EAC7D,GAAO,MAAM,MAEjB,OACE,EAAC,MAAD,CACE,UAAU,sBACV,cAAY,eAFd,SA2FE,CAvFA,EAAC,MAAD,CACE,aAAW,aACX,UAAU,aAFZ,SAME,CAFA,EAAC,EAAD,CAAM,GAAG,UAAT,kBAA2B,EAC3B,EAAC,OAAD,CAAM,cAAY,OAAlB,eAA8B,GAC9B,EACF,EAAC,SAAD,CAAQ,UAAU,eAAlB,SAyCE,CAxCA,EAAC,MAAD,CAAK,UAAU,gBAAf,SAgBE,CAfA,EAAC,KAAD,CAAI,UAAU,eAAd,SAA8B,EAAQ,EACrC,GAAO,MAAM,QAAU,EAAC,EAAD,CAAO,KAAK,UAAZ,kBAA8B,EACrD,KAAU,QACT,EAAC,OAAD,CACE,UAAU,cACV,cAAY,cAFd,SAKE,CADC,EAAa,EAAK,EAJrB,SAKE,EAEH,GAAO,cAAgB,GACtB,EAAC,OAAD,CAAM,UAAU,sBAAhB,SAEE,CAFF,WACU,EAAC,EAAD,CAAc,MAAO,GAAO,cAAe,GACnD,GAEJ,EACF,EAAC,MAAD,CAAK,UAAU,gBAAf,SAsBE,CArBC,IACC,EAAC,EAAD,CACE,IAAK,EACL,QAAQ,UACR,QAAS,IAAM,EAAU,EAAI,EAH/B,mBAME,EAEH,GACC,EAAC,EAAD,CACE,IAAK,GACL,QAAS,IAAM,EAAc,EAAI,EAFnC,oBAKE,EAEJ,EAAC,GAAD,CACE,MAAO,EACP,OAAQ,GAAO,MAAM,OACvB,GACA,GACF,EACD,IACC,EAAC,GAAD,CACE,MAAO,EACP,KAAM,GACN,QAAS,IAAM,CACb,EAAU,EAAK,EACf,EAAa,SAAS,MAAM,GAEhC,EAED,GAAc,IACb,EAAC,GAAD,CACE,MAAO,EACP,KAAI,GACJ,QAAS,IAAM,CACb,EAAc,EAAK,EACnB,GAAiB,SAAS,MAAM,GAEpC,EAED,EACC,EAAC,EAAD,CAAM,MAAM,OAAZ,SACE,EAAC,GAAD,CACE,MAAO,EACP,OAAQ,GAAO,KACjB,EACA,EAEF,EAAC,EAAD,CACE,MAAM,cACN,YAAY,sCACd,EAEF,EAAC,GAAD,CACE,MAAO,EACP,OAAQ,GAAO,KAAO,GAAO,KAAK,OAAS,OAC3C,cAAe,GAAW,GAAO,UACnC,GACA,EAeN,SAAS,EAAW,EAAG,QAAO,SAAQ,iBAAmC,CACvE,IAAM,EAAO,EAAQ,EACf,EAAY,GAAa,EACzB,EAAa,EAAO,cAAc,EAClC,EAAU,EAAO,aAAa,EAC9B,EAAa,EAAO,cAAc,EAClC,EAAiB,EAAO,kBAAkB,EAC1C,EAAc,GAAmB,EACjC,GAAM,EAAa,EAInB,EAAS,EAAS,CACtB,SAAU,GAAW,OAAO,GAAK,CAAK,EACtC,QAAS,EAAG,aAAa,GAAgB,GAAK,EAAO,EAAM,EAC3D,QAAS,EACT,MAAO,EACT,CAAC,EAEK,EACJ,CAAC,EACH,GAAI,EAAK,SAAS,SAAW,IAAW,QAAa,GACnD,EAAO,KAAK,CACV,MAAO,SACP,MAAO,SACP,OAAQ,IACN,IAAW,OACT,EAAC,EAAD,CACE,MAAM,iBACN,UAAS,GACX,EAEA,EAAC,GAAD,CACE,MAAO,EACP,OAAQ,EACR,SAAU,EAAU,eAAe,EACrC,CAEN,CAAC,EAEH,GAAI,EAAY,MACd,EAAO,KAAK,CACV,MAAO,eACP,MAAO,eACP,OAAQ,IACN,EAAC,GAAD,CACE,MAAO,EACP,QAAS,EAAY,QACrB,SAAU,EAAY,SACxB,CAEJ,CAAC,EAEH,GAAI,EAAK,SAAS,SAAW,EAC3B,EAAO,KAAK,CACV,MAAO,UACP,MAAO,UACP,OAAQ,IAAM,EAAC,GAAD,CAAc,MAAO,EAAO,CAC5C,CAAC,EAIH,GAAI,EAAK,SAAS,QAAU,EAC1B,EAAO,KAAK,CACV,MAAO,SACP,MAAO,SACP,OAAQ,IAAM,EAAC,GAAD,CAAa,MAAO,EAAO,CAC3C,CAAC,EAIH,GAAI,IAAY,EAAO,UAAY,EAAO,MAAQ,QAAU,MAC1D,EAAO,KAAK,CACV,MAAO,SACP,MAAO,SACP,OAAQ,IAAM,EAAC,GAAD,CAAa,MAAO,EAAO,CAC3C,CAAC,EAEH,GAAI,EAAK,SAAS,YAAc,EAC9B,EAAO,KAAK,CACV,MAAO,aACP,MAAO,aACP,OAAQ,IAAM,EAAC,GAAD,CAAiB,MAAO,EAAO,CAC/C,CAAC,EAEH,GAAI,EACF,EAAO,KAAK,CACV,MAAO,cACP,MAAO,cACP,OAAQ,IACN,EAAC,GAAD,CACE,MAAO,EACP,UAAW,EAAU,oBAAoB,EACzC,WAAY,EAAU,qBAAqB,EAC3C,UAAW,EAAU,oBAAoB,EAC3C,CAEJ,CAAC,EAEH,IAAM,GAAQ,EAAO,IAAI,OAAS,SAC5B,EAAW,GAAmB,QAAS,EAAQ,EAAK,EAC1D,GAAI,EAAO,SAAW,EACpB,OAAO,KAET,IAAM,GAAU,EAAO,KAAK,CAAC,KAAU,GAAM,QAAU,CAAQ,GAAK,EAAO,GAC3E,OACE,EAAC,EAAD,CACE,MAAM,UACN,UAAU,eAFZ,SAIE,EAAC,GAAD,CACE,KAAM,EAAO,IAAI,EAAG,SAAO,aAAa,CAAE,SAAO,QAAM,EAAE,EACzD,MAAM,QACN,aAAc,GACd,MAAM,gBAJR,SAMG,GAAQ,OAAO,EAChB,EACF,ECxTC,SAAS,EAAgB,EAAG,CACjC,IAAM,EAAM,EAAa,GACjB,UAAW,EAAQ,EACrB,EAAU,EAAO,aAAa,GAC7B,EAAQ,GAAU,EAAa,EAChC,EAAW,EAAM,EAEjB,EAAS,EAAO,IAAI,QAAQ,GAAK,GACjC,EAAiB,GAAiB,CAAM,EACxC,EAAQ,GACZ,GACE,EACA,QACA,KAAK,IAAI,EAAO,gBAAiB,EAAO,SAAS,CACnD,EACA,EAAO,SACT,EACM,EAAS,GAAS,EAAQ,SAAU,CAAC,EACrC,GAAQ,CAAE,OAAQ,EAAgB,SAAQ,OAAM,EAChD,EAAkB,GAAmB,MAAM,EACjD,GAAiB,CAAO,EAExB,IAAM,EAAS,EAAS,CACtB,SAAU,GAAU,KAAK,EAAK,EAC9B,QAAS,EAAG,aAAa,GAAe,EAAK,GAAO,EAAM,EAC1D,kBACA,gBAAiB,EACjB,QAAS,CACX,CAAC,EAED,GAAI,CAAC,EACH,OACE,EAAC,MAAD,CAAK,UAAU,SAAf,SAME,CALA,EAAC,KAAD,CAAI,UAAU,eAAd,kBAAqC,EACrC,EAAC,EAAD,CACE,MAAM,kBACN,YAAY,uCACd,GACA,EAIN,OACE,EAAC,MAAD,CACE,UAAU,SACV,cAAY,cAFd,SA6EE,CAzEA,EAAC,KAAD,CAAI,UAAU,eAAd,kBAAqC,EACrC,EAAC,EAAD,CACE,MAAM,SACN,QACE,EAAC,MAAD,CAAK,UAAU,SAAf,SAmBE,CAlBA,EAAC,QAAD,CACE,QAAS,EACT,UAAU,kBAFZ,iCAKE,EACF,EAAC,QAAD,CACE,GAAI,EACJ,KAAK,SACL,UAAU,QACV,YAAY,mBACR,GACJ,MAAO,EACP,UAAW,IACX,SAAU,CAAC,KACT,EAAO,CAAE,OAAQ,GAAM,OAAO,MAAO,OAAQ,IAAK,CAAC,EAEvD,GACA,EAtBN,SAyBG,EAAO,UACN,EAAC,EAAD,CACE,MAAM,iBACN,UAAS,GACX,EACE,EAAO,QACT,EAAC,EAAD,CACE,MAAO,EAAO,MACd,MAAM,wBACN,QAAS,IAAM,KAAK,EAAO,QAAQ,EACrC,EACE,EAAO,KAAK,MAAM,SAAW,GAAK,IAAW,EAC/C,EACE,EAAC,EAAD,CACE,MAAM,kBACN,YAAa,2BAA2B,MAC1C,EAEA,EAAC,EAAD,CACE,MAAM,gBACN,YAAY,iDACd,EAGF,cAoBE,CAnBA,EAAC,GAAD,CACE,MAAO,EAAO,KAAK,MACnB,WAAY,GACd,EACA,EAAC,GAAD,CACE,MAAM,cACN,OAAQ,EACR,MAAO,EACP,MAAO,EAAO,KAAK,KAAK,MACxB,UAAW,EAAO,KAAK,MAAM,OAC7B,QAAS,EAAO,KAAK,KAAK,QAC1B,YAAa,EAAO,UACpB,SAAU,CAAC,KACT,EAAO,CACL,OAAQ,GAAK,OAAS,EAAI,OAAO,GAAK,MAAM,EAAI,KAChD,MAAO,OAAO,GAAK,KAAK,CAC1B,CAAC,EAEL,GACA,EAEJ,GACF",
|
|
21
|
+
"debugId": "5378BD3EF15812B464756E2164756E21",
|
|
22
|
+
"names": []
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.worker-actions{display:flex;align-items:center;gap:var(--space-1);flex-wrap:wrap}.worker-actions-hint{display:inline-block;font-size:var(--text-sm);max-width:24rem}.worker-config-form{display:grid;gap:var(--space-3)}.worker-config-row{display:flex;align-items:flex-end;gap:var(--space-2);flex-wrap:wrap}.worker-config-row>.field{flex:12rem;min-width:0}.worker-config-note{padding:var(--space-2)var(--space-3);border-radius:var(--radius-sm);background:var(--surface-2);font-size:var(--text-sm);margin:0}.worker-config-instances{padding-left:var(--space-4);font-size:var(--text-sm);color:var(--text-muted);margin:0}.breadcrumb{font-size:var(--text-sm);color:var(--text-muted)}.queue-header{display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-3);flex-wrap:wrap}.queue-heading{display:flex;align-items:baseline;gap:var(--space-2)var(--space-3);flex-wrap:wrap;min-width:0}.queue-heading .screen-title{overflow-wrap:anywhere}.queue-total{font-variant-numeric:tabular-nums;color:var(--text-muted)}.queue-updated{font-size:var(--text-sm)}.queue-toolbar,.queue-actions{display:flex;gap:var(--space-2);flex-wrap:wrap}.jobs-section{display:flex;gap:var(--space-3);flex-direction:column;min-width:0}.jobs-filters{display:flex;align-items:flex-end;gap:var(--space-2)var(--space-3);flex-wrap:wrap}.jobs-filter{display:flex;gap:var(--space-1);font-size:var(--text-sm);flex-direction:column;min-width:0}.jobs-total{align-self:center}.bulk-bar{display:flex;align-items:center;gap:var(--space-2)var(--space-3);padding:var(--space-2)var(--space-3);border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-2);flex-wrap:wrap}.bulk-count{font-size:var(--text-sm);font-variant-numeric:tabular-nums}.select-cell{text-align:center;width:2rem}.job-id{display:flex;align-items:center;gap:var(--space-2);font-family:var(--font-mono);max-width:22rem;font-weight:400}.job-id a{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.job-processed-by-col{white-space:nowrap;width:12rem}.job-processed-by-text{display:inline-block;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom;white-space:nowrap;max-width:12rem}.job-failure{color:var(--text-muted);overflow-wrap:anywhere;max-width:24rem}tr[aria-selected=true]{background:var(--info-bg)}.queue-panels{min-width:0}.field-row{display:flex;gap:var(--space-3);flex-wrap:wrap}.field-row>.field{flex:10rem;min-width:0}.limits-editor{display:flex;gap:var(--space-3);flex-direction:column}.limits-names{display:flex;gap:var(--space-3);border:1px solid var(--border);border-radius:var(--radius);padding:var(--space-3);flex-direction:column}.limits-name{border-top:1px solid var(--border);padding-top:var(--space-3)}.form-actions{display:flex;gap:var(--space-2);flex-wrap:wrap}.limits-summary{display:grid;grid-template-columns:max-content 1fr;gap:var(--space-1)var(--space-3);margin:0}.limits-summary>div{display:contents}.limits-summary dd{margin:0}.throughput-controls{display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)}.throughput-chart{margin:0}.throughput-legend{display:flex;gap:var(--space-3);font-size:var(--text-sm);margin-bottom:var(--space-2);flex-wrap:wrap}.swatch{display:inline-block;vertical-align:middle;border-radius:2px;width:10px;height:10px}.swatch-completed,.bar-completed{background:var(--state-completed);fill:var(--state-completed)}.swatch-failed,.bar-failed{background:var(--state-failed);fill:var(--state-failed)}.throughput-svg{display:block;width:100%;height:160px}.throughput-baseline{stroke:var(--border);stroke-width:1;vector-effect:non-scaling-stroke}.throughput-hit{fill:transparent}.throughput-hit:hover{fill:var(--surface-2)}.throughput-axis{display:flex;font-size:var(--text-xs);color:var(--text-muted);justify-content:space-between}.repeatable-data{max-width:24rem}.repeatable-actions{display:flex;gap:var(--space-2);flex-wrap:wrap}.demand-panel{display:grid;gap:var(--space-3)}.demand-urls{display:grid;gap:var(--space-2)}.demand-urls p{margin:0}.demand-url{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;min-width:0}.demand-url-label{min-width:6rem;font-weight:600}.demand-url code{overflow-wrap:anywhere}.summon-panel{display:grid;gap:var(--space-3)}.summon-heading{margin:0 0 var(--space-2);font-size:1rem}.summon-actions{display:flex;gap:var(--space-2);flex-wrap:wrap}.summoning-unit{display:block;font-size:var(--text-sm);font-weight:400}.summon-budget{display:inline-grid;gap:var(--space-1)}.summon-budget-window{display:flex;align-items:center;gap:var(--space-2);font-variant-numeric:tabular-nums;flex-wrap:wrap}.summon-budget-window.is-exhausted{color:var(--warning-text);font-weight:600}.summon-budget-meter{flex:none;width:80px;height:8px}.summon-group{display:grid;gap:var(--space-2);padding:var(--space-3);border:1px solid var(--border);border-radius:var(--radius);background:var(--surface)}.summon-subheading{font-size:var(--text-sm);margin:0;font-weight:600}.summon-group-circuit.is-open{color:var(--warning-text)}.summon-group tr.is-current,.summon-groups tr.is-current{background:var(--surface-2);box-shadow:inset 3px 0 0 var(--accent)}.summon-groups{display:grid;gap:var(--space-2);margin-top:var(--space-5)}.summon-groups-title{font-size:var(--text-lg);margin:0}.summon-group-list{list-style:none;margin:0;padding:0}.summon-group-count{font-variant-numeric:tabular-nums;color:var(--text-muted)}.summon-coverage{display:flex;gap:var(--space-1)var(--space-2);list-style:none;flex-wrap:wrap;margin:0;padding:0}.summon-last-missing{display:block;color:var(--danger-text)}.summon-group-note{display:block;font-size:var(--text-sm);margin:0}.summon-policy-tag{font-size:var(--text-xs);font-style:italic}.summon-policy-names{display:inline-flex;gap:var(--space-1);flex-wrap:wrap}.summon-policy-names code{padding:0 var(--space-1);border:1px solid var(--border);border-radius:var(--radius-sm);overflow-wrap:anywhere}.range-picker{display:flex;align-items:flex-end;gap:var(--space-3);flex-wrap:wrap}.range-picker>.field{min-width:0}.range-scope{margin-top:var(--space-2)}.range-caption{font-size:var(--text-sm);color:var(--text-muted);margin:0}.range-not-retained{padding:var(--space-3);border:1px solid var(--border);border-radius:var(--radius);color:var(--text-muted)}.range-not-retained-title{margin:0 0 var(--space-1);color:var(--text);font-weight:600}.range-not-retained-detail{font-size:var(--text-sm);margin:0}.analytics-section .analytics-headline{display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-4);margin-bottom:var(--space-4);flex-wrap:wrap}.analytics-section .analytics-charts{display:flex;gap:var(--space-2);flex-direction:column;min-width:0}.analytics-section .analytics-note{font-size:var(--text-sm);color:var(--text-muted);max-width:34rem;margin:0}.overview-jobs-chart{margin-top:var(--space-3)}.events-screen{display:flex;gap:var(--space-4);flex-direction:column}.events-picker{display:flex;align-items:flex-end;gap:var(--space-2)var(--space-3);flex-wrap:wrap}.events-picker .field{min-width:12rem;margin:0}.events-types{margin-top:var(--space-3)}.events-types summary{cursor:pointer;overflow-wrap:anywhere}.events-types fieldset{margin:var(--space-2)0 0;border:0;padding:0}.events-type-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(9rem,1fr));gap:var(--space-1)var(--space-3);margin-bottom:var(--space-2)}.events-types-ignored{margin:var(--space-3)0 0;padding:var(--space-2)var(--space-3);background:var(--warning-bg);color:var(--warning-text);font-size:var(--text-sm);overflow-wrap:anywhere;border-radius:6px}.events-channel{margin:var(--space-3)0 0;font-size:var(--text-sm);overflow-wrap:anywhere}.events-rejected{margin-top:var(--space-3);padding:var(--space-2)var(--space-3);background:var(--warning-bg);color:var(--warning-text);border-radius:6px}.events-rejected p{overflow-wrap:anywhere;margin:0}.events-actions{display:flex;align-items:center;gap:var(--space-2);font-size:var(--text-sm);flex-wrap:wrap}.events-held{font-variant-numeric:tabular-nums}.events-table td{vertical-align:top}.events-time{font-variant-numeric:tabular-nums;white-space:nowrap}.events-kind{white-space:nowrap}.events-target,.events-id{overflow-wrap:anywhere}.events-payload{min-width:12rem}.events-gap td{font-style:italic}.events-summary{margin:0 0 var(--space-1)}.schema-tree{font-size:var(--text-sm);overflow-x:auto}.schema-list{list-style:none;margin:0;padding:0}.schema-list .schema-list{margin-left:var(--space-3);padding-left:var(--space-3);border-left:1px solid var(--border)}.schema-node{padding:var(--space-1)0}.schema-row{display:flex;align-items:baseline;gap:var(--space-1)var(--space-2);flex-wrap:wrap}.schema-toggle,.schema-toggle-spacer{text-align:center;flex:none;width:1.25rem}.schema-toggle{color:var(--text-muted);cursor:pointer;font:inherit;background:0 0;border:0;padding:0}.schema-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:1px;border-radius:var(--radius-sm)}.schema-ref-link:focus-visible{outline:2px solid var(--accent);outline-offset:1px;border-radius:var(--radius-sm)}.schema-name{font-family:var(--font-mono);overflow-wrap:anywhere;font-weight:600}.schema-required{color:var(--danger-text);font-size:var(--text-xs);text-transform:uppercase;letter-spacing:.03em}.schema-type{font-family:var(--font-mono);color:var(--accent);overflow-wrap:anywhere}.schema-ref{font-family:var(--font-mono);font-size:var(--text-sm)}.schema-ref-link{color:var(--accent);text-decoration:underline;cursor:pointer;background:0 0;border:0;padding:0}.schema-note-inline{font-size:var(--text-xs);color:var(--text-muted);font-style:italic}.schema-unresolved{color:var(--danger-text)}.schema-fact{padding:0 var(--space-1);border-radius:var(--radius-sm);background:var(--surface-2);font-size:var(--text-xs);overflow-wrap:anywhere}.schema-fact-label{color:var(--text-muted)}.schema-description,.schema-closed,.schema-open{margin:0 0 0 calc(1.25rem + var(--space-2));color:var(--text-muted)}.schema-description p{margin:var(--space-1)0 0;overflow-wrap:anywhere}.schema-closed,.schema-open{font-size:var(--text-xs)}.schema-closed{color:var(--warning-text)}.schema-composition{padding-top:var(--space-1);color:var(--text-muted);font-size:var(--text-xs);text-transform:uppercase;letter-spacing:.03em;font-weight:600}.http-docs-title{display:flex;justify-content:space-between;align-items:baseline;gap:var(--space-2)var(--space-4);flex-wrap:wrap}.http-docs-layout{display:grid;grid-template-columns:minmax(14rem,20rem)minmax(0,1fr);gap:var(--space-4);align-items:start}@media (max-width:800px){.http-docs-layout{grid-template-columns:minmax(0,1fr)}}.http-muted{color:var(--text-muted)}.http-sidebar{position:sticky;top:var(--space-3);display:flex;gap:var(--space-2);overflow-y:auto;flex-direction:column;max-height:calc(100vh - 6rem)}@media (max-width:800px){.http-sidebar{position:static;max-height:22rem}}.http-sidebar-count{font-size:var(--text-xs);margin:0}.http-tag{margin-bottom:var(--space-3)}.http-section{margin-bottom:var(--space-4)}.http-section-name{margin:0 0 var(--space-2);font-size:var(--text-md);color:var(--text)}.http-section .http-tag{padding-left:var(--space-2);border-left:1px solid var(--border)}.http-overview-tag+.http-overview-tag{margin-top:var(--space-3)}.http-overview-tag-name,.http-tag-name{margin:0 0 var(--space-1);font-size:var(--text-sm);text-transform:uppercase;letter-spacing:.04em;color:var(--text-muted)}.http-tag-ops,.http-overview-ops,.http-params,.http-responses,.http-components{list-style:none;margin:0;padding:0}.http-op-link{display:flex;gap:var(--space-2);padding:var(--space-1)var(--space-2);border-radius:var(--radius-sm);color:var(--text);text-decoration:none;align-items:flex-start}.http-op-link:hover{background:var(--surface-2)}.http-op-link:focus-visible{background:var(--surface-2)}.http-op-link:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.http-op-link[aria-current=page]{background:var(--info-bg);color:var(--info-text)}.http-method{font-family:var(--font-mono);flex:none;justify-content:center;min-width:3.5rem}.http-op-text{display:flex;flex-direction:column;min-width:0}.http-op-path{font-family:var(--font-mono);font-size:var(--text-sm);overflow-wrap:anywhere}.http-op-summary{font-size:var(--text-xs);color:var(--text-muted)}.http-docs-main{display:flex;gap:var(--space-4);flex-direction:column;min-width:0}.http-info-line,.http-operation-line,.http-markers,.http-param-head,.http-response-head,.http-tryit-status,.http-tryit-actions{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}.http-servers{display:grid;gap:var(--space-2);margin:var(--space-3)0}.http-servers dt{font-size:var(--text-sm);color:var(--text-muted)}.http-servers dd{display:flex;gap:var(--space-2);overflow-wrap:anywhere;align-items:center;margin:0}.http-overview{display:flex;gap:var(--space-3);flex-direction:column}.http-overview-ops li{padding:var(--space-1)0;overflow-wrap:anywhere}.http-components{display:flex;gap:var(--space-2)var(--space-3);flex-wrap:wrap}.http-operation{display:flex;gap:var(--space-3);flex-direction:column}.http-operation-path{font-size:var(--text-md);overflow-wrap:anywhere}.http-operation-title{margin:var(--space-2)0 0;font-size:var(--text-lg)}.http-operation-id{display:flex;align-items:center;gap:var(--space-2);margin:var(--space-1)0 0;font-size:var(--text-sm);flex-wrap:wrap}.http-requires code{margin-right:var(--space-1)}.http-csrf,.http-tryit-disabled{padding:var(--space-2)var(--space-3);border-radius:var(--radius-sm);background:var(--info-bg);color:var(--info-text)}.http-csrf p{margin:0}.http-csrf p+p{margin-top:var(--space-1)}.http-tryit-disabled{margin:0 0 var(--space-3);background:var(--warning-bg);color:var(--warning-text)}.http-description p{margin:0 0 var(--space-2);overflow-wrap:anywhere}.http-section h3,.http-tryit h3{margin:0 0 var(--space-2);font-size:var(--text-md)}.http-param,.http-response{padding:var(--space-2)0;border-top:1px solid var(--border)}.http-param-name{font-weight:600}.http-param-description p,.http-param-style{margin:var(--space-1)0;font-size:var(--text-sm)}.http-media{margin-top:var(--space-2)}.http-media-type{display:inline-block;margin-bottom:var(--space-1);font-size:var(--text-xs);color:var(--text-muted)}.http-codes{display:flex;gap:var(--space-1);list-style:none;margin:var(--space-1)0 0;flex-wrap:wrap;padding:0}.http-codes code{padding:0 var(--space-1);border-radius:var(--radius-sm);background:var(--surface-2);font-size:var(--text-xs)}.http-tryit{padding:var(--space-3);border:1px solid var(--border);border-radius:var(--radius);background:var(--surface)}.http-tryit-form{display:flex;gap:var(--space-2);flex-direction:column}.http-tryit-checks{display:flex;gap:var(--space-1)var(--space-3);flex-wrap:wrap}.http-snippets{margin-top:var(--space-3)}.http-snippet{position:relative}.http-snippet .copy-button,.http-snippet>button{position:absolute;top:var(--space-2);right:var(--space-2)}.http-snippet pre{padding:var(--space-3);border-radius:var(--radius-sm);background:var(--surface-2);font-family:var(--font-mono);font-size:var(--text-xs);overflow-x:auto;white-space:pre;margin:0}.http-tryit-result{display:flex;gap:var(--space-2);margin-top:var(--space-3);padding-top:var(--space-3);border-top:1px solid var(--border);flex-direction:column}.http-tryit-result p{font-size:var(--text-sm);margin:0}.http-tryit-timing{font-size:var(--text-sm);color:var(--text-muted)}.http-tryit-headers summary{cursor:pointer;font-size:var(--text-sm)}.http-tryit-headers dl{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:var(--space-1)var(--space-3);margin:var(--space-2)0 0;font-size:var(--text-xs)}.http-tryit-headers dl>div{display:contents}.http-tryit-headers dd{overflow-wrap:anywhere;margin:0}.http-tryit-text{padding:var(--space-2);border-radius:var(--radius-sm);background:var(--surface-2);font-family:var(--font-mono);font-size:var(--text-xs);overflow-x:auto;white-space:pre-wrap;margin:0}.ws-docs{display:flex;gap:var(--space-4);flex-direction:column}.ws-header .screen-title{display:flex;align-items:center;gap:var(--space-2);overflow-wrap:anywhere;flex-wrap:wrap}.ws-kicker{margin:0}.ws-description summary{cursor:pointer}.ws-description p{overflow-wrap:anywhere;max-width:72ch}.ws-overview{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,22rem),1fr));gap:var(--space-4)}.ws-layout{display:grid;grid-template-columns:minmax(14rem,18rem)minmax(0,1fr);gap:var(--space-4);align-items:start}@media (max-width:800px){.ws-layout{grid-template-columns:minmax(0,1fr)}}.ws-sidebar{position:sticky;top:var(--space-4);max-height:calc(100vh - 2*var(--space-4));overflow-y:auto;padding:var(--space-3);border:1px solid var(--border);border-radius:var(--radius);background:var(--surface)}@media (max-width:800px){.ws-sidebar{position:static;max-height:22rem}}.ws-group+.ws-group{margin-top:var(--space-3)}.ws-group-title{margin:0 0 var(--space-1);font-size:var(--text-xs);letter-spacing:.04em;text-transform:uppercase;color:var(--text-muted);font-weight:600}.ws-group ul{list-style:none;margin:0;padding:0}.ws-nav-link{display:flex;padding:var(--space-1)var(--space-2);border-radius:var(--radius-sm);color:var(--text);text-decoration:none;flex-direction:column}.ws-nav-link:hover{background:var(--surface-2)}.ws-nav-link[data-selected=true]{background:var(--info-bg);color:var(--info-text)}.ws-nav-label{font-family:var(--font-mono);font-size:var(--text-sm);overflow-wrap:anywhere}.ws-nav-hint{font-size:var(--text-xs);color:var(--text-muted);overflow-wrap:anywhere}.ws-main,.ws-pane{display:flex;gap:var(--space-4);flex-direction:column;min-width:0}.ws-links{display:flex;gap:var(--space-1)var(--space-3);list-style:none;flex-wrap:wrap;margin:0;padding:0}.ws-permissions{display:flex;gap:var(--space-2);flex-wrap:wrap}.ws-permission{display:inline-flex;align-items:center;gap:var(--space-1);flex-wrap:wrap}.ws-note{display:block;padding:var(--space-2)var(--space-3);border-radius:var(--radius-sm);background:var(--warning-bg);color:var(--warning-text);overflow-wrap:anywhere}.ws-requirements{margin:0 0 var(--space-3);padding-left:var(--space-5)}.ws-table td{vertical-align:top;overflow-wrap:anywhere}.ws-value{white-space:nowrap;font-variant-numeric:tabular-nums}.ws-highlight{border-color:var(--accent);box-shadow:inset 3px 0 0 var(--accent)}.ws-schema{overflow-x:auto}.ws-example+.ws-example{margin-top:var(--space-3)}.ws-try{display:flex;align-items:flex-start;gap:var(--space-2);flex-direction:column}.ws-try p{margin:0}.ws-try-fields{display:flex;gap:var(--space-2)var(--space-3);flex-wrap:wrap}.ws-try-fields .field{min-width:12rem;margin:0}.ws-try-channel{overflow-wrap:anywhere}.ws-disabled{opacity:.55;cursor:not-allowed}.ws-example-head{display:flex;justify-content:space-between;align-items:center;gap:var(--space-2)}.ws-example-event{margin-block:var(--space-2);padding:var(--space-2)var(--space-3);border:1px solid var(--accent);border-radius:var(--radius,6px)}.ws-try-reason{padding-left:var(--space-4);margin:0}.runner-actions{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}.runner-actions-hint{font-size:var(--text-sm);color:var(--text-muted);flex-basis:100%;margin:0}.runner-form{display:grid;gap:var(--space-3)}.runner-form-row{display:flex;gap:var(--space-3);flex-wrap:wrap}.runner-form-row>.field{flex:10rem;min-width:0}.runner-note{padding:var(--space-2)var(--space-3);border-radius:var(--radius-sm);background:var(--surface-2);font-size:var(--text-sm);margin:0}.runner-note-pending{background:var(--warning-bg);color:var(--warning-text)}.schedule-modes{display:flex;gap:var(--space-2)var(--space-4);padding:var(--space-2)var(--space-3);border:1px solid var(--border);border-radius:var(--radius);flex-wrap:wrap;margin:0}.schedule-modes>legend{padding:0 var(--space-1);font-weight:600;font-size:var(--text-sm)}.schedule-mode{display:inline-flex;align-items:center;gap:var(--space-1);cursor:pointer}.run-id{display:inline-flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}.run-id code{overflow-wrap:anywhere}.runner-note-refused{background:var(--danger-bg);color:var(--danger-text)}.config-source{display:flex;align-items:center;gap:var(--space-2);margin:calc(var(--space-2)*-1)0 0;font-size:var(--text-sm);color:var(--text-muted);flex-wrap:wrap}.log-lines{margin:0 0 var(--space-3);padding:var(--space-2)0;overflow:auto;list-style:none;background:var(--surface-2);border-radius:var(--radius-sm);font-family:var(--font-mono);font-size:var(--text-xs);max-height:480px}.log-line{display:flex;gap:var(--space-3);padding:0 var(--space-3)}.log-number{text-align:right;color:var(--text-muted);user-select:none;flex:none;min-width:4ch}.log-text{white-space:pre-wrap;overflow-wrap:anywhere}.run-log-line{align-items:baseline;gap:var(--space-2)}.run-log-time,.run-log-stream{color:var(--text-muted);user-select:none;flex:none}.run-log-stream{text-transform:uppercase;font-size:calc(var(--text-xs)*.9);letter-spacing:.04em;min-width:6ch}.run-log-line-stderr .run-log-stream,.run-log-line-stderr .log-text{color:var(--danger-text)}.run-log-level,.run-log-cut{flex:none;align-self:center}.run-log-notes{display:grid;gap:var(--space-2);margin:0 0 var(--space-3)}.run-log-notes p{font-size:var(--text-xs);margin:0}.run-log-filter{display:inline-flex;align-items:center;gap:var(--space-2);font-size:var(--text-sm)}.run-log-toggle,.run-log-section{margin-top:var(--space-3)}.runner-link{overflow-wrap:anywhere;font-weight:600}.runner-id{margin-left:var(--space-2);font-size:var(--text-sm);overflow-wrap:anywhere}.runners-where{white-space:nowrap;width:7rem}.runners-status{white-space:nowrap;width:16rem}.runners-count{margin:var(--space-2)0 0;font-size:var(--text-sm)}.runner-screen{display:flex;gap:var(--space-4);flex-direction:column}.runner-header{display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-3);flex-wrap:wrap}.runner-heading{display:flex;align-items:baseline;gap:var(--space-2)var(--space-3);flex-wrap:wrap;min-width:0}.runner-title{overflow-wrap:anywhere}.runner-badges{display:inline-flex;gap:var(--space-1);flex-wrap:wrap}.runner-last-error{color:var(--danger-text);overflow-wrap:anywhere}.run-list{display:flex;gap:var(--space-3);list-style:none;flex-direction:column;margin:0;padding:0}.run-list>li+li{padding-top:var(--space-3);border-top:1px solid var(--border)}.run-record{display:flex;gap:var(--space-3);flex-direction:column;min-width:0}.run-id-line{display:inline-flex;align-items:center;gap:var(--space-2);overflow-wrap:anywhere;flex-wrap:wrap}.run-id{font-size:var(--text-sm);overflow-wrap:anywhere}.run-toggle{display:inline-flex;border-radius:var(--radius-sm);color:inherit;cursor:pointer;background:0 0;border:1px solid #0000;justify-content:center;align-items:center;min-width:1.75rem;min-height:1.75rem;padding:0}.run-toggle:hover{border-color:var(--border)}.run-toggle:focus-visible{outline:2px solid var(--focus);outline-offset:1px}.run-details-row>td{background:var(--surface-2)}.history-table .numeric{text-align:right;font-variant-numeric:tabular-nums}.history-actions{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap}.history-limit{display:flex;align-items:center;gap:var(--space-2);font-size:var(--text-sm)}.history-open-log-note{display:flex;align-items:center;gap:var(--space-2);margin:var(--space-3)0 0;font-size:var(--text-xs);flex-wrap:wrap}.run-logs-cell{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}.worker-server+.worker-server{margin-top:var(--space-4)}.worker-server-title{margin:0 0 var(--space-2);font-size:var(--text-sm);font-weight:600}.worker-service-note{margin:0 0 var(--space-3);font-size:var(--text-sm)}.workers-sweep-note{margin:0 0 var(--space-3);padding:var(--space-2)var(--space-3);border-left:3px solid var(--warning-text);border-radius:var(--radius-sm);background:var(--warning-bg);color:var(--warning-text);font-size:var(--text-sm)}.worker-filters{display:flex;align-items:flex-end;gap:var(--space-2)var(--space-3);flex-flow:wrap}.worker-filters .field{min-width:10rem;margin:0}.worker-key{margin-top:var(--space-1);font-size:var(--text-sm);font-weight:400}.worker-header{display:flex;gap:var(--space-2);flex-direction:column}.worker-header .screen-title{overflow-wrap:anywhere}.worker-config-summary{margin:0 0 var(--space-3)}.worker-analytics-title{margin:0 0 var(--space-2);font-size:var(--text-sm);font-weight:600}.worker-analytics>*+*{margin-top:var(--space-4)}.worker-target-note{margin:0 0 var(--space-3)}.worker-target-file{overflow-wrap:anywhere}.providers-list{display:grid;gap:var(--space-3)}.provider-test,.provider-schema{display:grid;gap:var(--space-2);margin-top:var(--space-3);justify-items:start}.provider-test-result p{margin:0}.provider-test-fail{color:var(--danger)}.provider-checks{margin:var(--space-1)0 0;padding-left:var(--space-3)}.job-defaults{display:flex;gap:var(--space-3);flex-direction:column}.job-defaults p{margin:0}.job-defaults-heading{font-size:var(--text-md);margin:0}.job-defaults-form{display:flex;gap:var(--space-3);flex-direction:column}.job-defaults-row{display:flex;align-items:flex-start;gap:var(--space-2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:var(--space-3);flex-direction:column;margin:0}.job-defaults-row>.field,.job-defaults-row>.field-row{align-self:stretch}.job-defaults-row legend{font-weight:600}.job-defaults-values,.job-defaults-note,.job-defaults-uses-code{font-size:var(--text-sm);margin:0}.job-defaults-reset{display:flex;align-items:center;gap:var(--space-2)var(--space-3);margin-top:var(--space-3);flex-wrap:wrap}.job-defaults-reset-note{font-size:var(--text-sm);color:var(--text-muted);flex:16rem;margin:0}.job-defaults-conflict,.job-defaults-warning{border-radius:var(--radius-sm);padding:var(--space-2)var(--space-3);background:var(--warning-bg);color:var(--warning-text)}.job-defaults-conflict{display:flex;align-items:flex-start;gap:var(--space-2);margin-bottom:var(--space-3);flex-direction:column}.job-defaults-conflict p{margin:0}.job-defaults-apply-states{display:flex;gap:var(--space-1);border:1px solid var(--border);border-radius:var(--radius-sm);padding:var(--space-3);flex-direction:column}.job-defaults-apply-keys{padding-left:var(--space-4);margin:0}.job-defaults-preview h3{margin:var(--space-2)0;font-size:var(--text-md)}[data-testid=apply-progress] progress{width:100%}.breadcrumb ol{display:flex;gap:var(--space-1);margin:0 0 var(--space-3);list-style:none;font-size:var(--text-sm);color:var(--text-muted);flex-wrap:wrap;padding:0}.breadcrumb li+li:before{content:"/";margin-right:var(--space-1);color:var(--text-muted)}.breadcrumb [aria-current=page]{color:var(--text)}.job-header{display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-3);margin-bottom:var(--space-4);flex-wrap:wrap}.job-heading{min-width:0}.job-title{display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-1);flex-wrap:wrap}.job-name{overflow-wrap:anywhere}.job-id-line{display:flex;align-items:center;gap:var(--space-2);font-size:var(--text-sm);flex-wrap:wrap;margin:0}.job-id{font-family:var(--font-mono);overflow-wrap:anywhere;word-break:break-all}.job-actions{display:flex;gap:var(--space-2);flex-wrap:wrap}.job-screen .card{margin-bottom:var(--space-4)}.job-error{display:grid;gap:var(--space-2)}.job-error-line{overflow-wrap:anywhere;margin:0}.job-error-name{color:var(--danger-text)}.job-error-code{margin-left:var(--space-2);font-size:var(--text-xs);color:var(--text-muted)}.job-error-stack{padding:var(--space-2)var(--space-3);overflow-x:auto;background:var(--surface-2);border-radius:var(--radius-sm);font-family:var(--font-mono);font-size:var(--text-xs);white-space:pre;max-height:320px;margin:0}.job-error-causes{display:grid;gap:var(--space-2);padding-left:var(--space-4);border-left:2px solid var(--border);list-style:none;margin:0}.job-error-caused-by{display:block;font-size:var(--text-xs);text-transform:uppercase;letter-spacing:.04em}.job-details{margin-top:var(--space-3)}.job-details>summary{cursor:pointer;margin-bottom:var(--space-2);font-weight:600}.job-stacktraces{display:grid;gap:var(--space-3);list-style:none;margin:0;padding:0}.job-logs-controls{display:inline-flex;justify-content:flex-end;align-items:center;gap:var(--space-3);flex-wrap:wrap}.job-logs-clear{display:inline-flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}.job-logs-clear-reason{font-size:var(--text-xs)}.job-logs-order{display:inline-flex;align-items:center;gap:var(--space-2);font-size:var(--text-sm)}.job-logs-follow,.job-logs-total{margin:0 0 var(--space-2);font-size:var(--text-xs)}.job-flow-summary{margin-bottom:var(--space-3)}.job-ref code{font-family:var(--font-mono);overflow-wrap:anywhere}.job-form{display:grid;gap:var(--space-3)}.job-form-options,.job-states-picker{display:grid;gap:var(--space-3);padding:var(--space-3);border:1px solid var(--border);border-radius:var(--radius);margin:0}.job-states-picker{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:var(--space-2)}.job-form-options>legend,.job-states-picker>legend{padding:0 var(--space-1);font-weight:600;font-size:var(--text-sm)}.job-form-error{margin:0}@media (min-width:640px){.job-form-options{grid-template-columns:repeat(2,minmax(0,1fr))}.job-form-options>legend{grid-column:1/-1}}.job-note{padding:var(--space-2)var(--space-3);border-radius:var(--radius-sm);background:var(--surface-2);font-size:var(--text-sm);margin:0}.job-note-warning{background:var(--warning-bg);color:var(--warning-text)}.job-progress{display:inline-flex;align-items:center;gap:var(--space-2)}.job-progress-bar{display:inline-block;overflow:hidden;border-radius:var(--radius-sm);background:var(--surface-2);width:8rem;height:.5rem}.job-progress-fill{display:block;background:var(--accent);height:100%}.job-progress-value{font-variant-numeric:tabular-nums}.job-processed-by{display:inline-flex;align-items:baseline;gap:var(--space-1)var(--space-2);overflow-wrap:anywhere;flex-wrap:wrap}.flow-count{font-size:var(--text-sm);margin:0}.flow-tree{display:grid;gap:var(--space-3);list-style:none;margin:0;padding:0}.flow-children{margin-top:var(--space-3);padding-left:var(--space-4);border-left:2px solid var(--border)}.flow-node{display:grid;gap:var(--space-3);padding:var(--space-3);border:1px solid var(--border);border-radius:var(--radius);min-width:0;margin:0}.flow-node>legend{padding:0 var(--space-1);font-weight:600}.flow-node[data-refused]{border-color:var(--danger)}.flow-node-queue>p{margin:0}.flow-node-actions{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}.flow-node-blocked{flex-basis:100%;margin:0}.flow-result{display:grid;gap:var(--space-2)}.flow-result>p,.flow-result-list{margin:0}
|