@portel/photon 1.26.1 → 1.28.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 (50) hide show
  1. package/README.md +33 -0
  2. package/dist/auto-ui/beam/routes/api-daemon.d.ts +1 -0
  3. package/dist/auto-ui/beam/routes/api-daemon.d.ts.map +1 -1
  4. package/dist/auto-ui/beam/routes/api-daemon.js +35 -1
  5. package/dist/auto-ui/beam/routes/api-daemon.js.map +1 -1
  6. package/dist/beam-form.bundle.js +41 -1
  7. package/dist/beam-form.bundle.js.map +2 -2
  8. package/dist/beam.bundle.js +1661 -252
  9. package/dist/beam.bundle.js.map +4 -4
  10. package/dist/cli/commands/daemon.d.ts.map +1 -1
  11. package/dist/cli/commands/daemon.js +157 -0
  12. package/dist/cli/commands/daemon.js.map +1 -1
  13. package/dist/cli/commands/update.d.ts.map +1 -1
  14. package/dist/cli/commands/update.js +7 -8
  15. package/dist/cli/commands/update.js.map +1 -1
  16. package/dist/daemon/client.d.ts +1 -0
  17. package/dist/daemon/client.d.ts.map +1 -1
  18. package/dist/daemon/client.js +110 -23
  19. package/dist/daemon/client.js.map +1 -1
  20. package/dist/daemon/in-process-bridge.d.ts +29 -0
  21. package/dist/daemon/in-process-bridge.d.ts.map +1 -0
  22. package/dist/daemon/in-process-bridge.js +26 -0
  23. package/dist/daemon/in-process-bridge.js.map +1 -0
  24. package/dist/daemon/manager.d.ts +103 -1
  25. package/dist/daemon/manager.d.ts.map +1 -1
  26. package/dist/daemon/manager.js +313 -92
  27. package/dist/daemon/manager.js.map +1 -1
  28. package/dist/daemon/protocol.d.ts +1 -1
  29. package/dist/daemon/protocol.d.ts.map +1 -1
  30. package/dist/daemon/protocol.js +1 -0
  31. package/dist/daemon/protocol.js.map +1 -1
  32. package/dist/daemon/server.js +859 -38
  33. package/dist/daemon/server.js.map +1 -1
  34. package/dist/loader.d.ts.map +1 -1
  35. package/dist/loader.js +39 -13
  36. package/dist/loader.js.map +1 -1
  37. package/dist/sample-augmenter.d.ts +84 -0
  38. package/dist/sample-augmenter.d.ts.map +1 -0
  39. package/dist/sample-augmenter.js +164 -0
  40. package/dist/sample-augmenter.js.map +1 -0
  41. package/dist/shared/npm-registry.d.ts +30 -0
  42. package/dist/shared/npm-registry.d.ts.map +1 -0
  43. package/dist/shared/npm-registry.js +97 -0
  44. package/dist/shared/npm-registry.js.map +1 -0
  45. package/dist/version-notify.d.ts +5 -0
  46. package/dist/version-notify.d.ts.map +1 -1
  47. package/dist/version-notify.js +46 -23
  48. package/dist/version-notify.js.map +1 -1
  49. package/package.json +1 -1
  50. package/templates/cloudflare/worker.ts.template +94 -22
