kalvium-worklog 1.1.0 → 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 CHANGED
@@ -11,66 +11,40 @@ npm install -g kalvium-worklog
11
11
  ## Quick Start
12
12
 
13
13
  ```bash
14
- # Full setup (login + discover position + schedule + phone webapp)
14
+ # Full setup (login + discover position + schedule)
15
15
  kalvium-worklog install
16
16
 
17
17
  # Or step by step:
18
18
  kalvium-worklog login # log in via browser (Google + 2FA)
19
19
  kalvium-worklog discover # auto-discover your position ID
20
20
  kalvium-worklog schedule 14:00 # daily auto-submit at 2 PM
21
- kalvium-worklog webapp # generate phone web app
22
21
  ```
23
22
 
24
23
  ## Commands
25
24
 
26
25
  | Command | Description |
27
26
  |---------|-------------|
28
- | `kalvium-worklog install` | Full setup: login + discover + schedule + webapp |
27
+ | `kalvium-worklog install` | Full setup: login + discover + schedule |
29
28
  | `kalvium-worklog login` | Log in to Kalvium (opens browser, waits for Google + 2FA) |
30
29
  | `kalvium-worklog discover` | Auto-discover your internship position ID |
31
- | `kalvium-worklog submit [text]` | Submit worklog (default: "working") |
30
+ | `kalvium-worklog submit [text]` | Submit worklog (default: "Working on assigned task") |
32
31
  | `kalvium-worklog submit "built a feature"` | Submit with custom text |
33
- | `kalvium-worklog submit --status remote` | Submit with custom status |
34
32
  | `kalvium-worklog submit --dry-run` | Test without submitting |
35
33
  | `kalvium-worklog status` | Check recent worklog entries |
36
34
  | `kalvium-worklog schedule 14:00` | Set up daily auto-submit at 2 PM |
37
35
  | `kalvium-worklog schedule --remove` | Remove daily auto-submit |
38
- | `kalvium-worklog webapp [path]` | Generate phone web app HTML |
39
36
  | `kalvium-worklog daily` | Run daily submit (used by scheduler) |
37
+ | `kalvium-worklog catchup` | Check if today's worklog is missing and submit if needed |
40
38
 
41
39
  ## Features
42
40
 
43
41
  - **Cross-platform**: macOS (launchd), Windows (Task Scheduler), Linux (cron)
44
42
  - **Auto re-login**: If the token expires, automatically opens browser, waits for login, and retries
45
43
  - **Token auto-refresh**: Refresh token rotates on each use, lasts 6 days
46
- - **Phone web app**: Submit from your iPhone/Android (no computer needed after setup)
47
44
  - **Auto-discovers position ID**: Works for any user, no hardcoded IDs
48
45
  - **Weekend skipping**: Auto-submit skips Saturdays and Sundays
49
-
50
- ## Work Statuses
51
-
52
- | Key | Label |
53
- |-----|-------|
54
- | `on_site` | Working on-site (Company location) |
55
- | `remote` | Working Remotely (Not in the Kalvium environment) |
56
- | `classroom` | Working out of the Kalvium environment (Classroom) |
57
- | `holiday` | Today was a company Holiday |
58
- | `leave` | Took an approved leave from work |
59
-
60
- ```bash
61
- kalvium-worklog submit "worked from home" --status remote
62
- ```
63
-
64
- ## Phone Web App
65
-
66
- After running `kalvium-worklog webapp`:
67
-
68
- 1. AirDrop `~/KalviumWorklog.html` to your phone
69
- 2. Open in **Safari** (iPhone) or **Chrome** (Android)
70
- 3. **Add to Home Screen**
71
- 4. Tap the icon to submit worklogs from your phone
72
-
73
- The phone app works independently — it refreshes its own token and auto-retries if the token expires.
46
+ - **Network retry**: Retries up to 5 times on network failure (covers laptop waking up, internet reconnecting)
47
+ - **Catch-up on wake**: If the scheduled time was missed (laptop asleep/offline), submits when the device wakes up
74
48
 
