recappi 0.1.68 → 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
  }
@@ -2007,6 +2024,7 @@ function AppShell({
2007
2024
  const [selected, setSelected] = useState8(0);
2008
2025
  const [spinnerFrame, setSpinnerFrame] = useState8(0);
2009
2026
  const [loadingMoreRecordings, setLoadingMoreRecordings] = useState8(false);
2027
+ const [loaded, setLoaded] = useState8(false);
2010
2028
  const [loadError, setLoadError] = useState8(void 0);
2011
2029
  const [notice, setNotice] = useState8(void 0);
2012
2030
  const [summaryCache, setSummaryCache] = useState8(() => /* @__PURE__ */ new Map());
@@ -2246,6 +2264,7 @@ function AppShell({
2246
2264
  } else {
2247
2265
  setAccountStatus("error");
2248
2266
  }
2267
+ setLoaded(true);
2249
2268
  }, [fetchJobs, fetchRecordings, fetchDashboardStats, fetchAccountStatus]);
2250
2269
  const transcribeStoppedRecording = useCallback(async () => {
2251
2270
  const current = liveRecord;
@@ -2446,16 +2465,17 @@ function AppShell({
2446
2465
  }, [refresh, pollMs]);
2447
2466
  const hasRunning = jobs.some((item) => item.status === "running");
2448
2467
  useEffect4(() => {
2449
- if (!hasRunning) return;
2468
+ if (!hasRunning && loaded) return;
2450
2469
  const id = setInterval(() => setSpinnerFrame((f) => f + 1), spinnerMs);
2451
2470
  return () => clearInterval(id);
2452
- }, [hasRunning, spinnerMs]);
2453
- const jobRank = (s) => s === "running" ? 4 : s === "queued" ? 3 : s === "failed" ? 2 : s === "succeeded" ? 1 : 0;
2471
+ }, [hasRunning, loaded, spinnerMs]);
2472
+ const jobRank = (s) => s === "running" ? 5 : s === "stalled" ? 4 : s === "queued" ? 3 : s === "failed" ? 2 : s === "succeeded" ? 1 : 0;
2454
2473
  const jobStatusByRecording = /* @__PURE__ */ new Map();
2455
2474
  for (const job of jobs) {
2475
+ const status = effectiveJobStatus(job, now());
2456
2476
  const prev = jobStatusByRecording.get(job.recordingId);
2457
- if (!prev || jobRank(job.status) > jobRank(prev)) {
2458
- jobStatusByRecording.set(job.recordingId, job.status);
2477
+ if (!prev || jobRank(status) > jobRank(prev)) {
2478
+ jobStatusByRecording.set(job.recordingId, status);
2459
2479
  }
2460
2480
  }
2461
2481
  useEffect4(() => {
@@ -2682,12 +2702,12 @@ function AppShell({
2682
2702
  }
2683
2703
  if (screen.kind === "jobDetail") {
2684
2704
  const job = jobs.find((j) => j.jobId === screen.jobId);
2685
- if (!job) return /* @__PURE__ */ jsx19(Missing, { label: "Job" });
2705
+ if (!job) return !loaded ? /* @__PURE__ */ jsx19(Loading, { label: "job" }) : /* @__PURE__ */ jsx19(Missing, { label: "Job" });
2686
2706
  return /* @__PURE__ */ jsx19(Detail, { notice, children: /* @__PURE__ */ jsx19(JobDetailView, { item: job, origin, spinnerFrame, nowMs: now() }) });
2687
2707
  }
2688
2708
  if (screen.kind === "recordingDetail") {
2689
2709
  const rec = recordings.find((r) => r.recordingId === screen.recordingId);
2690
- if (!rec) return /* @__PURE__ */ jsx19(Missing, { label: "Recording" });
2710
+ if (!rec) return !loaded ? /* @__PURE__ */ jsx19(Loading, { label: "recording" }) : /* @__PURE__ */ jsx19(Missing, { label: "Recording" });
2691
2711
  const detailTranscript = rec.activeTranscriptId ? transcriptCache.get(rec.activeTranscriptId) : void 0;
2692
2712
  return /* @__PURE__ */ jsx19(Detail, { notice, children: /* @__PURE__ */ jsx19(
2693
2713
  RecordingDetailView,
@@ -2754,7 +2774,14 @@ function AppShell({
2754
2774
  const tab = screen.kind === "jobs" ? "jobs" : screen.kind === "account" ? "account" : "overview";
2755
2775
  let body;
2756
2776
  let position = "";
2757
- if (screen.kind === "overview") {
2777
+ if (!loaded) {
2778
+ position = "";
2779
+ const spin = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F"[spinnerFrame % 10];
2780
+ body = /* @__PURE__ */ jsx19(Box17, { marginTop: 1, children: /* @__PURE__ */ jsxs16(Text17, { color: "cyan", children: [
2781
+ spin,
2782
+ " Loading\u2026"
2783
+ ] }) });
2784
+ } else if (screen.kind === "overview") {
2758
2785
  const listBudget = Math.max(3, size.rows - 6);
2759
2786
  const buckets = recordings.map((r) => dateBucket(r.createdAt, now()));
2760
2787
  const win = groupedListWindow(buckets, selected, listBudget);
@@ -2795,7 +2822,8 @@ function AppShell({
2795
2822
  {
2796
2823
  items: jobs.slice(win.start, win.end),
2797
2824
  selectedIndex: selected - win.start,
2798
- spinnerFrame
2825
+ spinnerFrame,
2826
+ nowMs: now()
2799
2827
  }
2800
2828
  );
2801
2829
  }
@@ -2830,6 +2858,13 @@ function Missing({ label }) {
2830
2858
  /* @__PURE__ */ jsx19(Text17, { dimColor: true, children: "esc back \xB7 q quit" })
2831
2859
  ] });
2832
2860
  }
2861
+ function Loading({ label }) {
2862
+ return /* @__PURE__ */ jsx19(Box17, { paddingX: 1, children: /* @__PURE__ */ jsxs16(Text17, { color: "cyan", children: [
2863
+ "Loading ",
2864
+ label,
2865
+ "\u2026"
2866
+ ] }) });
2867
+ }
2833
2868
  var RECORDINGS_PAGE_SIZE, RECORDINGS_PREFETCH_REMAINING;
2834
2869
  var init_AppShell = __esm({
2835
2870
  "src/tui/AppShell.tsx"() {
@@ -17895,6 +17930,9 @@ var jobDataSchema = external_exports.object({
17895
17930
  model: external_exports.string().optional(),
17896
17931
  language: external_exports.string().nullable().optional(),
17897
17932
  progressPercent: external_exports.number().min(0).max(100).nullable().optional(),
17933
+ claimExpiresAt: external_exports.number().int().nullable().optional(),
17934
+ lastHeartbeatAt: external_exports.number().int().nullable().optional(),
17935
+ heartbeatPhase: external_exports.string().nullable().optional(),
17898
17936
  processedDurationMs: external_exports.number().int().nonnegative().nullable().optional(),
17899
17937
  recording: external_exports.object({
17900
17938
  title: external_exports.string().nullable().optional(),
@@ -17921,6 +17959,8 @@ var jobListItemSchema = external_exports.object({
17921
17959
  enqueuedAt: external_exports.number().int().nullable().optional(),
17922
17960
  startedAt: external_exports.number().int().nullable().optional(),
17923
17961
  finishedAt: external_exports.number().int().nullable().optional(),
17962
+ claimExpiresAt: external_exports.number().int().nullable().optional(),
17963
+ lastHeartbeatAt: external_exports.number().int().nullable().optional(),
17924
17964
  processedDurationMs: external_exports.number().int().nonnegative().nullable().optional(),
17925
17965
  heartbeatPhase: external_exports.string().nullable().optional(),
17926
17966
  recording: external_exports.object({
@@ -19560,6 +19600,9 @@ var RecappiApiClient = class {
19560
19600
  ...typeof parsed.model === "string" ? { model: parsed.model } : {},
19561
19601
  ...typeof parsed.language === "string" || parsed.language === null ? { language: parsed.language } : {},
19562
19602
  ...progressPercent !== void 0 ? { progressPercent } : {},
19603
+ ...typeof parsed.claimExpiresAt === "number" || parsed.claimExpiresAt === null ? { claimExpiresAt: parsed.claimExpiresAt } : {},
19604
+ ...typeof parsed.lastHeartbeatAt === "number" || parsed.lastHeartbeatAt === null ? { lastHeartbeatAt: parsed.lastHeartbeatAt } : {},
19605
+ ...typeof parsed.heartbeatPhase === "string" || parsed.heartbeatPhase === null ? { heartbeatPhase: parsed.heartbeatPhase } : {},
19563
19606
  ...processedDurationMs !== void 0 ? { processedDurationMs } : {},
19564
19607
  ...recording ? {
19565
19608
  recording: {
@@ -19824,6 +19867,8 @@ function mapJobListItem(row) {
19824
19867
  ...typeof row.enqueuedAt === "number" || row.enqueuedAt === null ? { enqueuedAt: row.enqueuedAt } : {},
19825
19868
  ...typeof row.startedAt === "number" || row.startedAt === null ? { startedAt: row.startedAt } : {},
19826
19869
  ...typeof row.finishedAt === "number" || row.finishedAt === null ? { finishedAt: row.finishedAt } : {},
19870
+ ...typeof row.claimExpiresAt === "number" || row.claimExpiresAt === null ? { claimExpiresAt: row.claimExpiresAt } : {},
19871
+ ...typeof row.lastHeartbeatAt === "number" || row.lastHeartbeatAt === null ? { lastHeartbeatAt: row.lastHeartbeatAt } : {},
19827
19872
  ...typeof row.processedDurationMs === "number" || row.processedDurationMs === null ? { processedDurationMs: row.processedDurationMs } : {},
19828
19873
  ...typeof row.heartbeatPhase === "string" || row.heartbeatPhase === null ? { heartbeatPhase: row.heartbeatPhase } : {},
19829
19874
  recording: {