clearotron 0.3.1-beta.3 → 0.3.1

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.
@@ -11629,6 +11629,11 @@ function decodeStatus(status, body) {
11629
11629
  errors: errorsOf(body),
11630
11630
  detail: refused
11631
11631
  };
11632
+ if (refused) return {
11633
+ kind: "gate",
11634
+ message: "That could not be done. Nothing was changed.",
11635
+ detail: refused
11636
+ };
11632
11637
  const msg = errorsOf(body)[0] ?? "This action could not be completed.";
11633
11638
  return /version|conflict/i.test(msg) && !/confirmation|plan again|re-confirm/i.test(msg) ? {
11634
11639
  kind: "conflict",
@@ -11794,6 +11799,33 @@ function decodeAccess(b) {
11794
11799
  }];
11795
11800
  });
11796
11801
  }
11802
+ var decodeSteps = (raw) => asArray(raw).flatMap((x) => {
11803
+ const s = asRecord(x);
11804
+ const text = asString(s["text"]);
11805
+ if (!text) return [];
11806
+ const hint = asString(s["hint"]);
11807
+ const c = s["copy"] == null ? null : asRecord(s["copy"]);
11808
+ let copy;
11809
+ if (c) {
11810
+ const kind = asString(c["kind"]);
11811
+ if (kind === "block" && asString(c["text"])) copy = {
11812
+ kind,
11813
+ text: asString(c["text"])
11814
+ };
11815
+ else if (kind === "secret" && asString(c["label"]) && asString(c["template"]) && asString(c["slot"]) && asString(c["template"]).includes(asString(c["slot"]))) copy = {
11816
+ kind,
11817
+ label: asString(c["label"]),
11818
+ template: asString(c["template"]),
11819
+ slot: asString(c["slot"])
11820
+ };
11821
+ else return [];
11822
+ }
11823
+ return [{
11824
+ text,
11825
+ ...hint ? { hint } : {},
11826
+ ...copy ? { copy } : {}
11827
+ }];
11828
+ });
11797
11829
  function decodePerson(r) {
11798
11830
  return {
11799
11831
  email: asString(r["email"]) ?? "",
@@ -12089,33 +12121,12 @@ var api = {
12089
12121
  ...asString(r["sub"]) ? { sub: asString(r["sub"]) } : {},
12090
12122
  ...asString(r["verifiedOn"]) ? { verifiedOn: asString(r["verifiedOn"]) } : {},
12091
12123
  ...asString(r["by"]) ? { by: asString(r["by"]) } : {},
12092
- steps: asArray(r["steps"]).flatMap((x) => {
12093
- const s = asRecord(x);
12094
- const text = asString(s["text"]);
12095
- if (!text) return [];
12096
- const hint = asString(s["hint"]);
12097
- const c = s["copy"] == null ? null : asRecord(s["copy"]);
12098
- let copy;
12099
- if (c) {
12100
- const kind = asString(c["kind"]);
12101
- if (kind === "block" && asString(c["text"])) copy = {
12102
- kind,
12103
- text: asString(c["text"])
12104
- };
12105
- else if (kind === "secret" && asString(c["label"]) && asString(c["template"]) && asString(c["slot"]) && asString(c["template"]).includes(asString(c["slot"]))) copy = {
12106
- kind,
12107
- label: asString(c["label"]),
12108
- template: asString(c["template"]),
12109
- slot: asString(c["slot"])
12110
- };
12111
- else return [];
12112
- }
12113
- return [{
12114
- text,
12115
- ...hint ? { hint } : {},
12116
- ...copy ? { copy } : {}
12117
- }];
12118
- }),
12124
+ steps: decodeSteps(r["steps"]),
12125
+ ...r["door"] === "sign-in" || r["door"] === "key" ? { door: r["door"] } : Object.hasOwn(r, "door") ? { door: null } : {},
12126
+ ...!(r["door"] === "sign-in" || r["door"] === "key") && Object.hasOwn(r, "altSteps") ? (() => {
12127
+ const a = decodeSteps(r["altSteps"]);
12128
+ return a.length ? { altSteps: a } : {};
12129
+ })() : {},
12119
12130
  launch: (() => {
12120
12131
  const u = asString(asRecord(r["launch"])["url"]);
12121
12132
  return u && /^https:\/\//.test(u) ? { url: u } : null;
@@ -13714,11 +13725,34 @@ var RANK = {
13714
13725
  paused: 2,
13715
13726
  queued: 3
13716
13727
  };
13728
+ var STOPPED = new Set(["failed", "cancelled"]);
13729
+ var whenOf = (r) => {
13730
+ for (const raw of [r.issuedAt, r.date]) {
13731
+ if (!raw) continue;
13732
+ const t = new Date(raw).getTime();
13733
+ if (!Number.isNaN(t)) return t;
13734
+ }
13735
+ return null;
13736
+ };
13737
+ var withinWindow = (r, now, days) => {
13738
+ const at = whenOf(r);
13739
+ return at === null || now - at <= days * 24 * 60 * 60 * 1e3;
13740
+ };
13741
+ var failuresNewestFirst = (a, b) => {
13742
+ const at = whenOf(a), bt = whenOf(b);
13743
+ if (at === null && bt === null) return 0;
13744
+ if (at === null) return -1;
13745
+ if (bt === null) return 1;
13746
+ return newestFirst(a, b);
13747
+ };
13717
13748
  function inFlight(runs) {
13718
- return runs.filter((r) => r.state !== "delivered" && !r.acked).slice().sort((a, b) => (RANK[a.state] ?? 9) - (RANK[b.state] ?? 9));
13749
+ return runs.filter((r) => r.state !== "delivered" && !STOPPED.has(r.state)).slice().sort((a, b) => (RANK[a.state] ?? 9) - (RANK[b.state] ?? 9));
13750
+ }
13751
+ function recentFailures(runs, { now = Date.now(), days = 7 } = {}) {
13752
+ return runs.filter((r) => STOPPED.has(r.state) && !r.acked && withinWindow(r, now, days)).slice().sort(failuresNewestFirst);
13719
13753
  }
13720
- function acknowledged(runs) {
13721
- return runs.filter((r) => r.state !== "delivered" && r.acked).slice().sort((a, b) => (RANK[a.state] ?? 9) - (RANK[b.state] ?? 9));
13754
+ function acknowledged(runs, { now = Date.now(), days = 7 } = {}) {
13755
+ return runs.filter((r) => STOPPED.has(r.state) && r.acked === true && withinWindow(r, now, days)).slice().sort(failuresNewestFirst);
13722
13756
  }
13723
13757
  function readStamps(rows) {
13724
13758
  const identity = (r) => [
@@ -13877,11 +13911,14 @@ function Home({ ctx }) {
13877
13911
  const allRuns = result?.kind === "ok" ? result.value : [];
13878
13912
  const runs = (0, import_react.useMemo)(() => runsFor(allRuns, ctx.owner), [allRuns, ctx.owner]);
13879
13913
  const rows = (0, import_react.useMemo)(() => inFlight(runs), [runs]);
13914
+ const stopped = (0, import_react.useMemo)(() => recentFailures(runs), [runs]);
13915
+ const [showStopped, setShowStopped] = (0, import_react.useState)(false);
13880
13916
  const acked = (0, import_react.useMemo)(() => acknowledged(runs), [runs]);
13881
13917
  const [showAcked, setShowAcked] = (0, import_react.useState)(false);
13882
13918
  const cards = (0, import_react.useMemo)(() => active(rows), [rows]);
13883
13919
  const queue = (0, import_react.useMemo)(() => waiting(rows), [rows]);
13884
13920
  const stamps = (0, import_react.useMemo)(() => readStamps(rows), [rows]);
13921
+ const stoppedStamps = (0, import_react.useMemo)(() => readStamps(stopped), [stopped]);
13885
13922
  const ackedStamps = (0, import_react.useMemo)(() => readStamps(acked), [acked]);
13886
13923
  const done = (0, import_react.useMemo)(() => recentlyFinished(runs, void 0, 3), [runs]);
13887
13924
  const recent = (0, import_react.useMemo)(() => done.map(lastFinishedOf).filter((r) => r !== null), [done]);
@@ -13933,6 +13970,28 @@ function Home({ ctx }) {
13933
13970
  onChanged: reload,
13934
13971
  stamps
13935
13972
  }) : null,
13973
+ stopped.length ? (0, import_jsx_runtime.jsxs)("div", {
13974
+ className: "home2-stopped",
13975
+ children: [(0, import_jsx_runtime.jsxs)("button", {
13976
+ type: "button",
13977
+ className: "home2-stopped-toggle",
13978
+ "aria-expanded": showStopped,
13979
+ onClick: () => setShowStopped((v) => !v),
13980
+ children: [
13981
+ stopped.length,
13982
+ " stopped recently",
13983
+ showStopped ? "" : " — show"
13984
+ ]
13985
+ }), showStopped ? (0, import_jsx_runtime.jsx)("div", {
13986
+ className: "home2-cards",
13987
+ children: stopped.map((r) => (0, import_jsx_runtime.jsx)(Card, {
13988
+ run: r,
13989
+ ctx,
13990
+ onChanged: reload,
13991
+ stamp: stoppedStamps.get(r.runId) ?? null
13992
+ }, r.runId))
13993
+ }) : null]
13994
+ }) : null,
13936
13995
  acked.length ? (0, import_jsx_runtime.jsxs)("div", {
13937
13996
  className: "home2-acked",
13938
13997
  children: [(0, import_jsx_runtime.jsxs)("button", {
@@ -13972,7 +14031,7 @@ function Home({ ctx }) {
13972
14031
  total: finishedCount,
13973
14032
  ctx
13974
14033
  }) : null,
13975
- answer === "ok" && !cards.length && !queue.length && !recent.length ? (0, import_jsx_runtime.jsx)(FirstRun$1, { onNew: canRun(ctx.me) ? () => ctx.go("/portal/new") : null }) : null,
14034
+ answer === "ok" && !cards.length && !queue.length && !recent.length && !stopped.length && !acked.length ? (0, import_jsx_runtime.jsx)(FirstRun$1, { onNew: canRun(ctx.me) ? () => ctx.go("/portal/new") : null }) : null,
13976
14035
  usageRes?.kind === "ok" ? (0, import_jsx_runtime.jsx)("p", {
13977
14036
  className: "home2-limits mono",
13978
14037
  children: limitLine(usageRes.value.today, usageRes.value.dailyRuns)
@@ -21521,74 +21580,103 @@ function StepsPanel({ offer, measuring = false, state = AT_REST, onBlock, onSecr
21521
21580
  className: "steps-name",
21522
21581
  children: offer.name
21523
21582
  }),
21524
- (0, import_jsx_runtime.jsx)("ol", {
21525
- className: "steps",
21526
- children: offer.steps.map((s, i) => {
21527
- const c = s.copy;
21528
- const flashed = !measuring && state.copiedAt === i;
21529
- const landed = measuring && c?.kind === "secret" ? fill(c, mask("")) : state.landedAt === i ? state.landed : null;
21530
- return (0, import_jsx_runtime.jsx)("li", { children: (0, import_jsx_runtime.jsxs)("div", { children: [
21531
- (0, import_jsx_runtime.jsx)("div", {
21532
- className: "step-text",
21533
- children: (0, import_jsx_runtime.jsx)(Marked, { text: s.text })
21534
- }),
21535
- c?.kind === "block" ? (0, import_jsx_runtime.jsxs)("div", {
21536
- className: "codeblock",
21537
- children: [(0, import_jsx_runtime.jsx)("pre", { children: c.text }), (0, import_jsx_runtime.jsx)("button", {
21538
- type: "button",
21539
- className: `btn-ghost${flashed ? " is-copied" : ""}`,
21540
- "data-step": i,
21541
- tabIndex: measuring ? -1 : void 0,
21542
- onClick: () => onBlock?.(i, c.text),
21543
- children: flashed ? "✓ Copied" : "Copy"
21544
- })]
21545
- }) : null,
21546
- c?.kind === "secret" ? (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
21547
- (0, import_jsx_runtime.jsx)("div", {
21548
- className: "secret-btn",
21549
- children: (0, import_jsx_runtime.jsx)("button", {
21550
- type: "button",
21551
- className: `btn-primary${flashed ? " is-copied" : ""}`,
21552
- "data-step": i,
21553
- tabIndex: measuring ? -1 : void 0,
21554
- onClick: () => onSecret?.(i, c),
21555
- children: flashed ? (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [(0, import_jsx_runtime.jsx)(Icon, {
21556
- name: "check",
21557
- size: 15
21558
- }), "Copied"] }) : landed ? "Copy again" : c.label
21559
- })
21560
- }),
21561
- landed ? (0, import_jsx_runtime.jsxs)("div", {
21562
- className: "landed",
21563
- children: [(0, import_jsx_runtime.jsx)("div", {
21564
- className: "landed-label",
21565
- children: "On your clipboard now"
21566
- }), landed.split("\n").map((line, n) => (0, import_jsx_runtime.jsx)("div", {
21567
- className: "mono",
21568
- children: line
21569
- }, n))]
21570
- }) : null,
21571
- !measuring && state.revealedAt === i && state.revealed ? (0, import_jsx_runtime.jsxs)("div", {
21572
- className: "landed",
21573
- children: [(0, import_jsx_runtime.jsx)("p", {
21574
- className: "ai-said",
21575
- children: "Your browser would not let us copy it. Copy this by hand — it is yours, and it will not be shown again."
21576
- }), (0, import_jsx_runtime.jsx)("pre", {
21577
- className: "ai-pre",
21578
- children: state.revealed
21579
- })]
21580
- }) : null
21581
- ] }) : null,
21582
- s.hint ? (0, import_jsx_runtime.jsx)("div", {
21583
- className: "step-hint",
21584
- children: (0, import_jsx_runtime.jsx)(Marked, { text: s.hint })
21585
- }) : null
21586
- ] }) }, i);
21587
- })
21588
- })
21583
+ offer.door === null && offer.altSteps?.length ? (0, import_jsx_runtime.jsx)("p", {
21584
+ className: "steps-unknown",
21585
+ children: "We could not check how this Clearotron lets an assistant in, so both ways are below. Try the first — your assistant will tell you if it needs the other."
21586
+ }) : null,
21587
+ (0, import_jsx_runtime.jsx)(StepList, {
21588
+ steps: offer.steps,
21589
+ offset: 0,
21590
+ measuring,
21591
+ state,
21592
+ onBlock,
21593
+ onSecret
21594
+ }),
21595
+ offer.door === null && offer.altSteps?.length ? (0, import_jsx_runtime.jsxs)("div", {
21596
+ className: "steps-alt",
21597
+ children: [(0, import_jsx_runtime.jsx)("h4", {
21598
+ className: "steps-alt-head",
21599
+ children: "If your assistant asks to be let in another way"
21600
+ }), (0, import_jsx_runtime.jsx)(StepList, {
21601
+ steps: offer.altSteps,
21602
+ offset: offer.steps.length,
21603
+ measuring,
21604
+ state,
21605
+ onBlock,
21606
+ onSecret
21607
+ })]
21608
+ }) : null
21589
21609
  ]
21590
21610
  });
21591
21611
  }
21612
+ function StepList({ steps, offset, measuring = false, state = AT_REST, onBlock, onSecret }) {
21613
+ return (0, import_jsx_runtime.jsx)("ol", {
21614
+ className: "steps",
21615
+ children: steps.map((s, n) => {
21616
+ const i = n + offset;
21617
+ const c = s.copy;
21618
+ const flashed = !measuring && state.copiedAt === i;
21619
+ const landed = measuring && c?.kind === "secret" ? fill(c, mask("")) : state.landedAt === i ? state.landed : null;
21620
+ return (0, import_jsx_runtime.jsx)("li", { children: (0, import_jsx_runtime.jsxs)("div", { children: [
21621
+ (0, import_jsx_runtime.jsx)("div", {
21622
+ className: "step-text",
21623
+ children: (0, import_jsx_runtime.jsx)(Marked, { text: s.text })
21624
+ }),
21625
+ c?.kind === "block" ? (0, import_jsx_runtime.jsxs)("div", {
21626
+ className: "codeblock",
21627
+ children: [(0, import_jsx_runtime.jsx)("pre", { children: c.text }), (0, import_jsx_runtime.jsx)("button", {
21628
+ type: "button",
21629
+ className: `btn-ghost${flashed ? " is-copied" : ""}`,
21630
+ "data-step": i,
21631
+ tabIndex: measuring ? -1 : void 0,
21632
+ onClick: () => onBlock?.(i, c.text),
21633
+ children: flashed ? "✓ Copied" : "Copy"
21634
+ })]
21635
+ }) : null,
21636
+ c?.kind === "secret" ? (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
21637
+ (0, import_jsx_runtime.jsx)("div", {
21638
+ className: "secret-btn",
21639
+ children: (0, import_jsx_runtime.jsx)("button", {
21640
+ type: "button",
21641
+ className: `btn-primary${flashed ? " is-copied" : ""}`,
21642
+ "data-step": i,
21643
+ tabIndex: measuring ? -1 : void 0,
21644
+ onClick: () => onSecret?.(i, c),
21645
+ children: flashed ? (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [(0, import_jsx_runtime.jsx)(Icon, {
21646
+ name: "check",
21647
+ size: 15
21648
+ }), "Copied"] }) : landed ? "Copy again" : c.label
21649
+ })
21650
+ }),
21651
+ landed ? (0, import_jsx_runtime.jsxs)("div", {
21652
+ className: "landed",
21653
+ children: [(0, import_jsx_runtime.jsx)("div", {
21654
+ className: "landed-label",
21655
+ children: "On your clipboard now"
21656
+ }), landed.split("\n").map((line, n) => (0, import_jsx_runtime.jsx)("div", {
21657
+ className: "mono",
21658
+ children: line
21659
+ }, n))]
21660
+ }) : null,
21661
+ !measuring && state.revealedAt === i && state.revealed ? (0, import_jsx_runtime.jsxs)("div", {
21662
+ className: "landed",
21663
+ children: [(0, import_jsx_runtime.jsx)("p", {
21664
+ className: "ai-said",
21665
+ children: "Your browser would not let us copy it. Copy this by hand — it is yours, and it will not be shown again."
21666
+ }), (0, import_jsx_runtime.jsx)("pre", {
21667
+ className: "ai-pre",
21668
+ children: state.revealed
21669
+ })]
21670
+ }) : null
21671
+ ] }) : null,
21672
+ s.hint ? (0, import_jsx_runtime.jsx)("div", {
21673
+ className: "step-hint",
21674
+ children: (0, import_jsx_runtime.jsx)(Marked, { text: s.hint })
21675
+ }) : null
21676
+ ] }) }, i);
21677
+ })
21678
+ });
21679
+ }
21592
21680
  function NotOnline() {
21593
21681
  return (0, import_jsx_runtime.jsxs)("div", {
21594
21682
  className: "notice ai-none",
@@ -23807,6 +23895,7 @@ function PeopleAccess({ ctx }) {
23807
23895
  }) }) : v.people.map((p) => (0, import_jsx_runtime.jsx)(Row, {
23808
23896
  person: p,
23809
23897
  you: p.email.toLowerCase() === ctx.me.email.toLowerCase(),
23898
+ canModify: !v.localSignIn,
23810
23899
  onModify: () => ctx.go(`${MODIFY_PERSON.path}?email=${encodeURIComponent(p.email)}`)
23811
23900
  }, p.email)) })]
23812
23901
  })
