Compare commits
18 Commits
bed99ff3fe
...
feat/issue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de1a36a0d6 | ||
|
|
d56a9eb401 | ||
|
|
5dc1bf6bfb | ||
|
|
935c8eadd2 | ||
|
|
453e709a7c | ||
|
|
74fdc0cef7 | ||
|
|
bde1237358 | ||
|
|
788a804810 | ||
|
|
62b96f718f | ||
|
|
6ed5151e50 | ||
|
|
3a7c86fc87 | ||
|
|
1226bd0a07 | ||
|
|
00a00b2c87 | ||
|
|
cc841a7a4c | ||
|
|
513cdb7a4d | ||
|
|
595007213c | ||
|
|
45001f042a | ||
|
|
d11378c254 |
@@ -161,3 +161,147 @@ jobs:
|
||||
# without first re-evaluating ADR-011.
|
||||
if: always()
|
||||
run: rm -f .env.staging
|
||||
|
||||
npm-audit:
|
||||
# Independent parallel job — a deploy failure cannot mask the audit signal
|
||||
# and a clean audit cannot hide a broken deploy. Intentionally no `needs:`.
|
||||
#
|
||||
# Scans dev deps too (no --omit=dev), which is deliberately broader than the
|
||||
# PR gate (ci.yml §Security audit) that uses --omit=dev. A nightly broader
|
||||
# result is NOT a PR gate failure — it catches dev-tooling advisories (esbuild,
|
||||
# Vite, etc.) early. See docs/infrastructure/ci-gitea.md §Nightly audit vs PR gate.
|
||||
#
|
||||
# Required Gitea secrets:
|
||||
# NIGHTLY_AUDIT_TOKEN — PAT with issues scope only. An issues-only token
|
||||
# means a leak via logs/process-args cannot push
|
||||
# branches, open PRs, or read repo contents (ADR-041).
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Assert jq is available
|
||||
run: which jq || sudo apt-get install -y jq
|
||||
|
||||
- name: Run npm audit and file tracking issue on findings
|
||||
# Never run under set -x — NIGHTLY_AUDIT_TOKEN in env would leak to logs.
|
||||
env:
|
||||
NIGHTLY_AUDIT_TOKEN: ${{ secrets.NIGHTLY_AUDIT_TOKEN }}
|
||||
run: |
|
||||
MARKER="Nightly npm audit: high-severity advisory"
|
||||
GITEA_URL="${{ github.server_url }}"
|
||||
REPO="${{ github.repository }}"
|
||||
RUN_URL="${GITEA_URL}/${REPO}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
# --- Self-test (mirrors ci.yml §Assert pattern) ---
|
||||
# Tests the exact jq test() call used in the dedupe step, before any
|
||||
# API call, so a broken matcher fails loudly early rather than silently
|
||||
# opening duplicate issues. Proves the regex only — create-vs-update
|
||||
# decision is exercised by the workflow_dispatch AC.
|
||||
echo "{\"title\": \"${MARKER}\"}" \
|
||||
| jq -e --arg m "$MARKER" '.title | test($m; "i")' > /dev/null \
|
||||
|| { echo "FAIL: self-test — jq test() missed tracking issue title"; exit 1; }
|
||||
echo '{"title": "fix(deps): update dependency esbuild (CVE-2025-12345)"}' \
|
||||
| jq -e --arg m "$MARKER" '.title | test($m; "i") | not' > /dev/null \
|
||||
|| { echo "FAIL: self-test — jq test() incorrectly matched unrelated title"; exit 1; }
|
||||
echo "Self-test passed."
|
||||
|
||||
# --- Run audit ---
|
||||
# No npm ci — audit reads only the lockfile (no network, no install).
|
||||
set +e
|
||||
(cd frontend && npm audit --audit-level=high --json > /tmp/audit.json)
|
||||
AUDIT_EXIT=$?
|
||||
set -e
|
||||
|
||||
if [ "$AUDIT_EXIT" -ne 0 ]; then
|
||||
# --- Build issue body with jq (never string-concat advisory text) ---
|
||||
# Advisory overview/title text is registry-controlled; string-concat
|
||||
# would be an injection/escaping vector into the API body. Truncate
|
||||
# raw excerpt to 500 chars so a pathological overview can't produce
|
||||
# a multi-MB PATCH body.
|
||||
ISSUE_BODY=$(jq -r \
|
||||
--arg run_url "$RUN_URL" \
|
||||
'
|
||||
(.vulnerabilities // {}) as $vulns |
|
||||
($vulns | to_entries |
|
||||
map(select(.value.severity == "high" or .value.severity == "critical")) |
|
||||
map("- **" + .key + "** (" + .value.severity + ")") |
|
||||
if length > 0 then join("\n") else "_See raw output for details._" end) as $pkg_list |
|
||||
"## npm audit: high/critical advisories\n\n" + $pkg_list +
|
||||
"\n\n**Run:** " + $run_url +
|
||||
"\n\n<details><summary>Raw audit excerpt (first 500 chars)</summary>\n\n```\n" +
|
||||
(tostring | .[0:500]) +
|
||||
"\n```\n\n</details>"
|
||||
' /tmp/audit.json)
|
||||
|
||||
# --- Dedupe: fetch open security issues, match by title marker ---
|
||||
# Renovate vuln PRs also carry the "security" label, so >1 open
|
||||
# "security" issue WILL occur. Title-match (not just label) ensures
|
||||
# we deduplicate only our own tracking issue.
|
||||
OPEN_ISSUES=$(curl -sf \
|
||||
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/issues?state=open&type=issues&labels=security&limit=50")
|
||||
|
||||
MATCHED=$(echo "$OPEN_ISSUES" | jq \
|
||||
--arg m "$MARKER" \
|
||||
'[.[] | select(.title | test($m; "i"))] | sort_by(.created_at)')
|
||||
MATCH_COUNT=$(echo "$MATCHED" | jq 'length')
|
||||
|
||||
if [ "$MATCH_COUNT" -gt 0 ]; then
|
||||
# Patch the oldest matched issue (append run URL to body).
|
||||
ISSUE_NUMBER=$(echo "$MATCHED" | jq -r '.[0].number')
|
||||
EXISTING_BODY=$(echo "$MATCHED" | jq -r '.[0].body')
|
||||
NEW_BODY=$(jq -n \
|
||||
--arg existing "$EXISTING_BODY" \
|
||||
--arg run_url "$RUN_URL" \
|
||||
'$existing + "\n\n---\n\nUpdated by run: " + $run_url')
|
||||
PAYLOAD=$(jq -n --arg body "$NEW_BODY" '{"body": $body}')
|
||||
curl -sf -X PATCH \
|
||||
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/issues/${ISSUE_NUMBER}" > /dev/null
|
||||
echo "Updated tracking issue #${ISSUE_NUMBER}"
|
||||
else
|
||||
# Closed prior issue that recurs → new issue (not reopened).
|
||||
# A re-opened issue would obscure when the advisory was re-discovered.
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg title "$MARKER" \
|
||||
--arg body "$ISSUE_BODY" \
|
||||
'{"title": $title, "body": $body}')
|
||||
CREATED=$(curl -sf -X POST \
|
||||
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/issues")
|
||||
NEW_NUMBER=$(echo "$CREATED" | jq -r '.number')
|
||||
echo "Opened new tracking issue #${NEW_NUMBER}"
|
||||
|
||||
# Labels are ignored on issue create in Gitea — add in a follow-up call.
|
||||
LABEL_IDS=$(curl -sf \
|
||||
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/labels?limit=50" \
|
||||
| jq '[.[] | select(.name == "security" or .name == "devops" or .name == "P1-high") | .id]')
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"labels\": $LABEL_IDS}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/issues/${NEW_NUMBER}/labels" > /dev/null
|
||||
fi
|
||||
|
||||
exit "$AUDIT_EXIT"
|
||||
|
||||
else
|
||||
# --- Heartbeat: proves the job ran and found nothing ---
|
||||
# "No issue created" is only meaningful evidence when paired with a
|
||||
# visible positive signal. Without this, a never-ran job is
|
||||
# indistinguishable from a clean run.
|
||||
#
|
||||
# $GITHUB_STEP_SUMMARY availability is unproven on this runner
|
||||
# (act_runner populates it, but this is the first run to verify it).
|
||||
# Guard before use so an unset variable does not fail the clean-path.
|
||||
MSG="✅ npm audit clean $(date -u)"
|
||||
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
||||
echo "$MSG" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
echo "$MSG"
|
||||
fi
|
||||
|
||||
44
.gitea/workflows/renovate.yml
Normal file
44
.gitea/workflows/renovate.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Renovate
|
||||
|
||||
# Runs Renovate daily to surface newly-published advisories via OSV.dev
|
||||
# (osvVulnerabilityAlerts) and open routine update PRs on a weekly batch
|
||||
# schedule (see renovate.json §schedule). Security/vulnerability PRs are
|
||||
# raised immediately regardless of the weekly schedule window.
|
||||
#
|
||||
# Required Gitea secrets (see docs/adr/041-renovate-runner-setup.md):
|
||||
# RENOVATE_TOKEN — PAT with scopes: contents + pull_request + issues
|
||||
# Belongs to a dedicated bot account. Branch protection
|
||||
# on main must forbid this bot pushing directly.
|
||||
#
|
||||
# Platform config is injected via env vars below; the renovate.json in the
|
||||
# repo root carries only dependency rules (no platform/endpoint/repos).
|
||||
#
|
||||
# Digest pin: renovatebot/github-action@8217b3fc286df088d7c27f3255fe8414463bc0fd
|
||||
# corresponds to release v46.1.15. Update by bumping both the digest and the
|
||||
# renovate-version when Renovate publishes a new release. Renovate itself
|
||||
# will open a PR to bump this digest once it runs.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 3 * * *" # daily at 03:00 UTC — cuts OSV-alert latency to ≤1 day
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run Renovate
|
||||
# Pinned by digest — this action holds contents+pull_request+issues
|
||||
# scopes; an unpinned tag is a supply-chain risk (see ADR-041).
|
||||
uses: renovatebot/github-action@8217b3fc286df088d7c27f3255fe8414463bc0fd # v46.1.15
|
||||
with:
|
||||
configurationFile: renovate.json
|
||||
token: ${{ secrets.RENOVATE_TOKEN }}
|
||||
renovate-version: "46.1.15"
|
||||
env:
|
||||
RENOVATE_PLATFORM: gitea
|
||||
RENOVATE_ENDPOINT: https://git.raddatz.cloud
|
||||
RENOVATE_REPOSITORIES: '["marcel/familienarchiv"]'
|
||||
LOG_LEVEL: info
|
||||
@@ -95,6 +95,7 @@ backend/src/main/java/org/raddatz/familienarchiv/
|
||||
│ └── relationship/ PersonRelationship sub-domain
|
||||
├── security/ SecurityConfig, Permission, @RequirePermission, PermissionAspect
|
||||
├── tag/ Tag domain
|
||||
├── timeline/ Timeline (Zeitstrahl) domain — TimelineEvent, EventType, TimelineEventRepository
|
||||
└── user/ User domain — AppUser, UserGroup, UserService
|
||||
```
|
||||
|
||||
@@ -115,6 +116,7 @@ backend/src/main/java/org/raddatz/familienarchiv/
|
||||
| `UserGroup` | `user_groups` | Has a `Set<String> permissions` |
|
||||
| `Geschichte` | `geschichten` | `GeschichteType` (`STORY`/`JOURNEY`); ManyToMany `persons` (Person); OneToMany `items` (JourneyItem) |
|
||||
| `JourneyItem` | `journey_items` | ManyToOne `geschichte` (Geschichte, ON DELETE CASCADE); ManyToOne `document` (Document, ON DELETE SET NULL); `position`, optional `note` |
|
||||
| `TimelineEvent` | `timeline_events` | `EventType` (`PERSONAL`/`HISTORICAL`); ManyToMany `persons` (Person) + `documents` (Document), both join FKs ON DELETE CASCADE; `DatePrecision` date block; `@Version` + NOT NULL `createdBy`/`updatedBy` audit trail |
|
||||
|
||||
**`DocumentStatus` lifecycle:** `PLACEHOLDER → UPLOADED → TRANSCRIBED → REVIEWED → ARCHIVED`
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ src/main/java/org/raddatz/familienarchiv/
|
||||
│ └── relationship/ # PersonRelationship sub-domain
|
||||
├── security/ # SecurityConfig, Permission, @RequirePermission, PermissionAspect
|
||||
├── tag/ # Tag domain — Tag, TagService, TagController
|
||||
├── timeline/ # Timeline (Zeitstrahl) domain — TimelineEvent, EventType, TimelineEventRepository
|
||||
└── user/ # User domain — AppUser, UserGroup, UserService
|
||||
```
|
||||
|
||||
@@ -67,6 +68,7 @@ For per-domain ownership and public surface, see each domain's `README.md`.
|
||||
| `Comment` | `document_comments` | Threaded comments with mentions |
|
||||
| `Notification` | `notifications` | User notification feed |
|
||||
| `OcrJob` / `OcrJobDocument` | `ocr_jobs`, `ocr_job_documents` | Batch OCR job tracking |
|
||||
| `TimelineEvent` | `timeline_events` | Curated Zeitstrahl event; ManyToMany persons + documents (join FKs ON DELETE CASCADE); `@Version` + NOT NULL createdBy/updatedBy |
|
||||
|
||||
**`DocumentStatus` lifecycle:** `PLACEHOLDER → UPLOADED → TRANSCRIBED → REVIEWED → ARCHIVED`
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.raddatz.familienarchiv.timeline;
|
||||
|
||||
/**
|
||||
* Kind of a curated {@link TimelineEvent}.
|
||||
*
|
||||
* <p>The string value names are a <strong>stable frontend styling contract</strong>: the
|
||||
* Svelte timeline components hard-code {@code "PERSONAL"} (family accent) and
|
||||
* {@code "HISTORICAL"} (muted world accent) as Tailwind class-map keys. There is no
|
||||
* mapping layer — renaming either value requires a coordinated frontend change. See
|
||||
* ADR-040.
|
||||
*/
|
||||
public enum EventType {
|
||||
/** A family/personal event (birth, wedding, move) — rendered with the family accent. */
|
||||
PERSONAL,
|
||||
/** A world/historical event providing context — rendered with the muted world accent. */
|
||||
HISTORICAL
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package org.raddatz.familienarchiv.timeline;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.BatchSize;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import org.raddatz.familienarchiv.document.DatePrecision;
|
||||
import org.raddatz.familienarchiv.document.Document;
|
||||
import org.raddatz.familienarchiv.person.Person;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A curated event on the family timeline (Zeitstrahl). Unlike a {@link Document}, which is
|
||||
* OCR-derived, a {@code TimelineEvent} is authored by curators — hence the optimistic-lock
|
||||
* {@link #version} and the {@link #createdBy}/{@link #updatedBy} audit trail that
|
||||
* {@code Document} lacks.
|
||||
*
|
||||
* <p>The date block ({@link #eventDate}, {@link #precision}, {@link #eventDateEnd}) mirrors
|
||||
* {@code Document}'s so events and letters share one rendering path. The mirror applies to
|
||||
* the date block only — the audit footprint deliberately diverges (see ADR-040).
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "timeline_events")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class TimelineEvent {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false)
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String title;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 16)
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private EventType type;
|
||||
|
||||
/** The most precise date known for the event. Always present — a curated event is never undated. */
|
||||
@Column(name = "event_date", nullable = false)
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDate eventDate;
|
||||
|
||||
/**
|
||||
* Precision of {@link #eventDate}. Reuses {@code document.DatePrecision} (one rendering
|
||||
* path; see ADR-025 / ADR-040). Every value except {@code UNKNOWN} is legal for a curated
|
||||
* event — including {@code SEASON} ("Sommer 1914") and {@code APPROX} ("ca. 1914"). The DB
|
||||
* CHECK forbids exactly {@code UNKNOWN}; do not narrow it further.
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "date_precision", nullable = false, length = 16)
|
||||
@Builder.Default
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private DatePrecision precision = DatePrecision.YEAR;
|
||||
|
||||
/** Range end — non-null <strong>iff</strong> {@link #precision} is {@code RANGE} (DB CHECK, both directions). */
|
||||
@Column(name = "event_date_end")
|
||||
private LocalDate eventDateEnd;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
/** People the event involves. */
|
||||
@ManyToMany(fetch = FetchType.LAZY)
|
||||
@JoinTable(name = "timeline_event_persons",
|
||||
joinColumns = @JoinColumn(name = "timeline_event_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "person_id"))
|
||||
@BatchSize(size = 50)
|
||||
@Builder.Default
|
||||
private Set<Person> persons = new HashSet<>();
|
||||
|
||||
/** Optional supporting letters linked to the event. */
|
||||
@ManyToMany(fetch = FetchType.LAZY)
|
||||
@JoinTable(name = "timeline_event_documents",
|
||||
joinColumns = @JoinColumn(name = "timeline_event_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "document_id"))
|
||||
@BatchSize(size = 50)
|
||||
@Builder.Default
|
||||
private Set<Document> documents = new HashSet<>();
|
||||
|
||||
/**
|
||||
* UUID of the {@code AppUser} who created the event. Bare UUID, no FK to {@code app_users}
|
||||
* (sidecar pattern — keeps {@code timeline} decoupled from {@code user}). Server-populated
|
||||
* from the session principal; never accepted from client input (authorship-forgery vector,
|
||||
* CWE-639 — see ADR-040).
|
||||
*/
|
||||
@Column(name = "created_by", nullable = false)
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private UUID createdBy;
|
||||
|
||||
@CreationTimestamp
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* UUID of the {@code AppUser} who last edited the event. Populated from the session
|
||||
* principal in {@code TimelineEventService}; <strong>must be set before every
|
||||
* {@code save()}</strong> — {@code @UpdateTimestamp} on {@link #updatedAt} does NOT set this
|
||||
* automatically, so without an explicit set the timestamp advances while the "who" goes
|
||||
* stale. Same forgery rationale as {@link #createdBy}.
|
||||
*/
|
||||
@Column(name = "updated_by", nullable = false)
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private UUID updatedBy;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* Optimistic-lock version for the multi-curator edit flow (#775). Object {@code Long}
|
||||
* (not primitive) so it is {@code null} before first persist; Hibernate sets {@code 0} on
|
||||
* insert. A concurrent-write conflict must be translated to {@code DomainException.conflict}
|
||||
* in the service layer (ADR-040) — otherwise it surfaces as HTTP 500 with Hibernate
|
||||
* internals (CWE-209).
|
||||
*/
|
||||
@Version
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long version;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.raddatz.familienarchiv.timeline;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface TimelineEventRepository extends JpaRepository<TimelineEvent, UUID> {
|
||||
// TODO(#777): findByPersonsContaining(Person) needed for the per-person filter
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
-- V77: timeline domain foundation (Zeitstrahl) — curated timeline events.
|
||||
-- Forward-only, additive DDL. No rollback script: rollback requires manual DROP TABLE
|
||||
-- (timeline_event_documents, timeline_event_persons, then timeline_events). See ADR-040.
|
||||
--
|
||||
-- The date block (event_date / date_precision / event_date_end) mirrors documents' so events
|
||||
-- and letters share one rendering path. Two divergences from documents are INTENTIONAL and
|
||||
-- enforced here in Postgres (ADR-040):
|
||||
-- 1. The RANGE rule is a strict biconditional (event_date_end non-null IFF RANGE), unlike
|
||||
-- documents' open-ended ranges — a curated event always has a known, closed end.
|
||||
-- 2. date_precision <> 'UNKNOWN' — only OCR-inferred letters are ever undated; a curated
|
||||
-- event always has at least a year. SEASON and APPROX stay legal (Sommer/ca. 1914).
|
||||
|
||||
CREATE TABLE timeline_events (
|
||||
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
title VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(16) NOT NULL,
|
||||
event_date DATE NOT NULL,
|
||||
date_precision VARCHAR(16) NOT NULL DEFAULT 'YEAR',
|
||||
event_date_end DATE,
|
||||
description TEXT,
|
||||
created_by UUID NOT NULL,
|
||||
created_at TIMESTAMP,
|
||||
updated_by UUID NOT NULL,
|
||||
updated_at TIMESTAMP,
|
||||
version BIGINT,
|
||||
CONSTRAINT pk_timeline_events PRIMARY KEY (id),
|
||||
-- event_date_end is non-null IFF precision is RANGE (both directions).
|
||||
CONSTRAINT chk_timeline_event_range
|
||||
CHECK ((date_precision = 'RANGE') = (event_date_end IS NOT NULL)),
|
||||
-- Curated events are never undated. Forbids exactly UNKNOWN — every other
|
||||
-- DatePrecision value (DAY, MONTH, SEASON, YEAR, RANGE, APPROX) stays legal.
|
||||
CONSTRAINT chk_timeline_event_precision
|
||||
CHECK (date_precision <> 'UNKNOWN')
|
||||
);
|
||||
|
||||
-- Join table: events ↔ persons involved.
|
||||
CREATE TABLE timeline_event_persons (
|
||||
timeline_event_id UUID NOT NULL,
|
||||
person_id UUID NOT NULL,
|
||||
CONSTRAINT pk_timeline_event_persons PRIMARY KEY (timeline_event_id, person_id),
|
||||
CONSTRAINT fk_tep_event
|
||||
FOREIGN KEY (timeline_event_id) REFERENCES timeline_events(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_tep_person
|
||||
FOREIGN KEY (person_id) REFERENCES persons(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Join table: events ↔ supporting letters.
|
||||
CREATE TABLE timeline_event_documents (
|
||||
timeline_event_id UUID NOT NULL,
|
||||
document_id UUID NOT NULL,
|
||||
CONSTRAINT pk_timeline_event_documents PRIMARY KEY (timeline_event_id, document_id),
|
||||
CONSTRAINT fk_ted_event
|
||||
FOREIGN KEY (timeline_event_id) REFERENCES timeline_events(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_ted_document
|
||||
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Indexes added up-front (avoid the V62 FK-index retrofit debt): the two query columns plus
|
||||
-- the inverse-side FK columns. timeline_event_id needs no extra index on either join table —
|
||||
-- it is the leading column of the composite PK, so the PK index already serves those lookups.
|
||||
CREATE INDEX idx_timeline_events_event_date ON timeline_events (event_date);
|
||||
CREATE INDEX idx_timeline_events_type ON timeline_events (type);
|
||||
CREATE INDEX idx_timeline_event_persons_person_id ON timeline_event_persons (person_id);
|
||||
CREATE INDEX idx_timeline_event_documents_document_id ON timeline_event_documents (document_id);
|
||||
@@ -122,7 +122,8 @@ class ArchitectureTest {
|
||||
.that().areAnnotatedWith(Entity.class)
|
||||
.should().resideInAnyPackage(
|
||||
"..document..", "..person..", "..tag..", "..user..",
|
||||
"..geschichte..", "..notification..", "..ocr..", "..audit.."
|
||||
"..geschichte..", "..notification..", "..ocr..", "..audit..",
|
||||
"..timeline.."
|
||||
);
|
||||
|
||||
// TODO Rule 5: Controllers expose endpoints under their domain prefix
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.raddatz.familienarchiv.timeline;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.raddatz.familienarchiv.PostgresContainerConfig;
|
||||
import org.raddatz.familienarchiv.document.Document;
|
||||
import org.raddatz.familienarchiv.document.DocumentRepository;
|
||||
import org.raddatz.familienarchiv.document.DocumentService;
|
||||
import org.raddatz.familienarchiv.document.DocumentStatus;
|
||||
import org.raddatz.familienarchiv.person.Person;
|
||||
import org.raddatz.familienarchiv.person.PersonRepository;
|
||||
import org.raddatz.familienarchiv.person.PersonService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Proves V77's FK {@code ON DELETE CASCADE} on the join tables: deleting a linked Person or
|
||||
* Document drops the join row and leaves the {@link TimelineEvent} intact (a person/document
|
||||
* delete must never 500 — V71-class regression guard). Needs the full Spring context for
|
||||
* {@link PersonService}/{@link DocumentService}, mirroring {@code PersonServiceIntegrationTest}.
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
@ActiveProfiles("test")
|
||||
@Import(PostgresContainerConfig.class)
|
||||
@Transactional
|
||||
class TimelineEventCascadeIntegrationTest {
|
||||
|
||||
@MockitoBean S3Client s3Client;
|
||||
@Autowired TimelineEventRepository events;
|
||||
@Autowired PersonRepository personRepository;
|
||||
@Autowired PersonService personService;
|
||||
@Autowired DocumentRepository documentRepository;
|
||||
@Autowired DocumentService documentService;
|
||||
@Autowired JdbcTemplate jdbc;
|
||||
@PersistenceContext EntityManager em;
|
||||
|
||||
private TimelineEvent.TimelineEventBuilder makeEvent() {
|
||||
return TimelineEvent.builder()
|
||||
.title("Hochzeit von Anna und Otto")
|
||||
.type(EventType.PERSONAL)
|
||||
.eventDate(LocalDate.of(1914, 7, 28))
|
||||
.createdBy(UUID.randomUUID())
|
||||
.updatedBy(UUID.randomUUID());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleting_linked_person_keeps_event_and_drops_join_row() {
|
||||
Person anna = personRepository.save(Person.builder().firstName("Anna").lastName("Raddatz").build());
|
||||
TimelineEvent event = events.save(makeEvent().persons(Set.of(anna)).build());
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
personService.deletePerson(anna.getId());
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
assertThat(events.findById(event.getId())).isPresent();
|
||||
Integer joinRows = jdbc.queryForObject(
|
||||
"SELECT COUNT(*) FROM timeline_event_persons WHERE timeline_event_id = ?",
|
||||
Integer.class, event.getId());
|
||||
assertThat(joinRows).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleting_linked_document_keeps_event_and_drops_join_row() {
|
||||
Document letter = documentRepository.save(Document.builder()
|
||||
.title("Brief").originalFilename("brief.pdf").status(DocumentStatus.UPLOADED).build());
|
||||
TimelineEvent event = events.save(makeEvent().documents(Set.of(letter)).build());
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
documentService.deleteDocument(letter.getId(), UUID.randomUUID());
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
assertThat(events.findById(event.getId())).isPresent();
|
||||
Integer joinRows = jdbc.queryForObject(
|
||||
"SELECT COUNT(*) FROM timeline_event_documents WHERE timeline_event_id = ?",
|
||||
Integer.class, event.getId());
|
||||
assertThat(joinRows).isZero();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package org.raddatz.familienarchiv.timeline;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.raddatz.familienarchiv.PostgresContainerConfig;
|
||||
import org.raddatz.familienarchiv.config.FlywayConfig;
|
||||
import org.raddatz.familienarchiv.document.DatePrecision;
|
||||
import org.raddatz.familienarchiv.document.Document;
|
||||
import org.raddatz.familienarchiv.document.DocumentRepository;
|
||||
import org.raddatz.familienarchiv.document.DocumentStatus;
|
||||
import org.raddatz.familienarchiv.person.Person;
|
||||
import org.raddatz.familienarchiv.person.PersonRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Persistence + DB-constraint tests for {@link TimelineEvent} against real Postgres (V77).
|
||||
* Mirrors {@code MigrationIntegrationTest}'s slice setup; never H2 — only the real DB proves
|
||||
* enum-as-varchar storage, the RANGE/UNKNOWN CHECK constraints, FK cascade, and {@code @Version}.
|
||||
*/
|
||||
@DataJpaTest
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||
@Import({PostgresContainerConfig.class, FlywayConfig.class})
|
||||
class TimelineEventTest {
|
||||
|
||||
@Autowired TimelineEventRepository events;
|
||||
@Autowired PersonRepository persons;
|
||||
@Autowired DocumentRepository documents;
|
||||
@Autowired EntityManager em;
|
||||
|
||||
/**
|
||||
* Sensible defaults; each test overrides only what it asserts. {@code createdBy}/{@code updatedBy}
|
||||
* default to random UUIDs — both columns are NOT NULL and not auto-populated, so without these
|
||||
* every test would fail at flush with the same constraint violation (red for the wrong reason).
|
||||
* Precision is intentionally left unset so {@code @Builder.Default YEAR} can be exercised.
|
||||
*/
|
||||
private TimelineEvent.TimelineEventBuilder makeEvent() {
|
||||
return TimelineEvent.builder()
|
||||
.title("Hochzeit von Anna und Otto")
|
||||
.type(EventType.PERSONAL)
|
||||
.eventDate(LocalDate.of(1914, 7, 28))
|
||||
.createdBy(UUID.randomUUID())
|
||||
.updatedBy(UUID.randomUUID());
|
||||
}
|
||||
|
||||
@Test
|
||||
void persists_and_loads_event_with_required_fields() {
|
||||
TimelineEvent saved = events.save(makeEvent().build());
|
||||
|
||||
assertThat(saved.getId()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void precision_defaults_to_YEAR_when_not_set() {
|
||||
TimelineEvent saved = events.save(makeEvent().build());
|
||||
|
||||
assertThat(saved.getPrecision()).isEqualTo(DatePrecision.YEAR);
|
||||
}
|
||||
|
||||
@Test
|
||||
void persists_event_with_linked_persons() {
|
||||
Person anna = persons.save(Person.builder().firstName("Anna").lastName("Raddatz").build());
|
||||
TimelineEvent saved = events.save(makeEvent().persons(Set.of(anna)).build());
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
TimelineEvent reloaded = events.findById(saved.getId()).orElseThrow();
|
||||
assertThat(reloaded.getPersons()).extracting(Person::getId).containsExactly(anna.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void persists_event_with_linked_documents() {
|
||||
Document letter = documents.save(Document.builder()
|
||||
.title("Brief").originalFilename("brief.pdf").status(DocumentStatus.UPLOADED).build());
|
||||
TimelineEvent saved = events.save(makeEvent().documents(Set.of(letter)).build());
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
TimelineEvent reloaded = events.findById(saved.getId()).orElseThrow();
|
||||
assertThat(reloaded.getDocuments()).extracting(Document::getId).containsExactly(letter.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void eventDateEnd_round_trips_null_for_non_range() {
|
||||
TimelineEvent saved = events.save(makeEvent().build()); // YEAR precision, no end
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
assertThat(events.findById(saved.getId()).orElseThrow().getEventDateEnd()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void eventDateEnd_round_trips_value_for_range() {
|
||||
TimelineEvent saved = events.save(makeEvent()
|
||||
.precision(DatePrecision.RANGE)
|
||||
.eventDate(LocalDate.of(1914, 1, 1))
|
||||
.eventDateEnd(LocalDate.of(1918, 12, 31))
|
||||
.build());
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
assertThat(events.findById(saved.getId()).orElseThrow().getEventDateEnd())
|
||||
.isEqualTo(LocalDate.of(1918, 12, 31));
|
||||
}
|
||||
|
||||
@Test
|
||||
void description_round_trips_null() {
|
||||
TimelineEvent saved = events.save(makeEvent().build());
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
assertThat(events.findById(saved.getId()).orElseThrow().getDescription()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void description_round_trips_multi_kb_text() {
|
||||
// Proves TEXT has no length cap — @Column(columnDefinition = "TEXT") overrides
|
||||
// Hibernate's default VARCHAR(255).
|
||||
String longText = "Sommertage am See. ".repeat(500); // ~9.5 KB
|
||||
TimelineEvent saved = events.save(makeEvent().description(longText).build());
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
assertThat(events.findById(saved.getId()).orElseThrow().getDescription()).isEqualTo(longText);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||
void range_invariant_rejects_non_null_end_without_range_precision() {
|
||||
// precision YEAR + non-null end violates chk_timeline_event_range.
|
||||
try {
|
||||
assertThatThrownBy(() -> events.saveAndFlush(makeEvent()
|
||||
.eventDateEnd(LocalDate.of(1918, 12, 31))
|
||||
.build()))
|
||||
.isInstanceOf(DataIntegrityViolationException.class);
|
||||
} finally {
|
||||
events.deleteAll(); // NOT_SUPPORTED opts out of the rollback; clean any leaked row
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||
void range_invariant_rejects_range_precision_without_end_date() {
|
||||
// precision RANGE + null end violates chk_timeline_event_range.
|
||||
try {
|
||||
assertThatThrownBy(() -> events.saveAndFlush(makeEvent()
|
||||
.precision(DatePrecision.RANGE)
|
||||
.build()))
|
||||
.isInstanceOf(DataIntegrityViolationException.class);
|
||||
} finally {
|
||||
events.deleteAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||
void unknown_precision_is_rejected() {
|
||||
// chk_timeline_event_precision forbids UNKNOWN — curated events are never undated.
|
||||
try {
|
||||
assertThatThrownBy(() -> events.saveAndFlush(makeEvent()
|
||||
.precision(DatePrecision.UNKNOWN)
|
||||
.build()))
|
||||
.isInstanceOf(DataIntegrityViolationException.class);
|
||||
} finally {
|
||||
events.deleteAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void version_is_null_before_persist_and_zero_after_save() {
|
||||
TimelineEvent fresh = makeEvent().build();
|
||||
assertThat(fresh.getVersion()).isNull(); // @Version Long is null pre-persist
|
||||
|
||||
TimelineEvent saved = events.saveAndFlush(fresh);
|
||||
assertThat(saved.getVersion()).isEqualTo(0L); // Hibernate sets 0 on insert
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(value = DatePrecision.class, names = {"DAY", "MONTH", "SEASON", "YEAR", "APPROX"})
|
||||
void all_non_unknown_precisions_are_accepted(DatePrecision precision) {
|
||||
// Accept-side of chk_timeline_event_precision: every non-RANGE, non-UNKNOWN value persists.
|
||||
// Documents that SEASON ("Sommer 1914") and APPROX ("ca. 1914") are intentionally legal,
|
||||
// so an over-tight CHECK cannot ship green. (RANGE is covered by the round-trip test.)
|
||||
TimelineEvent saved = events.saveAndFlush(makeEvent().precision(precision).build());
|
||||
|
||||
assertThat(saved.getId()).isNotNull();
|
||||
}
|
||||
}
|
||||
@@ -164,6 +164,12 @@ _Not to be confused with a document item's optional note_ — a document item's
|
||||
|
||||
**Lesereise** `[user-facing]` — a curated reading journey through a sequence of family documents, optionally annotated with editorial notes. Implemented as a `Geschichte` with `type=JOURNEY`. The reader UI (follow-on issue) renders items as a sequential reading experience.
|
||||
|
||||
**TimelineEvent** (`TimelineEvent`, table `timeline_events`) `[internal]` — a curated event on the family timeline (*Zeitstrahl*), authored by a curator rather than OCR-derived. Carries the same date block as a `Document` (`eventDate` + `precision` + nullable `eventDateEnd`) so events and letters render through one path, plus a `title`, optional `description`, an `EventType`, and `ManyToMany` links to the `Person`s it involves and the `Document`s that support it (both join FKs `ON DELETE CASCADE`). Diverges from `Document` with an optimistic-lock `@Version` and a NOT NULL `createdBy`/`updatedBy` audit trail (bare UUIDs, no FK to `app_users`) for the multi-curator edit flow. Two DB CHECKs: `event_date_end` is non-null **iff** precision is `RANGE` (a strict biconditional, intentionally tighter than `Document`'s open-ended ranges), and `precision` is never `UNKNOWN` (a curated event always has at least a year; `SEASON`/`APPROX` stay legal). See ADR-040.
|
||||
|
||||
**EventType** (`EventType`) `[user-facing]` — the kind of a `TimelineEvent`: `PERSONAL` (a family event, rendered with the family accent) or `HISTORICAL` (world/historical context, rendered with a muted world accent). The string value names are a stable frontend styling contract — renaming requires a coordinated frontend change (ADR-040).
|
||||
|
||||
**Zeitstrahl** `[user-facing]` — the family timeline view, rendering curated `TimelineEvent`s (and, in later issues, derived life-events) chronologically. The milestone home of the `timeline` domain.
|
||||
|
||||
**Notification** (`Notification`) — an in-app message delivered to an `AppUser`. No email or SMS delivery exists today. Delivered via Server-Sent Events (`SseEmitterRegistry`) and persisted in the `notifications` table.
|
||||
|
||||
**Audit log** (`AuditLog`, table `audit_log`) — an append-only event store recording domain-level activity (document edits, user actions, etc.). Append-only by application convention; a `REVOKE UPDATE, DELETE` is attempted at the DB layer (see migrations V46, V47) but is a no-op if the application role is the table owner in PostgreSQL. Do not rely on DB-enforced immutability — the constraint is application-layer only.
|
||||
|
||||
115
docs/adr/040-timeline-domain-data-model.md
Normal file
115
docs/adr/040-timeline-domain-data-model.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# ADR-040 — Timeline domain data model
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-06-12
|
||||
**Issue:** #774 (Zeitstrahl milestone, foundational)
|
||||
|
||||
## Context
|
||||
|
||||
The Zeitstrahl (family timeline) needs a home for *curated* events — births,
|
||||
weddings, moves, and world-historical context a curator types in by hand, distinct
|
||||
from the OCR-derived `Document` letters. This ADR commits the new `timeline` domain's
|
||||
data model: the `TimelineEvent` entity, the `EventType` enum, a repository, and the
|
||||
V77 migration. No service, controller, or DTO ships here — those land in later issues.
|
||||
|
||||
A `TimelineEvent` carries the same date block as `Document` (`eventDate` +
|
||||
`precision` + `eventDateEnd`) so events and letters render through one path, but its
|
||||
audit footprint deliberately diverges (see below).
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. New `timeline` domain package, separate from `geschichte`
|
||||
|
||||
Curated timeline events are their own concern, not a Lesereise/`Geschichte` subtype.
|
||||
The domain owns `TimelineEvent`, `EventType`, and `TimelineEventRepository`.
|
||||
|
||||
### 2. Responses (#775) will be views, not serialized entities
|
||||
|
||||
`TimelineEvent` has two LAZY `ManyToMany` collections (`persons`, `documents`) and
|
||||
`open-in-view` is `false` — exactly the shape that motivated ADR-036 for `geschichte`.
|
||||
#775 must assemble `TimelineEventView`/`TimelineEventSummary` inside the service
|
||||
transaction; a serialized entity is a 500 waiting to happen. Decided up front so it is
|
||||
not retrofitted later.
|
||||
|
||||
### 3. `precision` reuses `document.DatePrecision` — imported, not duplicated
|
||||
|
||||
The `timeline` package imports `org.raddatz.familienarchiv.document.DatePrecision`
|
||||
directly, the same cross-domain value-type sharing ADR-039 established for `person`.
|
||||
An enum has no behaviour and no persistence side effects, so the layering rule
|
||||
(services → own repository) does not govern it. The enum stays a verbatim mirror of the
|
||||
import normalizer's `Precision` values (ADR-025); changes must stay in sync with
|
||||
`tools/import-normalizer/dates.py`. Moving `DatePrecision` into a shared package is a
|
||||
wider refactor (touching `Document`, `importing`, `person`) and its own future ADR.
|
||||
|
||||
### 4. `precision = UNKNOWN` is forbidden; every other value is legal
|
||||
|
||||
`eventDate` is NOT NULL — a curated event always has at least a year, so only OCR letters
|
||||
fall into the "Ohne Datum" bucket. The CHECK `chk_timeline_event_precision`
|
||||
(`date_precision <> 'UNKNOWN'`) forbids exactly that one value. `SEASON` ("Sommer 1914")
|
||||
and `APPROX` ("ca. 1914") are explicitly legal — family memory is full of both, and the
|
||||
spec's rendering table covers them. Do not narrow the CHECK to an allow-list; an
|
||||
over-tight constraint would force curators to fake `YEAR` and render dishonest dates.
|
||||
|
||||
### 5. RANGE invariant is a strict biconditional at the DB, intentionally tighter than `Document`
|
||||
|
||||
`chk_timeline_event_range` enforces `(date_precision = 'RANGE') = (event_date_end IS NOT
|
||||
NULL)` — `eventDateEnd` is non-null **iff** precision is `RANGE`, both directions. This is
|
||||
*stricter* than `Document`'s open-ended ranges (which allow a null end on a RANGE) because a
|
||||
curated event always has a known, closed end when it spans a range — it is authored, not
|
||||
inferred. This divergence is deliberate: a future "bug fix" must not relax it to match
|
||||
`Document`.
|
||||
|
||||
### 6. Audit trail: `@Version` + NOT NULL `createdBy`/`updatedBy`, diverging from `Document`
|
||||
|
||||
`Document` has neither a version nor a creator. A curated entity edited by multiple curators
|
||||
warrants real protection, so `TimelineEvent` adds:
|
||||
|
||||
- `@Version Long version` — optimistic locking for the multi-curator edit flow (#775).
|
||||
Object `Long` (not primitive) so it is `null` before first persist; Hibernate sets `0` on
|
||||
insert. The service **must** catch `ObjectOptimisticLockingFailureException` and translate
|
||||
it to `DomainException.conflict(...)`. Without that translation a concurrent-write conflict
|
||||
surfaces as HTTP 500 with Hibernate internals in the body — information disclosure (CWE-209).
|
||||
- `createdBy`/`updatedBy` as bare `UUID`, `NOT NULL`, no FK to `app_users` (sidecar pattern,
|
||||
matching `DocumentAnnotation`/`OcrJob`; keeps `timeline` decoupled from `user`, avoids
|
||||
lazy-load surprises in the read-heavy assembly path). NOT NULL makes a curated event with no
|
||||
author impossible — an audit gap closed at the schema level. `DocumentAnnotation.createdBy`
|
||||
is nullable and has no `updatedBy`; the escalation here is deliberate because curated events
|
||||
are multi-author. Curator display names resolve through `UserService` at render time.
|
||||
|
||||
`updatedBy` is **not** advanced by `@UpdateTimestamp` — the service must set it from the
|
||||
session principal before every `save()`, or the timestamp moves while the "who" goes stale.
|
||||
|
||||
### 7. `createdBy`/`updatedBy` are server-populated only — never bound from client input
|
||||
|
||||
Both are set from the session principal in the service, never from a request body. Binding
|
||||
them from client input is an authorship-forgery / mass-assignment vector (CWE-639). #775's
|
||||
regression suite must include forgery cases on **both** write paths (`POST` body with
|
||||
`createdBy`, `PUT` body with `updatedBy`) — create and update are separate binding paths, so
|
||||
testing only one leaves half the vector open. The update test must assert `updatedBy` equals
|
||||
the *second* editor's UUID, not merely non-null.
|
||||
|
||||
### 8. `EventType` string values are a stable frontend styling contract
|
||||
|
||||
The Tailwind class map in the timeline Svelte components hard-codes `PERSONAL` (family accent)
|
||||
and `HISTORICAL` (muted world accent) as strings. There is no mapping layer — renaming either
|
||||
value requires a coordinated frontend change. Recorded here to prevent a silent regression.
|
||||
|
||||
### 9. Explicit `@JoinTable` on both ManyToMany fields
|
||||
|
||||
Without explicit `@JoinTable(name, joinColumns, inverseJoinColumns)`, Hibernate's naming
|
||||
strategy could diverge from the V77 DDL's explicit table/column names. Explicit mapping
|
||||
guarantees alignment and makes future column renames a deliberate, visible change. All four FK
|
||||
columns are `ON DELETE CASCADE`: deleting a Person or Document drops the join row and leaves
|
||||
the event intact (V71/ADR-032 hardening — a person delete must never 500).
|
||||
|
||||
## Consequences
|
||||
|
||||
- V77 is forward-only; rollback is manual DDL (`DROP TABLE` the two join tables, then
|
||||
`timeline_events`). No rollback script, no rollback test.
|
||||
- The `timeline → document.DatePrecision` compile coupling is permanent until a shared-package
|
||||
refactor; precedent already exists (`importing/DocumentImporter`, `person`).
|
||||
- The service/controller/DTO layer (#775) inherits the view-assembly, optimistic-lock
|
||||
translation, forgery-guard, and permission obligations recorded above. It must also add a
|
||||
service-level title-length check (new `ErrorCode`, e.g. `TIMELINE_TITLE_TOO_LONG`, mirroring
|
||||
`GESCHICHTE_TITLE_TOO_LONG`) — `title` is `VARCHAR(255)`, and without the guard an over-long
|
||||
title surfaces as a raw `DataIntegrityViolationException` → HTTP 500.
|
||||
123
docs/adr/041-renovate-runner-setup.md
Normal file
123
docs/adr/041-renovate-runner-setup.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# ADR-041 — Renovate runner stand-up: two-token model, OSV surfacing, digest pinning
|
||||
|
||||
**Date:** 2026-06-13
|
||||
**Status:** Accepted
|
||||
**Issue:** [#818](https://git.raddatz.cloud/marcel/familienarchiv/issues/818)
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Issue #817 (esbuild/cookie advisory) revealed that `main` had no early-warning
|
||||
mechanism for newly-published advisories. An advisory landed against already-pinned
|
||||
versions, turned the `npm audit --audit-level=high --omit=dev` gate red on `main`,
|
||||
and then ambushed the next unrelated PR (#774). The author who hit it did not cause
|
||||
it and had no warning.
|
||||
|
||||
`renovate.json` existed but `renovatebot` had never actually run: there was no
|
||||
`.gitea/workflows/renovate.yml` and zero Renovate-authored PRs in the repo's entire
|
||||
history. The three `packageRules` (bucket4j / tiptap / privileged-digest) were
|
||||
silently inert.
|
||||
|
||||
This ADR records the **negative space** — why specific design choices were made,
|
||||
so future maintainers do not "tidy up" toward a worse outcome.
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
### Why there is no auto-provided `GITEA_TOKEN`
|
||||
|
||||
Self-hosted Gitea runners do not auto-inject a `GITEA_TOKEN` equivalent.
|
||||
`docs/infrastructure/ci-gitea.md` (and its current line ~251) explicitly states the
|
||||
token "must be created manually." No existing workflow in this repo references
|
||||
`GITEA_TOKEN` for API calls — only for container registry auth (`docker login`).
|
||||
Both `RENOVATE_TOKEN` and `NIGHTLY_AUDIT_TOKEN` must be manually provisioned as
|
||||
Gitea secrets by a repository admin.
|
||||
|
||||
### Why two tokens, not one
|
||||
|
||||
The two jobs have different blast radii on token compromise:
|
||||
|
||||
| Token | Scopes | Used by |
|
||||
|-------|--------|---------|
|
||||
| `RENOVATE_TOKEN` | `contents` + `pull_request` + `issues` | Renovate — must read/write files and open PRs |
|
||||
| `NIGHTLY_AUDIT_TOKEN` | `issues` only | Nightly audit — only needs to file a tracking issue |
|
||||
|
||||
The nightly job's token appears in step `env:` and is passed to `curl -H`. A leak via
|
||||
runner logs, process arguments, or a misconfigured step would expose the token.
|
||||
An `issues`-only token cannot push branches, open PRs, or read repository contents —
|
||||
the leaked token's blast radius is limited to creating/editing issues.
|
||||
|
||||
A single broad token would give any leak path full `contents` + `pull_request` write
|
||||
access to the repository. That risk is asymmetric with the upside (one fewer secret).
|
||||
|
||||
Both tokens belong to one dedicated bot account (consistent authorship; one identity
|
||||
to audit and rotate). **Branch protection on `main` must forbid the bot pushing
|
||||
directly**, because a `contents`-scoped token can push to any unprotected branch.
|
||||
|
||||
### Why the Renovate action is digest-pinned
|
||||
|
||||
`renovatebot/github-action` executes with the `RENOVATE_TOKEN` in scope. That token
|
||||
carries `contents` + `pull_request` + `issues` — enough to read files, open PRs, and
|
||||
write issues. An unpinned `@v40` tag can be re-pointed by the upstream maintainer
|
||||
(or a compromised maintainer account) at any time. A pinned digest (`@<sha>`) cannot
|
||||
be silently modified; the SHA is immutable. This is the same threat model applied to
|
||||
all privileged CI steps in this repo (see the `matchFileNames` rule in `renovate.json`
|
||||
for `.gitea/workflows/**`).
|
||||
|
||||
Renovate itself will open a PR to bump the digest when a new release ships, which is
|
||||
the intended update path.
|
||||
|
||||
### Why `osvVulnerabilityAlerts` is the load-bearing detector on Gitea
|
||||
|
||||
Renovate's `vulnerabilityAlerts` config key triggers off a *platform* vulnerability
|
||||
graph. GitHub exposes the GitHub Advisory Database via its API; **Gitea does not
|
||||
expose an equivalent vulnerability graph**. On self-hosted Gitea, `vulnerabilityAlerts`
|
||||
is effectively a label carrier — it attaches the configured labels to PRs that
|
||||
`osvVulnerabilityAlerts` already detected, but it is not an independent detector.
|
||||
|
||||
`osvVulnerabilityAlerts: true` is the load-bearing flag: Renovate queries
|
||||
[OSV.dev](https://osv.dev) directly (platform-agnostic). The runner host must be able
|
||||
to reach OSV.dev over HTTPS — if egress is filtered, allow `osv.dev:443` or the flag
|
||||
silently no-ops.
|
||||
|
||||
### Why the root `schedule` does not mute security PRs
|
||||
|
||||
`"schedule": ["before 6am on monday"]` in `renovate.json` batches **routine** dependency
|
||||
updates (version bumps outside any security context) to a weekly window. This reduces
|
||||
noise from routine update PRs while still allowing review before merge.
|
||||
|
||||
**Security and vulnerability PRs bypass the schedule by design** — Renovate raises
|
||||
them immediately regardless of the schedule window. A future "tidy-up" that removes
|
||||
or widens the schedule cannot mute vulnerability alerts; this is worth stating
|
||||
explicitly to prevent that misunderstanding.
|
||||
|
||||
### Why `lockFileMaintenance` has no `automerge`
|
||||
|
||||
`lockFileMaintenance` refreshes transitive pins weekly so the dependency tree drifts
|
||||
into fewer advisories over time. It is explicitly set without `automerge: true` because
|
||||
a weekly transitive pin refresh can silently break the build if a transitive dep
|
||||
introduces a breaking change. These PRs are small and should be reviewed.
|
||||
|
||||
### Why there is no entry in `l2-containers.puml`
|
||||
|
||||
`docs/architecture/c4/l2-containers.puml` documents long-lived infrastructure
|
||||
containers (services that run continuously). Renovate is a scheduled CI job that runs
|
||||
on a Gitea Actions runner and exits — it is not a long-lived container. Adding it to
|
||||
the container diagram would misrepresent the architecture. This omission is deliberate,
|
||||
not an oversight.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
- Newly-published advisories against our frontend dependencies are surfaced within
|
||||
one day (daily Renovate cron) rather than at the next contributor PR.
|
||||
- A nightly `npm audit` job provides an independent signal for dev-dependency advisories
|
||||
that Renovate may not cover via OSV.
|
||||
- Two secrets (`RENOVATE_TOKEN`, `NIGHTLY_AUDIT_TOKEN`) must be manually provisioned
|
||||
and rotated annually (or on suspected compromise). See
|
||||
`docs/infrastructure/ci-gitea.md` for the runbook.
|
||||
- The bot account must be kept active and branch protection on `main` must forbid
|
||||
it pushing directly. These are operational prerequisites, not code invariants.
|
||||
24
docs/architecture/c4/l3-backend-timeline.puml
Normal file
24
docs/architecture/c4/l3-backend-timeline.puml
Normal file
@@ -0,0 +1,24 @@
|
||||
@startuml
|
||||
!include <C4/C4_Component>
|
||||
|
||||
title Component Diagram: API Backend — Timeline (Zeitstrahl)
|
||||
|
||||
ContainerDb(db, "PostgreSQL", "PostgreSQL 16")
|
||||
|
||||
System_Boundary(backend, "API Backend (Spring Boot)") {
|
||||
Component(timelineRepo, "TimelineEventRepository", "Spring Data JPA", "Reads and writes TimelineEvent rows and their persons/documents join tables (timeline_event_persons, timeline_event_documents). Issue #774 ships the repository empty; the per-person filter query lands in #777.")
|
||||
|
||||
Component(timelineSvc, "TimelineEventService", "Spring Service (planned, #775)", "Will own curated-event CRUD: assemble TimelineEventView/Summary inside the transaction (lazy ManyToMany + open-in-view=false, per ADR-036/ADR-040), populate createdBy/updatedBy from the session principal, and translate optimistic-lock conflicts to DomainException.conflict.")
|
||||
Component(timelineCtrl, "TimelineEventController", "Spring MVC (planned, #775)", "Will expose /api/timeline reads (READ_ALL) and writes (WRITE_ALL). createdBy/updatedBy are never bound from request bodies (CWE-639).")
|
||||
}
|
||||
|
||||
System_Ext(documentDomain, "Document domain", "Provides DatePrecision (shared value type) and Document references for linked letters")
|
||||
System_Ext(personDomain, "Person domain", "Provides Person references for who an event involves")
|
||||
|
||||
Rel(timelineRepo, db, "SQL queries", "JDBC")
|
||||
Rel(timelineSvc, timelineRepo, "Reads / writes events (planned)")
|
||||
Rel(timelineCtrl, timelineSvc, "Delegates to (planned)")
|
||||
Rel(timelineRepo, personDomain, "References persons via join table")
|
||||
Rel(timelineRepo, documentDomain, "References documents via join table")
|
||||
|
||||
@enduml
|
||||
@@ -1,6 +1,6 @@
|
||||
@startuml db-orm
|
||||
' Schema source: Flyway V1–V76 (excl. V37, V43 — intentionally removed)
|
||||
' Schema as of: V76 (2026-06-12)
|
||||
' Schema source: Flyway V1–V77 (excl. V37, V43 — intentionally removed)
|
||||
' Schema as of: V77 (2026-06-12)
|
||||
' ⚠ This is a versioned snapshot. Update when the schema changes significantly.
|
||||
|
||||
hide circle
|
||||
@@ -386,6 +386,39 @@ package "Supporting" {
|
||||
}
|
||||
}
|
||||
|
||||
' ── Timeline (Zeitstrahl) ──
|
||||
package "Timeline" {
|
||||
|
||||
entity timeline_events {
|
||||
id : UUID <<PK>>
|
||||
--
|
||||
title : VARCHAR(255) NOT NULL
|
||||
type : VARCHAR(16) NOT NULL
|
||||
event_date : DATE NOT NULL
|
||||
date_precision : VARCHAR(16) NOT NULL DEFAULT 'YEAR'
|
||||
event_date_end : DATE
|
||||
description : TEXT
|
||||
created_by : UUID NOT NULL
|
||||
created_at : TIMESTAMP
|
||||
updated_by : UUID NOT NULL
|
||||
updated_at : TIMESTAMP
|
||||
version : BIGINT
|
||||
==
|
||||
CHECK ((date_precision = 'RANGE') = (event_date_end IS NOT NULL))
|
||||
CHECK (date_precision <> 'UNKNOWN')
|
||||
}
|
||||
|
||||
entity timeline_event_persons {
|
||||
timeline_event_id : UUID <<FK>>
|
||||
person_id : UUID <<FK>>
|
||||
}
|
||||
|
||||
entity timeline_event_documents {
|
||||
timeline_event_id : UUID <<FK>>
|
||||
document_id : UUID <<FK>>
|
||||
}
|
||||
}
|
||||
|
||||
' Auth relationships
|
||||
app_users_groups }o--|| app_users : app_user_id
|
||||
app_users_groups }o--|| user_groups : group_id
|
||||
@@ -449,4 +482,10 @@ geschichten_persons }o--|| persons : person_id
|
||||
journey_items }o--|| geschichten : geschichte_id (ON DELETE CASCADE)
|
||||
journey_items }o--o| documents : document_id (ON DELETE SET NULL)
|
||||
|
||||
' Timeline relationships
|
||||
timeline_event_persons }o--|| timeline_events : timeline_event_id (ON DELETE CASCADE)
|
||||
timeline_event_persons }o--|| persons : person_id (ON DELETE CASCADE)
|
||||
timeline_event_documents }o--|| timeline_events : timeline_event_id (ON DELETE CASCADE)
|
||||
timeline_event_documents }o--|| documents : document_id (ON DELETE CASCADE)
|
||||
|
||||
@enduml
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
' precision/attribution fields); no new FK relationships, so this diagram is unchanged.
|
||||
' Note: V76 swaps persons.birth_year/death_year for birth_date/death_date +
|
||||
' precision columns; columns only, no new FK relationships, diagram unchanged.
|
||||
' Note: V77 adds the timeline_events table + two join tables (Timeline package below).
|
||||
|
||||
hide circle
|
||||
skinparam linetype ortho
|
||||
@@ -71,6 +72,13 @@ package "Supporting" {
|
||||
entity journey_items
|
||||
}
|
||||
|
||||
' ── Timeline (Zeitstrahl) ──
|
||||
package "Timeline" {
|
||||
entity timeline_events
|
||||
entity timeline_event_persons
|
||||
entity timeline_event_documents
|
||||
}
|
||||
|
||||
' Auth relationships
|
||||
app_users_groups }o--|| app_users : app_user_id
|
||||
app_users_groups }o--|| user_groups : group_id
|
||||
@@ -136,4 +144,11 @@ journey_items }o--o| documents : document_id (ON DELETE SET NULL)
|
||||
note right of journey_items : partial UNIQUE (geschichte_id, document_id)\nWHERE document_id IS NOT NULL (V74)
|
||||
note right of geschichten : CHECK length(body) <= 4000\nfor type = JOURNEY (V75)
|
||||
|
||||
' Timeline relationships (V77)
|
||||
timeline_event_persons }o--|| timeline_events : timeline_event_id (ON DELETE CASCADE)
|
||||
timeline_event_persons }o--|| persons : person_id (ON DELETE CASCADE)
|
||||
timeline_event_documents }o--|| timeline_events : timeline_event_id (ON DELETE CASCADE)
|
||||
timeline_event_documents }o--|| documents : document_id (ON DELETE CASCADE)
|
||||
note right of timeline_events : CHECK event_date_end non-null IFF RANGE\nCHECK date_precision <> 'UNKNOWN' (V77)
|
||||
|
||||
@enduml
|
||||
|
||||
@@ -462,3 +462,82 @@ jobs:
|
||||
name: e2e-results
|
||||
path: frontend/test-results/e2e/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Renovate + Nightly Audit — Token Model
|
||||
|
||||
> See [ADR-041](../adr/041-renovate-runner-setup.md) for full rationale.
|
||||
|
||||
### Two-token model
|
||||
|
||||
This repo uses two separate tokens for automated dependency work, both manually
|
||||
provisioned as Gitea secrets. There is **no auto-provided `GITEA_TOKEN`** on
|
||||
self-hosted Gitea runners — it must be created manually (see §Context Variable Names
|
||||
table above).
|
||||
|
||||
| Secret | Scopes | Job | Reason |
|
||||
|--------|--------|-----|--------|
|
||||
| `RENOVATE_TOKEN` | `contents` + `pull_request` + `issues` | `renovate.yml` | Renovate needs to read/write files and open PRs |
|
||||
| `NIGHTLY_AUDIT_TOKEN` | `issues` only | `nightly.yml` → `npm-audit` job | Only needs to file a tracking issue; an `issues`-only token cannot push branches or read contents — limits blast radius on leak |
|
||||
|
||||
Both tokens belong to a single dedicated bot account. **Branch protection on `main`
|
||||
must forbid the bot pushing directly**, because a `contents`-scoped token can push
|
||||
to any unprotected branch.
|
||||
|
||||
### PAT rotation cadence
|
||||
|
||||
Rotate both tokens:
|
||||
- **Annually** (calendar reminder)
|
||||
- **Immediately** on suspected compromise (runner log leak, accidental `set -x`, etc.)
|
||||
|
||||
Tokens are stored exclusively as Gitea secrets. Never commit them to `.env` files or
|
||||
log them. The nightly audit step passes its token via `env:` at the step level, reads
|
||||
it as `$NIGHTLY_AUDIT_TOKEN` in the shell, and never runs the API `curl` under
|
||||
`set -x`.
|
||||
|
||||
### OSV vs platform alerts on Gitea
|
||||
|
||||
Renovate's `vulnerabilityAlerts` config key requires a *platform* vulnerability graph.
|
||||
**Gitea does not expose one** — it has no equivalent to the GitHub Advisory Database
|
||||
API. On this runner, `vulnerabilityAlerts` is a label carrier only: it attaches
|
||||
`security` and `P1-high` labels to PRs that `osvVulnerabilityAlerts` already raised.
|
||||
|
||||
`osvVulnerabilityAlerts: true` is the load-bearing detector. Renovate queries
|
||||
[OSV.dev](https://osv.dev) directly, which works regardless of platform. The runner
|
||||
host must be able to reach `osv.dev:443`. If egress is filtered and OSV.dev is
|
||||
unreachable, the flag silently no-ops — verify egress when standing up the runner.
|
||||
|
||||
### Nightly audit vs PR gate (divergence)
|
||||
|
||||
| Gate | Command | Dev deps | When |
|
||||
|------|---------|----------|------|
|
||||
| PR gate (`ci.yml`) | `npm audit --audit-level=high --omit=dev` | ❌ excluded | Every PR |
|
||||
| Nightly audit (`nightly.yml`) | `npm audit --audit-level=high` | ✅ included | Nightly + `workflow_dispatch` |
|
||||
|
||||
The nightly job is **deliberately broader** — it catches dev-tooling advisories
|
||||
(esbuild, Vite, vitest, etc.) that the PR gate ignores. A red nightly audit job does
|
||||
**not** mean the PR gate is broken; the two signals are independent.
|
||||
|
||||
### Runbook: nightly-opened tracking issue
|
||||
|
||||
When the `npm-audit` job opens or updates the tracking issue
|
||||
"Nightly npm audit: high-severity advisory":
|
||||
|
||||
1. **Triage severity.** Check the advisory page (link in the issue body). Is it
|
||||
exploitable in production? A dev-only dep (e.g. esbuild, prettier) has no
|
||||
production attack surface — treat as low urgency.
|
||||
|
||||
2. **Pin or upgrade.** If a non-breaking upgrade is available, update
|
||||
`frontend/package.json` and regenerate the lockfile. Open a PR.
|
||||
|
||||
3. **Override if justified.** If the advisory does not apply (dev-only dep, no
|
||||
exploitable path), add an `npm audit` override in `package.json`:
|
||||
```json
|
||||
"overrides": { "esbuild": ">=0.25.4" }
|
||||
```
|
||||
Document the rationale in the PR body. See #817 for the reference decision tree.
|
||||
|
||||
4. **Close the tracking issue** once the advisory is resolved or overridden and the
|
||||
nightly job runs clean (verify via the `✅ npm audit clean` heartbeat in the job
|
||||
summary).
|
||||
|
||||
@@ -151,7 +151,7 @@ receivers:
|
||||
name: Renovate
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * 1' # every Monday at 3am
|
||||
- cron: '0 3 * * *' # daily at 03:00 UTC — cuts OSV-alert latency to ≤1 day
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -160,32 +160,58 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run Renovate
|
||||
uses: renovatebot/github-action@v40
|
||||
# Pin by digest — this action holds contents+pull_request+issues token;
|
||||
# an unpinned tag is a supply-chain risk. Update digest + renovate-version
|
||||
# together when Renovate publishes a new release.
|
||||
uses: renovatebot/github-action@8217b3fc286df088d7c27f3255fe8414463bc0fd # v46.1.15
|
||||
with:
|
||||
configurationFile: renovate.json
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
renovate-version: latest
|
||||
token: ${{ secrets.RENOVATE_TOKEN }}
|
||||
renovate-version: "46.1.15"
|
||||
env:
|
||||
RENOVATE_PLATFORM: gitea
|
||||
RENOVATE_ENDPOINT: https://gitea.example.com # replace with your Gitea URL
|
||||
RENOVATE_REPOSITORIES: '["org/repo"]' # replace with your repo slug
|
||||
LOG_LEVEL: info
|
||||
```
|
||||
|
||||
> **Token:** `RENOVATE_TOKEN` must be a PAT on a dedicated bot account with scopes
|
||||
> `contents` + `pull_request` + `issues`. **Do not reuse** `GITEA_TOKEN` — that variable
|
||||
> is not auto-provided on self-hosted Gitea runners and must be manually created anyway;
|
||||
> using a single broad token violates least-privilege. See ADR-041.
|
||||
|
||||
### Renovate Configuration
|
||||
|
||||
The `renovate.json` in the repo root carries only dependency rules — platform and
|
||||
endpoint config is injected via `env:` in the workflow above. Keep the two concerns
|
||||
separate so the config file remains portable.
|
||||
|
||||
```json
|
||||
// renovate.json
|
||||
{
|
||||
"platform": "gitea",
|
||||
"endpoint": "https://gitea.example.com",
|
||||
"repositories": ["org/familienarchiv"],
|
||||
"automerge": true,
|
||||
"automergeType": "pr",
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"osvVulnerabilityAlerts": true,
|
||||
"dependencyDashboard": true,
|
||||
"schedule": ["before 6am on monday"],
|
||||
"vulnerabilityAlerts": {
|
||||
"labels": ["security", "P1-high"]
|
||||
},
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"schedule": ["before 6am on monday"]
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"automerge": true
|
||||
"matchPackageNames": ["com.example:my-dep"],
|
||||
"automerge": true,
|
||||
"matchUpdateTypes": ["patch"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> **Do not add `automerge: true` at the root.** Security and digest-bump PRs should
|
||||
> always be reviewed manually. Per-rule `automerge` on patch-level routine deps is fine.
|
||||
|
||||
---
|
||||
|
||||
## Secrets Management -- age + git-crypt
|
||||
|
||||
226
frontend/package-lock.json
generated
226
frontend/package-lock.json
generated
@@ -558,9 +558,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -574,9 +574,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -590,9 +590,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -606,9 +606,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -622,9 +622,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -638,9 +638,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -654,9 +654,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -670,9 +670,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -686,9 +686,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -702,9 +702,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -718,9 +718,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -734,9 +734,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
|
||||
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||||
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -750,9 +750,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
|
||||
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -766,9 +766,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -782,9 +782,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
|
||||
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -798,9 +798,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
|
||||
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -814,9 +814,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -830,9 +830,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -846,9 +846,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -862,9 +862,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -878,9 +878,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -894,9 +894,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -910,9 +910,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -926,9 +926,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -942,9 +942,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -958,9 +958,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -5531,12 +5531,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
@@ -5799,9 +5803,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -5811,32 +5815,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.7",
|
||||
"@esbuild/android-arm": "0.27.7",
|
||||
"@esbuild/android-arm64": "0.27.7",
|
||||
"@esbuild/android-x64": "0.27.7",
|
||||
"@esbuild/darwin-arm64": "0.27.7",
|
||||
"@esbuild/darwin-x64": "0.27.7",
|
||||
"@esbuild/freebsd-arm64": "0.27.7",
|
||||
"@esbuild/freebsd-x64": "0.27.7",
|
||||
"@esbuild/linux-arm": "0.27.7",
|
||||
"@esbuild/linux-arm64": "0.27.7",
|
||||
"@esbuild/linux-ia32": "0.27.7",
|
||||
"@esbuild/linux-loong64": "0.27.7",
|
||||
"@esbuild/linux-mips64el": "0.27.7",
|
||||
"@esbuild/linux-ppc64": "0.27.7",
|
||||
"@esbuild/linux-riscv64": "0.27.7",
|
||||
"@esbuild/linux-s390x": "0.27.7",
|
||||
"@esbuild/linux-x64": "0.27.7",
|
||||
"@esbuild/netbsd-arm64": "0.27.7",
|
||||
"@esbuild/netbsd-x64": "0.27.7",
|
||||
"@esbuild/openbsd-arm64": "0.27.7",
|
||||
"@esbuild/openbsd-x64": "0.27.7",
|
||||
"@esbuild/openharmony-arm64": "0.27.7",
|
||||
"@esbuild/sunos-x64": "0.27.7",
|
||||
"@esbuild/win32-arm64": "0.27.7",
|
||||
"@esbuild/win32-ia32": "0.27.7",
|
||||
"@esbuild/win32-x64": "0.27.7"
|
||||
"@esbuild/aix-ppc64": "0.28.1",
|
||||
"@esbuild/android-arm": "0.28.1",
|
||||
"@esbuild/android-arm64": "0.28.1",
|
||||
"@esbuild/android-x64": "0.28.1",
|
||||
"@esbuild/darwin-arm64": "0.28.1",
|
||||
"@esbuild/darwin-x64": "0.28.1",
|
||||
"@esbuild/freebsd-arm64": "0.28.1",
|
||||
"@esbuild/freebsd-x64": "0.28.1",
|
||||
"@esbuild/linux-arm": "0.28.1",
|
||||
"@esbuild/linux-arm64": "0.28.1",
|
||||
"@esbuild/linux-ia32": "0.28.1",
|
||||
"@esbuild/linux-loong64": "0.28.1",
|
||||
"@esbuild/linux-mips64el": "0.28.1",
|
||||
"@esbuild/linux-ppc64": "0.28.1",
|
||||
"@esbuild/linux-riscv64": "0.28.1",
|
||||
"@esbuild/linux-s390x": "0.28.1",
|
||||
"@esbuild/linux-x64": "0.28.1",
|
||||
"@esbuild/netbsd-arm64": "0.28.1",
|
||||
"@esbuild/netbsd-x64": "0.28.1",
|
||||
"@esbuild/openbsd-arm64": "0.28.1",
|
||||
"@esbuild/openbsd-x64": "0.28.1",
|
||||
"@esbuild/openharmony-arm64": "0.28.1",
|
||||
"@esbuild/sunos-x64": "0.28.1",
|
||||
"@esbuild/win32-arm64": "0.28.1",
|
||||
"@esbuild/win32-ia32": "0.28.1",
|
||||
"@esbuild/win32-x64": "0.28.1"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
|
||||
@@ -73,5 +73,9 @@
|
||||
"vite-plugin-static-copy": "^4.1.0",
|
||||
"vitest": "^4.0.10",
|
||||
"vitest-browser-svelte": "^2.0.1"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "0.28.1",
|
||||
"cookie": ">=0.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"osvVulnerabilityAlerts": true,
|
||||
"dependencyDashboard": true,
|
||||
"schedule": ["before 6am on monday"],
|
||||
"vulnerabilityAlerts": {
|
||||
"labels": ["security", "P1-high"]
|
||||
},
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"schedule": ["before 6am on monday"]
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "bucket4j-core is manually pinned outside the Spring BOM — track patch auto-merge, minor/major as PRs.",
|
||||
@@ -9,13 +19,13 @@
|
||||
"matchUpdateTypes": ["patch"]
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["^@tiptap/"],
|
||||
"matchPackageNames": ["/^@tiptap/"],
|
||||
"groupName": "tiptap",
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "Digest bumps for images used in privileged CI steps (--privileged --pid=host) must be reviewed manually — a compromised image has root-equivalent host access. Covers .gitea/actions/** too: the reload-caddy alpine digest now lives in a composite action (#603).",
|
||||
"matchPaths": [".gitea/workflows/**", ".gitea/actions/**"],
|
||||
"matchFileNames": [".gitea/workflows/**", ".gitea/actions/**"],
|
||||
"matchUpdateTypes": ["digest"],
|
||||
"automerge": false,
|
||||
"reviewersFromCodeOwners": false
|
||||
|
||||
Reference in New Issue
Block a user