diff --git a/package-lock.json b/package-lock.json
index 192ccfe..3920bf8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -110,9 +110,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@hapi/tlds": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.4.tgz",
- "integrity": "sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA==",
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz",
+ "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -137,14 +137,14 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "25.2.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz",
- "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==",
+ "version": "25.9.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
+ "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
- "undici-types": "~7.16.0"
+ "undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/@types/sinonjs__fake-timers": {
@@ -179,6 +179,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
"node_modules/aggregate-error": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
@@ -372,23 +385,27 @@
"license": "MIT"
},
"node_modules/axios": {
- "version": "1.13.4",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
- "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz",
+ "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "follow-redirects": "^1.15.6",
- "form-data": "^4.0.4",
- "proxy-from-env": "^1.1.0"
+ "follow-redirects": "^1.16.0",
+ "form-data": "^4.0.5",
+ "https-proxy-agent": "^5.0.1",
+ "proxy-from-env": "^2.1.0"
}
},
"node_modules/axios/node_modules/proxy-from-env": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
},
"node_modules/balanced-match": {
"version": "1.0.2",
@@ -530,13 +547,13 @@
}
},
"node_modules/boxen/node_modules/strip-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
- "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-regex": "^6.0.1"
+ "ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
@@ -577,9 +594,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1120,9 +1137,9 @@
}
},
"node_modules/dayjs": {
- "version": "1.11.19",
- "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
- "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
+ "version": "1.11.20",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
+ "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
"dev": true,
"license": "MIT"
},
@@ -1400,9 +1417,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
- "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
+ "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"dev": true,
"funding": [
{
@@ -1443,9 +1460,9 @@
}
},
"node_modules/follow-redirects": {
- "version": "1.15.11",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
- "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"dev": true,
"funding": [
{
@@ -1674,9 +1691,9 @@
}
},
"node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1701,6 +1718,20 @@
"node": ">=0.10"
}
},
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/human-signals": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
@@ -1879,9 +1910,9 @@
"license": "MIT"
},
"node_modules/joi": {
- "version": "18.0.2",
- "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz",
- "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==",
+ "version": "18.2.1",
+ "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz",
+ "integrity": "sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -1891,7 +1922,7 @@
"@hapi/pinpoint": "^2.0.1",
"@hapi/tlds": "^1.1.1",
"@hapi/topo": "^6.0.2",
- "@standard-schema/spec": "^1.0.0"
+ "@standard-schema/spec": "^1.1.0"
},
"engines": {
"node": ">= 20"
@@ -1926,9 +1957,9 @@
"license": "ISC"
},
"node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1993,9 +2024,9 @@
}
},
"node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"dev": true,
"license": "MIT"
},
@@ -2364,9 +2395,9 @@
}
},
"node_modules/pump": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
- "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
+ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2527,9 +2558,9 @@
"license": "MIT"
},
"node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -2671,14 +2702,14 @@
}
},
"node_modules/side-channel-list": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
+ "object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
@@ -2788,9 +2819,9 @@
}
},
"node_modules/start-server-and-test": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.1.3.tgz",
- "integrity": "sha512-k4EcbNjeg0odaDkAMlIeDVDByqX9PIgL4tivgP2tES6Zd8o+4pTq/HgbWCyA3VHIoZopB+wGnNPKYGGSByNriQ==",
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.1.5.tgz",
+ "integrity": "sha512-A/SbXpgXE25ScSkpLLqvGvVZT0ykN6+AzS8tVqMBCTxbJy2Nwuen59opT+afalK5aS+AuQmZs0EsLwjnuDN+/g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2801,7 +2832,7 @@
"execa": "5.1.1",
"lazy-ass": "1.6.0",
"ps-tree": "1.2.0",
- "wait-on": "9.0.3"
+ "wait-on": "9.0.4"
},
"bin": {
"server-test": "src/bin/start.js",
@@ -3044,9 +3075,9 @@
}
},
"node_modules/undici-types": {
- "version": "7.16.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
- "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "version": "7.24.6",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
+ "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"dev": true,
"license": "MIT",
"optional": true
@@ -3086,6 +3117,7 @@
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
"dev": true,
"license": "MIT",
"bin": {
@@ -3118,15 +3150,15 @@
}
},
"node_modules/wait-on": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.3.tgz",
- "integrity": "sha512-13zBnyYvFDW1rBvWiJ6Av3ymAaq8EDQuvxZnPIw3g04UqGi4TyoIJABmfJ6zrvKo9yeFQExNkOk7idQbDJcuKA==",
+ "version": "9.0.4",
+ "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.4.tgz",
+ "integrity": "sha512-k8qrgfwrPVJXTeFY8tl6BxVHiclK11u72DVKhpybHfUL/K6KM4bdyK9EhIVYGytB5MJe/3lq4Tf0hrjM+pvJZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "axios": "^1.13.2",
- "joi": "^18.0.1",
- "lodash": "^4.17.21",
+ "axios": "^1.13.5",
+ "joi": "^18.0.2",
+ "lodash": "^4.17.23",
"minimist": "^1.2.8",
"rxjs": "^7.8.2"
},
@@ -3208,13 +3240,13 @@
}
},
"node_modules/widest-line/node_modules/strip-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
- "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-regex": "^6.0.1"
+ "ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
diff --git a/script.js b/script.js
index a47e66a..42c7346 100644
--- a/script.js
+++ b/script.js
@@ -39,6 +39,169 @@ let minutesDisplay,
changeHistoryDialog,
closeChangeHistoryBtn;
+let updateCurrentTimeDisplay = () => {};
+let updateSessionCountdownDisplay = () => {};
+
+const ONE_HOUR_MS = 60 * 60 * 1000;
+const ONE_MINUTE_MS = 60 * 1000;
+const SESSION_COUNTDOWN_HOUR_THRESHOLD_MS = 90 * ONE_MINUTE_MS;
+
+function getSessionEndDate(now, hour, minute) {
+ const end = new Date(now);
+ end.setHours(hour, minute, 0, 0);
+
+ if (end <= now) {
+ if (hour < 12 && now.getHours() >= 12) {
+ end.setDate(end.getDate() + 1);
+ } else {
+ return null;
+ }
+ }
+
+ return end;
+}
+
+function formatSessionCountdownText(remainingMs) {
+ if (remainingMs <= 0) {
+ return { text: "TIME'S UP!", phase: 'times-up' };
+ }
+
+ if (remainingMs >= SESSION_COUNTDOWN_HOUR_THRESHOLD_MS) {
+ const hours = Math.floor(remainingMs / ONE_HOUR_MS);
+ return {
+ text: `${hours} hour${hours === 1 ? '' : 's'} remaining`,
+ phase: 'hours',
+ };
+ }
+
+ const minutes = Math.max(1, Math.ceil(remainingMs / ONE_MINUTE_MS));
+ return {
+ text: `${minutes} minute${minutes === 1 ? '' : 's'} remaining`,
+ phase: 'minutes',
+ };
+}
+
+function initSessionEndTimeSelectors() {
+ const hourSelect = document.getElementById('sessionEndHour');
+ const minuteSelect = document.getElementById('sessionEndMinute');
+ if (!hourSelect || !minuteSelect || hourSelect.options.length > 0) return;
+
+ for (let hour = 0; hour < 24; hour += 1) {
+ const option = document.createElement('option');
+ option.value = hour;
+ option.textContent = String(hour).padStart(2, '0');
+ hourSelect.appendChild(option);
+ }
+
+ for (let minute = 0; minute < 60; minute += 1) {
+ const option = document.createElement('option');
+ option.value = minute;
+ option.textContent = String(minute).padStart(2, '0');
+ minuteSelect.appendChild(option);
+ }
+}
+
+function updateTimeSettingsUi() {
+ const showCurrentTimeInput = document.getElementById('showCurrentTime');
+ if (showCurrentTimeInput) {
+ showCurrentTimeInput.checked = showCurrentTime;
+ }
+
+ const currentTimeEl = document.getElementById('currentTime');
+ if (currentTimeEl) {
+ currentTimeEl.hidden = !showCurrentTime;
+ }
+
+ document
+ .querySelectorAll('label:has(input[name="clockFormat"])')
+ .forEach((label) => {
+ label.classList.toggle('inactive', !showCurrentTime);
+ const input = label.querySelector('input');
+ if (input) {
+ input.disabled = !showCurrentTime;
+ }
+ });
+
+ const showSessionCountdownInput = document.getElementById(
+ 'showSessionCountdown'
+ );
+ if (showSessionCountdownInput) {
+ showSessionCountdownInput.checked = showSessionCountdown;
+ }
+
+ const sessionEndHourSelect = document.getElementById('sessionEndHour');
+ const sessionEndMinuteSelect = document.getElementById('sessionEndMinute');
+ if (sessionEndHourSelect) {
+ sessionEndHourSelect.value = String(sessionEndHour);
+ sessionEndHourSelect.disabled = !showSessionCountdown;
+ }
+ if (sessionEndMinuteSelect) {
+ sessionEndMinuteSelect.value = String(sessionEndMinute);
+ sessionEndMinuteSelect.disabled = !showSessionCountdown;
+ }
+
+ const sessionEndTimeLabel = document.getElementById('sessionEndTimeLabel');
+ if (sessionEndTimeLabel) {
+ sessionEndTimeLabel.classList.toggle('inactive', !showSessionCountdown);
+ }
+
+ const sessionCountdownEl = document.getElementById('sessionCountdown');
+ if (sessionCountdownEl) {
+ sessionCountdownEl.hidden = !showSessionCountdown;
+ }
+
+ if (showCurrentTime) {
+ updateCurrentTimeDisplay();
+ }
+ if (showSessionCountdown) {
+ updateSessionCountdownDisplay();
+ }
+}
+
+function initTimeDisplays() {
+ initSessionEndTimeSelectors();
+
+ const currentTimeEl = document.getElementById('currentTime');
+ const sessionCountdownEl = document.getElementById('sessionCountdown');
+
+ updateCurrentTimeDisplay = () => {
+ if (!currentTimeEl) return;
+ const now = new Date();
+ currentTimeEl.dateTime = now.toISOString();
+ currentTimeEl.textContent = now.toLocaleTimeString(undefined, {
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: clockFormat === '12',
+ });
+ };
+
+ updateSessionCountdownDisplay = () => {
+ if (!sessionCountdownEl) return;
+
+ const now = new Date();
+ const sessionEnd = getSessionEndDate(now, sessionEndHour, sessionEndMinute);
+ const remainingMs = sessionEnd ? sessionEnd - now : 0;
+ const { text, phase } = formatSessionCountdownText(remainingMs);
+
+ sessionCountdownEl.textContent = text;
+ sessionCountdownEl.setAttribute('aria-label', text);
+ sessionCountdownEl.classList.toggle('final-hour', phase === 'minutes');
+ sessionCountdownEl.classList.toggle('times-up', phase === 'times-up');
+ };
+
+ const tickTimeDisplays = () => {
+ if (showCurrentTime) {
+ updateCurrentTimeDisplay();
+ }
+ if (showSessionCountdown) {
+ updateSessionCountdownDisplay();
+ }
+ };
+
+ tickTimeDisplays();
+ setInterval(tickTimeDisplays, 1000);
+}
+
// Utility functions
const connectivityUtils = {
isOnline: () => navigator.onLine,
@@ -400,6 +563,11 @@ let backgroundTheme = 'medieval-cartoon'; // Default background theme
let youtubePlaylistUrl = DEFAULT_YOUTUBE_PLAYLIST; // Default playlist
let keepDisplayOn = true; // Default to true for wake lock
let showPlayerCountQr = false; // Optional QR linking to count.arcane-scripts.net
+let showCurrentTime = true; // Show wall clock under day display
+let clockFormat = '24'; // '12' or '24'
+let showSessionCountdown = false;
+let sessionEndHour = 23;
+let sessionEndMinute = 0;
let youtubePlayer = null;
let endOfDaySound = 'cathedral-bell-v2.mp3'; // Default end of day sound
let wakeUpSoundFile = 'chisel-bell-01-loud-v2.mp3'; // Default wake up sound
@@ -695,6 +863,7 @@ document.addEventListener('DOMContentLoaded', async () => {
startBtn = document.getElementById('startBtn');
updateStartButtonText(BUTTON_LABELS.WAKE_UP);
startBtn.disabled = false; // Ensure Wake Up button is enabled on load
+ initTimeDisplays();
resetBtn = document.getElementById('resetBtn');
resetBtn.textContent = BUTTON_LABELS.RESET;
resetBtn.disabled = true; // Reset button should be disabled initially
@@ -899,6 +1068,36 @@ document.addEventListener('DOMContentLoaded', async () => {
.classList.toggle('visible', showPlayerCountQr);
saveSettings();
});
+ document.getElementById('showCurrentTime').addEventListener('change', (e) => {
+ showCurrentTime = e.target.checked;
+ saveSettings();
+ updateTimeSettingsUi();
+ });
+ document
+ .getElementById('showSessionCountdown')
+ .addEventListener('change', (e) => {
+ showSessionCountdown = e.target.checked;
+ saveSettings();
+ updateTimeSettingsUi();
+ });
+ document.getElementById('sessionEndHour').addEventListener('change', (e) => {
+ sessionEndHour = Number.parseInt(e.target.value, 10);
+ saveSettings();
+ updateSessionCountdownDisplay();
+ });
+ document.getElementById('sessionEndMinute').addEventListener('change', (e) => {
+ sessionEndMinute = Number.parseInt(e.target.value, 10);
+ saveSettings();
+ updateSessionCountdownDisplay();
+ });
+ document.querySelectorAll('input[name="clockFormat"]').forEach((input) => {
+ input.addEventListener('change', (e) => {
+ if (!e.target.checked) return;
+ clockFormat = e.target.value;
+ saveSettings();
+ updateCurrentTimeDisplay();
+ });
+ });
// Add keyboard shortcuts event listeners
document.querySelectorAll('.shortcut-input').forEach((input) => {
@@ -1176,6 +1375,22 @@ function applyParsedSettings(settings) {
keepDisplayOn =
settings.keepDisplayOn === undefined ? true : settings.keepDisplayOn;
showPlayerCountQr = settings.showPlayerCountQr === true;
+ showCurrentTime =
+ settings.showCurrentTime === undefined ? true : settings.showCurrentTime;
+ clockFormat = settings.clockFormat === '12' ? '12' : '24';
+ showSessionCountdown = settings.showSessionCountdown === true;
+ sessionEndHour =
+ Number.isInteger(settings.sessionEndHour) &&
+ settings.sessionEndHour >= 0 &&
+ settings.sessionEndHour <= 23
+ ? settings.sessionEndHour
+ : 23;
+ sessionEndMinute =
+ Number.isInteger(settings.sessionEndMinute) &&
+ settings.sessionEndMinute >= 0 &&
+ settings.sessionEndMinute <= 59
+ ? settings.sessionEndMinute
+ : 0;
youtubeVolume = settings.youtubeVolume || 15;
backgroundTheme = settings.backgroundTheme || 'medieval-cartoon';
youtubePlaylistUrl = settings.youtubePlaylistUrl || DEFAULT_YOUTUBE_PLAYLIST;
@@ -1247,6 +1462,10 @@ function applySettingsToForm() {
document.getElementById('musicVolume').value = youtubeVolume;
document.getElementById('soundEffectsVolume').value = soundEffectsVolume;
document.getElementById('backgroundTheme').value = backgroundTheme;
+ document.querySelector(
+ `input[name="clockFormat"][value="${clockFormat}"]`
+ ).checked = true;
+ updateTimeSettingsUi();
document.querySelector(
'label:has(#musicVolume) .volume-value'
).textContent = `${youtubeVolume}%`;
@@ -1423,6 +1642,11 @@ function saveSettings() {
soundEffectsVolume,
keepDisplayOn,
showPlayerCountQr,
+ showCurrentTime,
+ clockFormat,
+ showSessionCountdown,
+ sessionEndHour,
+ sessionEndMinute,
youtubeVolume,
youtubePlaylistUrl,
backgroundTheme,
diff --git a/styles.css b/styles.css
index 713d7c6..d476cb5 100644
--- a/styles.css
+++ b/styles.css
@@ -478,12 +478,71 @@ html.fonts-ready .timer-display .time {
background-color: var(--colour-accent);
}
-#startBtn {
+.timer-actions {
position: absolute;
top: 50%;
right: 0.5rem;
transform: translateY(-50%);
z-index: 2;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.4rem;
+}
+
+.day-info {
+ position: absolute;
+ top: 50%;
+ left: 0.5rem;
+ transform: translateY(-50%);
+ z-index: 2;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.4rem;
+ width: 120px;
+}
+
+.current-time {
+ font-family: 'Azeret Mono', ui-monospace, 'Cascadia Mono', monospace;
+ font-size: 0.95rem;
+ font-weight: 500;
+ font-variant-numeric: tabular-nums;
+ color: rgba(255, 255, 255, 0.85);
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
+ line-height: 1;
+ width: 100%;
+ text-align: center;
+}
+
+.session-countdown {
+ font-size: 0.85rem;
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+ color: rgba(255, 255, 255, 0.85);
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
+ line-height: 1.2;
+ min-width: 140px;
+ text-align: center;
+}
+
+.session-countdown[hidden] {
+ display: none;
+}
+
+.session-countdown.final-hour {
+ color: var(--colour-gold);
+}
+
+.session-countdown.times-up {
+ color: #ff6b6b;
+ font-weight: 700;
+ letter-spacing: 0.02em;
+}
+
+#startBtn {
+ position: static;
+ transform: none;
background-color: var(--colour-accent);
color: white;
padding: 1.5rem 1rem;
@@ -547,18 +606,18 @@ html.fonts-ready .timer-display .time {
#startBtn:hover {
background-color: var(--colour-accent-hover);
- transform: translateY(calc(-50% + 1px));
+ transform: translateY(1px);
}
#startBtn:active {
background-color: var(--colour-accent-hover);
- transform: translateY(calc(-50% + 2px)) scale(0.98);
+ transform: translateY(2px) scale(0.98);
}
#startBtn:disabled {
background-color: #cccccc;
cursor: not-allowed;
- transform: translateY(-50%);
+ transform: none;
}
#resetBtn {
@@ -1132,10 +1191,8 @@ body[data-pace='blitz'] .info-value {
}
.day-display {
- position: absolute;
- top: 50%;
- left: 0.5rem;
- transform: translateY(-50%);
+ position: static;
+ transform: none;
z-index: 2;
font-size: 1.25rem;
font-weight: 700;
@@ -1689,6 +1746,40 @@ body:has(.clocktower-settings.visible) #regularTimerControls {
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
+.session-end-time-inputs {
+ display: flex;
+ align-items: center;
+ gap: 0.35rem;
+}
+
+.session-end-select {
+ min-width: 4.5rem;
+ padding: 0.5rem;
+ font-size: 1rem;
+ font-family: 'Azeret Mono', ui-monospace, 'Cascadia Mono', monospace;
+ border: 1px solid #4a4a4a;
+ border-radius: var(--radius-md);
+ background-color: var(--colour-button-secondary);
+ color: white;
+ cursor: pointer;
+}
+
+.session-end-select:hover {
+ background-color: var(--colour-button-secondary-hover);
+}
+
+.session-end-select:focus {
+ outline: none;
+ border-color: var(--colour-accent);
+ box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
+}
+
+.session-end-separator {
+ color: rgba(255, 255, 255, 0.8);
+ font-family: 'Azeret Mono', ui-monospace, 'Cascadia Mono', monospace;
+ font-weight: 600;
+}
+
.game-pace-display {
font-size: 1rem;
color: var(--colour-gold);
@@ -1913,12 +2004,16 @@ button:disabled {
}
/* Restore checkbox styling */
-.setting-group input[type='checkbox'] {
+.setting-group input[type='checkbox'],
+.setting-group input[type='radio'] {
width: 24px;
height: 24px;
accent-color: var(--colour-accent);
cursor: pointer;
position: relative;
+}
+
+.setting-group input[type='checkbox'] {
border-radius: var(--radius-md);
}