akv-manager 0.0.1 → 0.0.4

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.
@@ -0,0 +1,369 @@
1
+ :root{
2
+ --ink:#0A0D14; --ink2:#0C1019;
3
+ --panel:#121826; --panel2:#0E141F; --raise:#171F2E;
4
+ --line:#222C3E; --line2:#1A2233;
5
+ --text:#E6EBF2; --muted:#79859B; --faint:#4C586E;
6
+ --azure:#5B9DFF; --azure2:#3F86F0; --azuredim:rgba(91,157,255,.12);
7
+ --ok:#46D6B4; --warn:#F2B53D; --alert:#FF5C6C; --bad:#FF5C6C; --good:#46D6B4;
8
+ --violet:#A98BFF;
9
+ --r:12px; --r-sm:8px;
10
+ --shadow:0 18px 40px -22px rgba(0,0,0,.8);
11
+ --mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
12
+ --sans:'IBM Plex Sans',-apple-system,BlinkMacSystemFont,sans-serif;
13
+ }
14
+ *{box-sizing:border-box}
15
+ html,body{margin:0}
16
+ body{font-family:var(--sans);font-size:14px;line-height:1.5;color:var(--text);min-height:100vh;
17
+ background:
18
+ radial-gradient(900px 500px at 88% -8%,rgba(91,157,255,.07),transparent 60%),
19
+ linear-gradient(180deg,var(--ink2),var(--ink));
20
+ -webkit-font-smoothing:antialiased}
21
+ ::selection{background:rgba(91,157,255,.32)}
22
+ a{color:var(--azure);text-decoration:none}
23
+ :focus-visible{outline:2px solid var(--azure);outline-offset:2px;border-radius:4px}
24
+ .mono{font-family:var(--mono);font-variant-numeric:tabular-nums}
25
+ .muted{color:var(--muted)} .faint{color:var(--faint)} .right{text-align:right}
26
+ .hide{display:none!important}
27
+ .eyebrow{font-family:var(--mono);font-size:10.5px;font-weight:500;letter-spacing:.18em;
28
+ text-transform:uppercase;color:var(--faint)}
29
+
30
+ /* page skeleton (.app grid, main, .topbar) now uses Tailwind utilities in index.html */
31
+
32
+ /* ---------- sidebar ---------- */
33
+ aside{background:linear-gradient(180deg,rgba(255,255,255,.015),transparent),var(--panel2);
34
+ border-right:1px solid var(--line);padding:22px 14px;display:flex;flex-direction:column;
35
+ position:sticky;top:0;height:100vh}
36
+ .brand{display:flex;align-items:center;gap:11px;padding:0 6px 20px}
37
+ .brand .mark{width:34px;height:34px;flex:none}
38
+ .brand .wm{font-family:var(--mono);font-weight:600;font-size:13px;letter-spacing:.14em;color:var(--text);line-height:1.15}
39
+ .brand .wm small{display:block;font-size:9.5px;letter-spacing:.22em;color:var(--faint);font-weight:400;margin-top:2px}
40
+ .navlabel{font-family:var(--mono);font-size:9.5px;letter-spacing:.2em;color:var(--faint);
41
+ text-transform:uppercase;padding:14px 10px 7px}
42
+ nav button{display:flex;align-items:center;gap:11px;width:100%;text-align:left;background:transparent;
43
+ border:0;color:var(--muted);padding:9px 11px;border-radius:var(--r-sm);cursor:pointer;
44
+ font-family:var(--sans);font-size:13.5px;font-weight:500;position:relative;transition:.15s}
45
+ nav button svg{width:17px;height:17px;flex:none;opacity:.8}
46
+ nav button:hover{background:rgba(255,255,255,.03);color:var(--text)}
47
+ nav button.active{background:var(--azuredim);color:#fff}
48
+ nav button.active svg{opacity:1;color:var(--azure)}
49
+ nav button.active::before{content:"";position:absolute;left:-14px;top:7px;bottom:7px;width:3px;
50
+ background:var(--azure);border-radius:0 3px 3px 0}
51
+ .who{margin-top:auto;font-size:12px;color:var(--muted);border-top:1px solid var(--line2);
52
+ padding:14px 8px 4px;line-height:1.6}
53
+ .who b{color:var(--text);font-weight:600}
54
+ .who .mono{font-size:11px}
55
+ .dot{width:7px;height:7px;border-radius:50%;display:inline-block;vertical-align:middle}
56
+ .ver{font-size:10px;letter-spacing:.04em;padding:8px 8px 2px}
57
+
58
+ /* ---------- main ---------- */
59
+ h1{font-size:23px;font-weight:600;margin:4px 0 0;letter-spacing:-.01em}
60
+ .sub{color:var(--muted);font-size:13px;margin-top:3px;max-width:62ch}
61
+ .row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
62
+
63
+ .btn{font-family:var(--sans);font-weight:500;font-size:13px;border:0;border-radius:var(--r-sm);
64
+ padding:9px 15px;cursor:pointer;color:#06101f;background:var(--azure);transition:.15s}
65
+ .btn:hover{background:#75AEFF}
66
+ .btn:disabled{opacity:.55;cursor:default}
67
+ .btn.ghost{background:var(--raise);color:var(--text);border:1px solid var(--line)}
68
+ .btn.ghost:hover{border-color:var(--azure);background:var(--raise)}
69
+ .btn.sm{padding:5px 11px;font-size:12px}
70
+ input.search{font-family:var(--mono);background:var(--panel2);border:1px solid var(--line);color:var(--text);
71
+ padding:9px 13px;border-radius:var(--r-sm);min-width:320px;font-size:12.5px}
72
+ input.search::placeholder{color:var(--faint)}
73
+ input.search:focus{outline:none;border-color:var(--azure);background:var(--panel)}
74
+
75
+ /* ---------- KPI cards ---------- */
76
+ .cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:13px;margin-bottom:24px}
77
+ .card{background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--line);
78
+ border-radius:var(--r);padding:15px 17px;position:relative;overflow:hidden}
79
+ .card::after{content:"";position:absolute;inset:0 0 auto;height:2px;background:var(--line)}
80
+ .card .k{font-family:var(--mono);color:var(--muted);font-size:10.5px;text-transform:uppercase;letter-spacing:.13em}
81
+ .card .v{font-family:var(--mono);font-size:30px;font-weight:600;margin-top:9px;line-height:1;letter-spacing:-.02em}
82
+ .card .v small{font-family:var(--sans);font-size:12px;font-weight:400;color:var(--muted);letter-spacing:0}
83
+ .card--alert::after{background:var(--alert)} .card--alert .v{color:#FF8893}
84
+ .card--warn::after{background:var(--warn)} .card--warn .v{color:#FBC56B}
85
+ .card--azure::after{background:var(--azure)} .card--azure .v{color:#8FBEFF}
86
+
87
+ /* ---------- chips ---------- */
88
+ .chips{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px}
89
+ .chip{font-family:var(--mono);background:var(--panel2);border:1px solid var(--line);color:var(--muted);
90
+ padding:6px 12px;border-radius:7px;cursor:pointer;font-size:11.5px;display:flex;gap:8px;align-items:center;transition:.15s}
91
+ .chip:hover{color:var(--text);border-color:var(--line)}
92
+ .chip.active{border-color:var(--azure);color:#fff;background:var(--azuredim)}
93
+ .chip .n{background:var(--ink);border-radius:5px;padding:1px 7px;font-size:10.5px;color:var(--muted)}
94
+ .chip.active .n{color:var(--azure)}
95
+
96
+ /* ---------- panel + table ---------- */
97
+ .panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);overflow:hidden;box-shadow:var(--shadow)}
98
+ table{width:100%;border-collapse:collapse;font-size:13px}
99
+ th,td{padding:11px 16px;text-align:left;border-bottom:1px solid var(--line2);white-space:nowrap}
100
+ thead th{position:sticky;top:0;z-index:2;background:var(--panel2);color:var(--muted);font-weight:500;
101
+ font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.1em;
102
+ cursor:pointer;user-select:none}
103
+ thead th:hover{color:var(--text)}
104
+ tbody tr{transition:background .12s}
105
+ tbody tr:hover{background:rgba(91,157,255,.05)}
106
+ tbody tr:last-child td{border-bottom:0}
107
+ td.mono,.mono{font-family:var(--mono);font-size:12.5px}
108
+ td b{font-weight:600}
109
+
110
+ .tag{display:inline-flex;align-items:center;font-family:var(--mono);font-size:10px;font-weight:500;
111
+ letter-spacing:.08em;text-transform:uppercase;padding:2px 7px;border-radius:5px;border:1px solid transparent}
112
+ .tag.e-dev{color:#7FE0C8;background:rgba(70,214,180,.10);border-color:rgba(70,214,180,.22)}
113
+ .tag.e-test{color:#7FD0E0;background:rgba(70,180,214,.10);border-color:rgba(70,180,214,.22)}
114
+ .tag.e-qa{color:#93C0FF;background:rgba(91,157,255,.12);border-color:rgba(91,157,255,.24)}
115
+ .tag.e-stage{color:#F4C977;background:rgba(242,181,61,.10);border-color:rgba(242,181,61,.22)}
116
+ .tag.e-prod{color:#FF9BA4;background:rgba(255,92,108,.10);border-color:rgba(255,92,108,.26)}
117
+ .tag.e-sandbox{color:#BCA8FF;background:rgba(169,139,255,.10);border-color:rgba(169,139,255,.22)}
118
+ .tag.e-other{color:var(--muted);background:rgba(255,255,255,.04);border-color:var(--line)}
119
+
120
+ .pill{font-family:var(--mono);font-size:10px;letter-spacing:.06em;color:var(--muted);
121
+ border:1px solid var(--line);border-radius:5px;padding:1px 7px;white-space:nowrap}
122
+ .pill.rbac{color:#7FE0C8;border-color:rgba(70,214,180,.3)}
123
+ .pill.policy{color:#F4C977;border-color:rgba(242,181,61,.3)}
124
+ .sev{font-family:var(--mono);font-size:10px;letter-spacing:.1em;padding:2px 8px;border-radius:5px;border:1px solid}
125
+ .sev.expired{color:#FF8893;background:rgba(255,92,108,.12);border-color:rgba(255,92,108,.32)}
126
+ .sev.urgent{color:#FBC56B;background:rgba(242,181,61,.12);border-color:rgba(242,181,61,.32)}
127
+ .sev.ok{color:var(--muted);border-color:var(--line);background:transparent}
128
+
129
+ .iconbtn{font-family:var(--mono);background:var(--raise);border:1px solid var(--line);color:var(--muted);
130
+ border-radius:6px;padding:4px 9px;cursor:pointer;font-size:11.5px;transition:.15s}
131
+ .iconbtn:hover{color:#fff;border-color:var(--azure)}
132
+ .empty{padding:48px 24px;text-align:center;color:var(--muted)}
133
+ .scroll{max-height:calc(100vh - 380px);overflow:auto}
134
+ .scroll::-webkit-scrollbar{width:10px;height:10px}
135
+ .scroll::-webkit-scrollbar-thumb{background:var(--line);border-radius:8px;border:2px solid var(--panel)}
136
+ .tablenote{font-family:var(--mono);font-size:11px;color:var(--faint);margin-top:10px}
137
+
138
+ /* ---------- signature: expiry horizon ---------- */
139
+ .horizon{padding:18px 20px;margin-bottom:22px}
140
+ .hz-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:14px}
141
+ .hz-body{display:flex;gap:12px;height:104px}
142
+ .hz-expired{position:relative;width:118px;flex:none;border:1px solid rgba(255,92,108,.28);
143
+ background:linear-gradient(180deg,rgba(255,92,108,.12),rgba(255,92,108,.02));
144
+ border-radius:10px;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:hidden}
145
+ .hz-expired-n{font-family:var(--mono);font-size:30px;font-weight:600;color:#FF8893;line-height:1}
146
+ .hz-expired-l{font-family:var(--mono);font-size:9.5px;letter-spacing:.18em;color:#FF8893;margin-top:3px}
147
+ .hz-axis{position:relative;flex:1;border:1px solid var(--line);border-radius:10px;
148
+ background:linear-gradient(180deg,rgba(255,255,255,.015),transparent);overflow:hidden}
149
+ .hz-grid{position:absolute;top:0;bottom:0;border-left:1px dashed var(--line);}
150
+ .hz-grid:first-of-type{border-left-color:rgba(91,157,255,.35)}
151
+ .hz-grid span{position:absolute;bottom:7px;left:6px;font-family:var(--mono);font-size:9.5px;color:var(--faint);white-space:nowrap}
152
+ .hz-tick{position:absolute;top:12px;bottom:26px;width:2px;border-radius:2px;background:var(--ok);
153
+ transform:translateX(-1px);opacity:.9}
154
+ .hz-tick.warn{background:var(--warn)} .hz-tick.alert{background:var(--alert)}
155
+ .hz-tick:hover{opacity:1;width:3px;box-shadow:0 0 8px currentColor}
156
+ .hz-expired .hz-tick{top:auto;bottom:9px;height:16px;opacity:.7}
157
+ .hz-legend{display:flex;gap:16px;align-items:center}
158
+ .hz-legend span{font-family:var(--mono);font-size:10px;color:var(--muted);display:flex;align-items:center;gap:5px}
159
+ .hz-legend i{width:8px;height:8px;border-radius:2px;display:inline-block}
160
+
161
+ /* ---------- segmented + window ---------- */
162
+ .seg{display:inline-flex;gap:4px;background:var(--panel2);border:1px solid var(--line);
163
+ border-radius:var(--r-sm);padding:3px}
164
+ .seg button{font-family:var(--mono);background:transparent;border:0;color:var(--muted);padding:6px 12px;
165
+ border-radius:6px;cursor:pointer;font-size:12px;transition:.15s}
166
+ .seg button:hover{color:var(--text)}
167
+ .seg button.active{background:var(--raise);color:#fff;box-shadow:0 1px 0 rgba(0,0,0,.4)}
168
+
169
+ /* ---------- drawer ---------- */
170
+ .overlay{position:fixed;inset:0;background:rgba(5,8,14,.6);backdrop-filter:blur(2px);opacity:0;
171
+ pointer-events:none;transition:.25s;z-index:30}
172
+ .overlay.show{opacity:1;pointer-events:auto}
173
+ .drawer{position:fixed;top:0;right:0;height:100vh;width:100vw;max-width:100vw;background:var(--panel);
174
+ border-left:1px solid var(--line);box-shadow:-30px 0 80px -30px rgba(0,0,0,.9);
175
+ transform:translateX(100%);transition:transform .28s cubic-bezier(.2,.7,.2,1);z-index:40;
176
+ display:flex;flex-direction:column}
177
+ .drawer.open{transform:translateX(0)}
178
+ .drawer header{padding:20px 22px;border-bottom:1px solid var(--line);display:flex;
179
+ justify-content:space-between;align-items:flex-start;gap:14px}
180
+ .drawer header .dt{font-family:var(--mono);font-size:16px;font-weight:600;word-break:break-all}
181
+ .drawer .body{padding:18px 22px;overflow:auto}
182
+ .kvitem{border-bottom:1px solid var(--line2)}
183
+ .kvitem:last-child{border-bottom:0}
184
+ .kvitem.sel{background:rgba(91,157,255,.06);margin:0 -22px;padding:0 22px}
185
+ .kvline{display:flex;align-items:center;gap:10px;padding:10px 0}
186
+ .kvline .nm{flex:1 1 auto;min-width:60px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono);font-size:12.5px}
187
+ .kvline .meta{display:flex;align-items:center;gap:8px;flex:0 0 auto}
188
+ .kvline input[type=checkbox]{flex:0 0 auto}
189
+ .revealed{padding:0 0 12px;display:flex;align-items:flex-start;gap:8px}
190
+ .revealed code.val{display:block;flex:1;min-width:0;background:var(--ink);border:1px solid var(--line);
191
+ border-radius:6px;padding:8px 10px;max-height:120px;overflow:auto}
192
+ .cmd{background:var(--ink);border:1px solid var(--line);border-radius:var(--r-sm);padding:10px 12px;
193
+ margin:8px 0;font-family:var(--mono);font-size:11.5px;color:#A9D2FF;display:flex;justify-content:space-between;gap:10px;align-items:center}
194
+ .secthead{font-family:var(--mono);margin:24px 0 6px;color:var(--faint);font-size:10px;text-transform:uppercase;letter-spacing:.16em}
195
+ .loadval{font-family:var(--mono);font-size:11px;color:var(--azure);cursor:pointer;background:none;border:0;text-decoration:underline}
196
+ code.val{font-family:var(--mono);user-select:all;color:#A9D2FF;font-size:11.5px;word-break:break-all}
197
+ /* drawer edit + batch */
198
+ .dtools{display:flex;align-items:center;gap:12px;margin-top:14px}
199
+ .dtools .search{min-width:0;flex:1;padding:8px 11px}
200
+ .selall{font-family:var(--mono);font-size:11px;color:var(--muted);display:flex;align-items:center;gap:6px;cursor:pointer;white-space:nowrap}
201
+ input[type=checkbox]{accent-color:var(--azure);width:14px;height:14px;cursor:pointer}
202
+ .batchbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:12px;padding:10px 12px;
203
+ background:var(--azuredim);border:1px solid rgba(91,157,255,.4);border-radius:var(--r-sm)}
204
+ .batchbar .bn{font-family:var(--mono);font-size:12px;color:#cfe2ff;margin-right:2px}
205
+ .batchbar input[type=date]{font-family:var(--mono);font-size:12px;background:var(--ink);border:1px solid var(--line);
206
+ color:var(--text);border-radius:6px;padding:5px 8px;color-scheme:dark}
207
+ .editor{margin:0 -22px;padding:14px 22px;background:var(--panel2);border-top:1px solid var(--line2)}
208
+ /* create panel */
209
+ .createp{margin-top:12px;padding:16px;background:var(--panel2);border:1px solid var(--azure);border-radius:var(--r-sm)}
210
+ .createp h4{margin:0 0 12px;font-family:var(--mono);font-size:12px;color:#cfe2ff;font-weight:500;letter-spacing:.04em}
211
+ .createp .fld{display:flex;align-items:center;gap:10px;margin-bottom:10px}
212
+ .createp label{font-family:var(--mono);font-size:11px;color:var(--muted);width:72px;flex:none}
213
+ .createp input[type=text],.createp input[type=date],.createp textarea{font-family:var(--mono);font-size:12px;
214
+ background:var(--ink);border:1px solid var(--line);color:var(--text);border-radius:6px;padding:8px 10px;flex:1;width:100%;color-scheme:dark}
215
+ .createp textarea{min-height:120px;resize:vertical;line-height:1.6}
216
+ .createp .actions{display:flex;gap:8px;align-items:center;margin-top:6px}
217
+ .newbtns{display:flex;gap:6px;margin-left:auto}
218
+ .editor .fld{display:flex;align-items:center;gap:10px;margin-bottom:10px}
219
+ .editor label{font-family:var(--mono);font-size:11px;color:var(--muted);width:78px;flex:none}
220
+ .editor input[type=text],.editor input[type=date]{font-family:var(--mono);font-size:12px;background:var(--ink);
221
+ border:1px solid var(--line);color:var(--text);border-radius:6px;padding:7px 9px;flex:1;color-scheme:dark}
222
+ .editor input[type=date],.createp input[type=date]{flex:0 0 auto;width:200px}
223
+ .editor .hint{font-family:var(--mono);font-size:10.5px;color:var(--faint)}
224
+ .toggle{display:flex;align-items:center;gap:7px;font-family:var(--mono);font-size:12px;color:var(--text);cursor:pointer}
225
+
226
+ /* ---------- misc ---------- */
227
+ .toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(20px);
228
+ background:var(--raise);border:1px solid var(--azure);color:#fff;padding:10px 18px;
229
+ border-radius:var(--r-sm);opacity:0;transition:.25s;z-index:60;font-size:13px}
230
+ .toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
231
+ .banner{background:linear-gradient(180deg,rgba(255,92,108,.14),rgba(255,92,108,.04));
232
+ border:1px solid rgba(255,92,108,.4);color:#FFC4C9;padding:13px 16px;border-radius:var(--r);
233
+ margin-bottom:18px;display:none;font-size:13px}
234
+ .banner.show{display:block}
235
+ .banner code{font-family:var(--mono);background:rgba(0,0,0,.3);padding:1px 6px;border-radius:4px;color:#fff}
236
+ .spin{width:18px;height:18px;border:2px solid var(--line);border-top-color:var(--azure);border-radius:50%;
237
+ display:inline-block;animation:spin .7s linear infinite;vertical-align:-4px}
238
+ @keyframes spin{to{transform:rotate(360deg)}}
239
+ .progress{width:64%;max-width:380px;height:6px;background:var(--ink);border:1px solid var(--line);
240
+ border-radius:999px;margin:16px auto 0;overflow:hidden}
241
+ .progress .bar{height:100%;background:linear-gradient(90deg,var(--azure),var(--ok));border-radius:999px;transition:width .4s}
242
+
243
+ /* ---------- sidebar: pinned / recent + account switcher ---------- */
244
+ .navpins{display:flex;flex-direction:column;gap:1px;margin:2px 0 4px}
245
+ .navpin{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:transparent;border:0;
246
+ color:var(--muted);padding:6px 11px;border-radius:var(--r-sm);cursor:pointer;font-family:var(--mono);
247
+ font-size:11.5px;transition:.12s;overflow:hidden}
248
+ .navpin:hover{background:rgba(255,255,255,.03);color:var(--text)}
249
+ .navpin.active{background:var(--azuredim);color:#fff}
250
+ .navpin .pn{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
251
+ .navpin .star{color:var(--warn);font-size:10px;flex:none}
252
+ .navpin .env-dot{width:6px;height:6px;border-radius:50%;flex:none}
253
+ .navempty{font-family:var(--mono);font-size:10.5px;color:var(--faint);padding:4px 12px}
254
+ .acct{margin-top:auto;border-top:1px solid var(--line2);padding-top:10px}
255
+ .acctbtn{display:flex;align-items:center;gap:9px;width:100%;text-align:left;background:transparent;border:0;
256
+ color:var(--muted);padding:8px;border-radius:var(--r-sm);cursor:pointer;font-size:12px;transition:.12s;line-height:1.5}
257
+ .acctbtn:hover{background:rgba(255,255,255,.03)}
258
+ .acctbtn b{color:var(--text);font-weight:600}
259
+ .acctbtn .chev{margin-left:auto;color:var(--faint);flex:none}
260
+
261
+ /* ---------- command palette (⌘K) ---------- */
262
+ .cmdk-ov{position:fixed;inset:0;background:rgba(5,8,14,.55);backdrop-filter:blur(3px);opacity:0;
263
+ pointer-events:none;transition:.14s;z-index:70;display:flex;align-items:flex-start;justify-content:center;padding-top:12vh}
264
+ .cmdk-ov.show{opacity:1;pointer-events:auto}
265
+ .cmdk{width:min(640px,92vw);background:var(--panel);border:1px solid var(--line);border-radius:14px;
266
+ box-shadow:0 40px 90px -30px rgba(0,0,0,.85);overflow:hidden;transform:translateY(-8px);transition:.16s}
267
+ .cmdk-ov.show .cmdk{transform:translateY(0)}
268
+ .cmdk-in{display:flex;align-items:center;gap:11px;padding:15px 18px;border-bottom:1px solid var(--line)}
269
+ .cmdk-in svg{width:17px;height:17px;color:var(--faint);flex:none}
270
+ .cmdk-in input{flex:1;background:transparent;border:0;outline:0;color:var(--text);font-family:var(--sans);font-size:15px}
271
+ .cmdk-in input::placeholder{color:var(--faint)}
272
+ .cmdk-in kbd{font-family:var(--mono);font-size:10px;color:var(--faint);border:1px solid var(--line);border-radius:5px;padding:2px 6px}
273
+ .cmdk-list{max-height:54vh;overflow:auto;padding:7px}
274
+ .cmdk-sec{font-family:var(--mono);font-size:9.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--faint);padding:9px 11px 5px}
275
+ .cmdk-it{display:flex;align-items:center;gap:11px;padding:9px 11px;border-radius:8px;cursor:pointer}
276
+ .cmdk-it .ico{width:16px;height:16px;color:var(--muted);flex:none}
277
+ .cmdk-it .lbl{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13.5px}
278
+ .cmdk-it .lbl small{color:var(--faint);font-family:var(--mono);font-size:11px;margin-left:8px}
279
+ .cmdk-it .hint{font-family:var(--mono);font-size:10px;color:var(--faint)}
280
+ .cmdk-it.cur{background:var(--azuredim)}
281
+ .cmdk-it.cur .ico{color:var(--azure)}
282
+ .cmdk-empty{padding:26px;text-align:center;color:var(--muted);font-size:13px}
283
+
284
+ /* ---------- dashboard ---------- */
285
+ .actcards{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:13px;margin-bottom:26px}
286
+ .actcard{display:block;text-align:left;background:linear-gradient(180deg,var(--panel),var(--panel2));
287
+ border:1px solid var(--line);border-radius:var(--r);padding:16px 17px;cursor:pointer;position:relative;
288
+ overflow:hidden;transition:.15s;font-family:inherit}
289
+ .actcard::after{content:"";position:absolute;inset:0 0 auto;height:2px;background:var(--line)}
290
+ .actcard:hover{border-color:var(--azure);transform:translateY(-1px)}
291
+ .actcard .k{display:flex;align-items:center;gap:8px;font-family:var(--mono);color:var(--muted);font-size:10.5px;
292
+ text-transform:uppercase;letter-spacing:.12em}
293
+ .actcard .k svg{width:14px;height:14px}
294
+ .actcard .v{font-family:var(--mono);font-size:32px;font-weight:600;margin-top:11px;line-height:1;letter-spacing:-.02em}
295
+ .actcard .sub{font-family:var(--sans);font-size:12px;color:var(--muted);margin-top:6px}
296
+ .actcard.sev-alert::after{background:var(--alert)} .actcard.sev-alert .v{color:#FF8893}
297
+ .actcard.sev-warn::after{background:var(--warn)} .actcard.sev-warn .v{color:#FBC56B}
298
+ .actcard.sev-azure::after{background:var(--azure)} .actcard.sev-azure .v{color:#8FBEFF}
299
+ .actcard.sev-ok::after{background:var(--ok)} .actcard.sev-ok .v{color:#7FE0C8}
300
+ .dashgrid{display:grid;grid-template-columns:1fr 1fr;gap:22px;align-items:start}
301
+ @media (max-width:1100px){.dashgrid{grid-template-columns:1fr}}
302
+ .secthead2{display:flex;align-items:center;justify-content:space-between;margin:0 0 12px}
303
+ .secthead2 .eyebrow{font-size:11px}
304
+ .vtiles{display:flex;flex-direction:column;gap:7px}
305
+ .vtile{display:flex;align-items:center;gap:11px;background:var(--panel);border:1px solid var(--line);
306
+ border-radius:10px;padding:11px 13px;cursor:pointer;transition:.13s}
307
+ .vtile:hover{border-color:var(--azure);background:var(--raise)}
308
+ .vtile .star{color:var(--warn);font-size:12px;flex:none;background:none;border:0;cursor:pointer;padding:0}
309
+ .vtile .star.off{color:var(--faint)}
310
+ .vtile .nm{flex:1;font-family:var(--mono);font-size:12.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
311
+ .vtile .rg{font-family:var(--mono);font-size:11px;color:var(--faint);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:40%}
312
+
313
+ /* ---------- facets (tag filtering) ---------- */
314
+ .facets{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:14px}
315
+ .facet{display:flex;align-items:center;gap:6px}
316
+ .facet label{font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
317
+ select.facetsel{font-family:var(--mono);font-size:12px;background:var(--panel2);border:1px solid var(--line);
318
+ color:var(--text);border-radius:7px;padding:6px 9px;cursor:pointer;color-scheme:dark;max-width:200px}
319
+ select.facetsel:focus{outline:none;border-color:var(--azure)}
320
+ select.facetsel.on{border-color:var(--azure);color:#fff}
321
+ .facetclear{font-family:var(--mono);font-size:11px;color:var(--azure);background:none;border:0;cursor:pointer}
322
+
323
+ /* ---------- tag chips + editor ---------- */
324
+ .tagchips{display:flex;gap:5px;flex-wrap:wrap}
325
+ .tagchip{font-family:var(--mono);font-size:10px;color:var(--muted);background:rgba(255,255,255,.04);
326
+ border:1px solid var(--line);border-radius:5px;padding:1px 7px;white-space:nowrap}
327
+ .tagchip b{color:var(--text);font-weight:500}
328
+ .tagedit{display:flex;flex-direction:column;gap:9px;max-width:680px}
329
+ .tagrow{display:flex;align-items:center;gap:9px}
330
+ .tagrow input{font-family:var(--mono);font-size:12px;background:var(--ink);border:1px solid var(--line);
331
+ color:var(--text);border-radius:6px;padding:8px 10px;color-scheme:dark}
332
+ .tagrow .tk{width:230px;flex:none} .tagrow .tv{flex:1}
333
+ .tagrow .del{flex:none}
334
+ .tagrow.fixed input{opacity:.6}
335
+
336
+ /* ---------- routed vault page ---------- */
337
+ .vhead{margin-bottom:18px}
338
+ .crumb{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:6px}
339
+ .crumb a{font-family:var(--mono);font-size:12px;color:var(--muted)}
340
+ .crumb a:hover{color:var(--text)}
341
+ .crumb .sep{color:var(--faint)}
342
+ .crumb .here{font-family:var(--mono);font-size:16px;font-weight:600;color:var(--text);word-break:break-all}
343
+ .star{background:none;border:0;cursor:pointer;color:var(--faint);font-size:16px;padding:0 2px;line-height:1}
344
+ .star.on{color:var(--warn)}
345
+ .vmeta{display:flex;align-items:center;gap:9px;flex-wrap:wrap;font-family:var(--mono);font-size:11.5px;color:var(--muted)}
346
+ .vmeta .mono{word-break:break-all}
347
+ .tabs{display:flex;gap:2px;border-bottom:1px solid var(--line);margin-bottom:16px}
348
+ .tabs button{font-family:var(--sans);font-size:13px;font-weight:500;background:transparent;border:0;
349
+ color:var(--muted);padding:10px 15px;cursor:pointer;position:relative;transition:.12s}
350
+ .tabs button:hover{color:var(--text)}
351
+ .tabs button.active{color:#fff}
352
+ .tabs button.active::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--azure)}
353
+ .tabs button .c{font-family:var(--mono);font-size:10.5px;color:var(--faint);margin-left:6px}
354
+ .vtools{display:flex;flex-direction:column;gap:10px;margin-bottom:6px}
355
+ .vtools-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
356
+ .vtools-row .search{flex:1;min-width:0}
357
+ .vtools-actions{margin-left:auto;display:flex;gap:7px}
358
+ .stick{position:sticky;top:0;z-index:5}
359
+ .itemwrap{margin-top:8px}
360
+
361
+ @media (max-width:900px){
362
+ /* .app grid + main padding handled by Tailwind max-[900px]: utilities */
363
+ aside{position:static;height:auto;flex-direction:row;flex-wrap:wrap;align-items:center}
364
+ aside nav{display:flex;gap:4px;flex-wrap:wrap} .navlabel{display:none} .who{display:none}
365
+ .navpins,.acct{display:none}
366
+ input.search{min-width:180px}
367
+ .dashgrid{grid-template-columns:1fr}
368
+ }
369
+ @media (prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;transition:none!important}}
package/server.js CHANGED
@@ -31,17 +31,25 @@ const SUB_RE = /^[0-9a-fA-F-]{1,40}$|^[\w .()-]{1,80}$/; // sub id or display na
31
31
 
32
32
  function isValidName(v) { return typeof v === 'string' && NAME_RE.test(v); }
33
33
 
34
- /** Run az with an args array (no shell => no injection). Returns parsed JSON. */
34
+ /** Run az with an args array. Returns parsed JSON.
35
+ * az() only runs trusted, fixed commands (get-access-token / logout) — never user
36
+ * input — so enabling the shell on Windows (required to launch az.cmd) is safe here. */
35
37
  function az(args, { json = true } = {}) {
36
38
  const finalArgs = json ? [...args, '--output', 'json'] : args;
39
+ const isWin = process.platform === 'win32';
37
40
  return new Promise((resolve, reject) => {
38
- execFile(AZ, finalArgs, { timeout: AZ_TIMEOUT, maxBuffer: 64 * 1024 * 1024 },
39
- (err, stdout, stderr) => {
41
+ execFile(AZ, finalArgs, {
42
+ timeout: AZ_TIMEOUT, maxBuffer: 64 * 1024 * 1024,
43
+ shell: isWin, // on Windows the CLI is `az.cmd`; execFile can't run it without a shell
44
+ windowsHide: true,
45
+ }, (err, stdout, stderr) => {
40
46
  if (err) {
41
47
  const msg = (stderr || err.message || '').trim();
42
- return reject(Object.assign(new Error(msg || 'az command failed'), {
43
- azError: true,
44
- needLogin: /AADSTS70043|refresh token|az login|InvalidAuthenticationToken|expired/i.test(msg),
48
+ const notFound = err.code === 'ENOENT' || /not recognized|cannot find the (path|file)|ENOENT/i.test(msg);
49
+ return reject(Object.assign(new Error(
50
+ notFound ? `Azure CLI not found (tried "${AZ}"). Install the Azure CLI, or set AZ_PATH to its full path.` : (msg || 'az command failed')), {
51
+ azError: true, notFound,
52
+ needLogin: /AADSTS70043|refresh token|please run ['"]?az login|InvalidAuthenticationToken|expired|no subscription|not logged in/i.test(msg),
45
53
  forbidden: /Forbidden|does not have secrets|AccessDenied|not authorized|403/i.test(msg),
46
54
  stderr: msg, args: finalArgs,
47
55
  }));
@@ -260,7 +268,8 @@ const VAULT_QUERY =
260
268
  "| project name, resourceGroup, subscriptionId, location, " +
261
269
  "sku=tostring(properties.sku.name), " +
262
270
  "enableRbac=tostring(properties.enableRbacAuthorization), " +
263
- "uri=tostring(properties.vaultUri) | order by name asc";
271
+ "uri=tostring(properties.vaultUri), tags " +
272
+ "| order by name asc";
264
273
 
265
274
  async function listVaultsRaw(force) {
266
275
  if (!force && vaultCache.data && Date.now() - vaultCache.at < VAULT_TTL) return vaultCache.data;
@@ -284,6 +293,65 @@ app.get('/api/vaults', async (req, res) => {
284
293
  }
285
294
  });
286
295
 
296
+ // Find a vault's management-plane coordinates (sub + rg) from the cached inventory.
297
+ async function findVaultMeta(name) {
298
+ const vaults = await listVaultsRaw();
299
+ return vaults.find((v) => (v.name || '').toLowerCase() === name.toLowerCase());
300
+ }
301
+
302
+ // "Default" / governance tags that this tool must never edit or remove — they are
303
+ // applied by Azure Policy / automation and are read-only here. Patterns are matched
304
+ // case-insensitively; a trailing '*' is a prefix wildcard (e.g. "automation.*").
305
+ // Operators can override the whole list with AZBO_PROTECTED_TAGS (comma-separated);
306
+ // set it to an empty string to disable protection entirely.
307
+ const PROTECTED_TAGS = (() => {
308
+ const def = ['env', 'environment', 'cost-center', 'costcenter', 'contact-*', 'owner',
309
+ 'solution', 'location', 'roles', 'managed-by', 'managedby',
310
+ 'expirationdate', 'runninginterval', 'automation.*', 'hidden-*'];
311
+ const raw = process.env.AZBO_PROTECTED_TAGS;
312
+ if (raw === undefined) return def;
313
+ return raw.split(',').map((s) => s.trim()).filter(Boolean);
314
+ })();
315
+ function tagMatch(pattern, key) {
316
+ const p = pattern.toLowerCase(), k = String(key).toLowerCase();
317
+ return p.endsWith('*') ? k.startsWith(p.slice(0, -1)) : k === p;
318
+ }
319
+ function isProtectedTag(key) { return PROTECTED_TAGS.some((p) => tagMatch(p, key)); }
320
+
321
+ // Replace a vault's tag set (management plane). The body's `tags` object is the
322
+ // COMPLETE desired set — Key Vault's PATCH overwrites the whole tags collection.
323
+ // Protected/default tags are force-preserved from the current state, so they can be
324
+ // neither edited nor removed regardless of what the client sends.
325
+ // Requires management-plane write (e.g. Contributor / Tag Contributor) on the vault.
326
+ app.put('/api/vaults/:name/tags', (req, res) => {
327
+ const { name } = req.params;
328
+ if (!isValidName(name)) return res.status(400).json({ ok: false, error: 'Invalid vault name' });
329
+ const { tags } = req.body || {};
330
+ if (!tags || typeof tags !== 'object' || Array.isArray(tags))
331
+ return res.status(400).json({ ok: false, error: 'tags must be an object of name → value' });
332
+ const entries = Object.entries(tags);
333
+ if (entries.length > 50) return res.status(400).json({ ok: false, error: 'Azure allows at most 50 tags per resource' });
334
+ for (const [k, v] of entries) {
335
+ if (typeof k !== 'string' || !k.length || k.length > 512) return res.status(400).json({ ok: false, error: `Invalid tag name: ${k}` });
336
+ if (typeof v !== 'string' || v.length > 256) return res.status(400).json({ ok: false, error: `Invalid value for tag "${k}" (max 256 chars)` });
337
+ }
338
+ send(res, (async () => {
339
+ const v = await findVaultMeta(name);
340
+ if (!v) throw new Error('Vault not found in inventory — try Refresh first');
341
+ const current = v.tags || {};
342
+ // Start from the client's desired set, then force every protected tag that exists
343
+ // today back to its original value — this blocks edit AND removal of default tags.
344
+ const finalTags = {};
345
+ for (const [k, val] of entries) { if (!isProtectedTag(k)) finalTags[k] = val; }
346
+ for (const [k, val] of Object.entries(current)) { if (isProtectedTag(k)) finalTags[k] = val; }
347
+ const url = `/subscriptions/${v.subscriptionId}/resourceGroups/${v.resourceGroup}` +
348
+ `/providers/Microsoft.KeyVault/vaults/${name}?api-version=2023-07-01`;
349
+ const out = await armFetch(url, { method: 'PATCH', body: { tags: finalTags } });
350
+ v.tags = (out && out.tags) || finalTags; // reflect immediately in the cached inventory
351
+ return { tags: v.tags };
352
+ })());
353
+ });
354
+
287
355
  // ---- API: expiring secrets & certificates (scans every readable vault) -----
288
356
 
289
357
  /** Run async worker over items with bounded concurrency. */
@@ -524,8 +592,10 @@ const ENV_RULES = (() => {
524
592
  try { return process.env.AZBO_ENV_RULES ? JSON.parse(process.env.AZBO_ENV_RULES) : null; }
525
593
  catch { console.warn(' AZBO_ENV_RULES is not valid JSON — ignoring.'); return null; }
526
594
  })();
595
+ const PKG_VERSION = (() => { try { return require('./package.json').version; } catch { return null; } })();
527
596
  app.get('/api/config', (req, res) =>
528
- res.json({ ok: true, data: { envRules: ENV_RULES, title: process.env.AZBO_TITLE || null } }));
597
+ res.json({ ok: true, data: { envRules: ENV_RULES, title: process.env.AZBO_TITLE || null,
598
+ version: PKG_VERSION, protectedTags: PROTECTED_TAGS } }));
529
599
 
530
600
  function openBrowser(url) {
531
601
  if (process.env.AZBO_NO_OPEN) return;