diff --git a/CLAUDE.md b/CLAUDE.md
index eea4abe..019c6c1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -85,6 +85,16 @@ SSRF-Schutz (`lib/security/ssrf.ts`).
`100vh` beziehen sich weiter auf das volle. `components/ui/Modal.tsx` hängt
dafür an `window.visualViewport`; wer ein eigenes Overlay baut, braucht
dasselbe.
+- **Ein verschluckter Fehler ist schlimmer als ein lauter.** `getJsonSafe` machte aus
+ jedem Fehlschlag eine leere Liste. Damit war ein 403 nicht mehr von „das Repository
+ hat keine Branches" zu unterscheiden: Der Abgleich meldete Erfolg, überschrieb den
+ letzten guten Stand mit Nullen, und die Ursache war von außen nicht zu ermitteln —
+ die Auskunft, die sie benannt hätte, war ja gerade weggeworfen worden. Wo ein
+ Fallback nötig ist, gehört der Grund festgehalten (`Teilabrufe` in `lib/git/http.ts`).
+- **`Number()` auf einer Umgebungsvariable prüft nichts.** `Number("")` ist `0`,
+ `Number("abc")` ist `NaN`, und `setTimeout` löst bei beidem sofort aus. Eine
+ verrutschte Zeile in der `.env` konnte so den kompletten Abgleich stilllegen, ohne
+ dass irgendwo etwas stand.
- **Blättern über Cursor, nicht über `skip`.** Der Aktivitätsverlauf schreibt
mehrere Zeilen im selben Augenblick; ein Offset über so eine Grenze hinweg
überspringt oder verdoppelt lautlos. Muster in `lib/timeline.ts`:
diff --git a/FEATURES.md b/FEATURES.md
index f053c33..0b2ce48 100644
--- a/FEATURES.md
+++ b/FEATURES.md
@@ -553,6 +553,33 @@ deshalb ohne Risiko.
| Releases | bis zu 10, inklusive Pre-Release- und Draft-Kennzeichnung |
| Sprachen | Verteilung als Balken mit Prozentwerten |
+#### Wenn ein Teil nicht ankommt
+
+Diese Listen werden gleichzeitig geholt, und einzelne davon können scheitern, während
+der Rest durchgeht — ein 403 wegen fehlender Scopes, ein Rate-Limit, eine
+Zeitüberschreitung.
+
+**Ein gescheiterter Abruf ist keine leere Liste.** Der Unterschied steht am Snapshot: zu
+jedem Feld, das nicht geholt werden konnte, wird der Grund samt HTTP-Status festgehalten.
+Daraus folgt dreierlei:
+
+- Die vorhandenen Daten **bleiben stehen**. Ein Abgleich, der die Tags nicht holen konnte,
+ ersetzt die gespeicherten nicht durch eine leere Liste. Angezeigt wird dann der letzte
+ vollständige Stand, mit Datum.
+- Die Meldung heißt „Teilweise aktualisiert – nicht geholt: …" statt „Snapshot
+ aktualisiert: 0 Branches, 0 Tags".
+- Auf der Projektseite steht ein Hinweis am Repository, der jedes fehlende Feld mit
+ seinem Grund nennt.
+
+Das gilt ausdrücklich **nicht** als Fehlschlag: `syncError` bleibt leer, der Zähler für
+das Zurückhalten wird nicht erhöht, und der nächste Durchlauf versucht es normal wieder.
+Der Rest des Standes ist ja frisch.
+
+> Vor v1.10.2 wurde jeder Fehlschlag in eine leere Liste verwandelt. Ein Repository mit
+> 65 Commits meldete „Snapshot aktualisiert: 0 Branches, 0 Tags", überschrieb dabei den
+> letzten guten Stand, und aus der Oberfläche war nicht zu erkennen, dass überhaupt etwas
+> schiefgegangen war.
+
Woher der CI-Status kommt:
| System | Quelle |
diff --git a/README.md b/README.md
index 6e531ca..2d070d1 100644
--- a/README.md
+++ b/README.md
@@ -131,7 +131,7 @@ Alle Variablen sind in [`.env.example`](.env.example) dokumentiert.
| `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` | Zugangsdaten des DB-Containers | `nebula` |
| `POSTGRES_PORT` | Host-Port der Datenbank — wirkt nur mit `docker-compose.dev.yml`, sonst hat die DB keinen | `127.0.0.1:5432` |
| `ADMINER_PORT` | Host-Port von Adminer (`--profile tools`) | `127.0.0.1:8080` |
-| `GIT_API_TIMEOUT_MS` | Timeout für Git-API-Aufrufe | `15000` |
+| `GIT_API_TIMEOUT_MS` | Timeout für Git-API-Aufrufe, in Millisekunden. Alles unter 1000 oder Unlesbares wird verworfen (mit Warnung im Log) — sonst bräche jeder Aufruf sofort ab | `15000` |
| `GIT_ALLOW_LOOPBACK` | Git-Instanz auf demselben Rechner erlauben | `false` |
| `GIT_BLOCK_PRIVATE` | Zugriff ins private LAN sperren | `false` |
| `GIT_ALLOWED_HOSTS` | Strengster Modus: nur diese Hosts erlauben | leer |
diff --git a/prisma/migrations/20260901160000_snapshot_teilabrufe/migration.sql b/prisma/migrations/20260901160000_snapshot_teilabrufe/migration.sql
new file mode 100644
index 0000000..af79c38
--- /dev/null
+++ b/prisma/migrations/20260901160000_snapshot_teilabrufe/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "RepoSnapshot" ADD COLUMN "completeAt" TIMESTAMP(3),
+ADD COLUMN "missing" JSONB NOT NULL DEFAULT '{}';
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index d865db8..6ded25a 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -649,6 +649,19 @@ model RepoSnapshot {
ciUrl String?
ciFinishedAt DateTime?
+ /// Teil-Abrufe, die beim letzten Abgleich nicht geklappt haben:
+ /// { "branches": "HTTP 403: Zugriff verweigert …" }. Leeres Objekt heisst,
+ /// dass alle Listen wirklich geholt wurden.
+ ///
+ /// Ohne diese Angabe ist eine leere Liste zweideutig – "nicht geholt" sieht
+ /// aus wie "nichts vorhanden". Genau daran ist eine Fehlersuche einmal
+ /// haengen geblieben.
+ missing Json @default("{}")
+
pushedAt DateTime?
fetchedAt DateTime @default(now())
+ /// Wann zuletzt ein Abgleich lief, bei dem alle Teile ankamen. Steht der
+ /// weiter zurueck als fetchedAt, ist ein Teil der Anzeige aelter als sie
+ /// vorgibt.
+ completeAt DateTime?
}
diff --git a/src/components/GitPanel.tsx b/src/components/GitPanel.tsx
index c38da89..42705e6 100644
--- a/src/components/GitPanel.tsx
+++ b/src/components/GitPanel.tsx
@@ -30,13 +30,14 @@ import { RepositoryDialog } from "@/components/RepositoryDialog";
import { WebhookDialog } from "@/components/WebhookDialog";
import { Sparkline } from "@/components/Sparkline";
import { repoTitle } from "@/lib/git/meta";
-import { asArray, asCalendar, asRecord } from "@/types";
+import { asArray, asCalendar, asRecord, asTextRecord } from "@/types";
import type {
BranchInfo,
CommitInfo,
ConnectionSafe,
ProjectDetail,
ReleaseInfo,
+ RepoSnapshot,
RepositoryWithSnapshot,
TagInfo,
TicketInfo,
@@ -69,17 +70,26 @@ export function GitPanel({
const [tab, setTab] = useState
- Verknuepfe ein Repository, um Branches, Tags, Commits und Releases hier zu sehen.
- Ein Projekt darf auch mehrere haben – etwa Frontend und Backend.
+ Verknuepfe ein Repository, um Branches, Tags, Commits und Releases
+ hier zu sehen. Ein Projekt darf auch mehrere haben – etwa Frontend
+ und Backend.
@@ -470,7 +534,8 @@ export function GitPanel({
title={`${name} – ${Math.round((bytes / languageTotal) * 100)}%`}
style={{
width: `${(bytes / languageTotal) * 100}%`,
- backgroundColor: LANGUAGE_COLORS[index % LANGUAGE_COLORS.length],
+ backgroundColor:
+ LANGUAGE_COLORS[index % LANGUAGE_COLORS.length],
}}
/>
))}
@@ -480,7 +545,10 @@ export function GitPanel({
{name}
@@ -537,8 +605,8 @@ export function GitPanel({
{issues.length > 0 ? (
- Aus offenen Issues Aufgaben machen. Bereits uebernommene werden
- uebersprungen.
+ Aus offenen Issues Aufgaben machen. Bereits uebernommene
+ werden uebersprungen.
{value}
+ {value}
+ Kein Repository verknuepft
+
+ Kein Repository verknuepft
+
- #{ticket.number} + + #{ticket.number} + {ticket.author ?? "unbekannt"} {ticket.labels.slice(0, 3).map((label) => ( - + {label} ))} @@ -650,7 +730,9 @@ function TicketList({