@datalabz_/dashboard-kit 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (6) hide show
  1. package/README.md +156 -0
  2. package/package.json +15 -0
  3. package/v4.css +694 -0
  4. package/v4.js +1093 -0
  5. package/wrapper.css +326 -0
  6. package/wrapper.js +310 -0
package/wrapper.css ADDED
@@ -0,0 +1,326 @@
1
+ :root {
2
+ --bg: #0f1115;
3
+ --surface: #161922;
4
+ --raised: #1c2030;
5
+ --border: #2a2f3e;
6
+ --row_even: #14171f;
7
+ --row_odd: #1a1d27;
8
+ --text: #e6e8ee;
9
+ --muted: #8a90a3;
10
+ --brand: #0b8f84;
11
+ --info: #3498db;
12
+ --ok: #2ea043;
13
+ --warn: #f7ad2b;
14
+ --err: #ff4d4f;
15
+ }
16
+ * {
17
+ box-sizing: border-box;
18
+ }
19
+ [hidden] {
20
+ display: none !important;
21
+ }
22
+ html,
23
+ body {
24
+ margin: 0;
25
+ padding: 0;
26
+ height: 100%;
27
+ background: var(--bg);
28
+ color: var(--text);
29
+ font:
30
+ 13px/1.45 system-ui,
31
+ -apple-system,
32
+ Segoe UI,
33
+ Roboto,
34
+ Helvetica,
35
+ Arial,
36
+ sans-serif;
37
+ }
38
+ body {
39
+ display: flex;
40
+ flex-direction: column;
41
+ height: 100vh;
42
+ }
43
+
44
+ header.topbar {
45
+ background: var(--surface);
46
+ border-bottom: 1px solid var(--border);
47
+ padding: 10px 16px;
48
+ display: flex;
49
+ gap: 12px;
50
+ min-height: 48px;
51
+ align-items: center;
52
+ flex-wrap: wrap;
53
+ flex-shrink: 0;
54
+ }
55
+ header .filters {
56
+ display: flex;
57
+ gap: 10px;
58
+ align-items: center;
59
+ flex-wrap: wrap;
60
+ flex: 1;
61
+ }
62
+ header label {
63
+ color: var(--muted);
64
+ display: inline-flex;
65
+ align-items: center;
66
+ gap: 6px;
67
+ font-size: 12px;
68
+ }
69
+ header input,
70
+ header select {
71
+ background: var(--raised);
72
+ color: var(--text);
73
+ border: 1px solid var(--border);
74
+ border-radius: 4px;
75
+ padding: 0 8px;
76
+ height: 24px;
77
+ font: inherit;
78
+ }
79
+ header input[type="date"] {
80
+ min-width: 130px;
81
+ }
82
+ header input#hierarchyId {
83
+ width: 140px;
84
+ appearance: textfield;
85
+ -moz-appearance: textfield;
86
+ }
87
+ header input#hierarchyId::-webkit-outer-spin-button,
88
+ header input#hierarchyId::-webkit-inner-spin-button {
89
+ -webkit-appearance: none;
90
+ margin: 0;
91
+ }
92
+ header select#timezone {
93
+ min-width: 170px;
94
+ max-width: 220px;
95
+ }
96
+ header select#datePreset {
97
+ min-width: 140px;
98
+ }
99
+
100
+ .btn {
101
+ background: var(--brand);
102
+ color: white;
103
+ border: 1px solid var(--brand);
104
+ border-radius: 4px;
105
+ padding: 0 10px;
106
+ height: 24px;
107
+ font: inherit;
108
+ font-size: 12px;
109
+ cursor: pointer;
110
+ display: inline-flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ }
114
+ .btn:hover {
115
+ background: color-mix(in srgb, var(--brand) 85%, white);
116
+ border-color: color-mix(in srgb, var(--brand) 85%, white);
117
+ }
118
+ .btn-sm {
119
+ height: 22px;
120
+ padding: 0 8px;
121
+ font-size: 11px;
122
+ }
123
+ .btn-ghost {
124
+ background: transparent;
125
+ color: var(--text);
126
+ border: 1px solid var(--border);
127
+ }
128
+ .btn-ghost:hover {
129
+ border-color: var(--brand);
130
+ background: var(--raised);
131
+ color: var(--text);
132
+ }
133
+
134
+ .status-pill {
135
+ padding: 4px 10px;
136
+ border-radius: 999px;
137
+ font-size: 11px;
138
+ background: color-mix(in srgb, var(--muted) 20%, transparent);
139
+ color: var(--muted);
140
+ }
141
+ .status-pill.ok {
142
+ background: color-mix(in srgb, var(--ok) 18%, transparent);
143
+ color: var(--ok);
144
+ }
145
+ .status-pill.warn {
146
+ background: color-mix(in srgb, var(--warn) 18%, transparent);
147
+ color: var(--warn);
148
+ }
149
+ .status-pill.err {
150
+ background: color-mix(in srgb, var(--err) 18%, transparent);
151
+ color: var(--err);
152
+ }
153
+ .status-pill.idle {
154
+ background: color-mix(in srgb, var(--muted) 20%, transparent);
155
+ color: var(--muted);
156
+ }
157
+
158
+ .theme-toggle {
159
+ display: inline-flex;
160
+ border: 1px solid var(--border);
161
+ border-radius: 4px;
162
+ overflow: hidden;
163
+ background: var(--raised);
164
+ height: 24px;
165
+ }
166
+ .theme-toggle button {
167
+ background: transparent;
168
+ color: var(--muted);
169
+ border: none;
170
+ padding: 0 10px;
171
+ font: inherit;
172
+ cursor: pointer;
173
+ display: inline-flex;
174
+ align-items: center;
175
+ }
176
+ .theme-toggle button:hover {
177
+ color: var(--text);
178
+ }
179
+ .theme-toggle button.active {
180
+ background: var(--brand);
181
+ color: white;
182
+ }
183
+
184
+ .user-badge {
185
+ display: inline-flex;
186
+ align-items: center;
187
+ gap: 8px;
188
+ font-size: 12px;
189
+ color: var(--muted);
190
+ }
191
+ .user-badge .name {
192
+ color: var(--text);
193
+ font-weight: 500;
194
+ }
195
+
196
+ main.iframe-host {
197
+ background: var(--bg);
198
+ position: relative;
199
+ overflow: hidden;
200
+ flex: 1;
201
+ }
202
+ iframe#iframe {
203
+ width: 100%;
204
+ height: 100%;
205
+ border: none;
206
+ background: var(--bg);
207
+ }
208
+
209
+ .empty-state {
210
+ position: absolute;
211
+ inset: 0;
212
+ display: flex;
213
+ align-items: center;
214
+ justify-content: center;
215
+ color: var(--muted);
216
+ flex-direction: column;
217
+ gap: 8px;
218
+ text-align: center;
219
+ padding: 40px;
220
+ }
221
+ .empty-state strong {
222
+ color: var(--text);
223
+ font-size: 14px;
224
+ }
225
+
226
+ .toast {
227
+ position: fixed;
228
+ bottom: 16px;
229
+ right: 16px;
230
+ background: var(--surface);
231
+ border: 1px solid var(--border);
232
+ padding: 8px 14px;
233
+ border-radius: 6px;
234
+ font-size: 12px;
235
+ color: var(--text);
236
+ opacity: 0;
237
+ transform: translateY(10px);
238
+ transition:
239
+ opacity 0.15s,
240
+ transform 0.15s;
241
+ z-index: 200;
242
+ pointer-events: none;
243
+ }
244
+ .toast.show {
245
+ opacity: 1;
246
+ transform: translateY(0);
247
+ }
248
+ .toast.err {
249
+ border-color: var(--err);
250
+ color: var(--err);
251
+ }
252
+
253
+ .modal-backdrop {
254
+ position: fixed;
255
+ inset: 0;
256
+ z-index: 1000;
257
+ background: rgba(0, 0, 0, 0.55);
258
+ display: flex;
259
+ align-items: center;
260
+ justify-content: center;
261
+ }
262
+ .modal {
263
+ background: var(--surface);
264
+ color: var(--text);
265
+ border: 1px solid var(--border);
266
+ border-radius: 8px;
267
+ width: 360px;
268
+ max-width: 90vw;
269
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
270
+ display: flex;
271
+ flex-direction: column;
272
+ }
273
+ .modal-header {
274
+ padding: 14px 18px;
275
+ border-bottom: 1px solid var(--border);
276
+ }
277
+ .modal-header h3 {
278
+ margin: 0;
279
+ font-size: 15px;
280
+ font-weight: 600;
281
+ }
282
+ .modal-body {
283
+ padding: 18px;
284
+ display: flex;
285
+ flex-direction: column;
286
+ gap: 14px;
287
+ }
288
+ .modal-body label {
289
+ display: flex;
290
+ flex-direction: column;
291
+ gap: 6px;
292
+ font-size: 12px;
293
+ color: var(--muted);
294
+ }
295
+ .modal-body input {
296
+ background: var(--raised);
297
+ color: var(--text);
298
+ border: 1px solid var(--border);
299
+ border-radius: 4px;
300
+ padding: 0 10px;
301
+ height: 32px;
302
+ font: inherit;
303
+ }
304
+ .modal-body input:focus {
305
+ outline: none;
306
+ border-color: var(--brand);
307
+ }
308
+ .modal-footer {
309
+ padding: 12px 18px;
310
+ border-top: 1px solid var(--border);
311
+ display: flex;
312
+ justify-content: flex-end;
313
+ gap: 8px;
314
+ }
315
+ .modal-footer .btn {
316
+ height: 28px;
317
+ padding: 0 14px;
318
+ }
319
+ .modal-error {
320
+ color: var(--err);
321
+ font-size: 12px;
322
+ padding: 8px 10px;
323
+ background: color-mix(in srgb, var(--err) 12%, transparent);
324
+ border: 1px solid color-mix(in srgb, var(--err) 35%, transparent);
325
+ border-radius: 4px;
326
+ }
package/wrapper.js ADDED
@@ -0,0 +1,310 @@
1
+ (function () {
2
+ "use strict";
3
+
4
+ const API_BASE = "https://bcproxy.draftplaza.com";
5
+ const LS_TIMEZONE = "iqg.harness.timezone";
6
+ const LS_TOKEN = "iqg.harness.token";
7
+ const LS_USER = "iqg.harness.user";
8
+ const LS_THEME = "iqg.harness.theme";
9
+
10
+ // Capture srcdoc from the inline iframe before DOM rebuild.
11
+ const _srcdoc = document.getElementById("iframe")?.getAttribute("srcdoc") || "";
12
+
13
+ document.body.innerHTML = `
14
+ <header class="topbar">
15
+ <div class="filters">
16
+ <label>Date
17
+ <select id="datePreset">
18
+ <option value="today">Today</option><option value="yesterday">Yesterday</option>
19
+ <option value="currentWeek">Current week</option><option value="lastWeek">Last week</option>
20
+ <option value="currentMonth">Current month</option><option value="lastMonth">Last month</option>
21
+ <option value="last30Days">Last 30 days</option><option value="last60Days">Last 60 days</option>
22
+ <option value="last365Days">Last 365 days</option><option value="allTime">All time</option>
23
+ <option value="manual">Manual…</option>
24
+ </select>
25
+ </label>
26
+ <label id="manualFromLabel" hidden>From <input type="date" id="manualFrom"></label>
27
+ <label id="manualToLabel" hidden>To <input type="date" id="manualTo"></label>
28
+ <label>Timezone
29
+ <select id="timezone">
30
+ <option value="America/Sao_Paulo">America/Sao_Paulo (BRT)</option>
31
+ <option value="Asia/Yerevan">Asia/Yerevan</option>
32
+ <option value="Turkey">Turkey</option>
33
+ <option value="Etc/UTC">UTC±00:00</option>
34
+ <option value="Etc/GMT+12">UTC−12:00</option><option value="Etc/GMT+11">UTC−11:00</option>
35
+ <option value="Etc/GMT+10">UTC−10:00</option><option value="Etc/GMT+9">UTC−09:00</option>
36
+ <option value="Etc/GMT+8">UTC−08:00</option><option value="Etc/GMT+7">UTC−07:00</option>
37
+ <option value="Etc/GMT+6">UTC−06:00</option><option value="Etc/GMT+5">UTC−05:00</option>
38
+ <option value="Etc/GMT+4">UTC−04:00</option><option value="Etc/GMT+3">UTC−03:00</option>
39
+ <option value="Etc/GMT+2">UTC−02:00</option><option value="Etc/GMT+1">UTC−01:00</option>
40
+ <option value="Etc/GMT-1">UTC+01:00</option><option value="Etc/GMT-2">UTC+02:00</option>
41
+ <option value="Etc/GMT-3">UTC+03:00</option><option value="Etc/GMT-4">UTC+04:00</option>
42
+ <option value="Etc/GMT-5">UTC+05:00</option><option value="Etc/GMT-6">UTC+06:00</option>
43
+ <option value="Etc/GMT-7">UTC+07:00</option><option value="Etc/GMT-8">UTC+08:00</option>
44
+ <option value="Etc/GMT-9">UTC+09:00</option><option value="Etc/GMT-10">UTC+10:00</option>
45
+ <option value="Etc/GMT-11">UTC+11:00</option><option value="Etc/GMT-12">UTC+12:00</option>
46
+ </select>
47
+ </label>
48
+ <label>Hierarchy <input type="number" id="hierarchyId" placeholder="Hierarchy Id"></label>
49
+ <label>Theme
50
+ <div class="theme-toggle" id="themeToggle">
51
+ <button type="button" data-theme="light">Light</button>
52
+ <button type="button" data-theme="dark">Dark</button>
53
+ </div>
54
+ </label>
55
+ <button class="btn btn-sm" id="snapshotBtn">Export snapshot</button>
56
+ </div>
57
+ <div class="user-badge">
58
+ <span class="name" id="userName">Not signed in</span>
59
+ <button class="btn btn-sm btn-ghost" id="signOut" hidden>Sign out</button>
60
+ </div>
61
+ <div class="status-pill idle" id="statusPill">no token</div>
62
+ </header>
63
+ <main class="iframe-host">
64
+ <iframe id="iframe" sandbox="allow-scripts allow-downloads"></iframe>
65
+ <div class="empty-state" id="emptyState">
66
+ <strong>No report embedded</strong>
67
+ <div>Ask Claude to generate one — it fills the iframe srcdoc and saves the result under reports/.</div>
68
+ </div>
69
+ </main>
70
+ <div class="modal-backdrop" id="authModalBackdrop" hidden>
71
+ <div class="modal" id="authModal">
72
+ <div class="modal-header"><h3>Sign in</h3></div>
73
+ <div class="modal-body">
74
+ <label>Username <input type="text" id="authUsername" autocomplete="username" autocapitalize="off" spellcheck="false"></label>
75
+ <label>Password <input type="password" id="authPassword" autocomplete="current-password"></label>
76
+ <div class="modal-error" id="authError" hidden></div>
77
+ </div>
78
+ <div class="modal-footer"><button class="btn" id="authSubmit">Sign in</button></div>
79
+ </div>
80
+ </div>
81
+ <div class="modal-backdrop" id="verifyModalBackdrop" hidden>
82
+ <div class="modal" id="verifyModal">
83
+ <div class="modal-header"><h3>Verification</h3></div>
84
+ <div class="modal-body">
85
+ <label>Verification key <input type="text" id="verifyKey" autocapitalize="off" spellcheck="false"></label>
86
+ <div class="modal-error" id="verifyError" hidden></div>
87
+ </div>
88
+ <div class="modal-footer">
89
+ <button class="btn btn-ghost" id="verifyCancel">Cancel</button>
90
+ <button class="btn" id="verifySubmit">Verify</button>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ <div class="toast" id="toast"></div>`;
95
+
96
+ document.getElementById("iframe").setAttribute("srcdoc", _srcdoc);
97
+
98
+ // ── Helpers ──────────────────────────────────────────────────────────────────
99
+
100
+ function lsGet(k, fb) { try { const v = localStorage.getItem(k); return v == null ? fb : JSON.parse(v); } catch { return fb; } }
101
+ function lsSet(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) { toast(`Storage error: ${e?.message || e}`, true); } }
102
+ function lsDel(k) { try { localStorage.removeItem(k); } catch { } }
103
+
104
+ const State = {
105
+ filters: {
106
+ datePreset: "today", manualFrom: todayYmd(), manualTo: todayYmd(),
107
+ timezone: lsGet(LS_TIMEZONE, "America/Sao_Paulo"), hierarchyId: null
108
+ },
109
+ user: lsGet(LS_USER, null), token: lsGet(LS_TOKEN, ""),
110
+ theme: lsGet(LS_THEME, "dark"), pendingUsername: null,
111
+ };
112
+
113
+ function todayYmd() {
114
+ const d = new Date();
115
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
116
+ }
117
+ function localToUtc(ymd, hh, mm, ss, ms, tz) {
118
+ const [y, mo, d] = ymd.split("-").map(Number);
119
+ const guess = Date.UTC(y, mo - 1, d, hh, mm, ss, ms);
120
+ const fmt = new Intl.DateTimeFormat("en-US", {
121
+ timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit",
122
+ hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false
123
+ });
124
+ const parts = fmt.formatToParts(new Date(guess));
125
+ const get = t => parts.find(p => p.type === t)?.value;
126
+ const tzUtc = Date.UTC(+get("year"), +get("month") - 1, +get("day"), +get("hour") % 24, +get("minute"), +get("second"), ms);
127
+ return new Date(guess - (tzUtc - guess));
128
+ }
129
+ function dayStart(ymd, tz) { return localToUtc(ymd, 0, 0, 0, 0, tz).toISOString(); }
130
+ function dayEnd(ymd, tz) { return localToUtc(ymd, 23, 59, 59, 999, tz).toISOString(); }
131
+ function addDays(d, n) { const r = new Date(d); r.setDate(r.getDate() + n); return r; }
132
+ function ymdOf(d) { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; }
133
+ function soWeek(d) { const day = d.getDay(); return addDays(d, day === 0 ? -6 : 1 - day); }
134
+
135
+ function presetDates(preset, tz) {
136
+ const now = new Date(), today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
137
+ const t = ymdOf(today);
138
+ switch (preset) {
139
+ case "today": return { dateFromIso: dayStart(t, tz), dateToIso: dayEnd(t, tz) };
140
+ case "yesterday": { const y = ymdOf(addDays(today, -1)); return { dateFromIso: dayStart(y, tz), dateToIso: dayEnd(y, tz) }; }
141
+ case "currentWeek": return { dateFromIso: dayStart(ymdOf(soWeek(today)), tz), dateToIso: dayEnd(t, tz) };
142
+ case "lastWeek": return { dateFromIso: dayStart(ymdOf(addDays(soWeek(today), -7)), tz), dateToIso: dayEnd(ymdOf(addDays(addDays(soWeek(today), 6), -7)), tz) };
143
+ case "currentMonth": return { dateFromIso: dayStart(ymdOf(new Date(today.getFullYear(), today.getMonth(), 1)), tz), dateToIso: dayEnd(t, tz) };
144
+ case "lastMonth": {
145
+ const p = new Date(today.getFullYear(), today.getMonth() - 1, 1);
146
+ return { dateFromIso: dayStart(ymdOf(p), tz), dateToIso: dayEnd(ymdOf(new Date(today.getFullYear(), today.getMonth(), 0)), tz) };
147
+ }
148
+ case "last30Days": return { dateFromIso: dayStart(ymdOf(addDays(today, -30)), tz), dateToIso: dayEnd(t, tz) };
149
+ case "last60Days": return { dateFromIso: dayStart(ymdOf(addDays(today, -60)), tz), dateToIso: dayEnd(t, tz) };
150
+ case "last365Days": return { dateFromIso: dayStart(ymdOf(addDays(today, -365)), tz), dateToIso: dayEnd(t, tz) };
151
+ case "allTime": return { dateFromIso: "1970-01-01T00:00:00.000Z", dateToIso: dayEnd(t, tz) };
152
+ default: return null;
153
+ }
154
+ }
155
+
156
+ function decodeJwt(jwt) {
157
+ if (!jwt) return null;
158
+ const parts = jwt.split(".");
159
+ if (parts.length !== 3) return null;
160
+ try {
161
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/"); const pad = b64 + "=".repeat((4 - b64.length % 4) % 4);
162
+ return JSON.parse(decodeURIComponent(escape(atob(pad))));
163
+ } catch { return null; }
164
+ }
165
+ function tokenStatus(jwt) {
166
+ if (!jwt) return { state: "idle", label: "no token" };
167
+ const d = decodeJwt(jwt);
168
+ if (!d) return { state: "err", label: "invalid jwt" };
169
+ if (d.exp && d.exp * 1000 < Date.now()) return { state: "err", label: `expired ${new Date(d.exp * 1000).toISOString().slice(0, 16)}Z` };
170
+ if (d.exp) return { state: "ok", label: `ok · expires in ${Math.round((d.exp * 1000 - Date.now()) / 60000)}m` };
171
+ return { state: "warn", label: "ok · no exp" };
172
+ }
173
+
174
+ function buildMsg() {
175
+ const tz = State.filters.timezone;
176
+ const r = State.filters.datePreset === "manual"
177
+ ? { dateFromIso: dayStart(State.filters.manualFrom, tz), dateToIso: dayEnd(State.filters.manualTo, tz) }
178
+ : (presetDates(State.filters.datePreset, tz) || { dateFromIso: dayStart(todayYmd(), tz), dateToIso: dayEnd(todayYmd(), tz) });
179
+ const hid = State.filters.hierarchyId;
180
+ return {
181
+ type: "CUSTOM_DASHBOARDS_PARAMS", token: State.token, theme: State.theme,
182
+ filters: {
183
+ dateFrom: r.dateFromIso, dateTo: r.dateToIso, timezone: tz,
184
+ hierarchyId: hid == null || Number.isNaN(hid) ? null : Number(hid)
185
+ }
186
+ };
187
+ }
188
+ function maybeSend() {
189
+ if (!State.token || tokenStatus(State.token).state === "err") return;
190
+ const f = document.getElementById("iframe");
191
+ if (!f || f.hidden || !f.contentWindow) return;
192
+ try { f.contentWindow.postMessage(buildMsg(), "*"); } catch (e) { toast(`postMessage failed: ${e?.message || e}`, true); }
193
+ }
194
+
195
+ // ── Render ───────────────────────────────────────────────────────────────────
196
+
197
+ const $ = id => document.getElementById(id);
198
+ function renderTokenStatus() { const s = tokenStatus(State.token); const p = $("statusPill"); p.className = `status-pill ${s.state}`; p.textContent = s.label; }
199
+ function renderThemeToggle() { document.querySelectorAll("#themeToggle button").forEach(b => b.classList.toggle("active", b.dataset.theme === State.theme)); }
200
+ function renderFilters() {
201
+ $("datePreset").value = State.filters.datePreset;
202
+ $("manualFrom").value = State.filters.manualFrom;
203
+ $("manualTo").value = State.filters.manualTo;
204
+ $("timezone").value = State.filters.timezone;
205
+ $("hierarchyId").value = State.filters.hierarchyId == null ? "" : State.filters.hierarchyId;
206
+ const m = State.filters.datePreset === "manual";
207
+ $("manualFromLabel").hidden = !m; $("manualToLabel").hidden = !m;
208
+ }
209
+ function renderUserBadge() {
210
+ if (State.user) { $("userName").textContent = State.user.username || "(unnamed)"; $("signOut").hidden = false; }
211
+ else { $("userName").textContent = "Not signed in"; $("signOut").hidden = true; }
212
+ }
213
+ function mountReport() {
214
+ const f = $("iframe"), e = $("emptyState"), s = f.getAttribute("srcdoc") || "";
215
+ f.hidden = s.trim().length === 0; e.hidden = !f.hidden;
216
+ }
217
+ function setFilter(k, v) {
218
+ State.filters = Object.assign({}, State.filters, { [k]: v });
219
+ if (k === "timezone" && v) lsSet(LS_TIMEZONE, v);
220
+ maybeSend();
221
+ }
222
+ function setTheme(t) {
223
+ if (t !== "light" && t !== "dark" || State.theme === t) return;
224
+ State.theme = t; lsSet(LS_THEME, t); document.documentElement.dataset.theme = t; renderThemeToggle(); maybeSend();
225
+ }
226
+
227
+ // ── Auth ─────────────────────────────────────────────────────────────────────
228
+
229
+ async function doSignIn(username, password) {
230
+ const u = (username || "").trim().toLowerCase(), p = (password || "").trim();
231
+ if (!u || !p) { setAuthErr("Username and password are required."); return; }
232
+ setAuthLoading(true); setAuthErr(null);
233
+ try {
234
+ const res = await fetch(`${API_BASE}/account/authenticate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: u, password: p }) });
235
+ const data = await res.json().catch(() => ({}));
236
+ if (res.ok && data.success) { data.result?.isVerification ? (State.pendingUsername = u, openVerify()) : onAuth(data.result, u); }
237
+ else { const k = data?.response?.data?.message?.key || data?.message?.key; setAuthErr(k === "account_not_allowed_ip_error" ? "Login not allowed from this IP." : "Incorrect username or password."); }
238
+ } catch (e) { setAuthErr(`Network error: ${e?.message || e}`); }
239
+ finally { setAuthLoading(false); }
240
+ }
241
+ async function doVerify(key) {
242
+ const k = (key || "").trim();
243
+ if (!k) { setVerifyErr("Verification key is required."); return; }
244
+ if (!State.pendingUsername) { setVerifyErr("No pending sign-in."); return; }
245
+ setVerifyLoading(true); setVerifyErr(null);
246
+ try {
247
+ const url = new URL(`${API_BASE}/account/verification`); url.searchParams.set("username", State.pendingUsername); url.searchParams.set("key", k);
248
+ const res = await fetch(url.toString()), data = await res.json().catch(() => ({}));
249
+ if (res.ok && data.success) onAuth(data.result, State.pendingUsername); else setVerifyErr("Verification failed.");
250
+ } catch (e) { setVerifyErr(`Network error: ${e?.message || e}`); }
251
+ finally { setVerifyLoading(false); }
252
+ }
253
+ function onAuth(result, enteredUsername) {
254
+ const decoded = decodeJwt(result.token);
255
+ const user = { ...result, expAt: decoded?.exp ?? null, username: result.username || enteredUsername };
256
+ lsSet(LS_USER, user); lsSet(LS_TOKEN, result.token);
257
+ State.user = user; State.token = result.token; State.pendingUsername = null;
258
+ closeAuth(); closeVerify(); renderUserBadge(); renderTokenStatus(); maybeSend();
259
+ }
260
+ function doSignOut() {
261
+ lsDel(LS_USER); lsDel(LS_TOKEN); State.user = null; State.token = ""; State.pendingUsername = null;
262
+ renderUserBadge(); renderTokenStatus(); openAuth();
263
+ }
264
+ function openAuth() { $("authModalBackdrop").hidden = false; $("authUsername").value = ""; $("authPassword").value = ""; setAuthErr(null); setTimeout(() => $("authUsername").focus(), 30); }
265
+ function closeAuth() { $("authModalBackdrop").hidden = true; }
266
+ function openVerify() { $("verifyModalBackdrop").hidden = false; $("verifyKey").value = ""; setVerifyErr(null); setTimeout(() => $("verifyKey").focus(), 30); }
267
+ function closeVerify() { $("verifyModalBackdrop").hidden = true; }
268
+ function setAuthErr(m) { const e = $("authError"); m ? (e.textContent = m, e.hidden = false) : (e.textContent = "", e.hidden = true); }
269
+ function setVerifyErr(m) { const e = $("verifyError"); m ? (e.textContent = m, e.hidden = false) : (e.textContent = "", e.hidden = true); }
270
+ function setAuthLoading(l) { const b = $("authSubmit"); b.disabled = l; b.textContent = l ? "Signing in…" : "Sign in"; }
271
+ function setVerifyLoading(l) { const b = $("verifySubmit"); b.disabled = l; b.textContent = l ? "Verifying…" : "Verify"; }
272
+
273
+ let _tt = null;
274
+ function toast(msg, isErr) { const e = $("toast"); e.textContent = msg; e.classList.toggle("err", !!isErr); e.classList.add("show"); clearTimeout(_tt); _tt = setTimeout(() => e.classList.remove("show"), 2400); }
275
+
276
+ // ── Wire + Boot ──────────────────────────────────────────────────────────────
277
+
278
+ function wire() {
279
+ $("datePreset").addEventListener("change", e => { setFilter("datePreset", e.target.value); const m = e.target.value === "manual"; $("manualFromLabel").hidden = !m; $("manualToLabel").hidden = !m; });
280
+ $("manualFrom").addEventListener("change", e => setFilter("manualFrom", e.target.value));
281
+ $("manualTo").addEventListener("change", e => setFilter("manualTo", e.target.value));
282
+ $("timezone").addEventListener("change", e => setFilter("timezone", e.target.value));
283
+ $("hierarchyId").addEventListener("change", e => { const v = e.target.value.trim(); setFilter("hierarchyId", v === "" ? null : Number(v)); });
284
+ $("hierarchyId").addEventListener("wheel", e => { if (document.activeElement === e.target) { e.preventDefault(); e.target.blur(); } }, { passive: false });
285
+ const su = () => doSignIn($("authUsername").value, $("authPassword").value);
286
+ $("authSubmit").addEventListener("click", su);
287
+ $("authModal").addEventListener("keydown", e => { if (e.key === "Enter") su(); });
288
+ const sv = () => doVerify($("verifyKey").value);
289
+ $("verifySubmit").addEventListener("click", sv);
290
+ $("verifyCancel").addEventListener("click", () => { closeVerify(); State.pendingUsername = null; openAuth(); });
291
+ $("verifyModal").addEventListener("keydown", e => { if (e.key === "Enter") sv(); });
292
+ $("signOut").addEventListener("click", doSignOut);
293
+ $("snapshotBtn").addEventListener("click", () => {
294
+ const f = $("iframe");
295
+ if (f && f.contentWindow) f.contentWindow.postMessage({ type: "RK_SNAPSHOT" }, "*");
296
+ });
297
+ $("themeToggle").addEventListener("click", e => { const b = e.target.closest("button[data-theme]"); if (b) setTheme(b.dataset.theme); });
298
+ $("iframe").addEventListener("load", () => maybeSend());
299
+ }
300
+
301
+ function boot() {
302
+ document.documentElement.dataset.theme = State.theme;
303
+ renderFilters(); renderUserBadge(); renderThemeToggle(); renderTokenStatus(); wire(); mountReport();
304
+ if (!State.token) { openAuth(); return; }
305
+ const s = tokenStatus(State.token);
306
+ if (s.state === "err") { lsDel(LS_TOKEN); lsDel(LS_USER); State.token = ""; State.user = null; renderUserBadge(); renderTokenStatus(); toast("Stored token was expired/invalid — please sign in again.", true); openAuth(); }
307
+ }
308
+
309
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot); else boot();
310
+ })();