feat: update better-sqlite3 dependency and enhance timer metrics with target hours functionality
Some checks failed
Build and Push Docker Image / build (push) Failing after 27s

This commit is contained in:
Felix Schlusche
2025-10-31 18:15:32 +01:00
parent 763d7d141b
commit 425c817522
4 changed files with 98 additions and 43 deletions

17
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^9.2.2",
"better-sqlite3": "^12.4.1",
"express": "^4.18.2"
}
},
@@ -53,14 +53,17 @@
"license": "MIT"
},
"node_modules/better-sqlite3": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.6.0.tgz",
"integrity": "sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==",
"version": "12.4.1",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz",
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
},
"engines": {
"node": "20.x || 22.x || 23.x || 24.x"
}
},
"node_modules/bindings": {
@@ -733,9 +736,9 @@
}
},
"node_modules/node-abi": {
"version": "3.78.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz",
"integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==",
"version": "3.80.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.80.0.tgz",
"integrity": "sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"

View File

@@ -7,11 +7,15 @@
"start": "node server.js",
"dev": "node server.js"
},
"keywords": ["timetracker", "express", "sqlite"],
"keywords": [
"timetracker",
"express",
"sqlite"
],
"author": "",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"better-sqlite3": "^9.2.2"
"better-sqlite3": "^12.4.1",
"express": "^4.18.2"
}
}

View File

@@ -372,7 +372,8 @@
<!-- Start/Stop Timer Section -->
<div class="mb-6 p-6 glass-card rounded-xl">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6">
<!-- Left side: Timer display and controls -->
<div class="flex-1">
<div class="text-sm text-gray-400 mb-2 font-medium">Heutige Arbeitszeit</div>
<div id="timerDisplay" class="text-5xl font-bold text-white">00:00:00</div>
@@ -384,11 +385,8 @@
<!-- Target hours selector -->
<div class="mt-3 flex items-center gap-2">
<label for="targetHoursSelect" class="text-sm text-gray-400">Soll-Stunden:</label>
<label for="targetHoursSelect" class="text-sm text-gray-400">Geplante Arbeitszeit:</label>
<select id="targetHoursSelect" class="px-3 py-1 bg-gray-700 text-gray-100 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm">
<option value="1">1h</option>
<option value="2">2h</option>
<option value="3">3h</option>
<option value="4">4h</option>
<option value="5">5h</option>
<option value="6">6h</option>
@@ -400,24 +398,30 @@
</div>
<input type="text" id="manualStartTimeInput" class="hidden">
</div>
<!-- Additional Timer Metrics -->
<div id="timerMetrics" class="hidden mt-4 space-y-1 text-sm">
<button id="timerStatus" class="flex items-center gap-2 text-gray-300 hover:text-gray-100 cursor-pointer transition-all text-left w-full" title="Startzeit manuell eingeben">
<i data-lucide="clock" class="w-4 h-4 text-yellow-400"></i>
<span id="timerStatusText">Nicht gestartet</span>
</button>
<div class="flex items-center gap-2 text-gray-300">
<i data-lucide="target" class="w-4 h-4 text-green-400"></i>
<span>Soll erreicht: <span id="targetReachedTime" class="font-semibold text-white">--:--</span></span>
</div>
<div class="flex items-center gap-2 text-gray-300">
<i data-lucide="timer" class="w-4 h-4 text-blue-400"></i>
<span>Zeit bis Soll: <span id="timeUntilTarget" class="font-semibold text-white">--:--</span></span>
</div>
<!-- Right side: Timer Metrics (responsive - below on small screens, right on large) -->
<div id="timerMetrics" class="hidden w-full lg:w-auto lg:min-w-[280px] space-y-1 text-sm">
<button id="timerStatus" class="flex items-center gap-2 text-gray-300 hover:text-gray-100 cursor-pointer transition-all text-left w-full" title="Startzeit manuell eingeben">
<i data-lucide="clock" class="w-4 h-4 text-yellow-400"></i>
<span id="timerStatusText">Nicht gestartet</span>
</button>
<div class="flex items-center gap-2 text-gray-300">
<i data-lucide="target" class="w-4 h-4 text-green-400"></i>
<span>Soll erreicht: <span id="targetReachedTime" class="font-semibold text-white">--:--</span></span>
</div>
<div class="flex items-center gap-2 text-gray-300">
<i data-lucide="timer" class="w-4 h-4 text-blue-400"></i>
<span>Zeit bis Soll: <span id="timeUntilTarget" class="font-semibold text-white">--:--</span></span>
</div>
<div class="flex items-center gap-2 text-gray-300">
<i data-lucide="trending-up" class="w-4 h-4 text-purple-400"></i>
<span>Saldo bei Soll: <span id="balanceAtTarget" class="font-semibold text-white">--:--</span></span>
</div>
</div>
<div class="flex gap-3">
<!-- Buttons -->
<div class="flex gap-3 w-full lg:w-auto justify-center lg:justify-start">
<button id="btnStartWork"
class="btn-elevated inline-flex items-center justify-center gap-2 px-8 py-4 bg-gradient-to-r from-green-600 to-green-500 text-white rounded-xl font-semibold text-lg" title="Start">
<i data-lucide="play" class="w-6 h-6"></i>

View File