@@ -32944,6 +32944,21 @@ function formatLabel(name2) {
32944
32944
  spaced = spaced.replace(/[_-]+/g, " ");
32945
32945
  let result = spaced.split(/\s+/).map((word) => {
32946
32946
  if (!word) return "";
32947
+ const lower = word.toLowerCase();
32948
+ const mixedCase = {
32949
+ oauth: "OAuth",
32950
+ whatsapp: "WhatsApp",
32951
+ github: "GitHub",
32952
+ gitlab: "GitLab",
32953
+ bitbucket: "Bitbucket",
32954
+ macos: "macOS",
32955
+ ios: "iOS",
32956
+ ipad: "iPad",
32957
+ iphone: "iPhone",
32958
+ npm: "npm",
32959
+ npx: "npx"
32960
+ };
32961
+ if (lower in mixedCase) return mixedCase[lower];
32947
32962
  const upper = word.toUpperCase();
32948
32963
  if ([
32949
32964
  "AI",
@@ -32972,7 +32987,32 @@ function formatLabel(name2) {
32972
32987
  "OS",
32973
32988
  "DB",
32974
32989
  "IO",
32975
- "GIT"
32990
+ "GIT",
32991
+ "CPU",
32992
+ "GPU",
32993
+ "RAM",
32994
+ "RPC",
32995
+ "TCP",
32996
+ "UDP",
32997
+ "DNS",
32998
+ "JWT",
32999
+ "TLS",
33000
+ "SSL",
33001
+ "CDN",
33002
+ "SVG",
33003
+ "PNG",
33004
+ "JPG",
33005
+ "PDF",
33006
+ "YAML",
33007
+ "TOML",
33008
+ "CSV",
33009
+ "TSV",
33010
+ "UUID",
33011
+ "CRUD",
33012
+ "REST",
33013
+ "GRPC",
33014
+ "MIME",
33015
+ "PATH"
32976
33016
  ].includes(upper)) {
32977
33017
  if (upper === "IDS") return "IDs";
32978
33018
  return upper;
@@ -35738,11 +35778,15 @@ var BeamApp = class extends i4 {
35738
35778
  .hasPath=${!!this._selectedPhoton?.path}
35739
35779
  @tab-change=${(e8) => {
35740
35780
  const tab = e8.detail.tab;
35781
+ const prevTab = this._mainTab;
35741
35782
  this._mainTab = tab;
35742
35783
  if (tab === "methods") {
35743
- this._closeSecondPanel();
35744
- this._selectedMethod = null;
35745
- this._view = "list";
35784
+ const fromOverlay = prevTab === "log" || prevTab === "help";
35785
+ if (!fromOverlay) {
35786
+ this._closeSecondPanel();
35787
+ this._selectedMethod = null;
35788
+ this._view = "list";
35789
+ }
35746
35790
  this._updateRoute();
35747
35791
  return;
35748
35792
  }
@@ -35788,9 +35832,13 @@ var BeamApp = class extends i4 {
35788
35832
  this._view = "mcp-app";
35789
35833
  }
35790
35834
  if (tab === "app" && this._selectedPhoton?.isApp && this._selectedPhoton?.appEntry) {
35791
- this._selectedMethod = this._selectedPhoton.appEntry;
35792
- this._view = "form";
35793
- this._maybeAutoInvoke(this._selectedPhoton.appEntry);
35835
+ const appEntry = this._selectedPhoton.appEntry;
35836
+ const alreadyMounted = this._selectedMethod?.name === appEntry.name && this._view === "form" && this._lastResult != null;
35837
+ if (!alreadyMounted) {
35838
+ this._selectedMethod = appEntry;
35839
+ this._view = "form";
35840
+ this._maybeAutoInvoke(appEntry);
35841
+ }
35794
35842
  }
35795
35843
  }}
35796
35844
  @toggle-focus=${() => this._toggleFocusMode()}
@@ -35893,12 +35941,35 @@ var BeamApp = class extends i4 {
35893
35941
  ${this._selectedPhoton && !this._selectedMethod && this._mainTab === "methods" && this._view !== "studio" ? b2`<div class="main-toolbar">
35894
35942
  <div style="flex: 1; min-width: 0;">${this._renderPhotonToolbar()}</div>
35895
35943
  </div>` : ""}
35896
- ${this._mainTab === "log" ? b2`<activity-log
35897
- .items=${this._activityLog}
35898
- .filter=${this._selectedPhoton?.name}
35899
- .fullscreen=${true}
35900
- @clear=${() => this._activityLog = []}
35901
- ></activity-log>` : this._mainTab === "help" && this._selectedPhoton ? this._renderPhotonHelpView() : this._mainTab === "settings" && this._selectedPhoton ? this._renderSettingsView() : this._mainTab === "methods" && !this._selectedMethod && (this._selectedPhoton?.isApp || this._selectedPhoton?.isExternalMCP && this._selectedPhoton?.hasMcpApp) ? this._renderMethodsBentoOnly() : this._renderContent()}
35944
+ ${(() => {
35945
+ const sel = this._selectedPhoton;
35946
+ if (!sel) return this._renderContent();
35947
+ if (this._mainTab === "settings") {
35948
+ return this._renderSettingsView();
35949
+ }
35950
+ const isOverlay = this._mainTab === "log" || this._mainTab === "help";
35951
+ const isAppPhoton = !!sel.isApp || !!(sel.isExternalMCP && sel.hasMcpApp);
35952
+ const showsAppBento = this._mainTab === "methods" && !this._selectedMethod && isAppPhoton;
35953
+ return b2`
35954
+ <div class="tab-pane" ?hidden=${isOverlay || showsAppBento}>
35955
+ ${this._renderContent()}
35956
+ </div>
35957
+ ${isAppPhoton ? b2`<div class="tab-pane" ?hidden=${!showsAppBento}>
35958
+ ${this._renderMethodsBentoOnly()}
35959
+ </div>` : ""}
35960
+ <div class="tab-pane" ?hidden=${this._mainTab !== "log"}>
35961
+ <photon-pulse
35962
+ .photonName=${sel.name}
35963
+ .availableMethods=${(sel.methods || []).map((m3) => m3.name)}
35964
+ .activity=${this._activityLog}
35965
+ @clear-activity=${() => this._activityLog = []}
35966
+ ></photon-pulse>
35967
+ </div>
35968
+ <div class="tab-pane" ?hidden=${this._mainTab !== "help"}>
35969
+ ${this._renderPhotonHelpView()}
35970
+ </div>
35971
+ `;
35972
+ })()}
35902
35973
  </div>
35903
35974
  </main>
35904
35975
 
@@ -37531,6 +37602,21 @@ ${photon.errorMessage || "Unknown error"}</pre
37531
37602
  ↓ Export
37532
37603
  </button>
37533
37604
  <button @click=${() => this._handleShareResult()}>Share</button>
37605
+ <button
37606
+ @click=${() => {
37607
+ const rv2 = this.shadowRoot?.querySelector("result-viewer");
37608
+ const el2 = rv2?.shadowRoot?.querySelector(".container") || rv2;
37609
+ if (!el2) return;
37610
+ if (document.fullscreenElement) {
37611
+ void document.exitFullscreen?.();
37612
+ } else {
37613
+ void el2.requestFullscreen?.();
37614
+ }
37615
+ }}
37616
+ title="Toggle fullscreen"
37617
+ >
37618
+ ⛶ Fullscreen
37619
+ </button>
37534
37620
  </div>
37535
37621
  </div>` : ""}
37536
37622
  ${this._canvasActive ? b2`<canvas-renderer
@@ -38538,10 +38624,16 @@ ${photon.errorMessage || "Unknown error"}</pre
38538
38624
  lines.push(method.description);
38539
38625
  lines.push("");
38540
38626
  }
38541
- if (method.params && method.params.length > 0) {
38627
+ const paramList = Array.isArray(method.params) ? method.params : method.params && typeof method.params === "object" && method.params.properties ? Object.entries(method.params.properties).map(([n5, prop]) => ({
38628
+ name: n5,
38629
+ type: prop?.type,
38630
+ description: prop?.description,
38631
+ required: Array.isArray(method.params.required) ? method.params.required.includes(n5) : false
38632
+ })) : [];
38633
+ if (paramList.length > 0) {
38542
38634
  lines.push("| Name | Type | Required | Description |");
38543
38635
  lines.push("|------|------|----------|-------------|");
38544
- for (const param of method.params) {
38636
+ for (const param of paramList) {
38545
38637
  const required2 = param.required ? "\u2713" : "";
38546
38638
  const desc = param.description || "-";
38547
38639
  lines.push(`| \`${param.name}\` | ${param.type || "any"} | ${required2} | ${desc} |`);
@@ -38549,7 +38641,7 @@ ${photon.errorMessage || "Unknown error"}</pre
38549
38641
  lines.push("");
38550
38642
  }
38551
38643
  if (!photon.internal) {
38552
- const paramHints = (method.params || []).filter((p5) => p5.required).map((p5) => `--${p5.name} <${p5.type || "value"}>`).join(" ");
38644
+ const paramHints = paramList.filter((p5) => p5.required).map((p5) => `--${p5.name} <${p5.type || "value"}>`).join(" ");
38553
38645
  const cliPfx = window.__PHOTON_SHELL_INIT ? name2 : `photon cli ${name2}`;
38554
38646
  lines.push(`CLI: \`${cliPfx} ${method.name}${paramHints ? " " + paramHints : ""}\``);
38555
38647
  lines.push("");
@@ -39324,7 +39416,7 @@ ${photon.errorMessage || "Unknown error"}</pre
39324
39416
  <h2
39325
39417
  style="font-family: var(--font-display); font-size: var(--text-xl); font-weight: 700; color: var(--t-primary); margin: 0 0 4px 0;"
39326
39418
  >
39327
- ${photon.name} Settings
39419
+ ${formatLabel(photon.name)} Settings
39328
39420
  </h2>
39329
39421
  <p style="color: var(--t-muted); font-size: var(--text-sm); margin: 0; line-height: 1.5;">
39330
39422
  ${activeTab === "setup" ? "What this photon needs to run. Applies to every instance; changes take effect on next load." : "How this photon behaves. Applies to the current instance only; changes are saved immediately."}
@@ -40082,6 +40174,21 @@ BeamApp.styles = [
40082
40174
  flex-direction: column;
40083
40175
  }
40084
40176
 
40177
+ /* Tab panes — siblings inside .main-content-scroll. The active pane
40178
+ is visible; non-active siblings stay mounted (DOM + component
40179
+ state) but are hidden via display: none, so an iframe survives a
40180
+ quick Pulse/Help peek and a half-typed form is still there when
40181
+ you return. */
40182
+ .tab-pane {
40183
+ display: flex;
40184
+ flex-direction: column;
40185
+ flex: 1;
40186
+ min-height: 0;
40187
+ }
40188
+ .tab-pane[hidden] {
40189
+ display: none !important;
40190
+ }
40191
+
40085
40192
  /* Page transition when switching photons */
40086
40193
  @keyframes page-enter {
40087
40194
  from {
@@ -42083,7 +42190,7 @@ var BeamSidebar = class extends i4 {
42083
42190
  @click=${() => this.dispatchEvent(new CustomEvent("home", { bubbles: true, composed: true }))}
42084
42191
  title="Go home"
42085
42192
  >
42086
- Photon Beam
42193
+ <span class="logo-prefix">Photon </span>Beam
42087
42194
  <span
42088
42195
  class="status-indicator ${this.connected ? "connected" : this.reconnecting ? "reconnecting" : "disconnected"}"
42089
42196
  title="${this.connected ? "Connected" : this.reconnecting ? "Reconnecting..." : "Disconnected"}"
@@ -42136,25 +42243,32 @@ var BeamSidebar = class extends i4 {
42136
42243
  @click=${() => this._toggleFavoritesFilter()}
42137
42244
  title="Show favorites only (f)"
42138
42245
  aria-pressed="${this._showFavoritesOnly}"
42139
- aria-label="Filter by favorites"
42246
+ aria-label="Filter by favorites${this._favorites.size > 0 ? ` (${this._favorites.size})` : ""}"
42140
42247
  >
42141
- ${starFilled} Favorites ${this._favorites.size > 0 ? `(${this._favorites.size})` : ""}
42248
+ <span class="filter-icon-wrap">
42249
+ ${starFilled}
42250
+ ${this._favorites.size > 0 ? b2`<span class="filter-count-in-star" aria-hidden="true"
42251
+ >${this._favorites.size}</span
42252
+ >` : ""}
42253
+ </span>
42254
+ <span class="filter-btn-label">Favorites</span>
42142
42255
  </button>
42143
42256
  <button
42144
42257
  class="filter-btn"
42145
42258
  @click=${() => this.dispatchEvent(new CustomEvent("marketplace"))}
42146
42259
  aria-label="Open marketplace"
42260
+ title="Marketplace"
42147
42261
  >
42148
- ${marketplace} Marketplace
42262
+ ${marketplace}<span class="filter-btn-label">Marketplace</span>
42149
42263
  ${this.updatesAvailable > 0 ? b2`<span class="update-badge">${this.updatesAvailable}</span>` : ""}
42150
42264
  </button>
42151
42265
  <button
42152
42266
  class="filter-btn"
42153
42267
  @click=${() => this.dispatchEvent(new CustomEvent("daemon"))}
42154
- aria-label="Open daemon panel"
42155
- title="View scheduled jobs, webhooks, and sessions"
42268
+ aria-label="Open Pulse panel"
42269
+ title="Pulse: scheduled jobs, webhooks, and sessions"
42156
42270
  >
42157
- ${activity} Daemon
42271
+ ${activity}<span class="filter-btn-label">Pulse</span>
42158
42272
  </button>
42159
42273
  </div>
42160
42274
  </div>
@@ -42507,7 +42621,7 @@ ${photon.path}` : ""}"
42507
42621
  const tabs = [
42508
42622
  ...this.isApp ? [{ id: "app", label: "App", icon: appTabIcon }] : [],
42509
42623
  { id: "methods", label: "Methods", icon: methodsTabIcon },
42510
- { id: "log", label: "Activity", icon: activity },
42624
+ { id: "log", label: "Pulse", icon: activity },
42511
42625
  ...this.hasSettings || this.hasSetup ? [{ id: "settings", label: "Settings", icon: settings }] : [],
42512
42626
  ...this.hasPath && !this.isExternalMCP ? [{ id: "source", label: "Source", icon: source }] : [],
42513
42627
  { id: "help", label: "Help", icon: docs }
@@ -42706,6 +42820,8 @@ BeamSidebar.styles = [
42706
42820
  height: 100%;
42707
42821
  color: var(--t-primary);
42708
42822
  overflow: visible;
42823
+ container-type: inline-size;
42824
+ container-name: sidebar;
42709
42825
  }
42710
42826
 
42711
42827
  .sidebar-content {
@@ -42829,6 +42945,8 @@ BeamSidebar.styles = [
42829
42945
  cursor: pointer;
42830
42946
  user-select: none;
42831
42947
  transition: opacity 0.15s;
42948
+ white-space: nowrap;
42949
+ min-width: 0;
42832
42950
  }
42833
42951
 
42834
42952
  .logo:hover {
@@ -43025,7 +43143,10 @@ BeamSidebar.styles = [
43025
43143
  }
43026
43144
 
43027
43145
  .photon-item {
43028
- padding: var(--space-sm) var(--space-sm) var(--space-sm) var(--space-md);
43146
+ /* Right padding intentionally tight (4px) — the counts pill is the
43147
+ visual end of the row. Anything more reads as wasted space and
43148
+ steals room from the photon name. */
43149
+ padding: var(--space-sm) 4px var(--space-sm) var(--space-md);
43029
43150
  margin-bottom: var(--space-xs);
43030
43151
  border-radius: var(--radius-sm);
43031
43152
  cursor: pointer;
@@ -43413,10 +43534,12 @@ BeamSidebar.styles = [
43413
43534
  display: flex;
43414
43535
  gap: var(--space-sm);
43415
43536
  margin-top: var(--space-sm);
43537
+ min-width: 0;
43416
43538
  }
43417
43539
 
43418
43540
  .filter-btn {
43419
- flex: 1;
43541
+ flex: 1 1 0;
43542
+ min-width: 0;
43420
43543
  padding: var(--space-xs) var(--space-sm);
43421
43544
  background: var(--bg-glass);
43422
43545
  border: 1px solid var(--border-glass);
@@ -43428,9 +43551,16 @@ BeamSidebar.styles = [
43428
43551
  justify-content: center;
43429
43552
  gap: 4px;
43430
43553
  font-size: var(--text-sm);
43554
+ white-space: nowrap;
43555
+ overflow: hidden;
43431
43556
  transition: all 0.2s;
43432
43557
  }
43433
43558
 
43559
+ .filter-btn-label {
43560
+ overflow: hidden;
43561
+ text-overflow: ellipsis;
43562
+ }
43563
+
43434
43564
  .filter-btn:hover {
43435
43565
  background: var(--bg-glass-strong);
43436
43566
  }
@@ -43441,6 +43571,77 @@ BeamSidebar.styles = [
43441
43571
  color: white;
43442
43572
  }
43443
43573
 
43574
+ /* Favorites count is rendered centered inside the star itself —
43575
+ no separate badge, no horizontal space stolen from the label. */
43576
+ .filter-icon-wrap {
43577
+ position: relative;
43578
+ display: inline-flex;
43579
+ align-items: center;
43580
+ justify-content: center;
43581
+ flex-shrink: 0;
43582
+ color: var(--color-warning, #f59e0b);
43583
+ }
43584
+
43585
+ .filter-icon-wrap svg {
43586
+ width: 22px;
43587
+ height: 22px;
43588
+ }
43589
+
43590
+ .filter-count-in-star {
43591
+ position: absolute;
43592
+ top: 56%;
43593
+ left: 50%;
43594
+ transform: translate(-50%, -50%);
43595
+ font-size: 10px;
43596
+ font-weight: 800;
43597
+ line-height: 1;
43598
+ color: var(--bg-elevated, #0b1018);
43599
+ font-variant-numeric: tabular-nums;
43600
+ letter-spacing: -0.04em;
43601
+ pointer-events: none;
43602
+ }
43603
+
43604
+ .filter-btn.active .filter-icon-wrap {
43605
+ color: white;
43606
+ }
43607
+ .filter-btn.active .filter-count-in-star {
43608
+ color: var(--accent-primary);
43609
+ }
43610
+
43611
+ /* Responsive narrow mode: shrink the title to "Beam" and switch
43612
+ the three filter buttons to icon-only when the sidebar itself
43613
+ (not the viewport) is narrow. Container query watches :host's
43614
+ inline-size, so it fires whenever the user drags the sidebar
43615
+ below the threshold regardless of window width. */
43616
+ @container sidebar (max-width: 280px) {
43617
+ .logo-prefix {
43618
+ display: none;
43619
+ }
43620
+ .filter-btn {
43621
+ position: relative;
43622
+ padding: var(--space-xs);
43623
+ gap: 0;
43624
+ }
43625
+ .filter-btn .filter-btn-label {
43626
+ display: none;
43627
+ }
43628
+ .filter-btn .update-badge {
43629
+ position: absolute;
43630
+ top: 2px;
43631
+ right: 4px;
43632
+ min-width: 14px;
43633
+ height: 14px;
43634
+ padding: 0 4px;
43635
+ font-size: 9px;
43636
+ line-height: 1;
43637
+ display: inline-flex;
43638
+ align-items: center;
43639
+ justify-content: center;
43640
+ border: 1.5px solid var(--bg-elevated, #0b1018);
43641
+ border-radius: 999px;
43642
+ }
43643
+ }
43644
+
43444
43645
  .settings-btn {
43445
43646
  background: none;
43446
43647
  border: none;
@@ -43778,7 +43979,7 @@ var MethodCard = class extends i4 {
43778
43979
  _renderParamSignature() {
43779
43980
  if (this.method.isTemplate) return "";
43780
43981
  const props = this.method.params?.properties || {};
43781
- const paramNames = Object.keys(props);
43982
+ const paramNames = Object.keys(props).map((p5) => formatLabel(p5));
43782
43983
  if (paramNames.length === 0) return "";
43783
43984
  if (paramNames.length <= 4) {
43784
43985
  return b2`<span class="method-params method-params-trunc">(${paramNames.join(", ")}</span
@@ -46541,7 +46742,7 @@ var ActivityLog = class extends i4 {
46541
46742
  }
46542
46743
  render() {
46543
46744
  const visible = this._filterActive && this.filter ? this.items.filter((i7) => i7.photonName === this.filter) : this.items;
46544
- if (this.items.length === 0) return b2``;
46745
+ if (this.items.length === 0 && !this.fullscreen) return b2``;
46545
46746
  const hasFilterableEntries = this.filter && this.items.some((i7) => i7.photonName === this.filter);
46546
46747
  const typeIcon = (type) => {
46547
46748
  switch (type) {
@@ -46579,22 +46780,24 @@ var ActivityLog = class extends i4 {
46579
46780
  </div>
46580
46781
  </div>
46581
46782
 
46582
- ${this._collapsed ? "" : b2`
46583
- <ul class="log-list" role="log" aria-live="polite" aria-label="Activity log entries">
46584
- ${visible.map(
46783
+ ${this._collapsed ? "" : visible.length === 0 ? b2`<div class="empty-hint" role="status">
46784
+ ${this.items.length === 0 ? this.filter ? `No activity yet for ${this.filter}. Method calls and tool fires will appear here in real time.` : "No activity yet. Method calls and tool fires will appear here in real time." : `No activity yet for ${this.filter}. Toggle the filter off to see all photons.`}
46785
+ </div>` : b2`
46786
+ <ul class="log-list" role="log" aria-live="polite" aria-label="Activity log entries">
46787
+ ${visible.map(
46585
46788
  (item) => b2`
46586
- <li class="log-item type-${item.type}">
46587
- <span class="type-icon" aria-hidden="true">${typeIcon(item.type)}</span>
46588
- <span class="meta">${new Date(item.timestamp).toLocaleTimeString()}</span>
46589
- <span class="content"
46590
- ><span class="visually-hidden">${item.type}: </span
46591
- >${item.message}${item.durationMs != null ? b2`<span class="duration">${item.durationMs}ms</span>` : ""}${item.count && item.count > 1 ? b2`<span class="count">(×${item.count})</span>` : ""}</span
46592
- >
46593
- </li>
46594
- `
46789
+ <li class="log-item type-${item.type}">
46790
+ <span class="type-icon" aria-hidden="true">${typeIcon(item.type)}</span>
46791
+ <span class="meta">${new Date(item.timestamp).toLocaleTimeString()}</span>
46792
+ <span class="content"
46793
+ ><span class="visually-hidden">${item.type}: </span
46794
+ >${item.message}${item.durationMs != null ? b2`<span class="duration">${item.durationMs}ms</span>` : ""}${item.count && item.count > 1 ? b2`<span class="count">(×${item.count})</span>` : ""}</span
46795
+ >
46796
+ </li>
46797
+ `
46595
46798
  )}
46596
- </ul>
46597
- `}
46799
+ </ul>
46800
+ `}
46598
46801
  `;
46599
46802
  }
46600
46803
  _clear() {
@@ -46814,6 +47017,24 @@ ActivityLog.styles = [
46814
47017
  transform: rotate(-90deg);
46815
47018
  }
46816
47019
 
47020
+ .empty-hint {
47021
+ color: var(--t-muted);
47022
+ font-size: var(--text-sm);
47023
+ padding: var(--space-lg) var(--space-md);
47024
+ text-align: center;
47025
+ border: 1px dashed var(--border-glass);
47026
+ border-radius: var(--radius-sm);
47027
+ background: color-mix(in srgb, var(--bg-glass) 50%, transparent);
47028
+ }
47029
+
47030
+ :host([fullscreen]) .empty-hint {
47031
+ flex: 1;
47032
+ display: flex;
47033
+ align-items: center;
47034
+ justify-content: center;
47035
+ min-height: 120px;
47036
+ }
47037
+
46817
47038
  /* ===== Responsive Design ===== */
46818
47039
  @media (max-width: 768px) {
46819
47040
  .log-item {
@@ -52288,7 +52509,7 @@ ${footerText || pageNum ? `<div class="slide-footer"><span>${footerText || ""}</
52288
52509
  return this._columnPipes;
52289
52510
  }
52290
52511
  _applyPipe(value, pipe2, arg) {
52291
- if (value === null || value === void 0) return "\u2014";
52512
+ if (value === null || value === void 0) return "\xB7";
52292
52513
  switch (pipe2) {
52293
52514
  case "currency":
52294
52515
  return typeof value === "number" ? value.toLocaleString(void 0, { style: "currency", currency: arg || "USD" }) : String(value);
@@ -52323,9 +52544,9 @@ ${footerText || pageNum ? `<div class="slide-footer"><span>${footerText || ""}</
52323
52544
  }
52324
52545
  }
52325
52546
  _formatCellValue(value, key, highlight = false) {
52326
- if (value === null || value === void 0) return "\u2014";
52547
+ if (value === null || value === void 0) return "\xB7";
52327
52548
  if (value === "" || typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0)
52328
- return "\u2014";
52549
+ return "\xB7";
52329
52550
  if (typeof value === "boolean") return value ? "\u2713" : "\u2717";
52330
52551
  const pipes = this._getColumnPipes();
52331
52552
  if (pipes.size > 0) {
@@ -52372,7 +52593,7 @@ ${footerText || pageNum ? `<div class="slide-footer"><span>${footerText || ""}</
52372
52593
  >`;
52373
52594
  }
52374
52595
  if (Array.isArray(value)) {
52375
- if (value.length === 0) return "\u2014";
52596
+ if (value.length === 0) return "\xB7";
52376
52597
  if (value.every((v2) => typeof v2 !== "object" || v2 === null)) {
52377
52598
  return b2`<span style="display:flex;flex-wrap:wrap;gap:3px;"
52378
52599
  >${value.map(
@@ -54026,9 +54247,7 @@ ResultViewer.styles = [
54026
54247
  width: 140px;
54027
54248
  font-weight: 600;
54028
54249
  color: var(--t-muted);
54029
- text-transform: uppercase;
54030
54250
  font-size: var(--text-xs);
54031
- letter-spacing: 0.05em;
54032
54251
  }
54033
54252
 
54034
54253
  /* List Styles */
@@ -57355,7 +57574,7 @@ var MarketplaceView = class extends i4 {
57355
57574
  All
57356
57575
  <span class="count">${this._allItems.length}</span>
57357
57576
  </button>
57358
- ${this._sources.filter((s5) => s5.enabled).map(
57577
+ ${this._sources.filter((s5) => s5.enabled && !s5.name.endsWith(".photon.ts")).map(
57359
57578
  (source3) => b2`
57360
57579
  <button
57361
57580
  class="filter-pill ${this._activeFilter === source3.name ? "active" : ""}"
@@ -58945,6 +59164,70 @@ function tilde(p5) {
58945
59164
  if (parts.length <= 2) return p5;
58946
59165
  return "\u2026/" + parts.slice(-2).join("/");
58947
59166
  }
59167
+ function isImminent(ts) {
59168
+ if (!ts) return false;
59169
+ const delta = ts - Date.now();
59170
+ return delta >= 0 && delta < 6e4;
59171
+ }
59172
+ function groupByLocation(rows) {
59173
+ const map4 = /* @__PURE__ */ new Map();
59174
+ for (const r7 of rows) {
59175
+ const key = r7.workingDir ?? "";
59176
+ const list = map4.get(key);
59177
+ if (list) list.push(r7);
59178
+ else map4.set(key, [r7]);
59179
+ }
59180
+ return [...map4.entries()].sort(([a5], [b3]) => a5.localeCompare(b3));
59181
+ }
59182
+ var WEEKDAY_NAMES = [
59183
+ "Sunday",
59184
+ "Monday",
59185
+ "Tuesday",
59186
+ "Wednesday",
59187
+ "Thursday",
59188
+ "Friday",
59189
+ "Saturday"
59190
+ ];
59191
+ function formatHour(h5) {
59192
+ return h5 === 0 ? "12am" : h5 < 12 ? `${h5}am` : h5 === 12 ? "12pm" : `${h5 - 12}pm`;
59193
+ }
59194
+ function describeWeekdays(spec) {
59195
+ if (spec === "*") return "every day";
59196
+ if (spec === "1-5") return "weekdays";
59197
+ if (spec === "0,6" || spec === "6,0") return "weekends";
59198
+ if (/^\d$/.test(spec)) {
59199
+ const n5 = parseInt(spec, 10);
59200
+ if (n5 >= 0 && n5 <= 6) return WEEKDAY_NAMES[n5];
59201
+ }
59202
+ return null;
59203
+ }
59204
+ function humanizeCron(cron) {
59205
+ const trimmed = cron.trim();
59206
+ if (trimmed === "* * * * *") return "Every minute";
59207
+ if (trimmed === "0 * * * *") return "Hourly";
59208
+ if (trimmed === "0 0 * * *") return "Daily at midnight";
59209
+ if (trimmed === "0 12 * * *") return "Daily at noon";
59210
+ if (trimmed === "0 0 1 * *") return "Monthly";
59211
+ let m3 = trimmed.match(/^(?:\*|0)\/(\d+) \* \* \* \*$/);
59212
+ if (m3) return `Every ${m3[1]} minutes`;
59213
+ m3 = trimmed.match(/^0 (?:\*|0)\/(\d+) \* \* \*$/);
59214
+ if (m3) return `Every ${m3[1]} hours`;
59215
+ m3 = trimmed.match(/^0 (\d{1,2}) \* \* \*$/);
59216
+ if (m3) {
59217
+ const h5 = parseInt(m3[1], 10);
59218
+ if (h5 >= 0 && h5 <= 23) return `Daily at ${formatHour(h5)}`;
59219
+ }
59220
+ m3 = trimmed.match(/^0 (\d{1,2}) \* \* ([0-9,-]+)$/);
59221
+ if (m3) {
59222
+ const h5 = parseInt(m3[1], 10);
59223
+ const dayLabel = describeWeekdays(m3[2]);
59224
+ if (h5 >= 0 && h5 <= 23 && dayLabel) {
59225
+ const cap = dayLabel[0].toUpperCase() + dayLabel.slice(1);
59226
+ return `${cap} at ${formatHour(h5)}`;
59227
+ }
59228
+ }
59229
+ return trimmed;
59230
+ }
58948
59231
  var DaemonPanel = class extends i4 {
58949
59232
  constructor() {
58950
59233
  super(...arguments);
@@ -59035,157 +59318,267 @@ var DaemonPanel = class extends i4 {
59035
59318
  _closeHistory() {
59036
59319
  this._history = null;
59037
59320
  }
59321
+ _sectionHead(title, count, sub) {
59322
+ return b2`
59323
+ <div class="section-head">
59324
+ <h2 class="section-title">
59325
+ ${title} ${count > 0 ? b2`<span class="section-count">· ${count}</span>` : ""}
59326
+ </h2>
59327
+ ${sub ? b2`<span class="section-sub">${sub}</span>` : ""}
59328
+ </div>
59329
+ `;
59330
+ }
59331
+ _entity(photon, method, dormant = false) {
59332
+ return b2`
59333
+ <span class="entity-row">
59334
+ <span class="status-dot ${dormant ? "dormant" : ""}"></span>
59335
+ <span class="entity"
59336
+ ><span class="photon-part">${photon}</span><span class="sep">.</span
59337
+ ><span class="method-part">${method}</span></span
59338
+ >
59339
+ </span>
59340
+ `;
59341
+ }
59342
+ _timingLine(cron, nextRun, lastRun) {
59343
+ const imminent = isImminent(nextRun);
59344
+ return b2`
59345
+ <span class="schedule" title="${cron}">${humanizeCron(cron)}</span>
59346
+ ${nextRun ? b2`<span class="timing-sep">·</span>
59347
+ <span class="${imminent ? "imminent" : ""}">next ${formatWhen(nextRun)}</span>` : ""}
59348
+ ${lastRun ? b2`<span class="timing-sep">·</span> <span>last ${formatWhen(lastRun)}</span>` : ""}
59349
+ `;
59350
+ }
59351
+ _groupHeader(loc, count, label) {
59352
+ return b2`
59353
+ <tr class="group-header">
59354
+ <td colspan="4">
59355
+ <span class="location-path" title="${loc || "(no working dir)"}">${tilde(loc)}</span>
59356
+ <span class="location-meta">${count} ${count === 1 ? label : label + "s"}</span>
59357
+ </td>
59358
+ </tr>
59359
+ `;
59360
+ }
59361
+ _iconBtn(opts) {
59362
+ const cls = opts.variant ? `icon-btn ${opts.variant}` : "icon-btn";
59363
+ return b2`
59364
+ <button
59365
+ class="${cls}"
59366
+ title="${opts.title}"
59367
+ aria-label="${opts.title}"
59368
+ @click=${opts.onClick}
59369
+ >
59370
+ ${opts.icon === "history" ? b2`<svg
59371
+ viewBox="0 0 24 24"
59372
+ fill="none"
59373
+ stroke="currentColor"
59374
+ stroke-width="2"
59375
+ stroke-linecap="round"
59376
+ stroke-linejoin="round"
59377
+ aria-hidden="true"
59378
+ >
59379
+ <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
59380
+ <path d="M3 3v5h5" />
59381
+ <path d="M12 7v5l3 2" />
59382
+ </svg>` : opts.icon === "pause" ? b2`<svg
59383
+ viewBox="0 0 24 24"
59384
+ fill="none"
59385
+ stroke="currentColor"
59386
+ stroke-width="2"
59387
+ stroke-linecap="round"
59388
+ stroke-linejoin="round"
59389
+ aria-hidden="true"
59390
+ >
59391
+ <rect x="6" y="4" width="4" height="16" />
59392
+ <rect x="14" y="4" width="4" height="16" />
59393
+ </svg>` : opts.icon === "disable" ? b2`<svg
59394
+ viewBox="0 0 24 24"
59395
+ fill="none"
59396
+ stroke="currentColor"
59397
+ stroke-width="2"
59398
+ stroke-linecap="round"
59399
+ stroke-linejoin="round"
59400
+ aria-hidden="true"
59401
+ >
59402
+ <path d="M18.36 6.64a9 9 0 1 1-12.73 0" />
59403
+ <line x1="12" y1="2" x2="12" y2="12" />
59404
+ </svg>` : b2`<svg
59405
+ viewBox="0 0 24 24"
59406
+ fill="none"
59407
+ stroke="currentColor"
59408
+ stroke-width="2"
59409
+ stroke-linecap="round"
59410
+ stroke-linejoin="round"
59411
+ aria-hidden="true"
59412
+ >
59413
+ <polygon points="6 3 20 12 6 21" />
59414
+ </svg>`}
59415
+ </button>
59416
+ `;
59417
+ }
59418
+ _renderLoaded() {
59419
+ const rows = this._snap?.sessions ?? [];
59420
+ if (rows.length === 0) return "";
59421
+ const sorted = [...rows].sort((a5, b3) => a5.photon.localeCompare(b3.photon));
59422
+ const totalConn = sorted.reduce((acc, r7) => acc + r7.instanceCount, 0);
59423
+ const sub = `${totalConn} ${totalConn === 1 ? "connection" : "connections"}`;
59424
+ return b2`
59425
+ <section class="card">
59426
+ ${this._sectionHead("Loaded photons", rows.length, sub)}
59427
+ <div class="chip-strip">
59428
+ ${sorted.map(
59429
+ (r7) => b2`
59430
+ <span
59431
+ class="chip ${r7.instanceCount === 0 ? "idle" : ""}"
59432
+ title="${r7.workingDir ?? ""}"
59433
+ >
59434
+ <span class="chip-dot"></span>
59435
+ <span>${r7.photon}</span>
59436
+ ${r7.instanceCount > 0 ? b2`<span class="conn-badge">${r7.instanceCount}</span>` : ""}
59437
+ </span>
59438
+ `
59439
+ )}
59440
+ </div>
59441
+ </section>
59442
+ `;
59443
+ }
59038
59444
  _renderActive() {
59039
59445
  const rows = this._snap?.active ?? [];
59446
+ const groups = groupByLocation(rows);
59447
+ for (const [, list] of groups) {
59448
+ list.sort((a5, b3) => (a5.nextRun ?? Infinity) - (b3.nextRun ?? Infinity));
59449
+ }
59040
59450
  return b2`
59041
- <h2>Active schedules (${rows.length})</h2>
59042
- <div class="table-wrap">
59043
- <table>
59044
- <thead>
59045
- <tr>
59046
- <th>Location</th>
59047
- <th>Photon</th>
59048
- <th>Method</th>
59049
- <th>Cron</th>
59050
- <th>Next run</th>
59051
- <th>Last run</th>
59052
- <th>Runs</th>
59053
- <th></th>
59054
- </tr>
59055
- </thead>
59056
- <tbody>
59057
- ${rows.length === 0 ? b2`<tr class="empty">
59058
- <td colspan="8">no active schedules</td>
59059
- </tr>` : rows.map(
59060
- (r7) => b2`
59061
- <tr>
59062
- <td>${tilde(r7.workingDir)}</td>
59063
- <td>${r7.photon}</td>
59064
- <td>${formatLabel(r7.method)}</td>
59065
- <td><code>${r7.cron}</code></td>
59066
- <td>${formatWhen(r7.nextRun)}</td>
59067
- <td>${formatWhen(r7.lastRun)}</td>
59068
- <td>${r7.runCount}</td>
59069
- <td class="actions">
59070
- <button @click=${() => this._openHistory(r7.photon, r7.method)}>
59071
- History
59072
- </button>
59073
- <button @click=${() => this._scheduleAction("pause", r7.photon, r7.method)}>
59074
- Pause
59075
- </button>
59076
- <button @click=${() => this._scheduleAction("disable", r7.photon, r7.method)}>
59077
- Disable
59078
- </button>
59079
- </td>
59080
- </tr>
59451
+ <section class="card">
59452
+ ${this._sectionHead("Schedules", rows.length)}
59453
+ ${rows.length === 0 ? b2`<p class="empty-line">No active schedules.</p>` : b2`<div class="table-wrap">
59454
+ <table class="grouped">
59455
+ ${groups.map(
59456
+ ([loc, items]) => b2`
59457
+ <tbody>
59458
+ ${this._groupHeader(loc, items.length, "schedule")}
59459
+ ${items.map(
59460
+ (r7) => b2`
59461
+ <tr>
59462
+ <td class="entity-cell">${this._entity(r7.photon, r7.method)}</td>
59463
+ <td class="timing">
59464
+ ${this._timingLine(r7.cron, r7.nextRun, r7.lastRun)}
59465
+ </td>
59466
+ <td class="runs">
59467
+ ${r7.runCount.toLocaleString()}<span class="runs-label"
59468
+ >${r7.runCount === 1 ? "run" : "runs"}</span
59469
+ >
59470
+ </td>
59471
+ <td class="actions">
59472
+ <span class="icon-bar">
59473
+ ${this._iconBtn({
59474
+ icon: "history",
59475
+ title: "History",
59476
+ onClick: () => this._openHistory(r7.photon, r7.method)
59477
+ })}
59478
+ ${this._iconBtn({
59479
+ icon: "pause",
59480
+ title: "Pause",
59481
+ onClick: () => this._scheduleAction("pause", r7.photon, r7.method)
59482
+ })}
59483
+ ${this._iconBtn({
59484
+ icon: "disable",
59485
+ title: "Disable",
59486
+ onClick: () => this._scheduleAction("disable", r7.photon, r7.method),
59487
+ variant: "danger"
59488
+ })}
59489
+ </span>
59490
+ </td>
59491
+ </tr>
59492
+ `
59493
+ )}
59494
+ </tbody>
59081
59495
  `
59082
59496
  )}
59083
- </tbody>
59084
- </table>
59085
- </div>
59497
+ </table>
59498
+ </div>`}
59499
+ </section>
59086
59500
  `;
59087
59501
  }
59088
59502
  _renderDeclared() {
59089
59503
  const rows = (this._snap?.declared ?? []).filter((d5) => !d5.active);
59504
+ if (rows.length === 0) return "";
59505
+ const groups = groupByLocation(rows);
59090
59506
  return b2`
59091
- <h2>Declared but not enrolled (${rows.length})</h2>
59092
- <p class="hint">
59093
- These photons declare <code>@scheduled</code> methods that haven't been enrolled yet.
59094
- </p>
59095
- <div class="table-wrap">
59096
- <table>
59097
- <thead>
59098
- <tr>
59099
- <th>Location</th>
59100
- <th>Photon</th>
59101
- <th>Method</th>
59102
- <th>Cron</th>
59103
- <th></th>
59104
- </tr>
59105
- </thead>
59106
- <tbody>
59107
- ${rows.length === 0 ? b2`<tr class="empty">
59108
- <td colspan="5">no dormant declarations</td>
59109
- </tr>` : rows.map(
59110
- (r7) => b2`
59111
- <tr>
59112
- <td>${tilde(r7.workingDir)}</td>
59113
- <td>${r7.photon}</td>
59114
- <td>${formatLabel(r7.method)}</td>
59115
- <td><code>${r7.cron}</code></td>
59116
- <td class="actions">
59117
- <button @click=${() => this._scheduleAction("enable", r7.photon, r7.method)}>
59118
- Enable
59119
- </button>
59120
- </td>
59121
- </tr>
59122
- `
59507
+ <section class="card">
59508
+ ${this._sectionHead("Pending", rows.length)}
59509
+ <p class="section-hint"><code>@scheduled</code> methods discovered but not yet enrolled.</p>
59510
+ <div class="table-wrap">
59511
+ <table class="grouped">
59512
+ ${groups.map(
59513
+ ([loc, items]) => b2`
59514
+ <tbody>
59515
+ ${this._groupHeader(loc, items.length, "pending")}
59516
+ ${items.map(
59517
+ (r7) => b2`
59518
+ <tr>
59519
+ <td class="entity-cell">${this._entity(r7.photon, r7.method, true)}</td>
59520
+ <td class="timing">
59521
+ <span class="schedule" title="${r7.cron}">${humanizeCron(r7.cron)}</span>
59522
+ </td>
59523
+ <td></td>
59524
+ <td class="actions">
59525
+ <span class="icon-bar">
59526
+ ${this._iconBtn({
59527
+ icon: "play",
59528
+ title: "Enable",
59529
+ onClick: () => this._scheduleAction("enable", r7.photon, r7.method),
59530
+ variant: "primary"
59531
+ })}
59532
+ </span>
59533
+ </td>
59534
+ </tr>
59535
+ `
59536
+ )}
59537
+ </tbody>
59538
+ `
59123
59539
  )}
59124
- </tbody>
59125
- </table>
59126
- </div>
59540
+ </table>
59541
+ </div>
59542
+ </section>
59127
59543
  `;
59128
59544
  }
59129
59545
  _renderWebhooks() {
59130
59546
  const rows = this._snap?.webhooks ?? [];
59547
+ if (rows.length === 0) return "";
59548
+ const groups = groupByLocation(rows);
59131
59549
  return b2`
59132
- <h2>Webhooks (${rows.length})</h2>
59133
- <div class="table-wrap">
59134
- <table>
59135
- <thead>
59136
- <tr>
59137
- <th>Location</th>
59138
- <th>Photon</th>
59139
- <th>Route</th>
59140
- <th>Method</th>
59141
- </tr>
59142
- </thead>
59143
- <tbody>
59144
- ${rows.length === 0 ? b2`<tr class="empty">
59145
- <td colspan="4">no webhook routes</td>
59146
- </tr>` : rows.map(
59147
- (r7) => b2`
59148
- <tr>
59149
- <td>${tilde(r7.workingDir)}</td>
59150
- <td>${r7.photon}</td>
59151
- <td><code>${r7.route}</code></td>
59152
- <td>${formatLabel(r7.method)}</td>
59153
- </tr>
59154
- `
59155
- )}
59156
- </tbody>
59157
- </table>
59158
- </div>
59159
- `;
59160
- }
59161
- _renderSessions() {
59162
- const rows = this._snap?.sessions ?? [];
59163
- return b2`
59164
- <h2>Active sessions (${rows.length})</h2>
59165
- <div class="table-wrap">
59166
- <table>
59167
- <thead>
59168
- <tr>
59169
- <th>Location</th>
59170
- <th>Photon</th>
59171
- <th>Instances</th>
59172
- </tr>
59173
- </thead>
59174
- <tbody>
59175
- ${rows.length === 0 ? b2`<tr class="empty">
59176
- <td colspan="3">no loaded sessions</td>
59177
- </tr>` : rows.map(
59178
- (r7) => b2`
59179
- <tr>
59180
- <td>${tilde(r7.workingDir)}</td>
59181
- <td>${r7.photon}</td>
59182
- <td>${r7.instanceCount}</td>
59183
- </tr>
59184
- `
59550
+ <section class="card">
59551
+ ${this._sectionHead("Webhooks", rows.length)}
59552
+ <div class="table-wrap">
59553
+ <table class="grouped">
59554
+ ${groups.map(
59555
+ ([loc, items]) => b2`
59556
+ <tbody>
59557
+ ${this._groupHeader(loc, items.length, "route")}
59558
+ ${items.map((r7) => {
59559
+ const m3 = r7.route.match(/^([A-Z]+)\s+(.+)$/);
59560
+ const verb = m3?.[1] ?? "";
59561
+ const path = m3?.[2] ?? r7.route;
59562
+ return b2`
59563
+ <tr>
59564
+ <td>
59565
+ <span class="route-cell">
59566
+ <span class="route-photon">${r7.photon}</span>
59567
+ ${verb ? b2`<span class="route-verb">${verb}</span>` : ""}
59568
+ <span class="route-path">${path}</span>
59569
+ <span class="route-arrow">→</span>
59570
+ <span class="route-handler">${r7.method}</span>
59571
+ </span>
59572
+ </td>
59573
+ </tr>
59574
+ `;
59575
+ })}
59576
+ </tbody>
59577
+ `
59185
59578
  )}
59186
- </tbody>
59187
- </table>
59188
- </div>
59579
+ </table>
59580
+ </div>
59581
+ </section>
59189
59582
  `;
59190
59583
  }
59191
59584
  _renderHistoryDrawer() {
@@ -59230,21 +59623,19 @@ var DaemonPanel = class extends i4 {
59230
59623
  }
59231
59624
  render() {
59232
59625
  return b2`
59233
- <h1>Daemon</h1>
59234
- <p class="hint">
59235
- Observability for scheduled work, webhook routes, and loaded sessions. Mirrors
59236
- <code>photon ps</code>.
59237
- </p>
59238
-
59239
- <div class="refresh-bar">
59626
+ <div class="pulse-header">
59627
+ <h1>Pulse</h1>
59240
59628
  <button class="refresh" @click=${() => this._refresh()} ?disabled=${this._loading}>
59241
59629
  ${this._loading ? "Refreshing\u2026" : "Refresh"}
59242
59630
  </button>
59243
- <span class="hint">auto-refresh every ${POLL_INTERVAL_MS / 1e3}s</span>
59244
59631
  </div>
59632
+ <p class="hint">
59633
+ Heartbeat of the daemon: loaded photons, schedules, and webhooks. Mirrors
59634
+ <code>photon ps</code>. Auto-refresh every ${POLL_INTERVAL_MS / 1e3}s.
59635
+ </p>
59245
59636
 
59246
- ${this._error ? b2`<div class="error">${this._error}</div>` : ""} ${this._renderActive()}
59247
- ${this._renderDeclared()} ${this._renderWebhooks()} ${this._renderSessions()}
59637
+ ${this._error ? b2`<div class="error">${this._error}</div>` : ""} ${this._renderLoaded()}
59638
+ ${this._renderActive()} ${this._renderWebhooks()} ${this._renderDeclared()}
59248
59639
  ${this._renderHistoryDrawer()}
59249
59640
  `;
59250
59641
  }
@@ -59259,139 +59650,423 @@ DaemonPanel.styles = [
59259
59650
  overflow-y: auto;
59260
59651
  }
59261
59652
 
59653
+ .pulse-header {
59654
+ display: flex;
59655
+ align-items: baseline;
59656
+ justify-content: space-between;
59657
+ gap: var(--space-md);
59658
+ margin-bottom: var(--space-xs);
59659
+ }
59660
+
59262
59661
  h1 {
59263
59662
  font-size: 1.5rem;
59264
- margin: 0 0 var(--space-sm) 0;
59265
- }
59266
-
59267
- h2 {
59268
- font-size: 1rem;
59269
- margin: var(--space-lg) 0 var(--space-xs) 0;
59270
- color: var(--t-primary);
59663
+ margin: 0;
59664
+ color: var(--t-primary);
59271
59665
  }
59272
59666
 
59273
59667
  p.hint {
59274
59668
  color: var(--t-muted);
59275
59669
  font-size: 0.85rem;
59276
- margin: 0 0 var(--space-md) 0;
59277
- }
59278
-
59279
- .refresh-bar {
59280
- display: flex;
59281
- align-items: center;
59282
- gap: var(--space-sm);
59283
- margin-bottom: var(--space-md);
59670
+ margin: 0 0 var(--space-lg) 0;
59284
59671
  }
59285
59672
 
59286
59673
  button.refresh {
59287
- background: var(--surface-secondary, #222);
59674
+ background: var(--bg-glass);
59288
59675
  color: var(--t-primary);
59289
- border: 1px solid var(--border, #333);
59676
+ border: 1px solid var(--border-glass);
59290
59677
  padding: 4px 12px;
59291
- border-radius: 4px;
59678
+ border-radius: var(--radius-sm);
59292
59679
  cursor: pointer;
59680
+ font-size: 0.85rem;
59681
+ transition: all 0.15s ease;
59293
59682
  }
59294
59683
  button.refresh:hover {
59295
- background: var(--surface-hover, #2a2a2a);
59684
+ background: var(--bg-glass-strong);
59685
+ border-color: color-mix(in srgb, var(--accent-primary) 38%, var(--border-glass));
59686
+ }
59687
+ button.refresh:disabled {
59688
+ opacity: 0.6;
59689
+ cursor: not-allowed;
59690
+ }
59691
+
59692
+ /* Section card */
59693
+ section.card {
59694
+ background: color-mix(in srgb, var(--bg-glass) 60%, transparent);
59695
+ border: 1px solid var(--border-glass);
59696
+ border-radius: var(--radius-md);
59697
+ padding: var(--space-md);
59698
+ margin-bottom: var(--space-md);
59699
+ }
59700
+
59701
+ .section-head {
59702
+ display: flex;
59703
+ align-items: baseline;
59704
+ justify-content: space-between;
59705
+ gap: var(--space-md);
59706
+ margin-bottom: var(--space-sm);
59707
+ }
59708
+
59709
+ .section-title {
59710
+ margin: 0;
59711
+ font-family: 'Azeret Mono', var(--font-mono);
59712
+ font-size: 0.72rem;
59713
+ text-transform: uppercase;
59714
+ letter-spacing: 0.12em;
59715
+ color: var(--t-primary);
59716
+ font-weight: 600;
59717
+ display: flex;
59718
+ align-items: baseline;
59719
+ gap: var(--space-xs);
59720
+ }
59721
+
59722
+ .section-count {
59723
+ color: var(--t-muted);
59724
+ font-size: 0.7rem;
59725
+ font-weight: 500;
59726
+ letter-spacing: 0.04em;
59727
+ }
59728
+
59729
+ .section-sub {
59730
+ color: var(--t-muted);
59731
+ font-size: 0.7rem;
59732
+ font-weight: 500;
59733
+ letter-spacing: 0.04em;
59734
+ }
59735
+
59736
+ .section-hint {
59737
+ color: var(--t-muted);
59738
+ font-size: 0.78rem;
59739
+ margin: 0 0 var(--space-sm) 0;
59740
+ }
59741
+
59742
+ .empty-line {
59743
+ color: var(--t-muted);
59744
+ font-size: 0.85rem;
59745
+ font-style: italic;
59746
+ margin: var(--space-xs) 0 0 0;
59296
59747
  }
59297
59748
 
59749
+ /* Tables */
59298
59750
  .table-wrap {
59299
59751
  overflow-x: auto;
59300
- border: 1px solid var(--border, #333);
59301
- border-radius: 4px;
59302
59752
  }
59303
59753
 
59304
- table {
59754
+ table.grouped {
59305
59755
  width: 100%;
59306
- border-collapse: collapse;
59756
+ border-collapse: separate;
59757
+ border-spacing: 0;
59758
+ }
59759
+
59760
+ table.grouped td {
59761
+ padding: 6px 8px;
59762
+ border-bottom: 1px solid color-mix(in srgb, var(--border-glass) 50%, transparent);
59763
+ vertical-align: middle;
59307
59764
  font-size: 0.85rem;
59308
59765
  }
59309
59766
 
59310
- th,
59311
- td {
59312
- padding: 6px 10px;
59313
- text-align: left;
59314
- border-bottom: 1px solid var(--border, #2a2a2a);
59767
+ table.grouped tbody:last-child tr:last-child td {
59768
+ border-bottom: none;
59315
59769
  }
59316
59770
 
59317
- th {
59318
- background: var(--surface-secondary, #1a1a1a);
59771
+ table.grouped tbody tr:not(.group-header):hover td {
59772
+ background: color-mix(in srgb, var(--bg-glass) 60%, transparent);
59773
+ }
59774
+
59775
+ /* Group header row (location) — separated by spacing + thin top rule */
59776
+ tr.group-header td {
59777
+ padding: var(--space-xl) 8px var(--space-xs) 8px;
59778
+ background: transparent;
59779
+ border-bottom: none;
59780
+ border-top: 1px solid var(--border-glass);
59781
+ }
59782
+ table.grouped tbody:first-child tr.group-header td {
59783
+ padding-top: var(--space-2xs);
59784
+ border-top: none;
59785
+ }
59786
+ .location-path {
59787
+ font-family: 'Azeret Mono', var(--font-mono);
59788
+ font-size: 0.72rem;
59789
+ color: var(--t-primary);
59790
+ letter-spacing: 0.08em;
59791
+ text-transform: uppercase;
59319
59792
  font-weight: 600;
59793
+ }
59794
+ .location-meta {
59320
59795
  color: var(--t-muted);
59796
+ font-size: 0.7rem;
59797
+ margin-left: var(--space-sm);
59798
+ opacity: 0.7;
59799
+ text-transform: lowercase;
59800
+ font-weight: 400;
59801
+ letter-spacing: 0.02em;
59321
59802
  }
59322
59803
 
59323
- tr.empty td {
59324
- text-align: center;
59804
+ /* Entity cell — status dot + photon.method */
59805
+ td.entity-cell {
59806
+ white-space: nowrap;
59807
+ width: 1%;
59808
+ }
59809
+ .entity-row {
59810
+ display: inline-flex;
59811
+ align-items: center;
59812
+ gap: var(--space-xs);
59813
+ }
59814
+ .status-dot {
59815
+ width: 7px;
59816
+ height: 7px;
59817
+ border-radius: 50%;
59818
+ background: var(--color-success);
59819
+ box-shadow: 0 0 5px color-mix(in srgb, var(--color-success) 50%, transparent);
59820
+ flex-shrink: 0;
59821
+ }
59822
+ .status-dot.dormant {
59823
+ background: var(--t-muted);
59824
+ box-shadow: none;
59825
+ opacity: 0.45;
59826
+ }
59827
+ .entity {
59828
+ font-family: 'Azeret Mono', var(--font-mono);
59829
+ font-size: 0.85rem;
59830
+ white-space: nowrap;
59831
+ }
59832
+ .entity .photon-part {
59325
59833
  color: var(--t-muted);
59326
- padding: var(--space-md);
59834
+ }
59835
+ .entity .sep {
59836
+ color: var(--t-muted);
59837
+ }
59838
+ .entity .method-part {
59839
+ color: var(--t-primary);
59840
+ font-weight: 600;
59327
59841
  }
59328
59842
 
59329
- .actions {
59330
- display: flex;
59331
- gap: 4px;
59843
+ /* Timing line */
59844
+ td.timing {
59845
+ color: var(--t-muted);
59846
+ font-size: 0.8rem;
59847
+ font-variant-numeric: tabular-nums;
59848
+ }
59849
+ .timing .schedule {
59850
+ color: var(--t-primary);
59851
+ }
59852
+ .timing .timing-sep {
59853
+ margin: 0 var(--space-xs);
59854
+ opacity: 0.4;
59855
+ }
59856
+ .timing .imminent {
59857
+ color: var(--accent-primary);
59332
59858
  }
59333
59859
 
59334
- .actions button {
59335
- font-size: 0.75rem;
59336
- padding: 2px 8px;
59337
- border: 1px solid var(--border, #333);
59338
- background: transparent;
59860
+ /* Runs */
59861
+ td.runs {
59862
+ text-align: right;
59863
+ font-variant-numeric: tabular-nums;
59339
59864
  color: var(--t-primary);
59340
- cursor: pointer;
59341
- border-radius: 3px;
59865
+ font-size: 0.82rem;
59866
+ white-space: nowrap;
59867
+ width: 1%;
59868
+ }
59869
+ .runs-label {
59870
+ color: var(--t-muted);
59871
+ margin-left: 4px;
59872
+ font-size: 0.72rem;
59873
+ }
59874
+
59875
+ /* Webhook route cell */
59876
+ .route-cell {
59877
+ font-family: 'Azeret Mono', var(--font-mono);
59878
+ font-size: 0.83rem;
59879
+ display: inline-flex;
59880
+ align-items: center;
59881
+ gap: var(--space-xs);
59882
+ white-space: nowrap;
59883
+ }
59884
+ .route-photon {
59885
+ color: var(--t-muted);
59886
+ }
59887
+ .route-verb {
59888
+ color: var(--accent-secondary);
59889
+ font-weight: 600;
59890
+ font-size: 0.7rem;
59891
+ padding: 1px 5px;
59892
+ border-radius: var(--radius-xs);
59893
+ background: color-mix(in srgb, var(--accent-secondary) 12%, transparent);
59894
+ letter-spacing: 0.04em;
59895
+ }
59896
+ .route-path {
59897
+ color: var(--t-primary);
59898
+ }
59899
+ .route-arrow {
59900
+ color: var(--t-muted);
59901
+ opacity: 0.5;
59902
+ margin: 0 4px;
59903
+ }
59904
+ .route-handler {
59905
+ color: var(--t-primary);
59906
+ font-weight: 600;
59342
59907
  }
59343
59908
 
59344
- .actions button:hover {
59345
- background: var(--surface-hover, #2a2a2a);
59909
+ /* Action icons */
59910
+ td.actions {
59911
+ text-align: right;
59912
+ white-space: nowrap;
59913
+ width: 1%;
59914
+ }
59915
+ .icon-bar {
59916
+ display: inline-flex;
59917
+ gap: 2px;
59918
+ justify-content: flex-end;
59919
+ opacity: 0.65;
59920
+ transition: opacity 0.15s ease;
59921
+ }
59922
+ tr:hover .icon-bar,
59923
+ .icon-bar:focus-within {
59924
+ opacity: 1;
59925
+ }
59926
+ .icon-btn svg {
59927
+ width: 14px;
59928
+ height: 14px;
59929
+ }
59930
+ .icon-btn {
59931
+ width: 26px;
59932
+ height: 26px;
59933
+ display: inline-flex;
59934
+ align-items: center;
59935
+ justify-content: center;
59936
+ background: transparent;
59937
+ border: 1px solid transparent;
59938
+ color: var(--t-muted);
59939
+ border-radius: var(--radius-xs);
59940
+ cursor: pointer;
59941
+ padding: 0;
59942
+ transition: all 0.15s ease;
59943
+ }
59944
+ .icon-btn:hover {
59945
+ background: var(--bg-glass-strong);
59946
+ color: var(--t-primary);
59947
+ border-color: var(--border-glass);
59948
+ }
59949
+ .icon-btn.danger:hover {
59950
+ color: var(--color-error);
59951
+ }
59952
+ .icon-btn.primary {
59953
+ background: color-mix(in srgb, var(--accent-primary) 16%, transparent);
59954
+ color: var(--accent-primary);
59955
+ border-color: color-mix(in srgb, var(--accent-primary) 30%, var(--border-glass));
59956
+ }
59957
+ .icon-btn.primary:hover {
59958
+ background: color-mix(in srgb, var(--accent-primary) 24%, transparent);
59959
+ }
59960
+ /* Loaded photons chip strip */
59961
+ .chip-strip {
59962
+ display: flex;
59963
+ flex-wrap: wrap;
59964
+ gap: var(--space-xs);
59965
+ }
59966
+ .chip {
59967
+ font-family: 'Azeret Mono', var(--font-mono);
59968
+ font-size: 0.78rem;
59969
+ background: color-mix(in srgb, var(--bg-glass) 70%, transparent);
59970
+ color: var(--t-primary);
59971
+ border: 1px solid var(--border-glass);
59972
+ border-radius: var(--radius-full);
59973
+ padding: 3px 10px 3px 8px;
59974
+ display: inline-flex;
59975
+ align-items: center;
59976
+ gap: 6px;
59977
+ }
59978
+ .chip.idle {
59979
+ color: var(--t-muted);
59980
+ }
59981
+ .chip-dot {
59982
+ width: 6px;
59983
+ height: 6px;
59984
+ border-radius: 50%;
59985
+ background: var(--color-success);
59986
+ box-shadow: 0 0 4px color-mix(in srgb, var(--color-success) 40%, transparent);
59987
+ }
59988
+ .chip.idle .chip-dot {
59989
+ background: var(--t-muted);
59990
+ opacity: 0.4;
59991
+ box-shadow: none;
59992
+ }
59993
+ .conn-badge {
59994
+ background: var(--accent-primary);
59995
+ color: var(--bg-app);
59996
+ font-weight: 700;
59997
+ font-size: 0.66rem;
59998
+ padding: 1px 7px;
59999
+ border-radius: var(--radius-full);
60000
+ letter-spacing: 0.02em;
59346
60001
  }
59347
60002
 
60003
+ /* Status colors (history drawer) */
59348
60004
  .status-success {
59349
- color: var(--success, #4ade80);
60005
+ color: var(--color-success);
59350
60006
  }
59351
60007
  .status-error {
59352
- color: var(--danger, #f87171);
60008
+ color: var(--color-error);
59353
60009
  }
59354
60010
  .status-timeout {
59355
- color: var(--warning, #fbbf24);
60011
+ color: var(--color-warning);
59356
60012
  }
59357
60013
 
59358
60014
  .error {
59359
- background: var(--danger-bg, #3a1b1b);
59360
- color: var(--danger, #f87171);
59361
- padding: var(--space-sm);
59362
- border-radius: 4px;
60015
+ background: color-mix(in srgb, var(--color-error) 12%, transparent);
60016
+ color: var(--color-error);
60017
+ border: 1px solid color-mix(in srgb, var(--color-error) 30%, transparent);
60018
+ padding: var(--space-sm) var(--space-md);
60019
+ border-radius: var(--radius-sm);
59363
60020
  font-size: 0.85rem;
59364
60021
  margin-bottom: var(--space-md);
59365
60022
  }
59366
60023
 
60024
+ /* History drawer */
59367
60025
  .drawer {
59368
60026
  position: fixed;
59369
60027
  top: 0;
59370
60028
  right: 0;
59371
60029
  bottom: 0;
59372
60030
  width: min(520px, 100vw);
59373
- background: var(--surface-primary, #111);
59374
- border-left: 1px solid var(--border, #333);
60031
+ background: var(--bg-elevated, #111);
60032
+ border-left: 1px solid var(--border-glass);
59375
60033
  padding: var(--space-md);
59376
60034
  overflow-y: auto;
59377
60035
  z-index: 9999;
59378
60036
  box-shadow: -4px 0 12px rgba(0, 0, 0, 0.4);
59379
60037
  }
59380
-
59381
60038
  .drawer-header {
59382
60039
  display: flex;
59383
60040
  justify-content: space-between;
59384
60041
  align-items: center;
59385
60042
  margin-bottom: var(--space-md);
59386
60043
  }
59387
-
59388
60044
  .drawer-header button {
59389
60045
  background: transparent;
59390
- border: 1px solid var(--border, #333);
60046
+ border: 1px solid var(--border-glass);
59391
60047
  color: var(--t-primary);
59392
60048
  padding: 4px 10px;
59393
60049
  cursor: pointer;
59394
- border-radius: 3px;
60050
+ border-radius: var(--radius-xs);
60051
+ }
60052
+ .drawer table {
60053
+ width: 100%;
60054
+ border-collapse: separate;
60055
+ border-spacing: 0;
60056
+ font-size: 0.85rem;
60057
+ }
60058
+ .drawer th,
60059
+ .drawer td {
60060
+ padding: 8px 10px;
60061
+ text-align: left;
60062
+ border-bottom: 1px solid var(--border-glass);
60063
+ }
60064
+ .drawer th {
60065
+ font-weight: 600;
60066
+ color: var(--t-muted);
60067
+ font-size: 0.75rem;
60068
+ text-transform: uppercase;
60069
+ letter-spacing: 0.05em;
59395
60070
  }
59396
60071
  `
59397
60072
  ];
@@ -59414,6 +60089,740 @@ DaemonPanel = __decorateClass([
59414
60089
  t4("daemon-panel")
59415
60090
  ], DaemonPanel);
59416
60091
 
60092
+ // src/auto-ui/frontend/components/photon-pulse.ts
60093
+ init_lit();
60094
+ init_decorators();
60095
+ init_theme();
60096
+ var POLL_INTERVAL_MS2 = 5e3;
60097
+ var WEEKDAY_NAMES2 = [
60098
+ "Sunday",
60099
+ "Monday",
60100
+ "Tuesday",
60101
+ "Wednesday",
60102
+ "Thursday",
60103
+ "Friday",
60104
+ "Saturday"
60105
+ ];
60106
+ function formatHour2(h5) {
60107
+ return h5 === 0 ? "12am" : h5 < 12 ? `${h5}am` : h5 === 12 ? "12pm" : `${h5 - 12}pm`;
60108
+ }
60109
+ function describeWeekdays2(spec) {
60110
+ if (spec === "*") return "every day";
60111
+ if (spec === "1-5") return "weekdays";
60112
+ if (spec === "0,6" || spec === "6,0") return "weekends";
60113
+ if (/^\d$/.test(spec)) {
60114
+ const n5 = parseInt(spec, 10);
60115
+ if (n5 >= 0 && n5 <= 6) return WEEKDAY_NAMES2[n5];
60116
+ }
60117
+ return null;
60118
+ }
60119
+ function humanizeCron2(cron) {
60120
+ const trimmed = cron.trim();
60121
+ if (trimmed === "* * * * *") return "Every minute";
60122
+ if (trimmed === "0 * * * *") return "Hourly";
60123
+ if (trimmed === "0 0 * * *") return "Daily at midnight";
60124
+ if (trimmed === "0 12 * * *") return "Daily at noon";
60125
+ if (trimmed === "0 0 1 * *") return "Monthly";
60126
+ let m3 = trimmed.match(/^(?:\*|0)\/(\d+) \* \* \* \*$/);
60127
+ if (m3) return `Every ${m3[1]} minutes`;
60128
+ m3 = trimmed.match(/^0 (?:\*|0)\/(\d+) \* \* \*$/);
60129
+ if (m3) return `Every ${m3[1]} hours`;
60130
+ m3 = trimmed.match(/^0 (\d{1,2}) \* \* \*$/);
60131
+ if (m3) {
60132
+ const h5 = parseInt(m3[1], 10);
60133
+ if (h5 >= 0 && h5 <= 23) return `Daily at ${formatHour2(h5)}`;
60134
+ }
60135
+ m3 = trimmed.match(/^0 (\d{1,2}) \* \* ([0-9,-]+)$/);
60136
+ if (m3) {
60137
+ const h5 = parseInt(m3[1], 10);
60138
+ const dayLabel = describeWeekdays2(m3[2]);
60139
+ if (h5 >= 0 && h5 <= 23 && dayLabel) {
60140
+ const cap = dayLabel[0].toUpperCase() + dayLabel.slice(1);
60141
+ return `${cap} at ${formatHour2(h5)}`;
60142
+ }
60143
+ }
60144
+ return trimmed;
60145
+ }
60146
+ var CRON_PRESETS = [
60147
+ { label: "Every minute", cron: "* * * * *" },
60148
+ { label: "Every 5 min", cron: "*/5 * * * *" },
60149
+ { label: "Hourly", cron: "0 * * * *" },
60150
+ { label: "Daily 9am", cron: "0 9 * * *" },
60151
+ { label: "Weekday mornings", cron: "0 9 * * 1-5" },
60152
+ { label: "Weekly Mon", cron: "0 9 * * 1" },
60153
+ { label: "Monthly 1st", cron: "0 0 1 * *" }
60154
+ ];
60155
+ function isCronShapeValid(cron) {
60156
+ const trimmed = cron.trim();
60157
+ if (!trimmed) return false;
60158
+ const parts = trimmed.split(/\s+/);
60159
+ if (parts.length !== 5) return false;
60160
+ return parts.every((p5) => /^[0-9*/,-]+$/.test(p5));
60161
+ }
60162
+ function formatWhen2(ts) {
60163
+ if (!ts) return "-";
60164
+ const delta = ts - Date.now();
60165
+ const abs = Math.abs(delta);
60166
+ const mins = Math.round(abs / 6e4);
60167
+ const hrs = Math.round(mins / 60);
60168
+ if (abs < 6e4) return delta < 0 ? "just now" : "in <1m";
60169
+ if (mins < 90) return delta < 0 ? `${mins}m ago` : `in ${mins}m`;
60170
+ if (hrs < 48) return delta < 0 ? `${hrs}h ago` : `in ${hrs}h`;
60171
+ return new Date(ts).toISOString().replace("T", " ").slice(0, 16);
60172
+ }
60173
+ var PhotonPulse = class extends i4 {
60174
+ constructor() {
60175
+ super(...arguments);
60176
+ this.photonName = "";
60177
+ this.availableMethods = [];
60178
+ this.activity = [];
60179
+ this._active = [];
60180
+ this._declared = [];
60181
+ this._error = null;
60182
+ this._showAddForm = false;
60183
+ this._formMethod = "";
60184
+ this._formCron = "0 9 * * *";
60185
+ this._formSubmitting = false;
60186
+ this._pollTimer = null;
60187
+ }
60188
+ connectedCallback() {
60189
+ super.connectedCallback();
60190
+ void this._refresh();
60191
+ this._pollTimer = setInterval(() => void this._refresh(), POLL_INTERVAL_MS2);
60192
+ }
60193
+ disconnectedCallback() {
60194
+ super.disconnectedCallback();
60195
+ if (this._pollTimer) {
60196
+ clearInterval(this._pollTimer);
60197
+ this._pollTimer = null;
60198
+ }
60199
+ }
60200
+ updated(changed) {
60201
+ if (changed.has("photonName")) {
60202
+ void this._refresh();
60203
+ }
60204
+ }
60205
+ async _refresh() {
60206
+ if (!this.photonName) return;
60207
+ try {
60208
+ const res = await fetch("/api/daemon/ps", { signal: AbortSignal.timeout(1e4) });
60209
+ if (!res.ok) {
60210
+ this._error = `HTTP ${res.status}`;
60211
+ return;
60212
+ }
60213
+ const snap = await res.json();
60214
+ this._active = (snap.active ?? []).filter((r7) => r7.photon === this.photonName);
60215
+ this._declared = (snap.declared ?? []).filter(
60216
+ (r7) => r7.photon === this.photonName && !r7.active
60217
+ );
60218
+ this._error = null;
60219
+ } catch (err) {
60220
+ this._error = err instanceof Error ? err.message : String(err);
60221
+ }
60222
+ }
60223
+ async _addManual() {
60224
+ const method = this._formMethod.trim();
60225
+ const cron = this._formCron.trim();
60226
+ if (!method) {
60227
+ showToast("Pick a method first", "error");
60228
+ return;
60229
+ }
60230
+ if (!cron) {
60231
+ showToast("Cron expression is required", "error");
60232
+ return;
60233
+ }
60234
+ this._formSubmitting = true;
60235
+ try {
60236
+ const res = await fetch("/api/daemon/schedules/add", {
60237
+ method: "POST",
60238
+ headers: {
60239
+ "Content-Type": "application/json",
60240
+ "X-Photon-Request": "1"
60241
+ },
60242
+ body: JSON.stringify({ photon: this.photonName, method, cron }),
60243
+ signal: AbortSignal.timeout(1e4)
60244
+ });
60245
+ const body = await res.json().catch(() => ({}));
60246
+ if (!res.ok) {
60247
+ showToast(body.error || "Failed to add schedule", "error");
60248
+ return;
60249
+ }
60250
+ showToast(`Scheduled ${this.photonName}:${method}`, "success");
60251
+ this._showAddForm = false;
60252
+ this._formMethod = "";
60253
+ this._formCron = "0 9 * * *";
60254
+ await this._refresh();
60255
+ } catch (err) {
60256
+ showToast(err instanceof Error ? err.message : String(err), "error");
60257
+ } finally {
60258
+ this._formSubmitting = false;
60259
+ }
60260
+ }
60261
+ async _action(op, photon, method) {
60262
+ try {
60263
+ const res = await fetch(`/api/daemon/schedules/${op}`, {
60264
+ method: "POST",
60265
+ headers: {
60266
+ "Content-Type": "application/json",
60267
+ "X-Photon-Request": "1"
60268
+ },
60269
+ body: JSON.stringify({ photon, method }),
60270
+ signal: AbortSignal.timeout(1e4)
60271
+ });
60272
+ const body = await res.json().catch(() => ({}));
60273
+ if (!res.ok) {
60274
+ showToast(body.error || `Action "${op}" failed`, "error");
60275
+ return;
60276
+ }
60277
+ showToast(`${op}d ${photon}:${method}`, "success");
60278
+ await this._refresh();
60279
+ } catch (err) {
60280
+ showToast(err instanceof Error ? err.message : String(err), "error");
60281
+ }
60282
+ }
60283
+ _renderAddForm() {
60284
+ const usedNames = /* @__PURE__ */ new Set([
60285
+ ...this._active.map((r7) => r7.method),
60286
+ ...this._declared.map((r7) => r7.method)
60287
+ ]);
60288
+ const candidates = this.availableMethods.filter((m3) => !usedNames.has(m3));
60289
+ if (this._formMethod && !candidates.includes(this._formMethod)) {
60290
+ candidates.unshift(this._formMethod);
60291
+ }
60292
+ const trimmedCron = this._formCron.trim();
60293
+ const cronShapeValid = isCronShapeValid(trimmedCron);
60294
+ const humanized = cronShapeValid ? humanizeCron2(trimmedCron) : "";
60295
+ const cronHasFriendlyName = humanized && humanized !== trimmedCron;
60296
+ const canSubmit = !!this._formMethod && cronShapeValid && !this._formSubmitting;
60297
+ return b2`
60298
+ <div class="add-form" role="group" aria-label="Add schedule">
60299
+ <div class="field">
60300
+ <span class="field-label">Method</span>
60301
+ <select
60302
+ aria-label="Method"
60303
+ .value=${this._formMethod}
60304
+ @change=${(e8) => this._formMethod = e8.target.value}
60305
+ >
60306
+ <option value="" disabled ?selected=${!this._formMethod}>Pick a method…</option>
60307
+ ${candidates.map(
60308
+ (m3) => b2`<option value=${m3} ?selected=${m3 === this._formMethod}>${m3}</option>`
60309
+ )}
60310
+ </select>
60311
+ </div>
60312
+
60313
+ <div class="field">
60314
+ <span class="field-label">Preset</span>
60315
+ <div class="preset-row" role="group" aria-label="Cron presets">
60316
+ ${CRON_PRESETS.map(
60317
+ (p5) => b2`
60318
+ <button
60319
+ type="button"
60320
+ class="preset-chip ${trimmedCron === p5.cron ? "active" : ""}"
60321
+ title="${p5.cron}"
60322
+ @click=${() => this._formCron = p5.cron}
60323
+ >
60324
+ ${p5.label}
60325
+ </button>
60326
+ `
60327
+ )}
60328
+ </div>
60329
+ </div>
60330
+
60331
+ <div class="field">
60332
+ <span class="field-label">Cron</span>
60333
+ <input
60334
+ aria-label="Cron expression"
60335
+ type="text"
60336
+ placeholder="0 9 * * *"
60337
+ class=${trimmedCron && !cronShapeValid ? "invalid" : ""}
60338
+ spellcheck="false"
60339
+ autocomplete="off"
60340
+ .value=${this._formCron}
60341
+ @input=${(e8) => this._formCron = e8.target.value}
60342
+ @keydown=${(e8) => {
60343
+ if (e8.key === "Enter" && canSubmit) void this._addManual();
60344
+ if (e8.key === "Escape") this._showAddForm = false;
60345
+ }}
60346
+ />
60347
+ </div>
60348
+
60349
+ <div
60350
+ class="preview-line ${trimmedCron && !cronShapeValid ? "error" : ""}"
60351
+ role="status"
60352
+ aria-live="polite"
60353
+ >
60354
+ ${!trimmedCron ? b2`<span>Pick a preset or type a cron expression.</span>` : !cronShapeValid ? b2`<span>Cron must have 5 fields: minute hour day month weekday.</span>` : cronHasFriendlyName ? b2`<span class="preview-arrow">→</span>
60355
+ <span class="preview-text">${humanized}</span>` : b2`<span class="preview-arrow">→</span>
60356
+ <span class="preview-text">${trimmedCron}</span>
60357
+ <span>(custom schedule)</span>`}
60358
+ </div>
60359
+
60360
+ <div class="add-actions">
60361
+ <span class="add-hint">
60362
+ Need help?
60363
+ <a href="https://crontab.guru/" target="_blank" rel="noopener">crontab.guru</a>
60364
+ </span>
60365
+ <span style="display: flex; gap: 6px;">
60366
+ <button
60367
+ ?disabled=${this._formSubmitting}
60368
+ @click=${() => {
60369
+ this._showAddForm = false;
60370
+ }}
60371
+ >
60372
+ Cancel
60373
+ </button>
60374
+ <button class="primary" ?disabled=${!canSubmit} @click=${() => this._addManual()}>
60375
+ ${this._formSubmitting ? "Saving\u2026" : "Save"}
60376
+ </button>
60377
+ </span>
60378
+ </div>
60379
+ </div>
60380
+ `;
60381
+ }
60382
+ _renderSchedules() {
60383
+ const total = this._active.length + this._declared.length;
60384
+ return b2`
60385
+ <div class="section">
60386
+ <div class="section-header">
60387
+ <h3 class="section-title">
60388
+ Schedules ${total > 0 ? b2`<span class="section-count">${total}</span>` : ""}
60389
+ </h3>
60390
+ ${this._showAddForm ? "" : b2`<button
60391
+ class="add-btn"
60392
+ title="Add a manual cron schedule for any method"
60393
+ @click=${() => {
60394
+ this._showAddForm = true;
60395
+ if (!this._formMethod) {
60396
+ const used = /* @__PURE__ */ new Set([
60397
+ ...this._active.map((r7) => r7.method),
60398
+ ...this._declared.map((r7) => r7.method)
60399
+ ]);
60400
+ this._formMethod = this.availableMethods.find((m3) => !used.has(m3)) ?? this.availableMethods[0] ?? "";
60401
+ }
60402
+ }}
60403
+ >
60404
+ + Add schedule
60405
+ </button>`}
60406
+ </div>
60407
+ ${this._showAddForm ? this._renderAddForm() : ""}
60408
+ ${total === 0 && !this._showAddForm ? b2`<div class="empty-hint" role="status">
60409
+ No scheduled work for ${this.photonName}. Add a
60410
+ <code>@scheduled &lt;cron&gt;</code> JSDoc tag to a method, or use the "+ Add
60411
+ schedule" button above to set one up manually.
60412
+ </div>` : total === 0 ? "" : b2`<div class="schedule-list">
60413
+ ${this._active.map(
60414
+ (r7) => b2`
60415
+ <div class="schedule-row" role="listitem">
60416
+ <span class="schedule-method" title="${r7.method}">
60417
+ ${formatLabel(r7.method)}
60418
+ ${r7.createdBy === "manual" ? b2`<span class="manual-badge" title="Added manually via Pulse"
60419
+ >manual</span
60420
+ >` : ""}
60421
+ </span>
60422
+ <span class="schedule-cron" title="${r7.cron}">${humanizeCron2(r7.cron)}</span>
60423
+ <span class="schedule-next">next: ${formatWhen2(r7.nextRun)}</span>
60424
+ <span class="schedule-actions">
60425
+ <button @click=${() => this._action("pause", r7.photon, r7.method)}>
60426
+ Pause
60427
+ </button>
60428
+ <button @click=${() => this._action("disable", r7.photon, r7.method)}>
60429
+ Disable
60430
+ </button>
60431
+ </span>
60432
+ </div>
60433
+ `
60434
+ )}
60435
+ ${this._declared.map(
60436
+ (r7) => b2`
60437
+ <div class="schedule-row dormant" role="listitem">
60438
+ <span class="schedule-method" title="${r7.method}"
60439
+ >${formatLabel(r7.method)}</span
60440
+ >
60441
+ <span class="schedule-cron" title="${r7.cron}">${humanizeCron2(r7.cron)}</span>
60442
+ <span class="schedule-next">declared, not enrolled</span>
60443
+ <span class="schedule-actions">
60444
+ <button
60445
+ class="primary"
60446
+ @click=${() => this._action("enable", r7.photon, r7.method)}
60447
+ >
60448
+ Enable
60449
+ </button>
60450
+ </span>
60451
+ </div>
60452
+ `
60453
+ )}
60454
+ </div>`}
60455
+ </div>
60456
+ `;
60457
+ }
60458
+ render() {
60459
+ return b2`
60460
+ ${this._renderSchedules()}
60461
+ <div class="section activity">
60462
+ <activity-log
60463
+ .items=${this.activity}
60464
+ .filter=${this.photonName}
60465
+ .fullscreen=${true}
60466
+ @clear=${() => this.dispatchEvent(
60467
+ new CustomEvent("clear-activity", { bubbles: true, composed: true })
60468
+ )}
60469
+ ></activity-log>
60470
+ </div>
60471
+ `;
60472
+ }
60473
+ };
60474
+ PhotonPulse.styles = [
60475
+ theme,
60476
+ i`
60477
+ :host {
60478
+ display: flex;
60479
+ flex-direction: column;
60480
+ flex: 1;
60481
+ min-height: 0;
60482
+ gap: var(--space-lg);
60483
+ }
60484
+
60485
+ .section {
60486
+ display: flex;
60487
+ flex-direction: column;
60488
+ gap: var(--space-sm);
60489
+ min-height: 0;
60490
+ }
60491
+
60492
+ .section.activity {
60493
+ flex: 1;
60494
+ min-height: 0;
60495
+ }
60496
+
60497
+ .section-header {
60498
+ display: flex;
60499
+ align-items: center;
60500
+ justify-content: space-between;
60501
+ gap: var(--space-sm);
60502
+ padding: 0 var(--space-xs);
60503
+ }
60504
+
60505
+ .section-title {
60506
+ margin: 0;
60507
+ font-size: var(--text-sm);
60508
+ text-transform: uppercase;
60509
+ letter-spacing: 0.1em;
60510
+ color: var(--t-primary);
60511
+ font-weight: 600;
60512
+ }
60513
+
60514
+ .section-count {
60515
+ color: var(--t-muted);
60516
+ font-size: var(--text-xs);
60517
+ font-weight: 500;
60518
+ letter-spacing: normal;
60519
+ text-transform: none;
60520
+ }
60521
+
60522
+ .schedule-list {
60523
+ display: flex;
60524
+ flex-direction: column;
60525
+ gap: var(--space-xs);
60526
+ }
60527
+
60528
+ .schedule-row {
60529
+ display: grid;
60530
+ grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, auto) auto;
60531
+ align-items: center;
60532
+ gap: var(--space-md);
60533
+ padding: var(--space-sm) var(--space-md);
60534
+ background: var(--bg-glass);
60535
+ border: 1px solid var(--border-glass);
60536
+ border-radius: var(--radius-sm);
60537
+ }
60538
+
60539
+ .schedule-row.dormant {
60540
+ opacity: 0.7;
60541
+ }
60542
+
60543
+ .schedule-method {
60544
+ color: var(--t-primary);
60545
+ font-weight: 500;
60546
+ overflow: hidden;
60547
+ text-overflow: ellipsis;
60548
+ white-space: nowrap;
60549
+ display: inline-flex;
60550
+ align-items: center;
60551
+ gap: 6px;
60552
+ }
60553
+
60554
+ .manual-badge {
60555
+ font-size: 9px;
60556
+ text-transform: uppercase;
60557
+ letter-spacing: 0.08em;
60558
+ padding: 1px 6px;
60559
+ border-radius: 999px;
60560
+ background: color-mix(in srgb, var(--accent-primary) 18%, transparent);
60561
+ border: 1px solid color-mix(in srgb, var(--accent-primary) 36%, var(--border-glass));
60562
+ color: var(--t-primary);
60563
+ font-weight: 600;
60564
+ }
60565
+
60566
+ .schedule-cron {
60567
+ color: var(--t-muted);
60568
+ font-size: var(--text-sm);
60569
+ font-family: var(--font-mono);
60570
+ }
60571
+
60572
+ .schedule-next {
60573
+ color: var(--t-muted);
60574
+ font-size: var(--text-sm);
60575
+ font-variant-numeric: tabular-nums;
60576
+ }
60577
+
60578
+ .schedule-actions {
60579
+ display: flex;
60580
+ gap: 4px;
60581
+ justify-self: end;
60582
+ }
60583
+
60584
+ .schedule-actions button {
60585
+ font-size: var(--text-xs);
60586
+ padding: 3px 10px;
60587
+ border: 1px solid var(--border-glass);
60588
+ background: transparent;
60589
+ color: var(--t-primary);
60590
+ cursor: pointer;
60591
+ border-radius: var(--radius-xs);
60592
+ transition: all 0.15s ease;
60593
+ }
60594
+
60595
+ .schedule-actions button:hover {
60596
+ background: var(--bg-glass-strong);
60597
+ border-color: color-mix(in srgb, var(--accent-primary) 38%, var(--border-glass));
60598
+ }
60599
+
60600
+ .schedule-actions button.primary {
60601
+ background: color-mix(in srgb, var(--accent-primary) 18%, var(--bg-glass));
60602
+ border-color: color-mix(in srgb, var(--accent-primary) 40%, var(--border-glass));
60603
+ }
60604
+
60605
+ .empty-hint {
60606
+ color: var(--t-muted);
60607
+ font-size: var(--text-sm);
60608
+ padding: var(--space-md) var(--space-md);
60609
+ text-align: center;
60610
+ border: 1px dashed var(--border-glass);
60611
+ border-radius: var(--radius-sm);
60612
+ background: color-mix(in srgb, var(--bg-glass) 50%, transparent);
60613
+ }
60614
+
60615
+ .add-btn {
60616
+ font-size: var(--text-xs);
60617
+ padding: 3px 10px;
60618
+ border: 1px solid var(--border-glass);
60619
+ background: color-mix(in srgb, var(--accent-primary) 14%, var(--bg-glass));
60620
+ color: var(--t-primary);
60621
+ cursor: pointer;
60622
+ border-radius: var(--radius-xs);
60623
+ transition: all 0.15s ease;
60624
+ }
60625
+
60626
+ .add-btn:hover {
60627
+ background: color-mix(in srgb, var(--accent-primary) 22%, var(--bg-glass));
60628
+ border-color: color-mix(in srgb, var(--accent-primary) 50%, var(--border-glass));
60629
+ }
60630
+
60631
+ .add-form {
60632
+ display: flex;
60633
+ flex-direction: column;
60634
+ gap: var(--space-sm);
60635
+ padding: var(--space-md);
60636
+ background: color-mix(in srgb, var(--accent-primary) 6%, var(--bg-glass));
60637
+ border: 1px solid color-mix(in srgb, var(--accent-primary) 28%, var(--border-glass));
60638
+ border-radius: var(--radius-sm);
60639
+ }
60640
+
60641
+ .field {
60642
+ display: grid;
60643
+ grid-template-columns: 90px 1fr;
60644
+ align-items: center;
60645
+ gap: var(--space-sm);
60646
+ }
60647
+
60648
+ .field-label {
60649
+ font-size: var(--text-xs);
60650
+ color: var(--t-muted);
60651
+ text-transform: uppercase;
60652
+ letter-spacing: 0.08em;
60653
+ }
60654
+
60655
+ .add-form select,
60656
+ .add-form input {
60657
+ font: inherit;
60658
+ font-size: var(--text-sm);
60659
+ color: var(--t-primary);
60660
+ background: var(--bg-elevated, #0b1018);
60661
+ border: 1px solid var(--border-glass);
60662
+ border-radius: var(--radius-xs);
60663
+ padding: 5px 8px;
60664
+ width: 100%;
60665
+ box-sizing: border-box;
60666
+ }
60667
+
60668
+ .add-form input {
60669
+ font-family: var(--font-mono);
60670
+ }
60671
+
60672
+ .add-form select:focus,
60673
+ .add-form input:focus {
60674
+ outline: none;
60675
+ border-color: color-mix(in srgb, var(--accent-primary) 60%, var(--border-glass));
60676
+ }
60677
+
60678
+ .add-form input.invalid {
60679
+ border-color: color-mix(in srgb, var(--color-warning, #ef4444) 60%, var(--border-glass));
60680
+ }
60681
+
60682
+ .preset-row {
60683
+ display: flex;
60684
+ flex-wrap: wrap;
60685
+ gap: 6px;
60686
+ }
60687
+
60688
+ .preset-chip {
60689
+ font-size: var(--text-xs);
60690
+ padding: 3px 10px;
60691
+ border: 1px solid var(--border-glass);
60692
+ background: var(--bg-elevated, #0b1018);
60693
+ color: var(--t-primary);
60694
+ cursor: pointer;
60695
+ border-radius: 999px;
60696
+ transition: all 0.15s ease;
60697
+ font-family: var(--font-mono);
60698
+ }
60699
+
60700
+ .preset-chip:hover {
60701
+ background: color-mix(in srgb, var(--accent-primary) 14%, var(--bg-elevated));
60702
+ border-color: color-mix(in srgb, var(--accent-primary) 38%, var(--border-glass));
60703
+ }
60704
+
60705
+ .preset-chip.active {
60706
+ background: color-mix(in srgb, var(--accent-primary) 22%, var(--bg-elevated));
60707
+ border-color: color-mix(in srgb, var(--accent-primary) 55%, var(--border-glass));
60708
+ color: var(--accent-primary);
60709
+ }
60710
+
60711
+ .preview-line {
60712
+ font-size: var(--text-xs);
60713
+ color: var(--t-muted);
60714
+ display: flex;
60715
+ align-items: center;
60716
+ gap: 6px;
60717
+ padding-left: calc(90px + var(--space-sm));
60718
+ min-height: 18px;
60719
+ }
60720
+
60721
+ .preview-line.error {
60722
+ color: var(--color-warning, #ef4444);
60723
+ }
60724
+
60725
+ .preview-arrow {
60726
+ opacity: 0.5;
60727
+ }
60728
+
60729
+ .preview-text {
60730
+ color: var(--t-primary);
60731
+ font-weight: 500;
60732
+ }
60733
+
60734
+ .add-actions {
60735
+ display: flex;
60736
+ justify-content: space-between;
60737
+ align-items: center;
60738
+ gap: var(--space-sm);
60739
+ }
60740
+
60741
+ .add-form button {
60742
+ font-size: var(--text-xs);
60743
+ padding: 5px 14px;
60744
+ border: 1px solid var(--border-glass);
60745
+ background: transparent;
60746
+ color: var(--t-primary);
60747
+ cursor: pointer;
60748
+ border-radius: var(--radius-xs);
60749
+ transition: all 0.15s ease;
60750
+ }
60751
+
60752
+ .add-form button.primary {
60753
+ background: color-mix(in srgb, var(--accent-primary) 18%, var(--bg-glass));
60754
+ border-color: color-mix(in srgb, var(--accent-primary) 40%, var(--border-glass));
60755
+ }
60756
+
60757
+ .add-form button:hover:not([disabled]) {
60758
+ background: var(--bg-glass-strong);
60759
+ }
60760
+
60761
+ .add-form button[disabled] {
60762
+ opacity: 0.5;
60763
+ cursor: not-allowed;
60764
+ }
60765
+
60766
+ .add-hint {
60767
+ font-size: var(--text-xs);
60768
+ color: var(--t-muted);
60769
+ }
60770
+
60771
+ .add-hint a {
60772
+ color: var(--accent-primary);
60773
+ text-decoration: none;
60774
+ }
60775
+
60776
+ .add-hint a:hover {
60777
+ text-decoration: underline;
60778
+ }
60779
+
60780
+ @media (max-width: 640px) {
60781
+ .schedule-row {
60782
+ grid-template-columns: 1fr;
60783
+ gap: var(--space-xs);
60784
+ }
60785
+
60786
+ .schedule-actions {
60787
+ justify-self: start;
60788
+ }
60789
+ }
60790
+ `
60791
+ ];
60792
+ __decorateClass([
60793
+ n4({ type: String })
60794
+ ], PhotonPulse.prototype, "photonName", 2);
60795
+ __decorateClass([
60796
+ n4({ type: Array })
60797
+ ], PhotonPulse.prototype, "availableMethods", 2);
60798
+ __decorateClass([
60799
+ n4({ type: Array })
60800
+ ], PhotonPulse.prototype, "activity", 2);
60801
+ __decorateClass([
60802
+ r5()
60803
+ ], PhotonPulse.prototype, "_active", 2);
60804
+ __decorateClass([
60805
+ r5()
60806
+ ], PhotonPulse.prototype, "_declared", 2);
60807
+ __decorateClass([
60808
+ r5()
60809
+ ], PhotonPulse.prototype, "_error", 2);
60810
+ __decorateClass([
60811
+ r5()
60812
+ ], PhotonPulse.prototype, "_showAddForm", 2);
60813
+ __decorateClass([
60814
+ r5()
60815
+ ], PhotonPulse.prototype, "_formMethod", 2);
60816
+ __decorateClass([
60817
+ r5()
60818
+ ], PhotonPulse.prototype, "_formCron", 2);
60819
+ __decorateClass([
60820
+ r5()
60821
+ ], PhotonPulse.prototype, "_formSubmitting", 2);
60822
+ PhotonPulse = __decorateClass([
60823
+ t4("photon-pulse")
60824
+ ], PhotonPulse);
60825
+
59417
60826
  // src/auto-ui/frontend/components/custom-ui-renderer.ts
59418
60827
  init_lit();
59419
60828
  init_decorators();