@chrysb/alphaclaw 0.2.3 → 0.3.0

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.
Files changed (63) hide show
  1. package/bin/alphaclaw.js +79 -0
  2. package/lib/public/css/shell.css +57 -2
  3. package/lib/public/css/theme.css +184 -0
  4. package/lib/public/js/app.js +330 -89
  5. package/lib/public/js/components/action-button.js +92 -0
  6. package/lib/public/js/components/channels.js +16 -7
  7. package/lib/public/js/components/confirm-dialog.js +25 -19
  8. package/lib/public/js/components/credentials-modal.js +32 -23
  9. package/lib/public/js/components/device-pairings.js +15 -2
  10. package/lib/public/js/components/envars.js +22 -65
  11. package/lib/public/js/components/features.js +1 -1
  12. package/lib/public/js/components/gateway.js +139 -32
  13. package/lib/public/js/components/global-restart-banner.js +31 -0
  14. package/lib/public/js/components/google.js +9 -9
  15. package/lib/public/js/components/icons.js +19 -0
  16. package/lib/public/js/components/info-tooltip.js +18 -0
  17. package/lib/public/js/components/loading-spinner.js +32 -0
  18. package/lib/public/js/components/modal-shell.js +42 -0
  19. package/lib/public/js/components/models.js +34 -29
  20. package/lib/public/js/components/onboarding/welcome-form-step.js +45 -32
  21. package/lib/public/js/components/onboarding/welcome-pairing-step.js +2 -2
  22. package/lib/public/js/components/onboarding/welcome-setup-step.js +7 -24
  23. package/lib/public/js/components/page-header.js +13 -0
  24. package/lib/public/js/components/pairings.js +15 -2
  25. package/lib/public/js/components/providers.js +216 -142
  26. package/lib/public/js/components/scope-picker.js +1 -1
  27. package/lib/public/js/components/secret-input.js +1 -0
  28. package/lib/public/js/components/telegram-workspace.js +37 -49
  29. package/lib/public/js/components/toast.js +34 -5
  30. package/lib/public/js/components/toggle-switch.js +25 -0
  31. package/lib/public/js/components/update-action-button.js +13 -53
  32. package/lib/public/js/components/watchdog-tab.js +312 -0
  33. package/lib/public/js/components/webhooks.js +981 -0
  34. package/lib/public/js/components/welcome.js +2 -1
  35. package/lib/public/js/lib/api.js +102 -1
  36. package/lib/public/js/lib/model-config.js +0 -5
  37. package/lib/server/alphaclaw-version.js +5 -3
  38. package/lib/server/constants.js +33 -0
  39. package/lib/server/discord-api.js +48 -0
  40. package/lib/server/gateway.js +64 -4
  41. package/lib/server/log-writer.js +102 -0
  42. package/lib/server/onboarding/github.js +21 -1
  43. package/lib/server/openclaw-version.js +2 -6
  44. package/lib/server/restart-required-state.js +86 -0
  45. package/lib/server/routes/auth.js +9 -4
  46. package/lib/server/routes/proxy.js +12 -14
  47. package/lib/server/routes/system.js +61 -15
  48. package/lib/server/routes/telegram.js +17 -48
  49. package/lib/server/routes/watchdog.js +68 -0
  50. package/lib/server/routes/webhooks.js +214 -0
  51. package/lib/server/telegram-api.js +11 -0
  52. package/lib/server/watchdog-db.js +148 -0
  53. package/lib/server/watchdog-notify.js +93 -0
  54. package/lib/server/watchdog.js +585 -0
  55. package/lib/server/webhook-middleware.js +195 -0
  56. package/lib/server/webhooks-db.js +265 -0
  57. package/lib/server/webhooks.js +238 -0
  58. package/lib/server.js +119 -4
  59. package/lib/setup/core-prompts/AGENTS.md +84 -0
  60. package/lib/setup/core-prompts/TOOLS.md +13 -0
  61. package/lib/setup/core-prompts/UI-DRY-OPPORTUNITIES.md +50 -0
  62. package/lib/setup/gitignore +2 -0
  63. package/package.json +2 -1
