From 29301aa84597c0156a5ec57a799c82ab31e165a4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Per=20Andre=20R=C3=B8nsen?=
Date: Mon, 25 May 2026 18:09:18 +0200
Subject: [PATCH 1/8] Restore one-entry-per-(project+activityType) rule in AI
prompt
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Removed in a39bf85, the absence of this rule lets the model emit one
suggestion per source activity (commit/email/event), producing 50+
duplicate entries on the same project and blowing past the 11h cap.
Bump 2.1.1 → 2.1.2.
Co-Authored-By: Claude Opus 4.7
---
package.json | 2 +-
prompts/timelog-system.md | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 041bcef..de14642 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "worklog",
- "version": "2.1.1",
+ "version": "2.1.2",
"private": true,
"scripts": {
"dev": "next dev",
diff --git a/prompts/timelog-system.md b/prompts/timelog-system.md
index 70da9a7..135cb97 100644
--- a/prompts/timelog-system.md
+++ b/prompts/timelog-system.md
@@ -50,6 +50,7 @@ RULES:
- Round to nearest 0.5 hour per project (minimum 0.5h)
- Respond ONLY with valid JSON matching the schema below
- Include at most 10 sourceActivities per suggestion (the most representative ones)
+- One entry per (projectId + activityTypeId) combination — never create two entries with the same project and activity type. Merge all evidence into a single entry. Do NOT emit one entry per commit, email, or calendar event.
- NEVER suggest time for work that is already logged (shown in "ALREADY LOGGED TODAY"). Those activities are done — skip them entirely. Only suggest NEW entries for unlogged work.
- Total hours across all NEW entries plus already-logged hours must fall within the 7–11h range above — adjust hours to ensure this
- If rounding would push total above the activity-evidenced amount, reduce the lowest-confidence entry's hours to compensate
From be95502c259e099d8bf7c7d812a60164d3b1f85f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Per=20Andre=20R=C3=B8nsen?=
Date: Thu, 28 May 2026 12:13:33 +0200
Subject: [PATCH 2/8] Add ESLint, Vitest config, and git-hook tooling
Introduce a working lint/typecheck/test toolchain:
- ESLint flat config (eslint-config-next) wired into `npm run lint`
- `typecheck` script (tsc --noEmit)
- Vitest config for unit tests
- husky pre-commit (lint-staged) + pre-push (typecheck + test)
- Escape JSX entities on the about page to make the lint baseline clean
Pre-existing `any` usage and react-hooks deps are downgraded to warnings
to track as follow-ups rather than blocking CI on legacy patterns.
Co-Authored-By: Claude Opus 4.7
---
.husky/pre-commit | 1 +
.husky/pre-push | 1 +
.lintstagedrc.json | 3 +
app/about/page.tsx | 14 +-
eslint.config.mjs | 52 +
package-lock.json | 9071 ++++++++++++++++++++++++++++++++++++--------
package.json | 14 +-
vitest.config.ts | 10 +
8 files changed, 7673 insertions(+), 1493 deletions(-)
create mode 100755 .husky/pre-commit
create mode 100755 .husky/pre-push
create mode 100644 .lintstagedrc.json
create mode 100644 eslint.config.mjs
create mode 100644 vitest.config.ts
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 0000000..2312dc5
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1 @@
+npx lint-staged
diff --git a/.husky/pre-push b/.husky/pre-push
new file mode 100755
index 0000000..70e6d98
--- /dev/null
+++ b/.husky/pre-push
@@ -0,0 +1 @@
+npm run typecheck && npm test
diff --git a/.lintstagedrc.json b/.lintstagedrc.json
new file mode 100644
index 0000000..3d064b8
--- /dev/null
+++ b/.lintstagedrc.json
@@ -0,0 +1,3 @@
+{
+ "*.{ts,tsx}": "eslint --fix"
+}
diff --git a/app/about/page.tsx b/app/about/page.tsx
index 2781464..ad856a5 100644
--- a/app/about/page.tsx
+++ b/app/about/page.tsx
@@ -111,7 +111,7 @@ export default function AboutPage() {
You had a productive day. Back-to-back meetings, a long email thread, three code reviews, half a dozen Slack threads, and two Jira tickets closed. Then 4:55 PM arrives and your time-tracking system is just… waiting. Blinking cursor, empty rows.
- Worklog is the missing piece. It collects everything you did, lays it out hour by hour, and uses AI to translate it into billable time entries — mapped to the right projects and activity types, with descriptions you'd actually write yourself.
+ Worklog is the missing piece. It collects everything you did, lays it out hour by hour, and uses AI to translate it into billable time entries — mapped to the right projects and activity types, with descriptions you'd actually write yourself.
@@ -146,7 +146,7 @@ export default function AboutPage() {
- This is where the interesting stuff happens. Here's the full journey from "you pick a date" to "here's your timelog."
+ This is where the interesting stuff happens. Here's the full journey from "you pick a date" to "here's your timelog."
@@ -171,7 +171,7 @@ export default function AboutPage() {
Parallel fetch
- All seven sources are queried simultaneously. Each has its own OAuth token stored as a secure HTTP-only cookie. Google uses NextAuth's JWT; Jira stores only the refresh token (access tokens are too big for cookies) and fetches a fresh one per request.
+ All seven sources are queried simultaneously. Each has its own OAuth token stored as a secure HTTP-only cookie. Google uses NextAuth's JWT; Jira stores only the refresh token (access tokens are too big for cookies) and fetches a fresh one per request.
@@ -197,7 +197,7 @@ export default function AboutPage() {
PM context fetch
- When you click Generate, the app pulls your project context from the time-tracking system: the 20 projects you've used most in the last 14 days, the top 3 activity types per project, your hour allocations for the day, any existing time records, and the time-lock date (logged hours before that date are untouchable).
+ When you click Generate, the app pulls your project context from the time-tracking system: the 20 projects you've used most in the last 14 days, the top 3 activity types per project, your hour allocations for the day, any existing time records, and the time-lock date (logged hours before that date are untouchable).
@@ -222,7 +222,7 @@ export default function AboutPage() {
Review and submit
- You see the suggestions, edit anything that looks off, and hit Submit. Each entry is validated server-side against the time-lock before being written to your time-tracking system. The suggestions are cached in localStorage per date so a page refresh doesn't throw away your work.
+ You see the suggestions, edit anything that looks off, and hit Submit. Each entry is validated server-side against the time-lock before being written to your time-tracking system. The suggestions are cached in localStorage per date so a page refresh doesn't throw away your work.
@@ -247,7 +247,7 @@ export default function AboutPage() {
Milient / Moment
- Milient (also known as Moment) is a time management platform widely used by Norwegian consulting firms. Worklog uses it as the source of truth for projects, activity types, hour allocations, and as the destination for submitted time records. Your user account is resolved dynamically from your Google sign-in email, so there's no separate login.
+ Milient (also known as Moment) is a time management platform widely used by Norwegian consulting firms. Worklog uses it as the source of truth for projects, activity types, hour allocations, and as the destination for submitted time records. Your user account is resolved dynamically from your Google sign-in email, so there's no separate login.
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..24ba4bb
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,52 @@
+import { dirname } from "node:path"
+import { fileURLToPath } from "node:url"
+import { FlatCompat } from "@eslint/eslintrc"
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
+
+const compat = new FlatCompat({ baseDirectory: __dirname })
+
+const eslintConfig = [
+ {
+ ignores: [
+ ".next/**",
+ "out/**",
+ "build/**",
+ "node_modules/**",
+ "next-env.d.ts",
+ "worklog-next/**",
+ "tmp/**",
+ ],
+ },
+ ...compat.extends("next/core-web-vitals", "next/typescript"),
+ {
+ rules: {
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ argsIgnorePattern: "^_",
+ varsIgnorePattern: "^_",
+ caughtErrorsIgnorePattern: "^_",
+ },
+ ],
+ // Many integrations wrap untyped third-party REST APIs (Google, Slack,
+ // Jira, HubSpot, …). `any` at those boundaries is pragmatic; surface it
+ // as a warning to burn down incrementally rather than blocking CI.
+ "@typescript-eslint/no-explicit-any": "warn",
+ // The OAuth "connect" links intentionally use