@enigmax/dashboard 0.1.96 → 0.1.98
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 +566 -32
- package/package.json +1 -1
package/assets/index.html
CHANGED
|
@@ -87,12 +87,27 @@
|
|
|
87
87
|
/* Sections as a vertical rail: the sidebar is already the surface, so the active
|
|
88
88
|
entry is a filled pill and the rest are plain rows. */
|
|
89
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
|
+
}
|
|
90
101
|
.tabs .tab {
|
|
102
|
+
display: flex; align-items: center; gap: 9px;
|
|
91
103
|
font-family: var(--mono); text-decoration: none; color: var(--muted); font-size: 12.5px; font-weight: 600;
|
|
92
104
|
padding: 8px 12px; border-radius: 9px; border: 1px solid transparent; transition: background .15s, color .15s, border-color .15s;
|
|
93
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; }
|
|
94
108
|
.tabs .tab:hover { color: var(--text); background: rgba(255,255,255,0.03); }
|
|
95
109
|
.tabs .tab.active { color: var(--accent); background: var(--surface2); border-color: var(--border); }
|
|
110
|
+
.tabs .tab.active .tab-ic { opacity: 1; }
|
|
96
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; }
|
|
97
112
|
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--good); box-shadow: 0 0 8px var(--good); }
|
|
98
113
|
.dot.stale { background: var(--muted); box-shadow: none; }
|
|
@@ -113,6 +128,21 @@
|
|
|
113
128
|
.card .value.accent { color: var(--accent); }
|
|
114
129
|
.card .value.good { color: var(--good); }
|
|
115
130
|
.card .foot { position: relative; color: var(--muted); font-size: 12px; margin-top: 5px; }
|
|
131
|
+
/* Gate status strip. Two booleans and a path do not each need a 30px stat card:
|
|
132
|
+
one surface, three inline items (frontend-policy, Visual Hierarchy). */
|
|
133
|
+
.gate-bar {
|
|
134
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 12px 28px;
|
|
135
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
|
|
136
|
+
padding: 13px 18px; margin-bottom: 18px; box-shadow: var(--shadow-1);
|
|
137
|
+
}
|
|
138
|
+
.gate-stat { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
139
|
+
.gate-stat-t { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
|
|
140
|
+
.gate-stat-n { font-family: var(--mono); color: var(--muted); font-size: 10.5px; text-transform: uppercase; letter-spacing: .8px; }
|
|
141
|
+
.gate-stat-v { font-family: var(--mono); font-size: 13px; font-weight: 600; color: var(--text); }
|
|
142
|
+
.gate-stat-v.good { color: var(--good); }
|
|
143
|
+
/* Takes the remaining width so the path truncates instead of wrapping the strip. */
|
|
144
|
+
.gate-stat-scope { flex: 1 1 220px; min-width: 0; }
|
|
145
|
+
.gate-stat-f { min-width: 0; font-size: 11.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
116
146
|
.panel { position: relative; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 22px; margin-bottom: 18px; box-shadow: var(--shadow-1); }
|
|
117
147
|
.panel-head { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
|
118
148
|
/* Section heading: a mono eyebrow led by a short accent rule (structure as signal). */
|
|
@@ -564,6 +594,51 @@
|
|
|
564
594
|
}
|
|
565
595
|
.modal-back.show .modal { transform: none; opacity: 1; }
|
|
566
596
|
.modal.wide { max-width: 900px; }
|
|
597
|
+
/* Command palette: the same backdrop and surface as every other dialog, sat in the top
|
|
598
|
+
third so the result list grows downwards without moving the input under the caret. */
|
|
599
|
+
/* The palette lives in the markup instead of being created per open, so it needs this:
|
|
600
|
+
`display: flex` above beats the UA's `[hidden] { display: none }` on specificity, and a
|
|
601
|
+
closed backdrop would otherwise sit invisibly over the page eating every click. */
|
|
602
|
+
.modal-back[hidden] { display: none; }
|
|
603
|
+
.modal-back.pal-back { align-items: flex-start; padding-top: 12vh; }
|
|
604
|
+
.modal.pal { max-width: 520px; padding: 12px; }
|
|
605
|
+
.pal-input {
|
|
606
|
+
width: 100%; background: transparent; color: var(--text); border: none; border-bottom: 1px solid var(--border);
|
|
607
|
+
padding: 8px 6px 12px; font-size: 14px; font-family: var(--mono);
|
|
608
|
+
}
|
|
609
|
+
.pal-input:focus { outline: none; }
|
|
610
|
+
.pal-input::-webkit-search-cancel-button { display: none; }
|
|
611
|
+
.pal-list { max-height: 46vh; overflow-y: auto; overscroll-behavior: contain; margin-top: 8px; }
|
|
612
|
+
.pal-gt {
|
|
613
|
+
font-family: var(--mono); font-size: 9.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.4px;
|
|
614
|
+
color: var(--muted); opacity: .6; padding: 10px 8px 4px;
|
|
615
|
+
}
|
|
616
|
+
.pal-row {
|
|
617
|
+
display: flex; align-items: center; gap: 10px; padding: 8px 8px; border-radius: 8px;
|
|
618
|
+
cursor: pointer; color: var(--muted); font-size: 13px;
|
|
619
|
+
}
|
|
620
|
+
.pal-row .tab-ic { opacity: .75; }
|
|
621
|
+
.pal-row .pal-name { font-family: var(--mono); font-weight: 600; color: var(--text); }
|
|
622
|
+
.pal-row .pal-hint { margin-left: auto; font-family: var(--mono); font-size: 11px; opacity: .6; }
|
|
623
|
+
.pal-row.sel { background: var(--surface2); color: var(--text); }
|
|
624
|
+
.pal-row.sel .tab-ic { opacity: 1; color: var(--accent); }
|
|
625
|
+
.pal-empty { padding: 18px 8px; color: var(--muted); font-size: 13px; }
|
|
626
|
+
.pal-foot {
|
|
627
|
+
display: flex; gap: 14px; padding: 10px 8px 2px; border-top: 1px solid var(--border-soft);
|
|
628
|
+
font-family: var(--mono); font-size: 10.5px; color: var(--muted); opacity: .7;
|
|
629
|
+
}
|
|
630
|
+
.kbd {
|
|
631
|
+
font-family: var(--mono); font-size: 10px; padding: 1px 5px; border: 1px solid var(--border);
|
|
632
|
+
border-radius: 4px; background: var(--surface2);
|
|
633
|
+
}
|
|
634
|
+
/* The shortcut has to be discoverable, so the palette also has a clickable entry point. */
|
|
635
|
+
.cmdk {
|
|
636
|
+
display: inline-flex; align-items: center; gap: 8px; cursor: pointer; color: var(--muted);
|
|
637
|
+
background: rgba(0,0,0,0.25); border: 1px solid var(--border-soft); border-radius: 999px;
|
|
638
|
+
padding: 5px 8px 5px 12px; font-family: var(--mono); font-size: 12px;
|
|
639
|
+
}
|
|
640
|
+
.cmdk:hover { color: var(--text); border-color: var(--border); }
|
|
641
|
+
.cmdk svg { width: 13px; height: 13px; }
|
|
567
642
|
.as-top { display: flex; gap: 8px; margin: 14px 0 6px; }
|
|
568
643
|
.as-list { max-height: 50vh; overflow: auto; border: 1px solid var(--border); border-radius: 8px; padding: 4px 12px; }
|
|
569
644
|
.as-row { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--surface2); cursor: pointer; }
|
|
@@ -646,18 +721,68 @@
|
|
|
646
721
|
</button>
|
|
647
722
|
</div>
|
|
648
723
|
<nav class="tabs">
|
|
649
|
-
<
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
<
|
|
724
|
+
<div class="nav-group" role="group" aria-labelledby="ng-overview">
|
|
725
|
+
<div class="nav-group-t" id="ng-overview">Overview</div>
|
|
726
|
+
<a href="#/" data-view="savings" class="tab active">
|
|
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"><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>
|
|
728
|
+
<span>Savings</span>
|
|
729
|
+
</a>
|
|
730
|
+
<a href="#/usage" data-view="usage" 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="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
|
732
|
+
<span>Usage</span>
|
|
733
|
+
</a>
|
|
734
|
+
</div>
|
|
735
|
+
<div class="nav-group" role="group" aria-labelledby="ng-agents">
|
|
736
|
+
<div class="nav-group-t" id="ng-agents">Agents</div>
|
|
737
|
+
<a href="#/accounts" data-view="accounts" class="tab">
|
|
738
|
+
<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>
|
|
739
|
+
<span>Accounts</span>
|
|
740
|
+
</a>
|
|
741
|
+
<a href="#/skills" data-view="skills" 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="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>
|
|
743
|
+
<span>Skills</span>
|
|
744
|
+
</a>
|
|
745
|
+
<a href="#/packs" data-view="packs" 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"><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>
|
|
747
|
+
<span>Packs</span>
|
|
748
|
+
</a>
|
|
749
|
+
<a href="#/playground" data-view="playground" 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="m4 17 6-6-6-6"/><path d="M12 19h8"/></svg>
|
|
751
|
+
<span>Playground</span>
|
|
752
|
+
</a>
|
|
753
|
+
</div>
|
|
754
|
+
<div class="nav-group" role="group" aria-labelledby="ng-code">
|
|
755
|
+
<div class="nav-group-t" id="ng-code">Code</div>
|
|
756
|
+
<a href="#/gate" data-view="gate" 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"><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>
|
|
758
|
+
<span>Quality gate</span>
|
|
759
|
+
</a>
|
|
760
|
+
<a href="#/codegraph" data-view="codegraph" 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"><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>
|
|
762
|
+
<span>Code graph</span>
|
|
763
|
+
</a>
|
|
764
|
+
<a href="#/recall" data-view="recall" class="tab">
|
|
765
|
+
<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>
|
|
766
|
+
<span>Recall</span>
|
|
767
|
+
</a>
|
|
768
|
+
</div>
|
|
769
|
+
<div class="nav-group" role="group" aria-labelledby="ng-system">
|
|
770
|
+
<div class="nav-group-t" id="ng-system">System</div>
|
|
771
|
+
<a href="#/resources" data-view="resources" class="tab">
|
|
772
|
+
<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>
|
|
773
|
+
<span>Resources</span>
|
|
774
|
+
</a>
|
|
775
|
+
<a href="#/ssh" data-view="ssh" class="tab">
|
|
776
|
+
<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>
|
|
777
|
+
<span>SSH</span>
|
|
778
|
+
</a>
|
|
779
|
+
</div>
|
|
780
|
+
<div class="nav-group nav-tail">
|
|
781
|
+
<a href="#/settings" data-view="settings" class="tab">
|
|
782
|
+
<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>
|
|
783
|
+
<span>Settings</span>
|
|
784
|
+
</a>
|
|
785
|
+
</div>
|
|
661
786
|
</nav>
|
|
662
787
|
<div class="side-foot">
|
|
663
788
|
<nav class="links">
|
|
@@ -674,12 +799,30 @@
|
|
|
674
799
|
</div>
|
|
675
800
|
</aside>
|
|
676
801
|
<div id="navScrim" class="nav-scrim" hidden></div>
|
|
802
|
+
<div id="palBack" class="modal-back pal-back" hidden>
|
|
803
|
+
<div class="modal pal" role="dialog" aria-modal="true" aria-label="Search views and projects">
|
|
804
|
+
<input id="palInput" class="pal-input" type="search" role="combobox" aria-expanded="true"
|
|
805
|
+
aria-controls="palList" aria-autocomplete="list" placeholder="Jump to a view or a project"
|
|
806
|
+
autocomplete="off" autocapitalize="none" spellcheck="false" enterkeyhint="go">
|
|
807
|
+
<div id="palList" class="pal-list" role="listbox" aria-label="Results"></div>
|
|
808
|
+
<div class="pal-foot">
|
|
809
|
+
<span><span class="kbd">up</span> <span class="kbd">down</span> move</span>
|
|
810
|
+
<span><span class="kbd">enter</span> open</span>
|
|
811
|
+
<span><span class="kbd">esc</span> close</span>
|
|
812
|
+
</div>
|
|
813
|
+
</div>
|
|
814
|
+
</div>
|
|
677
815
|
<div class="content">
|
|
678
816
|
<header>
|
|
679
817
|
<button type="button" id="navOpen" class="iconbtn nav-burger" title="Menu" aria-label="Open the menu" aria-expanded="false" aria-controls="sidebar">
|
|
680
818
|
<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>
|
|
681
819
|
</button>
|
|
682
820
|
<div class="nav">
|
|
821
|
+
<button type="button" id="palOpen" class="cmdk" aria-label="Search views and projects" aria-haspopup="dialog">
|
|
822
|
+
<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>
|
|
823
|
+
<span>Search</span>
|
|
824
|
+
<span class="kbd" id="palHint">Ctrl K</span>
|
|
825
|
+
</button>
|
|
683
826
|
<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>
|
|
684
827
|
<span class="live"><span id="dot" class="dot stale"></span><span id="updated">Connecting...</span></span>
|
|
685
828
|
</div>
|
|
@@ -934,20 +1077,59 @@
|
|
|
934
1077
|
</div>
|
|
935
1078
|
<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>
|
|
936
1079
|
</div>
|
|
937
|
-
<div class="
|
|
938
|
-
<div class="
|
|
939
|
-
|
|
940
|
-
|
|
1080
|
+
<div class="gate-bar">
|
|
1081
|
+
<div class="gate-stat">
|
|
1082
|
+
<button id="gateOnBtn" type="button" class="toggle" data-kind="bool" role="switch" aria-checked="false" aria-label="AI quality gate" title="Auto-driven after a code task"></button>
|
|
1083
|
+
<div class="gate-stat-t"><span class="gate-stat-n">Gate</span><span id="gateOn" class="gate-stat-v">-</span></div>
|
|
1084
|
+
</div>
|
|
1085
|
+
<div class="gate-stat">
|
|
1086
|
+
<button id="gateDaemonBtn" type="button" class="toggle" data-kind="bool" role="switch" aria-checked="false" aria-label="Gate daemon" title="Runs the pipeline in the background"></button>
|
|
1087
|
+
<div class="gate-stat-t"><span class="gate-stat-n">Daemon</span><span id="gateDaemon" class="gate-stat-v">-</span></div>
|
|
1088
|
+
</div>
|
|
1089
|
+
<div class="gate-stat gate-stat-scope">
|
|
1090
|
+
<div class="gate-stat-t"><span class="gate-stat-n">Scope</span><span id="gateRepo" class="gate-stat-v">-</span></div>
|
|
1091
|
+
<span id="gateRepoFoot" class="gate-stat-f"></span>
|
|
1092
|
+
</div>
|
|
941
1093
|
</div>
|
|
942
1094
|
|
|
943
1095
|
<div class="panel">
|
|
944
|
-
<div class="panel-head"><h2>Runs <small id="gateRunsSub">most recent first</small></h2></div>
|
|
1096
|
+
<div class="panel-head"><h2>Runs <small id="gateRunsSub">most recent first</small></h2><span id="gateLive" class="set-hint"></span></div>
|
|
945
1097
|
<div id="gateRuns"><div class="empty" style="padding:12px 0">Loading...</div></div>
|
|
946
1098
|
</div>
|
|
947
1099
|
|
|
1100
|
+
<div class="panel" id="gateSetPanel">
|
|
1101
|
+
<div class="set-cat-h">Pipeline settings <code id="gateCfgPath"></code></div>
|
|
1102
|
+
<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>
|
|
1103
|
+
<div class="set-grid">
|
|
1104
|
+
<div class="set-row">
|
|
1105
|
+
<div class="set-meta"><div class="set-name">Agent</div><div class="set-hint" id="gateAgentHint">which coding agent the pipeline drives</div></div>
|
|
1106
|
+
<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>
|
|
1107
|
+
</div>
|
|
1108
|
+
<div class="set-row" id="gateModelRow">
|
|
1109
|
+
<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>
|
|
1110
|
+
<div class="set-ctl ladd" style="align-items:center"><select id="gateModelSel" class="linput" style="flex:none;width:240px"></select><input id="gateModel" class="linput" style="flex:none;width:200px" spellcheck="false" autocapitalize="none" autocorrect="off" placeholder="model id"><button id="gateModelSave" type="button" class="toggle on">Save</button></div>
|
|
1111
|
+
</div>
|
|
1112
|
+
<div class="set-row">
|
|
1113
|
+
<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>
|
|
1114
|
+
<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>
|
|
1115
|
+
</div>
|
|
1116
|
+
<div class="set-row">
|
|
1117
|
+
<div class="set-meta"><div class="set-name">Log level</div><div class="set-hint">daemon output verbosity</div></div>
|
|
1118
|
+
<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>
|
|
1119
|
+
</div>
|
|
1120
|
+
<div class="set-row">
|
|
1121
|
+
<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>
|
|
1122
|
+
<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>
|
|
1123
|
+
</div>
|
|
1124
|
+
</div>
|
|
1125
|
+
<div class="set-cat-h" style="margin-top:18px">Auto-fix attempts</div>
|
|
1126
|
+
<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>
|
|
1127
|
+
<div class="set-grid" id="gateAutoFix"></div>
|
|
1128
|
+
</div>
|
|
1129
|
+
|
|
948
1130
|
<div class="panel">
|
|
949
|
-
<div class="set-cat-h">Global configuration <code
|
|
950
|
-
<div class="sub" style="margin-bottom:10px">
|
|
1131
|
+
<div class="set-cat-h">Global configuration <code class="gate-cfg-path"></code></div>
|
|
1132
|
+
<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>
|
|
951
1133
|
<textarea id="gateCfg" class="linput gate-yaml" spellcheck="false" autocapitalize="none" autocorrect="off" aria-label="Global gate configuration"></textarea>
|
|
952
1134
|
<div class="ladd" style="gap:8px;align-items:center">
|
|
953
1135
|
<button id="gateCfgSave" type="button" class="toggle on" disabled title="No changes to save">Save</button>
|
|
@@ -4158,12 +4340,29 @@ export {
|
|
|
4158
4340
|
if (ms < 60000) return (ms / 1000).toFixed(1) + "s";
|
|
4159
4341
|
return Math.round(ms / 60000) + "m " + Math.round((ms % 60000) / 1000) + "s";
|
|
4160
4342
|
}
|
|
4343
|
+
// Server-minus-browser clock offset, so a step's live elapsed is measured against the
|
|
4344
|
+
// clock that stamped started_at rather than against this machine's.
|
|
4345
|
+
let gateClockSkew = 0;
|
|
4346
|
+
// A step only gets its duration written when a round ends, so a step that is running or
|
|
4347
|
+
// in a fix round would otherwise freeze at its last round. Tick it from started_at instead.
|
|
4348
|
+
const GATE_ACTIVE_STEP = { running: 1, fixing: 1, awaiting_approval: 1, fix_review: 1 };
|
|
4349
|
+
function gateStepDur(s) {
|
|
4350
|
+
if (GATE_ACTIVE_STEP[s.status] && s.startedAt) {
|
|
4351
|
+
return gateDur(Math.max(0, Date.now() + gateClockSkew - s.startedAt * 1000));
|
|
4352
|
+
}
|
|
4353
|
+
return s.durationMs ? gateDur(s.durationMs) : "";
|
|
4354
|
+
}
|
|
4355
|
+
function gateStepsHtml(steps) {
|
|
4356
|
+
return (steps || []).map((s) => {
|
|
4357
|
+
const d = gateStepDur(s);
|
|
4358
|
+
return '<span class="gate-step ' + esc(s.status) + '">' + esc(s.name)
|
|
4359
|
+
+ (d ? " · " + esc(d) : "")
|
|
4360
|
+
+ (s.findings ? " · " + s.findings : "") + "</span>";
|
|
4361
|
+
}).join("");
|
|
4362
|
+
}
|
|
4161
4363
|
function gateRunRow(r) {
|
|
4162
4364
|
const pr = r.prUrl ? ' <a href="' + esc(r.prUrl) + '" target="_blank" rel="noopener noreferrer">PR</a>' : "";
|
|
4163
|
-
const steps = (r.steps
|
|
4164
|
-
'<span class="gate-step ' + esc(s.status) + '">' + esc(s.name)
|
|
4165
|
-
+ (s.durationMs ? " · " + esc(gateDur(s.durationMs)) : "")
|
|
4166
|
-
+ (s.findings ? " · " + s.findings : "") + "</span>").join("");
|
|
4365
|
+
const steps = gateStepsHtml(r.steps);
|
|
4167
4366
|
const findings = (r.findings || []).length
|
|
4168
4367
|
? '<div class="gate-find">' + r.findings.map((f) =>
|
|
4169
4368
|
'<div class="gate-find-row"><b>' + esc(f.id) + "</b> · " + esc(f.severity) + " · " + esc(f.action)
|
|
@@ -4176,21 +4375,120 @@ export {
|
|
|
4176
4375
|
+ '<span class="gate-run-meta">' + esc(r.headSha) + pr + "</span>"
|
|
4177
4376
|
+ "</div>" + '<div class="gate-steps">' + steps + "</div>" + findings + err + "</div>";
|
|
4178
4377
|
}
|
|
4179
|
-
|
|
4378
|
+
// The two on/off switches at the top of the page. Both are optimistic: the switch flips
|
|
4379
|
+
// first and rolls back if the server refuses, like every other control here.
|
|
4380
|
+
function gateBool(btn, on) {
|
|
4381
|
+
btn.textContent = on ? "on" : "off";
|
|
4382
|
+
btn.classList.toggle("on", !!on);
|
|
4383
|
+
btn.dataset.on = on ? "1" : "0";
|
|
4384
|
+
btn.setAttribute("aria-checked", on ? "true" : "false");
|
|
4385
|
+
}
|
|
4386
|
+
const GATE_AUTOFIX = [
|
|
4387
|
+
["rebase", "conflicts and stale-base failures"],
|
|
4388
|
+
["review", "0 by default: review findings are yours to approve"],
|
|
4389
|
+
["test", "failing tests"],
|
|
4390
|
+
["document", "gaps the initial documentation pass left"],
|
|
4391
|
+
["lint", "lint and formatting failures"],
|
|
4392
|
+
["ci", "CI failures and merge conflicts on the open PR"]
|
|
4393
|
+
];
|
|
4394
|
+
// Rendered once per non-live load; the number inputs write on change, so a re-render
|
|
4395
|
+
// mid-edit would drop what is being typed.
|
|
4396
|
+
// Model choices per agent. Claude Code's `--model` takes these aliases, so they can be
|
|
4397
|
+
// offered as a real list; codex and opencode take a provider-specific string, and a
|
|
4398
|
+
// guessed list there would be worse than none - they get auto plus Custom.
|
|
4399
|
+
const GATE_MODELS = {
|
|
4400
|
+
claude: [
|
|
4401
|
+
["claude-opus-5", "claude-opus-5 (most capable)"],
|
|
4402
|
+
["claude-sonnet-5", "claude-sonnet-5 (faster, cheaper)"],
|
|
4403
|
+
["claude-haiku-4-5", "claude-haiku-4-5 (fastest)"],
|
|
4404
|
+
["claude-opus-4-8", "claude-opus-4-8 (previous Opus)"],
|
|
4405
|
+
],
|
|
4406
|
+
};
|
|
4407
|
+
const GATE_MODEL_CUSTOM = "__custom__";
|
|
4408
|
+
let gateSavedModel = "";
|
|
4409
|
+
|
|
4410
|
+
/** Rebuilds the model dropdown for `agent` and selects `model` (empty = the agent default). */
|
|
4411
|
+
function renderGateModel(agent, model) {
|
|
4412
|
+
const sel = $("gateModelSel"), inp = $("gateModel");
|
|
4413
|
+
const opts = [["", "auto (agent default)"]].concat(GATE_MODELS[agent] || [], [[GATE_MODEL_CUSTOM, "Custom model id..."]]);
|
|
4414
|
+
sel.innerHTML = opts.map(([v, label]) => '<option value="' + esc(v) + '">' + esc(label) + "</option>").join("");
|
|
4415
|
+
const listed = opts.some(([v]) => v === model && v !== GATE_MODEL_CUSTOM);
|
|
4416
|
+
sel.value = listed ? model : GATE_MODEL_CUSTOM;
|
|
4417
|
+
syncDD(sel);
|
|
4418
|
+
inp.value = model;
|
|
4419
|
+
gateSavedModel = model;
|
|
4420
|
+
gateModelMode();
|
|
4421
|
+
}
|
|
4422
|
+
/** Shows the free-text field and its Save only while "Custom" is selected. */
|
|
4423
|
+
function gateModelMode() {
|
|
4424
|
+
const custom = $("gateModelSel").value === GATE_MODEL_CUSTOM;
|
|
4425
|
+
$("gateModel").style.display = custom ? "" : "none";
|
|
4426
|
+
$("gateModelSave").style.display = custom ? "" : "none";
|
|
4427
|
+
syncGateModelSave();
|
|
4428
|
+
}
|
|
4429
|
+
/** No-op rule: nothing to save while the typed id still equals the stored one. */
|
|
4430
|
+
function syncGateModelSave() {
|
|
4431
|
+
const btn = $("gateModelSave"), dirty = $("gateModel").value.trim() !== gateSavedModel;
|
|
4432
|
+
btn.disabled = !gateWritable || !dirty;
|
|
4433
|
+
btn.title = dirty ? "" : "No changes to save";
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4436
|
+
function renderGateSettings(s) {
|
|
4437
|
+
const panel = $("gateSetPanel");
|
|
4438
|
+
if (!s) { panel.style.display = "none"; return; }
|
|
4439
|
+
panel.style.display = "";
|
|
4440
|
+
// An `acp:<target>` agent set from the raw editor is not one of the fixed options;
|
|
4441
|
+
// carry it as one so the select shows the truth instead of going blank.
|
|
4442
|
+
const agentSel = $("gateAgent");
|
|
4443
|
+
if (!Array.prototype.some.call(agentSel.options, (o) => o.value === s.agent)) {
|
|
4444
|
+
agentSel.add(new Option(s.agent, s.agent));
|
|
4445
|
+
}
|
|
4446
|
+
agentSel.value = s.agent;
|
|
4447
|
+
syncDD(agentSel);
|
|
4448
|
+
$("gateAgentHint").textContent = s.agent === "auto"
|
|
4449
|
+
? "auto: first agent found on PATH - currently " + s.agentResolved
|
|
4450
|
+
: "which coding agent the pipeline drives";
|
|
4451
|
+
$("gateModelRow").style.display = s.modelSupported ? "" : "none";
|
|
4452
|
+
renderGateModel(s.agent, s.model || "");
|
|
4453
|
+
$("gateModelHint").textContent = "passed to " + s.agentResolved
|
|
4454
|
+
+ " on every step; a smaller model is the one change that cuts both run time and tokens";
|
|
4455
|
+
$("gateCiTimeout").value = s.ciTimeout || "";
|
|
4456
|
+
$("gateLogLevel").value = s.logLevel;
|
|
4457
|
+
syncDD($("gateLogLevel"));
|
|
4458
|
+
gateBool($("gateIntent"), s.intentEnabled);
|
|
4459
|
+
$("gateAutoFix").innerHTML = GATE_AUTOFIX.map(([step, hint]) =>
|
|
4460
|
+
'<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(step) + '</div><div class="set-hint">' + esc(hint) + "</div></div>"
|
|
4461
|
+
+ '<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("");
|
|
4462
|
+
const ctls = panel.querySelectorAll("input, select, button");
|
|
4463
|
+
for (const c of ctls) c.disabled = !gateWritable;
|
|
4464
|
+
syncGateModelSave(); // after the sweep, which would otherwise re-enable a clean Save
|
|
4465
|
+
}
|
|
4466
|
+
// `live` marks a poll refresh: it repaints the run strip and the two switches, and leaves
|
|
4467
|
+
// every editor alone so a poll can never overwrite what is being typed.
|
|
4468
|
+
function renderGate(g, live) {
|
|
4180
4469
|
g = g || {};
|
|
4470
|
+
if (typeof g.serverNow === "number") gateClockSkew = g.serverNow * 1000 - Date.now();
|
|
4181
4471
|
$("gateOn").textContent = g.on ? "On" : "Off";
|
|
4182
|
-
$("gateOn").className = "
|
|
4472
|
+
$("gateOn").className = "gate-stat-v " + (g.on ? "good" : "");
|
|
4473
|
+
gateBool($("gateOnBtn"), g.on);
|
|
4183
4474
|
$("gateDaemon").textContent = g.daemon ? "Running" : "Stopped";
|
|
4184
|
-
$("gateDaemon").className = "
|
|
4475
|
+
$("gateDaemon").className = "gate-stat-v " + (g.daemon ? "good" : "");
|
|
4476
|
+
gateBool($("gateDaemonBtn"), g.daemon);
|
|
4185
4477
|
const repo = g.repo;
|
|
4186
4478
|
$("gateRepo").textContent = repo ? (repo.initialized ? "Initialized" : "Not initialized") : "Global";
|
|
4187
4479
|
$("gateRepoFoot").textContent = repo ? repo.path : "Every active run on this machine";
|
|
4480
|
+
$("gateRepoFoot").title = $("gateRepoFoot").textContent; // the path truncates; keep it recoverable
|
|
4188
4481
|
$("gateRunsSub").textContent = repo ? "most recent first, this repository" : "active runs across every repository";
|
|
4189
4482
|
const runs = g.runs || [];
|
|
4483
|
+
gateRuns = runs;
|
|
4190
4484
|
$("gateRuns").innerHTML = runs.length
|
|
4191
4485
|
? runs.map(gateRunRow).join("")
|
|
4192
4486
|
: '<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>";
|
|
4193
|
-
|
|
4487
|
+
gatePoll();
|
|
4488
|
+
if (live) return;
|
|
4489
|
+
const cfgPath = (g.globalConfig || {}).path || "";
|
|
4490
|
+
$("gateCfgPath").textContent = cfgPath;
|
|
4491
|
+
for (const el of document.querySelectorAll(".gate-cfg-path")) el.textContent = cfgPath;
|
|
4194
4492
|
gateSaved.global = (g.globalConfig || {}).text || "";
|
|
4195
4493
|
$("gateCfg").value = gateSaved.global;
|
|
4196
4494
|
// A runtime that cannot parse a gate config must not write one: read-only, with the reason.
|
|
@@ -4199,6 +4497,7 @@ export {
|
|
|
4199
4497
|
$("gateRepoCfg").readOnly = !gateWritable;
|
|
4200
4498
|
if (!gateWritable) { setNote("gateCfgNote", g.writeNote || "Read-only here.", "err"); setNote("gateRepoCfgNote", g.writeNote || "Read-only here.", "err"); }
|
|
4201
4499
|
syncGateSave("global");
|
|
4500
|
+
renderGateSettings(g.settings);
|
|
4202
4501
|
$("gateRepoPanel").style.display = repo ? "" : "none";
|
|
4203
4502
|
if (repo) {
|
|
4204
4503
|
$("gateRepoCfgPath").textContent = repo.configPath || "";
|
|
@@ -4207,18 +4506,48 @@ export {
|
|
|
4207
4506
|
syncGateSave("repo");
|
|
4208
4507
|
}
|
|
4209
4508
|
}
|
|
4210
|
-
|
|
4509
|
+
|
|
4510
|
+
// --- keeping the run strip live -------------------------------------------------
|
|
4511
|
+
// A gate run takes tens of minutes and the page has no push channel, so without this the
|
|
4512
|
+
// strip shows whatever was true when the page was opened. Two loops: a 5s refetch while
|
|
4513
|
+
// something is in flight (20s when idle) and a 1s repaint so the active step's elapsed
|
|
4514
|
+
// counts up between fetches. Both stop when the tab is hidden or the view is not open.
|
|
4515
|
+
let gateRuns = [], gateFetchTimer = null, gateTickTimer = null;
|
|
4516
|
+
function gateBusy() {
|
|
4517
|
+
return gateRuns.some((r) => r.status === "running" || r.status === "awaiting_approval");
|
|
4518
|
+
}
|
|
4519
|
+
function gateStopPoll() {
|
|
4520
|
+
clearTimeout(gateFetchTimer); gateFetchTimer = null;
|
|
4521
|
+
clearInterval(gateTickTimer); gateTickTimer = null;
|
|
4522
|
+
$("gateLive").textContent = "";
|
|
4523
|
+
}
|
|
4524
|
+
function gatePoll() {
|
|
4525
|
+
gateStopPoll();
|
|
4526
|
+
if (activeView !== "gate" || document.hidden) return;
|
|
4527
|
+
const busy = gateBusy();
|
|
4528
|
+
$("gateLive").textContent = busy ? "live" : "";
|
|
4529
|
+
gateFetchTimer = setTimeout(() => loadGate(true), busy ? 5000 : 20000);
|
|
4530
|
+
if (!busy) return;
|
|
4531
|
+
// Repaint only the step strips: replacing whole rows every second would drop a text
|
|
4532
|
+
// selection inside the findings of a parked run.
|
|
4533
|
+
gateTickTimer = setInterval(() => {
|
|
4534
|
+
const strips = $("gateRuns").querySelectorAll(".gate-steps");
|
|
4535
|
+
strips.forEach((el, i) => { if (gateRuns[i]) el.innerHTML = gateStepsHtml(gateRuns[i].steps); });
|
|
4536
|
+
}, 1000);
|
|
4537
|
+
}
|
|
4538
|
+
async function loadGate(live) {
|
|
4211
4539
|
const q = isProjectScope() ? "?path=" + encodeURIComponent(currentScope) : "";
|
|
4212
|
-
const note = (msg) => { $("gateRuns").innerHTML = '<div class="empty" style="padding:12px 0">' + msg + "</div>"; };
|
|
4540
|
+
const note = (msg) => { gateStopPoll(); $("gateRuns").innerHTML = '<div class="empty" style="padding:12px 0">' + msg + "</div>"; };
|
|
4213
4541
|
try {
|
|
4214
4542
|
const res = await fetch("/api/gate" + q, { cache: "no-store" });
|
|
4215
4543
|
// The UI bundle updates on its own; the route it calls ships with the CLI, so a
|
|
4216
4544
|
// 404 here means the running server predates this view rather than being down.
|
|
4217
4545
|
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; }
|
|
4218
4546
|
if (!res.ok) { note("The server refused the request (" + res.status + ")."); return; }
|
|
4219
|
-
renderGate(await res.json());
|
|
4547
|
+
renderGate(await res.json(), live);
|
|
4220
4548
|
} catch { note("Could not reach the server."); }
|
|
4221
4549
|
}
|
|
4550
|
+
document.addEventListener("visibilitychange", () => { if (activeView === "gate") { if (document.hidden) gateStopPoll(); else loadGate(true); } });
|
|
4222
4551
|
async function saveGateCfg(which) {
|
|
4223
4552
|
const { area, note } = gateEls(which);
|
|
4224
4553
|
if (area.value === gateSaved[which]) return; // no-op: nothing changed
|
|
@@ -4231,11 +4560,60 @@ export {
|
|
|
4231
4560
|
if (out.ok) { gateSaved[which] = area.value; syncGateSave(which); }
|
|
4232
4561
|
} catch { setNote(note, "Could not reach the server.", "err"); }
|
|
4233
4562
|
}
|
|
4234
|
-
|
|
4563
|
+
/** One form control -> one key of the global config. Reloads so the file stays the truth. */
|
|
4564
|
+
async function saveGateSetting(key, value, label) {
|
|
4565
|
+
setNote("gateCfgNote", "Saving " + label + "...");
|
|
4566
|
+
try {
|
|
4567
|
+
const out = await (await fetch("/api/gate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "setting", key, value }) })).json();
|
|
4568
|
+
setNote("gateCfgNote", out.message || (out.ok ? "Saved." : "Not saved."), out.ok ? "ok" : "err");
|
|
4569
|
+
await loadGate();
|
|
4570
|
+
} catch { setNote("gateCfgNote", "Could not reach the server.", "err"); await loadGate(); }
|
|
4571
|
+
}
|
|
4572
|
+
async function gateDaemonPost(on) {
|
|
4573
|
+
gateBool($("gateDaemonBtn"), on); // optimistic; the reload below is the truth
|
|
4574
|
+
setNote("gateCfgNote", on ? "Starting the daemon..." : "Stopping the daemon...");
|
|
4575
|
+
try {
|
|
4576
|
+
const out = await (await fetch("/api/gate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "daemon", on }) })).json();
|
|
4577
|
+
setNote("gateCfgNote", out.message || (out.ok ? "Done." : "Failed."), out.ok ? "ok" : "err");
|
|
4578
|
+
} catch { setNote("gateCfgNote", "Could not reach the server.", "err"); }
|
|
4579
|
+
await loadGate();
|
|
4580
|
+
}
|
|
4581
|
+
// The gate on/off flag is a normal enigma setting, so it goes through the settings route
|
|
4582
|
+
// the Settings page uses rather than growing a second writer for the same key.
|
|
4583
|
+
async function gateOnPost(on) {
|
|
4584
|
+
gateBool($("gateOnBtn"), on);
|
|
4585
|
+
setNote("gateCfgNote", on ? "Turning the gate on..." : "Turning the gate off...");
|
|
4586
|
+
try {
|
|
4587
|
+
const out = await (await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "gate", value: on }) })).json();
|
|
4588
|
+
setNote("gateCfgNote", out.ok ? (out.restartNote || "Saved. Restart your agent to apply.") : ("Could not change the gate: " + (out.error || "error")), out.ok ? "warn" : "err");
|
|
4589
|
+
} catch { setNote("gateCfgNote", "Could not reach the server.", "err"); }
|
|
4590
|
+
await loadGate();
|
|
4591
|
+
}
|
|
4592
|
+
$("gateRefresh").addEventListener("click", () => loadGate());
|
|
4235
4593
|
$("gateCfg").addEventListener("input", () => syncGateSave("global"));
|
|
4236
4594
|
$("gateRepoCfg").addEventListener("input", () => syncGateSave("repo"));
|
|
4237
4595
|
$("gateCfgSave").addEventListener("click", () => saveGateCfg("global"));
|
|
4238
4596
|
$("gateRepoCfgSave").addEventListener("click", () => saveGateCfg("repo"));
|
|
4597
|
+
$("gateOnBtn").addEventListener("click", () => gateOnPost($("gateOnBtn").dataset.on !== "1"));
|
|
4598
|
+
$("gateDaemonBtn").addEventListener("click", () => gateDaemonPost($("gateDaemonBtn").dataset.on !== "1"));
|
|
4599
|
+
$("gateAgent").addEventListener("change", (e) => saveGateSetting("agent", e.target.value, "the agent"));
|
|
4600
|
+
$("gateLogLevel").addEventListener("change", (e) => saveGateSetting("log_level", e.target.value, "the log level"));
|
|
4601
|
+
$("gateIntent").addEventListener("click", () => saveGateSetting("intent.enabled", $("gateIntent").dataset.on !== "1", "intent extraction"));
|
|
4602
|
+
// A listed model commits on pick, like the agent and log-level selects; "Custom" reveals
|
|
4603
|
+
// the field and waits for Save, since a half-typed model id must not be written.
|
|
4604
|
+
$("gateModelSel").addEventListener("change", () => {
|
|
4605
|
+
gateModelMode();
|
|
4606
|
+
if ($("gateModelSel").value === GATE_MODEL_CUSTOM) { $("gateModel").focus(); return; }
|
|
4607
|
+
saveGateSetting("model", $("gateModelSel").value, "the model");
|
|
4608
|
+
});
|
|
4609
|
+
$("gateModel").addEventListener("input", syncGateModelSave);
|
|
4610
|
+
$("gateModelSave").addEventListener("click", () => saveGateSetting("model", $("gateModel").value.trim(), "the model"));
|
|
4611
|
+
$("gateCiSave").addEventListener("click", () => saveGateSetting("ci_timeout", $("gateCiTimeout").value.trim(), "the CI timeout"));
|
|
4612
|
+
// Number inputs commit on change (blur or Enter), so typing "12" is not read as "1" then "12".
|
|
4613
|
+
$("gateAutoFix").addEventListener("change", (e) => {
|
|
4614
|
+
const step = e.target.dataset && e.target.dataset.gateAf;
|
|
4615
|
+
if (step) saveGateSetting("auto_fix." + step, Number(e.target.value), "the " + step + " limit");
|
|
4616
|
+
});
|
|
4239
4617
|
|
|
4240
4618
|
// --- section rail (a full-screen drawer below 900px) ---
|
|
4241
4619
|
const sidebarEl = $("sidebar"), navScrimEl = $("navScrim"), navOpenBtn = $("navOpen");
|
|
@@ -4262,6 +4640,149 @@ export {
|
|
|
4262
4640
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && sidebarEl.classList.contains("open")) setNav(false); });
|
|
4263
4641
|
window.matchMedia("(min-width: 901px)").addEventListener("change", (e) => { if (e.matches) setNav(false); });
|
|
4264
4642
|
|
|
4643
|
+
// --- command palette (Cmd/Ctrl+K) ---------------------------------------------
|
|
4644
|
+
// Twelve views plus every registered project is more than the rail can answer by
|
|
4645
|
+
// pointing, so typing is the second way in (frontend-policy, "Ctrl+K Opens A Command
|
|
4646
|
+
// Palette"). enigma: the dashboard is a single dependency-free HTML file served over
|
|
4647
|
+
// loopback, so the ~20-line matcher below stands in for fuse.js - upgrade to fuse.js
|
|
4648
|
+
// if this page ever gains a build step.
|
|
4649
|
+
const palBack = $("palBack"), palInput = $("palInput"), palList = $("palList");
|
|
4650
|
+
const IS_MAC = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
4651
|
+
let palItems = [], palSel = 0, palReturnFocus = null;
|
|
4652
|
+
$("palHint").textContent = IS_MAC ? "Cmd K" : "Ctrl K";
|
|
4653
|
+
|
|
4654
|
+
/**
|
|
4655
|
+
* Fuzzy rank of `query` inside `text`: -1 when the query is not a subsequence, otherwise
|
|
4656
|
+
* a score that rewards consecutive runs and word starts. Beats a substring filter because
|
|
4657
|
+
* "qgate" finds "Quality gate" and "plyg" finds "Playground", which .includes() cannot.
|
|
4658
|
+
*/
|
|
4659
|
+
function palScore(query, text) {
|
|
4660
|
+
const q = query.toLowerCase(), t = text.toLowerCase();
|
|
4661
|
+
if (!q) return 0;
|
|
4662
|
+
let at = 0, from = 0, score = 0, run = 0;
|
|
4663
|
+
for (let i = 0; i < q.length; i++) {
|
|
4664
|
+
at = t.indexOf(q[i], from);
|
|
4665
|
+
if (at === -1) return -1;
|
|
4666
|
+
run = (i > 0 && at === from) ? run + 1 : 0;
|
|
4667
|
+
const wordStart = at === 0 || /[^a-z0-9]/.test(t[at - 1]);
|
|
4668
|
+
score += 10 + run * 6 + (wordStart ? 8 : 0) - Math.min(at - from, 6);
|
|
4669
|
+
from = at + 1;
|
|
4670
|
+
}
|
|
4671
|
+
return score;
|
|
4672
|
+
}
|
|
4673
|
+
|
|
4674
|
+
/** Built from the live DOM on every open, so it can never drift from the rail or the scopes. */
|
|
4675
|
+
function palIndex() {
|
|
4676
|
+
const out = [];
|
|
4677
|
+
document.querySelectorAll(".tabs .tab").forEach((t) => {
|
|
4678
|
+
if (t.classList.contains("disabled")) return; // out of reach in this scope
|
|
4679
|
+
const group = t.closest(".nav-group");
|
|
4680
|
+
const label = (t.querySelector("span") || t).textContent.trim();
|
|
4681
|
+
const icon = t.querySelector(".tab-ic");
|
|
4682
|
+
out.push({
|
|
4683
|
+
kind: "View", name: label, hint: t.getAttribute("href"),
|
|
4684
|
+
icon: icon ? icon.outerHTML : "",
|
|
4685
|
+
group: group && group.querySelector(".nav-group-t") ? group.querySelector(".nav-group-t").textContent.trim() : "",
|
|
4686
|
+
run: () => { location.hash = t.getAttribute("href"); },
|
|
4687
|
+
});
|
|
4688
|
+
});
|
|
4689
|
+
const sel = $("scopeSel");
|
|
4690
|
+
if (sel) Array.from(sel.options).forEach((o) => {
|
|
4691
|
+
if (o.value === "__create__" || o.value === currentScope) return;
|
|
4692
|
+
out.push({
|
|
4693
|
+
kind: "Scope", name: o.textContent.trim(), hint: o.value === "global" ? "" : "project", icon: "", group: "",
|
|
4694
|
+
run: () => { sel.value = o.value; syncDD(sel); sel.dispatchEvent(new Event("change")); },
|
|
4695
|
+
});
|
|
4696
|
+
});
|
|
4697
|
+
return out;
|
|
4698
|
+
}
|
|
4699
|
+
|
|
4700
|
+
function palRender(query) {
|
|
4701
|
+
const q = query.trim();
|
|
4702
|
+
const all = palIndex();
|
|
4703
|
+
palItems = (!q ? all : all
|
|
4704
|
+
.map((it) => ({ it, s: Math.max(palScore(q, it.name), palScore(q, it.kind + " " + it.name) - 4) }))
|
|
4705
|
+
.filter((r) => r.s >= 0)
|
|
4706
|
+
.sort((a, b) => b.s - a.s)
|
|
4707
|
+
.map((r) => r.it));
|
|
4708
|
+
palSel = 0;
|
|
4709
|
+
if (!palItems.length) {
|
|
4710
|
+
palList.innerHTML = '<div class="pal-empty">No view or project matches "' + esc(q) + '".</div>';
|
|
4711
|
+
palInput.removeAttribute("aria-activedescendant");
|
|
4712
|
+
return;
|
|
4713
|
+
}
|
|
4714
|
+
let html = "", kind = null;
|
|
4715
|
+
palItems.forEach((it, i) => {
|
|
4716
|
+
if (it.kind !== kind) { kind = it.kind; html += '<div class="pal-gt">' + esc(kind === "View" ? "Views" : "Switch scope") + "</div>"; }
|
|
4717
|
+
html += '<div class="pal-row' + (i === 0 ? " sel" : "") + '" role="option" id="pal-o' + i + '"'
|
|
4718
|
+
+ ' aria-selected="' + (i === 0 ? "true" : "false") + '" data-i="' + i + '">'
|
|
4719
|
+
+ it.icon
|
|
4720
|
+
+ '<span class="pal-name">' + esc(it.name) + "</span>"
|
|
4721
|
+
+ (it.group ? '<span class="pal-hint">' + esc(it.group) + "</span>" : (it.hint ? '<span class="pal-hint">' + esc(it.hint) + "</span>" : ""))
|
|
4722
|
+
+ "</div>";
|
|
4723
|
+
});
|
|
4724
|
+
palList.innerHTML = html;
|
|
4725
|
+
palMove(0);
|
|
4726
|
+
}
|
|
4727
|
+
|
|
4728
|
+
function palMove(next) {
|
|
4729
|
+
if (!palItems.length) return;
|
|
4730
|
+
const rows = palList.querySelectorAll(".pal-row");
|
|
4731
|
+
palSel = (next + palItems.length) % palItems.length;
|
|
4732
|
+
rows.forEach((r, i) => {
|
|
4733
|
+
const on = i === palSel;
|
|
4734
|
+
r.classList.toggle("sel", on);
|
|
4735
|
+
r.setAttribute("aria-selected", on ? "true" : "false");
|
|
4736
|
+
if (on) { r.scrollIntoView({ block: "nearest" }); palInput.setAttribute("aria-activedescendant", r.id); }
|
|
4737
|
+
});
|
|
4738
|
+
}
|
|
4739
|
+
|
|
4740
|
+
function palOpen() {
|
|
4741
|
+
if (!palBack.hidden) return;
|
|
4742
|
+
palReturnFocus = document.activeElement;
|
|
4743
|
+
palBack.hidden = false;
|
|
4744
|
+
document.body.classList.add("nav-open"); // reuses the drawer's scroll lock
|
|
4745
|
+
requestAnimationFrame(() => palBack.classList.add("show"));
|
|
4746
|
+
palInput.value = "";
|
|
4747
|
+
palRender("");
|
|
4748
|
+
palInput.focus();
|
|
4749
|
+
}
|
|
4750
|
+
function palClose() {
|
|
4751
|
+
if (palBack.hidden) return;
|
|
4752
|
+
palBack.classList.remove("show");
|
|
4753
|
+
setTimeout(() => { palBack.hidden = true; }, 200);
|
|
4754
|
+
if (!sidebarEl.classList.contains("open")) document.body.classList.remove("nav-open");
|
|
4755
|
+
if (palReturnFocus) { try { palReturnFocus.focus(); } catch { /* gone from the DOM */ } palReturnFocus = null; }
|
|
4756
|
+
}
|
|
4757
|
+
function palRun() {
|
|
4758
|
+
const it = palItems[palSel];
|
|
4759
|
+
if (!it) return;
|
|
4760
|
+
palClose();
|
|
4761
|
+
setNav(false);
|
|
4762
|
+
it.run();
|
|
4763
|
+
}
|
|
4764
|
+
|
|
4765
|
+
$("palOpen").addEventListener("click", palOpen);
|
|
4766
|
+
palInput.addEventListener("input", () => palRender(palInput.value));
|
|
4767
|
+
palList.addEventListener("mousemove", (e) => { const r = e.target.closest(".pal-row"); if (r) palMove(Number(r.dataset.i)); });
|
|
4768
|
+
palList.addEventListener("click", (e) => { const r = e.target.closest(".pal-row"); if (r) { palMove(Number(r.dataset.i)); palRun(); } });
|
|
4769
|
+
palBack.addEventListener("mousedown", (e) => { if (e.target === palBack) palClose(); });
|
|
4770
|
+
// Focus stays inside: the dialog holds one input and a list driven from it.
|
|
4771
|
+
palBack.addEventListener("keydown", (e) => {
|
|
4772
|
+
if (e.key === "ArrowDown") { e.preventDefault(); palMove(palSel + 1); }
|
|
4773
|
+
else if (e.key === "ArrowUp") { e.preventDefault(); palMove(palSel - 1); }
|
|
4774
|
+
else if (e.key === "Enter") { e.preventDefault(); palRun(); }
|
|
4775
|
+
// Escape closes the topmost surface only: the drawer behind it stays open.
|
|
4776
|
+
else if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); palClose(); }
|
|
4777
|
+
else if (e.key === "Tab") e.preventDefault();
|
|
4778
|
+
});
|
|
4779
|
+
document.addEventListener("keydown", (e) => {
|
|
4780
|
+
// Ctrl+K is the browser's own search-bar shortcut, so it has to be taken explicitly.
|
|
4781
|
+
if (e.key.toLowerCase() !== "k" || !(IS_MAC ? e.metaKey : e.ctrlKey) || e.altKey) return;
|
|
4782
|
+
e.preventDefault();
|
|
4783
|
+
if (palBack.hidden) palOpen(); else palClose();
|
|
4784
|
+
});
|
|
4785
|
+
|
|
4265
4786
|
// --- hash routing between the subpages ---
|
|
4266
4787
|
// Single source of truth for the subpage ids (savings is the default/fallback).
|
|
4267
4788
|
const VIEWS = ["savings", "usage", "accounts", "skills", "settings", "gate", "resources", "recall", "codegraph", "playground", "packs", "ssh"];
|
|
@@ -4277,7 +4798,20 @@ export {
|
|
|
4277
4798
|
if (project && PROJECT_VIEWS.indexOf(v) === -1) v = "settings";
|
|
4278
4799
|
activeView = v;
|
|
4279
4800
|
VIEWS.forEach((view) => { $("view-" + view).style.display = view === v ? "" : "none"; });
|
|
4280
|
-
document.querySelectorAll(".tab").forEach((t) =>
|
|
4801
|
+
document.querySelectorAll(".tab").forEach((t) => {
|
|
4802
|
+
const on = t.dataset.view === v;
|
|
4803
|
+
t.classList.toggle("active", on);
|
|
4804
|
+
// Colour alone does not carry the active state to a screen reader.
|
|
4805
|
+
if (on) t.setAttribute("aria-current", "page"); else t.removeAttribute("aria-current");
|
|
4806
|
+
});
|
|
4807
|
+
// A group whose every entry is out of scope would otherwise render as a lone caption.
|
|
4808
|
+
document.querySelectorAll(".nav-group").forEach((g) => {
|
|
4809
|
+
const tabs = Array.from(g.querySelectorAll(".tab"));
|
|
4810
|
+
g.hidden = tabs.length > 0 && tabs.every((t) => t.classList.contains("disabled"));
|
|
4811
|
+
});
|
|
4812
|
+
// Leaving the view must not keep polling for runs. Kept OUT of the chain below: an
|
|
4813
|
+
// `if` in the middle of it detaches every later `else if` from the dispatch.
|
|
4814
|
+
if (v !== "gate") gateStopPoll();
|
|
4281
4815
|
// Settings + Skills always reload (they are scope-dependent); the rest keep their once-load.
|
|
4282
4816
|
if (v === "settings") loadSettings();
|
|
4283
4817
|
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.98",
|
|
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": [
|