75
49
  ## How It Works
76
50
 
@@ -81,9 +55,10 @@ One-time setup:
81
55
  Daily auto-submit (scheduler):
82
56
  Refresh token → PUT worklog API → done
83
57
  If token expired → auto open browser → re-login → retry
58
+ If network fails → retry up to 5 times (1 min apart)
84
59
 
85
- Manual submit:
86
- kalvium-worklog submit "my update" refresh token PUT worklog
60
+ Catch-up (on login/wake, every 30 min):
61
+ Check if today's worklog is submittedif not, submit it
87
62
  ```
88
63
 
89
64
  ## Token Lifecycle
@@ -100,10 +75,10 @@ Manual submit:
100
75
  ├── refresh_token.json # Current token (auto-rotates)
101
76
  ├── config.json # Position ID + API URL
102
77
  ├── daily_submit.js # Scheduler entry point
78
+ ├── catchup_submit.js # Catch-up entry point
103
79
  └── worklog.log # Log file
104
80
 
105
81
  ~/.kalvium_profile/ # Browser profile (saved login)
106
- ~/KalviumWorklog.html # Phone web app
107
82
  ```
108
83
 
109
84
  ## Uninstall
@@ -111,7 +86,7 @@ Manual submit:
111
86
  ```bash
112
87
  kalvium-worklog schedule --remove
113
88
  npm uninstall -g kalvium-worklog
114
- rm -rf ~/.kalvium ~/.kalvium_profile ~/KalviumWorklog.html
89
+ rm -rf ~/.kalvium ~/.kalvium_profile
115
90
  ```
116
91
 
117
92
  ## Requirements
package/dist/cli.js CHANGED
@@ -4,7 +4,6 @@ import { captureToken } from "./capture-token.js";
4
4
  import { discoverPosition } from "./discover-position.js";
5
5
  import { submitWorklog, checkStatus, dailySubmit, catchUpSubmit } from "./submit.js";
6
6
  import { setupScheduler, removeScheduler } from "./scheduler.js";
7
- import { generateWebapp } from "./generate-webapp.js";
8
7
  import { loadToken, saveConfig, getPositionIdFromToken } from "./config.js";
9
8
  import { existsSync } from "fs";
10
9
  import { join, dirname } from "path";
@@ -42,7 +41,6 @@ program
42
41
  program
43
42
  .command("submit [text]")
44
43
  .description("Submit worklog (default text: 'Working on assigned task')")
45
- .option("--status <type>", "Work status", "on_site")
46
44
  .option("--dry-run", "Test without submitting")
47
45
  .action(async (text, opts) => {
48
46
  // Skip weekends (unless dry run)
@@ -53,7 +51,6 @@ program
53
51
  }
54
52
  const result = await submitWorklog({
55
53
  text: text ?? "Working on assigned task",
56
- status: opts.status,
57
54
  dryRun: opts.dryRun,
58
55
  });
59
56
  if (!result.success)
@@ -135,19 +132,10 @@ program
135
132
  if (!ok)
136
133
  process.exit(1);
137
134
  });
138
- // ─── webapp ────────────────────────────────────────────────────────────────
139
- program
140
- .command("webapp [path]")
141
- .description("Generate phone web app HTML (default: ~/KalviumWorklog.html)")
142
- .action(async (path) => {
143
- const ok = await generateWebapp(path);
144
- if (!ok)
145
- process.exit(1);
146
- });
147
135
  // ─── install (full setup) ──────────────────────────────────────────────────
148
136
  program
149
137
  .command("install")
