kalvium-worklog 1.0.3 → 1.2.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.
- package/README.md +11 -36
- package/dist/catchup.d.ts +2 -0
- package/dist/catchup.js +10 -0
- package/dist/cli.js +13 -19
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/postinstall.js +0 -1
- package/dist/scheduler.js +67 -10
- package/dist/submit.d.ts +6 -2
- package/dist/submit.js +94 -18
- package/package.json +2 -3
- package/templates/worklog_standalone.html +0 -423
- package/templates/worklog_template.html +0 -387
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
|
6
|
-
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
7
|
-
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
8
|
-
<meta name="apple-mobile-web-app-title" content="Worklog">
|
|
9
|
-
<meta name="theme-color" content="#0f172a">
|
|
10
|
-
<title>Kalvium Worklog</title>
|
|
11
|
-
<style>
|
|
12
|
-
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
|
13
|
-
body {
|
|
14
|
-
font-family: -apple-system, sans-serif;
|
|
15
|
-
background: #0f172a; color: #f8fafc;
|
|
16
|
-
min-height: 100vh; display: flex; flex-direction: column;
|
|
17
|
-
align-items: center; justify-content: center; padding: 20px;
|
|
18
|
-
-webkit-user-select: none; user-select: none;
|
|
19
|
-
}
|
|
20
|
-
.card {
|
|
21
|
-
background: #1e293b; border-radius: 20px; padding: 28px 22px;
|
|
22
|
-
width: 100%; max-width: 380px; box-shadow: 0 10px 40px rgba(0,0,0,0.4);
|
|
23
|
-
}
|
|
24
|
-
.header { display: flex; align-items: center; gap: 10px; margin-bottom: 20px; }
|
|
25
|
-
.logo { width: 36px; height: 36px; border-radius: 10px; background: #3b82f6;
|
|
26
|
-
display: flex; align-items: center; justify-content: center;
|
|
27
|
-
font-weight: 800; font-size: 18px; color: white; flex-shrink: 0; }
|
|
28
|
-
h1 { font-size: 20px; font-weight: 700; }
|
|
29
|
-
.sub { color: #64748b; font-size: 13px; margin-bottom: 20px; }
|
|
30
|
-
textarea {
|
|
31
|
-
width: 100%; min-height: 80px; border-radius: 12px;
|
|
32
|
-
border: 2px solid #334155; background: #0f172a; color: #f8fafc;
|
|
33
|
-
padding: 14px; font-size: 16px; resize: none; margin-bottom: 14px;
|
|
34
|
-
-webkit-user-select: text; user-select: text; font-family: inherit;
|
|
35
|
-
}
|
|
36
|
-
textarea:focus { outline: none; border-color: #3b82f6; }
|
|
37
|
-
.btn {
|
|
38
|
-
width: 100%; padding: 15px; border-radius: 12px; border: none;
|
|
39
|
-
font-size: 16px; font-weight: 600; cursor: pointer; transition: 0.15s;
|
|
40
|
-
-webkit-user-select: none; user-select: none;
|
|
41
|
-
}
|
|
42
|
-
.btn-submit { background: #3b82f6; color: white; margin-bottom: 8px; }
|
|
43
|
-
.btn-submit:active { transform: scale(0.97); background: #2563eb; }
|
|
44
|
-
.btn-submit:disabled { opacity: 0.4; }
|
|
45
|
-
.btn-status { background: #334155; color: #94a3b8; font-size: 14px; }
|
|
46
|
-
.btn-status:active { transform: scale(0.97); }
|
|
47
|
-
.btn-setup { background: transparent; color: #64748b; font-size: 13px;
|
|
48
|
-
margin-top: 12px; text-decoration: underline; }
|
|
49
|
-
#result {
|
|
50
|
-
margin-top: 14px; padding: 12px 14px; border-radius: 10px;
|
|
51
|
-
font-size: 14px; display: none; line-height: 1.4;
|
|
52
|
-
}
|
|
53
|
-
.success { background: #064e3b; color: #6ee7b7; }
|
|
54
|
-
.error { background: #7f1d1d; color: #fca5a5; }
|
|
55
|
-
.loading { background: #1e293b; color: #94a3b8; }
|
|
56
|
-
.spinner { display: inline-block; width: 14px; height: 14px;
|
|
57
|
-
border: 2px solid #64748b; border-top-color: #f8fafc;
|
|
58
|
-
border-radius: 50%; animation: spin 0.8s linear infinite;
|
|
59
|
-
vertical-align: middle; margin-right: 6px; }
|
|
60
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
61
|
-
.setup-panel { display: none; margin-top: 16px; }
|
|
62
|
-
.setup-panel textarea { min-height: 120px; font-size: 12px; }
|
|
63
|
-
.token-status { font-size: 12px; color: #64748b; text-align: center; margin-top: 10px; }
|
|
64
|
-
.setup-tabs { display: flex; gap: 8px; margin-bottom: 12px; }
|
|
65
|
-
.tab-btn { flex: 1; padding: 10px; border-radius: 8px; border: none;
|
|
66
|
-
font-size: 13px; cursor: pointer; background: #0f172a; color: #64748b; }
|
|
67
|
-
.tab-btn.active { background: #3b82f6; color: white; }
|
|
68
|
-
.tab-content { display: none; }
|
|
69
|
-
.tab-content.active { display: block; }
|
|
70
|
-
.hint { font-size: 12px; color: #64748b; margin-top: 8px; line-height: 1.5; }
|
|
71
|
-
</style>
|
|
72
|
-
</head>
|
|
73
|
-
<body>
|
|
74
|
-
<div class="card">
|
|
75
|
-
<div class="header">
|
|
76
|
-
<div class="logo">K</div>
|
|
77
|
-
<div>
|
|
78
|
-
<h1>Kalvium Worklog</h1>
|
|
79
|
-
<div class="sub" id="dateDisplay"></div>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
|
|
83
|
-
<textarea id="text" placeholder="What did you worked on today?">Working on assigned task</textarea>
|
|
84
|
-
|
|
85
|
-
<button class="btn btn-submit" id="submitBtn" onclick="submitWorklog()">Submit Worklog</button>
|
|
86
|
-
<button class="btn btn-status" onclick="checkStatus()">Check Status</button>
|
|
87
|
-
<button class="btn btn-setup" onclick="toggleSetup()">Setup / Token</button>
|
|
88
|
-
|
|
89
|
-
<div class="setup-panel" id="setupPanel">
|
|
90
|
-
<div class="setup-tabs">
|
|
91
|
-
<button class="tab-btn active" onclick="switchTab('paste')">Paste Token</button>
|
|
92
|
-
<button class="tab-btn" onclick="switchTab('url')">From URL</button>
|
|
93
|
-
</div>
|
|
94
|
-
|
|
95
|
-
<div class="tab-content active" id="tab-paste">
|
|
96
|
-
<p class="sub" style="margin-bottom:10px">Paste token JSON from computer setup:</p>
|
|
97
|
-
<textarea id="tokenInput" placeholder='{"refresh_token":"eyJ...","access_token":"eyJ..."}'></textarea>
|
|
98
|
-
<button class="btn btn-submit" onclick="saveToken()" style="margin-top:8px">Save Token</button>
|
|
99
|
-
</div>
|
|
100
|
-
|
|
101
|
-
<div class="tab-content" id="tab-url">
|
|
102
|
-
<p class="sub" style="margin-bottom:10px">If you opened a link with a token, it will be loaded automatically.</p>
|
|
103
|
-
<p class="hint" id="urlStatus">No token found in URL.</p>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
<div class="token-status" id="tokenStatus"></div>
|
|
108
|
-
<div id="result"></div>
|
|
109
|
-
</div>
|
|
110
|
-
|
|
111
|
-
<script>
|
|
112
|
-
// ─── Config ──────────────────────────────────────────────────────────────
|
|
113
|
-
const KEYCLOAK_URL = "https://auth.kalvium.community/auth/realms/kalvium/protocol/openid-connect/token";
|
|
114
|
-
const CLIENT_ID = "login_client";
|
|
115
|
-
const WORK_STATUS = "Working on-site (Company location)";
|
|
116
|
-
const CATEGORY = "on_site";
|
|
117
|
-
const API_BASE = "https://student-api.kalvium.community/api/internships/worklogs";
|
|
118
|
-
|
|
119
|
-
// ─── JWT decode (extract position_id from token sub claim) ───────────────
|
|
120
|
-
function decodeJwt(token) {
|
|
121
|
-
try {
|
|
122
|
-
const payload = token.split('.')[1];
|
|
123
|
-
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
|
|
124
|
-
return JSON.parse(decoded);
|
|
125
|
-
} catch (e) { return null; }
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function getPositionId() {
|
|
129
|
-
const tokens = getTokenData();
|
|
130
|
-
if (!tokens || !tokens.access_token) return null;
|
|
131
|
-
const payload = decodeJwt(tokens.access_token);
|
|
132
|
-
return payload ? payload.sub : null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function getWorklogApi() {
|
|
136
|
-
const posId = getPositionId();
|
|
137
|
-
return posId ? `${API_BASE}/${posId}` : null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ─── Token Management ────────────────────────────────────────────────────
|
|
141
|
-
function getTokenData() {
|
|
142
|
-
try { return JSON.parse(localStorage.getItem("kalvium_tokens")); }
|
|
143
|
-
catch { return null; }
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function saveTokenData(data) {
|
|
147
|
-
localStorage.setItem("kalvium_tokens", JSON.stringify(data));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
let pendingAction = null;
|
|
151
|
-
|
|
152
|
-
async function refreshAccessToken() {
|
|
153
|
-
const tokens = getTokenData();
|
|
154
|
-
if (!tokens || !tokens.refresh_token) {
|
|
155
|
-
const err = new Error("No token stored");
|
|
156
|
-
err.code = "NO_TOKEN";
|
|
157
|
-
throw err;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const body = new URLSearchParams({
|
|
161
|
-
grant_type: "refresh_token",
|
|
162
|
-
client_id: CLIENT_ID,
|
|
163
|
-
refresh_token: tokens.refresh_token
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const res = await fetch(KEYCLOAK_URL, {
|
|
167
|
-
method: "POST",
|
|
168
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
169
|
-
body: body
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const data = await res.json();
|
|
173
|
-
if (data.error) {
|
|
174
|
-
const err = new Error(`Token expired (${data.error})`);
|
|
175
|
-
err.code = "TOKEN_EXPIRED";
|
|
176
|
-
throw err;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
saveTokenData(data);
|
|
180
|
-
updateTokenStatus();
|
|
181
|
-
return data.access_token;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function showTokenExpiredUI(originalError) {
|
|
185
|
-
const result = document.getElementById("result");
|
|
186
|
-
result.style.display = "block";
|
|
187
|
-
result.className = "error";
|
|
188
|
-
result.innerHTML = `
|
|
189
|
-
✗ ${originalError.message}<br><br>
|
|
190
|
-
<b>Token expired.</b> Get a new token from your computer:<br>
|
|
191
|
-
1. Run <code>python ~/.kalvium/capture_token.py</code> on your computer<br>
|
|
192
|
-
2. Copy the token JSON<br>
|
|
193
|
-
3. Tap "Setup / Token" below and paste it<br>
|
|
194
|
-
4. Your action will retry automatically
|
|
195
|
-
`;
|
|
196
|
-
document.getElementById("setupPanel").style.display = "block";
|
|
197
|
-
document.getElementById("tokenInput").focus();
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ─── Submit Worklog ──────────────────────────────────────────────────────
|
|
201
|
-
async function submitWorklog() {
|
|
202
|
-
const text = document.getElementById("text").value.trim();
|
|
203
|
-
if (!text) return alert("Enter some text first");
|
|
204
|
-
|
|
205
|
-
pendingAction = { type: 'submit', text: text };
|
|
206
|
-
await doSubmit(text);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function doSubmit(text) {
|
|
210
|
-
const btn = document.getElementById("submitBtn");
|
|
211
|
-
const result = document.getElementById("result");
|
|
212
|
-
btn.disabled = true;
|
|
213
|
-
result.style.display = "block";
|
|
214
|
-
result.className = "loading";
|
|
215
|
-
result.innerHTML = '<span class="spinner"></span>Refreshing token...';
|
|
216
|
-
|
|
217
|
-
try {
|
|
218
|
-
const accessToken = await refreshAccessToken();
|
|
219
|
-
const worklogApi = getWorklogApi();
|
|
220
|
-
if (!worklogApi) throw new Error("Could not determine position ID from token");
|
|
221
|
-
|
|
222
|
-
result.innerHTML = '<span class="spinner"></span>Submitting worklog...';
|
|
223
|
-
|
|
224
|
-
const body = JSON.stringify({
|
|
225
|
-
title: WORK_STATUS, description: WORK_STATUS, category: CATEGORY,
|
|
226
|
-
timeSpent: 0, priorityLevel: "medium", blockers: "",
|
|
227
|
-
worklogs: JSON.stringify({ content: `<p>${text}</p>` })
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
const res = await fetch(worklogApi, {
|
|
231
|
-
method: "PUT",
|
|
232
|
-
headers: {
|
|
233
|
-
"authorization": `Bearer ${accessToken}`,
|
|
234
|
-
"content-type": "application/json",
|
|
235
|
-
"origin": "https://kalvium.community",
|
|
236
|
-
"referer": "https://kalvium.community/"
|
|
237
|
-
},
|
|
238
|
-
body: body
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
const data = await res.json();
|
|
242
|
-
if (res.ok && data.worklogId) {
|
|
243
|
-
result.className = "success";
|
|
244
|
-
result.innerHTML = `✓ Submitted!<br>ID: ${data.worklogId} | Status: ${data.status}`;
|
|
245
|
-
pendingAction = null;
|
|
246
|
-
} else if (res.status === 401 || res.status === 403) {
|
|
247
|
-
const err = new Error("Token rejected by server");
|
|
248
|
-
err.code = "TOKEN_EXPIRED";
|
|
249
|
-
throw err;
|
|
250
|
-
} else if (res.status === 404) {
|
|
251
|
-
result.className = "success";
|
|
252
|
-
result.innerHTML = "✓ Already submitted today!";
|
|
253
|
-
pendingAction = null;
|
|
254
|
-
} else {
|
|
255
|
-
result.className = "error";
|
|
256
|
-
result.innerHTML = `✗ ${data.message || "Failed"}`;
|
|
257
|
-
}
|
|
258
|
-
} catch (e) {
|
|
259
|
-
if (e.code === "TOKEN_EXPIRED" || e.code === "NO_TOKEN") {
|
|
260
|
-
showTokenExpiredUI(e);
|
|
261
|
-
} else {
|
|
262
|
-
result.className = "error";
|
|
263
|
-
result.innerHTML = `✗ ${e.message}`;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
btn.disabled = false;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// ─── Check Status ────────────────────────────────────────────────────────
|
|
270
|
-
async function checkStatus() {
|
|
271
|
-
pendingAction = { type: 'status' };
|
|
272
|
-
await doCheckStatus();
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async function doCheckStatus() {
|
|
276
|
-
const result = document.getElementById("result");
|
|
277
|
-
result.style.display = "block";
|
|
278
|
-
result.className = "loading";
|
|
279
|
-
result.innerHTML = '<span class="spinner"></span>Checking...';
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
const accessToken = await refreshAccessToken();
|
|
283
|
-
const worklogApi = getWorklogApi();
|
|
284
|
-
if (!worklogApi) throw new Error("Could not determine position ID from token");
|
|
285
|
-
|
|
286
|
-
const res = await fetch(worklogApi, {
|
|
287
|
-
headers: { "authorization": `Bearer ${accessToken}` }
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
if (res.status === 401 || res.status === 403) {
|
|
291
|
-
const err = new Error("Token rejected by server");
|
|
292
|
-
err.code = "TOKEN_EXPIRED";
|
|
293
|
-
throw err;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const worklogs = await res.json();
|
|
297
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
298
|
-
|
|
299
|
-
let html = "";
|
|
300
|
-
for (const w of worklogs.slice(0, 5)) {
|
|
301
|
-
const date = (w.worklogDate || "").slice(0, 10);
|
|
302
|
-
const status = w.status || "?";
|
|
303
|
-
let content = "";
|
|
304
|
-
if (w.worklogs) content = (w.worklogs.content || "").replace(/<\/?p>/g, "");
|
|
305
|
-
const marker = date === today ? " ← TODAY" : "";
|
|
306
|
-
html += `${date} | ${status} | ${content}${marker}<br>`;
|
|
307
|
-
}
|
|
308
|
-
result.className = "success";
|
|
309
|
-
result.innerHTML = html;
|
|
310
|
-
pendingAction = null;
|
|
311
|
-
} catch (e) {
|
|
312
|
-
if (e.code === "TOKEN_EXPIRED" || e.code === "NO_TOKEN") {
|
|
313
|
-
showTokenExpiredUI(e);
|
|
314
|
-
} else {
|
|
315
|
-
result.className = "error";
|
|
316
|
-
result.innerHTML = `✗ ${e.message}`;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// ─── Setup ───────────────────────────────────────────────────────────────
|
|
322
|
-
function toggleSetup() {
|
|
323
|
-
document.getElementById("setupPanel").style.display =
|
|
324
|
-
document.getElementById("setupPanel").style.display === "block" ? "none" : "block";
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function switchTab(tab) {
|
|
328
|
-
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
|
|
329
|
-
document.querySelectorAll(".tab-content").forEach(c => c.classList.remove("active"));
|
|
330
|
-
event.target.classList.add("active");
|
|
331
|
-
document.getElementById("tab-" + tab).classList.add("active");
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function saveToken() {
|
|
335
|
-
const input = document.getElementById("tokenInput").value.trim();
|
|
336
|
-
try {
|
|
337
|
-
const data = JSON.parse(input);
|
|
338
|
-
if (data.refresh_token) {
|
|
339
|
-
saveTokenData(data);
|
|
340
|
-
updateTokenStatus();
|
|
341
|
-
document.getElementById("tokenInput").value = "";
|
|
342
|
-
document.getElementById("setupPanel").style.display = "none";
|
|
343
|
-
|
|
344
|
-
// Auto-retry the pending action if there was one
|
|
345
|
-
if (pendingAction) {
|
|
346
|
-
if (pendingAction.type === 'submit') {
|
|
347
|
-
alert("Token saved! Retrying your submission...");
|
|
348
|
-
doSubmit(pendingAction.text);
|
|
349
|
-
} else if (pendingAction.type === 'status') {
|
|
350
|
-
alert("Token saved! Retrying status check...");
|
|
351
|
-
doCheckStatus();
|
|
352
|
-
}
|
|
353
|
-
} else {
|
|
354
|
-
alert("Token saved! You can now submit worklogs.");
|
|
355
|
-
}
|
|
356
|
-
} else {
|
|
357
|
-
alert("Invalid token JSON — no refresh_token found");
|
|
358
|
-
}
|
|
359
|
-
} catch (e) {
|
|
360
|
-
alert("Invalid JSON: " + e.message);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function updateTokenStatus() {
|
|
365
|
-
const tokens = getTokenData();
|
|
366
|
-
const el = document.getElementById("tokenStatus");
|
|
367
|
-
if (tokens && tokens.refresh_token) {
|
|
368
|
-
const posId = getPositionId();
|
|
369
|
-
el.textContent = `✓ Token stored${posId ? ' (ID: ' + posId.slice(0,8) + '...)' : ''}`;
|
|
370
|
-
el.style.color = "#6ee7b7";
|
|
371
|
-
} else {
|
|
372
|
-
el.textContent = "⚠ No token — tap Setup to add";
|
|
373
|
-
el.style.color = "#fca5a5";
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// ─── URL token loading (for hosted version) ──────────────────────────────
|
|
378
|
-
function loadTokenFromUrl() {
|
|
379
|
-
// Check URL hash for token (e.g., #token=base64encodedjson)
|
|
380
|
-
const hash = window.location.hash;
|
|
381
|
-
if (hash.startsWith("#token=")) {
|
|
382
|
-
try {
|
|
383
|
-
const encoded = hash.substring(7);
|
|
384
|
-
const decoded = atob(encoded);
|
|
385
|
-
const data = JSON.parse(decoded);
|
|
386
|
-
if (data.refresh_token) {
|
|
387
|
-
saveTokenData(data);
|
|
388
|
-
updateTokenStatus();
|
|
389
|
-
document.getElementById("urlStatus").textContent = "✓ Token loaded from URL!";
|
|
390
|
-
document.getElementById("urlStatus").style.color = "#6ee7b7";
|
|
391
|
-
// Clean the URL
|
|
392
|
-
history.replaceState(null, "", window.location.pathname);
|
|
393
|
-
}
|
|
394
|
-
} catch (e) {
|
|
395
|
-
document.getElementById("urlStatus").textContent = "✗ Invalid token in URL";
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
// Also check query param
|
|
399
|
-
const params = new URLSearchParams(window.location.search);
|
|
400
|
-
const tokenParam = params.get("token");
|
|
401
|
-
if (tokenParam) {
|
|
402
|
-
try {
|
|
403
|
-
const data = JSON.parse(atob(tokenParam));
|
|
404
|
-
if (data.refresh_token) {
|
|
405
|
-
saveTokenData(data);
|
|
406
|
-
updateTokenStatus();
|
|
407
|
-
document.getElementById("urlStatus").textContent = "✓ Token loaded from URL!";
|
|
408
|
-
document.getElementById("urlStatus").style.color = "#6ee7b7";
|
|
409
|
-
history.replaceState(null, "", window.location.pathname);
|
|
410
|
-
}
|
|
411
|
-
} catch (e) {}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// ─── Init ────────────────────────────────────────────────────────────────
|
|
416
|
-
document.getElementById("dateDisplay").textContent = new Date().toLocaleDateString("en-US", {
|
|
417
|
-
weekday: "long", year: "numeric", month: "long", day: "numeric"
|
|
418
|
-
});
|
|
419
|
-
loadTokenFromUrl();
|
|
420
|
-
updateTokenStatus();
|
|
421
|
-
</script>
|
|
422
|
-
</body>
|
|
423
|
-
</html>
|