DaemonControl.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import { useApi, postApi } from "../hooks/use-api";
  2. import { useEffect, useState } from "react";
  3. import type { Theme } from "../hooks/use-theme";
  4. import type { TFunction } from "../hooks/use-i18n";
  5. import { useColors } from "../hooks/use-colors";
  6. import type { SSEMessage } from "../hooks/use-sse";
  7. import { shouldRefetchDaemonStatus } from "../hooks/use-book-activity";
  8. interface Nav {
  9. toDashboard: () => void;
  10. }
  11. export function DaemonControl({ nav, theme, t, sse }: { nav: Nav; theme: Theme; t: TFunction; sse: { messages: ReadonlyArray<SSEMessage> } }) {
  12. const c = useColors(theme);
  13. const { data, refetch } = useApi<{ running: boolean }>("/daemon");
  14. const [loading, setLoading] = useState(false);
  15. useEffect(() => {
  16. const recent = sse.messages.at(-1);
  17. if (!shouldRefetchDaemonStatus(recent)) return;
  18. void refetch();
  19. }, [refetch, sse.messages]);
  20. const daemonEvents = sse.messages
  21. .filter((m) => m.event.startsWith("daemon:") || m.event === "log")
  22. .slice(-20);
  23. const handleStart = async () => {
  24. setLoading(true);
  25. try {
  26. await postApi("/daemon/start");
  27. refetch();
  28. } catch (e) {
  29. alert(e instanceof Error ? e.message : "Failed");
  30. } finally {
  31. setLoading(false);
  32. }
  33. };
  34. const handleStop = async () => {
  35. setLoading(true);
  36. try {
  37. await postApi("/daemon/stop");
  38. refetch();
  39. } catch (e) {
  40. alert(e instanceof Error ? e.message : "Failed");
  41. } finally {
  42. setLoading(false);
  43. }
  44. };
  45. const isRunning = data?.running ?? false;
  46. return (
  47. <div className="space-y-8">
  48. <div className="flex items-center gap-2 text-sm text-muted-foreground">
  49. <button onClick={nav.toDashboard} className={c.link}>{t("bread.home")}</button>
  50. <span className="text-border">/</span>
  51. <span className="text-foreground">{t("nav.daemon")}</span>
  52. </div>
  53. <div className="flex items-baseline justify-between">
  54. <h1 className="font-serif text-3xl">{t("daemon.title")}</h1>
  55. <div className="flex items-center gap-3">
  56. <span className={`text-sm uppercase tracking-wide font-medium ${isRunning ? "text-emerald-500" : "text-muted-foreground"}`}>
  57. {isRunning ? t("daemon.running") : t("daemon.stopped")}
  58. </span>
  59. {isRunning ? (
  60. <button
  61. onClick={handleStop}
  62. disabled={loading}
  63. className={`px-4 py-2.5 text-sm rounded-md ${c.btnDanger} disabled:opacity-50`}
  64. >
  65. {loading ? t("daemon.stopping") : t("daemon.stop")}
  66. </button>
  67. ) : (
  68. <button
  69. onClick={handleStart}
  70. disabled={loading}
  71. className={`px-4 py-2.5 text-sm rounded-md ${c.btnPrimary} disabled:opacity-50`}
  72. >
  73. {loading ? t("daemon.starting") : t("daemon.start")}
  74. </button>
  75. )}
  76. </div>
  77. </div>
  78. {/* Daemon event log */}
  79. <div className={`border ${c.cardStatic} rounded-lg`}>
  80. <div className="px-5 py-3.5 border-b border-border">
  81. <span className="text-sm uppercase tracking-wide text-muted-foreground font-medium">Event Log</span>
  82. </div>
  83. <div className="p-4 max-h-[500px] overflow-y-auto">
  84. {daemonEvents.length > 0 ? (
  85. <div className="space-y-1.5 font-mono text-sm">
  86. {daemonEvents.map((msg, i) => {
  87. const d = msg.data as Record<string, unknown>;
  88. return (
  89. <div key={i} className="leading-relaxed text-muted-foreground">
  90. <span className="text-primary/50">{msg.event}</span>
  91. <span className="text-border mx-1.5">›</span>
  92. <span>{String(d.message ?? d.bookId ?? JSON.stringify(d))}</span>
  93. </div>
  94. );
  95. })}
  96. </div>
  97. ) : (
  98. <div className="text-muted-foreground text-sm italic py-8 text-center">
  99. {isRunning ? t("daemon.waitingEvents") : t("daemon.startHint")}
  100. </div>
  101. )}
  102. </div>
  103. </div>
  104. </div>
  105. );
  106. }