150
- .description("Full setup: login + discover + schedule + webapp")
138
+ .description("Full setup: login + discover + schedule")
151
139
  .action(async () => {
152
140
  const readline = await import("readline");
153
141
  const rl = readline.createInterface({
@@ -205,9 +193,6 @@ program
205
193
  }
206
194
  }
207
195
  await setupScheduler({ hour, minute });
208
- // Step 4: Webapp
209
- console.log("\n── Step 4: Generate phone web app ──");
210
- await generateWebapp();
211
196
  console.log("\n✅ Setup complete!\n");
212
197
  console.log("Commands:");
213
198
  console.log(" kalvium-worklog submit # submit with default text");
@@ -230,7 +215,7 @@ if (process.argv.length <= 2) {
230
215
  console.log(" kalvium-worklog install");
231
216
  console.log("");
232
217
  console.log(" This will log you in, discover your position ID,");
233
- console.log(" set up daily auto-submit, and generate a phone web app.");
218
+ console.log(" and set up daily auto-submit.");
234
219
  console.log("");
235
220
  console.log(" Or run kalvium-worklog --help to see all commands.");
236
221
  console.log("");
package/dist/index.d.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  export { captureToken } from "./capture-token.js";
2
2
  export { discoverPosition } from "./discover-position.js";
3
- export { submitWorklog, checkStatus, dailySubmit, catchUpSubmit, STATUS_OPTIONS } from "./submit.js";
3
+ export { submitWorklog, checkStatus, dailySubmit, catchUpSubmit } from "./submit.js";
4
4
  export type { SubmitOptions, SubmitResult } from "./submit.js";
5
5
  export { setupScheduler, removeScheduler } from "./scheduler.js";
6
6
  export type { ScheduleOptions } from "./scheduler.js";
7
- export { generateWebapp } from "./generate-webapp.js";
8
7
  export { refreshToken } from "./refresh.js";
9
8
  export { loadToken, saveToken, loadConfig, saveConfig, getWorklogApi, INSTALL_DIR, TOKEN_FILE, CONFIG_FILE, LOG_FILE, } from "./config.js";
10
9
  export type { TokenData, ConfigData } from "./config.js";
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  export { captureToken } from "./capture-token.js";
2
2
  export { discoverPosition } from "./discover-position.js";
3
- export { submitWorklog, checkStatus, dailySubmit, catchUpSubmit, STATUS_OPTIONS } from "./submit.js";
3
+ export { submitWorklog, checkStatus, dailySubmit, catchUpSubmit } from "./submit.js";
4
4
  export { setupScheduler, removeScheduler } from "./scheduler.js";
5
- export { generateWebapp } from "./generate-webapp.js";
6
5
  export { refreshToken } from "./refresh.js";
7
6
  export { loadToken, saveToken, loadConfig, saveConfig, getWorklogApi, INSTALL_DIR, TOKEN_FILE, CONFIG_FILE, LOG_FILE, } from "./config.js";
@@ -50,7 +50,6 @@ else if (isGlobal) {
50
50
  log(" 1. Open a browser for you to log in to Kalvium");
51
51
  log(" 2. Auto-discover your internship position ID");
52
52
  log(" 3. Set up daily auto-submit at your chosen time");
53
- log(" 4. Generate a phone web app");
54
53
  log("");
55
54
  log(" Or set up step by step:");
56
55
  log(" " + BOLD + "kalvium-worklog login" + NC + " log in");
package/dist/submit.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export interface SubmitOptions {
2
2
  text?: string;
3
- status?: string;
4
3
  dryRun?: boolean;
5
4
  }
6
5
  export interface SubmitResult {
@@ -8,7 +7,6 @@ export interface SubmitResult {
8
7
  message: string;
9
8
  worklogId?: string;
10
9
  }
11
- export declare const STATUS_OPTIONS: Record<string, string>;
12
10
  /**
13
11
  * Submit worklog with auto-relogin retry.
14
12
  * If the refresh token has expired, automatically opens a browser
package/dist/submit.js CHANGED
@@ -1,13 +1,8 @@
1
1
  import { getWorklogApi, log } from "./config.js";
2
2
  import { refreshToken } from "./refresh.js";
3
3
  import { captureToken } from "./capture-token.js";
4
- export const STATUS_OPTIONS = {
5
- on_site: "Working on-site (Company location)",
6
- remote: "Working Remotely (Not in the Kalvium environment)",
7
- classroom: "Working out of the Kalvium environment (Classroom)",
8
- holiday: "Today was a company Holiday",
9
- leave: "Took an approved leave from work",
10
- };
4
+ const STATUS_KEY = "on_site";
5
+ const STATUS_LABEL = "Working on-site (Company location)";
11
6
  /**
12
7
  * Submit worklog with auto-relogin retry.
13
8
  * If the refresh token has expired, automatically opens a browser
@@ -15,7 +10,6 @@ export const STATUS_OPTIONS = {
15
10
  */
16
11
  export async function submitWorklog(options = {}) {
17
12
  const text = options.text ?? "Working on assigned task";
18
- const statusKey = options.status ?? "on_site";
19
13
  const dryRun = options.dryRun ?? false;
20
14
  const worklogApi = getWorklogApi();
21
15
  if (!worklogApi) {
@@ -24,11 +18,10 @@ export async function submitWorklog(options = {}) {
24
18
  message: "No config found. Run `kalvium-worklog discover` first.",
25
19
  };
26
20
  }
27
- const statusLabel = STATUS_OPTIONS[statusKey] ?? STATUS_OPTIONS.on_site;
28
21
  const body = JSON.stringify({
29
- title: statusLabel,
30
- description: statusLabel,
31
- category: statusKey,
22
+ title: STATUS_LABEL,
23
+ description: STATUS_LABEL,
24
+ category: STATUS_KEY,
32
25
  timeSpent: 0,
33
26
  priorityLevel: "medium",
34
27
  blockers: "",
@@ -36,7 +29,7 @@ export async function submitWorklog(options = {}) {
36
29
  });
37
30
  if (dryRun) {
38
31
  console.log(`[DRY RUN] Would submit to ${worklogApi}`);
39
- console.log(` text='${text}', status=${statusKey}`);
32
+ console.log(` text='${text}', status=${STATUS_KEY}`);
40
33
  console.log(` Body: ${body}`);
41
34
  return { success: true, message: "Dry run" };
42
35
  }
@@ -196,7 +189,6 @@ export async function dailySubmit() {
196
189
  }
197
190
  const result = await submitWorklog({
198
191
  text: "Working on assigned task",
199
- status: "on_site",
200
192
  });
201
193
  if (result.success) {
202
194
  log(`[INFO] SUCCESS: ${result.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kalvium-worklog",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Auto-submit your daily Kalvium worklog without opening the website",
5
5
  "keywords": [
6
6
  "kalvium",
@@ -27,8 +27,7 @@
27
27
  "kworklog": "dist/cli.js"
28
28
  },
29
29
  "files": [
30
- "dist",
31
- "templates"
30
+ "dist"
32
31
  ],
33
32
  "scripts": {
34
33
  "build": "tsc",
@@ -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>
@@ -1,387 +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
- .qr-section { margin-top: 16px; text-align: center; }
65
- .qr-section canvas { margin: 10px auto; display: block; }
66
- .qr-hint { font-size: 12px; color: #64748b; margin-top: 8px; }
67
- </style>
68
- </head>
69
- <body>
70
- <div class="card">
71
- <div class="header">
72
- <div class="logo">K</div>
73
- <div>
74
- <h1>Kalvium Worklog</h1>
75
- <div class="sub" id="dateDisplay"></div>
76
- </div>
77
- </div>
78
-
79
- <textarea id="text" placeholder="What did you work on today?">Working on assigned task</textarea>
80
-
81
- <button class="btn btn-submit" id="submitBtn" onclick="submitWorklog()">Submit Worklog</button>
82
- <button class="btn btn-status" onclick="checkStatus()">Check Status</button>
83
- <button class="btn btn-setup" onclick="toggleSetup()">Setup / Token</button>
84
-
85
- <div class="setup-panel" id="setupPanel">
86
- <p class="sub" style="margin-bottom:10px">Paste token JSON from computer, or scan QR code:</p>
87
- <textarea id="tokenInput" placeholder='Paste refresh token JSON here'></textarea>
88
- <button class="btn btn-submit" onclick="saveToken()" style="margin-top:8px">Save Token</button>
89
-
90
- <div class="qr-section" id="qrSection">
91
- <p class="sub">Or scan QR code from computer:</p>
92
- <button class="btn btn-status" onclick="scanQR()">Scan QR Code</button>
93
- </div>
94
- </div>
95
-
96
- <div class="token-status" id="tokenStatus"></div>
97
- <div id="result"></div>
98
- </div>
99
-
100
- <script>
101
- // ─── Config ──────────────────────────────────────────────────────────────
102
- const KEYCLOAK_URL = "https://auth.kalvium.community/auth/realms/kalvium/protocol/openid-connect/token";
103
- const CLIENT_ID = "login_client";
104
- const WORK_STATUS = "Working on-site (Company location)";
105
- const CATEGORY = "on_site";
106
- const API_BASE = "https://student-api.kalvium.community/api/internships/worklogs";
107
-
108
- // ─── JWT decode (extract position_id from token) ─────────────────────────
109
- function decodeJwt(token) {
110
- try {
111
- const payload = token.split('.')[1];
112
- const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
113
- return JSON.parse(decoded);
114
- } catch (e) {
115
- return null;
116
- }
117
- }
118
-
119
- function getPositionId() {
120
- const tokens = getTokenData();
121
- if (!tokens || !tokens.access_token) return null;
122
- const payload = decodeJwt(tokens.access_token);
123
- return payload ? payload.sub : null;
124
- }
125
-
126
- function getWorklogApi() {
127
- const posId = getPositionId();
128
- if (!posId) return null;
129
- return `${API_BASE}/${posId}`;
130
- }
131
-
132
- // ─── Token Management ────────────────────────────────────────────────────
133
- function getTokenData() {
134
- try { return JSON.parse(localStorage.getItem("kalvium_tokens")); }
135
- catch { return null; }
136
- }
137
-
138
- function saveTokenData(data) {
139
- localStorage.setItem("kalvium_tokens", JSON.stringify(data));
140
- }
141
-
142
- // Track pending action for retry after re-login
143
- let pendingAction = null; // 'submit' or 'status'
144
-
145
- async function refreshAccessToken() {
146
- const tokens = getTokenData();
147
- if (!tokens || !tokens.refresh_token) {
148
- const err = new Error("No token stored");
149
- err.code = "NO_TOKEN";
150
- throw err;
151
- }
152
-
153
- const body = new URLSearchParams({
154
- grant_type: "refresh_token",
155
- client_id: CLIENT_ID,
156
- refresh_token: tokens.refresh_token
157
- });
158
-
159
- const res = await fetch(KEYCLOAK_URL, {
160
- method: "POST",
161
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
162
- body: body
163
- });
164
-
165
- const data = await res.json();
166
- if (data.error) {
167
- const err = new Error(`Token expired (${data.error})`);
168
- err.code = "TOKEN_EXPIRED";
169
- err.data = data;
170
- throw err;
171
- }
172
-
173
- // Save new tokens (refresh token rotates)
174
- saveTokenData(data);
175
- updateTokenStatus();
176
- return data.access_token;
177
- }
178
-
179
- // Show "token expired" UI and prompt user to paste a new one
180
- function showTokenExpiredUI(originalError) {
181
- const result = document.getElementById("result");
182
- result.style.display = "block";
183
- result.className = "error";
184
- result.innerHTML = `
185
- ✗ ${originalError.message}<br><br>
186
- <b>Token expired.</b> Get a new token from your computer:<br>
187
- 1. Run <code>python ~/.kalvium/capture_token.py</code> on your computer<br>
188
- 2. Copy the token JSON<br>
189
- 3. Tap "Setup / Token" below and paste it<br>
190
- 4. Your action will retry automatically
191
- `;
192
- // Auto-open the setup panel
193
- document.getElementById("setupPanel").style.display = "block";
194
- document.getElementById("tokenInput").focus();
195
- }
196
-
197
- // ─── Submit Worklog ──────────────────────────────────────────────────────
198
- async function submitWorklog() {
199
- const text = document.getElementById("text").value.trim();
200
- if (!text) return alert("Enter some text first");
201
-
202
- pendingAction = { type: 'submit', text: text };
203
- await doSubmit(text);
204
- }
205
-
206
- async function doSubmit(text) {
207
- const btn = document.getElementById("submitBtn");
208
- const result = document.getElementById("result");
209
- btn.disabled = true;
210
- result.style.display = "block";
211
- result.className = "loading";
212
- result.innerHTML = '<span class="spinner"></span>Refreshing token...';
213
-
214
- try {
215
- const accessToken = await refreshAccessToken();
216
- const worklogApi = getWorklogApi();
217
- if (!worklogApi) throw new Error("Could not determine position ID from token");
218
-
219
- result.innerHTML = '<span class="spinner"></span>Submitting worklog...';
220
-
221
- const body = JSON.stringify({
222
- title: WORK_STATUS,
223
- description: WORK_STATUS,
224
- category: CATEGORY,
225
- timeSpent: 0,
226
- priorityLevel: "medium",
227
- blockers: "",
228
- worklogs: JSON.stringify({ content: `<p>${text}</p>` })
229
- });
230
-
231
- const res = await fetch(worklogApi, {
232
- method: "PUT",
233
- headers: {
234
- "authorization": `Bearer ${accessToken}`,
235
- "content-type": "application/json",
236
- "origin": "https://kalvium.community",
237
- "referer": "https://kalvium.community/"
238
- },
239
- body: body
240
- });
241
-
242
- const data = await res.json();
243
-
244
- if (res.ok && data.worklogId) {
245
- result.className = "success";
246
- result.innerHTML = `✓ Submitted!<br>ID: ${data.worklogId} | Status: ${data.status}`;
247
- pendingAction = null; // Clear on success
248
- } else if (res.status === 401 || res.status === 403) {
249
- // Token invalid at API level — treat as expired
250
- const err = new Error("Token rejected by server");
251
- err.code = "TOKEN_EXPIRED";
252
- throw err;
253
- } else if (res.status === 404) {
254
- result.className = "success";
255
- result.innerHTML = "✓ Already submitted today!";
256
- pendingAction = null;
257
- } else {
258
- result.className = "error";
259
- result.innerHTML = `✗ ${data.message || "Failed"}`;
260
- }
261
- } catch (e) {
262
- if (e.code === "TOKEN_EXPIRED" || e.code === "NO_TOKEN") {
263
- showTokenExpiredUI(e);
264
- } else {
265
- result.className = "error";
266
- result.innerHTML = `✗ ${e.message}`;
267
- }
268
- }
269
- btn.disabled = false;
270
- }
271
-
272
- // ─── Check Status ────────────────────────────────────────────────────────
273
- async function checkStatus() {
274
- pendingAction = { type: 'status' };
275
- await doCheckStatus();
276
- }
277
-
278
- async function doCheckStatus() {
279
- const result = document.getElementById("result");
280
- result.style.display = "block";
281
- result.className = "loading";
282
- result.innerHTML = '<span class="spinner"></span>Checking...';
283
-
284
- try {
285
- const accessToken = await refreshAccessToken();
286
- const worklogApi = getWorklogApi();
287
- if (!worklogApi) throw new Error("Could not determine position ID from token");
288
-
289
- const res = await fetch(worklogApi, {
290
- headers: { "authorization": `Bearer ${accessToken}` }
291
- });
292
-
293
- if (res.status === 401 || res.status === 403) {
294
- const err = new Error("Token rejected by server");
295
- err.code = "TOKEN_EXPIRED";
296
- throw err;
297
- }
298
-
299
- const worklogs = await res.json();
300
- const today = new Date().toISOString().slice(0, 10);
301
-
302
- let html = "";
303
- for (const w of worklogs.slice(0, 5)) {
304
- const date = (w.worklogDate || "").slice(0, 10);
305
- const status = w.status || "?";
306
- let content = "";
307
- if (w.worklogs) content = (w.worklogs.content || "").replace(/<\/?p>/g, "");
308
- const marker = date === today ? " ← TODAY" : "";
309
- html += `${date} | ${status} | ${content}${marker}<br>`;
310
- }
311
- result.className = "success";
312
- result.innerHTML = html;
313
- pendingAction = null; // Clear on success
314
- } catch (e) {
315
- if (e.code === "TOKEN_EXPIRED" || e.code === "NO_TOKEN") {
316
- showTokenExpiredUI(e);
317
- } else {
318
- result.className = "error";
319
- result.innerHTML = `✗ ${e.message}`;
320
- }
321
- }
322
- }
323
-
324
- // ─── Setup ───────────────────────────────────────────────────────────────
325
- function toggleSetup() {
326
- const panel = document.getElementById("setupPanel");
327
- panel.style.display = panel.style.display === "block" ? "none" : "block";
328
- }
329
-
330
- function saveToken() {
331
- const input = document.getElementById("tokenInput").value.trim();
332
- try {
333
- const data = JSON.parse(input);
334
- if (data.refresh_token) {
335
- saveTokenData(data);
336
- updateTokenStatus();
337
- document.getElementById("tokenInput").value = "";
338
- document.getElementById("setupPanel").style.display = "none";
339
-
340
- // Auto-retry the pending action if there was one
341
- if (pendingAction) {
342
- if (pendingAction.type === 'submit') {
343
- alert("Token saved! Retrying your submission...");
344
- doSubmit(pendingAction.text);
345
- } else if (pendingAction.type === 'status') {
346
- alert("Token saved! Retrying status check...");
347
- doCheckStatus();
348
- }
349
- } else {
350
- alert("Token saved! You can now submit worklogs.");
351
- }
352
- } else {
353
- alert("Invalid token JSON — no refresh_token found");
354
- }
355
- } catch (e) {
356
- alert("Invalid JSON: " + e.message);
357
- }
358
- }
359
-
360
- function scanQR() {
361
- // Open camera for QR scanning using a simple approach
362
- // We use a public QR scanner that returns the data via URL
363
- const qrScannerUrl = "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=";
364
- alert("To scan QR:\n1. Run 'python generate_webapp.py' on your computer\n2. It will show a QR code\n3. Scan it with your phone camera\n4. Copy the text and paste it above");
365
- }
366
-
367
- function updateTokenStatus() {
368
- const tokens = getTokenData();
369
- const el = document.getElementById("tokenStatus");
370
- if (tokens && tokens.refresh_token) {
371
- const posId = getPositionId();
372
- el.textContent = `✓ Token stored${posId ? ' (Position: ' + posId.slice(0,8) + '...)' : ''}`;
373
- el.style.color = "#6ee7b7";
374
- } else {
375
- el.textContent = "⚠ No token — tap Setup to add";
376
- el.style.color = "#fca5a5";
377
- }
378
- }
379
-
380
- // ─── Init ────────────────────────────────────────────────────────────────
381
- document.getElementById("dateDisplay").textContent = new Date().toLocaleDateString("en-US", {
382
- weekday: "long", year: "numeric", month: "long", day: "numeric"
383
- });
384
- updateTokenStatus();
385
- </script>
386
- </body>
387
- </html>