@@ -226,6 +226,9 @@ async function startWork() {
timerPausedDuration = 0;
isPaused = false;
// Save current target hours
await setSetting('targetHours', targetHours);
// Update UI
const startBtn = document.getElementById('btnStartWork');
const stopBtn = document.getElementById('btnStopWork');
@@ -278,6 +281,11 @@ async function stopWork() {
timerPausedDuration = 0;
isPaused = false;
// Reset target hours to 8 and clear saved setting
targetHours = 8;
document.getElementById('targetHoursSelect').value = 8;
await setSetting('targetHours', null);
// Update UI
const startBtn = document.getElementById('btnStartWork');
const stopBtn = document.getElementById('btnStopWork');
@@ -339,7 +347,8 @@ function updateTimer() {
const balanceCell = document.getElementById('current-day-balance');
if (balanceCell) {
const netHours = elapsed / 3600; // Convert seconds to hours
const deviation = netHours - targetHours;
// Always use 8h standard target for balance calculation (not the dropdown target)
const deviation = netHours - 8.0;
const baseBalance = parseFloat(balanceCell.dataset.baseBalance || 0);
const currentBalance = baseBalance + deviation;
@@ -383,6 +392,7 @@ function updateTimer() {
function updateTimerMetrics(netElapsedSeconds) {
const targetReachedTimeSpan = document.getElementById('targetReachedTime');
const timeUntilTargetSpan = document.getElementById('timeUntilTarget');
const balanceAtTargetSpan = document.getElementById('balanceAtTarget');
if (!timerStartTime) {
return;
@@ -424,6 +434,23 @@ function updateTimerMetrics(netElapsedSeconds) {
timeUntilTargetSpan.classList.remove('text-green-400');
}
// Calculate balance when target is reached
// Get base balance from current day row (balance before today)
const balanceCell = document.getElementById('current-day-balance');
if (balanceCell && balanceAtTargetSpan) {
const baseBalance = parseFloat(balanceCell.dataset.baseBalance || 0);
// Deviation when target hours are reached: targetHours - 8h (standard)
const targetNetHours = targetHours;
const deviation = targetNetHours - 8.0;
const balanceAtTarget = baseBalance + deviation;
const balanceColor = balanceAtTarget >= 0 ? 'text-green-400' : 'text-red-400';
const balanceSign = balanceAtTarget >= 0 ? '+' : '';
balanceAtTargetSpan.className = `font-semibold ${balanceColor}`;
balanceAtTargetSpan.textContent = `${balanceSign}${balanceAtTarget.toFixed(2)}h`;
}
// Reinitialize Lucide icons for the new metrics
if (typeof lucide !== 'undefined' && lucide.createIcons) {
lucide.createIcons();
@@ -1055,20 +1082,25 @@ async function loadMonthlyView() {
updateStatistics(entries);
updateBridgeDaysDisplay();
// Show/hide PDF export button based on whether month is complete
const today = new Date();
const isCurrentOrFutureMonth = displayYear > today.getFullYear() ||
(displayYear === today.getFullYear() && displayMonth >= today.getMonth());
// Show/hide PDF export button based on whether last day has complete times
const pdfButton = document.getElementById('btnExportPDF');
const pdfButtonMobile = document.getElementById('btnExportPDFMobile');
if (isCurrentOrFutureMonth) {
pdfButton.style.display = 'none';
if (pdfButtonMobile) pdfButtonMobile.style.display = 'none';
} else {
// Check if last day of month has start and end time (and they're different)
const lastDayDate = `${displayYear}-${String(displayMonth + 1).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
const lastDayEntry = entries.find(e => e.date === lastDayDate);
const isLastDayComplete = lastDayEntry &&
lastDayEntry.startTime &&
lastDayEntry.endTime &&
lastDayEntry.startTime !== lastDayEntry.endTime;
if (isLastDayComplete) {
pdfButton.style.display = '';
if (pdfButtonMobile) pdfButtonMobile.style.display = '';
} else {
pdfButton.style.display = 'none';
if (pdfButtonMobile) pdfButtonMobile.style.display = 'none';
}
}
@@ -2804,6 +2836,13 @@ async function loadSettings() {
document.getElementById('companyHolidayNewYear').checked = true;
}
}
// Load target hours (always restore dropdown value)
const savedTargetHours = await getSetting('targetHours');
if (savedTargetHours) {
targetHours = parseInt(savedTargetHours);
document.getElementById('targetHoursSelect').value = targetHours;
}
}
/**
@@ -3706,6 +3745,9 @@ async function handleManualStartTime(timeStr) {
timerPausedDuration = 0;
isPaused = false;
// Save current target hours
await setSetting('targetHours', targetHours);
// Update UI
const startBtn = document.getElementById('btnStartWork');
const stopBtn = document.getElementById('btnStopWork');
@@ -3846,10 +3888,12 @@ function initializeEventListeners() {
document.getElementById('btnStopWork').addEventListener('click', stopWork);
// Target hours selector
document.getElementById('targetHoursSelect').addEventListener('change', (e) => {
document.getElementById('targetHoursSelect').addEventListener('change', async (e) => {
targetHours = parseInt(e.target.value);
// Update metrics if timer is running
// Save target hours if timer is running
if (timerStartTime) {
await setSetting('targetHours', targetHours);
const elapsed = Math.floor((Date.now() - timerStartTime) / 1000) - timerPausedDuration;
updateTimerMetrics(elapsed);
}