Compare commits

...

3 Commits

Author SHA1 Message Date
2028ba80e0 fix: copy public/ and .next/static/ into standalone output on deploy
Some checks are pending
Playwright Tests / test (push) Waiting to run
next.config.js sets output: 'standalone', but Next.js does not copy public/
or .next/static/ into .next/standalone — that is a documented manual step.
Skipping it shipped new server code referencing new chunk hashes while the
old chunks stayed in the standalone tree, so those requests 404'd, React
never hydrated, and pages hung on "Loading session..." while the API kept
returning 200.

v1.114.2

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 13:13:42 +02:00
3338978163 chore: version-control the production deploy script
Moves the deploy script off the server and into the repo as
scripts/update-app.sh, with the two fixes made during the v1.114.0 deploy:

- set -euo pipefail, so a failed git pull aborts instead of rebuilding and
  restarting PM2 on unchanged code while printing "Update Complete"
- npm ci instead of npm install, which rewrote package-lock.json and left the
  tree dirty so the next git pull aborted

The body is wrapped in a main() function because the script lives inside the
checkout it updates, and git pull rewriting it mid-run would leave bash
reading from a stale file offset.

v1.114.1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 19:03:08 +02:00
7e3e30ec83 feat: add SKIP button to Focus Mode
Focus Mode now shows a SKIP button that moves to the next task of the day
without completing the current one. Skipped task ids are tracked in
WeeklyView and filtered out of the focus queue, so completing a task still
picks the correct next one; the list is cleared when Focus Mode closes.
The timer resets on each new task, and the button is disabled when no other
task is left for today.

Also drops the standalone "Element Colors" and "Weekend Colors" sections from
Styling settings (those controls live in the CSS Variables panel since
1.113.1) and enlarges the CSS variable swatches.

v1.114.0

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 17:37:17 +02:00
7 changed files with 140 additions and 182 deletions

View File

@ -2,6 +2,37 @@
All notable changes to this project are documented in this file. All notable changes to this project are documented in this file.
## [1.114.2] - 2026-09-01
### Fixed
- Deploy script: copy `public/` and `.next/static/` into `.next/standalone/` after the build.
`next.config.js` sets `output: 'standalone'`, and Next.js does not copy those itself. Without
the copy every deploy left the old chunk hashes in the standalone tree, so the browser's
requests for the new chunks 404'd, React never hydrated, and the app hung on "Loading
session..." — while the API kept returning 200, which made it look like an auth problem.
## [1.114.1] - 2026-09-01
### Added
- `scripts/update-app.sh`: the production deploy script, now version-controlled instead of
living only on the server. Hardened with `set -euo pipefail` (a failed `git pull` used to
still trigger a rebuild and PM2 restart on unchanged code) and switched from `npm install`
to `npm ci` (npm install rewrote package-lock.json, leaving the tree dirty so the next
pull aborted).
## [1.114.0] - 2026-09-01
### Added
- Focus Mode: "SKIP" button to move on to the next task of the day without completing the
current one. Skipped tasks are remembered until Focus Mode is closed, and the timer
resets for each new task. The button is disabled when no other task is left for today.
### Changed
- Styling settings: removed the standalone "Element Colors" and "Weekend Colors" sections —
those controls now live only in the CSS Variables panel (added there in 1.113.1).
- Styling settings: larger, borderless color swatches and bigger labels in the CSS Variables
panel.
## [1.113.1] - 2026-07-08 ## [1.113.1] - 2026-07-08
### Changed ### Changed

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.104.1", "version": "1.114.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.104.1", "version": "1.114.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@auth/prisma-adapter": "^2.11.1", "@auth/prisma-adapter": "^2.11.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.113.1", "version": "1.114.2",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

56
scripts/update-app.sh Normal file
View File

@ -0,0 +1,56 @@
#!/bin/bash
#
# Deploy script for the production host (WebApp-Testing-live).
#
# The app runs there as a PM2 process ("my-weekly-todo") from /root/My-Weekly-ToDo-List.
# It is NOT run under Docker — deploy.sh and docker-compose.yml in this repo describe an
# older Portainer setup that is not what serves todo.martin-bierschenk.de.
#
# Usage on the host: /root/My-Weekly-ToDo-List/scripts/update-app.sh
#
# The whole body is wrapped in a function so bash parses it in one go: this script lives
# inside the very checkout it updates, and `git pull` rewriting it mid-run would otherwise
# make bash resume at a stale file offset.
main() {
set -euo pipefail
# Fail fast instead of hanging on a credential prompt when run without a terminal
# (e.g. from systemd). The host authenticates via a read-only SSH deploy key.
export GIT_TERMINAL_PROMPT=0
local APP_DIR="/root/My-Weekly-ToDo-List"
local PM2_APP="my-weekly-todo"
cd "$APP_DIR"
echo "--- Pulling latest changes from git ---"
git pull origin main
# npm ci, not npm install: it installs exactly what package-lock.json specifies and
# never rewrites it. npm install rewrites the lockfile, which leaves the working tree
# dirty and makes the *next* git pull abort.
echo "--- Installing dependencies ---"
npm ci
echo "--- Building application ---"
npm run build
# next.config.js sets output: 'standalone', and Next.js does NOT copy public/ or
# .next/static/ into .next/standalone — that is a documented manual step. Skipping it
# ships new server code whose HTML references new chunk hashes while the old chunks sit
# in the standalone tree: they 404, React never hydrates, and every page hangs on its
# server-rendered shell ("Loading session...") while the API still answers 200.
echo "--- Copying static assets into standalone output ---"
cp -r public .next/standalone/
cp -r .next/static .next/standalone/.next/
echo "--- Restarting PM2 process ---"
pm2 restart "$PM2_APP" --update-env
pm2 save
echo "--- Update Complete ---"
echo "Deployed version: $(node -p "require('$APP_DIR/package.json').version")"
}
main "$@"

