recappi 0.1.69 → 0.1.70

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/dist/index.js CHANGED
@@ -150,6 +150,12 @@ function transcribeFraction(item) {
150
150
  if (!total || total <= 0 || done == null) return null;
151
151
  return Math.max(0, Math.min(1, done / total));
152
152
  }
153
+ function isJobStalled(item, nowMs) {
154
+ return item.status === "running" && typeof item.claimExpiresAt === "number" && item.claimExpiresAt < nowMs;
155
+ }
156
+ function effectiveJobStatus(item, nowMs) {
157
+ return isJobStalled(item, nowMs) ? "stalled" : item.status;
158
+ }
153
159
  function statusStyle(status) {
154
160
  switch (status) {
155
161
  case "running":
@@ -160,6 +166,8 @@ function statusStyle(status) {
160
166
  return { label: "Ready", color: "green" };
161
167
  case "failed":
162
168
  return { label: "Failed", color: "red" };
169
+ case "stalled":
170
+ return { label: "Stalled", color: "yellow" };
163
171
  default:
164
172
  return { label: status, color: "gray" };
165
173
  }
@@ -191,11 +199,14 @@ function statusGlyph(status, spinnerFrame) {
191
199
  return "\u2713";
192
200
  case "failed":
193
201
  return "\u2717";
202
+ case "stalled":
203
+ return "!";
194
204
  default:
195
205
  return "\u2022";
196
206
  }
197
207
  }
198
- function jobDetail(item) {
208
+ function jobDetail(item, nowMs) {
209
+ if (nowMs != null && isJobStalled(item, nowMs)) return "stalled \u2014 worker lost \xB7 T retry";
199
210
  if (item.status === "running") {
200
211
  const fraction = transcribeFraction(item);
201
212
  if (fraction != null) {
@@ -735,17 +746,19 @@ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
735
746
  function JobRow({
736
747
  item,
737
748
  selected,
738
- spinnerFrame
749
+ spinnerFrame,
750
+ nowMs
739
751
  }) {
740
- const style = statusStyle(item.status);
741
- const glyph = statusGlyph(item.status, spinnerFrame);
752
+ const status = nowMs != null ? effectiveJobStatus(item, nowMs) : item.status;
753
+ const style = statusStyle(status);
754
+ const glyph = statusGlyph(status, spinnerFrame);
742
755
  const title = item.recording?.title ?? item.recordingId;
743
756
  return /* @__PURE__ */ jsxs5(Box5, { children: [
744
757
  /* @__PURE__ */ jsx7(Box5, { width: 3, children: /* @__PURE__ */ jsx7(Text5, { color: "cyan", children: selected ? "\u25B8" : "" }) }),
745
758
  /* @__PURE__ */ jsx7(Box5, { width: 2, children: /* @__PURE__ */ jsx7(Text5, { color: style.color, children: glyph }) }),
746
759
  /* @__PURE__ */ jsx7(Box5, { width: 13, children: /* @__PURE__ */ jsx7(Text5, { color: style.color, children: style.label }) }),
747
760
  /* @__PURE__ */ jsx7(Box5, { width: 26, children: /* @__PURE__ */ jsx7(Text5, { bold: selected, wrap: "truncate-end", children: title }) }),
748
- /* @__PURE__ */ jsx7(Text5, { dimColor: !selected, children: jobDetail(item) })
761
+ /* @__PURE__ */ jsx7(Text5, { dimColor: !selected, children: jobDetail(item, nowMs) })
749
762
  ] });
750
763
  }
751
764
  var init_JobRow = __esm({
@@ -761,7 +774,8 @@ import { jsx as jsx8 } from "react/jsx-runtime";
761
774
  function JobsView({
762
775
  items,
763
776
  selectedIndex,
764
- spinnerFrame
777
+ spinnerFrame,
778
+ nowMs
765
779
  }) {
766
780
  if (items.length === 0) {
767
781
  return /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: "No transcription jobs yet \u2014 run: recappi upload <file> --transcribe" }) });
@@ -771,7 +785,8 @@ function JobsView({
771
785
  {
772
786
  item,
773
787
  selected: index === selectedIndex,
774
- spinnerFrame
788
+ spinnerFrame,
789
+ nowMs
775
790
  },
776
791
  item.jobId
777
792
  )) });
@@ -794,6 +809,7 @@ function recordingTitle2(item) {
794
809
  function recordingProcessingState(item, jobStatus, spinnerFrame) {
795
810
  if (item.status === "uploading") return { glyph: "\u2191", color: "cyan" };
796
811
  if (item.status === "failed" || jobStatus === "failed") return { glyph: "\u2717", color: "red" };
812
+ if (jobStatus === "stalled") return { glyph: "!", color: "yellow" };
797
813
  if (jobStatus === "running") return { glyph: spinnerChar(spinnerFrame), color: "cyan" };
798
814
  if (jobStatus === "queued") return { glyph: "\u25CB", color: "yellow" };
799
815
  if (item.status === "aborted") return { glyph: "\u2022", color: "gray" };
@@ -1019,7 +1035,8 @@ function JobDetailView({
1019
1035
  spinnerFrame,
1020
1036
  nowMs
1021
1037
  }) {
1022
- const style = statusStyle(item.status);
1038
+ const status = effectiveJobStatus(item, nowMs);
1039
+ const style = statusStyle(status);
1023
1040
  const links = resolveJobLinks(item, origin);
1024
1041
  const title = item.recording?.title ?? item.recordingId;
1025
1042
  return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", paddingX: 1, children: [
@@ -1067,10 +1084,10 @@ function JobDetailView({
1067
1084
  /* @__PURE__ */ jsx13(
1068
1085
  TimelineRow,
1069
1086
  {
1070
- label: item.status === "failed" ? "Failed" : item.status === "running" ? "Transcribing" : "Finished",
1087
+ label: status === "failed" ? "Failed" : status === "stalled" ? "Stalled \u2014 worker lost" : status === "running" ? "Transcribing" : "Finished",
1071
1088
  done: item.finishedAt != null,
1072
1089
  failed: item.status === "failed",
1073
- running: item.status === "running",
1090
+ running: status === "running",
1074
1091
  at: item.finishedAt,
1075
1092
  nowMs
1076
1093
  }
@@ -2452,12 +2469,13 @@ function AppShell({
2452
2469
  const id = setInterval(() => setSpinnerFrame((f) => f + 1), spinnerMs);
2453
2470
  return () => clearInterval(id);
2454
2471
  }, [hasRunning, loaded, spinnerMs]);
2455
- const jobRank = (s) => s === "running" ? 4 : s === "queued" ? 3 : s === "failed" ? 2 : s === "succeeded" ? 1 : 0;
2472
+ const jobRank = (s) => s === "running" ? 5 : s === "stalled" ? 4 : s === "queued" ? 3 : s === "failed" ? 2 : s === "succeeded" ? 1 : 0;
2456
2473
  const jobStatusByRecording = /* @__PURE__ */ new Map();
2457
2474
  for (const job of jobs) {
2475
+ const status = effectiveJobStatus(job, now());
2458
2476
  const prev = jobStatusByRecording.get(job.recordingId);
2459
- if (!prev || jobRank(job.status) > jobRank(prev)) {
2460
- jobStatusByRecording.set(job.recordingId, job.status);
2477
+ if (!prev || jobRank(status) > jobRank(prev)) {
2478
+ jobStatusByRecording.set(job.recordingId, status);
2461
2479
  }
2462
2480
  }
2463
2481
  useEffect4(() => {
@@ -2804,7 +2822,8 @@ function AppShell({
2804
2822
  {
2805
2823
  items: jobs.slice(win.start, win.end),
2806
2824
  selectedIndex: selected - win.start,
2807
- spinnerFrame
2825
+ spinnerFrame,
2826
+ nowMs: now()
2808
2827
  }
2809
2828
  );
2810
2829
  }