18 Commits

Author SHA1 Message Date
Hartmut 55b861cb43 ci: retrigger — verify actions clone from internal Gitea mirror 2026-05-22 17:07:29 +02:00
Hartmut 184efee0e1 Merge pull request 'ci: increase timeouts + pnpm store cache to reduce QNAP runner flakes' (#66) from ci/reduce-qnap-flakes into main
CI / Architecture Guardrails (push) Successful in 3m42s
CI / Assistant Split Regression (push) Successful in 4m39s
CI / Lint (push) Successful in 5m1s
CI / Typecheck (push) Successful in 5m6s
CI / Unit Tests (push) Successful in 6m58s
CI / Build (push) Successful in 6m49s
CI / E2E Tests (push) Successful in 4m50s
CI / Fresh-Linux Docker Deploy (push) Successful in 5m9s
CI / Release Images (push) Successful in 6m48s
ci: increase timeouts + pnpm store cache to reduce QNAP runner flakes
2026-05-22 15:13:30 +02:00
Hartmut 5db6e24ee0 ci: increase timeouts + pnpm store cache to reduce QNAP runner flakes
CI / Architecture Guardrails (pull_request) Successful in 3m48s
CI / Assistant Split Regression (pull_request) Successful in 5m11s
CI / Typecheck (pull_request) Successful in 5m42s
CI / Lint (pull_request) Successful in 5m46s
CI / Build (pull_request) Successful in 7m54s
CI / E2E Tests (pull_request) Successful in 5m35s
CI / Fresh-Linux Docker Deploy (pull_request) Successful in 5m53s
CI / Unit Tests (pull_request) Successful in 5m43s
CI / Release Images (pull_request) Has been skipped
Three distinct flake types addressed:
- pnpm store cache: faster installs reduce total job time
- E2E webServer timeout: 3m → 5m (QNAP cold-start is slow)
- Docker deploy health poll: 3m → 5m headroom for image build
- Pre-pull node:20-bookworm-slim: warms Docker layer cache

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 11:50:52 +02:00
Hartmut eb1875e524 Merge pull request 'feat(timeline): start at today + infinite scroll into the past' (#65) from feature/timeline-past-scroll into main
CI / Architecture Guardrails (push) Successful in 3m43s
CI / Lint (push) Successful in 4m31s
CI / Typecheck (push) Successful in 4m33s
CI / Assistant Split Regression (push) Successful in 4m53s
CI / Build (push) Successful in 8m5s
CI / E2E Tests (push) Successful in 8m48s
CI / Fresh-Linux Docker Deploy (push) Successful in 8m41s
CI / Unit Tests (push) Successful in 5m17s
CI / Release Images (push) Has been cancelled
feat(timeline): start at today + infinite scroll into the past
2026-05-22 11:43:07 +02:00
Hartmut 12044f638e ci: retrigger — E2E webServer timeout on run #172 (QNAP runner flake)
CI / Typecheck (pull_request) Successful in 3m25s
CI / Architecture Guardrails (pull_request) Successful in 3m44s
CI / Lint (pull_request) Successful in 2m8s
CI / Assistant Split Regression (pull_request) Successful in 3m44s
CI / Unit Tests (pull_request) Successful in 7m29s
CI / Build (pull_request) Successful in 6m52s
CI / Fresh-Linux Docker Deploy (pull_request) Successful in 3m53s
CI / E2E Tests (pull_request) Successful in 20m24s
CI / Release Images (pull_request) Has been skipped
2026-05-22 09:34:10 +02:00
Hartmut 6bc23b3407 Merge pull request 'rename(cleanup): drop last capakraken strings from UI, scripts, schema, tests' (#64) from rename/nexus-final-cleanup into main
CI / Architecture Guardrails (push) Successful in 4m52s
CI / Typecheck (push) Successful in 5m53s
CI / Assistant Split Regression (push) Successful in 9m3s
CI / Lint (push) Successful in 9m39s
CI / Unit Tests (push) Successful in 12m5s
CI / Build (push) Successful in 9m53s
CI / Fresh-Linux Docker Deploy (push) Failing after 8m54s
CI / E2E Tests (push) Successful in 12m55s
CI / Release Images (push) Has been skipped
rename(cleanup): drop last capakraken strings from UI, scripts, schema, tests
2026-05-22 09:13:05 +02:00
Hartmut 2383bcbdc0 fix(timeline): trigger scroll-to-today on isInitialLoading→false not totalCanvasWidth
CI / Architecture Guardrails (pull_request) Successful in 2m53s
CI / Typecheck (pull_request) Successful in 3m28s
CI / Assistant Split Regression (pull_request) Successful in 3m40s
CI / Lint (pull_request) Successful in 4m26s
CI / Unit Tests (pull_request) Successful in 8m36s
CI / Build (pull_request) Successful in 9m47s
CI / E2E Tests (pull_request) Failing after 14m2s
CI / Fresh-Linux Docker Deploy (pull_request) Successful in 16m53s
CI / Release Images (pull_request) Has been skipped
totalCanvasWidth is computed from viewStart/viewDays before data loads,
so the previous trigger fired during the loading spinner. scrollLeft
was clipped to 0 (no canvas in DOM yet) and the guard was set, blocking
the real scroll after data arrived. Using isInitialLoading as the dep
fires the effect exactly when the canvas enters the DOM.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 08:48:23 +02:00
Hartmut 0e9d6ec388 fix(timeline): wait for canvas width before scrolling to today
CI / Assistant Split Regression (pull_request) Has been cancelled
CI / Lint (pull_request) Has been cancelled
CI / Typecheck (pull_request) Has been cancelled
CI / Unit Tests (pull_request) Has been cancelled
CI / Build (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Fresh-Linux Docker Deploy (pull_request) Has been cancelled
CI / Release Images (pull_request) Has been cancelled
CI / Architecture Guardrails (pull_request) Has been cancelled
useLayoutEffect([]) fired before isInitialLoading resolved, so the
scroll container had no canvas yet — scrollLeft was clipped to 0.
Now the scroll-to-today fires on the first render where totalCanvasWidth
becomes non-zero. The cleanup effect resets the guard on unmount so
React Strict Mode's fake-unmount+remount also scrolls correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 08:45:09 +02:00
Hartmut 7285668c52 fix(timeline): use empty-deps useLayoutEffect for mount scroll to today
CI / Architecture Guardrails (pull_request) Successful in 4m53s
CI / Typecheck (pull_request) Successful in 4m55s
CI / Assistant Split Regression (pull_request) Successful in 5m38s
CI / Build (pull_request) Has been cancelled
CI / Lint (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Fresh-Linux Docker Deploy (pull_request) Has been cancelled
CI / Release Images (pull_request) Has been cancelled
CI / Unit Tests (pull_request) Has been cancelled
The guard-ref approach broke in React Strict Mode (dev): the ref
persisted as `true` across the simulated remount, so the second
invocation skipped the scroll — leaving scrollLeft=0 (today-90
at the left edge, not today). An empty-deps useLayoutEffect runs
twice in Strict Mode but both executions fire against the same
initial `toLeft` and produce the correct result.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 08:38:08 +02:00
Hartmut 944d36bdb2 fix(timeline): pre-load 90-day past buffer + scroll to today on mount
CI / Architecture Guardrails (pull_request) Successful in 5m6s
CI / Typecheck (pull_request) Successful in 7m31s
CI / Assistant Split Regression (pull_request) Successful in 6m45s
CI / Lint (pull_request) Successful in 6m19s
CI / Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Fresh-Linux Docker Deploy (pull_request) Has been cancelled
CI / Release Images (pull_request) Has been cancelled
CI / Build (pull_request) Has been cancelled
viewStart=today left no canvas to the left of scrollLeft=0, making
left-scroll physically impossible. Now viewStart defaults to today-90
so the canvas always has 90 days to scroll into, and a mount-time
useLayoutEffect positions the viewport with today at the left edge.

The Today button restores this view: scrolls in-range, or resets
viewStart and schedules a post-layout scroll if today has scrolled
out of the visible window.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 08:15:37 +02:00
Hartmut ad8843d956 ci: retrigger — QNAP network flake cloning actions/checkout (run #165)
CI / Architecture Guardrails (pull_request) Successful in 5m39s
CI / Lint (pull_request) Successful in 4m45s
CI / Assistant Split Regression (pull_request) Successful in 7m5s
CI / Typecheck (pull_request) Successful in 8m36s
CI / Unit Tests (pull_request) Successful in 11m52s
CI / Build (pull_request) Successful in 11m39s
CI / Fresh-Linux Docker Deploy (pull_request) Successful in 15m4s
CI / E2E Tests (pull_request) Successful in 17m15s
CI / Release Images (pull_request) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 08:07:11 +02:00
Hartmut 6ec512e302 test(cron): raise timeout for next/server cold-import on act runner
CI / Architecture Guardrails (pull_request) Successful in 4m22s
CI / Typecheck (pull_request) Successful in 6m48s
CI / Assistant Split Regression (pull_request) Successful in 7m49s
CI / Lint (pull_request) Successful in 7m59s
CI / E2E Tests (pull_request) Has been cancelled
CI / Fresh-Linux Docker Deploy (pull_request) Has been cancelled
CI / Release Images (pull_request) Has been cancelled
CI / Unit Tests (pull_request) Has been cancelled
CI / Build (pull_request) Has been cancelled
The test takes >5s on the QNAP act runner because dynamic import of
next/server has to transpile the module cold on first call. Raise the
per-test timeout to 15s to give it headroom without changing the test logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 08:06:56 +02:00
Hartmut 4a841d5acb feat(timeline): start at today and allow infinite scroll into the past
CI / Architecture Guardrails (pull_request) Successful in 17m31s
CI / Assistant Split Regression (pull_request) Successful in 9m42s
CI / Typecheck (pull_request) Successful in 20m48s
CI / Lint (pull_request) Successful in 8m6s
CI / Unit Tests (pull_request) Failing after 7m32s
CI / Build (pull_request) Successful in 9m12s
CI / E2E Tests (pull_request) Successful in 6m12s
CI / Fresh-Linux Docker Deploy (pull_request) Successful in 6m58s
CI / Release Images (pull_request) Has been skipped
Previously viewStart defaulted to today-30 and the scroll container had
no left-edge expansion logic, so users hit a hard wall when scrolling
left. This change:

- Sets viewStart default to today so the viewport opens with today at
  the left edge (URL ?startDate= override still respected).
- Adds left-edge auto-expansion in handleContainerScroll: when the user
  scrolls within 40 cells of the left boundary, 120 days are prepended
  and a useLayoutEffect applies the matching scrollLeft compensation in
  the same paint frame to prevent a visual jump.
- Floors backward navigation at 5 years (minDate) to prevent unbounded
  viewDays growth.
- Updates handleNavigateToday to match: resets to today rather than
  today-30.

Both resource view and project view use the same TimelineContext /
TimelineView, so both are fixed by this change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 07:16:34 +02:00
Hartmut 749a39097c ci: retrigger — runner flake on unit-tests step (run #163)
CI / Architecture Guardrails (pull_request) Successful in 4m9s
CI / Typecheck (pull_request) Successful in 5m41s
CI / Lint (pull_request) Successful in 5m47s
CI / Assistant Split Regression (pull_request) Successful in 6m8s
CI / Build (pull_request) Failing after 15m55s
CI / E2E Tests (pull_request) Has been skipped
CI / Fresh-Linux Docker Deploy (pull_request) Has been skipped
CI / Unit Tests (pull_request) Successful in 30m26s
CI / Release Images (pull_request) Failing after 10m48s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 07:05:16 +02:00
Hartmut a58b99a33a rename(cleanup): drop last capakraken strings from UI, scripts, schema, tests
CI / Architecture Guardrails (pull_request) Successful in 4m26s
CI / Assistant Split Regression (pull_request) Successful in 5m38s
CI / Lint (pull_request) Successful in 6m6s
CI / Typecheck (pull_request) Successful in 6m34s
CI / Build (pull_request) Successful in 4m13s
CI / Unit Tests (pull_request) Failing after 10m20s
CI / E2E Tests (pull_request) Successful in 5m28s
CI / Fresh-Linux Docker Deploy (pull_request) Successful in 6m14s
CI / Release Images (pull_request) Has been skipped
AppShell.tsx top-left brand → Nexus (desktop sidebar + mobile top-bar),
shell echo strings, prisma schema header, test fixture token, playwright
runtime DB URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 20:57:43 +02:00
Hartmut c5b58a5bdc fix(docs): update nginx-hardening.conf to nexus domain and log paths
Server block comment, access_log and error_log paths all updated from
capakraken.hartmut-noerenberg.com to nexus.hartmut-noerenberg.com.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 20:41:58 +02:00
Hartmut 52ddbe7377 fix(migrate): use relname not table_name in pg_stat_user_tables query
CI / Architecture Guardrails (push) Successful in 2m54s
CI / Typecheck (push) Successful in 2m56s
CI / Lint (push) Successful in 3m2s
CI / Assistant Split Regression (push) Successful in 4m49s
CI / Unit Tests (push) Successful in 6m26s
CI / Build (push) Successful in 6m36s
CI / E2E Tests (push) Successful in 5m26s
CI / Fresh-Linux Docker Deploy (push) Successful in 6m2s
CI / Release Images (push) Successful in 7m53s
pg_stat_user_tables uses relname, not table_name. The wrong column caused
the row-count verification step to abort with ERROR: column does not exist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 20:11:57 +02:00
Hartmut 19aeb2ba04 rename(phase 3): compose/DB/infra + stray code refs capakraken → nexus (#62)
CI / Lint (push) Successful in 3m4s
CI / Typecheck (push) Successful in 3m6s
CI / Architecture Guardrails (push) Successful in 3m8s
CI / Assistant Split Regression (push) Successful in 3m48s
CI / Build (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
rename(phase 3): compose/DB/infra + stray code refs capakraken → nexus (#62)

Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
2026-05-21 20:07:18 +02:00
14 changed files with 206 additions and 61 deletions
+61 -1
View File
@@ -43,6 +43,14 @@ jobs:
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache pnpm store
uses: actions/cache@v4
continue-on-error: true
with:
path: ~/.local/share/pnpm/store
key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -77,6 +85,14 @@ jobs:
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache pnpm store
uses: actions/cache@v4
continue-on-error: true
with:
path: ~/.local/share/pnpm/store
key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -107,6 +123,14 @@ jobs:
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache pnpm store
uses: actions/cache@v4
continue-on-error: true
with:
path: ~/.local/share/pnpm/store
key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -132,6 +156,14 @@ jobs:
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache pnpm store
uses: actions/cache@v4
continue-on-error: true
with:
path: ~/.local/share/pnpm/store
key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -196,6 +228,14 @@ jobs:
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache pnpm store
uses: actions/cache@v4
continue-on-error: true
with:
path: ~/.local/share/pnpm/store
key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -251,6 +291,14 @@ jobs:
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache pnpm store
uses: actions/cache@v4
continue-on-error: true
with:
path: ~/.local/share/pnpm/store
key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -347,6 +395,14 @@ jobs:
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache pnpm store
uses: actions/cache@v4
continue-on-error: true
with:
path: ~/.local/share/pnpm/store
key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -493,6 +549,10 @@ jobs:
sleep 3
done
- name: Pre-pull Docker base image
run: docker pull node:20-bookworm-slim
continue-on-error: true
- name: Build and start app (full profile)
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile full up -d --build app
@@ -509,7 +569,7 @@ jobs:
# the act_runner job can reach). No DNS, no guessing.
run: |
set -e
for i in $(seq 1 36); do
for i in $(seq 1 60); do
CID=$(docker compose -f docker-compose.yml -f docker-compose.ci.yml ps -q app || true)
if [ -n "$CID" ]; then
APP_IP=$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{if eq $k "gitea_gitea"}}{{$v.IPAddress}}{{end}}{{end}}' "$CID")
+1 -1
View File
@@ -24,6 +24,6 @@ export default defineConfig({
command: "node ./e2e/test-server.mjs",
url: e2eBaseUrl,
reuseExistingServer: false,
timeout: 180000,
timeout: 300000,
},
});
@@ -82,7 +82,7 @@ describe("GET /api/cron/auth-anomaly-check — cron secret enforcement", () => {
const { GET } = await importRoute();
const res = await GET(makeRequest());
expect(res.status).toBe(401);
});
}, 15_000); // next/server cold-import can take >5s on the act runner
it("proceeds when verifyCronSecret returns null (allowed)", async () => {
verifyCronSecretMock.mockReturnValue(null);
+2 -2
View File
@@ -450,7 +450,7 @@ function SidebarContent({
{!sidebarCollapsed && (
<div className="overflow-hidden">
<h1 className="font-display text-xl font-semibold text-gray-900 dark:text-gray-50">
Capa<span className="text-brand-600">Kraken</span>
Nex<span className="text-brand-600">us</span>
</h1>
<p className="text-xs uppercase tracking-[0.18em] text-gray-500 dark:text-gray-400">
Resource & Capacity Planning
@@ -984,7 +984,7 @@ export function AppShell({
<HamburgerIcon />
</button>
<span className="ml-3 font-display text-sm font-semibold text-gray-900 dark:text-gray-50">
Capa<span className="text-brand-600">Kraken</span>
Nex<span className="text-brand-600">us</span>
</span>
</div>
<PageTransition>{children}</PageTransition>
@@ -284,7 +284,7 @@ export function TimelineProvider({
const d = new Date(sp);
if (!isNaN(d.getTime())) return d;
}
return addDays(today, -30);
return addDays(today, -90);
});
const [viewDays, setViewDays] = useState(() => {
const sp = searchParams.get("days");
@@ -310,7 +310,7 @@ export function TimelineProvider({
const d = new Date(spStart);
if (!isNaN(d.getTime())) return d;
}
return addDays(today, -30);
return addDays(today, -90);
});
const spDays = searchParams.get("days");
@@ -2,7 +2,7 @@
import { clsx } from "clsx";
import { useSession } from "next-auth/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useAllocationHistory } from "~/hooks/useAllocationHistory.js";
import { useProjectDragContext } from "~/hooks/useProjectDragContext.js";
import { useTimelineDrag } from "~/hooks/useTimelineDrag.js";
@@ -685,15 +685,70 @@ function TimelineViewContent({
const scrollRafRef = useRef<number | null>(null);
const [scrollLeft, setScrollLeft] = useState(0);
// Pixels to add to scrollLeft after a left-extension re-render (prevents jump).
const pendingLeftCompensationPx = useRef(0);
// Flag: scroll viewport to today after the next viewStart-driven re-layout.
const pendingScrollToTodayRef = useRef(false);
// Guard reset on every real unmount (including Strict Mode fake-unmount) so the
// scroll-to-today fires correctly on remount.
const hasScrolledToTodayOnLoad = useRef(false);
useLayoutEffect(() => {
return () => {
hasScrolledToTodayOnLoad.current = false;
};
}, []);
// Scroll to today the first time the canvas is in the DOM (isInitialLoading → false).
// totalCanvasWidth is non-zero before data loads, so it can't be used as the trigger.
useLayoutEffect(() => {
if (isInitialLoading) return;
if (hasScrolledToTodayOnLoad.current) return;
const el = scrollContainerRef.current;
if (!el) return;
el.scrollLeft = toLeft(today);
hasScrolledToTodayOnLoad.current = true;
}, [isInitialLoading, toLeft, today]);
// Apply scroll compensation synchronously after the canvas grows (left-extend or Today button).
useLayoutEffect(() => {
const el = scrollContainerRef.current;
if (!el) return;
const px = pendingLeftCompensationPx.current;
if (px !== 0) {
el.scrollLeft += px;
pendingLeftCompensationPx.current = 0;
} else if (pendingScrollToTodayRef.current) {
el.scrollLeft = toLeft(today);
pendingScrollToTodayRef.current = false;
}
}, [viewStart, toLeft, today]);
// 5-year floor — no practical data exists further back; prevents runaway growth.
const minDate = useMemo(() => addDays(today, -(365 * 5)), [today]);
// ─── Navigation callbacks for TimelineToolbar ────────────────────────────
const handleNavigateBack = useCallback(
() => setViewStart((v) => addDays(v, -28)),
[setViewStart],
);
const handleNavigateToday = useCallback(
() => setViewStart(addDays(today, -30)),
[setViewStart, today],
() =>
setViewStart((v) => {
const candidate = addDays(v, -28);
return candidate < minDate ? minDate : candidate;
}),
[setViewStart, minDate],
);
const handleNavigateToday = useCallback(() => {
const el = scrollContainerRef.current;
const todayMs = new Date(today).setHours(0, 0, 0, 0);
const vsMs = new Date(viewStart).setHours(0, 0, 0, 0);
const veMs = new Date(addDays(viewStart, viewDays)).setHours(0, 0, 0, 0);
if (todayMs >= vsMs && todayMs < veMs && el) {
// Today is in range — just scroll without touching state.
el.scrollLeft = toLeft(today);
} else {
// Today is out of range — reset the window and schedule a scroll.
pendingScrollToTodayRef.current = true;
setViewStart(addDays(today, -90));
}
}, [today, viewStart, viewDays, toLeft, setViewStart]);
const handleNavigateForward = useCallback(
() => setViewStart((v) => addDays(v, 28)),
[setViewStart],
@@ -709,10 +764,31 @@ function TimelineViewContent({
const handleContainerScroll = useCallback(() => {
const el = scrollContainerRef.current;
if (!el) return;
// Right-edge: extend future range
const distanceFromRight = el.scrollWidth - el.scrollLeft - el.clientWidth;
if (distanceFromRight < CELL_WIDTH * 40) {
setViewDays((d) => d + 120);
}
// Left-edge: prepend past range and compensate scroll position so viewport doesn't jump
if (el.scrollLeft < CELL_WIDTH * 40 && viewStart > minDate) {
const daysToPrepend = 120;
// Count the exact visible days (respecting showWeekends) being prepended
let prependedVisible = 0;
for (let i = 1; i <= daysToPrepend; i++) {
const d = addDays(viewStart, -i);
const dow = d.getDay();
if (filters.showWeekends || (dow !== 0 && dow !== 6)) prependedVisible++;
}
pendingLeftCompensationPx.current = prependedVisible * CELL_WIDTH;
setViewStart((v) => {
const candidate = addDays(v, -daysToPrepend);
return candidate < minDate ? minDate : candidate;
});
setViewDays((d) => d + daysToPrepend);
}
scrollLeftRef.current = el.scrollLeft;
if (scrollRafRef.current === null) {
scrollRafRef.current = requestAnimationFrame(() => {
@@ -720,7 +796,7 @@ function TimelineViewContent({
setScrollLeft(scrollLeftRef.current);
});
}
}, [CELL_WIDTH, setViewDays]);
}, [CELL_WIDTH, setViewDays, viewStart, minDate, setViewStart, filters.showWeekends]);
// ─── Canvas mousemove — only forwards event when drag overlay is active ───
const handleMouseMove = useCallback(
+4 -4
View File
@@ -1,5 +1,5 @@
# CapaKraken nginx Security Hardening
# Apply to the server block for capakraken.hartmut-noerenberg.com
# Nexus nginx Security Hardening
# Apply to the server block for nexus.hartmut-noerenberg.com
#
# References:
# - EAPPS 3.3.1.3.04 (Server Header entfernen)
@@ -113,5 +113,5 @@ log_format security '$remote_addr - $remote_user [$time_local] '
'"$http_referer" "$http_user_agent" '
'$request_time $upstream_response_time';
access_log /var/log/nginx/capakraken_access.log security;
error_log /var/log/nginx/capakraken_error.log warn;
access_log /var/log/nginx/nexus_access.log security;
error_log /var/log/nginx/nexus_error.log warn;
@@ -7,12 +7,14 @@ vi.mock("../lib/audit.js", () => ({
vi.mock("../router/assistant-approvals.js", () => ({
clearPendingAssistantApproval: vi.fn().mockResolvedValue(undefined),
consumePendingAssistantApproval: vi.fn(),
toApprovalPayload: vi.fn((approval: { id: string; toolName: string; summary: string }, status: string) => ({
id: approval.id,
toolName: approval.toolName,
summary: approval.summary,
status,
})),
toApprovalPayload: vi.fn(
(approval: { id: string; toolName: string; summary: string }, status: string) => ({
id: approval.id,
toolName: approval.toolName,
summary: approval.summary,
status,
}),
),
}));
vi.mock("../router/assistant-confirmation.js", () => ({
@@ -39,16 +41,10 @@ import {
clearPendingAssistantApproval,
consumePendingAssistantApproval,
} from "../router/assistant-approvals.js";
import {
canExecuteMutationTool,
isCancellationReply,
} from "../router/assistant-confirmation.js";
import { canExecuteMutationTool, isCancellationReply } from "../router/assistant-confirmation.js";
import { buildAssistantInsight } from "../router/assistant-insights.js";
import { handlePendingAssistantApproval } from "../router/assistant-chat-response.js";
import {
readToolError,
readToolSuccessMessage,
} from "../router/assistant-tool-results.js";
import { readToolError, readToolSuccessMessage } from "../router/assistant-tool-results.js";
import { executeTool } from "../router/assistant-tools.js";
function createPendingApproval() {
@@ -57,14 +53,16 @@ function createPendingApproval() {
userId: "user_1",
conversationId: "conv_1",
toolName: "create_project",
toolArguments: "{\"name\":\"Apollo\"}",
toolArguments: '{"name":"Apollo"}',
summary: "create project (name=Apollo)",
createdAt: Date.now(),
expiresAt: Date.now() + 60_000,
};
}
function createHandleInput(overrides: Partial<Parameters<typeof handlePendingAssistantApproval>[0]> = {}) {
function createHandleInput(
overrides: Partial<Parameters<typeof handlePendingAssistantApproval>[0]> = {},
) {
return {
db: {} as never,
dbUserId: "user_1",
@@ -81,7 +79,10 @@ function createHandleInput(overrides: Partial<Parameters<typeof handlePendingAss
pendingApproval: createPendingApproval(),
lastUserMessage: { role: "user" as const, content: "ja" },
messages: [
{ role: "assistant" as const, content: "__CAPAKRAKEN_CONFIRM__ create project (name=Apollo). Bitte bestätigen." },
{
role: "assistant" as const,
content: "__NEXUS_CONFIRM__ create project (name=Apollo). Bitte bestätigen.",
},
{ role: "user" as const, content: "ja" },
],
collectedActions: [],
@@ -103,9 +104,11 @@ describe("assistant pending approval handling", () => {
it("cancels pending approvals when the user aborts", async () => {
vi.mocked(isCancellationReply).mockReturnValue(true);
const result = await handlePendingAssistantApproval(createHandleInput({
lastUserMessage: { role: "user", content: "nein, abbrechen" },
}));
const result = await handlePendingAssistantApproval(
createHandleInput({
lastUserMessage: { role: "user", content: "nein, abbrechen" },
}),
);
expect(result).toMatchObject({
response: {
@@ -127,7 +130,7 @@ describe("assistant pending approval handling", () => {
summary: "create project (name=Apollo, status=DRAFT)",
} as never);
vi.mocked(executeTool).mockResolvedValue({
content: "{\"message\":\"Projekt Apollo angelegt\"}",
content: '{"message":"Projekt Apollo angelegt"}',
data: { message: "Projekt Apollo angelegt" },
action: { type: "refresh" },
} as never);
@@ -148,29 +151,35 @@ describe("assistant pending approval handling", () => {
status: "approved",
},
actions: [{ type: "refresh" }],
insights: [{
kind: "holiday_region",
title: "Berlin",
}],
insights: [
{
kind: "holiday_region",
title: "Berlin",
},
],
},
});
expect(executeTool).toHaveBeenCalledWith(
"create_project",
"{\"name\":\"Apollo\"}",
'{"name":"Apollo"}',
expect.objectContaining({ userId: "user_1" }),
);
expect(createAuditEntry).toHaveBeenCalledWith(expect.objectContaining({
entityName: "create_project",
summary: "AI executed previously approved tool: create_project",
}));
expect(createAuditEntry).toHaveBeenCalledWith(
expect.objectContaining({
entityName: "create_project",
summary: "AI executed previously approved tool: create_project",
}),
);
});
it("does nothing when the user reply is not a valid confirmation", async () => {
vi.mocked(canExecuteMutationTool).mockReturnValue(false);
const result = await handlePendingAssistantApproval(createHandleInput({
lastUserMessage: { role: "user", content: "vielleicht" },
}));
const result = await handlePendingAssistantApproval(
createHandleInput({
lastUserMessage: { role: "user", content: "vielleicht" },
}),
);
expect(result).toBeNull();
expect(consumePendingAssistantApproval).not.toHaveBeenCalled();
+1 -1
View File
@@ -1,4 +1,4 @@
// CapaKraken — Prisma Schema
// Nexus — Prisma Schema
// All monetary values stored as integer cents to avoid float precision issues.
generator client {
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# restart.sh — Rebuild the CapaKraken app container from scratch.
# restart.sh — Rebuild the Nexus app container from scratch.
#
# When to use:
# - After changing pnpm-lock.yaml (new/removed dependencies)
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
cd "$(dirname "$0")/.."
echo "Restarting CapaKraken..."
echo "Restarting Nexus..."
echo ""
# Stop
+2 -2
View File
@@ -5,7 +5,7 @@ cd "$(dirname "$0")/.."
APP_PORT="${APP_PORT:-3100}"
APP_CONTAINER="${APP_CONTAINER:-$(docker compose --profile full ps -q app 2>/dev/null | head -1)}"
echo "Starting CapaKraken..."
echo "Starting Nexus..."
# 1. Start Docker services
echo " Starting PostgreSQL + Redis..."
@@ -34,7 +34,7 @@ echo " Waiting for server (up to 90s)..."
for i in {1..90}; do
if curl -sf "http://localhost:${APP_PORT}/api/health" > /dev/null 2>&1; then
echo ""
echo "CapaKraken is running!"
echo "Nexus is running!"
curl -s "http://localhost:${APP_PORT}/api/ready" | python3 -m json.tool 2>/dev/null || curl -s "http://localhost:${APP_PORT}/api/ready"
echo ""
echo " URL: http://localhost:${APP_PORT}"
+2 -2
View File
@@ -2,7 +2,7 @@
set -euo pipefail
cd "$(dirname "$0")/.."
echo "Stopping CapaKraken..."
echo "Stopping Nexus..."
# 1. Stop any legacy local dev server
if [ -f /tmp/nexus-dev.pid ]; then
@@ -28,4 +28,4 @@ echo " Stopping app, PostgreSQL and Redis..."
docker compose --profile full stop app postgres redis 2>/dev/null || true
echo ""
echo "CapaKraken stopped."
echo "Nexus stopped."
+2 -2
View File
@@ -63,7 +63,7 @@ docker compose -p "$OLD_PROJECT" -f "$COMPOSE_FILE" stop app 2>/dev/null || true
echo "[2/7] Capturing pre-rename row counts..."
PRE_COUNTS=$(docker compose -p "$OLD_PROJECT" -f "$COMPOSE_FILE" exec -T postgres \
psql -U capakraken -d capakraken -t -c \
"SELECT table_name, n_live_tup FROM pg_stat_user_tables ORDER BY table_name;")
"SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY relname;")
echo "$PRE_COUNTS" | head -20
echo "..."
@@ -149,7 +149,7 @@ sleep 15
echo "=== Verification ==="
POST_COUNTS=$(docker compose -p "$NEW_PROJECT" -f "$COMPOSE_FILE" exec -T postgres \
psql -U nexus -d nexus -t -c \
"SELECT table_name, n_live_tup FROM pg_stat_user_tables ORDER BY table_name;")
"SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY relname;")
echo "Post-rename row counts (sample):"
echo "$POST_COUNTS" | head -20