@@ -23937,7 +24026,7 @@ function Observed({ result }) {
23937
24026
  ]
23938
24027
  });
23939
24028
  }
23940
- function Row({ person, you, onModify }) {
24029
+ function Row({ person, you, canModify, onModify }) {
23941
24030
  const chips = accessChips(person.access);
23942
24031
  const viewOnly = !person.permissions.run && !person.permissions.manage;
23943
24032
  return (0, import_jsx_runtime.jsxs)("tr", { children: [
@@ -23988,8 +24077,9 @@ function Row({ person, you, onModify }) {
23988
24077
  children: you ? null : (0, import_jsx_runtime.jsx)("button", {
23989
24078
  type: "button",
23990
24079
  className: "pill",
24080
+ disabled: !canModify,
23991
24081
  style: {
23992
- cursor: "pointer",
24082
+ cursor: canModify ? "pointer" : "default",
23993
24083
  fontSize: 12
23994
24084
  },
23995
24085
  onClick: onModify,
@@ -24103,7 +24193,7 @@ function GiveAccess({ ctx }) {
24103
24193
  setBusy(false);
24104
24194
  }
24105
24195
  };
24106
- const failure = result && !isOk(result) ? result.kind === "conflict" ? "This Clearotron now signs in one person, so nobody can be added. Nothing was saved." : result.kind === "notFound" ? "You can only give access to what you have access to yourself. Nothing was saved." : result.kind === "reject" ? result.errors.join(" ") : saveFailureText(result, "That could not be saved. Nothing was changed.") : null;
24196
+ const failure = result && !isOk(result) ? result.kind === "gate" && result.detail?.code === "local_sign_in" ? "This Clearotron now signs in one person, so nobody can be added. Nothing was saved." : result.kind === "conflict" ? "This Clearotron now signs in one person, so nobody can be added. Nothing was saved." : result.kind === "notFound" ? "You can only give access to what you have access to yourself. Nothing was saved." : result.kind === "reject" ? result.errors.join(" ") : saveFailureText(result, "That could not be saved. Nothing was changed.") : null;
24107
24197
  return (0, import_jsx_runtime.jsx)("div", {
24108
24198
  className: "screen",
24109
24199
  children: (0, import_jsx_runtime.jsxs)("div", {
@@ -24408,6 +24498,7 @@ function ModifyAccess({ ctx }) {
24408
24498
  }, person.email);
24409
24499
  }
24410
24500
  function Form({ ctx, person, view }) {
24501
+ const readOnlyPeople = view.localSignIn;
24411
24502
  const held = heldBy(person);
24412
24503
  const [permissions, setPermissions] = (0, import_react.useState)(person.permissions);
24413
24504
  const [chosen, setChosen] = (0, import_react.useState)(held);
@@ -24476,7 +24567,7 @@ function Form({ ctx, person, view }) {
24476
24567
  setBusy(false);
24477
24568
  }
24478
24569
  };
24479
- const failure = (r) => r === null || isOk(r) ? null : r.kind === "conflict" ? "This Clearotron now signs in one person, so nobody else can be changed. Nothing was saved." : r.kind === "notFound" ? "That is not access you can change. Nothing was saved." : r.kind === "reject" ? r.errors.join(" ") : saveFailureText(r, "That could not be saved. Nothing was changed.");
24570
+ const failure = (r) => r === null || isOk(r) ? null : r.kind === "gate" && r.detail?.code === "local_sign_in" ? "This Clearotron now signs in one person, so nobody else can be changed. Nothing was saved." : r.kind === "conflict" ? "This Clearotron now signs in one person, so nobody else can be changed. Nothing was saved." : r.kind === "notFound" ? "That is not access you can change. Nothing was saved." : r.kind === "reject" ? r.errors.join(" ") : saveFailureText(r, "That could not be saved. Nothing was changed.");
24480
24571
  const problem = failure(saved) ?? failure(gone);
24481
24572
  const loses = person.access.length ? person.access.map((a) => a.kind === "everything" ? "everything on this Clearotron" : a.name ?? a.key) : [];
24482
24573
  const holdsKey = (person.keys ?? 0) > 0;
@@ -24640,6 +24731,21 @@ function Form({ ctx, person, view }) {
24640
24731
  ] })]
24641
24732
  })
24642
24733
  }) : null,
24734
+ readOnlyPeople ? (0, import_jsx_runtime.jsx)("div", {
24735
+ className: "notice quiet",
24736
+ style: { marginBottom: 14 },
24737
+ children: (0, import_jsx_runtime.jsxs)("p", {
24738
+ style: {
24739
+ margin: 0,
24740
+ fontSize: 13.5,
24741
+ color: "var(--text-muted)"
24742
+ },
24743
+ children: [(0, import_jsx_runtime.jsx)("b", {
24744
+ style: { color: "var(--text-strong)" },
24745
+ children: "This Clearotron signs in one person: you."
24746
+ }), " Nobody else can be changed or removed here. To hold more than one person, put it behind a login system such as your company single sign-on."]
24747
+ })
24748
+ }) : null,
24643
24749
  (0, import_jsx_runtime.jsxs)("div", {
24644
24750
  style: {
24645
24751
  display: "flex",
@@ -24650,7 +24756,7 @@ function Form({ ctx, person, view }) {
24650
24756
  (0, import_jsx_runtime.jsx)("button", {
24651
24757
  type: "button",
24652
24758
  className: "btn-primary",
24653
- disabled: !dirty || busy || leftWithNothing,
24759
+ disabled: !dirty || busy || leftWithNothing || readOnlyPeople,
24654
24760
  onClick: () => void save(),
24655
24761
  children: busy && !confirming ? "Saving…" : "Save changes"
24656
24762
  }),
@@ -24768,10 +24874,10 @@ function Form({ ctx, person, view }) {
24768
24874
  type: "button",
24769
24875
  className: "pill",
24770
24876
  style: {
24771
- cursor: "pointer",
24877
+ cursor: readOnlyPeople ? "default" : "pointer",
24772
24878
  fontSize: 12
24773
24879
  },
24774
- disabled: busy,
24880
+ disabled: busy || readOnlyPeople,
24775
24881
  onClick: () => setConfirming(true),
24776
24882
  children: whole ? domain ? `Remove everyone at ${domain}` : "Remove from Clearotron" : `Remove from ${[...held.orgs].map(orgName).join(", ") || "here"}`
24777
24883
  }), whole ? null : (0, import_jsx_runtime.jsx)("p", {
@@ -2082,6 +2082,28 @@ input.filter {
2082
2082
  Quiet on purpose. These are runs the reader has already dealt with, and the whole point of the
2083
2083
  feature is that they stop competing with live work for attention — a panel styled like the queue
2084
2084
  would put them back in the fight it just took them out of. */
2085
+ /* WHAT STOPPED RECENTLY — louder than acknowledged, quieter than live work.
2086
+ The count carries the page's only standing signal that something failed, so it is readable rather
2087
+ than faint; the fold below it is the part that keeps the dashboard about what is happening now. */
2088
+ .home2-stopped { margin-top: 18px; }
2089
+ .home2-stopped-toggle {
2090
+ padding: 0; background: none; border: 0; cursor: pointer; font: inherit; font-size: 13.5px;
2091
+ font-weight: 600; color: var(--text-strong); text-decoration: underline;
2092
+ }
2093
+ .home2-stopped-toggle:hover { color: var(--text-accent); }
2094
+ .home2-stopped .home2-cards { margin-top: 11px; }
2095
+
2096
+ /* THE UNKNOWN DOOR — a sentence the reader must not skip, and the alternative under its own heading.
2097
+ Both are quiet relative to the steps themselves: the primary set is still what to try first. */
2098
+ .steps-unknown {
2099
+ margin: 0 0 12px; font-size: 12.5px; line-height: 1.5; color: var(--text-muted);
2100
+ border-left: 2px solid var(--border-hairline); padding-left: 10px;
2101
+ }
2102
+ .steps-alt { margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--border-hairline); }
2103
+ .steps-alt-head {
2104
+ margin: 0 0 10px; font-size: 12.5px; font-weight: 600; letter-spacing: .02em; color: var(--text-muted);
2105
+ }
2106
+
2085
2107
  .home2-acked { margin-top: 13px; }
2086
2108
  .home2-acked-toggle {
2087
2109
  padding: 0; background: none; border: 0; cursor: pointer; font: inherit; font-size: 12.5px;
@@ -49,8 +49,8 @@
49
49
  -->
50
50
  <link rel="preconnect" href="https://api.fontshare.com" crossorigin />
51
51
  <link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap" rel="stylesheet" />
52
- <script type="module" crossorigin src="/portal/assets/index-C-QysgZM.js"></script>
53
- <link rel="stylesheet" crossorigin href="/portal/assets/index-CaSZbEMb.css">
52
+ <script type="module" crossorigin src="/portal/assets/index-ChIQsMYp.js"></script>
53
+ <link rel="stylesheet" crossorigin href="/portal/assets/index-DBIs21e4.css">
54
54
  </head>
55
55
  <body>
56
56
  <div id="root"></div>
@@ -2,7 +2,7 @@
2
2
  "name": "portal-ui",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.1-beta.3",
5
+ "version": "0.3.1",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
8
8
  "engines": {
@@ -1,5 +1,13 @@
1
1
  # trademark-oauth-mcp-bridge
2
2
 
3
+ ## 0.3.1
4
+
5
+ No changes in this release.
6
+
7
+ ## 0.3.1-beta.4
8
+
9
+ No changes in this release.
10
+
3
11
  ## 0.3.1-beta.3
4
12
 
5
13
  No changes in this release.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-oauth-mcp-bridge",
3
- "version": "0.3.1-beta.3",
3
+ "version": "0.3.1",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
@@ -713,12 +713,63 @@ const OWN_NUMBER = /^\s*(?:at\s+\d|:\d)/;
713
713
  // The cost of naming a file is that a REAL bare citation added there also escapes. That is the same trade
714
714
  // the reference-strip signatures make for the same reason, and it is preferable to a pattern, which would
715
715
  // quietly widen to files nobody considered.
716
- export const RATCHET_EXEMPT = ["driver/test/a-new-citation-carries-something-that-can-be-checked.test.mjs"];
716
+ // ── EXEMPT FROM THE RATCHET, AND ONLY WHERE SOMETHING STRONGER CHECKS THE SAME CITATIONS ───────────
717
+ //
718
+ // The ratchet refuses a NEW bare citation, which is right everywhere it is the only check. It is not the
719
+ // only check on `contract-e3-backlog.mjs`: `contract-audit.test.mjs` reads each backlog row's own
720
+ // evidence, finds where that dictation now starts, and refuses a row whose number points away from it.
721
+ // That is a stronger guarantee than "a symbol sits beside the number" — it verifies the number itself.
722
+ //
723
+ // THE TWO ARE IN DIRECT CONFLICT ON THAT FILE, and the conflict is not hypothetical: an edit to
724
+ // `stages.mjs` moves those rows, the evidence guard then DEMANDS they be repointed, and every repointed
725
+ // row reads to the ratchet as a newly added bare citation. Under both rules at once the only permitted
726
+ // change to that file is no change at all. Measured on the change that found this: 31 rows repointed at
727
+ // the evidence guard's own instruction, 31 refused by the ratchet.
728
+ //
729
+ // So the file is exempt from the weaker check and stays under the stronger one. Anything NOT covered by
730
+ // an evidence guard does not belong on this list — the exemption is earned by being checked better, not
731
+ // by being inconvenient.
732
+ //
733
+ // `contract-vocabulary.mjs` is the same conflict one file over, and it arrives by the same route. Each
734
+ // INNER_CODES row carries a `mints` array naming the line that WRITES its code, and
735
+ // `contract-audit.test.mjs` re-reads every one of them on every run: it checks the cited line still
736
+ // mints that code, and when it does not it prints where the code is NOW and refuses. That arm's own
737
+ // message says "Do not delete the citation: it is what makes the ruling checkable rather than merely
738
+ // written down" — so the ratchet's remedy, dropping the number, is the one repair that file forbids.
739
+ //
740
+ // Measured on the change that added this: a 41-line insertion into `connotation-search.mjs` moved 12
741
+ // mint sites, the audit demanded all 12 be repointed, and the ratchet then read all 12 as newly added
742
+ // bare citations. Under both rules at once the only permitted change is no change at all.
743
+ //
744
+ // The alternative considered and rejected: teach the audit's `m.split(":")` to accept `file:line symbol`
745
+ // and tag the 12. It widens a parser every INNER_CODES row depends on, to buy a weaker guarantee than
746
+ // the one already in place — the audit verifies the NUMBER, which is what a symbol only approximates.
747
+ //
748
+ // SO THE EXEMPTION IS SCOPED TO THE LINES THE AUDIT ACTUALLY COVERS, not to the file. Measured on this
749
+ // tree: `contract-vocabulary.mjs` carries 117 numbered citations, 21 of them inside `mints` arrays and 96
750
+ // outside. A file-wide entry would have taken the ratchet off all 117 to protect 21 — and the 96 are
751
+ // precisely the prose and `site:` citations nothing else checks. Every `mints` array is written on one
752
+ // line, so a `when` pattern matching that line is exact rather than approximate.
753
+ //
754
+ // An entry is either a bare path (the whole file, for the two cases where the whole file earns it) or
755
+ // `{ file, when }`, where `when` must match the LINE the citation sits on. A `when` that stops matching
756
+ // re-arms the ratchet over those lines, which is the failure direction worth having.
757
+ export const RATCHET_EXEMPT = [
758
+ "driver/test/a-new-citation-carries-something-that-can-be-checked.test.mjs",
759
+ "driver/contract-e3-backlog.mjs",
760
+ // The `mints` lines only: `contract-audit.test.mjs` verifies each of those numbers against the line
761
+ // that writes the code. Everything else in this file stays under the ratchet.
762
+ { file: "driver/contract-vocabulary.mjs", when: /mints:\s*\[/ },
763
+ ];
764
+
765
+ /** Whether a citation on `text` in `file` is exempt from the ratchet. PURE. */
766
+ export const ratchetExempt = (file, text) => RATCHET_EXEMPT.some((e) =>
767
+ (typeof e === "string" ? e === file : e.file === file && e.when.test(String(text ?? ""))));
717
768
 
718
769
  export function newBareCitations(addedLines) {
719
770
  const out = [];
720
771
  for (const { file, line, text } of addedLines ?? []) {
721
- if (RATCHET_EXEMPT.includes(file)) continue;
772
+ if (ratchetExempt(file, text)) continue;
722
773
  for (const m of String(text ?? "").matchAll(CITE_RE)) {
723
774
  // The same discriminator the corpus scan uses: a second `:number` closing a paren is a V8 stack
724
775
  // frame, not a citation. Reading them as citations would refuse a fixture that captured a trace.