@kky42/pievo-server 2.3.0 → 2.4.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/.output/nitro.json +1 -1
- package/.output/public/assets/MachinesModal-CSbnrUER.js +1 -0
- package/.output/public/assets/SignIn-0Tjqs-Ma.js +1 -0
- package/.output/public/assets/actionUi-DpwGIGpw.js +1 -0
- package/.output/public/assets/app-CaZpuwOS.css +2 -0
- package/.output/public/assets/index-CaS_SmDq.js +10 -0
- package/.output/public/assets/invite._token-Btg_0aLp.js +1 -0
- package/.output/public/assets/invite._token-KMD2JAgE.js +1 -0
- package/.output/public/assets/lifecycleUi-C7dwHtbw.js +1 -0
- package/.output/public/assets/loops._loopId-D5usF3DO.js +67 -0
- package/.output/public/assets/loops._loopId_.runs._runId-DRCgXaCq.js +3 -0
- package/.output/public/assets/preload-helper-DsxECZIS.js +5 -0
- package/.output/public/assets/routes-CtFgwdgW.js +1 -0
- package/.output/public/assets/routes-LDbnnQ3f.js +1 -0
- package/.output/public/assets/t._teamId-D41xcbsF.js +1 -0
- package/.output/public/assets/t._teamId-DUena1pV.js +1 -0
- package/.output/public/assets/ui-Do4gqMyP.js +1 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +4130 -4130
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +1 -1
- package/.output/server/_libs/drizzle-orm+postgres.mjs +28 -1
- package/.output/server/_ssr/DashboardView-C2LdV6J0.mjs +914 -0
- package/.output/server/_ssr/{notifyFns-DuPR1zd7.mjs → MachinesModal-BjkcVjjx.mjs} +55 -35
- package/.output/server/_ssr/{SignIn-RmpWm9uy.mjs → SignIn-DEGrvJvb.mjs} +10 -24
- package/.output/server/_ssr/SocialLinks-Cm4H8pUJ.mjs +1044 -0
- package/.output/server/_ssr/{actionUi-BN55TNYE.mjs → actionUi-DJBWuNwr.mjs} +3 -3
- package/.output/server/_ssr/{artifactFiles-BCaIIrrT.mjs → artifactFiles-DtpRidFZ.mjs} +7 -8
- package/.output/server/_ssr/{artifactKind-BW0isV8L.mjs → artifactKind-FBYbgSuf.mjs} +27 -2
- package/.output/server/_ssr/artifacts-BR3jj0NC.mjs +2 -0
- package/.output/server/_ssr/{auth-30Z4lV1w.mjs → auth-C0zoZCGe.mjs} +16 -19
- package/.output/server/_ssr/boot-CdOO4xOo.mjs +2 -0
- package/.output/server/_ssr/{boot-3BNKujyp.mjs → boot-sNlIkoLe.mjs} +1331 -3927
- package/.output/server/_ssr/{bun-sqlite-dialect-BW9W1_Ps-_7tfuNRi.mjs → bun-sqlite-dialect-BW9W1_Ps-DmlZWqO7.mjs} +2 -2
- package/.output/server/_ssr/{d1-sqlite-dialect-BLC8LXE6-ucDUbeBk.mjs → d1-sqlite-dialect-BLC8LXE6-CCNzqZwZ.mjs} +2 -2
- package/.output/server/_ssr/db-CwLD-5w1.mjs +2 -0
- package/.output/server/_ssr/{dist-Cd6nlLlO.mjs → dist-B3SxDOpW.mjs} +2 -2
- package/.output/server/_ssr/{factory-CWWwV7XZ.mjs → factory-Dv3Zopis.mjs} +2 -2
- package/.output/server/_ssr/invite._token-CJ6-H-s3.mjs +20 -0
- package/.output/server/_ssr/{invite._token-SdWXemIc.mjs → invite._token-dLKDJKVs.mjs} +9 -17
- package/.output/server/_ssr/{invite._token-BKlz0R2f.mjs → invite._token-l1SjouDz.mjs} +6 -15
- package/.output/server/_ssr/{kysely-adapter-C8iS51DB.mjs → kysely-adapter-nL7WKSgw.mjs} +1 -1
- package/.output/server/_ssr/{lifecycleUi-DgBQAYzp.mjs → lifecycleUi-Dad3hvhm.mjs} +11 -11
- package/.output/server/_ssr/logger-B8m8F1Qu.mjs +2 -0
- package/.output/server/_ssr/loopApi-BKyFB4KM.mjs +342 -0
- package/.output/server/_ssr/{adapters-BoLkOf1T.mjs → loopProjection-B8a829ij.mjs} +25 -55
- package/.output/server/_ssr/loops._loopId-DS16Phmq.mjs +1271 -0
- package/.output/server/_ssr/{loops._loopId_.runs._runId-PUGBrD5q.mjs → loops._loopId_.runs._runId-BEJI6iK5.mjs} +23 -74
- package/.output/server/_ssr/{loops._loopId_.runs._runId-Ymy4lwee.mjs → loops._loopId_.runs._runId-CduETLqH.mjs} +4 -5
- package/.output/server/_ssr/{machineFns-BZ9puxDt.mjs → machineFns-ByMsgG12.mjs} +3 -5
- package/.output/server/_ssr/{node-sqlite-dialect-D_3JmkMo.mjs → node-sqlite-dialect-CjNineno.mjs} +2 -2
- package/.output/server/_ssr/router-CXyrnvx-.mjs +565 -0
- package/.output/server/_ssr/{routes-ClZEdsnB.mjs → routes-BulNEUEE.mjs} +6 -10
- package/.output/server/_ssr/{routes-ComqHvCL.mjs → routes-DD94baie.mjs} +3 -3
- package/.output/server/_ssr/{routes-D5jJJ-30.mjs → routes-Wjexi35W.mjs} +6 -6
- package/.output/server/_ssr/runDiff-Ba8Lg1vg.mjs +2 -0
- package/.output/server/_ssr/ssr.mjs +60 -100
- package/.output/server/_ssr/store-Cujtg2QE.mjs +2 -0
- package/.output/server/_ssr/{t._teamId-BgHV_KFP.mjs → t._teamId-1CreIlPV.mjs} +7 -11
- package/.output/server/_ssr/{t._teamId-DDERNgfn.mjs → t._teamId-Bd6-zZsy.mjs} +4 -4
- package/.output/server/_ssr/{t._teamId-DRJvKHZn.mjs → t._teamId-COuMKQ4r.mjs} +7 -7
- package/.output/server/_ssr/{teamFns-Bp81UO-x.mjs → teamFns-H438WnZC.mjs} +12 -30
- package/.output/server/_ssr/{teamFns-bm82twd5.mjs → teamFns-j2JKjorZ.mjs} +2 -2
- package/.output/server/_ssr/tokens-DgG_Syq3.mjs +2 -0
- package/.output/server/_ssr/{ui-Ww2tTM2u.mjs → ui-D5KWstlb.mjs} +26 -48
- package/.output/server/{_tanstack-start-manifest_v-DwKENYCN.mjs → _tanstack-start-manifest_v-3H7yBKLz.mjs} +25 -35
- package/{drizzle/0000_round_marauders.sql → .output/server/drizzle/0000_baseline.sql} +102 -44
- package/.output/server/drizzle/meta/_journal.json +3 -115
- package/.output/server/index.mjs +83 -132
- package/{.output/server/drizzle/0000_round_marauders.sql → drizzle/0000_baseline.sql} +102 -44
- package/drizzle/meta/_journal.json +3 -115
- package/package.json +2 -11
- package/scripts/pievo-server.mjs +3 -1
- package/scripts/server-cli-lib.mjs +23 -16
- package/.output/public/assets/CodeField-dOzyFQNH.js +0 -34
- package/.output/public/assets/LoopView-DjVHUyrR.js +0 -59
- package/.output/public/assets/Modal-CCRvx959.js +0 -1
- package/.output/public/assets/SignIn-Qxw5zDFq.js +0 -1
- package/.output/public/assets/actionUi-CXxhzYe2.js +0 -1
- package/.output/public/assets/app-D1cij_NJ.css +0 -2
- package/.output/public/assets/artifactView-Cd5wWPil.js +0 -66
- package/.output/public/assets/index-BRN_CQ8U.js +0 -12
- package/.output/public/assets/invite._token-BIxr8DlP.js +0 -1
- package/.output/public/assets/invite._token-j2BTCeG1.js +0 -1
- package/.output/public/assets/jsx-runtime-BseJUIpC.js +0 -1
- package/.output/public/assets/lifecycleUi-Qy_8beQK.js +0 -1
- package/.output/public/assets/loopApi-CT1mMhlb.js +0 -3
- package/.output/public/assets/loops._loopId-BRBQzioy.js +0 -4
- package/.output/public/assets/loops._loopId_.runs._runId-CMnOJkem.js +0 -3
- package/.output/public/assets/notifyFns-DYD4EKaG.js +0 -1
- package/.output/public/assets/preload-helper-DFkkpP6W.js +0 -3
- package/.output/public/assets/routes-3nuEJGdo.js +0 -1
- package/.output/public/assets/routes-DORLim-C.js +0 -1
- package/.output/public/assets/t._teamId-F5f4K8kd.js +0 -1
- package/.output/public/assets/t._teamId-XfE_S1Fo.js +0 -1
- package/.output/public/assets/ui-Dhsx6SW0.js +0 -1
- package/.output/public/assets/useRouter-DsjH8G0p.js +0 -1
- package/.output/server/_libs/@codemirror/autocomplete+[...].mjs +0 -19713
- package/.output/server/_libs/@codemirror/lang-css+[...].mjs +0 -2636
- package/.output/server/_libs/@codemirror/lang-html+[...].mjs +0 -2247
- package/.output/server/_libs/@codemirror/lang-json+[...].mjs +0 -79
- package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +0 -1736
- package/.output/server/_libs/@uiw/codemirror-extensions-basic-setup+[...].mjs +0 -76
- package/.output/server/_libs/babel__runtime.mjs +0 -30
- package/.output/server/_libs/clsx.mjs +0 -16
- package/.output/server/_libs/codemirror__commands.mjs +0 -1725
- package/.output/server/_libs/codemirror__lint.mjs +0 -565
- package/.output/server/_libs/codemirror__search.mjs +0 -1138
- package/.output/server/_libs/codemirror__theme-one-dark.mjs +0 -161
- package/.output/server/_libs/d3-array.mjs +0 -231
- package/.output/server/_libs/d3-color.mjs +0 -330
- package/.output/server/_libs/d3-format.mjs +0 -252
- package/.output/server/_libs/d3-interpolate.mjs +0 -163
- package/.output/server/_libs/d3-path.mjs +0 -77
- package/.output/server/_libs/d3-scale+[...].mjs +0 -1907
- package/.output/server/_libs/d3-shape.mjs +0 -818
- package/.output/server/_libs/decimal.js-light.mjs +0 -884
- package/.output/server/_libs/dom-serializer+[...].mjs +0 -708
- package/.output/server/_libs/domhandler.mjs +0 -461
- package/.output/server/_libs/domutils.mjs +0 -868
- package/.output/server/_libs/es-toolkit.mjs +0 -1737
- package/.output/server/_libs/eventemitter3.mjs +0 -236
- package/.output/server/_libs/html-dom-parser+htmlparser2.mjs +0 -1560
- package/.output/server/_libs/html-react-parser+[...].mjs +0 -1360
- package/.output/server/_libs/react-is.mjs +0 -48
- package/.output/server/_libs/react-redux+[...].mjs +0 -205
- package/.output/server/_libs/recharts+victory-vendor.mjs +0 -17303
- package/.output/server/_libs/uiw__codemirror-themes.mjs +0 -32
- package/.output/server/_libs/uiw__react-codemirror.mjs +0 -354
- package/.output/server/_ssr/CodeField-B7oOmmwH.mjs +0 -143
- package/.output/server/_ssr/DashboardView-BF7PciP7.mjs +0 -2736
- package/.output/server/_ssr/LoopPlaybook-DC7QVvPY.mjs +0 -2105
- package/.output/server/_ssr/LoopView-DwSrm9Ja.mjs +0 -1300
- package/.output/server/_ssr/Modal-MaCG52Pv.mjs +0 -49
- package/.output/server/_ssr/artifactView-iuPsp5aa.mjs +0 -304
- package/.output/server/_ssr/artifacts-jQ-bfdhI.mjs +0 -2
- package/.output/server/_ssr/boot-CsEFT53K.mjs +0 -2
- package/.output/server/_ssr/db-CXownd1H.mjs +0 -2
- package/.output/server/_ssr/invite._token-CWl6kipD.mjs +0 -28
- package/.output/server/_ssr/logger-Gy-47z3k.mjs +0 -2
- package/.output/server/_ssr/loopApi-8_KgBapr.mjs +0 -109
- package/.output/server/_ssr/loopApi-ABi9SgWS.mjs +0 -564
- package/.output/server/_ssr/loops._loopId-SON_t43g.mjs +0 -30
- package/.output/server/_ssr/loops._loopId-p1KWsvMM.mjs +0 -1477
- package/.output/server/_ssr/notifyFns-BUCePuF3.mjs +0 -118
- package/.output/server/_ssr/router-Czxvra6l.mjs +0 -685
- package/.output/server/_ssr/runDiff--qLtU4FI.mjs +0 -2
- package/.output/server/_ssr/store-40eVzW1S.mjs +0 -2
- package/.output/server/_ssr/tokens-CbfoRC_s.mjs +0 -2
- package/.output/server/drizzle/0001_naive_silver_fox.sql +0 -25
- package/.output/server/drizzle/0002_funny_sage.sql +0 -12
- package/.output/server/drizzle/0003_optimal_slapstick.sql +0 -102
- package/.output/server/drizzle/0004_remove_grok_agent.sql +0 -5
- package/.output/server/drizzle/0005_heavy_gauntlet.sql +0 -8
- package/.output/server/drizzle/0006_tranquil_sharon_carter.sql +0 -1
- package/.output/server/drizzle/0007_faulty_starbolt.sql +0 -30
- package/.output/server/drizzle/0008_durable_lifecycle_evidence.sql +0 -15
- package/.output/server/drizzle/0009_ordinary_electro.sql +0 -1
- package/.output/server/drizzle/0010_workable_rage.sql +0 -14
- package/.output/server/drizzle/0011_steep_marvel_zombies.sql +0 -3
- package/.output/server/drizzle/0012_blocked_run_status.sql +0 -12
- package/.output/server/drizzle/0013_drop_run_outcome.sql +0 -2
- package/.output/server/drizzle/0014_luxuriant_scrambler.sql +0 -5
- package/.output/server/drizzle/0015_run_history_foundation.sql +0 -43
- package/.output/server/drizzle/0016_plural_machine_runs.sql +0 -4
- package/.output/server/drizzle/meta/0000_snapshot.json +0 -1333
- package/.output/server/drizzle/meta/0001_snapshot.json +0 -1498
- package/.output/server/drizzle/meta/0002_snapshot.json +0 -1576
- package/.output/server/drizzle/meta/0003_snapshot.json +0 -1666
- package/.output/server/drizzle/meta/0005_snapshot.json +0 -1666
- package/.output/server/drizzle/meta/0006_snapshot.json +0 -1672
- package/.output/server/drizzle/meta/0007_snapshot.json +0 -1787
- package/.output/server/drizzle/meta/0009_snapshot.json +0 -1793
- package/.output/server/drizzle/meta/0010_snapshot.json +0 -1891
- package/.output/server/drizzle/meta/0011_snapshot.json +0 -1872
- package/drizzle/0001_naive_silver_fox.sql +0 -25
- package/drizzle/0002_funny_sage.sql +0 -12
- package/drizzle/0003_optimal_slapstick.sql +0 -102
- package/drizzle/0004_remove_grok_agent.sql +0 -5
- package/drizzle/0005_heavy_gauntlet.sql +0 -8
- package/drizzle/0006_tranquil_sharon_carter.sql +0 -1
- package/drizzle/0007_faulty_starbolt.sql +0 -30
- package/drizzle/0008_durable_lifecycle_evidence.sql +0 -15
- package/drizzle/0009_ordinary_electro.sql +0 -1
- package/drizzle/0010_workable_rage.sql +0 -14
- package/drizzle/0011_steep_marvel_zombies.sql +0 -3
- package/drizzle/0012_blocked_run_status.sql +0 -12
- package/drizzle/0013_drop_run_outcome.sql +0 -2
- package/drizzle/0014_luxuriant_scrambler.sql +0 -5
- package/drizzle/0015_run_history_foundation.sql +0 -43
- package/drizzle/0016_plural_machine_runs.sql +0 -4
- package/drizzle/meta/0000_snapshot.json +0 -1333
- package/drizzle/meta/0001_snapshot.json +0 -1498
- package/drizzle/meta/0002_snapshot.json +0 -1576
- package/drizzle/meta/0003_snapshot.json +0 -1666
- package/drizzle/meta/0005_snapshot.json +0 -1666
- package/drizzle/meta/0006_snapshot.json +0 -1672
- package/drizzle/meta/0007_snapshot.json +0 -1787
- package/drizzle/meta/0009_snapshot.json +0 -1793
- package/drizzle/meta/0010_snapshot.json +0 -1891
- package/drizzle/meta/0011_snapshot.json +0 -1872
|
@@ -1,2736 +0,0 @@
|
|
|
1
|
-
import { o as __toESM } from "../_runtime.mjs";
|
|
2
|
-
import { g as require_jsx_runtime, h as require_react, t as TooltipProvider } from "../_libs/@base-ui/react+[...].mjs";
|
|
3
|
-
import { C as lastRunOf, M as useHydrated, S as labelCls, T as rel, c as btnDanger, f as btnSm, h as dotLabel, k as selectCls, l as btnPrimary, o as btn, p as cronText, r as Pill, t as ErrorBanner, u as btnPrimaryPill, x as inputCls } from "./ui-Ww2tTM2u.mjs";
|
|
4
|
-
import { d as useNavigate } from "../_libs/@tanstack/react-router+[...].mjs";
|
|
5
|
-
import { _ as loadOlderRuns, h as listMyTeams, m as listJobs, n as claimStatus, u as getConfig, v as mintClaim } from "./loopApi-8_KgBapr.mjs";
|
|
6
|
-
import { n as ModalHead, r as ModalSection, t as Modal } from "./Modal-MaCG52Pv.mjs";
|
|
7
|
-
import { i as PievoLogo, n as GitHubIcon, r as LoopPlaybook, t as GITHUB_URL } from "./LoopPlaybook-DC7QVvPY.mjs";
|
|
8
|
-
import { t as setActiveTeamCookie } from "./teamCookie-B7hLOIie.mjs";
|
|
9
|
-
import { a as getTeamDetail, d as revokeTeamInvite, f as setTeamMemberRole, i as deleteTeam, l as removeTeamMember, n as createTeam, o as leaveTeam, r as createTeamInvite, s as listManagedTeams, t as addTeamMember, u as renameTeam } from "./teamFns-bm82twd5.mjs";
|
|
10
|
-
import { r as deriveLoopLifecycle } from "./lifecycleUi-DgBQAYzp.mjs";
|
|
11
|
-
import { a as listChannels, c as testChannel, i as deleteChannel, n as Timeline, o as listMachines, r as createChannel, s as mergeRuns, t as MachinesModal } from "./notifyFns-DuPR1zd7.mjs";
|
|
12
|
-
//#region node_modules/.nitro/vite/services/ssr/assets/DashboardView-BF7PciP7.js
|
|
13
|
-
var import_react = /* @__PURE__ */ __toESM(require_react());
|
|
14
|
-
var import_jsx_runtime = require_jsx_runtime();
|
|
15
|
-
function LoopCard({ job, onOpen, onPickRun }) {
|
|
16
|
-
const en = job.enabled;
|
|
17
|
-
const last = lastRunOf(job);
|
|
18
|
-
const lifecycle = deriveLoopLifecycle(job);
|
|
19
|
-
const hydrated = useHydrated();
|
|
20
|
-
const [older, setOlder] = (0, import_react.useState)([]);
|
|
21
|
-
const runs = (0, import_react.useMemo)(() => older.length ? mergeRuns(job.runs ?? [], older) : job.runs ?? [], [job.runs, older]);
|
|
22
|
-
const newestRun = runs.at(-1);
|
|
23
|
-
const latestIncidentRun = newestRun?.reportIncident ? newestRun : void 0;
|
|
24
|
-
const loadMore = async () => {
|
|
25
|
-
const oldest = runs[0];
|
|
26
|
-
if (!oldest) return 0;
|
|
27
|
-
const more = await loadOlderRuns({ data: {
|
|
28
|
-
loopId: job.id,
|
|
29
|
-
beforeTs: oldest.ts,
|
|
30
|
-
limit: 16
|
|
31
|
-
} });
|
|
32
|
-
if (more.length) setOlder((prev) => mergeRuns(prev, more));
|
|
33
|
-
return more.length;
|
|
34
|
-
};
|
|
35
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
36
|
-
onClick: () => onOpen(job.id),
|
|
37
|
-
className: `mb-[18px] cursor-pointer rounded-card border border-hairline bg-surface px-[26px] pb-5 pt-[22px] shadow-card transition-colors hover:border-wire ${en || latestIncidentRun?.reportIncident || job.pauseCause?.kind === "failure-streak" || job.pauseCause?.kind === "blocked" ? "" : "opacity-60"}`,
|
|
38
|
-
style: { animation: "fadeIn .25s cubic-bezier(0.25,0.1,0.25,1) both" },
|
|
39
|
-
children: [
|
|
40
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
41
|
-
className: "mb-6 flex flex-wrap items-center gap-3",
|
|
42
|
-
children: [
|
|
43
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
44
|
-
type: "button",
|
|
45
|
-
onClick: (e) => {
|
|
46
|
-
e.stopPropagation();
|
|
47
|
-
onOpen(job.id);
|
|
48
|
-
},
|
|
49
|
-
className: "cursor-pointer rounded-sm text-left text-[17px] font-semibold tracking-[-0.01em] text-display outline-none focus-visible:ring-2 focus-visible:ring-interactive focus-visible:ring-offset-4 focus-visible:ring-offset-surface",
|
|
50
|
-
children: job.name
|
|
51
|
-
}),
|
|
52
|
-
lifecycle === "stopping" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pill, {
|
|
53
|
-
tone: "running",
|
|
54
|
-
dot: "pulse",
|
|
55
|
-
children: "Stopping"
|
|
56
|
-
}) : lifecycle === "paused-finishing" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pill, { children: job.pauseCause?.kind === "blocked" ? "Paused — blocked · current run finishing" : job.pauseCause?.kind === "owner" ? "Paused by owner · current run finishing" : "Paused · current run finishing" }) : lifecycle === "deleting" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pill, { children: "Deleting" }) : job.running ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pill, {
|
|
57
|
-
tone: "running",
|
|
58
|
-
dot: "pulse",
|
|
59
|
-
children: "Running"
|
|
60
|
-
}) : job.queued && job.reconciliationBlocking ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pill, {
|
|
61
|
-
tone: "outline",
|
|
62
|
-
children: "Queued · waiting for machine recovery"
|
|
63
|
-
}) : job.queued ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pill, {
|
|
64
|
-
tone: "outline",
|
|
65
|
-
children: "Queued"
|
|
66
|
-
}) : null,
|
|
67
|
-
job.graduation && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pill, { children: job.graduation }),
|
|
68
|
-
job.goal && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pill, {
|
|
69
|
-
tone: "success",
|
|
70
|
-
title: job.goal ?? void 0,
|
|
71
|
-
children: "Objective"
|
|
72
|
-
}),
|
|
73
|
-
lifecycle === "paused" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pill, {
|
|
74
|
-
tone: job.pauseCause?.kind === "blocked" ? "accent" : void 0,
|
|
75
|
-
children: job.pauseCause?.kind === "blocked" ? "Paused — blocked" : job.pauseCause?.kind === "failure-streak" ? "Paused automatically" : job.pauseCause?.kind === "owner" ? "Paused by owner" : "Paused"
|
|
76
|
-
}),
|
|
77
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
78
|
-
className: "ml-auto min-w-0 text-right text-meta text-secondary",
|
|
79
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
80
|
-
className: "whitespace-nowrap",
|
|
81
|
-
children: [
|
|
82
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
83
|
-
className: "text-primary",
|
|
84
|
-
title: job.cron,
|
|
85
|
-
children: job.scheduleMode === "continuous" ? `continuous · ${job.continuousDelayMinutes}m` : cronText(job.cron)
|
|
86
|
-
}),
|
|
87
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
88
|
-
className: "mx-2.5 text-wire",
|
|
89
|
-
children: "·"
|
|
90
|
-
}),
|
|
91
|
-
job.kind
|
|
92
|
-
]
|
|
93
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
94
|
-
className: "mt-1 truncate text-caption text-disabled",
|
|
95
|
-
title: `Model: ${job.model || "default"} · Reasoning: ${job.reasoningEffort || "default"}`,
|
|
96
|
-
children: [
|
|
97
|
-
"Model: ",
|
|
98
|
-
job.model || "default",
|
|
99
|
-
" · Reasoning: ",
|
|
100
|
-
job.reasoningEffort || "default"
|
|
101
|
-
]
|
|
102
|
-
})]
|
|
103
|
-
})
|
|
104
|
-
]
|
|
105
|
-
}),
|
|
106
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Timeline, {
|
|
107
|
-
job,
|
|
108
|
-
runs,
|
|
109
|
-
total: job.runCount,
|
|
110
|
-
onLoadMore: loadMore,
|
|
111
|
-
onPickRun: (run) => onPickRun(job.id, run)
|
|
112
|
-
}),
|
|
113
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
114
|
-
className: "mt-[18px] flex items-center gap-2 text-label text-secondary",
|
|
115
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [job.runCount, " runs"] }), last && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
|
|
116
|
-
"· last ",
|
|
117
|
-
dotLabel(last),
|
|
118
|
-
hydrated && ` · ${rel(last.ts)}`
|
|
119
|
-
] })]
|
|
120
|
-
}),
|
|
121
|
-
latestIncidentRun?.reportIncident && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
122
|
-
className: "mt-1.5 text-label text-secondary",
|
|
123
|
-
children: latestIncidentRun.phase === "error" ? "Last run failed · Terminal report rejected" : "Last run telemetry warning · Terminal report rejected"
|
|
124
|
-
})
|
|
125
|
-
]
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Header team entry. Always visible when team data exists: a single-team user
|
|
130
|
-
* sees a quiet pill naming the active team (so the workspace context is never
|
|
131
|
-
* invisible); anyone who can reach more than one team gets the select.
|
|
132
|
-
*
|
|
133
|
-
* Switching NAVIGATES to `/t/<id>` (the explicit team URL — bookmarkable, and
|
|
134
|
-
* each tab keeps its own team). The cookie is still written, now only as the
|
|
135
|
-
* last-used default that the bare `/` redirect falls back to (no longer an
|
|
136
|
-
* authorization key). The navigation (and the route's `key={teamId}` remount) is
|
|
137
|
-
* what re-scopes the dashboard, so no refresh callback is needed.
|
|
138
|
-
*/
|
|
139
|
-
function TeamSwitcher({ data }) {
|
|
140
|
-
const navigate = useNavigate();
|
|
141
|
-
if (!data || data.teams.length === 0) return null;
|
|
142
|
-
const selectTeam = (id) => {
|
|
143
|
-
setActiveTeamCookie(id);
|
|
144
|
-
navigate({
|
|
145
|
-
to: "/t/$teamId",
|
|
146
|
-
params: { teamId: id }
|
|
147
|
-
});
|
|
148
|
-
};
|
|
149
|
-
if (data.teams.length === 1) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
150
|
-
title: "Active team",
|
|
151
|
-
className: "inline-flex items-center rounded-full bg-raised px-3 py-1 text-label font-medium text-secondary",
|
|
152
|
-
children: data.teams[0].name
|
|
153
|
-
});
|
|
154
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("select", {
|
|
155
|
-
"aria-label": "Active team",
|
|
156
|
-
value: data.activeTeamId,
|
|
157
|
-
onChange: (e) => selectTeam(e.target.value),
|
|
158
|
-
className: "lp-select cursor-pointer rounded-full bg-raised py-1 pl-3 text-label font-medium text-secondary outline-none transition-colors hover:text-display",
|
|
159
|
-
children: data.teams.map((t) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
160
|
-
value: t.id,
|
|
161
|
-
children: t.name
|
|
162
|
-
}, t.id))
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
var FORMS = {
|
|
166
|
-
telegram: {
|
|
167
|
-
label: "Telegram",
|
|
168
|
-
help: "Create a bot with @BotFather, then DM it once so it can message you.",
|
|
169
|
-
fields: [{
|
|
170
|
-
key: "botToken",
|
|
171
|
-
label: "Bot token",
|
|
172
|
-
ph: "123456:ABC-DEF…"
|
|
173
|
-
}, {
|
|
174
|
-
key: "chatId",
|
|
175
|
-
label: "Chat id",
|
|
176
|
-
ph: "e.g. 87654321 (message @userinfobot to get yours)"
|
|
177
|
-
}]
|
|
178
|
-
},
|
|
179
|
-
feishu: {
|
|
180
|
-
label: "Feishu",
|
|
181
|
-
help: "In a Feishu group: 设置 → 机器人 → 添加自定义机器人. Pick a security setting (signing recommended; paste the secret below if you enabled 签名校验).",
|
|
182
|
-
fields: [{
|
|
183
|
-
key: "webhookUrl",
|
|
184
|
-
label: "Webhook URL",
|
|
185
|
-
ph: "https://open.feishu.cn/open-apis/bot/v2/hook/…"
|
|
186
|
-
}, {
|
|
187
|
-
key: "secret",
|
|
188
|
-
label: "Signing secret (optional)",
|
|
189
|
-
ph: "only if you enabled 签名校验",
|
|
190
|
-
optional: true
|
|
191
|
-
}]
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
var ADDABLE = ["telegram", "feishu"];
|
|
195
|
-
function typeLabel(type) {
|
|
196
|
-
return FORMS[type]?.label ?? (type === "slack" ? "Slack" : type);
|
|
197
|
-
}
|
|
198
|
-
function NotificationsModal({ open, onClose }) {
|
|
199
|
-
const [channels, setChannels] = (0, import_react.useState)([]);
|
|
200
|
-
const [adding, setAdding] = (0, import_react.useState)(null);
|
|
201
|
-
const [name, setName] = (0, import_react.useState)("");
|
|
202
|
-
const [fields, setFields] = (0, import_react.useState)({});
|
|
203
|
-
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
204
|
-
const [err, setErr] = (0, import_react.useState)(null);
|
|
205
|
-
const [test, setTest] = (0, import_react.useState)({});
|
|
206
|
-
const load = (0, import_react.useCallback)(async () => {
|
|
207
|
-
try {
|
|
208
|
-
setChannels(await listChannels());
|
|
209
|
-
} catch {}
|
|
210
|
-
}, []);
|
|
211
|
-
(0, import_react.useEffect)(() => {
|
|
212
|
-
if (!open) {
|
|
213
|
-
setErr(null);
|
|
214
|
-
setAdding(null);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
load();
|
|
218
|
-
}, [open, load]);
|
|
219
|
-
function startAdd(type) {
|
|
220
|
-
setName("");
|
|
221
|
-
setFields({});
|
|
222
|
-
setErr(null);
|
|
223
|
-
setAdding(type);
|
|
224
|
-
}
|
|
225
|
-
const spec = adding ? FORMS[adding] : null;
|
|
226
|
-
const filled = !!name.trim() && !!spec && spec.fields.every((f) => f.optional || fields[f.key]?.trim());
|
|
227
|
-
async function add() {
|
|
228
|
-
if (!adding || !spec) return;
|
|
229
|
-
setErr(null);
|
|
230
|
-
setBusy(true);
|
|
231
|
-
try {
|
|
232
|
-
const config = Object.fromEntries(spec.fields.filter((f) => fields[f.key]?.trim()).map((f) => [f.key, fields[f.key].trim()]));
|
|
233
|
-
const r = await createChannel({ data: {
|
|
234
|
-
type: adding,
|
|
235
|
-
name: name.trim(),
|
|
236
|
-
config
|
|
237
|
-
} });
|
|
238
|
-
if (!r.ok) {
|
|
239
|
-
setErr(r.error ?? "Could not save this channel.");
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
setAdding(null);
|
|
243
|
-
await load();
|
|
244
|
-
} finally {
|
|
245
|
-
setBusy(false);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
async function remove(id) {
|
|
249
|
-
setErr(null);
|
|
250
|
-
const r = await deleteChannel({ data: id });
|
|
251
|
-
if (!r.ok) {
|
|
252
|
-
setErr(r.error ?? "Could not delete this channel.");
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
await load();
|
|
256
|
-
}
|
|
257
|
-
async function runTest(id) {
|
|
258
|
-
setTest((t) => ({
|
|
259
|
-
...t,
|
|
260
|
-
[id]: "sending"
|
|
261
|
-
}));
|
|
262
|
-
const r = await testChannel({ data: id });
|
|
263
|
-
setTest((t) => ({
|
|
264
|
-
...t,
|
|
265
|
-
[id]: r.ok ? "ok" : r.error || "failed"
|
|
266
|
-
}));
|
|
267
|
-
setTimeout(() => setTest((t) => ({
|
|
268
|
-
...t,
|
|
269
|
-
[id]: ""
|
|
270
|
-
})), 4e3);
|
|
271
|
-
}
|
|
272
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Modal, {
|
|
273
|
-
open,
|
|
274
|
-
onClose,
|
|
275
|
-
children: [
|
|
276
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalHead, {
|
|
277
|
-
title: "Notifications",
|
|
278
|
-
sub: "Push channels for this team. A loop routes its run messages to the channel you pick on it."
|
|
279
|
-
}),
|
|
280
|
-
err && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, {
|
|
281
|
-
message: err,
|
|
282
|
-
onDismiss: () => setErr(null),
|
|
283
|
-
className: "mb-2 mt-3"
|
|
284
|
-
}),
|
|
285
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalSection, { children: "Channels" }),
|
|
286
|
-
channels.length === 0 && !adding && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
287
|
-
className: "py-3 text-body text-secondary",
|
|
288
|
-
children: "No channels yet."
|
|
289
|
-
}),
|
|
290
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
|
|
291
|
-
className: "flex flex-col gap-2",
|
|
292
|
-
children: channels.map((c) => {
|
|
293
|
-
const t = test[c.id];
|
|
294
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", {
|
|
295
|
-
className: "flex items-center justify-between gap-3 rounded-control border border-hairline bg-surface px-4 py-3",
|
|
296
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
297
|
-
className: "flex min-w-0 items-center gap-2.5",
|
|
298
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
299
|
-
className: "text-[15px] font-medium text-display",
|
|
300
|
-
children: c.name
|
|
301
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
302
|
-
className: "text-label text-secondary",
|
|
303
|
-
children: [
|
|
304
|
-
typeLabel(c.type),
|
|
305
|
-
" · ",
|
|
306
|
-
c.hint
|
|
307
|
-
]
|
|
308
|
-
})]
|
|
309
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
310
|
-
className: "flex shrink-0 items-center gap-2",
|
|
311
|
-
children: [
|
|
312
|
-
t && t !== "sending" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
313
|
-
className: `inline-flex h-[22px] items-center gap-1.5 rounded-full px-2.5 text-caption font-medium ${t === "ok" ? "bg-success-soft text-success" : "bg-accent-soft text-accent"}`,
|
|
314
|
-
title: t === "ok" ? void 0 : t,
|
|
315
|
-
children: t === "ok" ? "Sent ✓" : "Failed"
|
|
316
|
-
}),
|
|
317
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
318
|
-
className: btn,
|
|
319
|
-
disabled: t === "sending",
|
|
320
|
-
onClick: () => void runTest(c.id),
|
|
321
|
-
children: t === "sending" ? "Sending…" : "Test"
|
|
322
|
-
}),
|
|
323
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
324
|
-
className: btnDanger,
|
|
325
|
-
onClick: () => void remove(c.id),
|
|
326
|
-
children: "Delete"
|
|
327
|
-
})
|
|
328
|
-
]
|
|
329
|
-
})]
|
|
330
|
-
}, c.id);
|
|
331
|
-
})
|
|
332
|
-
}),
|
|
333
|
-
adding && spec ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
334
|
-
className: "mt-4 rounded-control border border-hairline bg-raised px-4 py-3",
|
|
335
|
-
children: [
|
|
336
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ModalSection, { children: [
|
|
337
|
-
"New ",
|
|
338
|
-
spec.label,
|
|
339
|
-
" channel"
|
|
340
|
-
] }),
|
|
341
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", {
|
|
342
|
-
className: labelCls,
|
|
343
|
-
children: "Name"
|
|
344
|
-
}),
|
|
345
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
|
|
346
|
-
className: inputCls,
|
|
347
|
-
value: name,
|
|
348
|
-
onChange: (e) => setName(e.target.value),
|
|
349
|
-
placeholder: "e.g. My alerts"
|
|
350
|
-
}),
|
|
351
|
-
spec.fields.map((f) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", {
|
|
352
|
-
className: labelCls,
|
|
353
|
-
children: f.label
|
|
354
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
|
|
355
|
-
className: `${inputCls} font-mono`,
|
|
356
|
-
value: fields[f.key] ?? "",
|
|
357
|
-
onChange: (e) => setFields((s) => ({
|
|
358
|
-
...s,
|
|
359
|
-
[f.key]: e.target.value
|
|
360
|
-
})),
|
|
361
|
-
placeholder: f.ph
|
|
362
|
-
})] }, f.key)),
|
|
363
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
364
|
-
className: "mt-1 text-label text-secondary",
|
|
365
|
-
children: spec.help
|
|
366
|
-
}),
|
|
367
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
368
|
-
className: "mt-4 flex justify-end gap-2",
|
|
369
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
370
|
-
className: btn,
|
|
371
|
-
onClick: () => setAdding(null),
|
|
372
|
-
children: "Cancel"
|
|
373
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
374
|
-
className: btnPrimary,
|
|
375
|
-
disabled: busy || !filled,
|
|
376
|
-
onClick: () => void add(),
|
|
377
|
-
children: busy ? "Saving…" : "Save channel"
|
|
378
|
-
})]
|
|
379
|
-
})
|
|
380
|
-
]
|
|
381
|
-
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
382
|
-
className: "mt-6 flex gap-2",
|
|
383
|
-
children: ADDABLE.map((type) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
|
|
384
|
-
className: btnPrimary,
|
|
385
|
-
onClick: () => startAdd(type),
|
|
386
|
-
children: ["+ Add ", FORMS[type]?.label ?? type]
|
|
387
|
-
}, type))
|
|
388
|
-
})
|
|
389
|
-
]
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
function inviteUrl(token) {
|
|
393
|
-
return `${typeof window !== "undefined" ? window.location.origin : ""}/invite/${token}`;
|
|
394
|
-
}
|
|
395
|
-
function CopyLink({ text }) {
|
|
396
|
-
const [done, setDone] = (0, import_react.useState)(false);
|
|
397
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
398
|
-
className: btnSm,
|
|
399
|
-
onClick: () => {
|
|
400
|
-
navigator.clipboard?.writeText(text);
|
|
401
|
-
setDone(true);
|
|
402
|
-
setTimeout(() => setDone(false), 1400);
|
|
403
|
-
},
|
|
404
|
-
children: done ? "Copied ✓" : "Copy link"
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
/**
|
|
408
|
-
* Team settings — the owner-facing surface for the full lifecycle: create,
|
|
409
|
-
* rename, members (add-by-email + role + remove), invite links, leave, and
|
|
410
|
-
* delete (with the blocked-while-loops explanation). A master list of the
|
|
411
|
-
* caller's teams on the left; the selected team's detail below. Every action
|
|
412
|
-
* calls an EXPLICIT-teamId server fn (never the active cookie), so managing team
|
|
413
|
-
* B while the dashboard is on team A works. Owner-only controls are hidden for a
|
|
414
|
-
* plain member (the server re-authorizes regardless).
|
|
415
|
-
*/
|
|
416
|
-
function TeamsModal({ open, onClose, activeTeamId }) {
|
|
417
|
-
const navigate = useNavigate();
|
|
418
|
-
const [teams, setTeams] = (0, import_react.useState)([]);
|
|
419
|
-
const [selected, setSelected] = (0, import_react.useState)(null);
|
|
420
|
-
const [detail, setDetail] = (0, import_react.useState)(null);
|
|
421
|
-
const [err, setErr] = (0, import_react.useState)(null);
|
|
422
|
-
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
423
|
-
const [newName, setNewName] = (0, import_react.useState)("");
|
|
424
|
-
const [renameVal, setRenameVal] = (0, import_react.useState)("");
|
|
425
|
-
const [email, setEmail] = (0, import_react.useState)("");
|
|
426
|
-
const [addRole, setAddRole] = (0, import_react.useState)("member");
|
|
427
|
-
const [inviteRole, setInviteRole] = (0, import_react.useState)("member");
|
|
428
|
-
const [freshInvite, setFreshInvite] = (0, import_react.useState)(null);
|
|
429
|
-
const loadTeams = (0, import_react.useCallback)(async () => {
|
|
430
|
-
try {
|
|
431
|
-
setTeams(await listManagedTeams());
|
|
432
|
-
} catch {}
|
|
433
|
-
}, []);
|
|
434
|
-
const loadDetail = (0, import_react.useCallback)(async (teamId) => {
|
|
435
|
-
const d = await getTeamDetail({ data: teamId });
|
|
436
|
-
if ("error" in d) {
|
|
437
|
-
setErr(d.error);
|
|
438
|
-
setDetail(null);
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
setDetail(d);
|
|
442
|
-
setRenameVal(d.name);
|
|
443
|
-
}, []);
|
|
444
|
-
(0, import_react.useEffect)(() => {
|
|
445
|
-
if (!open) {
|
|
446
|
-
setErr(null);
|
|
447
|
-
setSelected(null);
|
|
448
|
-
setDetail(null);
|
|
449
|
-
setFreshInvite(null);
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
loadTeams();
|
|
453
|
-
}, [open, loadTeams]);
|
|
454
|
-
(0, import_react.useEffect)(() => {
|
|
455
|
-
if (!open || !selected) return;
|
|
456
|
-
setFreshInvite(null);
|
|
457
|
-
loadDetail(selected);
|
|
458
|
-
}, [
|
|
459
|
-
open,
|
|
460
|
-
selected,
|
|
461
|
-
loadDetail
|
|
462
|
-
]);
|
|
463
|
-
async function withBusy(fn) {
|
|
464
|
-
setBusy(true);
|
|
465
|
-
setErr(null);
|
|
466
|
-
try {
|
|
467
|
-
return await fn();
|
|
468
|
-
} finally {
|
|
469
|
-
setBusy(false);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
async function doCreate() {
|
|
473
|
-
const name = newName.trim();
|
|
474
|
-
if (!name) return;
|
|
475
|
-
await withBusy(async () => {
|
|
476
|
-
const r = await createTeam({ data: name });
|
|
477
|
-
if (!r.ok) return setErr(r.error);
|
|
478
|
-
setNewName("");
|
|
479
|
-
await loadTeams();
|
|
480
|
-
setSelected(r.id);
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
async function doRename() {
|
|
484
|
-
if (!detail) return;
|
|
485
|
-
const name = renameVal.trim();
|
|
486
|
-
if (!name || name === detail.name) return;
|
|
487
|
-
await withBusy(async () => {
|
|
488
|
-
const r = await renameTeam({ data: {
|
|
489
|
-
teamId: detail.id,
|
|
490
|
-
name
|
|
491
|
-
} });
|
|
492
|
-
if (!r.ok) return setErr(r.error);
|
|
493
|
-
await loadTeams();
|
|
494
|
-
await loadDetail(detail.id);
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
async function doDelete() {
|
|
498
|
-
if (!detail) return;
|
|
499
|
-
await withBusy(async () => {
|
|
500
|
-
const r = await deleteTeam({ data: detail.id });
|
|
501
|
-
if (!r.ok) return setErr(r.error);
|
|
502
|
-
const removed = detail.id;
|
|
503
|
-
setSelected(null);
|
|
504
|
-
setDetail(null);
|
|
505
|
-
await loadTeams();
|
|
506
|
-
if (removed === activeTeamId) {
|
|
507
|
-
onClose();
|
|
508
|
-
navigate({ to: "/" });
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
async function doLeave() {
|
|
513
|
-
if (!detail) return;
|
|
514
|
-
await withBusy(async () => {
|
|
515
|
-
const r = await leaveTeam({ data: detail.id });
|
|
516
|
-
if (!r.ok) return setErr(r.error);
|
|
517
|
-
const left = detail.id;
|
|
518
|
-
setSelected(null);
|
|
519
|
-
setDetail(null);
|
|
520
|
-
await loadTeams();
|
|
521
|
-
if (left === activeTeamId) {
|
|
522
|
-
onClose();
|
|
523
|
-
navigate({ to: "/" });
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
async function doAdd() {
|
|
528
|
-
if (!detail) return;
|
|
529
|
-
const addr = email.trim();
|
|
530
|
-
if (!addr) return;
|
|
531
|
-
await withBusy(async () => {
|
|
532
|
-
const r = await addTeamMember({ data: {
|
|
533
|
-
teamId: detail.id,
|
|
534
|
-
email: addr,
|
|
535
|
-
role: addRole
|
|
536
|
-
} });
|
|
537
|
-
if (!r.ok) return setErr(r.error);
|
|
538
|
-
setEmail("");
|
|
539
|
-
await loadDetail(detail.id);
|
|
540
|
-
await loadTeams();
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
async function doSetRole(userId, role) {
|
|
544
|
-
if (!detail) return;
|
|
545
|
-
await withBusy(async () => {
|
|
546
|
-
const r = await setTeamMemberRole({ data: {
|
|
547
|
-
teamId: detail.id,
|
|
548
|
-
userId,
|
|
549
|
-
role
|
|
550
|
-
} });
|
|
551
|
-
if (!r.ok) return setErr(r.error);
|
|
552
|
-
await loadDetail(detail.id);
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
async function doRemove(userId) {
|
|
556
|
-
if (!detail) return;
|
|
557
|
-
await withBusy(async () => {
|
|
558
|
-
const r = await removeTeamMember({ data: {
|
|
559
|
-
teamId: detail.id,
|
|
560
|
-
userId
|
|
561
|
-
} });
|
|
562
|
-
if (!r.ok) return setErr(r.error);
|
|
563
|
-
await loadDetail(detail.id);
|
|
564
|
-
await loadTeams();
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
async function doInvite() {
|
|
568
|
-
if (!detail) return;
|
|
569
|
-
await withBusy(async () => {
|
|
570
|
-
const r = await createTeamInvite({ data: {
|
|
571
|
-
teamId: detail.id,
|
|
572
|
-
role: inviteRole
|
|
573
|
-
} });
|
|
574
|
-
if (!r.ok) return setErr(r.error);
|
|
575
|
-
setFreshInvite(r.token);
|
|
576
|
-
await loadDetail(detail.id);
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
async function doRevoke(token) {
|
|
580
|
-
if (!detail) return;
|
|
581
|
-
await withBusy(async () => {
|
|
582
|
-
const r = await revokeTeamInvite({ data: {
|
|
583
|
-
teamId: detail.id,
|
|
584
|
-
token
|
|
585
|
-
} });
|
|
586
|
-
if (!r.ok) return setErr(r.error);
|
|
587
|
-
if (freshInvite === token) setFreshInvite(null);
|
|
588
|
-
await loadDetail(detail.id);
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
const isOwner = detail?.role === "owner";
|
|
592
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Modal, {
|
|
593
|
-
open,
|
|
594
|
-
onClose,
|
|
595
|
-
wide: true,
|
|
596
|
-
children: [
|
|
597
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalHead, {
|
|
598
|
-
title: "Teams",
|
|
599
|
-
sub: "Create teams, manage members and roles, and invite people. Team management is owner-only; any member can create loops."
|
|
600
|
-
}),
|
|
601
|
-
err && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBanner, {
|
|
602
|
-
message: err,
|
|
603
|
-
onDismiss: () => setErr(null),
|
|
604
|
-
className: "mb-2 mt-3"
|
|
605
|
-
}),
|
|
606
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
607
|
-
className: "mt-3 grid gap-6 md:grid-cols-[minmax(0,18rem)_minmax(0,1fr)]",
|
|
608
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
609
|
-
className: "min-w-0",
|
|
610
|
-
children: [
|
|
611
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalSection, { children: "My teams" }),
|
|
612
|
-
teams.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
613
|
-
className: "py-2 text-body text-secondary",
|
|
614
|
-
children: "No teams yet."
|
|
615
|
-
}),
|
|
616
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
|
|
617
|
-
className: "flex flex-col gap-1.5",
|
|
618
|
-
children: teams.map((t) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
|
|
619
|
-
onClick: () => setSelected(t.id),
|
|
620
|
-
className: `flex w-full items-center justify-between gap-2 rounded-control border px-3 py-2 text-left transition-colors ${selected === t.id ? "border-display bg-raised" : "border-hairline bg-surface hover:bg-raised"}`,
|
|
621
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
622
|
-
className: "min-w-0 truncate text-[15px] font-medium text-display",
|
|
623
|
-
children: t.name
|
|
624
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
625
|
-
className: "flex shrink-0 items-center gap-1.5",
|
|
626
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pill, {
|
|
627
|
-
tone: t.role === "owner" ? "ink" : "neutral",
|
|
628
|
-
children: t.role
|
|
629
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
630
|
-
className: "text-label text-secondary",
|
|
631
|
-
children: [
|
|
632
|
-
t.memberCount,
|
|
633
|
-
" member",
|
|
634
|
-
t.memberCount === 1 ? "" : "s"
|
|
635
|
-
]
|
|
636
|
-
})]
|
|
637
|
-
})]
|
|
638
|
-
}) }, t.id))
|
|
639
|
-
}),
|
|
640
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
641
|
-
className: "mt-4 rounded-control border border-hairline bg-raised px-3 py-3",
|
|
642
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", {
|
|
643
|
-
className: `${labelCls} mt-0`,
|
|
644
|
-
children: "New team"
|
|
645
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
646
|
-
className: "flex gap-2",
|
|
647
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
|
|
648
|
-
className: inputCls,
|
|
649
|
-
value: newName,
|
|
650
|
-
onChange: (e) => setNewName(e.target.value),
|
|
651
|
-
placeholder: "e.g. Growth Squad",
|
|
652
|
-
onKeyDown: (e) => e.key === "Enter" && void doCreate()
|
|
653
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
654
|
-
className: btnPrimary,
|
|
655
|
-
disabled: busy || !newName.trim(),
|
|
656
|
-
onClick: () => void doCreate(),
|
|
657
|
-
children: "Create"
|
|
658
|
-
})]
|
|
659
|
-
})]
|
|
660
|
-
})
|
|
661
|
-
]
|
|
662
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
663
|
-
className: "min-w-0",
|
|
664
|
-
children: !detail ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
665
|
-
className: "flex h-full items-center justify-center py-10 text-body text-secondary",
|
|
666
|
-
children: "Select a team to manage it, or create one."
|
|
667
|
-
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
668
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ModalSection, {
|
|
669
|
-
action: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
670
|
-
className: btnSm,
|
|
671
|
-
onClick: () => {
|
|
672
|
-
setActiveTeamCookie(detail.id);
|
|
673
|
-
onClose();
|
|
674
|
-
navigate({
|
|
675
|
-
to: "/t/$teamId",
|
|
676
|
-
params: { teamId: detail.id }
|
|
677
|
-
});
|
|
678
|
-
},
|
|
679
|
-
children: "Open dashboard →"
|
|
680
|
-
}),
|
|
681
|
-
children: [detail.name, detail.personal ? " · personal" : ""]
|
|
682
|
-
}),
|
|
683
|
-
isOwner && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
684
|
-
className: "mb-4",
|
|
685
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", {
|
|
686
|
-
className: "mb-1.5 mt-3 block text-label font-medium text-secondary",
|
|
687
|
-
children: "Team name"
|
|
688
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
689
|
-
className: "flex gap-2",
|
|
690
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
|
|
691
|
-
"aria-label": "Team name",
|
|
692
|
-
className: inputCls,
|
|
693
|
-
value: renameVal,
|
|
694
|
-
onChange: (e) => setRenameVal(e.target.value)
|
|
695
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
696
|
-
className: btn,
|
|
697
|
-
disabled: busy || !renameVal.trim() || renameVal.trim() === detail.name,
|
|
698
|
-
onClick: () => void doRename(),
|
|
699
|
-
children: "Rename"
|
|
700
|
-
})]
|
|
701
|
-
})]
|
|
702
|
-
}),
|
|
703
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalSection, { children: "Members" }),
|
|
704
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
|
|
705
|
-
className: "flex flex-col gap-2",
|
|
706
|
-
children: detail.members.map((m) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", {
|
|
707
|
-
className: "flex items-center justify-between gap-3 rounded-control border border-hairline bg-surface px-3 py-2.5",
|
|
708
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
709
|
-
className: "flex min-w-0 flex-col",
|
|
710
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
711
|
-
className: "truncate text-[15px] font-medium text-display",
|
|
712
|
-
children: [m.email ?? m.displayName ?? m.userId, m.isSelf && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
713
|
-
className: "ml-1.5 text-label text-secondary",
|
|
714
|
-
children: "(you)"
|
|
715
|
-
})]
|
|
716
|
-
})
|
|
717
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
718
|
-
className: "flex shrink-0 items-center gap-2",
|
|
719
|
-
children: isOwner && !m.isSelf ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
|
|
720
|
-
"aria-label": "Role",
|
|
721
|
-
className: `${selectCls} w-auto py-1.5`,
|
|
722
|
-
value: m.role,
|
|
723
|
-
onChange: (e) => void doSetRole(m.userId, e.target.value),
|
|
724
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
725
|
-
value: "owner",
|
|
726
|
-
children: "owner"
|
|
727
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
728
|
-
value: "member",
|
|
729
|
-
children: "member"
|
|
730
|
-
})]
|
|
731
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
732
|
-
className: btnDanger,
|
|
733
|
-
disabled: busy,
|
|
734
|
-
onClick: () => void doRemove(m.userId),
|
|
735
|
-
children: "Remove"
|
|
736
|
-
})] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pill, {
|
|
737
|
-
tone: m.role === "owner" ? "ink" : "neutral",
|
|
738
|
-
children: m.role
|
|
739
|
-
})
|
|
740
|
-
})]
|
|
741
|
-
}, m.userId))
|
|
742
|
-
}),
|
|
743
|
-
isOwner && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
744
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
745
|
-
className: "mt-4 rounded-control border border-hairline bg-raised px-3 py-3",
|
|
746
|
-
children: [
|
|
747
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", {
|
|
748
|
-
className: `mb-1.5 mt-3 block text-label font-medium text-secondary mt-0`,
|
|
749
|
-
children: "Add a member by email"
|
|
750
|
-
}),
|
|
751
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
752
|
-
className: "flex flex-wrap gap-2",
|
|
753
|
-
children: [
|
|
754
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
|
|
755
|
-
className: `${inputCls} min-w-[12rem] flex-1`,
|
|
756
|
-
value: email,
|
|
757
|
-
onChange: (e) => setEmail(e.target.value),
|
|
758
|
-
placeholder: "teammate@example.com",
|
|
759
|
-
onKeyDown: (e) => e.key === "Enter" && void doAdd()
|
|
760
|
-
}),
|
|
761
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
|
|
762
|
-
"aria-label": "Role for new member",
|
|
763
|
-
className: `${selectCls} w-auto`,
|
|
764
|
-
value: addRole,
|
|
765
|
-
onChange: (e) => setAddRole(e.target.value),
|
|
766
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
767
|
-
value: "member",
|
|
768
|
-
children: "member"
|
|
769
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
770
|
-
value: "owner",
|
|
771
|
-
children: "owner"
|
|
772
|
-
})]
|
|
773
|
-
}),
|
|
774
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
775
|
-
className: btnPrimary,
|
|
776
|
-
disabled: busy || !email.trim(),
|
|
777
|
-
onClick: () => void doAdd(),
|
|
778
|
-
children: "Add"
|
|
779
|
-
})
|
|
780
|
-
]
|
|
781
|
-
}),
|
|
782
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
783
|
-
className: "mt-1.5 text-label text-secondary",
|
|
784
|
-
children: "Adds an existing Pievo account immediately. No account yet? Generate an invite link below."
|
|
785
|
-
})
|
|
786
|
-
]
|
|
787
|
-
}),
|
|
788
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalSection, { children: "Invite links" }),
|
|
789
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
790
|
-
className: "flex flex-wrap items-center gap-2",
|
|
791
|
-
children: [
|
|
792
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
|
|
793
|
-
"aria-label": "Invite role",
|
|
794
|
-
className: `${selectCls} w-auto`,
|
|
795
|
-
value: inviteRole,
|
|
796
|
-
onChange: (e) => setInviteRole(e.target.value),
|
|
797
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
798
|
-
value: "member",
|
|
799
|
-
children: "as member"
|
|
800
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
801
|
-
value: "owner",
|
|
802
|
-
children: "as owner"
|
|
803
|
-
})]
|
|
804
|
-
}),
|
|
805
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
806
|
-
className: btn,
|
|
807
|
-
disabled: busy,
|
|
808
|
-
onClick: () => void doInvite(),
|
|
809
|
-
children: "Generate invite link"
|
|
810
|
-
}),
|
|
811
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
812
|
-
className: "text-label text-secondary",
|
|
813
|
-
children: "Single-use · expires in 7 days"
|
|
814
|
-
})
|
|
815
|
-
]
|
|
816
|
-
}),
|
|
817
|
-
detail.invites.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
|
|
818
|
-
className: "mt-3 flex flex-col gap-2",
|
|
819
|
-
children: detail.invites.map((i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", {
|
|
820
|
-
className: "flex items-center justify-between gap-3 rounded-control border border-hairline bg-surface px-3 py-2.5",
|
|
821
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
822
|
-
className: "flex min-w-0 items-center gap-2",
|
|
823
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pill, {
|
|
824
|
-
tone: "neutral",
|
|
825
|
-
children: i.role
|
|
826
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
827
|
-
className: `min-w-0 truncate font-mono text-label ${freshInvite === i.token ? "text-display" : "text-secondary"}`,
|
|
828
|
-
children: inviteUrl(i.token)
|
|
829
|
-
})]
|
|
830
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
831
|
-
className: "flex shrink-0 items-center gap-2",
|
|
832
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyLink, { text: inviteUrl(i.token) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
833
|
-
className: btnDanger,
|
|
834
|
-
disabled: busy,
|
|
835
|
-
onClick: () => void doRevoke(i.token),
|
|
836
|
-
children: "Revoke"
|
|
837
|
-
})]
|
|
838
|
-
})]
|
|
839
|
-
}, i.token))
|
|
840
|
-
})
|
|
841
|
-
] }),
|
|
842
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
843
|
-
className: "mt-6 flex items-center justify-between gap-3 border-t border-hairline pt-4",
|
|
844
|
-
children: [!detail.personal ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
845
|
-
className: btn,
|
|
846
|
-
disabled: busy,
|
|
847
|
-
onClick: () => void doLeave(),
|
|
848
|
-
children: "Leave team"
|
|
849
|
-
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
850
|
-
className: "text-label text-secondary",
|
|
851
|
-
children: "Your personal team can't be left or deleted."
|
|
852
|
-
}), isOwner && !detail.personal && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
|
|
853
|
-
className: btnDanger,
|
|
854
|
-
disabled: busy || detail.loopCount > 0,
|
|
855
|
-
title: detail.loopCount > 0 ? `Move or delete this team's ${detail.loopCount} loop${detail.loopCount === 1 ? "" : "s"} first` : void 0,
|
|
856
|
-
onClick: () => void doDelete(),
|
|
857
|
-
children: ["Delete team", detail.loopCount > 0 ? ` (${detail.loopCount} loop${detail.loopCount === 1 ? "" : "s"})` : ""]
|
|
858
|
-
})]
|
|
859
|
-
})
|
|
860
|
-
] })
|
|
861
|
-
})]
|
|
862
|
-
})
|
|
863
|
-
]
|
|
864
|
-
});
|
|
865
|
-
}
|
|
866
|
-
var FLOWS = {
|
|
867
|
-
"react-doctor": {
|
|
868
|
-
worktreeLabel: "Isolated git worktree · off main",
|
|
869
|
-
nodes: [
|
|
870
|
-
{
|
|
871
|
-
id: "tick",
|
|
872
|
-
kicker: "On schedule",
|
|
873
|
-
glyph: "◷",
|
|
874
|
-
title: "Every day · 6am",
|
|
875
|
-
detail: "the loop wakes on cadence"
|
|
876
|
-
},
|
|
877
|
-
{
|
|
878
|
-
id: "scan",
|
|
879
|
-
kicker: "Step 1 · Scan",
|
|
880
|
-
glyph: "⌕",
|
|
881
|
-
title: "Scan the app",
|
|
882
|
-
detail: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "npx react-doctor@latest" })
|
|
883
|
-
},
|
|
884
|
-
{
|
|
885
|
-
id: "prio",
|
|
886
|
-
kicker: "Step 2 · Prioritise",
|
|
887
|
-
glyph: "⚖",
|
|
888
|
-
title: "Pick one issue",
|
|
889
|
-
detail: "the worst, by severity & perf impact"
|
|
890
|
-
},
|
|
891
|
-
{
|
|
892
|
-
id: "fix",
|
|
893
|
-
wt: true,
|
|
894
|
-
kicker: "Step 3 · Fix",
|
|
895
|
-
glyph: "✎",
|
|
896
|
-
title: "Fix in isolation",
|
|
897
|
-
detail: "smallest change, fresh branch"
|
|
898
|
-
},
|
|
899
|
-
{
|
|
900
|
-
id: "verify",
|
|
901
|
-
wt: true,
|
|
902
|
-
kicker: "Step 4 · Verify",
|
|
903
|
-
glyph: "▷",
|
|
904
|
-
title: "Verify the fix",
|
|
905
|
-
detail: "prove it holds before proposing"
|
|
906
|
-
},
|
|
907
|
-
{
|
|
908
|
-
id: "ship",
|
|
909
|
-
wt: true,
|
|
910
|
-
kicker: "Step 5 · Ship",
|
|
911
|
-
glyph: "⑂",
|
|
912
|
-
title: "Open a PR",
|
|
913
|
-
detail: "skip if a prior PR's still open"
|
|
914
|
-
}
|
|
915
|
-
],
|
|
916
|
-
dashboard: [{
|
|
917
|
-
type: "kanban",
|
|
918
|
-
heading: "PR board",
|
|
919
|
-
sub: "open → merged",
|
|
920
|
-
columns: [["Open", [[
|
|
921
|
-
"#231",
|
|
922
|
-
"Key-less list items in <Feed/>",
|
|
923
|
-
"opened 6h ago"
|
|
924
|
-
], [
|
|
925
|
-
"#219",
|
|
926
|
-
"Missing memo on heavy list row",
|
|
927
|
-
"opened 1d ago"
|
|
928
|
-
]]], ["Merged", [
|
|
929
|
-
[
|
|
930
|
-
"#212",
|
|
931
|
-
"Effect re-runs on every render",
|
|
932
|
-
"merged 2d ago"
|
|
933
|
-
],
|
|
934
|
-
[
|
|
935
|
-
"#205",
|
|
936
|
-
"Unmemoised context value",
|
|
937
|
-
"merged 4d ago"
|
|
938
|
-
],
|
|
939
|
-
[
|
|
940
|
-
"#198",
|
|
941
|
-
"Inline object prop churn",
|
|
942
|
-
"merged 6d ago"
|
|
943
|
-
]
|
|
944
|
-
]]]
|
|
945
|
-
}, {
|
|
946
|
-
type: "metric",
|
|
947
|
-
label: "Health score",
|
|
948
|
-
series: [
|
|
949
|
-
66,
|
|
950
|
-
68,
|
|
951
|
-
67,
|
|
952
|
-
71,
|
|
953
|
-
72,
|
|
954
|
-
71,
|
|
955
|
-
75,
|
|
956
|
-
77,
|
|
957
|
-
76,
|
|
958
|
-
79,
|
|
959
|
-
81,
|
|
960
|
-
80,
|
|
961
|
-
83,
|
|
962
|
-
84
|
|
963
|
-
]
|
|
964
|
-
}]
|
|
965
|
-
},
|
|
966
|
-
"docs-sweep": {
|
|
967
|
-
worktreeLabel: "Isolated git worktree · off main",
|
|
968
|
-
nodes: [
|
|
969
|
-
{
|
|
970
|
-
id: "tick",
|
|
971
|
-
kicker: "On schedule",
|
|
972
|
-
glyph: "◷",
|
|
973
|
-
title: "Every Mon · 6am",
|
|
974
|
-
detail: "weekly · picks up where it left off"
|
|
975
|
-
},
|
|
976
|
-
{
|
|
977
|
-
id: "diff",
|
|
978
|
-
kicker: "Step 1 · Compare",
|
|
979
|
-
glyph: "⌕",
|
|
980
|
-
title: "Diff since last sweep",
|
|
981
|
-
detail: "docs vs. what the code ships now"
|
|
982
|
-
},
|
|
983
|
-
{
|
|
984
|
-
id: "verify",
|
|
985
|
-
kicker: "Step 2 · Verify",
|
|
986
|
-
glyph: "▷",
|
|
987
|
-
title: "Verify for real",
|
|
988
|
-
detail: "run the commands, links & examples"
|
|
989
|
-
},
|
|
990
|
-
{
|
|
991
|
-
id: "fix",
|
|
992
|
-
wt: true,
|
|
993
|
-
kicker: "Step 3 · Fix",
|
|
994
|
-
glyph: "✎",
|
|
995
|
-
title: "Fix real drift only",
|
|
996
|
-
detail: "never rewrite accurate docs"
|
|
997
|
-
},
|
|
998
|
-
{
|
|
999
|
-
id: "ship",
|
|
1000
|
-
wt: true,
|
|
1001
|
-
kicker: "Step 4 · Ship",
|
|
1002
|
-
glyph: "⑂",
|
|
1003
|
-
title: "Open a PR",
|
|
1004
|
-
detail: "explains the drift · no stacking"
|
|
1005
|
-
}
|
|
1006
|
-
],
|
|
1007
|
-
dashboard: [{
|
|
1008
|
-
type: "embed",
|
|
1009
|
-
heading: "Latest sweep summary",
|
|
1010
|
-
title: "Docs drift — weekly sweep",
|
|
1011
|
-
date: "2026-07-06",
|
|
1012
|
-
lines: [
|
|
1013
|
-
"3 items fixed, each verified by running it",
|
|
1014
|
-
"README — dev server is :3001, not :3000",
|
|
1015
|
-
"Setup guide — dropped a dead /api/v1 link",
|
|
1016
|
-
"Quickstart — refreshed `pievo new` flags"
|
|
1017
|
-
]
|
|
1018
|
-
}, {
|
|
1019
|
-
type: "metric",
|
|
1020
|
-
label: "Drift count",
|
|
1021
|
-
series: [
|
|
1022
|
-
6,
|
|
1023
|
-
4,
|
|
1024
|
-
5,
|
|
1025
|
-
3,
|
|
1026
|
-
2,
|
|
1027
|
-
3,
|
|
1028
|
-
1,
|
|
1029
|
-
2,
|
|
1030
|
-
1,
|
|
1031
|
-
0
|
|
1032
|
-
],
|
|
1033
|
-
note: "0 = docs are honest — a clean stop, no PR.",
|
|
1034
|
-
betterDown: true
|
|
1035
|
-
}]
|
|
1036
|
-
},
|
|
1037
|
-
"error-sweep": {
|
|
1038
|
-
worktreeLabel: "Isolated git worktree · off main",
|
|
1039
|
-
nodes: [
|
|
1040
|
-
{
|
|
1041
|
-
id: "setup",
|
|
1042
|
-
setup: true,
|
|
1043
|
-
kicker: "Before first run",
|
|
1044
|
-
glyph: "⚙",
|
|
1045
|
-
title: "Verify an error source",
|
|
1046
|
-
detail: "smoke-test logs / tracker / gh · confirm window"
|
|
1047
|
-
},
|
|
1048
|
-
{
|
|
1049
|
-
id: "tick",
|
|
1050
|
-
kicker: "On schedule",
|
|
1051
|
-
glyph: "◷",
|
|
1052
|
-
title: "Every day · 6am",
|
|
1053
|
-
detail: "sweeps the agreed window"
|
|
1054
|
-
},
|
|
1055
|
-
{
|
|
1056
|
-
id: "sweep",
|
|
1057
|
-
kicker: "Step 1 · Sweep",
|
|
1058
|
-
glyph: "⌕",
|
|
1059
|
-
title: "Group into incidents",
|
|
1060
|
-
detail: "cluster repeated symptoms"
|
|
1061
|
-
},
|
|
1062
|
-
{
|
|
1063
|
-
id: "triage",
|
|
1064
|
-
kicker: "Step 2 · Triage",
|
|
1065
|
-
glyph: "⚖",
|
|
1066
|
-
title: "Actionable vs. noise",
|
|
1067
|
-
detail: "drop upstream & noise"
|
|
1068
|
-
},
|
|
1069
|
-
{
|
|
1070
|
-
id: "fix",
|
|
1071
|
-
wt: true,
|
|
1072
|
-
kicker: "Step 3 · Fix",
|
|
1073
|
-
glyph: "✎",
|
|
1074
|
-
title: "Root-cause & fix",
|
|
1075
|
-
detail: "smallest verified fix"
|
|
1076
|
-
},
|
|
1077
|
-
{
|
|
1078
|
-
id: "ship",
|
|
1079
|
-
wt: true,
|
|
1080
|
-
kicker: "Step 4 · Ship",
|
|
1081
|
-
glyph: "⑂",
|
|
1082
|
-
title: "One PR per fix",
|
|
1083
|
-
detail: "no stacking · never leak secrets"
|
|
1084
|
-
}
|
|
1085
|
-
],
|
|
1086
|
-
dashboard: [{
|
|
1087
|
-
type: "embed",
|
|
1088
|
-
heading: "Newest report",
|
|
1089
|
-
title: "Error sweep — daily",
|
|
1090
|
-
date: "2026-07-08",
|
|
1091
|
-
lines: [
|
|
1092
|
-
"2 actionable, 14 noise filtered out",
|
|
1093
|
-
"Checkout 500 — null cart on retry (fixed)",
|
|
1094
|
-
"/api/render timeout — upstream, watching",
|
|
1095
|
-
"No credentials or PII in this report"
|
|
1096
|
-
]
|
|
1097
|
-
}, {
|
|
1098
|
-
type: "metric",
|
|
1099
|
-
label: "Actionable errors",
|
|
1100
|
-
series: [
|
|
1101
|
-
5,
|
|
1102
|
-
3,
|
|
1103
|
-
4,
|
|
1104
|
-
2,
|
|
1105
|
-
3,
|
|
1106
|
-
1,
|
|
1107
|
-
2,
|
|
1108
|
-
1,
|
|
1109
|
-
0,
|
|
1110
|
-
1
|
|
1111
|
-
],
|
|
1112
|
-
note: "0 actionable = a clean stop, no PR.",
|
|
1113
|
-
betterDown: true
|
|
1114
|
-
}]
|
|
1115
|
-
},
|
|
1116
|
-
housekeeper: {
|
|
1117
|
-
worktreeLabel: "Isolated git worktree · off main",
|
|
1118
|
-
nodes: [
|
|
1119
|
-
{
|
|
1120
|
-
id: "tick",
|
|
1121
|
-
kicker: "On schedule",
|
|
1122
|
-
glyph: "◷",
|
|
1123
|
-
title: "Every day · 7am",
|
|
1124
|
-
detail: "one cleanup a day, no more"
|
|
1125
|
-
},
|
|
1126
|
-
{
|
|
1127
|
-
id: "survey",
|
|
1128
|
-
kicker: "Step 1 · Survey",
|
|
1129
|
-
glyph: "⌕",
|
|
1130
|
-
title: "Find housekeeping debt",
|
|
1131
|
-
detail: "dead code, stale files, dupes, unused deps"
|
|
1132
|
-
},
|
|
1133
|
-
{
|
|
1134
|
-
id: "prove",
|
|
1135
|
-
kicker: "Step 2 · Prove",
|
|
1136
|
-
glyph: "⚖",
|
|
1137
|
-
title: "Pick one, prove it safe",
|
|
1138
|
-
detail: "concrete evidence · uncertain → deferred"
|
|
1139
|
-
},
|
|
1140
|
-
{
|
|
1141
|
-
id: "fix",
|
|
1142
|
-
wt: true,
|
|
1143
|
-
kicker: "Step 3 · Fix",
|
|
1144
|
-
glyph: "✎",
|
|
1145
|
-
title: "Smallest change",
|
|
1146
|
-
detail: "keep only if build & tests stay green"
|
|
1147
|
-
},
|
|
1148
|
-
{
|
|
1149
|
-
id: "ship",
|
|
1150
|
-
wt: true,
|
|
1151
|
-
kicker: "Step 4 · Ship",
|
|
1152
|
-
glyph: "⑂",
|
|
1153
|
-
title: "Open a PR",
|
|
1154
|
-
detail: "no stacking on an open PR"
|
|
1155
|
-
}
|
|
1156
|
-
],
|
|
1157
|
-
dashboard: [{
|
|
1158
|
-
type: "kanban",
|
|
1159
|
-
heading: "Cleanup board",
|
|
1160
|
-
sub: "open → merged",
|
|
1161
|
-
columns: [["Open", [[
|
|
1162
|
-
"#142",
|
|
1163
|
-
"Remove dead export in utils/date",
|
|
1164
|
-
"opened 5h ago"
|
|
1165
|
-
], [
|
|
1166
|
-
"#138",
|
|
1167
|
-
"Drop unused dep left-pad",
|
|
1168
|
-
"opened 2d ago"
|
|
1169
|
-
]]], ["Merged", [
|
|
1170
|
-
[
|
|
1171
|
-
"#131",
|
|
1172
|
-
"Delete stale scripts/legacy dir",
|
|
1173
|
-
"merged 1d ago"
|
|
1174
|
-
],
|
|
1175
|
-
[
|
|
1176
|
-
"#124",
|
|
1177
|
-
"Dedupe two clamp() helpers",
|
|
1178
|
-
"merged 3d ago"
|
|
1179
|
-
],
|
|
1180
|
-
[
|
|
1181
|
-
"#119",
|
|
1182
|
-
"Rename inconsistent isLoading flags",
|
|
1183
|
-
"merged 5d ago"
|
|
1184
|
-
]
|
|
1185
|
-
]]]
|
|
1186
|
-
}, {
|
|
1187
|
-
type: "metric",
|
|
1188
|
-
label: "Cleanups landed",
|
|
1189
|
-
series: [
|
|
1190
|
-
0,
|
|
1191
|
-
1,
|
|
1192
|
-
1,
|
|
1193
|
-
2,
|
|
1194
|
-
3,
|
|
1195
|
-
3,
|
|
1196
|
-
4,
|
|
1197
|
-
5,
|
|
1198
|
-
6,
|
|
1199
|
-
6,
|
|
1200
|
-
7,
|
|
1201
|
-
9,
|
|
1202
|
-
10,
|
|
1203
|
-
12
|
|
1204
|
-
]
|
|
1205
|
-
}]
|
|
1206
|
-
},
|
|
1207
|
-
"dependency-triage": {
|
|
1208
|
-
worktreeLabel: "Isolated git worktree · off main",
|
|
1209
|
-
nodes: [
|
|
1210
|
-
{
|
|
1211
|
-
id: "setup",
|
|
1212
|
-
setup: true,
|
|
1213
|
-
kicker: "Before first run",
|
|
1214
|
-
glyph: "⚙",
|
|
1215
|
-
title: "Confirm merge authority",
|
|
1216
|
-
detail: "smoke-test gh sees Dependabot/Renovate · agree merge policy"
|
|
1217
|
-
},
|
|
1218
|
-
{
|
|
1219
|
-
id: "tick",
|
|
1220
|
-
kicker: "On schedule",
|
|
1221
|
-
glyph: "◷",
|
|
1222
|
-
title: "Every Mon",
|
|
1223
|
-
detail: "weekly · snapshots the open PRs"
|
|
1224
|
-
},
|
|
1225
|
-
{
|
|
1226
|
-
id: "snapshot",
|
|
1227
|
-
kicker: "Step 1 · Snapshot",
|
|
1228
|
-
glyph: "⌕",
|
|
1229
|
-
title: "List open dep PRs",
|
|
1230
|
-
detail: "process each exactly once"
|
|
1231
|
-
},
|
|
1232
|
-
{
|
|
1233
|
-
id: "judge",
|
|
1234
|
-
kicker: "Step 2 · Judge",
|
|
1235
|
-
glyph: "⚖",
|
|
1236
|
-
title: "Weigh real evidence",
|
|
1237
|
-
detail: "diff · release notes · advisories · CI at head"
|
|
1238
|
-
},
|
|
1239
|
-
{
|
|
1240
|
-
id: "test",
|
|
1241
|
-
wt: true,
|
|
1242
|
-
kicker: "Step 3 · Test",
|
|
1243
|
-
glyph: "▷",
|
|
1244
|
-
title: "Run tests in isolation",
|
|
1245
|
-
detail: "at the exact head · checkout untouched"
|
|
1246
|
-
},
|
|
1247
|
-
{
|
|
1248
|
-
id: "merge",
|
|
1249
|
-
wt: true,
|
|
1250
|
-
kicker: "Step 4 · Merge",
|
|
1251
|
-
glyph: "⑂",
|
|
1252
|
-
title: "Merge only what’s authorized",
|
|
1253
|
-
detail: "major/breaking/security → recommend, don’t merge"
|
|
1254
|
-
}
|
|
1255
|
-
],
|
|
1256
|
-
dashboard: [{
|
|
1257
|
-
type: "kanban",
|
|
1258
|
-
heading: "Dependency PRs",
|
|
1259
|
-
sub: "merged · deferred · blocked",
|
|
1260
|
-
columns: [
|
|
1261
|
-
["Merged", [[
|
|
1262
|
-
"#412",
|
|
1263
|
-
"bump vite 7.0→7.1 (patch)",
|
|
1264
|
-
"merged 1h ago"
|
|
1265
|
-
], [
|
|
1266
|
-
"#408",
|
|
1267
|
-
"bump @types/node 20→20.14 (minor)",
|
|
1268
|
-
"merged 3h ago"
|
|
1269
|
-
]]],
|
|
1270
|
-
["Deferred", [[
|
|
1271
|
-
"#399",
|
|
1272
|
-
"react 18→19 (major) — review",
|
|
1273
|
-
"deferred 2d ago"
|
|
1274
|
-
], [
|
|
1275
|
-
"#391",
|
|
1276
|
-
"drizzle-orm 0.30→0.31 — CI red",
|
|
1277
|
-
"deferred 2d ago"
|
|
1278
|
-
]]],
|
|
1279
|
-
["Blocked", [[
|
|
1280
|
-
"#405",
|
|
1281
|
-
"lodash advisory GHSA-…-qxrp (high)",
|
|
1282
|
-
"flagged 1h ago"
|
|
1283
|
-
]]]
|
|
1284
|
-
]
|
|
1285
|
-
}, {
|
|
1286
|
-
type: "metric",
|
|
1287
|
-
label: "Open dependency PRs",
|
|
1288
|
-
series: [
|
|
1289
|
-
9,
|
|
1290
|
-
8,
|
|
1291
|
-
8,
|
|
1292
|
-
6,
|
|
1293
|
-
7,
|
|
1294
|
-
5,
|
|
1295
|
-
4,
|
|
1296
|
-
3,
|
|
1297
|
-
4,
|
|
1298
|
-
2
|
|
1299
|
-
],
|
|
1300
|
-
note: "trends down as PRs are merged or resolved.",
|
|
1301
|
-
betterDown: true
|
|
1302
|
-
}]
|
|
1303
|
-
},
|
|
1304
|
-
"market-research": {
|
|
1305
|
-
worktreeLabel: "",
|
|
1306
|
-
nodes: [
|
|
1307
|
-
{
|
|
1308
|
-
id: "setup",
|
|
1309
|
-
setup: true,
|
|
1310
|
-
kicker: "Before first run",
|
|
1311
|
-
glyph: "⚙",
|
|
1312
|
-
title: "Confirm the research focus",
|
|
1313
|
-
detail: "infer the product & space · propose a focus, confirm"
|
|
1314
|
-
},
|
|
1315
|
-
{
|
|
1316
|
-
id: "tick",
|
|
1317
|
-
kicker: "On schedule",
|
|
1318
|
-
glyph: "◷",
|
|
1319
|
-
title: "Every day · 5am",
|
|
1320
|
-
detail: "the loop wakes on cadence"
|
|
1321
|
-
},
|
|
1322
|
-
{
|
|
1323
|
-
id: "research",
|
|
1324
|
-
kicker: "Step 1 · Research",
|
|
1325
|
-
glyph: "⌕",
|
|
1326
|
-
title: "Scan today's market",
|
|
1327
|
-
detail: "competitors, launches, pricing, ecosystem news"
|
|
1328
|
-
},
|
|
1329
|
-
{
|
|
1330
|
-
id: "report",
|
|
1331
|
-
kicker: "Step 2 · Report",
|
|
1332
|
-
glyph: "✎",
|
|
1333
|
-
title: "One dated report",
|
|
1334
|
-
detail: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "type: report · title · date" })
|
|
1335
|
-
},
|
|
1336
|
-
{
|
|
1337
|
-
id: "sharpen",
|
|
1338
|
-
kicker: "Step 3 · Sharpen",
|
|
1339
|
-
glyph: "✧",
|
|
1340
|
-
title: "Sharpen the focus",
|
|
1341
|
-
detail: "lean into what keeps turning out to matter"
|
|
1342
|
-
}
|
|
1343
|
-
],
|
|
1344
|
-
dashboard: [{
|
|
1345
|
-
type: "calendar",
|
|
1346
|
-
heading: "Reports calendar",
|
|
1347
|
-
monthLabel: "July 2026",
|
|
1348
|
-
days: 31,
|
|
1349
|
-
firstWeekday: 2,
|
|
1350
|
-
reportDays: [
|
|
1351
|
-
1,
|
|
1352
|
-
2,
|
|
1353
|
-
3,
|
|
1354
|
-
6,
|
|
1355
|
-
7,
|
|
1356
|
-
8
|
|
1357
|
-
]
|
|
1358
|
-
}, {
|
|
1359
|
-
type: "embed",
|
|
1360
|
-
heading: "Newest report",
|
|
1361
|
-
title: "Market watch — daily digest",
|
|
1362
|
-
date: "2026-07-08",
|
|
1363
|
-
lines: [
|
|
1364
|
-
"Today's signal: two rivals converged on usage-based pricing",
|
|
1365
|
-
"Northwind shipped an AI triage add-on, free in beta",
|
|
1366
|
-
"Acme cut its Team tier 20% and dropped the seat minimum",
|
|
1367
|
-
"Ecosystem: a new OSS connector spec is gaining traction"
|
|
1368
|
-
]
|
|
1369
|
-
}]
|
|
1370
|
-
},
|
|
1371
|
-
"follow-up-tracker": {
|
|
1372
|
-
worktreeLabel: "",
|
|
1373
|
-
nodes: [
|
|
1374
|
-
{
|
|
1375
|
-
id: "setup",
|
|
1376
|
-
setup: true,
|
|
1377
|
-
kicker: "Before first run",
|
|
1378
|
-
glyph: "⚙",
|
|
1379
|
-
title: "Verify + define the target",
|
|
1380
|
-
detail: "smoke-test an observation path · set a standing objective"
|
|
1381
|
-
},
|
|
1382
|
-
{
|
|
1383
|
-
id: "tick",
|
|
1384
|
-
kicker: "On cadence",
|
|
1385
|
-
glyph: "◷",
|
|
1386
|
-
title: "A few times a day",
|
|
1387
|
-
detail: "wakes on cadence — no fixed clock time"
|
|
1388
|
-
},
|
|
1389
|
-
{
|
|
1390
|
-
id: "observe",
|
|
1391
|
-
kicker: "Step 1 · Observe",
|
|
1392
|
-
glyph: "⌕",
|
|
1393
|
-
title: "Check the outcome",
|
|
1394
|
-
detail: "through the verified path — logs / URL / gh"
|
|
1395
|
-
},
|
|
1396
|
-
{
|
|
1397
|
-
id: "report",
|
|
1398
|
-
kicker: "Step 2 · Report",
|
|
1399
|
-
glyph: "◈",
|
|
1400
|
-
title: "Report what you find",
|
|
1401
|
-
detail: "one report · a metric when natural"
|
|
1402
|
-
},
|
|
1403
|
-
{
|
|
1404
|
-
id: "check",
|
|
1405
|
-
kicker: "Step 3 · Check",
|
|
1406
|
-
glyph: "⚖",
|
|
1407
|
-
title: "Track the objective",
|
|
1408
|
-
detail: "keep watching and surface regressions"
|
|
1409
|
-
}
|
|
1410
|
-
],
|
|
1411
|
-
dashboard: [{
|
|
1412
|
-
type: "embed",
|
|
1413
|
-
heading: "Latest observation",
|
|
1414
|
-
title: "Checkout fix — follow-up",
|
|
1415
|
-
date: "2026-07-08",
|
|
1416
|
-
lines: [
|
|
1417
|
-
"conversion 29% → 44% over 3 days",
|
|
1418
|
-
"no error spikes since the deploy",
|
|
1419
|
-
"goal: hold ≥ 40% for 48h — 31h in, on track"
|
|
1420
|
-
]
|
|
1421
|
-
}, {
|
|
1422
|
-
type: "metric",
|
|
1423
|
-
label: "Checkout conversion %",
|
|
1424
|
-
series: [
|
|
1425
|
-
29,
|
|
1426
|
-
31,
|
|
1427
|
-
30,
|
|
1428
|
-
34,
|
|
1429
|
-
37,
|
|
1430
|
-
39,
|
|
1431
|
-
41,
|
|
1432
|
-
43,
|
|
1433
|
-
44
|
|
1434
|
-
],
|
|
1435
|
-
note: "objective: sustain ≥ 40%; keep watching for regressions."
|
|
1436
|
-
}]
|
|
1437
|
-
}
|
|
1438
|
-
};
|
|
1439
|
-
/** Whether a template has a preview — drives the modal's two-column layout. */
|
|
1440
|
-
var hasLoopFlow = (name) => name in FLOWS;
|
|
1441
|
-
var VIEW_W = 344;
|
|
1442
|
-
var NODE_X = 68;
|
|
1443
|
-
var NODE_W = 248;
|
|
1444
|
-
var NODE_H = 64;
|
|
1445
|
-
var CX = 192;
|
|
1446
|
-
var TOP = 6;
|
|
1447
|
-
var LANE_X = 30;
|
|
1448
|
-
var WT_X = 48;
|
|
1449
|
-
var WT_W = 280;
|
|
1450
|
-
var WT_PAD_TOP = 18;
|
|
1451
|
-
var WT_PAD_BOT = 16;
|
|
1452
|
-
var hOf = (d) => d.setup ? 78 : NODE_H;
|
|
1453
|
-
function buildGeometry(defs) {
|
|
1454
|
-
const tickIndex = defs.findIndex((d) => !d.setup);
|
|
1455
|
-
const nodes = [];
|
|
1456
|
-
let y = TOP;
|
|
1457
|
-
defs.forEach((d, i) => {
|
|
1458
|
-
if (i > 0) {
|
|
1459
|
-
const prevWt = !!defs[i - 1].wt;
|
|
1460
|
-
const curWt = !!d.wt;
|
|
1461
|
-
const gap = !prevWt && curWt ? 48 : prevWt && curWt ? 30 : 36;
|
|
1462
|
-
y += hOf(defs[i - 1]) + gap;
|
|
1463
|
-
}
|
|
1464
|
-
const kind = d.setup ? "setup" : i === tickIndex ? "tick" : "step";
|
|
1465
|
-
nodes.push({
|
|
1466
|
-
...d,
|
|
1467
|
-
kind,
|
|
1468
|
-
x: NODE_X,
|
|
1469
|
-
y,
|
|
1470
|
-
w: NODE_W,
|
|
1471
|
-
h: hOf(d)
|
|
1472
|
-
});
|
|
1473
|
-
});
|
|
1474
|
-
const wt = nodes.filter((n) => n.wt);
|
|
1475
|
-
const worktree = wt.length ? {
|
|
1476
|
-
x: WT_X,
|
|
1477
|
-
y: wt[0].y - WT_PAD_TOP,
|
|
1478
|
-
w: WT_W,
|
|
1479
|
-
h: wt[wt.length - 1].y + wt[wt.length - 1].h + WT_PAD_BOT - (wt[0].y - WT_PAD_TOP)
|
|
1480
|
-
} : null;
|
|
1481
|
-
const wires = [];
|
|
1482
|
-
for (let i = 0; i < nodes.length - 1; i++) wires.push(`M${CX} ${nodes[i].y + nodes[i].h} L${CX} ${nodes[i + 1].y - 4}`);
|
|
1483
|
-
const tickNode = nodes[tickIndex];
|
|
1484
|
-
const tickMidY = tickNode.y + tickNode.h / 2;
|
|
1485
|
-
const cycleLast = nodes[nodes.length - 1];
|
|
1486
|
-
const returnWireIndex = wires.length;
|
|
1487
|
-
const laneY = cycleLast.y + cycleLast.h + 28;
|
|
1488
|
-
wires.push(`M${CX} ${cycleLast.y + cycleLast.h} L${CX} ${laneY - 6} Q${CX} ${laneY} ${CX - 6} ${laneY} L36 ${laneY} Q${LANE_X} ${laneY} ${LANE_X} ${laneY - 6} L${LANE_X} ${tickMidY + 6} Q${LANE_X} ${tickMidY} 36 ${tickMidY} L${NODE_X - 4} ${tickMidY}`);
|
|
1489
|
-
return {
|
|
1490
|
-
nodes,
|
|
1491
|
-
wires,
|
|
1492
|
-
route: `M${nodes.slice(tickIndex).map((n) => `${CX} ${n.y + n.h / 2}`).join(" L ")} L${CX} ${laneY} L${LANE_X} ${laneY} L${LANE_X} ${tickMidY} L${NODE_X - 4} ${tickMidY}`,
|
|
1493
|
-
worktree,
|
|
1494
|
-
returnWireIndex,
|
|
1495
|
-
viewW: VIEW_W,
|
|
1496
|
-
viewH: laneY + 14
|
|
1497
|
-
};
|
|
1498
|
-
}
|
|
1499
|
-
function LoopFlow({ template }) {
|
|
1500
|
-
const spec = FLOWS[template.name];
|
|
1501
|
-
const [tab, setTab] = (0, import_react.useState)("flow");
|
|
1502
|
-
if (!spec) return null;
|
|
1503
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1504
|
-
className: "min-w-0",
|
|
1505
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1506
|
-
className: "mb-4 inline-flex rounded-full border border-hairline bg-raised p-0.5 text-label",
|
|
1507
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TabBtn, {
|
|
1508
|
-
active: tab === "flow",
|
|
1509
|
-
onClick: () => setTab("flow"),
|
|
1510
|
-
children: "Loop flow"
|
|
1511
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TabBtn, {
|
|
1512
|
-
active: tab === "dashboard",
|
|
1513
|
-
onClick: () => setTab("dashboard"),
|
|
1514
|
-
children: "Dashboard"
|
|
1515
|
-
})]
|
|
1516
|
-
}), tab === "flow" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FlowDiagram, { spec }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dashboard, { spec })]
|
|
1517
|
-
});
|
|
1518
|
-
}
|
|
1519
|
-
function TabBtn({ active, onClick, children }) {
|
|
1520
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
1521
|
-
type: "button",
|
|
1522
|
-
onClick,
|
|
1523
|
-
className: `cursor-pointer rounded-full px-3.5 py-1.5 font-medium transition-colors ${active ? "bg-surface text-display shadow-card" : "text-secondary hover:text-display"}`,
|
|
1524
|
-
children
|
|
1525
|
-
});
|
|
1526
|
-
}
|
|
1527
|
-
function FlowDiagram({ spec }) {
|
|
1528
|
-
const geo = (0, import_react.useMemo)(() => buildGeometry(spec.nodes), [spec]);
|
|
1529
|
-
const rootRef = (0, import_react.useRef)(null);
|
|
1530
|
-
(0, import_react.useEffect)(() => {
|
|
1531
|
-
const root = rootRef.current;
|
|
1532
|
-
if (!root) return;
|
|
1533
|
-
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
|
|
1534
|
-
const route = root.querySelector(".lpf-route");
|
|
1535
|
-
const pulse = root.querySelector(".lpf-pulse");
|
|
1536
|
-
const comet = root.querySelector(".lpf-comet");
|
|
1537
|
-
if (!route || !pulse || !comet) return;
|
|
1538
|
-
const total = route.getTotalLength();
|
|
1539
|
-
const SAMPLES = 600;
|
|
1540
|
-
const samp = [];
|
|
1541
|
-
for (let i = 0; i <= SAMPLES; i++) {
|
|
1542
|
-
const L = total * i / SAMPLES;
|
|
1543
|
-
const p = route.getPointAtLength(L);
|
|
1544
|
-
samp.push([
|
|
1545
|
-
L,
|
|
1546
|
-
p.x,
|
|
1547
|
-
p.y
|
|
1548
|
-
]);
|
|
1549
|
-
}
|
|
1550
|
-
const lenAt = (x, y) => {
|
|
1551
|
-
let best = 0;
|
|
1552
|
-
let bd = Infinity;
|
|
1553
|
-
for (const s of samp) {
|
|
1554
|
-
const d = (s[1] - x) ** 2 + (s[2] - y) ** 2;
|
|
1555
|
-
if (d < bd) {
|
|
1556
|
-
bd = d;
|
|
1557
|
-
best = s[0];
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
return best;
|
|
1561
|
-
};
|
|
1562
|
-
const nodeEls = /* @__PURE__ */ new Map();
|
|
1563
|
-
root.querySelectorAll("[data-lpf]").forEach((el) => nodeEls.set(el.dataset.lpf, el));
|
|
1564
|
-
const order = geo.nodes.filter((n) => n.kind !== "setup").map((n) => ({
|
|
1565
|
-
len: lenAt(n.x + n.w / 2, n.y + n.h / 2),
|
|
1566
|
-
el: nodeEls.get(n.id)
|
|
1567
|
-
})).filter((n) => n.el).sort((a, b) => a.len - b.len);
|
|
1568
|
-
const SPEED = 210 / 1e3;
|
|
1569
|
-
const DWELL = 520;
|
|
1570
|
-
const segs = [];
|
|
1571
|
-
const first = order[0];
|
|
1572
|
-
const lastN = order[order.length - 1];
|
|
1573
|
-
segs.push({
|
|
1574
|
-
type: "dwell",
|
|
1575
|
-
at: first,
|
|
1576
|
-
dur: DWELL
|
|
1577
|
-
});
|
|
1578
|
-
for (let i = 0; i < order.length - 1; i++) {
|
|
1579
|
-
const a = order[i];
|
|
1580
|
-
const b = order[i + 1];
|
|
1581
|
-
segs.push({
|
|
1582
|
-
type: "move",
|
|
1583
|
-
from: a.len,
|
|
1584
|
-
to: b.len,
|
|
1585
|
-
dur: Math.max(360, (b.len - a.len) / SPEED)
|
|
1586
|
-
});
|
|
1587
|
-
segs.push({
|
|
1588
|
-
type: "dwell",
|
|
1589
|
-
at: b,
|
|
1590
|
-
dur: DWELL
|
|
1591
|
-
});
|
|
1592
|
-
}
|
|
1593
|
-
segs.push({
|
|
1594
|
-
type: "return",
|
|
1595
|
-
from: lastN.len,
|
|
1596
|
-
to: total,
|
|
1597
|
-
dur: Math.max(720, (total - lastN.len) / SPEED)
|
|
1598
|
-
});
|
|
1599
|
-
segs.push({
|
|
1600
|
-
type: "dwell",
|
|
1601
|
-
at: first,
|
|
1602
|
-
dur: 160,
|
|
1603
|
-
restart: true
|
|
1604
|
-
});
|
|
1605
|
-
const COMET = 52;
|
|
1606
|
-
comet.setAttribute("stroke-dasharray", `${COMET} ${total}`);
|
|
1607
|
-
const place = (L) => {
|
|
1608
|
-
const c = Math.max(0, Math.min(total, L));
|
|
1609
|
-
const p = route.getPointAtLength(c);
|
|
1610
|
-
pulse.setAttribute("cx", p.x.toFixed(2));
|
|
1611
|
-
pulse.setAttribute("cy", p.y.toFixed(2));
|
|
1612
|
-
comet.setAttribute("stroke-dashoffset", (-(L - COMET * .5)).toFixed(2));
|
|
1613
|
-
};
|
|
1614
|
-
const clearActive = () => order.forEach((n) => n.el.classList.remove("active"));
|
|
1615
|
-
const resetPass = () => order.forEach((n) => n.el.classList.remove("visited"));
|
|
1616
|
-
const activate = (n) => {
|
|
1617
|
-
clearActive();
|
|
1618
|
-
n.el.classList.add("active", "visited");
|
|
1619
|
-
};
|
|
1620
|
-
let si = 0;
|
|
1621
|
-
let t0 = performance.now();
|
|
1622
|
-
let raf = 0;
|
|
1623
|
-
const frame = (now) => {
|
|
1624
|
-
let seg = segs[si];
|
|
1625
|
-
const e = now - t0;
|
|
1626
|
-
if (e >= seg.dur) {
|
|
1627
|
-
if ((seg.type === "move" || seg.type === "return") && seg.to != null) place(seg.to);
|
|
1628
|
-
si = (si + 1) % segs.length;
|
|
1629
|
-
t0 = now;
|
|
1630
|
-
seg = segs[si];
|
|
1631
|
-
if (seg.restart) resetPass();
|
|
1632
|
-
if (seg.type === "dwell" && seg.at) {
|
|
1633
|
-
activate(seg.at);
|
|
1634
|
-
place(seg.at.len);
|
|
1635
|
-
}
|
|
1636
|
-
if (seg.type === "return") clearActive();
|
|
1637
|
-
} else if ((seg.type === "move" || seg.type === "return") && seg.from != null && seg.to != null) {
|
|
1638
|
-
const k = e / seg.dur;
|
|
1639
|
-
const ease = k < .5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2;
|
|
1640
|
-
place(seg.from + (seg.to - seg.from) * ease);
|
|
1641
|
-
} else if (seg.at) place(seg.at.len);
|
|
1642
|
-
raf = requestAnimationFrame(frame);
|
|
1643
|
-
};
|
|
1644
|
-
activate(first);
|
|
1645
|
-
place(first.len);
|
|
1646
|
-
raf = requestAnimationFrame(frame);
|
|
1647
|
-
return () => cancelAnimationFrame(raf);
|
|
1648
|
-
}, [geo]);
|
|
1649
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1650
|
-
ref: rootRef,
|
|
1651
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1652
|
-
className: "overflow-x-auto",
|
|
1653
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1654
|
-
className: "relative mx-auto",
|
|
1655
|
-
style: {
|
|
1656
|
-
width: geo.viewW,
|
|
1657
|
-
height: geo.viewH
|
|
1658
|
-
},
|
|
1659
|
-
children: [
|
|
1660
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", {
|
|
1661
|
-
className: "absolute inset-0",
|
|
1662
|
-
width: geo.viewW,
|
|
1663
|
-
height: geo.viewH,
|
|
1664
|
-
viewBox: `0 0 ${geo.viewW} ${geo.viewH}`,
|
|
1665
|
-
"aria-hidden": "true",
|
|
1666
|
-
children: [
|
|
1667
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("defs", { children: [
|
|
1668
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("marker", {
|
|
1669
|
-
id: "lpf-arw",
|
|
1670
|
-
markerUnits: "userSpaceOnUse",
|
|
1671
|
-
viewBox: "0 0 8 8",
|
|
1672
|
-
refX: "6",
|
|
1673
|
-
refY: "4",
|
|
1674
|
-
markerWidth: "9",
|
|
1675
|
-
markerHeight: "9",
|
|
1676
|
-
orient: "auto-start-reverse",
|
|
1677
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", {
|
|
1678
|
-
d: "M1.2 1.2 L7 4 L1.2 6.8 Z",
|
|
1679
|
-
fill: "var(--color-interactive)"
|
|
1680
|
-
})
|
|
1681
|
-
}),
|
|
1682
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("filter", {
|
|
1683
|
-
id: "lpf-glow",
|
|
1684
|
-
x: "-60%",
|
|
1685
|
-
y: "-60%",
|
|
1686
|
-
width: "220%",
|
|
1687
|
-
height: "220%",
|
|
1688
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("feGaussianBlur", {
|
|
1689
|
-
stdDeviation: "2.4",
|
|
1690
|
-
result: "b"
|
|
1691
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("feMerge", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("feMergeNode", { in: "b" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("feMergeNode", { in: "SourceGraphic" })] })]
|
|
1692
|
-
}),
|
|
1693
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", {
|
|
1694
|
-
className: "lpf-route",
|
|
1695
|
-
fill: "none",
|
|
1696
|
-
d: geo.route
|
|
1697
|
-
})
|
|
1698
|
-
] }),
|
|
1699
|
-
geo.wires.map((d, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", {
|
|
1700
|
-
className: "lpf-wire",
|
|
1701
|
-
d,
|
|
1702
|
-
fill: "none",
|
|
1703
|
-
strokeWidth: 2,
|
|
1704
|
-
strokeLinecap: "round",
|
|
1705
|
-
strokeDasharray: i === geo.returnWireIndex ? "6 6" : void 0,
|
|
1706
|
-
markerEnd: "url(#lpf-arw)"
|
|
1707
|
-
}, i)),
|
|
1708
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", {
|
|
1709
|
-
className: "lpf-comet",
|
|
1710
|
-
d: geo.route,
|
|
1711
|
-
fill: "none",
|
|
1712
|
-
strokeWidth: 2.5,
|
|
1713
|
-
strokeLinecap: "round",
|
|
1714
|
-
filter: "url(#lpf-glow)",
|
|
1715
|
-
opacity: .9
|
|
1716
|
-
}),
|
|
1717
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", {
|
|
1718
|
-
className: "lpf-pulse",
|
|
1719
|
-
r: 3.8,
|
|
1720
|
-
filter: "url(#lpf-glow)"
|
|
1721
|
-
})
|
|
1722
|
-
]
|
|
1723
|
-
}),
|
|
1724
|
-
geo.worktree && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1725
|
-
className: "absolute rounded-[14px] border border-dashed",
|
|
1726
|
-
style: {
|
|
1727
|
-
left: geo.worktree.x,
|
|
1728
|
-
top: geo.worktree.y,
|
|
1729
|
-
width: geo.worktree.w,
|
|
1730
|
-
height: geo.worktree.h,
|
|
1731
|
-
borderColor: "var(--color-interactive)",
|
|
1732
|
-
background: "var(--color-interactive-soft)"
|
|
1733
|
-
},
|
|
1734
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1735
|
-
className: "absolute px-2 font-mono text-[9.5px] font-semibold uppercase tracking-[0.1em]",
|
|
1736
|
-
style: {
|
|
1737
|
-
top: -9,
|
|
1738
|
-
left: 16,
|
|
1739
|
-
background: "var(--color-surface)",
|
|
1740
|
-
color: "var(--color-interactive)"
|
|
1741
|
-
},
|
|
1742
|
-
children: spec.worktreeLabel
|
|
1743
|
-
})
|
|
1744
|
-
}),
|
|
1745
|
-
geo.nodes.map((n) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1746
|
-
"data-lpf": n.id,
|
|
1747
|
-
className: `lpf-node${n.kind === "tick" ? " tick" : ""}`,
|
|
1748
|
-
style: {
|
|
1749
|
-
left: n.x,
|
|
1750
|
-
top: n.y,
|
|
1751
|
-
width: n.w,
|
|
1752
|
-
height: n.h
|
|
1753
|
-
},
|
|
1754
|
-
children: [
|
|
1755
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1756
|
-
className: "lpf-k",
|
|
1757
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1758
|
-
className: "g",
|
|
1759
|
-
children: n.glyph
|
|
1760
|
-
}), n.kicker]
|
|
1761
|
-
}),
|
|
1762
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1763
|
-
className: "lpf-t",
|
|
1764
|
-
children: n.title
|
|
1765
|
-
}),
|
|
1766
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1767
|
-
className: "lpf-d",
|
|
1768
|
-
children: n.detail
|
|
1769
|
-
})
|
|
1770
|
-
]
|
|
1771
|
-
}, n.id))
|
|
1772
|
-
]
|
|
1773
|
-
})
|
|
1774
|
-
})
|
|
1775
|
-
});
|
|
1776
|
-
}
|
|
1777
|
-
function Dashboard({ spec }) {
|
|
1778
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1779
|
-
className: "min-w-0",
|
|
1780
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
1781
|
-
className: "mb-4 text-body leading-snug text-secondary",
|
|
1782
|
-
children: "Pievo sets up a dashboard to track results."
|
|
1783
|
-
}), spec.dashboard.map((w, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1784
|
-
className: `min-w-0 ${i > 0 ? "mt-6" : ""}`,
|
|
1785
|
-
children: w.type === "kanban" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KanbanWidget, { w }) : w.type === "metric" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MetricWidget, { w }) : w.type === "calendar" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CalendarWidget, { w }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmbedWidget, { w })
|
|
1786
|
-
}, i))]
|
|
1787
|
-
});
|
|
1788
|
-
}
|
|
1789
|
-
var CAL_DOW = [
|
|
1790
|
-
"Mon",
|
|
1791
|
-
"Tue",
|
|
1792
|
-
"Wed",
|
|
1793
|
-
"Thu",
|
|
1794
|
-
"Fri",
|
|
1795
|
-
"Sat",
|
|
1796
|
-
"Sun"
|
|
1797
|
-
];
|
|
1798
|
-
/** A month grid of report days, matching LoopCalendar's look: Monday-start weekday
|
|
1799
|
-
* headers, mono day numbers top-right, hairline grid, an interactive-ink dot on days
|
|
1800
|
-
* that produced a report. grid-cols-7 fills the ~360px column and reflows (min-w-0). */
|
|
1801
|
-
function CalendarWidget({ w }) {
|
|
1802
|
-
const marked = new Set(w.reportDays);
|
|
1803
|
-
const trail = (7 - (w.firstWeekday + w.days) % 7) % 7;
|
|
1804
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1805
|
-
className: "min-w-0",
|
|
1806
|
-
children: [
|
|
1807
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1808
|
-
className: "mb-2 flex items-baseline gap-2",
|
|
1809
|
-
children: [
|
|
1810
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1811
|
-
className: "text-meta font-medium text-secondary",
|
|
1812
|
-
children: w.heading
|
|
1813
|
-
}),
|
|
1814
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1815
|
-
className: "font-mono text-caption text-disabled",
|
|
1816
|
-
children: w.monthLabel
|
|
1817
|
-
}),
|
|
1818
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
1819
|
-
className: "ml-auto font-mono text-caption text-disabled",
|
|
1820
|
-
children: [
|
|
1821
|
-
w.reportDays.length,
|
|
1822
|
-
" report",
|
|
1823
|
-
w.reportDays.length === 1 ? "" : "s"
|
|
1824
|
-
]
|
|
1825
|
-
})
|
|
1826
|
-
]
|
|
1827
|
-
}),
|
|
1828
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1829
|
-
className: "grid min-w-0 grid-cols-7 border-l border-t border-hairline",
|
|
1830
|
-
children: [
|
|
1831
|
-
CAL_DOW.map((d) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1832
|
-
className: "border-b border-r border-hairline px-1.5 py-1 text-right text-micro font-medium text-disabled",
|
|
1833
|
-
children: d
|
|
1834
|
-
}, d)),
|
|
1835
|
-
Array.from({ length: w.firstWeekday }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "border-b border-r border-hairline bg-raised" }, `lead${i}`)),
|
|
1836
|
-
Array.from({ length: w.days }, (_, i) => {
|
|
1837
|
-
const day = i + 1;
|
|
1838
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1839
|
-
className: "relative min-h-[34px] min-w-0 border-b border-r border-hairline bg-surface px-1 pb-1 pt-4",
|
|
1840
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1841
|
-
className: "absolute right-1 top-0.5 font-mono text-micro text-disabled",
|
|
1842
|
-
children: day
|
|
1843
|
-
}), marked.has(day) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1844
|
-
title: `report · day ${day}`,
|
|
1845
|
-
className: "absolute bottom-1 left-1 inline-block size-1.5 rounded-full",
|
|
1846
|
-
style: { background: "var(--color-interactive)" }
|
|
1847
|
-
})]
|
|
1848
|
-
}, day);
|
|
1849
|
-
}),
|
|
1850
|
-
Array.from({ length: trail }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "border-b border-r border-hairline bg-raised" }, `trail${i}`))
|
|
1851
|
-
]
|
|
1852
|
-
}),
|
|
1853
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1854
|
-
className: "mt-2 flex items-center gap-1.5 text-caption font-medium text-secondary",
|
|
1855
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1856
|
-
className: "inline-block size-1.5 rounded-full",
|
|
1857
|
-
style: { background: "var(--color-interactive)" }
|
|
1858
|
-
}), " Day with a report"]
|
|
1859
|
-
})
|
|
1860
|
-
]
|
|
1861
|
-
});
|
|
1862
|
-
}
|
|
1863
|
-
function KanbanWidget({ w }) {
|
|
1864
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1865
|
-
className: "min-w-0",
|
|
1866
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1867
|
-
className: "mb-2 text-meta font-medium text-secondary",
|
|
1868
|
-
children: [
|
|
1869
|
-
w.heading,
|
|
1870
|
-
" ",
|
|
1871
|
-
w.sub && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1872
|
-
className: "font-mono text-caption text-disabled",
|
|
1873
|
-
children: w.sub
|
|
1874
|
-
})
|
|
1875
|
-
]
|
|
1876
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1877
|
-
className: "flex min-w-0 gap-3 overflow-x-auto pb-1",
|
|
1878
|
-
children: w.columns.map(([name, cards]) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1879
|
-
className: "flex w-[190px] shrink-0 flex-col",
|
|
1880
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1881
|
-
className: "mb-2 flex items-center gap-2 border-b border-hairline pb-1.5",
|
|
1882
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1883
|
-
className: "text-label font-semibold text-primary",
|
|
1884
|
-
children: name
|
|
1885
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1886
|
-
className: "text-caption text-disabled",
|
|
1887
|
-
children: cards.length
|
|
1888
|
-
})]
|
|
1889
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1890
|
-
className: "flex flex-col gap-2",
|
|
1891
|
-
children: cards.map(([id, title, when]) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1892
|
-
className: "min-w-0 overflow-hidden rounded-control border border-hairline bg-surface shadow-card transition-colors hover:border-wire",
|
|
1893
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1894
|
-
className: "flex flex-col gap-1 px-2.5 py-2",
|
|
1895
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1896
|
-
className: "truncate text-meta text-primary",
|
|
1897
|
-
children: title
|
|
1898
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
1899
|
-
className: "truncate font-mono text-micro text-disabled",
|
|
1900
|
-
children: [
|
|
1901
|
-
id,
|
|
1902
|
-
" · ",
|
|
1903
|
-
when
|
|
1904
|
-
]
|
|
1905
|
-
})]
|
|
1906
|
-
})
|
|
1907
|
-
}, id))
|
|
1908
|
-
})]
|
|
1909
|
-
}, name))
|
|
1910
|
-
})]
|
|
1911
|
-
});
|
|
1912
|
-
}
|
|
1913
|
-
function EmbedWidget({ w }) {
|
|
1914
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1915
|
-
className: "min-w-0",
|
|
1916
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1917
|
-
className: "mb-2 text-meta font-medium text-secondary",
|
|
1918
|
-
children: w.heading
|
|
1919
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1920
|
-
className: "rounded-card border border-hairline bg-surface p-3.5 shadow-card",
|
|
1921
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1922
|
-
className: "flex items-start gap-2",
|
|
1923
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1924
|
-
className: "min-w-0 flex-1",
|
|
1925
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1926
|
-
className: "truncate text-meta font-semibold text-display",
|
|
1927
|
-
children: w.title
|
|
1928
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1929
|
-
className: "font-mono text-micro text-disabled",
|
|
1930
|
-
children: w.date
|
|
1931
|
-
})]
|
|
1932
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1933
|
-
className: "shrink-0 rounded-full border border-hairline bg-raised px-2 py-0.5 font-mono text-micro text-secondary",
|
|
1934
|
-
children: "report"
|
|
1935
|
-
})]
|
|
1936
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1937
|
-
className: "mt-3 flex flex-col gap-1.5 border-t border-hairline pt-3",
|
|
1938
|
-
children: w.lines.map((l, i) => i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1939
|
-
className: "text-meta font-medium text-primary",
|
|
1940
|
-
children: l
|
|
1941
|
-
}, i) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1942
|
-
className: "flex gap-2 text-caption text-secondary",
|
|
1943
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1944
|
-
className: "text-disabled",
|
|
1945
|
-
children: "—"
|
|
1946
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1947
|
-
className: "min-w-0",
|
|
1948
|
-
children: l
|
|
1949
|
-
})]
|
|
1950
|
-
}, i))
|
|
1951
|
-
})]
|
|
1952
|
-
})]
|
|
1953
|
-
});
|
|
1954
|
-
}
|
|
1955
|
-
function MetricWidget({ w }) {
|
|
1956
|
-
const s = w.series;
|
|
1957
|
-
const now = s[s.length - 1] ?? 0;
|
|
1958
|
-
const delta = now - (s[0] ?? 0);
|
|
1959
|
-
const good = w.betterDown ? delta < 0 : delta > 0;
|
|
1960
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1961
|
-
className: "min-w-0 rounded-card border border-hairline bg-surface p-3.5 shadow-card",
|
|
1962
|
-
children: [
|
|
1963
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1964
|
-
className: "mb-3 flex items-baseline gap-2",
|
|
1965
|
-
children: [
|
|
1966
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1967
|
-
className: "text-label font-semibold text-primary",
|
|
1968
|
-
children: w.label
|
|
1969
|
-
}),
|
|
1970
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1971
|
-
className: "ml-auto font-mono text-body font-semibold text-display",
|
|
1972
|
-
children: now
|
|
1973
|
-
}),
|
|
1974
|
-
delta !== 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
1975
|
-
className: "font-mono text-caption",
|
|
1976
|
-
style: { color: good ? "var(--color-success)" : "var(--color-accent)" },
|
|
1977
|
-
children: [
|
|
1978
|
-
delta > 0 ? "▲" : "▼",
|
|
1979
|
-
" ",
|
|
1980
|
-
Math.abs(delta)
|
|
1981
|
-
]
|
|
1982
|
-
})
|
|
1983
|
-
]
|
|
1984
|
-
}),
|
|
1985
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AreaTrend, { series: s }),
|
|
1986
|
-
w.note && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
1987
|
-
className: "mt-2 font-mono text-caption text-secondary",
|
|
1988
|
-
children: w.note
|
|
1989
|
-
})
|
|
1990
|
-
]
|
|
1991
|
-
});
|
|
1992
|
-
}
|
|
1993
|
-
/** A single-series area trend, drawn like LoopChart: horizontal grid, mono axis
|
|
1994
|
-
* ticks, gradient fill in the chart-1 (display) ink. */
|
|
1995
|
-
function AreaTrend({ series }) {
|
|
1996
|
-
const gid = (0, import_react.useMemo)(() => `lpf-grad-${Math.round(series.reduce((a, b) => a + b, 0))}-${series.length}`, [series]);
|
|
1997
|
-
const W = 320;
|
|
1998
|
-
const H = 150;
|
|
1999
|
-
const padL = 26;
|
|
2000
|
-
const padR = 8;
|
|
2001
|
-
const padT = 10;
|
|
2002
|
-
const padB = 18;
|
|
2003
|
-
const dmin = Math.min(...series);
|
|
2004
|
-
const dmax = Math.max(...series);
|
|
2005
|
-
const span = Math.max(1, dmax - dmin);
|
|
2006
|
-
let lo = Math.floor(dmin - span * .15);
|
|
2007
|
-
const hi = Math.ceil(dmax + span * .15);
|
|
2008
|
-
if (dmin >= 0 && lo < 0) lo = 0;
|
|
2009
|
-
const X = (i) => padL + i * (W - padL - padR) / (series.length - 1);
|
|
2010
|
-
const Y = (v) => padT + (1 - (v - lo) / (hi - lo || 1)) * (H - padT - padB);
|
|
2011
|
-
const f = (n) => Math.round(n * 100) / 100;
|
|
2012
|
-
const line = "M" + series.map((v, i) => `${f(X(i))} ${f(Y(v))}`).join(" L ");
|
|
2013
|
-
const area = `${line} L ${f(X(series.length - 1))} ${H - padB} L ${f(X(0))} ${H - padB} Z`;
|
|
2014
|
-
const ticks = [
|
|
2015
|
-
lo,
|
|
2016
|
-
Math.round((lo + hi) / 2),
|
|
2017
|
-
hi
|
|
2018
|
-
];
|
|
2019
|
-
const last = series.length - 1;
|
|
2020
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", {
|
|
2021
|
-
viewBox: `0 0 ${W} ${H}`,
|
|
2022
|
-
width: "100%",
|
|
2023
|
-
className: "block",
|
|
2024
|
-
role: "img",
|
|
2025
|
-
"aria-label": "metric trend",
|
|
2026
|
-
children: [
|
|
2027
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("linearGradient", {
|
|
2028
|
-
id: gid,
|
|
2029
|
-
x1: "0",
|
|
2030
|
-
y1: "0",
|
|
2031
|
-
x2: "0",
|
|
2032
|
-
y2: "1",
|
|
2033
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", {
|
|
2034
|
-
offset: "0",
|
|
2035
|
-
stopColor: "var(--color-chart-1)",
|
|
2036
|
-
stopOpacity: "0.16"
|
|
2037
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", {
|
|
2038
|
-
offset: "1",
|
|
2039
|
-
stopColor: "var(--color-chart-1)",
|
|
2040
|
-
stopOpacity: "0"
|
|
2041
|
-
})]
|
|
2042
|
-
}) }),
|
|
2043
|
-
ticks.map((v, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", {
|
|
2044
|
-
x1: padL,
|
|
2045
|
-
y1: f(Y(v)),
|
|
2046
|
-
x2: W - padR,
|
|
2047
|
-
y2: f(Y(v)),
|
|
2048
|
-
stroke: "var(--color-hairline)",
|
|
2049
|
-
strokeWidth: 1
|
|
2050
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("text", {
|
|
2051
|
-
x: padL - 6,
|
|
2052
|
-
y: f(Y(v)) + 3,
|
|
2053
|
-
textAnchor: "end",
|
|
2054
|
-
fontFamily: "var(--font-mono)",
|
|
2055
|
-
fontSize: 9,
|
|
2056
|
-
fill: "var(--color-disabled)",
|
|
2057
|
-
children: v
|
|
2058
|
-
})] }, i)),
|
|
2059
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", {
|
|
2060
|
-
d: area,
|
|
2061
|
-
fill: `url(#${gid})`
|
|
2062
|
-
}),
|
|
2063
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", {
|
|
2064
|
-
d: line,
|
|
2065
|
-
fill: "none",
|
|
2066
|
-
stroke: "var(--color-chart-1)",
|
|
2067
|
-
strokeWidth: 2,
|
|
2068
|
-
strokeLinecap: "round",
|
|
2069
|
-
strokeLinejoin: "round"
|
|
2070
|
-
}),
|
|
2071
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", {
|
|
2072
|
-
cx: f(X(last)),
|
|
2073
|
-
cy: f(Y(series[last] ?? 0)),
|
|
2074
|
-
r: 3,
|
|
2075
|
-
fill: "var(--color-chart-1)",
|
|
2076
|
-
stroke: "var(--color-surface)",
|
|
2077
|
-
strokeWidth: 1.5
|
|
2078
|
-
}),
|
|
2079
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("text", {
|
|
2080
|
-
x: padL,
|
|
2081
|
-
y: H - 5,
|
|
2082
|
-
fontFamily: "var(--font-mono)",
|
|
2083
|
-
fontSize: 9,
|
|
2084
|
-
fill: "var(--color-disabled)",
|
|
2085
|
-
children: "older"
|
|
2086
|
-
}),
|
|
2087
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("text", {
|
|
2088
|
-
x: W - padR,
|
|
2089
|
-
y: H - 5,
|
|
2090
|
-
textAnchor: "end",
|
|
2091
|
-
fontFamily: "var(--font-mono)",
|
|
2092
|
-
fontSize: 9,
|
|
2093
|
-
fill: "var(--color-disabled)",
|
|
2094
|
-
children: "now"
|
|
2095
|
-
})
|
|
2096
|
-
]
|
|
2097
|
-
});
|
|
2098
|
-
}
|
|
2099
|
-
var SLOW_WAIT_MS = 1e5;
|
|
2100
|
-
var AGENT_LABEL = {
|
|
2101
|
-
"claude-code": "Claude Code",
|
|
2102
|
-
codex: "Codex"
|
|
2103
|
-
};
|
|
2104
|
-
function ClaudeCodeMark({ size = 14 }) {
|
|
2105
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", {
|
|
2106
|
-
"aria-hidden": true,
|
|
2107
|
-
width: size,
|
|
2108
|
-
height: size,
|
|
2109
|
-
viewBox: "0 0 24 24",
|
|
2110
|
-
fill: "#D97757",
|
|
2111
|
-
fillRule: "evenodd",
|
|
2112
|
-
className: "shrink-0",
|
|
2113
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
2114
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", {
|
|
2115
|
-
clipRule: "evenodd",
|
|
2116
|
-
d: "M20.998 10.949H24v3.102h-3v3.028h-1.487V20H18v-2.921h-1.487V20H15v-2.921H9V20H7.488v-2.921H6V20H4.487v-2.921H3V14.05H0V10.95h3V5h17.998v5.949zM6 10.949h1.488V8.102H6v2.847zm10.51 0H18V8.102h-1.49v2.847z"
|
|
2117
|
-
})
|
|
2118
|
-
});
|
|
2119
|
-
}
|
|
2120
|
-
/** Codex color mark (LobeHub icon set) — verbatim: white app tile + gradient glyph. */
|
|
2121
|
-
function CodexMark({ size = 14 }) {
|
|
2122
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", {
|
|
2123
|
-
"aria-hidden": true,
|
|
2124
|
-
width: size,
|
|
2125
|
-
height: size,
|
|
2126
|
-
viewBox: "0 0 24 24",
|
|
2127
|
-
className: "shrink-0",
|
|
2128
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
2129
|
-
children: [
|
|
2130
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", {
|
|
2131
|
-
d: "M19.503 0H4.496A4.496 4.496 0 000 4.496v15.007A4.496 4.496 0 004.496 24h15.007A4.496 4.496 0 0024 19.503V4.496A4.496 4.496 0 0019.503 0z",
|
|
2132
|
-
fill: "#fff"
|
|
2133
|
-
}),
|
|
2134
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", {
|
|
2135
|
-
d: "M9.064 3.344a4.578 4.578 0 012.285-.312c1 .115 1.891.54 2.673 1.275.01.01.024.017.037.021a.09.09 0 00.043 0 4.55 4.55 0 013.046.275l.047.022.116.057a4.581 4.581 0 012.188 2.399c.209.51.313 1.041.315 1.595a4.24 4.24 0 01-.134 1.223.123.123 0 00.03.115c.594.607.988 1.33 1.183 2.17.289 1.425-.007 2.71-.887 3.854l-.136.166a4.548 4.548 0 01-2.201 1.388.123.123 0 00-.081.076c-.191.551-.383 1.023-.74 1.494-.9 1.187-2.222 1.846-3.711 1.838-1.187-.006-2.239-.44-3.157-1.302a.107.107 0 00-.105-.024c-.388.125-.78.143-1.204.138a4.441 4.441 0 01-1.945-.466 4.544 4.544 0 01-1.61-1.335c-.152-.202-.303-.392-.414-.617a5.81 5.81 0 01-.37-.961 4.582 4.582 0 01-.014-2.298.124.124 0 00.006-.056.085.085 0 00-.027-.048 4.467 4.467 0 01-1.034-1.651 3.896 3.896 0 01-.251-1.192 5.189 5.189 0 01.141-1.6c.337-1.112.982-1.985 1.933-2.618.212-.141.413-.251.601-.33.215-.089.43-.164.646-.227a.098.098 0 00.065-.066 4.51 4.51 0 01.829-1.615 4.535 4.535 0 011.837-1.388zm3.482 10.565a.637.637 0 000 1.272h3.636a.637.637 0 100-1.272h-3.636zM8.462 9.23a.637.637 0 00-1.106.631l1.272 2.224-1.266 2.136a.636.636 0 101.095.649l1.454-2.455a.636.636 0 00.005-.64L8.462 9.23z",
|
|
2136
|
-
fill: "url(#lobe-icons-codex-grad)"
|
|
2137
|
-
}),
|
|
2138
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("linearGradient", {
|
|
2139
|
-
gradientUnits: "userSpaceOnUse",
|
|
2140
|
-
id: "lobe-icons-codex-grad",
|
|
2141
|
-
x1: "12",
|
|
2142
|
-
x2: "12",
|
|
2143
|
-
y1: "3",
|
|
2144
|
-
y2: "21",
|
|
2145
|
-
children: [
|
|
2146
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { stopColor: "#B1A7FF" }),
|
|
2147
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", {
|
|
2148
|
-
offset: ".5",
|
|
2149
|
-
stopColor: "#7A9DFF"
|
|
2150
|
-
}),
|
|
2151
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", {
|
|
2152
|
-
offset: "1",
|
|
2153
|
-
stopColor: "#3941FF"
|
|
2154
|
-
})
|
|
2155
|
-
]
|
|
2156
|
-
}) })
|
|
2157
|
-
]
|
|
2158
|
-
});
|
|
2159
|
-
}
|
|
2160
|
-
var instructionFor = (origin) => `Fetch ${origin}/api/bootstrap and help me build a loop.`;
|
|
2161
|
-
/**
|
|
2162
|
-
* New loop = capture-from-Claude-Code (paste-forward, no machine picker). The web
|
|
2163
|
-
* mints a claim token, shows ONE instruction block, and waits. The user pastes it
|
|
2164
|
-
* into their own Claude Code session (where they just did the task); Claude
|
|
2165
|
-
* follows /api/bootstrap — ensures a daemon is running (the token authorizes a new
|
|
2166
|
-
* machine, or the machine's stored token reuses an existing one) and POSTs the
|
|
2167
|
-
* loop to /api/machine/loop with this token as `claim`. We poll the claim until
|
|
2168
|
-
* the loop lands, then close. No machine selection: the binding is decided on the
|
|
2169
|
-
* machine, the claim just correlates the result back to this dialog.
|
|
2170
|
-
*
|
|
2171
|
-
* A `template` (a canned loop intent picked from the dashboard cards) reuses this exact
|
|
2172
|
-
* machinery and appends its `description` under the config lines — bootstrap.md +
|
|
2173
|
-
* create.md then handle cadence/config the same way. Pievo is BYOA-only: every loop runs
|
|
2174
|
-
* on the connected machine; there is no hosted-agent option.
|
|
2175
|
-
*/
|
|
2176
|
-
function ComposeModal({ open, onClose, onCreated, template = null, teamId }) {
|
|
2177
|
-
const [token, setToken] = (0, import_react.useState)(null);
|
|
2178
|
-
const [config, setConfig] = (0, import_react.useState)(null);
|
|
2179
|
-
const [created, setCreated] = (0, import_react.useState)(null);
|
|
2180
|
-
const [copied, setCopied] = (0, import_react.useState)(false);
|
|
2181
|
-
const [slow, setSlow] = (0, import_react.useState)(false);
|
|
2182
|
-
const [error, setError] = (0, import_react.useState)(null);
|
|
2183
|
-
const [configError, setConfigError] = (0, import_react.useState)(false);
|
|
2184
|
-
const pollRef = (0, import_react.useRef)(null);
|
|
2185
|
-
const onCreatedRef = (0, import_react.useRef)(onCreated);
|
|
2186
|
-
onCreatedRef.current = onCreated;
|
|
2187
|
-
const origin = typeof window !== "undefined" ? window.location.origin : "";
|
|
2188
|
-
const instruction = instructionFor(origin);
|
|
2189
|
-
const configLines = token && config ? [
|
|
2190
|
-
`server-url: ${origin}`,
|
|
2191
|
-
`connect-key: ${token}`,
|
|
2192
|
-
...config?.customCli ? [`pievo-cli: ${config.pievoCli}`] : []
|
|
2193
|
-
].join("\n") : "";
|
|
2194
|
-
const description = template?.description?.trim() ?? "";
|
|
2195
|
-
const snippet = token && config ? [
|
|
2196
|
-
instruction,
|
|
2197
|
-
"",
|
|
2198
|
-
configLines,
|
|
2199
|
-
...description ? ["", description] : []
|
|
2200
|
-
].join("\n") : "";
|
|
2201
|
-
(0, import_react.useEffect)(() => {
|
|
2202
|
-
if (!open) return;
|
|
2203
|
-
setToken(null);
|
|
2204
|
-
setConfig(null);
|
|
2205
|
-
setCreated(null);
|
|
2206
|
-
setError(null);
|
|
2207
|
-
setConfigError(false);
|
|
2208
|
-
setCopied(false);
|
|
2209
|
-
setSlow(false);
|
|
2210
|
-
}, [open, template]);
|
|
2211
|
-
(0, import_react.useEffect)(() => {
|
|
2212
|
-
if (!open) return;
|
|
2213
|
-
let active = true;
|
|
2214
|
-
getConfig().then((value) => {
|
|
2215
|
-
if (active) setConfig(value);
|
|
2216
|
-
}).catch(() => {
|
|
2217
|
-
if (active) setConfigError(true);
|
|
2218
|
-
});
|
|
2219
|
-
mintClaim({ data: teamId }).then((r) => {
|
|
2220
|
-
if (active) "token" in r ? setToken(r.token) : setError(r.error);
|
|
2221
|
-
}).catch(() => {
|
|
2222
|
-
if (active) setError("could not mint a connect key");
|
|
2223
|
-
});
|
|
2224
|
-
return () => {
|
|
2225
|
-
active = false;
|
|
2226
|
-
};
|
|
2227
|
-
}, [open, teamId]);
|
|
2228
|
-
(0, import_react.useEffect)(() => {
|
|
2229
|
-
if (!open || created || !token) return;
|
|
2230
|
-
if (pollRef.current) clearInterval(pollRef.current);
|
|
2231
|
-
pollRef.current = setInterval(() => {
|
|
2232
|
-
claimStatus({ data: token }).then((s) => {
|
|
2233
|
-
if (s.done && s.id) {
|
|
2234
|
-
if (pollRef.current) clearInterval(pollRef.current);
|
|
2235
|
-
setCreated({
|
|
2236
|
-
id: s.id,
|
|
2237
|
-
name: s.name ?? "loop",
|
|
2238
|
-
agent: s.agent ?? "claude-code"
|
|
2239
|
-
});
|
|
2240
|
-
onCreatedRef.current();
|
|
2241
|
-
}
|
|
2242
|
-
}).catch(() => {});
|
|
2243
|
-
}, 2500);
|
|
2244
|
-
const slowTimer = setTimeout(() => setSlow(true), SLOW_WAIT_MS);
|
|
2245
|
-
return () => {
|
|
2246
|
-
if (pollRef.current) clearInterval(pollRef.current);
|
|
2247
|
-
clearTimeout(slowTimer);
|
|
2248
|
-
};
|
|
2249
|
-
}, [
|
|
2250
|
-
open,
|
|
2251
|
-
created,
|
|
2252
|
-
token
|
|
2253
|
-
]);
|
|
2254
|
-
async function copy() {
|
|
2255
|
-
try {
|
|
2256
|
-
await navigator.clipboard.writeText(snippet);
|
|
2257
|
-
setCopied(true);
|
|
2258
|
-
setTimeout(() => setCopied(false), 1500);
|
|
2259
|
-
} catch {
|
|
2260
|
-
setError("could not copy - select the text and copy manually");
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
const snippetBox = (showInlineCopy = true) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2264
|
-
className: "min-w-0 flex-1",
|
|
2265
|
-
children: [
|
|
2266
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2267
|
-
className: "flex items-center justify-between gap-3",
|
|
2268
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", {
|
|
2269
|
-
className: "text-label font-semibold text-display",
|
|
2270
|
-
children: "Paste to capture it"
|
|
2271
|
-
}), showInlineCopy && snippet && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
2272
|
-
className: btnSm,
|
|
2273
|
-
onClick: () => void copy(),
|
|
2274
|
-
children: copied ? "✓ Copied" : "Copy"
|
|
2275
|
-
})]
|
|
2276
|
-
}),
|
|
2277
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2278
|
-
className: "mt-2 overflow-hidden rounded-control border border-hairline bg-raised p-3 font-mono text-label text-primary",
|
|
2279
|
-
children: [
|
|
2280
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
2281
|
-
className: "leading-relaxed",
|
|
2282
|
-
children: instruction
|
|
2283
|
-
}),
|
|
2284
|
-
configLines ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", {
|
|
2285
|
-
className: "mt-3 overflow-x-auto whitespace-pre-wrap border-t border-hairline pt-3 leading-relaxed text-secondary",
|
|
2286
|
-
children: configLines
|
|
2287
|
-
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
2288
|
-
className: "mt-3 border-t border-hairline pt-3 leading-relaxed text-secondary",
|
|
2289
|
-
children: configError ? "Could not load the CLI configuration. Close and try again." : token ? "Loading CLI configuration…" : "Minting a connect key…"
|
|
2290
|
-
}),
|
|
2291
|
-
description && configLines && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
2292
|
-
className: "mt-3 whitespace-pre-wrap border-t border-hairline pt-3 leading-relaxed text-primary",
|
|
2293
|
-
children: description
|
|
2294
|
-
})
|
|
2295
|
-
]
|
|
2296
|
-
}),
|
|
2297
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
|
|
2298
|
-
className: "mt-2 text-body leading-snug text-secondary",
|
|
2299
|
-
children: [
|
|
2300
|
-
"Paste it in that same session - reuses this machine automatically. Your agent will",
|
|
2301
|
-
" ",
|
|
2302
|
-
template ? "set the loop up from here." : "ask what the loop should do."
|
|
2303
|
-
]
|
|
2304
|
-
})
|
|
2305
|
-
]
|
|
2306
|
-
});
|
|
2307
|
-
const wait = slow ? {
|
|
2308
|
-
dot: "bg-secondary",
|
|
2309
|
-
text: "Still waiting - check your coding agent is running in the right project, then paste again."
|
|
2310
|
-
} : {
|
|
2311
|
-
dot: "animate-pulse bg-rubik-orange",
|
|
2312
|
-
text: "Waiting for your coding agent…"
|
|
2313
|
-
};
|
|
2314
|
-
const copyPromptButton = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
|
|
2315
|
-
className: `${btnPrimaryPill} ml-auto`,
|
|
2316
|
-
onClick: () => void copy(),
|
|
2317
|
-
disabled: !snippet,
|
|
2318
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
2319
|
-
className: "inline-flex items-center gap-0.5",
|
|
2320
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ClaudeCodeMark, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CodexMark, {})]
|
|
2321
|
-
}), copied ? "✓ Copied" : "Copy prompt"]
|
|
2322
|
-
});
|
|
2323
|
-
if (created) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Modal, {
|
|
2324
|
-
open,
|
|
2325
|
-
onClose,
|
|
2326
|
-
children: [
|
|
2327
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalHead, {
|
|
2328
|
-
title: "Loop created",
|
|
2329
|
-
sub: `${AGENT_LABEL[created.agent]} built and registered it.`
|
|
2330
|
-
}),
|
|
2331
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2332
|
-
className: "mt-5 rounded-card border border-hairline bg-surface p-5 shadow-card",
|
|
2333
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2334
|
-
className: "text-[17px] font-medium text-display",
|
|
2335
|
-
children: ["✓ ", created.name]
|
|
2336
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
2337
|
-
className: "mt-1 text-body text-secondary",
|
|
2338
|
-
children: "It’s scheduled now and will run on the machine."
|
|
2339
|
-
})]
|
|
2340
|
-
}),
|
|
2341
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
2342
|
-
className: "mt-4",
|
|
2343
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
2344
|
-
className: btnPrimary,
|
|
2345
|
-
onClick: onClose,
|
|
2346
|
-
children: "Done"
|
|
2347
|
-
})
|
|
2348
|
-
})
|
|
2349
|
-
]
|
|
2350
|
-
});
|
|
2351
|
-
if (template) {
|
|
2352
|
-
const showFlow = hasLoopFlow(template.name);
|
|
2353
|
-
const promptCol = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2354
|
-
className: "min-w-0",
|
|
2355
|
-
children: [
|
|
2356
|
-
snippetBox(false),
|
|
2357
|
-
error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2358
|
-
className: "mt-3 text-body text-accent",
|
|
2359
|
-
children: ["Error: ", error]
|
|
2360
|
-
}),
|
|
2361
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2362
|
-
className: "mt-6 flex items-start gap-3",
|
|
2363
|
-
children: [
|
|
2364
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `mt-1 inline-block h-2 w-2 shrink-0 rounded-full ${wait.dot}` }),
|
|
2365
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
2366
|
-
className: "text-label leading-relaxed text-secondary",
|
|
2367
|
-
children: wait.text
|
|
2368
|
-
}),
|
|
2369
|
-
copyPromptButton
|
|
2370
|
-
]
|
|
2371
|
-
})
|
|
2372
|
-
]
|
|
2373
|
-
});
|
|
2374
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Modal, {
|
|
2375
|
-
open,
|
|
2376
|
-
onClose,
|
|
2377
|
-
wide: showFlow,
|
|
2378
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalHead, {
|
|
2379
|
-
title: template.label,
|
|
2380
|
-
sub: "Paste this into your coding agent, in the project you want the loop for."
|
|
2381
|
-
}), showFlow ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2382
|
-
className: "mt-5 grid gap-8 md:grid-cols-[minmax(0,1fr)_400px]",
|
|
2383
|
-
children: [promptCol, /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
2384
|
-
className: "min-w-0 md:border-l md:border-hairline md:pl-8",
|
|
2385
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoopFlow, { template })
|
|
2386
|
-
})]
|
|
2387
|
-
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
2388
|
-
className: "mt-6",
|
|
2389
|
-
children: promptCol
|
|
2390
|
-
})]
|
|
2391
|
-
});
|
|
2392
|
-
}
|
|
2393
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Modal, {
|
|
2394
|
-
open,
|
|
2395
|
-
onClose,
|
|
2396
|
-
children: [
|
|
2397
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalHead, { title: "New loop" }),
|
|
2398
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2399
|
-
className: "mt-6",
|
|
2400
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2401
|
-
className: "flex gap-3.5",
|
|
2402
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2403
|
-
className: "flex flex-col items-center",
|
|
2404
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
2405
|
-
className: "flex size-5 shrink-0 items-center justify-center rounded-full border border-display text-micro font-medium leading-none text-display",
|
|
2406
|
-
children: "1"
|
|
2407
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mt-1 w-px flex-1 bg-hairline" })]
|
|
2408
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2409
|
-
className: "pb-6",
|
|
2410
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", {
|
|
2411
|
-
className: "text-label font-semibold text-display",
|
|
2412
|
-
children: "Do it once in your coding agent"
|
|
2413
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
2414
|
-
className: "mt-1.5 text-body leading-snug text-secondary",
|
|
2415
|
-
children: "Run the task yourself first, to a real result you’re happy with."
|
|
2416
|
-
})]
|
|
2417
|
-
})]
|
|
2418
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2419
|
-
className: "flex gap-3.5",
|
|
2420
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
2421
|
-
className: "flex size-5 shrink-0 items-center justify-center rounded-full border border-display text-micro font-medium leading-none text-display",
|
|
2422
|
-
children: "2"
|
|
2423
|
-
}), snippetBox(false)]
|
|
2424
|
-
})]
|
|
2425
|
-
}),
|
|
2426
|
-
error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2427
|
-
className: "mt-3 text-body text-accent",
|
|
2428
|
-
children: ["Error: ", error]
|
|
2429
|
-
}),
|
|
2430
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2431
|
-
className: "mt-6 flex items-start gap-3",
|
|
2432
|
-
children: [
|
|
2433
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `mt-1 inline-block h-2 w-2 shrink-0 rounded-full ${wait.dot}` }),
|
|
2434
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
2435
|
-
className: "text-label leading-relaxed text-secondary",
|
|
2436
|
-
children: wait.text
|
|
2437
|
-
}),
|
|
2438
|
-
copyPromptButton
|
|
2439
|
-
]
|
|
2440
|
-
})
|
|
2441
|
-
]
|
|
2442
|
-
});
|
|
2443
|
-
}
|
|
2444
|
-
/** The LIVE data fan-out - jobs/machines/teams change between polls. Templates
|
|
2445
|
-
* are static per deploy (a compile-time registry), so only the route loader
|
|
2446
|
-
* fetches them; the poll must not re-ship the thumb SVGs every 3-10s.
|
|
2447
|
-
*
|
|
2448
|
-
* `teamId` (the `/t/<id>` route's team, in id form or undefined in open mode)
|
|
2449
|
-
* scopes every list fn EXPLICITLY - so a tab on /t/A and one on /t/B show
|
|
2450
|
-
* different teams simultaneously, independent of the shared last-used cookie. */
|
|
2451
|
-
async function fetchLiveData(teamId) {
|
|
2452
|
-
const [jobs, machines, teams] = await Promise.all([
|
|
2453
|
-
listJobs({ data: teamId }),
|
|
2454
|
-
listMachines({ data: teamId }),
|
|
2455
|
-
listMyTeams({ data: teamId })
|
|
2456
|
-
]);
|
|
2457
|
-
return {
|
|
2458
|
-
jobs,
|
|
2459
|
-
machines,
|
|
2460
|
-
teams
|
|
2461
|
-
};
|
|
2462
|
-
}
|
|
2463
|
-
/**
|
|
2464
|
-
* The dashboard body, shared by the `/` (open mode) and `/t/<teamId>` routes. It
|
|
2465
|
-
* renders from its own fetch-then-set poll state, seeded once from the route
|
|
2466
|
-
* loader's data, and scopes every fetch to `teamId` so the view is pinned to the
|
|
2467
|
-
* URL's team (multi-tab safe). The route mounts it with `key={teamId}` so a
|
|
2468
|
-
* team switch (a `/t/<id>` navigation) re-seeds state from the new loader data.
|
|
2469
|
-
*/
|
|
2470
|
-
function DashboardView({ teamId, initial }) {
|
|
2471
|
-
const [live, setLive] = (0, import_react.useState)(null);
|
|
2472
|
-
const { jobs, machines, teams } = live ?? initial;
|
|
2473
|
-
const templates = initial.templates;
|
|
2474
|
-
const online = machines.filter((m) => m.online).length;
|
|
2475
|
-
const navigate = useNavigate();
|
|
2476
|
-
const [compose, setCompose] = (0, import_react.useState)({
|
|
2477
|
-
open: false,
|
|
2478
|
-
template: null
|
|
2479
|
-
});
|
|
2480
|
-
const [machinesOpen, setMachinesOpen] = (0, import_react.useState)(false);
|
|
2481
|
-
const [notifyOpen, setNotifyOpen] = (0, import_react.useState)(false);
|
|
2482
|
-
const [teamsOpen, setTeamsOpen] = (0, import_react.useState)(false);
|
|
2483
|
-
const refetch = (0, import_react.useCallback)(async () => {
|
|
2484
|
-
try {
|
|
2485
|
-
setLive(await fetchLiveData(teamId));
|
|
2486
|
-
} catch {}
|
|
2487
|
-
}, [teamId]);
|
|
2488
|
-
const openRef = (0, import_react.useRef)(false);
|
|
2489
|
-
openRef.current = compose.open || machinesOpen || notifyOpen || teamsOpen;
|
|
2490
|
-
const anyRunning = jobs.some((j) => j.running);
|
|
2491
|
-
(0, import_react.useEffect)(() => {
|
|
2492
|
-
const t = setInterval(() => {
|
|
2493
|
-
if (!openRef.current) refetch();
|
|
2494
|
-
}, anyRunning ? 3e3 : 1e4);
|
|
2495
|
-
return () => clearInterval(t);
|
|
2496
|
-
}, [refetch, anyRunning]);
|
|
2497
|
-
const refresh = () => void refetch();
|
|
2498
|
-
const activeOn = jobs.filter((j) => j.enabled).length;
|
|
2499
|
-
const cardProps = () => ({
|
|
2500
|
-
onOpen: (id) => void navigate({
|
|
2501
|
-
to: "/loops/$loopId",
|
|
2502
|
-
params: { loopId: id }
|
|
2503
|
-
}),
|
|
2504
|
-
onPickRun: (jobId, run) => void navigate({
|
|
2505
|
-
to: "/loops/$loopId/runs/$runId",
|
|
2506
|
-
params: {
|
|
2507
|
-
loopId: jobId,
|
|
2508
|
-
runId: run.id
|
|
2509
|
-
}
|
|
2510
|
-
})
|
|
2511
|
-
});
|
|
2512
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TooltipProvider, {
|
|
2513
|
-
delay: 120,
|
|
2514
|
-
children: [
|
|
2515
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("header", {
|
|
2516
|
-
className: "glass glass-bar sticky top-0 z-50",
|
|
2517
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2518
|
-
className: "mx-auto flex max-w-[1180px] items-center gap-3 px-8 py-2.5",
|
|
2519
|
-
children: [
|
|
2520
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PievoLogo, { size: 30 }),
|
|
2521
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
2522
|
-
className: "text-[18px] font-semibold tracking-[-0.015em] text-display",
|
|
2523
|
-
children: "Pievo"
|
|
2524
|
-
}),
|
|
2525
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TeamSwitcher, { data: teams }),
|
|
2526
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex-1" }),
|
|
2527
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
|
|
2528
|
-
href: GITHUB_URL,
|
|
2529
|
-
target: "_blank",
|
|
2530
|
-
rel: "noreferrer",
|
|
2531
|
-
"aria-label": "GitHub repository",
|
|
2532
|
-
title: "GitHub",
|
|
2533
|
-
className: headerIconBtn,
|
|
2534
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GitHubIcon, { className: "size-[17px]" })
|
|
2535
|
-
}),
|
|
2536
|
-
teams && teams.teams.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
2537
|
-
onClick: () => setTeamsOpen(true),
|
|
2538
|
-
className: headerBtn,
|
|
2539
|
-
children: "Teams"
|
|
2540
|
-
}),
|
|
2541
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
2542
|
-
onClick: () => setNotifyOpen(true),
|
|
2543
|
-
className: headerBtn,
|
|
2544
|
-
children: "Notifications"
|
|
2545
|
-
}),
|
|
2546
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
|
|
2547
|
-
onClick: () => setMachinesOpen(true),
|
|
2548
|
-
className: `${headerBtn} gap-1.5`,
|
|
2549
|
-
children: [
|
|
2550
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `inline-block size-1.5 rounded-full ${online ? "bg-rubik-green" : "bg-disabled"}` }),
|
|
2551
|
-
online,
|
|
2552
|
-
" ",
|
|
2553
|
-
online === 1 ? "machine" : "machines",
|
|
2554
|
-
" online"
|
|
2555
|
-
]
|
|
2556
|
-
}),
|
|
2557
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
2558
|
-
onClick: () => setCompose({
|
|
2559
|
-
open: true,
|
|
2560
|
-
template: null
|
|
2561
|
-
}),
|
|
2562
|
-
className: "inline-flex shrink-0 cursor-pointer items-center rounded-full bg-display px-3.5 py-1.5 text-meta font-medium text-paper transition-opacity hover:opacity-85",
|
|
2563
|
-
children: "New Loop"
|
|
2564
|
-
})
|
|
2565
|
-
]
|
|
2566
|
-
})
|
|
2567
|
-
}),
|
|
2568
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("main", {
|
|
2569
|
-
className: "mx-auto max-w-[1180px] px-8 pb-24",
|
|
2570
|
-
children: [
|
|
2571
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
|
|
2572
|
-
className: "pb-2 pt-14 text-center",
|
|
2573
|
-
children: [
|
|
2574
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", {
|
|
2575
|
-
className: "font-pixel text-[clamp(28px,4.5vw,38px)] leading-[1.15] text-display",
|
|
2576
|
-
children: "What should happen while you sleep?"
|
|
2577
|
-
}),
|
|
2578
|
-
templates.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
2579
|
-
className: "mb-5 mt-2 text-body text-secondary",
|
|
2580
|
-
children: "Start with a template…"
|
|
2581
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TemplateFan, {
|
|
2582
|
-
templates,
|
|
2583
|
-
onPick: (t) => setCompose({
|
|
2584
|
-
open: true,
|
|
2585
|
-
template: t
|
|
2586
|
-
})
|
|
2587
|
-
})] }),
|
|
2588
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
2589
|
-
className: "mt-7",
|
|
2590
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
|
|
2591
|
-
onClick: () => setCompose({
|
|
2592
|
-
open: true,
|
|
2593
|
-
template: null
|
|
2594
|
-
}),
|
|
2595
|
-
className: "inline-flex cursor-pointer items-center gap-1.5 rounded-full bg-display px-6 py-2.5 text-body font-medium text-paper transition-opacity hover:opacity-85",
|
|
2596
|
-
children: [templates.length ? "Start a blank loop" : "Start your first loop", /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
2597
|
-
"aria-hidden": true,
|
|
2598
|
-
children: "→"
|
|
2599
|
-
})]
|
|
2600
|
-
})
|
|
2601
|
-
})
|
|
2602
|
-
]
|
|
2603
|
-
}),
|
|
2604
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2605
|
-
className: "mb-5 mt-12 flex items-baseline gap-2.5",
|
|
2606
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
|
|
2607
|
-
className: "text-body font-semibold text-display",
|
|
2608
|
-
children: "Active loops"
|
|
2609
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
2610
|
-
className: "text-label text-secondary",
|
|
2611
|
-
children: jobs.length ? `${activeOn} scheduled · ${jobs.length} total` : ""
|
|
2612
|
-
})]
|
|
2613
|
-
}),
|
|
2614
|
-
jobs.length ? jobs.map((j) => /* @__PURE__ */ (0, import_react.createElement)(LoopCard, {
|
|
2615
|
-
job: j,
|
|
2616
|
-
...cardProps(),
|
|
2617
|
-
key: j.id
|
|
2618
|
-
})) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2619
|
-
className: "py-16 text-center",
|
|
2620
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
2621
|
-
className: "text-[15px] text-secondary",
|
|
2622
|
-
children: "No loops yet"
|
|
2623
|
-
}), !jobs.length && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
2624
|
-
className: "mt-1.5 text-body text-disabled",
|
|
2625
|
-
children: "Pick a template above, or start a blank loop."
|
|
2626
|
-
})]
|
|
2627
|
-
}),
|
|
2628
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoopPlaybook, { onStart: () => setCompose({
|
|
2629
|
-
open: true,
|
|
2630
|
-
template: null
|
|
2631
|
-
}) })
|
|
2632
|
-
]
|
|
2633
|
-
}),
|
|
2634
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ComposeModal, {
|
|
2635
|
-
open: compose.open,
|
|
2636
|
-
template: compose.template,
|
|
2637
|
-
teamId,
|
|
2638
|
-
onClose: () => setCompose({
|
|
2639
|
-
open: false,
|
|
2640
|
-
template: null
|
|
2641
|
-
}),
|
|
2642
|
-
onCreated: refresh
|
|
2643
|
-
}),
|
|
2644
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(MachinesModal, {
|
|
2645
|
-
open: machinesOpen,
|
|
2646
|
-
onClose: () => setMachinesOpen(false),
|
|
2647
|
-
teamId
|
|
2648
|
-
}),
|
|
2649
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(NotificationsModal, {
|
|
2650
|
-
open: notifyOpen,
|
|
2651
|
-
onClose: () => setNotifyOpen(false)
|
|
2652
|
-
}),
|
|
2653
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TeamsModal, {
|
|
2654
|
-
open: teamsOpen,
|
|
2655
|
-
onClose: () => setTeamsOpen(false),
|
|
2656
|
-
activeTeamId: teamId
|
|
2657
|
-
})
|
|
2658
|
-
]
|
|
2659
|
-
});
|
|
2660
|
-
}
|
|
2661
|
-
var headerBtn = "inline-flex shrink-0 cursor-pointer items-center rounded-full px-3 py-1.5 text-meta font-medium text-secondary transition-colors hover:bg-raised hover:text-display";
|
|
2662
|
-
var headerIconBtn = "inline-flex shrink-0 cursor-pointer items-center rounded-full p-1.5 text-secondary transition-colors hover:bg-raised hover:text-display";
|
|
2663
|
-
/**
|
|
2664
|
-
* The template fan - a hand of tilted cards (tilt/lift computed from each
|
|
2665
|
-
* card's offset from its ROW's center). Up to 5 cards stay a single hand;
|
|
2666
|
-
* more split into balanced rows of at most 4 (7 -> 4/3, 9 -> 3/3/3), so no
|
|
2667
|
-
* count can flex-wrap into one orphan card stranded under a full row.
|
|
2668
|
-
* Hover/focus straightens a card via the `.fan-card` rules in app.css.
|
|
2669
|
-
* One click carries the template into ComposeModal, same as the old flat cards.
|
|
2670
|
-
*/
|
|
2671
|
-
function TemplateFan({ templates, onPick }) {
|
|
2672
|
-
const rowCount = templates.length <= 5 ? 1 : Math.ceil(templates.length / 4);
|
|
2673
|
-
const rows = [];
|
|
2674
|
-
for (let r = 0, at = 0; r < rowCount; r++) {
|
|
2675
|
-
const size = Math.ceil((templates.length - at) / (rowCount - r));
|
|
2676
|
-
rows.push(templates.slice(at, at + size));
|
|
2677
|
-
at += size;
|
|
2678
|
-
}
|
|
2679
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
2680
|
-
className: "pb-3 pt-1",
|
|
2681
|
-
children: rows.map((row, r) => {
|
|
2682
|
-
const center = (row.length - 1) / 2;
|
|
2683
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
2684
|
-
className: "flex flex-wrap items-start justify-center",
|
|
2685
|
-
children: row.map((t, i) => {
|
|
2686
|
-
const off = i - center;
|
|
2687
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
|
|
2688
|
-
type: "button",
|
|
2689
|
-
onClick: () => onPick(t),
|
|
2690
|
-
title: t.desc,
|
|
2691
|
-
className: "fan-card relative w-[196px] shrink-0 cursor-pointer rounded-card border border-hairline bg-surface p-3 text-left shadow-[0_12px_28px_-16px_rgba(0,0,0,0.25)] outline-none focus-visible:ring-2 focus-visible:ring-interactive",
|
|
2692
|
-
style: {
|
|
2693
|
-
"--tilt": `${off * 3.5}deg`,
|
|
2694
|
-
"--lift": `${Math.abs(off) * 7}px`,
|
|
2695
|
-
marginInline: row.length > 1 ? "-5px" : void 0
|
|
2696
|
-
},
|
|
2697
|
-
children: [
|
|
2698
|
-
t.thumb ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
2699
|
-
className: "block overflow-hidden rounded-control bg-raised [&_svg]:block [&_svg]:h-auto [&_svg]:w-full",
|
|
2700
|
-
dangerouslySetInnerHTML: { __html: t.thumb }
|
|
2701
|
-
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
2702
|
-
className: "flex h-[76px] items-center justify-center rounded-control bg-raised text-secondary",
|
|
2703
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoopGlyph, {})
|
|
2704
|
-
}),
|
|
2705
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
2706
|
-
className: "mt-2.5 block truncate text-center text-meta font-semibold text-primary",
|
|
2707
|
-
children: t.label
|
|
2708
|
-
}),
|
|
2709
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
2710
|
-
className: "mt-0.5 line-clamp-3 min-h-[45px] text-center text-caption leading-snug text-secondary",
|
|
2711
|
-
children: t.desc
|
|
2712
|
-
})
|
|
2713
|
-
]
|
|
2714
|
-
}, t.name);
|
|
2715
|
-
})
|
|
2716
|
-
}, r);
|
|
2717
|
-
})
|
|
2718
|
-
});
|
|
2719
|
-
}
|
|
2720
|
-
/** A circular-arrow "loop" mark - one glyph for every template, tinted per card. */
|
|
2721
|
-
function LoopGlyph() {
|
|
2722
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", {
|
|
2723
|
-
"aria-hidden": true,
|
|
2724
|
-
width: "30",
|
|
2725
|
-
height: "30",
|
|
2726
|
-
viewBox: "0 0 30 30",
|
|
2727
|
-
fill: "none",
|
|
2728
|
-
stroke: "currentColor",
|
|
2729
|
-
strokeWidth: "1.8",
|
|
2730
|
-
strokeLinecap: "round",
|
|
2731
|
-
strokeLinejoin: "round",
|
|
2732
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M25.5 15a10.5 10.5 0 1 1-3.1-7.4" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M25.5 3.5v5.2h-5.2" })]
|
|
2733
|
-
});
|
|
2734
|
-
}
|
|
2735
|
-
//#endregion
|
|
2736
|
-
export { fetchLiveData as n, DashboardView as t };
|