@enigmax/dashboard 0.1.95 → 0.1.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/index.html +499 -29
- package/package.json +1 -1
package/assets/index.html
CHANGED
|
@@ -52,7 +52,9 @@
|
|
|
52
52
|
two-class form to win: a bare .side-x would lose to it on source order. */
|
|
53
53
|
.iconbtn.side-x { display: none; }
|
|
54
54
|
.side-foot { margin-top: auto; padding: 12px 6px 0; border-top: 1px solid var(--border-soft); }
|
|
55
|
-
|
|
55
|
+
/* Auto side margins, not just a max-width: past 1560px + the rail the column would
|
|
56
|
+
otherwise stay glued to the sidebar with all the empty space piled on the right. */
|
|
57
|
+
.content { flex: 1; min-width: 0; padding: 30px 30px 48px; max-width: 1560px; margin-inline: auto; }
|
|
56
58
|
.iconbtn.nav-burger { display: none; }
|
|
57
59
|
.nav-scrim { position: fixed; inset: 0; z-index: 25; background: rgba(0,0,0,0.6); }
|
|
58
60
|
a { color: var(--accent2); }
|
|
@@ -85,12 +87,27 @@
|
|
|
85
87
|
/* Sections as a vertical rail: the sidebar is already the surface, so the active
|
|
86
88
|
entry is a filled pill and the rest are plain rows. */
|
|
87
89
|
.tabs { display: flex; flex-direction: column; gap: 2px; }
|
|
90
|
+
/* Twelve flat entries were a wall of similar words, so they are grouped and iconified
|
|
91
|
+
(frontend-policy, "Navigation Is Structured"). The group label is a quiet caption,
|
|
92
|
+
not another bordered box - the rail is already the surface. */
|
|
93
|
+
.nav-group { display: flex; flex-direction: column; gap: 2px; }
|
|
94
|
+
.nav-group[hidden] { display: none; }
|
|
95
|
+
.nav-group + .nav-group { margin-top: 13px; }
|
|
96
|
+
.nav-group.nav-tail { margin-top: 20px; }
|
|
97
|
+
.nav-group-t {
|
|
98
|
+
font-family: var(--mono); font-size: 9.5px; font-weight: 600; text-transform: uppercase;
|
|
99
|
+
letter-spacing: 1.5px; color: var(--muted); opacity: .6; padding: 0 12px; margin-bottom: 3px;
|
|
100
|
+
}
|
|
88
101
|
.tabs .tab {
|
|
102
|
+
display: flex; align-items: center; gap: 9px;
|
|
89
103
|
font-family: var(--mono); text-decoration: none; color: var(--muted); font-size: 12.5px; font-weight: 600;
|
|
90
104
|
padding: 8px 12px; border-radius: 9px; border: 1px solid transparent; transition: background .15s, color .15s, border-color .15s;
|
|
91
105
|
}
|
|
106
|
+
/* The label carries the meaning here (unlike an icon action), so the glyph is decorative. */
|
|
107
|
+
.tab-ic { width: 15px; height: 15px; flex: 0 0 15px; opacity: .8; }
|
|
92
108
|
.tabs .tab:hover { color: var(--text); background: rgba(255,255,255,0.03); }
|
|
93
109
|
.tabs .tab.active { color: var(--accent); background: var(--surface2); border-color: var(--border); }
|
|
110
|
+
.tabs .tab.active .tab-ic { opacity: 1; }
|
|
94
111
|
.live { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 12px; font-family: var(--mono); padding: 5px 12px; background: rgba(0,0,0,0.25); border: 1px solid var(--border-soft); border-radius: 999px; }
|
|
95
112
|
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--good); box-shadow: 0 0 8px var(--good); }
|
|
96
113
|
.dot.stale { background: var(--muted); box-shadow: none; }
|
|
@@ -168,6 +185,8 @@
|
|
|
168
185
|
.set-cat { margin-bottom: 18px; }
|
|
169
186
|
.set-cat:last-child { margin-bottom: 0; }
|
|
170
187
|
.set-cat-h { font-family: var(--mono); color: var(--accent); font-size: 12px; text-transform: uppercase; letter-spacing: 0.7px; margin-bottom: 4px; }
|
|
188
|
+
/* A path in a header is a literal: uppercasing it prints something that does not exist. */
|
|
189
|
+
.set-cat-h code { text-transform: none; letter-spacing: 0; }
|
|
171
190
|
.set-cat-b { color: var(--muted); font-size: 12px; margin-bottom: 8px; }
|
|
172
191
|
.set-row { display: flex; align-items: center; gap: 14px; padding: 9px 0; border-bottom: 1px solid var(--surface2); }
|
|
173
192
|
.set-row:last-child { border-bottom: none; }
|
|
@@ -560,6 +579,51 @@
|
|
|
560
579
|
}
|
|
561
580
|
.modal-back.show .modal { transform: none; opacity: 1; }
|
|
562
581
|
.modal.wide { max-width: 900px; }
|
|
582
|
+
/* Command palette: the same backdrop and surface as every other dialog, sat in the top
|
|
583
|
+
third so the result list grows downwards without moving the input under the caret. */
|
|
584
|
+
/* The palette lives in the markup instead of being created per open, so it needs this:
|
|
585
|
+
`display: flex` above beats the UA's `[hidden] { display: none }` on specificity, and a
|
|
586
|
+
closed backdrop would otherwise sit invisibly over the page eating every click. */
|
|
587
|
+
.modal-back[hidden] { display: none; }
|
|
588
|
+
.modal-back.pal-back { align-items: flex-start; padding-top: 12vh; }
|
|
589
|
+
.modal.pal { max-width: 520px; padding: 12px; }
|
|
590
|
+
.pal-input {
|
|
591
|
+
width: 100%; background: transparent; color: var(--text); border: none; border-bottom: 1px solid var(--border);
|
|
592
|
+
padding: 8px 6px 12px; font-size: 14px; font-family: var(--mono);
|
|
593
|
+
}
|
|
594
|
+
.pal-input:focus { outline: none; }
|
|
595
|
+
.pal-input::-webkit-search-cancel-button { display: none; }
|
|
596
|
+
.pal-list { max-height: 46vh; overflow-y: auto; overscroll-behavior: contain; margin-top: 8px; }
|
|
597
|
+
.pal-gt {
|
|
598
|
+
font-family: var(--mono); font-size: 9.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.4px;
|
|
599
|
+
color: var(--muted); opacity: .6; padding: 10px 8px 4px;
|
|
600
|
+
}
|
|
601
|
+
.pal-row {
|
|
602
|
+
display: flex; align-items: center; gap: 10px; padding: 8px 8px; border-radius: 8px;
|
|
603
|
+
cursor: pointer; color: var(--muted); font-size: 13px;
|
|
604
|
+
}
|
|
605
|
+
.pal-row .tab-ic { opacity: .75; }
|
|
606
|
+
.pal-row .pal-name { font-family: var(--mono); font-weight: 600; color: var(--text); }
|
|
607
|
+
.pal-row .pal-hint { margin-left: auto; font-family: var(--mono); font-size: 11px; opacity: .6; }
|
|
608
|
+
.pal-row.sel { background: var(--surface2); color: var(--text); }
|
|
609
|
+
.pal-row.sel .tab-ic { opacity: 1; color: var(--accent); }
|
|
610
|
+
.pal-empty { padding: 18px 8px; color: var(--muted); font-size: 13px; }
|
|
611
|
+
.pal-foot {
|
|
612
|
+
display: flex; gap: 14px; padding: 10px 8px 2px; border-top: 1px solid var(--border-soft);
|
|
613
|
+
font-family: var(--mono); font-size: 10.5px; color: var(--muted); opacity: .7;
|
|
614
|
+
}
|
|
615
|
+
.kbd {
|
|
616
|
+
font-family: var(--mono); font-size: 10px; padding: 1px 5px; border: 1px solid var(--border);
|
|
617
|
+
border-radius: 4px; background: var(--surface2);
|
|
618
|
+
}
|
|
619
|
+
/* The shortcut has to be discoverable, so the palette also has a clickable entry point. */
|
|
620
|
+
.cmdk {
|
|
621
|
+
display: inline-flex; align-items: center; gap: 8px; cursor: pointer; color: var(--muted);
|
|
622
|
+
background: rgba(0,0,0,0.25); border: 1px solid var(--border-soft); border-radius: 999px;
|
|
623
|
+
padding: 5px 8px 5px 12px; font-family: var(--mono); font-size: 12px;
|
|
624
|
+
}
|
|
625
|
+
.cmdk:hover { color: var(--text); border-color: var(--border); }
|
|
626
|
+
.cmdk svg { width: 13px; height: 13px; }
|
|
563
627
|
.as-top { display: flex; gap: 8px; margin: 14px 0 6px; }
|
|
564
628
|
.as-list { max-height: 50vh; overflow: auto; border: 1px solid var(--border); border-radius: 8px; padding: 4px 12px; }
|
|
565
629
|
.as-row { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--surface2); cursor: pointer; }
|
|
@@ -642,18 +706,68 @@
|
|
|
642
706
|
</button>
|
|
643
707
|
</div>
|
|
644
708
|
<nav class="tabs">
|
|
645
|
-
<
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
<
|
|
709
|
+
<div class="nav-group" role="group" aria-labelledby="ng-overview">
|
|
710
|
+
<div class="nav-group-t" id="ng-overview">Overview</div>
|
|
711
|
+
<a href="#/" data-view="savings" class="tab active">
|
|
712
|
+
<svg class="tab-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>
|
|
713
|
+
<span>Savings</span>
|
|
714
|
+
</a>
|
|
715
|
+
<a href="#/usage" data-view="usage" class="tab">
|
|
716
|
+
<svg class="tab-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
|
717
|
+
<span>Usage</span>
|
|
718
|
+
</a>
|
|
719
|
+
</div>
|
|
720
|
+
<div class="nav-group" role="group" aria-labelledby="ng-agents">
|
|
721
|
+
<div class="nav-group-t" id="ng-agents">Agents</div>
|
|
722
|
+
<a href="#/accounts" data-view="accounts" class="tab">
|
|
723
|
+
<svg class="tab-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
|
724
|
+
<span>Accounts</span>
|
|
725
|
+
</a>
|
|
726
|
+
<a href="#/skills" data-view="skills" class="tab">
|
|
727
|
+
<svg class="tab-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z"/></svg>
|
|
728
|
+
<span>Skills</span>
|
|
729
|
+
</a>
|
|
730
|
+
<a href="#/packs" data-view="packs" class="tab">
|
|
731
|
+
<svg class="tab-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7.5 4.27 9 5.15"/><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>
|
|
732
|
+
<span>Packs</span>
|
|
733
|
+
</a>
|
|
734
|
+
<a href="#/playground" data-view="playground" class="tab">
|
|
735
|
+
<svg class="tab-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4 17 6-6-6-6"/><path d="M12 19h8"/></svg>
|
|
736
|
+
<span>Playground</span>
|
|
737
|
+
</a>
|
|
738
|
+
</div>
|
|
739
|
+
<div class="nav-group" role="group" aria-labelledby="ng-code">
|
|
740
|
+
<div class="nav-group-t" id="ng-code">Code</div>
|
|
741
|
+
<a href="#/gate" data-view="gate" class="tab">
|
|
742
|
+
<svg class="tab-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg>
|
|
743
|
+
<span>Quality gate</span>
|
|
744
|
+
</a>
|
|
745
|
+
<a href="#/codegraph" data-view="codegraph" class="tab">
|
|
746
|
+
<svg class="tab-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 13.5 6.8 4"/><path d="m15.4 6.5-6.8 4"/></svg>
|
|
747
|
+
<span>Code graph</span>
|
|
748
|
+
</a>
|
|
749
|
+
<a href="#/recall" data-view="recall" class="tab">
|
|
750
|
+
<svg class="tab-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></svg>
|
|
751
|
+
<span>Recall</span>
|
|
752
|
+
</a>
|
|
753
|
+
</div>
|
|
754
|
+
<div class="nav-group" role="group" aria-labelledby="ng-system">
|
|
755
|
+
<div class="nav-group-t" id="ng-system">System</div>
|
|
756
|
+
<a href="#/resources" data-view="resources" class="tab">
|
|
757
|
+
<svg class="tab-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="16" height="16" x="4" y="4" rx="2"/><rect width="6" height="6" x="9" y="9" rx="1"/><path d="M15 2v2"/><path d="M15 20v2"/><path d="M2 15h2"/><path d="M2 9h2"/><path d="M20 15h2"/><path d="M20 9h2"/><path d="M9 2v2"/><path d="M9 20v2"/></svg>
|
|
758
|
+
<span>Resources</span>
|
|
759
|
+
</a>
|
|
760
|
+
<a href="#/ssh" data-view="ssh" class="tab">
|
|
761
|
+
<svg class="tab-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/></svg>
|
|
762
|
+
<span>SSH</span>
|
|
763
|
+
</a>
|
|
764
|
+
</div>
|
|
765
|
+
<div class="nav-group nav-tail">
|
|
766
|
+
<a href="#/settings" data-view="settings" class="tab">
|
|
767
|
+
<svg class="tab-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 4h-7"/><path d="M10 4H3"/><path d="M21 12h-9"/><path d="M8 12H3"/><path d="M21 20h-5"/><path d="M12 20H3"/><path d="M14 2v4"/><path d="M8 10v4"/><path d="M16 18v4"/></svg>
|
|
768
|
+
<span>Settings</span>
|
|
769
|
+
</a>
|
|
770
|
+
</div>
|
|
657
771
|
</nav>
|
|
658
772
|
<div class="side-foot">
|
|
659
773
|
<nav class="links">
|
|
@@ -670,12 +784,30 @@
|
|
|
670
784
|
</div>
|
|
671
785
|
</aside>
|
|
672
786
|
<div id="navScrim" class="nav-scrim" hidden></div>
|
|
787
|
+
<div id="palBack" class="modal-back pal-back" hidden>
|
|
788
|
+
<div class="modal pal" role="dialog" aria-modal="true" aria-label="Search views and projects">
|
|
789
|
+
<input id="palInput" class="pal-input" type="search" role="combobox" aria-expanded="true"
|
|
790
|
+
aria-controls="palList" aria-autocomplete="list" placeholder="Jump to a view or a project"
|
|
791
|
+
autocomplete="off" autocapitalize="none" spellcheck="false" enterkeyhint="go">
|
|
792
|
+
<div id="palList" class="pal-list" role="listbox" aria-label="Results"></div>
|
|
793
|
+
<div class="pal-foot">
|
|
794
|
+
<span><span class="kbd">up</span> <span class="kbd">down</span> move</span>
|
|
795
|
+
<span><span class="kbd">enter</span> open</span>
|
|
796
|
+
<span><span class="kbd">esc</span> close</span>
|
|
797
|
+
</div>
|
|
798
|
+
</div>
|
|
799
|
+
</div>
|
|
673
800
|
<div class="content">
|
|
674
801
|
<header>
|
|
675
802
|
<button type="button" id="navOpen" class="iconbtn nav-burger" title="Menu" aria-label="Open the menu" aria-expanded="false" aria-controls="sidebar">
|
|
676
803
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
|
|
677
804
|
</button>
|
|
678
805
|
<div class="nav">
|
|
806
|
+
<button type="button" id="palOpen" class="cmdk" aria-label="Search views and projects" aria-haspopup="dialog">
|
|
807
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
|
|
808
|
+
<span>Search</span>
|
|
809
|
+
<span class="kbd" id="palHint">Ctrl K</span>
|
|
810
|
+
</button>
|
|
679
811
|
<label class="scope-wrap" title="Scope: global config or a specific project"><span class="scope-lbl">Scope</span><select id="scopeSel" class="scope-sel"><option value="global">Global</option></select></label>
|
|
680
812
|
<span class="live"><span id="dot" class="dot stale"></span><span id="updated">Connecting...</span></span>
|
|
681
813
|
</div>
|
|
@@ -931,19 +1063,55 @@
|
|
|
931
1063
|
<button id="gateRefresh" type="button" class="iconbtn" title="Refresh" aria-label="Refresh the gate view"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></button>
|
|
932
1064
|
</div>
|
|
933
1065
|
<div class="grid">
|
|
934
|
-
<div class="card"
|
|
935
|
-
|
|
1066
|
+
<div class="card">
|
|
1067
|
+
<div class="label">Gate</div><div id="gateOn" class="value">-</div>
|
|
1068
|
+
<div class="foot" style="display:flex;align-items:center;gap:8px">Auto-driven after a code task<button id="gateOnBtn" type="button" class="toggle" data-kind="bool" role="switch" aria-checked="false" aria-label="AI quality gate"></button></div>
|
|
1069
|
+
</div>
|
|
1070
|
+
<div class="card">
|
|
1071
|
+
<div class="label">Daemon</div><div id="gateDaemon" class="value">-</div>
|
|
1072
|
+
<div class="foot" style="display:flex;align-items:center;gap:8px">Runs the pipeline in the background<button id="gateDaemonBtn" type="button" class="toggle" data-kind="bool" role="switch" aria-checked="false" aria-label="Gate daemon"></button></div>
|
|
1073
|
+
</div>
|
|
936
1074
|
<div class="card"><div class="label">Repository</div><div id="gateRepo" class="value">-</div><div id="gateRepoFoot" class="foot">Scope</div></div>
|
|
937
1075
|
</div>
|
|
938
1076
|
|
|
939
1077
|
<div class="panel">
|
|
940
|
-
<div class="panel-head"><h2>Runs <small id="gateRunsSub">most recent first</small></h2></div>
|
|
1078
|
+
<div class="panel-head"><h2>Runs <small id="gateRunsSub">most recent first</small></h2><span id="gateLive" class="set-hint"></span></div>
|
|
941
1079
|
<div id="gateRuns"><div class="empty" style="padding:12px 0">Loading...</div></div>
|
|
942
1080
|
</div>
|
|
943
1081
|
|
|
1082
|
+
<div class="panel" id="gateSetPanel">
|
|
1083
|
+
<div class="set-cat-h">Pipeline settings <code id="gateCfgPath"></code></div>
|
|
1084
|
+
<div class="sub" style="margin-bottom:10px">What the daemon runs and how hard it tries. Model and auto-fix limits are what a run's duration and token cost actually hang on.</div>
|
|
1085
|
+
<div class="set-grid">
|
|
1086
|
+
<div class="set-row">
|
|
1087
|
+
<div class="set-meta"><div class="set-name">Agent</div><div class="set-hint" id="gateAgentHint">which coding agent the pipeline drives</div></div>
|
|
1088
|
+
<div class="set-ctl"><select id="gateAgent" class="linput"><option value="auto">auto</option><option value="claude">claude</option><option value="codex">codex</option><option value="opencode">opencode</option><option value="rovodev">rovodev</option><option value="pi">pi</option></select></div>
|
|
1089
|
+
</div>
|
|
1090
|
+
<div class="set-row" id="gateModelRow">
|
|
1091
|
+
<div class="set-meta"><div class="set-name">Model</div><div class="set-hint" id="gateModelHint">the model every step runs on; a smaller one cuts run time and tokens</div></div>
|
|
1092
|
+
<div class="set-ctl ladd" style="align-items:center"><input id="gateModel" class="linput" style="flex:none;width:210px" spellcheck="false" autocapitalize="none" autocorrect="off" placeholder="agent default"><button id="gateModelSave" type="button" class="toggle on">Save</button></div>
|
|
1093
|
+
</div>
|
|
1094
|
+
<div class="set-row">
|
|
1095
|
+
<div class="set-meta"><div class="set-name">CI timeout</div><div class="set-hint">how long the CI step babysits an idle PR: 168h, 24h, or unlimited</div></div>
|
|
1096
|
+
<div class="set-ctl ladd" style="align-items:center"><input id="gateCiTimeout" class="linput" style="flex:none;width:120px" spellcheck="false" autocapitalize="none" autocorrect="off" placeholder="168h"><button id="gateCiSave" type="button" class="toggle on">Save</button></div>
|
|
1097
|
+
</div>
|
|
1098
|
+
<div class="set-row">
|
|
1099
|
+
<div class="set-meta"><div class="set-name">Log level</div><div class="set-hint">daemon output verbosity</div></div>
|
|
1100
|
+
<div class="set-ctl"><select id="gateLogLevel" class="linput"><option value="debug">debug</option><option value="info">info</option><option value="warn">warn</option><option value="error">error</option></select></div>
|
|
1101
|
+
</div>
|
|
1102
|
+
<div class="set-row">
|
|
1103
|
+
<div class="set-meta"><div class="set-name">Read intent from transcripts</div><div class="set-hint">feeds what you were trying to do to every step; off makes the agents work from the diff alone</div></div>
|
|
1104
|
+
<div class="set-ctl"><button id="gateIntent" type="button" class="toggle" data-kind="bool" role="switch" aria-checked="false" aria-label="Read intent from transcripts"></button></div>
|
|
1105
|
+
</div>
|
|
1106
|
+
</div>
|
|
1107
|
+
<div class="set-cat-h" style="margin-top:18px">Auto-fix attempts</div>
|
|
1108
|
+
<div class="sub" style="margin-bottom:10px">Follow-up rounds a step gets before it pauses for you. Every round is another full agent pass over the diff, so this is the setting that multiplies a run's cost. 0 pauses immediately.</div>
|
|
1109
|
+
<div class="set-grid" id="gateAutoFix"></div>
|
|
1110
|
+
</div>
|
|
1111
|
+
|
|
944
1112
|
<div class="panel">
|
|
945
|
-
<div class="set-cat-h">Global configuration <code
|
|
946
|
-
<div class="sub" style="margin-bottom:10px">
|
|
1113
|
+
<div class="set-cat-h">Global configuration <code class="gate-cfg-path"></code></div>
|
|
1114
|
+
<div class="sub" style="margin-bottom:10px">The same file as raw YAML, for the keys with no control above (agent paths, ACP targets, ignore patterns, evidence). Saved only when it parses, so a typo here never reaches the daemon.</div>
|
|
947
1115
|
<textarea id="gateCfg" class="linput gate-yaml" spellcheck="false" autocapitalize="none" autocorrect="off" aria-label="Global gate configuration"></textarea>
|
|
948
1116
|
<div class="ladd" style="gap:8px;align-items:center">
|
|
949
1117
|
<button id="gateCfgSave" type="button" class="toggle on" disabled title="No changes to save">Save</button>
|
|
@@ -4154,12 +4322,29 @@ export {
|
|
|
4154
4322
|
if (ms < 60000) return (ms / 1000).toFixed(1) + "s";
|
|
4155
4323
|
return Math.round(ms / 60000) + "m " + Math.round((ms % 60000) / 1000) + "s";
|
|
4156
4324
|
}
|
|
4325
|
+
// Server-minus-browser clock offset, so a step's live elapsed is measured against the
|
|
4326
|
+
// clock that stamped started_at rather than against this machine's.
|
|
4327
|
+
let gateClockSkew = 0;
|
|
4328
|
+
// A step only gets its duration written when a round ends, so a step that is running or
|
|
4329
|
+
// in a fix round would otherwise freeze at its last round. Tick it from started_at instead.
|
|
4330
|
+
const GATE_ACTIVE_STEP = { running: 1, fixing: 1, awaiting_approval: 1, fix_review: 1 };
|
|
4331
|
+
function gateStepDur(s) {
|
|
4332
|
+
if (GATE_ACTIVE_STEP[s.status] && s.startedAt) {
|
|
4333
|
+
return gateDur(Math.max(0, Date.now() + gateClockSkew - s.startedAt * 1000));
|
|
4334
|
+
}
|
|
4335
|
+
return s.durationMs ? gateDur(s.durationMs) : "";
|
|
4336
|
+
}
|
|
4337
|
+
function gateStepsHtml(steps) {
|
|
4338
|
+
return (steps || []).map((s) => {
|
|
4339
|
+
const d = gateStepDur(s);
|
|
4340
|
+
return '<span class="gate-step ' + esc(s.status) + '">' + esc(s.name)
|
|
4341
|
+
+ (d ? " · " + esc(d) : "")
|
|
4342
|
+
+ (s.findings ? " · " + s.findings : "") + "</span>";
|
|
4343
|
+
}).join("");
|
|
4344
|
+
}
|
|
4157
4345
|
function gateRunRow(r) {
|
|
4158
4346
|
const pr = r.prUrl ? ' <a href="' + esc(r.prUrl) + '" target="_blank" rel="noopener noreferrer">PR</a>' : "";
|
|
4159
|
-
const steps = (r.steps
|
|
4160
|
-
'<span class="gate-step ' + esc(s.status) + '">' + esc(s.name)
|
|
4161
|
-
+ (s.durationMs ? " · " + esc(gateDur(s.durationMs)) : "")
|
|
4162
|
-
+ (s.findings ? " · " + s.findings : "") + "</span>").join("");
|
|
4347
|
+
const steps = gateStepsHtml(r.steps);
|
|
4163
4348
|
const findings = (r.findings || []).length
|
|
4164
4349
|
? '<div class="gate-find">' + r.findings.map((f) =>
|
|
4165
4350
|
'<div class="gate-find-row"><b>' + esc(f.id) + "</b> · " + esc(f.severity) + " · " + esc(f.action)
|
|
@@ -4172,21 +4357,78 @@ export {
|
|
|
4172
4357
|
+ '<span class="gate-run-meta">' + esc(r.headSha) + pr + "</span>"
|
|
4173
4358
|
+ "</div>" + '<div class="gate-steps">' + steps + "</div>" + findings + err + "</div>";
|
|
4174
4359
|
}
|
|
4175
|
-
|
|
4360
|
+
// The two on/off switches at the top of the page. Both are optimistic: the switch flips
|
|
4361
|
+
// first and rolls back if the server refuses, like every other control here.
|
|
4362
|
+
function gateBool(btn, on) {
|
|
4363
|
+
btn.textContent = on ? "on" : "off";
|
|
4364
|
+
btn.classList.toggle("on", !!on);
|
|
4365
|
+
btn.dataset.on = on ? "1" : "0";
|
|
4366
|
+
btn.setAttribute("aria-checked", on ? "true" : "false");
|
|
4367
|
+
}
|
|
4368
|
+
const GATE_AUTOFIX = [
|
|
4369
|
+
["rebase", "conflicts and stale-base failures"],
|
|
4370
|
+
["review", "0 by default: review findings are yours to approve"],
|
|
4371
|
+
["test", "failing tests"],
|
|
4372
|
+
["document", "gaps the initial documentation pass left"],
|
|
4373
|
+
["lint", "lint and formatting failures"],
|
|
4374
|
+
["ci", "CI failures and merge conflicts on the open PR"]
|
|
4375
|
+
];
|
|
4376
|
+
// Rendered once per non-live load; the number inputs write on change, so a re-render
|
|
4377
|
+
// mid-edit would drop what is being typed.
|
|
4378
|
+
function renderGateSettings(s) {
|
|
4379
|
+
const panel = $("gateSetPanel");
|
|
4380
|
+
if (!s) { panel.style.display = "none"; return; }
|
|
4381
|
+
panel.style.display = "";
|
|
4382
|
+
// An `acp:<target>` agent set from the raw editor is not one of the fixed options;
|
|
4383
|
+
// carry it as one so the select shows the truth instead of going blank.
|
|
4384
|
+
const agentSel = $("gateAgent");
|
|
4385
|
+
if (!Array.prototype.some.call(agentSel.options, (o) => o.value === s.agent)) {
|
|
4386
|
+
agentSel.add(new Option(s.agent, s.agent));
|
|
4387
|
+
}
|
|
4388
|
+
agentSel.value = s.agent;
|
|
4389
|
+
syncDD(agentSel);
|
|
4390
|
+
$("gateAgentHint").textContent = s.agent === "auto"
|
|
4391
|
+
? "auto: first agent found on PATH - currently " + s.agentResolved
|
|
4392
|
+
: "which coding agent the pipeline drives";
|
|
4393
|
+
$("gateModelRow").style.display = s.modelSupported ? "" : "none";
|
|
4394
|
+
$("gateModel").value = s.model || "";
|
|
4395
|
+
$("gateModelHint").textContent = "passed to " + s.agentResolved
|
|
4396
|
+
+ " on every step; a smaller model is the one change that cuts both run time and tokens";
|
|
4397
|
+
$("gateCiTimeout").value = s.ciTimeout || "";
|
|
4398
|
+
$("gateLogLevel").value = s.logLevel;
|
|
4399
|
+
syncDD($("gateLogLevel"));
|
|
4400
|
+
gateBool($("gateIntent"), s.intentEnabled);
|
|
4401
|
+
$("gateAutoFix").innerHTML = GATE_AUTOFIX.map(([step, hint]) =>
|
|
4402
|
+
'<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(step) + '</div><div class="set-hint">' + esc(hint) + "</div></div>"
|
|
4403
|
+
+ '<div class="set-ctl"><input type="number" min="0" max="20" step="1" inputmode="numeric" class="linput" style="flex:none;width:80px" data-gate-af="' + esc(step) + '" value="' + Number(s.autoFix[step] || 0) + '"></div></div>').join("");
|
|
4404
|
+
const ctls = panel.querySelectorAll("input, select, button");
|
|
4405
|
+
for (const c of ctls) c.disabled = !gateWritable;
|
|
4406
|
+
}
|
|
4407
|
+
// `live` marks a poll refresh: it repaints the run strip and the two switches, and leaves
|
|
4408
|
+
// every editor alone so a poll can never overwrite what is being typed.
|
|
4409
|
+
function renderGate(g, live) {
|
|
4176
4410
|
g = g || {};
|
|
4411
|
+
if (typeof g.serverNow === "number") gateClockSkew = g.serverNow * 1000 - Date.now();
|
|
4177
4412
|
$("gateOn").textContent = g.on ? "On" : "Off";
|
|
4178
4413
|
$("gateOn").className = "value " + (g.on ? "good" : "");
|
|
4414
|
+
gateBool($("gateOnBtn"), g.on);
|
|
4179
4415
|
$("gateDaemon").textContent = g.daemon ? "Running" : "Stopped";
|
|
4180
4416
|
$("gateDaemon").className = "value " + (g.daemon ? "good" : "");
|
|
4417
|
+
gateBool($("gateDaemonBtn"), g.daemon);
|
|
4181
4418
|
const repo = g.repo;
|
|
4182
4419
|
$("gateRepo").textContent = repo ? (repo.initialized ? "Initialized" : "Not initialized") : "Global";
|
|
4183
4420
|
$("gateRepoFoot").textContent = repo ? repo.path : "Every active run on this machine";
|
|
4184
4421
|
$("gateRunsSub").textContent = repo ? "most recent first, this repository" : "active runs across every repository";
|
|
4185
4422
|
const runs = g.runs || [];
|
|
4423
|
+
gateRuns = runs;
|
|
4186
4424
|
$("gateRuns").innerHTML = runs.length
|
|
4187
4425
|
? runs.map(gateRunRow).join("")
|
|
4188
4426
|
: '<div class="empty" style="padding:12px 0">' + (g.runsAvailable === false ? esc(g.runsNote || "Run history unavailable.") : (repo ? "No runs recorded for this repository yet." : "No run in flight.")) + "</div>";
|
|
4189
|
-
|
|
4427
|
+
gatePoll();
|
|
4428
|
+
if (live) return;
|
|
4429
|
+
const cfgPath = (g.globalConfig || {}).path || "";
|
|
4430
|
+
$("gateCfgPath").textContent = cfgPath;
|
|
4431
|
+
for (const el of document.querySelectorAll(".gate-cfg-path")) el.textContent = cfgPath;
|
|
4190
4432
|
gateSaved.global = (g.globalConfig || {}).text || "";
|
|
4191
4433
|
$("gateCfg").value = gateSaved.global;
|
|
4192
4434
|
// A runtime that cannot parse a gate config must not write one: read-only, with the reason.
|
|
@@ -4195,6 +4437,7 @@ export {
|
|
|
4195
4437
|
$("gateRepoCfg").readOnly = !gateWritable;
|
|
4196
4438
|
if (!gateWritable) { setNote("gateCfgNote", g.writeNote || "Read-only here.", "err"); setNote("gateRepoCfgNote", g.writeNote || "Read-only here.", "err"); }
|
|
4197
4439
|
syncGateSave("global");
|
|
4440
|
+
renderGateSettings(g.settings);
|
|
4198
4441
|
$("gateRepoPanel").style.display = repo ? "" : "none";
|
|
4199
4442
|
if (repo) {
|
|
4200
4443
|
$("gateRepoCfgPath").textContent = repo.configPath || "";
|
|
@@ -4203,18 +4446,48 @@ export {
|
|
|
4203
4446
|
syncGateSave("repo");
|
|
4204
4447
|
}
|
|
4205
4448
|
}
|
|
4206
|
-
|
|
4449
|
+
|
|
4450
|
+
// --- keeping the run strip live -------------------------------------------------
|
|
4451
|
+
// A gate run takes tens of minutes and the page has no push channel, so without this the
|
|
4452
|
+
// strip shows whatever was true when the page was opened. Two loops: a 5s refetch while
|
|
4453
|
+
// something is in flight (20s when idle) and a 1s repaint so the active step's elapsed
|
|
4454
|
+
// counts up between fetches. Both stop when the tab is hidden or the view is not open.
|
|
4455
|
+
let gateRuns = [], gateFetchTimer = null, gateTickTimer = null;
|
|
4456
|
+
function gateBusy() {
|
|
4457
|
+
return gateRuns.some((r) => r.status === "running" || r.status === "awaiting_approval");
|
|
4458
|
+
}
|
|
4459
|
+
function gateStopPoll() {
|
|
4460
|
+
clearTimeout(gateFetchTimer); gateFetchTimer = null;
|
|
4461
|
+
clearInterval(gateTickTimer); gateTickTimer = null;
|
|
4462
|
+
$("gateLive").textContent = "";
|
|
4463
|
+
}
|
|
4464
|
+
function gatePoll() {
|
|
4465
|
+
gateStopPoll();
|
|
4466
|
+
if (activeView !== "gate" || document.hidden) return;
|
|
4467
|
+
const busy = gateBusy();
|
|
4468
|
+
$("gateLive").textContent = busy ? "live" : "";
|
|
4469
|
+
gateFetchTimer = setTimeout(() => loadGate(true), busy ? 5000 : 20000);
|
|
4470
|
+
if (!busy) return;
|
|
4471
|
+
// Repaint only the step strips: replacing whole rows every second would drop a text
|
|
4472
|
+
// selection inside the findings of a parked run.
|
|
4473
|
+
gateTickTimer = setInterval(() => {
|
|
4474
|
+
const strips = $("gateRuns").querySelectorAll(".gate-steps");
|
|
4475
|
+
strips.forEach((el, i) => { if (gateRuns[i]) el.innerHTML = gateStepsHtml(gateRuns[i].steps); });
|
|
4476
|
+
}, 1000);
|
|
4477
|
+
}
|
|
4478
|
+
async function loadGate(live) {
|
|
4207
4479
|
const q = isProjectScope() ? "?path=" + encodeURIComponent(currentScope) : "";
|
|
4208
|
-
const note = (msg) => { $("gateRuns").innerHTML = '<div class="empty" style="padding:12px 0">' + msg + "</div>"; };
|
|
4480
|
+
const note = (msg) => { gateStopPoll(); $("gateRuns").innerHTML = '<div class="empty" style="padding:12px 0">' + msg + "</div>"; };
|
|
4209
4481
|
try {
|
|
4210
4482
|
const res = await fetch("/api/gate" + q, { cache: "no-store" });
|
|
4211
4483
|
// The UI bundle updates on its own; the route it calls ships with the CLI, so a
|
|
4212
4484
|
// 404 here means the running server predates this view rather than being down.
|
|
4213
4485
|
if (res.status === 404 || res.status === 405) { note("This view needs a newer enigma CLI: the running server has no /api/gate route yet. Run <code>enigma update</code> once the next release ships."); return; }
|
|
4214
4486
|
if (!res.ok) { note("The server refused the request (" + res.status + ")."); return; }
|
|
4215
|
-
renderGate(await res.json());
|
|
4487
|
+
renderGate(await res.json(), live);
|
|
4216
4488
|
} catch { note("Could not reach the server."); }
|
|
4217
4489
|
}
|
|
4490
|
+
document.addEventListener("visibilitychange", () => { if (activeView === "gate") { if (document.hidden) gateStopPoll(); else loadGate(true); } });
|
|
4218
4491
|
async function saveGateCfg(which) {
|
|
4219
4492
|
const { area, note } = gateEls(which);
|
|
4220
4493
|
if (area.value === gateSaved[which]) return; // no-op: nothing changed
|
|
@@ -4227,11 +4500,52 @@ export {
|
|
|
4227
4500
|
if (out.ok) { gateSaved[which] = area.value; syncGateSave(which); }
|
|
4228
4501
|
} catch { setNote(note, "Could not reach the server.", "err"); }
|
|
4229
4502
|
}
|
|
4230
|
-
|
|
4503
|
+
/** One form control -> one key of the global config. Reloads so the file stays the truth. */
|
|
4504
|
+
async function saveGateSetting(key, value, label) {
|
|
4505
|
+
setNote("gateCfgNote", "Saving " + label + "...");
|
|
4506
|
+
try {
|
|
4507
|
+
const out = await (await fetch("/api/gate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "setting", key, value }) })).json();
|
|
4508
|
+
setNote("gateCfgNote", out.message || (out.ok ? "Saved." : "Not saved."), out.ok ? "ok" : "err");
|
|
4509
|
+
await loadGate();
|
|
4510
|
+
} catch { setNote("gateCfgNote", "Could not reach the server.", "err"); await loadGate(); }
|
|
4511
|
+
}
|
|
4512
|
+
async function gateDaemonPost(on) {
|
|
4513
|
+
gateBool($("gateDaemonBtn"), on); // optimistic; the reload below is the truth
|
|
4514
|
+
setNote("gateCfgNote", on ? "Starting the daemon..." : "Stopping the daemon...");
|
|
4515
|
+
try {
|
|
4516
|
+
const out = await (await fetch("/api/gate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "daemon", on }) })).json();
|
|
4517
|
+
setNote("gateCfgNote", out.message || (out.ok ? "Done." : "Failed."), out.ok ? "ok" : "err");
|
|
4518
|
+
} catch { setNote("gateCfgNote", "Could not reach the server.", "err"); }
|
|
4519
|
+
await loadGate();
|
|
4520
|
+
}
|
|
4521
|
+
// The gate on/off flag is a normal enigma setting, so it goes through the settings route
|
|
4522
|
+
// the Settings page uses rather than growing a second writer for the same key.
|
|
4523
|
+
async function gateOnPost(on) {
|
|
4524
|
+
gateBool($("gateOnBtn"), on);
|
|
4525
|
+
setNote("gateCfgNote", on ? "Turning the gate on..." : "Turning the gate off...");
|
|
4526
|
+
try {
|
|
4527
|
+
const out = await (await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "gate", value: on }) })).json();
|
|
4528
|
+
setNote("gateCfgNote", out.ok ? (out.restartNote || "Saved. Restart your agent to apply.") : ("Could not change the gate: " + (out.error || "error")), out.ok ? "warn" : "err");
|
|
4529
|
+
} catch { setNote("gateCfgNote", "Could not reach the server.", "err"); }
|
|
4530
|
+
await loadGate();
|
|
4531
|
+
}
|
|
4532
|
+
$("gateRefresh").addEventListener("click", () => loadGate());
|
|
4231
4533
|
$("gateCfg").addEventListener("input", () => syncGateSave("global"));
|
|
4232
4534
|
$("gateRepoCfg").addEventListener("input", () => syncGateSave("repo"));
|
|
4233
4535
|
$("gateCfgSave").addEventListener("click", () => saveGateCfg("global"));
|
|
4234
4536
|
$("gateRepoCfgSave").addEventListener("click", () => saveGateCfg("repo"));
|
|
4537
|
+
$("gateOnBtn").addEventListener("click", () => gateOnPost($("gateOnBtn").dataset.on !== "1"));
|
|
4538
|
+
$("gateDaemonBtn").addEventListener("click", () => gateDaemonPost($("gateDaemonBtn").dataset.on !== "1"));
|
|
4539
|
+
$("gateAgent").addEventListener("change", (e) => saveGateSetting("agent", e.target.value, "the agent"));
|
|
4540
|
+
$("gateLogLevel").addEventListener("change", (e) => saveGateSetting("log_level", e.target.value, "the log level"));
|
|
4541
|
+
$("gateIntent").addEventListener("click", () => saveGateSetting("intent.enabled", $("gateIntent").dataset.on !== "1", "intent extraction"));
|
|
4542
|
+
$("gateModelSave").addEventListener("click", () => saveGateSetting("model", $("gateModel").value.trim(), "the model"));
|
|
4543
|
+
$("gateCiSave").addEventListener("click", () => saveGateSetting("ci_timeout", $("gateCiTimeout").value.trim(), "the CI timeout"));
|
|
4544
|
+
// Number inputs commit on change (blur or Enter), so typing "12" is not read as "1" then "12".
|
|
4545
|
+
$("gateAutoFix").addEventListener("change", (e) => {
|
|
4546
|
+
const step = e.target.dataset && e.target.dataset.gateAf;
|
|
4547
|
+
if (step) saveGateSetting("auto_fix." + step, Number(e.target.value), "the " + step + " limit");
|
|
4548
|
+
});
|
|
4235
4549
|
|
|
4236
4550
|
// --- section rail (a full-screen drawer below 900px) ---
|
|
4237
4551
|
const sidebarEl = $("sidebar"), navScrimEl = $("navScrim"), navOpenBtn = $("navOpen");
|
|
@@ -4258,6 +4572,149 @@ export {
|
|
|
4258
4572
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && sidebarEl.classList.contains("open")) setNav(false); });
|
|
4259
4573
|
window.matchMedia("(min-width: 901px)").addEventListener("change", (e) => { if (e.matches) setNav(false); });
|
|
4260
4574
|
|
|
4575
|
+
// --- command palette (Cmd/Ctrl+K) ---------------------------------------------
|
|
4576
|
+
// Twelve views plus every registered project is more than the rail can answer by
|
|
4577
|
+
// pointing, so typing is the second way in (frontend-policy, "Ctrl+K Opens A Command
|
|
4578
|
+
// Palette"). enigma: the dashboard is a single dependency-free HTML file served over
|
|
4579
|
+
// loopback, so the ~20-line matcher below stands in for fuse.js - upgrade to fuse.js
|
|
4580
|
+
// if this page ever gains a build step.
|
|
4581
|
+
const palBack = $("palBack"), palInput = $("palInput"), palList = $("palList");
|
|
4582
|
+
const IS_MAC = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
4583
|
+
let palItems = [], palSel = 0, palReturnFocus = null;
|
|
4584
|
+
$("palHint").textContent = IS_MAC ? "Cmd K" : "Ctrl K";
|
|
4585
|
+
|
|
4586
|
+
/**
|
|
4587
|
+
* Fuzzy rank of `query` inside `text`: -1 when the query is not a subsequence, otherwise
|
|
4588
|
+
* a score that rewards consecutive runs and word starts. Beats a substring filter because
|
|
4589
|
+
* "qgate" finds "Quality gate" and "plyg" finds "Playground", which .includes() cannot.
|
|
4590
|
+
*/
|
|
4591
|
+
function palScore(query, text) {
|
|
4592
|
+
const q = query.toLowerCase(), t = text.toLowerCase();
|
|
4593
|
+
if (!q) return 0;
|
|
4594
|
+
let at = 0, from = 0, score = 0, run = 0;
|
|
4595
|
+
for (let i = 0; i < q.length; i++) {
|
|
4596
|
+
at = t.indexOf(q[i], from);
|
|
4597
|
+
if (at === -1) return -1;
|
|
4598
|
+
run = (i > 0 && at === from) ? run + 1 : 0;
|
|
4599
|
+
const wordStart = at === 0 || /[^a-z0-9]/.test(t[at - 1]);
|
|
4600
|
+
score += 10 + run * 6 + (wordStart ? 8 : 0) - Math.min(at - from, 6);
|
|
4601
|
+
from = at + 1;
|
|
4602
|
+
}
|
|
4603
|
+
return score;
|
|
4604
|
+
}
|
|
4605
|
+
|
|
4606
|
+
/** Built from the live DOM on every open, so it can never drift from the rail or the scopes. */
|
|
4607
|
+
function palIndex() {
|
|
4608
|
+
const out = [];
|
|
4609
|
+
document.querySelectorAll(".tabs .tab").forEach((t) => {
|
|
4610
|
+
if (t.classList.contains("disabled")) return; // out of reach in this scope
|
|
4611
|
+
const group = t.closest(".nav-group");
|
|
4612
|
+
const label = (t.querySelector("span") || t).textContent.trim();
|
|
4613
|
+
const icon = t.querySelector(".tab-ic");
|
|
4614
|
+
out.push({
|
|
4615
|
+
kind: "View", name: label, hint: t.getAttribute("href"),
|
|
4616
|
+
icon: icon ? icon.outerHTML : "",
|
|
4617
|
+
group: group && group.querySelector(".nav-group-t") ? group.querySelector(".nav-group-t").textContent.trim() : "",
|
|
4618
|
+
run: () => { location.hash = t.getAttribute("href"); },
|
|
4619
|
+
});
|
|
4620
|
+
});
|
|
4621
|
+
const sel = $("scopeSel");
|
|
4622
|
+
if (sel) Array.from(sel.options).forEach((o) => {
|
|
4623
|
+
if (o.value === "__create__" || o.value === currentScope) return;
|
|
4624
|
+
out.push({
|
|
4625
|
+
kind: "Scope", name: o.textContent.trim(), hint: o.value === "global" ? "" : "project", icon: "", group: "",
|
|
4626
|
+
run: () => { sel.value = o.value; syncDD(sel); sel.dispatchEvent(new Event("change")); },
|
|
4627
|
+
});
|
|
4628
|
+
});
|
|
4629
|
+
return out;
|
|
4630
|
+
}
|
|
4631
|
+
|
|
4632
|
+
function palRender(query) {
|
|
4633
|
+
const q = query.trim();
|
|
4634
|
+
const all = palIndex();
|
|
4635
|
+
palItems = (!q ? all : all
|
|
4636
|
+
.map((it) => ({ it, s: Math.max(palScore(q, it.name), palScore(q, it.kind + " " + it.name) - 4) }))
|
|
4637
|
+
.filter((r) => r.s >= 0)
|
|
4638
|
+
.sort((a, b) => b.s - a.s)
|
|
4639
|
+
.map((r) => r.it));
|
|
4640
|
+
palSel = 0;
|
|
4641
|
+
if (!palItems.length) {
|
|
4642
|
+
palList.innerHTML = '<div class="pal-empty">No view or project matches "' + esc(q) + '".</div>';
|
|
4643
|
+
palInput.removeAttribute("aria-activedescendant");
|
|
4644
|
+
return;
|
|
4645
|
+
}
|
|
4646
|
+
let html = "", kind = null;
|
|
4647
|
+
palItems.forEach((it, i) => {
|
|
4648
|
+
if (it.kind !== kind) { kind = it.kind; html += '<div class="pal-gt">' + esc(kind === "View" ? "Views" : "Switch scope") + "</div>"; }
|
|
4649
|
+
html += '<div class="pal-row' + (i === 0 ? " sel" : "") + '" role="option" id="pal-o' + i + '"'
|
|
4650
|
+
+ ' aria-selected="' + (i === 0 ? "true" : "false") + '" data-i="' + i + '">'
|
|
4651
|
+
+ it.icon
|
|
4652
|
+
+ '<span class="pal-name">' + esc(it.name) + "</span>"
|
|
4653
|
+
+ (it.group ? '<span class="pal-hint">' + esc(it.group) + "</span>" : (it.hint ? '<span class="pal-hint">' + esc(it.hint) + "</span>" : ""))
|
|
4654
|
+
+ "</div>";
|
|
4655
|
+
});
|
|
4656
|
+
palList.innerHTML = html;
|
|
4657
|
+
palMove(0);
|
|
4658
|
+
}
|
|
4659
|
+
|
|
4660
|
+
function palMove(next) {
|
|
4661
|
+
if (!palItems.length) return;
|
|
4662
|
+
const rows = palList.querySelectorAll(".pal-row");
|
|
4663
|
+
palSel = (next + palItems.length) % palItems.length;
|
|
4664
|
+
rows.forEach((r, i) => {
|
|
4665
|
+
const on = i === palSel;
|
|
4666
|
+
r.classList.toggle("sel", on);
|
|
4667
|
+
r.setAttribute("aria-selected", on ? "true" : "false");
|
|
4668
|
+
if (on) { r.scrollIntoView({ block: "nearest" }); palInput.setAttribute("aria-activedescendant", r.id); }
|
|
4669
|
+
});
|
|
4670
|
+
}
|
|
4671
|
+
|
|
4672
|
+
function palOpen() {
|
|
4673
|
+
if (!palBack.hidden) return;
|
|
4674
|
+
palReturnFocus = document.activeElement;
|
|
4675
|
+
palBack.hidden = false;
|
|
4676
|
+
document.body.classList.add("nav-open"); // reuses the drawer's scroll lock
|
|
4677
|
+
requestAnimationFrame(() => palBack.classList.add("show"));
|
|
4678
|
+
palInput.value = "";
|
|
4679
|
+
palRender("");
|
|
4680
|
+
palInput.focus();
|
|
4681
|
+
}
|
|
4682
|
+
function palClose() {
|
|
4683
|
+
if (palBack.hidden) return;
|
|
4684
|
+
palBack.classList.remove("show");
|
|
4685
|
+
setTimeout(() => { palBack.hidden = true; }, 200);
|
|
4686
|
+
if (!sidebarEl.classList.contains("open")) document.body.classList.remove("nav-open");
|
|
4687
|
+
if (palReturnFocus) { try { palReturnFocus.focus(); } catch { /* gone from the DOM */ } palReturnFocus = null; }
|
|
4688
|
+
}
|
|
4689
|
+
function palRun() {
|
|
4690
|
+
const it = palItems[palSel];
|
|
4691
|
+
if (!it) return;
|
|
4692
|
+
palClose();
|
|
4693
|
+
setNav(false);
|
|
4694
|
+
it.run();
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4697
|
+
$("palOpen").addEventListener("click", palOpen);
|
|
4698
|
+
palInput.addEventListener("input", () => palRender(palInput.value));
|
|
4699
|
+
palList.addEventListener("mousemove", (e) => { const r = e.target.closest(".pal-row"); if (r) palMove(Number(r.dataset.i)); });
|
|
4700
|
+
palList.addEventListener("click", (e) => { const r = e.target.closest(".pal-row"); if (r) { palMove(Number(r.dataset.i)); palRun(); } });
|
|
4701
|
+
palBack.addEventListener("mousedown", (e) => { if (e.target === palBack) palClose(); });
|
|
4702
|
+
// Focus stays inside: the dialog holds one input and a list driven from it.
|
|
4703
|
+
palBack.addEventListener("keydown", (e) => {
|
|
4704
|
+
if (e.key === "ArrowDown") { e.preventDefault(); palMove(palSel + 1); }
|
|
4705
|
+
else if (e.key === "ArrowUp") { e.preventDefault(); palMove(palSel - 1); }
|
|
4706
|
+
else if (e.key === "Enter") { e.preventDefault(); palRun(); }
|
|
4707
|
+
// Escape closes the topmost surface only: the drawer behind it stays open.
|
|
4708
|
+
else if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); palClose(); }
|
|
4709
|
+
else if (e.key === "Tab") e.preventDefault();
|
|
4710
|
+
});
|
|
4711
|
+
document.addEventListener("keydown", (e) => {
|
|
4712
|
+
// Ctrl+K is the browser's own search-bar shortcut, so it has to be taken explicitly.
|
|
4713
|
+
if (e.key.toLowerCase() !== "k" || !(IS_MAC ? e.metaKey : e.ctrlKey) || e.altKey) return;
|
|
4714
|
+
e.preventDefault();
|
|
4715
|
+
if (palBack.hidden) palOpen(); else palClose();
|
|
4716
|
+
});
|
|
4717
|
+
|
|
4261
4718
|
// --- hash routing between the subpages ---
|
|
4262
4719
|
// Single source of truth for the subpage ids (savings is the default/fallback).
|
|
4263
4720
|
const VIEWS = ["savings", "usage", "accounts", "skills", "settings", "gate", "resources", "recall", "codegraph", "playground", "packs", "ssh"];
|
|
@@ -4273,7 +4730,20 @@ export {
|
|
|
4273
4730
|
if (project && PROJECT_VIEWS.indexOf(v) === -1) v = "settings";
|
|
4274
4731
|
activeView = v;
|
|
4275
4732
|
VIEWS.forEach((view) => { $("view-" + view).style.display = view === v ? "" : "none"; });
|
|
4276
|
-
document.querySelectorAll(".tab").forEach((t) =>
|
|
4733
|
+
document.querySelectorAll(".tab").forEach((t) => {
|
|
4734
|
+
const on = t.dataset.view === v;
|
|
4735
|
+
t.classList.toggle("active", on);
|
|
4736
|
+
// Colour alone does not carry the active state to a screen reader.
|
|
4737
|
+
if (on) t.setAttribute("aria-current", "page"); else t.removeAttribute("aria-current");
|
|
4738
|
+
});
|
|
4739
|
+
// A group whose every entry is out of scope would otherwise render as a lone caption.
|
|
4740
|
+
document.querySelectorAll(".nav-group").forEach((g) => {
|
|
4741
|
+
const tabs = Array.from(g.querySelectorAll(".tab"));
|
|
4742
|
+
g.hidden = tabs.length > 0 && tabs.every((t) => t.classList.contains("disabled"));
|
|
4743
|
+
});
|
|
4744
|
+
// Leaving the view must not keep polling for runs. Kept OUT of the chain below: an
|
|
4745
|
+
// `if` in the middle of it detaches every later `else if` from the dispatch.
|
|
4746
|
+
if (v !== "gate") gateStopPoll();
|
|
4277
4747
|
// Settings + Skills always reload (they are scope-dependent); the rest keep their once-load.
|
|
4278
4748
|
if (v === "settings") loadSettings();
|
|
4279
4749
|
else if (v === "skills") loadSkills();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/dashboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.97",
|
|
4
4
|
"description": "Local browser dashboard UI for enigma: the static page and chart assets enigma serves on its loopback dashboard (savings, real tool usage, in-browser settings). Installed on demand by enigma-cli; not a runtime dependency.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|