@@ -1,6 +1,7 @@
1
1
  import { h, render } from "https://esm.sh/preact";
2
2
  import { useState, useEffect, useRef, useCallback } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
+ import { Router, Route, Switch, useLocation } from "https://esm.sh/wouter-preact";
4
5
  import {
5
6
  fetchStatus,
6
7
  fetchPairings,
@@ -16,6 +17,10 @@ import {
16
17
  updateSyncCron,
17
18
  fetchAlphaclawVersion,
18
19
  updateAlphaclaw,
20
+ fetchRestartStatus,
21
+ restartGateway,
22
+ fetchWatchdogStatus,
23
+ triggerWatchdogRepair,
19
24
  } from "./lib/api.js";
20
25
  import { usePolling } from "./hooks/usePolling.js";
21
26
  import { Gateway } from "./components/gateway.js";
@@ -27,21 +32,69 @@ import { Features } from "./components/features.js";
27
32
  import { Providers } from "./components/providers.js";
28
33
  import { Welcome } from "./components/welcome.js";
29
34
  import { Envars } from "./components/envars.js";
35
+ import { Webhooks } from "./components/webhooks.js";
30
36
  import { ToastContainer, showToast } from "./components/toast.js";
31
37
  import { TelegramWorkspace } from "./components/telegram-workspace.js";
32
38
  import { ChevronDownIcon } from "./components/icons.js";
33
39
  import { UpdateActionButton } from "./components/update-action-button.js";
40
+ import { GlobalRestartBanner } from "./components/global-restart-banner.js";
41
+ import { LoadingSpinner } from "./components/loading-spinner.js";
42
+ import { WatchdogTab } from "./components/watchdog-tab.js";
34
43
  const html = htm.bind(h);
35
- const kUiTabs = ["general", "providers", "envars"];
36
- const kSubScreens = ["telegram"];
37
44
  const kDefaultUiTab = "general";
38
45
 
39
- const GeneralTab = ({ onSwitchTab, onNavigate, isActive }) => {
46
+ const getHashPath = () => {
47
+ const hash = window.location.hash.replace(/^#/, "");
48
+ if (!hash) return `/${kDefaultUiTab}`;
49
+ return hash.startsWith("/") ? hash : `/${hash}`;
50
+ };
51
+
52
+ const useHashLocation = () => {
53
+ const [location, setLocationState] = useState(getHashPath);
54
+
55
+ useEffect(() => {
56
+ const onHashChange = () => setLocationState(getHashPath());
57
+ window.addEventListener("hashchange", onHashChange);
58
+ return () => window.removeEventListener("hashchange", onHashChange);
59
+ }, []);
60
+
61
+ const setLocation = useCallback((to) => {
62
+ const normalized = to.startsWith("/") ? to : `/${to}`;
63
+ const nextHash = `#${normalized}`;
64
+ if (window.location.hash !== nextHash) {
65
+ window.location.hash = normalized;
66
+ return;
67
+ }
68
+ setLocationState(normalized);
69
+ }, []);
70
+
71
+ return [location, setLocation];
72
+ };
73
+
74
+ const RouteRedirect = ({ to }) => {
75
+ const [, setLocation] = useLocation();
76
+ useEffect(() => {
77
+ setLocation(to);
78
+ }, [to, setLocation]);
79
+ return null;
80
+ };
81
+
82
+ const GeneralTab = ({
83
+ statusData = null,
84
+ watchdogData = null,
85
+ onRefreshStatuses = () => {},
86
+ onSwitchTab,
87
+ onNavigate,
88
+ isActive,
89
+ restartingGateway,
90
+ onRestartGateway,
91
+ restartSignal = 0,
92
+ }) => {
40
93
  const [googleKey, setGoogleKey] = useState(0);
41
94
  const [dashboardLoading, setDashboardLoading] = useState(false);
42
-
43
- const statusPoll = usePolling(fetchStatus, 15000);
44
- const status = statusPoll.data;
95
+ const [repairingWatchdog, setRepairingWatchdog] = useState(false);
96
+ const status = statusData;
97
+ const watchdogStatus = watchdogData;
45
98
  const gatewayStatus = status?.gateway ?? null;
46
99
  const channels = status?.channels ?? null;
47
100
  const repo = status?.repo || null;
@@ -67,18 +120,10 @@ const GeneralTab = ({ onSwitchTab, onNavigate, isActive }) => {
67
120
  );
68
121
  const pending = pairingsPoll.data || [];
69
122
 
70
- // Poll status faster when gateway isn't running yet
71
- useEffect(() => {
72
- if (!gatewayStatus || gatewayStatus !== "running") {
73
- const id = setInterval(statusPoll.refresh, 3000);
74
- return () => clearInterval(id);
75
- }
76
- }, [gatewayStatus, statusPoll.refresh]);
77
-
78
123
  const refreshAfterAction = () => {
79
124
  setTimeout(pairingsPoll.refresh, 500);
80
125
  setTimeout(pairingsPoll.refresh, 2000);
81
- setTimeout(statusPoll.refresh, 3000);
126
+ setTimeout(onRefreshStatuses, 3000);
82
127
  };
83
128
 
84
129
  const handleApprove = async (id, channel) => {
@@ -114,7 +159,7 @@ const GeneralTab = ({ onSwitchTab, onNavigate, isActive }) => {
114
159
  };
115
160
 
116
161
  const fullRefresh = () => {
117
- statusPoll.refresh();
162
+ onRefreshStatuses();
118
163
  pairingsPoll.refresh();
119
164
  devicePoll.refresh();
120
165
  setGoogleKey((k) => k + 1);
@@ -125,6 +170,33 @@ const GeneralTab = ({ onSwitchTab, onNavigate, isActive }) => {
125
170
  fullRefresh();
126
171
  }, [isActive]);
127
172
 
173
+ useEffect(() => {
174
+ if (!restartSignal || !isActive) return;
175
+ onRefreshStatuses();
176
+ pairingsPoll.refresh();
177
+ devicePoll.refresh();
178
+ const t1 = setTimeout(() => {
179
+ onRefreshStatuses();
180
+ pairingsPoll.refresh();
181
+ devicePoll.refresh();
182
+ }, 1200);
183
+ const t2 = setTimeout(() => {
184
+ onRefreshStatuses();
185
+ pairingsPoll.refresh();
186
+ devicePoll.refresh();
187
+ }, 3500);
188
+ return () => {
189
+ clearTimeout(t1);
190
+ clearTimeout(t2);
191
+ };
192
+ }, [
193
+ restartSignal,
194
+ isActive,
195
+ onRefreshStatuses,
196
+ pairingsPoll.refresh,
197
+ devicePoll.refresh,
198
+ ]);
199
+
128
200
  useEffect(() => {
129
201
  if (!syncCron) return;
130
202
  setSyncCronEnabled(syncCron.enabled !== false);
@@ -147,7 +219,7 @@ const GeneralTab = ({ onSwitchTab, onNavigate, isActive }) => {
147
219
  if (!data.ok)
148
220
  throw new Error(data.error || "Could not save sync settings");
149
221
  showToast("Sync schedule updated", "success");
150
- statusPoll.refresh();
222
+ onRefreshStatuses();
151
223
  } catch (err) {
152
224
  showToast(err.message || "Could not save sync settings", "error");
153
225
  }
@@ -155,10 +227,35 @@ const GeneralTab = ({ onSwitchTab, onNavigate, isActive }) => {
155
227
  };
156
228
 
157
229
  const syncCronStatusText = syncCronEnabled ? "Enabled" : "Disabled";
230
+ const handleWatchdogRepair = async () => {
231
+ if (repairingWatchdog) return;
232
+ setRepairingWatchdog(true);
233
+ try {
234
+ const data = await triggerWatchdogRepair();
235
+ if (!data.ok) throw new Error(data.error || "Repair failed");
236
+ showToast("Repair triggered", "success");
237
+ setTimeout(() => {
238
+ onRefreshStatuses();
239
+ }, 800);
240
+ } catch (err) {
241
+ showToast(err.message || "Could not run repair", "error");
242
+ } finally {
243
+ setRepairingWatchdog(false);
244
+ }
245
+ };
158
246
 
159
247
  return html`
160
248
  <div class="space-y-4">
161
- <${Gateway} status=${gatewayStatus} openclawVersion=${openclawVersion} />
249
+ <${Gateway}
250
+ status=${gatewayStatus}
251
+ openclawVersion=${openclawVersion}
252
+ restarting=${restartingGateway}
253
+ onRestart=${onRestartGateway}
254
+ watchdogStatus=${watchdogStatus}
255
+ onOpenWatchdog=${() => onSwitchTab("watchdog")}
256
+ onRepair=${handleWatchdogRepair}
257
+ repairing=${repairingWatchdog}
258
+ />
162
259
  <${Channels} channels=${channels} onSwitchTab=${onSwitchTab} onNavigate=${onNavigate} />
163
260
  <${Pairings}
164
261
  pending=${pending}
@@ -279,17 +376,9 @@ const GeneralTab = ({ onSwitchTab, onNavigate, isActive }) => {
279
376
  `;
280
377
  };
281
378
 
282
- function App() {
379
+ const App = () => {
283
380
  const [onboarded, setOnboarded] = useState(null);
284
- const [tab, setTab] = useState(() => {
285
- const hash = window.location.hash.replace("#", "");
286
- if (kSubScreens.includes(hash)) return kDefaultUiTab;
287
- return kUiTabs.includes(hash) ? hash : kDefaultUiTab;
288
- });
289
- const [subScreen, setSubScreen] = useState(() => {
290
- const hash = window.location.hash.replace("#", "");
291
- return kSubScreens.includes(hash) ? hash : null;
292
- });
381
+ const [location, setLocation] = useLocation();
293
382
  const [acVersion, setAcVersion] = useState(null);
294
383
  const [acLatest, setAcLatest] = useState(null);
295
384
  const [acHasUpdate, setAcHasUpdate] = useState(false);
@@ -299,7 +388,23 @@ function App() {
299
388
  const [menuOpen, setMenuOpen] = useState(false);
300
389
  const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
301
390
  const [mobileTopbarScrolled, setMobileTopbarScrolled] = useState(false);
391
+ const [restartRequired, setRestartRequired] = useState(false);
392
+ const [restartingGateway, setRestartingGateway] = useState(false);
393
+ const [gatewayRestartSignal, setGatewayRestartSignal] = useState(0);
394
+ const [statusPollCadenceMs, setStatusPollCadenceMs] = useState(15000);
302
395
  const menuRef = useRef(null);
396
+ const sharedStatusPoll = usePolling(fetchStatus, statusPollCadenceMs, {
397
+ enabled: onboarded === true,
398
+ });
399
+ const sharedWatchdogPoll = usePolling(fetchWatchdogStatus, statusPollCadenceMs, {
400
+ enabled: onboarded === true,
401
+ });
402
+ const sharedStatus = sharedStatusPoll.data || null;
403
+ const sharedWatchdogStatus = sharedWatchdogPoll.data?.status || null;
404
+ const refreshSharedStatuses = useCallback(() => {
405
+ sharedStatusPoll.refresh();
406
+ sharedWatchdogPoll.refresh();
407
+ }, [sharedStatusPoll.refresh, sharedWatchdogPoll.refresh]);
303
408
 
304
409
  const closeMenu = useCallback((e) => {
305
410
  if (menuRef.current && !menuRef.current.contains(e.target)) {
@@ -323,10 +428,6 @@ function App() {
323
428
  .catch(() => {});
324
429
  }, []);
325
430
 
326
- useEffect(() => {
327
- history.replaceState(null, "", `#${subScreen || tab}`);
328
- }, [tab, subScreen]);
329
-
330
431
  useEffect(() => {
331
432
  if (!mobileSidebarOpen) return;
332
433
  const previousOverflow = document.body.style.overflow;
@@ -356,6 +457,72 @@ function App() {
356
457
  };
357
458
  }, [onboarded]);
358
459
 
460
+ const refreshRestartStatus = useCallback(async () => {
461
+ if (!onboarded) return;
462
+ try {
463
+ const data = await fetchRestartStatus();
464
+ setRestartRequired(!!data.restartRequired);
465
+ setRestartingGateway(!!data.restartInProgress);
466
+ } catch {}
467
+ }, [onboarded]);
468
+
469
+ useEffect(() => {
470
+ if (!onboarded) return;
471
+ refreshRestartStatus();
472
+ }, [onboarded, refreshRestartStatus]);
473
+
474
+ useEffect(() => {
475
+ if (onboarded !== true) return;
476
+ const inStatusView =
477
+ location.startsWith("/general") || location.startsWith("/watchdog");
478
+ const gatewayStatus = sharedStatus?.gateway ?? null;
479
+ const watchdogHealth = String(sharedWatchdogStatus?.health || "").toLowerCase();
480
+ const watchdogLifecycle = String(sharedWatchdogStatus?.lifecycle || "").toLowerCase();
481
+ const shouldFastPollWatchdog =
482
+ watchdogHealth === "unknown" ||
483
+ watchdogLifecycle === "restarting" ||
484
+ watchdogLifecycle === "stopped" ||
485
+ !!sharedWatchdogStatus?.operationInProgress;
486
+ const shouldFastPollGateway = !gatewayStatus || gatewayStatus !== "running";
487
+ const nextCadenceMs =
488
+ inStatusView && (shouldFastPollWatchdog || shouldFastPollGateway) ? 2000 : 15000;
489
+ setStatusPollCadenceMs((currentCadenceMs) =>
490
+ currentCadenceMs === nextCadenceMs ? currentCadenceMs : nextCadenceMs,
491
+ );
492
+ }, [
493
+ onboarded,
494
+ location,
495
+ sharedStatus?.gateway,
496
+ sharedWatchdogStatus?.health,
497
+ sharedWatchdogStatus?.lifecycle,
498
+ sharedWatchdogStatus?.operationInProgress,
499
+ ]);
500
+
501
+ useEffect(() => {
502
+ if (!onboarded || (!restartRequired && !restartingGateway)) return;
503
+ const id = setInterval(refreshRestartStatus, 2000);
504
+ return () => clearInterval(id);
505
+ }, [onboarded, restartRequired, restartingGateway, refreshRestartStatus]);
506
+
507
+ const handleGatewayRestart = useCallback(async () => {
508
+ if (restartingGateway) return;
509
+ setRestartingGateway(true);
510
+ try {
511
+ const data = await restartGateway();
512
+ if (!data?.ok) throw new Error(data?.error || "Gateway restart failed");
513
+ setRestartRequired(!!data.restartRequired);
514
+ setGatewayRestartSignal(Date.now());
515
+ refreshSharedStatuses();
516
+ showToast("Gateway restarted", "success");
517
+ setTimeout(refreshRestartStatus, 800);
518
+ } catch (err) {
519
+ showToast(err.message || "Restart failed", "error");
520
+ setTimeout(refreshRestartStatus, 800);
521
+ } finally {
522
+ setRestartingGateway(false);
523
+ }
524
+ }, [restartingGateway, refreshRestartStatus, refreshSharedStatuses]);
525
+
359
526
  const handleAcUpdate = async () => {
360
527
  if (acUpdating) return;
361
528
  setAcUpdating(true);
@@ -380,26 +547,10 @@ function App() {
380
547
  class="min-h-screen flex items-center justify-center"
381
548
  style="position: relative; z-index: 1"
382
549
  >
383
- <svg
384
- class="animate-spin h-6 w-6"
550
+ <${LoadingSpinner}
551
+ className="h-6 w-6"
385
552
  style="color: var(--text-muted)"
386
- viewBox="0 0 24 24"
387
- fill="none"
388
- >
389
- <circle
390
- class="opacity-25"
391
- cx="12"
392
- cy="12"
393
- r="10"
394
- stroke="currentColor"
395
- stroke-width="4"
396
- />
397
- <path
398
- class="opacity-75"
399
- fill="currentColor"
400
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
401
- />
402
- </svg>
553
+ />
403
554
  </div>
404
555
  <${ToastContainer} />
405
556
  `;
@@ -418,11 +569,11 @@ function App() {
418
569
  }
419
570
 
420
571
  const navigateToSubScreen = (screen) => {
421
- setSubScreen(screen);
572
+ setLocation(`/${screen}`);
422
573
  setMobileSidebarOpen(false);
423
574
  };
424
575
  const exitSubScreen = () => {
425
- setSubScreen(null);
576
+ setLocation(`/${kDefaultUiTab}`);
426
577
  setMobileSidebarOpen(false);
427
578
  };
428
579
  const handleAppContentScroll = (e) => {
@@ -432,14 +583,55 @@ function App() {
432
583
  );
433
584
  };
434
585
 
435
- const kNavItems = [
436
- { id: "general", label: "General" },
437
- { id: "providers", label: "Providers" },
438
- { id: "envars", label: "Envars" },
586
+ const kNavSections = [
587
+ {
588
+ label: "Status",
589
+ items: [
590
+ { id: "general", label: "General" },
591
+ { id: "watchdog", label: "Watchdog" },
592
+ ],
593
+ },
594
+ {
595
+ label: "Config",
596
+ items: [
597
+ { id: "providers", label: "Providers" },
598
+ { id: "envars", label: "Envars" },
599
+ { id: "webhooks", label: "Webhooks" },
600
+ ],
601
+ },
439
602
  ];
440
603
 
604
+ const selectedNavId =
605
+ location === "/telegram"
606
+ ? ""
607
+ : location.startsWith("/providers")
608
+ ? "providers"
609
+ : location.startsWith("/watchdog")
610
+ ? "watchdog"
611
+ : location.startsWith("/envars")
612
+ ? "envars"
613
+ : location.startsWith("/webhooks")
614
+ ? "webhooks"
615
+ : "general";
616
+
617
+ const renderWebhooks = (hookName = "") => html`
618
+ <div class="pt-4">
619
+ <${Webhooks}
620
+ selectedHookName=${hookName}
621
+ onSelectHook=${(name) => setLocation(`/webhooks/${encodeURIComponent(name)}`)}
622
+ onBackToList=${() => setLocation("/webhooks")}
623
+ onRestartRequired=${setRestartRequired}
624
+ />
625
+ </div>
626
+ `;
627
+
441
628
  return html`
442
629
  <div class="app-shell">
630
+ <${GlobalRestartBanner}
631
+ visible=${restartRequired}
632
+ restarting=${restartingGateway}
633
+ onRestart=${handleGatewayRestart}
634
+ />
443
635
  <div class=${`app-sidebar ${mobileSidebarOpen ? "mobile-open" : ""}`}>
444
636
  <div class="sidebar-brand">
445
637
  <img src="./img/logo.svg" alt="" width="20" height="20" />
@@ -465,6 +657,10 @@ function App() {
465
657
  e.preventDefault();
466
658
  setMenuOpen(false);
467
659
  await logout();
660
+ try {
661
+ window.localStorage.clear();
662
+ window.sessionStorage.clear();
663
+ } catch {}
468
664
  window.location.href = "/login.html";
469
665
  }}
470
666
  >Log out</a>
@@ -473,23 +669,26 @@ function App() {
473
669
  </div>
474
670
  `}
475
671
  </div>
476
- <div class="sidebar-label">Setup</div>
477
- <nav class="sidebar-nav">
478
- ${kNavItems.map(
479
- (item) => html`
480
- <a
481
- class=${tab === item.id && !subScreen ? "active" : ""}
482
- onclick=${() => {
483
- setSubScreen(null);
484
- setTab(item.id);
485
- setMobileSidebarOpen(false);
486
- }}
487
- >
488
- ${item.label}
489
- </a>
490
- `,
491
- )}
492
- </nav>
672
+ ${kNavSections.map(
673
+ (section) => html`
674
+ <div class="sidebar-label">${section.label}</div>
675
+ <nav class="sidebar-nav">
676
+ ${section.items.map(
677
+ (item) => html`
678
+ <a
679
+ class=${selectedNavId === item.id ? "active" : ""}
680
+ onclick=${() => {
681
+ setLocation(`/${item.id}`);
682
+ setMobileSidebarOpen(false);
683
+ }}
684
+ >
685
+ ${item.label}
686
+ </a>
687
+ `,
688
+ )}
689
+ </nav>
690
+ `,
691
+ )}
493
692
  <div class="sidebar-footer">
494
693
  ${acHasUpdate && acLatest && !acDismissed
495
694
  ? html`
@@ -530,28 +729,64 @@ function App() {
530
729
  </span>
531
730
  </div>
532
731
  <div class="max-w-2xl w-full mx-auto">
533
- ${subScreen === "telegram"
534
- ? html`
732
+ <${Switch}>
733
+ <${Route} path="/telegram">
535
734
  <div class="pt-4">
536
735
  <${TelegramWorkspace} onBack=${exitSubScreen} />
537
736
  </div>
538
- `
539
- : html`
540
- <div class="pt-4" style=${{ display: tab === "general" ? "" : "none" }}>
737
+ </${Route}>
738
+ <${Route} path="/general">
739
+ <div class="pt-4">
541
740
  <${GeneralTab}
542
- onSwitchTab=${setTab}
741
+ statusData=${sharedStatus}
742
+ watchdogData=${sharedWatchdogStatus}
743
+ onRefreshStatuses=${refreshSharedStatuses}
744
+ onSwitchTab=${(nextTab) => setLocation(`/${nextTab}`)}
543
745
  onNavigate=${navigateToSubScreen}
544
- isActive=${tab === "general" && !subScreen}
746
+ isActive=${location === "/general"}
747
+ restartingGateway=${restartingGateway}
748
+ onRestartGateway=${handleGatewayRestart}
749
+ restartSignal=${gatewayRestartSignal}
545
750
  />
546
751
  </div>
547
- <div class="pt-4" style=${{ display: tab === "providers" ? "" : "none" }}>
548
- <${Providers} />
752
+ </${Route}>
753
+ <${Route} path="/providers">
754
+ <div class="pt-4">
755
+ <${Providers} onRestartRequired=${setRestartRequired} />
549
756
  </div>
550
- <div class="pt-4" style=${{ display: tab === "envars" ? "" : "none" }}>
551
- <${Envars} />
757
+ </${Route}>
758
+ <${Route} path="/watchdog">
759
+ <div class="pt-4">
760
+ <${WatchdogTab}
761
+ gatewayStatus=${sharedStatus?.gateway || null}
762
+ openclawVersion=${sharedStatus?.openclawVersion || null}
763
+ watchdogStatus=${sharedWatchdogStatus}
764
+ onRefreshStatuses=${refreshSharedStatuses}
765
+ restartingGateway=${restartingGateway}
766
+ onRestartGateway=${handleGatewayRestart}
767
+ restartSignal=${gatewayRestartSignal}
768
+ />
552
769
  </div>
553
- `}
770
+ </${Route}>
771
+ <${Route} path="/envars">
772
+ <div class="pt-4">
773
+ <${Envars} onRestartRequired=${setRestartRequired} />
774
+ </div>
775
+ </${Route}>
776
+ <${Route} path="/webhooks/:hookName">
777
+ ${(params) => renderWebhooks(decodeURIComponent(params.hookName || ""))}
778
+ </${Route}>
779
+ <${Route} path="/webhooks">
780
+ ${() => renderWebhooks("")}
781
+ </${Route}>
782
+ <${Route}>
783
+ <${RouteRedirect} to="/general" />
784
+ </${Route}>
785
+ </${Switch}>
554
786
  </div>
787
+ <${ToastContainer}
788
+ className="fixed bottom-10 right-4 z-50 space-y-2 pointer-events-none"
789
+ />
555
790
  </div>
556
791
 
557
792
  <div class="app-statusbar">
@@ -579,8 +814,14 @@ function App() {
579
814
  </div>
580
815
  </div>
581
816
  </div>
582
- <${ToastContainer} />
583
817
  `;
584
- }
818
+ };
585
819
 
586
- render(html`<${App} />`, document.getElementById("app"));
820
+ render(
821
+ html`
822
+ <${Router} hook=${useHashLocation}>
823
+ <${App} />
824
+ </${Router}>
825
+ `,
826
+ document.getElementById("app"),
827
+ );
@@ -0,0 +1,92 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { LoadingSpinner } from "./loading-spinner.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ const kStaticToneClassByTone = {
8
+ primary: "ac-btn-cyan",
9
+ secondary: "ac-btn-secondary",
10
+ success: "ac-btn-green",
11
+ danger: "ac-btn-danger",
12
+ ghost: "ac-btn-ghost",
13
+ };
14
+
15
+ const getToneClass = (tone, isInteractive) => {
16
+ if (tone === "neutral") {
17
+ return isInteractive
18
+ ? "border border-border text-gray-500 hover:text-gray-300 hover:border-gray-500"
19
+ : "border border-border text-gray-500";
20
+ }
21
+ if (tone === "warning") {
22
+ return isInteractive
23
+ ? "border border-yellow-500/35 text-yellow-400 bg-yellow-500/10 hover:border-yellow-400/60 hover:text-yellow-300 hover:bg-yellow-500/15"
24
+ : "border border-yellow-500/35 text-yellow-400 bg-yellow-500/10";
25
+ }
26
+ return kStaticToneClassByTone[tone] || kStaticToneClassByTone.primary;
27
+ };
28
+
29
+ const kSizeClassBySize = {
30
+ sm: "h-7 text-xs leading-none px-2.5 py-1 rounded-lg",
31
+ md: "h-9 text-sm font-medium leading-none px-4 rounded-xl",
32
+ lg: "h-10 text-sm font-medium leading-none px-5 rounded-lg",
33
+ };
34
+
35
+ export const ActionButton = ({
36
+ onClick,
37
+ type = "button",
38
+ disabled = false,
39
+ loading = false,
40
+ tone = "primary",
41
+ size = "sm",
42
+ idleLabel = "Action",
43
+ loadingLabel = "Working...",
44
+ loadingMode = "replace",
45
+ className = "",
46
+ }) => {
47
+ const isDisabled = disabled || loading;
48
+ const isInteractive = !isDisabled;
49
+ const toneClass = getToneClass(tone, isInteractive);
50
+ const sizeClass = kSizeClassBySize[size] || kSizeClassBySize.sm;
51
+ const loadingClass = loading
52
+ ? `cursor-not-allowed ${
53
+ tone === "warning"
54
+ ? "opacity-90 animate-pulse shadow-[0_0_0_1px_rgba(234,179,8,0.22),0_0_18px_rgba(234,179,8,0.12)]"
55
+ : "opacity-80"
56
+ }`
57
+ : "";
58
+ const spinnerSizeClass = size === "md" || size === "lg" ? "h-4 w-4" : "h-3 w-3";
59
+ const isInlineLoading = loadingMode === "inline";
60
+ const currentLabel = loading && !isInlineLoading ? loadingLabel : idleLabel;
61
+
62
+ return html`
63
+ <button
64
+ type=${type}
65
+ onclick=${onClick}
66
+ disabled=${isDisabled}
67
+ class="inline-flex items-center justify-center transition-colors whitespace-nowrap ${sizeClass} ${toneClass} ${loadingClass} ${className}"
68
+ >
69
+ ${isInlineLoading
70
+ ? html`
71
+ <span class="relative inline-flex items-center justify-center leading-none">
72
+ <span class=${loading ? "invisible" : ""}>${currentLabel}</span>
73
+ ${loading
74
+ ? html`
75
+ <span class="absolute inset-0 inline-flex items-center justify-center">
76
+ <${LoadingSpinner} className=${spinnerSizeClass} />
77
+ </span>
78
+ `
79
+ : null}
80
+ </span>
81
+ `
82
+ : loading
83
+ ? html`
84
+ <span class="inline-flex items-center gap-1.5 leading-none">
85
+ <${LoadingSpinner} className=${spinnerSizeClass} />
86
+ ${currentLabel}
87
+ </span>
88
+ `
89
+ : currentLabel}
90
+ </button>
91
+ `;
92
+ };