Compare commits
3 Commits
9ce5664621
...
2028ba80e0
| Author | SHA1 | Date | |
|---|---|---|---|
| 2028ba80e0 | |||
| 3338978163 | |||
| 7e3e30ec83 |
31
CHANGELOG.md
31
CHANGELOG.md
@ -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
4
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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
56
scripts/update-app.sh
Normal 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 "$@"
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user