View File

@ -11,9 +11,11 @@ interface FocusModeOverlayProps {
duration: number; // in minutes duration: number; // in minutes
onClose: () => void; onClose: () => void;
onComplete: (taskId: string) => void; onComplete: (taskId: string) => void;
onSkip?: (taskId: string) => void;
hasNext?: boolean;
} }
export default function FocusModeOverlay({ task, duration, onClose, onComplete }: FocusModeOverlayProps) { export default function FocusModeOverlay({ task, duration, onClose, onComplete, onSkip, hasNext }: FocusModeOverlayProps) {
const [timeLeft, setTimeLeft] = useState(duration * 60); const [timeLeft, setTimeLeft] = useState(duration * 60);
const [isActive, setIsActive] = useState(false); const [isActive, setIsActive] = useState(false);
const [isCompleting, setIsCompleting] = useState(false); const [isCompleting, setIsCompleting] = useState(false);
@ -51,6 +53,12 @@ export default function FocusModeOverlay({ task, duration, onClose, onComplete }
setTimeLeft(duration * 60); setTimeLeft(duration * 60);
}; };
const handleSkip = () => {
if (!task || !onSkip) return;
onSkip(task.id);
handleReset(); // Fresh timer for the next task
};
const handleComplete = async () => { const handleComplete = async () => {
if (!task) return; if (!task) return;
setIsCompleting(true); setIsCompleting(true);
@ -127,6 +135,16 @@ export default function FocusModeOverlay({ task, duration, onClose, onComplete }
<button className="focus-btn secondary" onClick={handleReset}> <button className="focus-btn secondary" onClick={handleReset}>
RESET RESET
</button> </button>
{task && onSkip && (
<button
className="focus-btn secondary"
onClick={handleSkip}
disabled={!hasNext || isCompleting}
title={hasNext ? 'Skip to the next task' : 'No other task left today'}
>
SKIP
</button>
)}
{task && ( {task && (
<button className="focus-btn success" onClick={handleComplete} disabled={isCompleting}> <button className="focus-btn success" onClick={handleComplete} disabled={isCompleting}>
{isCompleting ? 'COMPLETING...' : 'COMPLETE TASK'} {isCompleting ? 'COMPLETING...' : 'COMPLETE TASK'}
@ -249,6 +267,13 @@ export default function FocusModeOverlay({ task, duration, onClose, onComplete }
.focus-btn.secondary:hover { .focus-btn.secondary:hover {
border-color: white; border-color: white;
} }
.focus-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.focus-btn:disabled:hover {
transform: none;
}
.focus-btn.success { .focus-btn.success {
background: #009a9a; /* Weekly Teal */ background: #009a9a; /* Weekly Teal */
color: white; color: white;

View File

@ -358,9 +358,9 @@ function CssVariablesEditor({
onChange={onChange} onChange={onChange}
style={{ style={{
width: "32px", width: "32px",
height: "22px", height: "32px",
cursor: "pointer", cursor: "pointer",
border: "1px solid var(--weekly-settings-input-border)", border: "none",
borderRadius: "4px", borderRadius: "4px",
background: "transparent", background: "transparent",
flexShrink: 0, flexShrink: 0,
@ -368,7 +368,7 @@ function CssVariablesEditor({
/> />
<span <span
style={{ style={{
fontSize: "0.72rem", fontSize: "0.85rem",
color: "var(--weekly-settings-label)", color: "var(--weekly-settings-label)",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
@ -3790,171 +3790,7 @@ function SettingsSidebar({
</div> </div>
</div> </div>
{/* Element Colors */} {/* CSS Variables (includes Element Colors / Weekend Highlight Colors groups) */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
marginBottom: "12px",
}}
>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
Element Colors
</label>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "12px",
}}
>
<div>
<label
style={{
display: "block",
fontSize: "0.75rem",
color: "var(--weekly-settings-label)",
marginBottom: "4px",
}}
>
Today Highlight
</label>
<input
type="color"
value={profile.todayHighlightColor || "#f0fafa"}
onChange={(e) =>
saveFieldDebounced("todayHighlightColor", e.target.value)
}
style={{
width: "100%",
height: "30px",
cursor: "pointer",
border: "none",
background: "transparent",
}}
/>
</div>
<div>
<label
style={{
display: "block",
fontSize: "0.75rem",
color: "var(--weekly-settings-label)",
marginBottom: "4px",
}}
>
Past Days
</label>
<input
type="color"
value={profile.pastDayColor || "#a6a6a7"}
onChange={(e) =>
saveFieldDebounced("pastDayColor", e.target.value)
}
style={{
width: "100%",
height: "30px",
cursor: "pointer",
border: "none",
background: "transparent",
}}
/>
</div>
</div>
</div>
{/* Weekend Colors */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "12px",
borderRadius: "8px",
marginBottom: "12px",
}}
>
<label
style={{
display: "block",
fontSize: "0.85rem",
fontWeight: 600,
color: "var(--weekly-settings-label)",
marginBottom: "8px",
}}
>
Weekend Highlight Colors
</label>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "12px",
}}
>
<div>
<label
style={{
display: "block",
fontSize: "0.75rem",
color: "var(--weekly-settings-label)",
marginBottom: "4px",
}}
>
Saturday
</label>
<input
type="color"
value={profile.weekendColorSat || "#666666"}
onChange={(e) =>
saveFieldDebounced("weekendColorSat", e.target.value)
}
style={{
width: "100%",
height: "30px",
cursor: "pointer",
border: "none",
background: "transparent",
}}
/>
</div>
<div>
<label
style={{
display: "block",
fontSize: "0.75rem",
color: "var(--weekly-settings-label)",
marginBottom: "4px",
}}
>
Sunday
</label>
<input
type="color"
value={profile.weekendColorSun || "#dc2626"}
onChange={(e) =>
saveFieldDebounced("weekendColorSun", e.target.value)
}
style={{
width: "100%",
height: "30px",
cursor: "pointer",
border: "none",
background: "transparent",
}}
/>
</div>
</div>
</div>
{/* CSS Variables */}
<CssVariablesEditor profile={profile} setProfile={setProfile} saveSetting={saveSetting} /> <CssVariablesEditor profile={profile} setProfile={setProfile} saveSetting={saveSetting} />
{/* Custom CSS */} {/* Custom CSS */}

View File

@ -1250,6 +1250,7 @@ export default function WeeklyView() {
const [selectedTaskForRecurrence, setSelectedTaskForRecurrence] = const [selectedTaskForRecurrence, setSelectedTaskForRecurrence] =
useState<Task | null>(null); useState<Task | null>(null);
const [showFocusMode, setShowFocusMode] = useState(false); const [showFocusMode, setShowFocusMode] = useState(false);
const [focusSkippedIds, setFocusSkippedIds] = useState<string[]>([]);
const [showSchedule, setShowSchedule] = useState(true); const [showSchedule, setShowSchedule] = useState(true);
const [focusBreakDuration, setFocusBreakDuration] = useState(5); const [focusBreakDuration, setFocusBreakDuration] = useState(5);
const [weekdayFormat, setWeekdayFormat] = useState<"long" | "short" | "narrow" | "custom">("long"); const [weekdayFormat, setWeekdayFormat] = useState<"long" | "short" | "narrow" | "custom">("long");
@ -9382,9 +9383,8 @@ export default function WeeklyView() {
)} )}
{/* Focus Mode Overlay */} {/* Focus Mode Overlay */}
{ {
showFocusMode && ( showFocusMode && (() => {
<FocusModeOverlay const focusQueue = (() => {
task={(() => {
// Logic to find the "Next Task" // Logic to find the "Next Task"
// 1. Tasks for today with start time, sorted by time // 1. Tasks for today with start time, sorted by time
// 2. Tasks for today without start time, sorted by order // 2. Tasks for today without start time, sorted by order
@ -9424,13 +9424,23 @@ export default function WeeklyView() {
return a.order - b.order; return a.order - b.order;
}); });
return activeTasks.length > 0 ? activeTasks[0] : null; return activeTasks.filter((t) => !focusSkippedIds.includes(t.id));
})()} })();
duration={focusTimerDuration}
onClose={() => setShowFocusMode(false)} return (
onComplete={(taskId) => toggleTask(taskId)} <FocusModeOverlay
/> task={focusQueue.length > 0 ? focusQueue[0] : null}
) hasNext={focusQueue.length > 1}
onSkip={(taskId) => setFocusSkippedIds((prev) => [...prev, taskId])}
duration={focusTimerDuration}
onClose={() => {
setShowFocusMode(false);
setFocusSkippedIds([]);
}}
onComplete={(taskId) => toggleTask(taskId)}
/>
);
})()
} }
{/* Settings Sidebar */} {/* Settings Sidebar */}