diff --git a/apps/server/src/gits/Layers/AutomodeProposalSweep.ts b/apps/server/src/gits/Layers/AutomodeProposalSweep.ts index 1a3ff3c69b3..9a72c587c9b 100644 --- a/apps/server/src/gits/Layers/AutomodeProposalSweep.ts +++ b/apps/server/src/gits/Layers/AutomodeProposalSweep.ts @@ -212,11 +212,13 @@ export const AutomodeProposalSweepLive = Layer.effect( yield* Ref.set(stateRef, next); // Sequential: hermes invocations share codex capacity — never parallelise. const newGoals: AutomodeGoal[] = []; + let attemptedRepos = 0; for (const repo of policy.proposalRepos) { if (!repoAllowed(policy, repo)) { yield* Effect.logWarning("gits.sweep.repo-not-allowed", { repo }); continue; } + attemptedRepos += 1; const goal = yield* sweepRepo(repo); if (goal === null) continue; newGoals.push(goal); @@ -235,6 +237,21 @@ export const AutomodeProposalSweepLive = Layer.effect( ); } } + if (attemptedRepos > 0 && newGoals.length === 0) { + // A sweep night is consumed up front, so a silent zero-goal run means the owner + // waits a whole day without knowing anything went wrong. Always say so. + // ponytail: generic message; per-repo reasons live in the gits.sweep.* logs. + yield* notifier + .notify({ + subject: "GITS nightly sweep", + text: `Sweep ran for ${attemptedRepos} repo(s) but drafted no goals. Check the cockpit logs (gits.sweep.*) for reasons.`, + }) + .pipe( + Effect.catchCause((cause) => + Effect.logWarning("gits.sweep.telegram-failed", { cause: Cause.pretty(cause) }), + ), + ); + } if (newGoals.length > 0) { const goalLines = newGoals.map( (goal) => `${goal.title} — ${path.basename(goal.repo)} [${goal.id}]`, diff --git a/apps/web/index.html b/apps/web/index.html index 3671338a578..bc67a98f082 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -92,15 +92,30 @@ color: #171717; user-select: none; } + + html.dark .gits-boot { + background: #161616; + } + + html.dark .gits-boot-title { + color: #f5f5f5; + } GITS
- + GITS
+
diff --git a/apps/web/public/gits-logo-anim-dark.gif b/apps/web/public/gits-logo-anim-dark.gif new file mode 100644 index 00000000000..8b4c8218d98 Binary files /dev/null and b/apps/web/public/gits-logo-anim-dark.gif differ diff --git a/apps/web/public/gits-logo-hold-dark.png b/apps/web/public/gits-logo-hold-dark.png new file mode 100644 index 00000000000..809d2fbb8ee Binary files /dev/null and b/apps/web/public/gits-logo-hold-dark.png differ diff --git a/apps/web/public/gits-logo-hold.png b/apps/web/public/gits-logo-hold.png new file mode 100644 index 00000000000..9d505945a9c Binary files /dev/null and b/apps/web/public/gits-logo-hold.png differ diff --git a/apps/web/src/components/SplashScreen.tsx b/apps/web/src/components/SplashScreen.tsx index 25efae0be43..b99872cb4dd 100644 --- a/apps/web/src/components/SplashScreen.tsx +++ b/apps/web/src/components/SplashScreen.tsx @@ -1,11 +1,10 @@ import { useEffect, useState } from "react"; -// One full cycle of /gits-logo-anim.gif. Elapsed time counts from the static +// One full cycle of /gits-logo-anim(-dark).gif. Elapsed time counts from the static // index.html boot splash (window.__gitsBootStart) so the animation plays once -// total across both layers, then holds the static logo instead of looping. +// total across both layers, then holds the last frame instead of looping. // ponytail: constant must track the gif asset; re-measure if the gif changes. const LOGO_CYCLE_MS = 4000; -const HOLD_LOGO_SRC = "/apple-touch-icon.png"; function elapsedSinceBoot(): number { const start = (window as Window & { __gitsBootStart?: number }).__gitsBootStart; @@ -13,6 +12,8 @@ function elapsedSinceBoot(): number { } export function SplashScreen() { + // Same signal the index.html boot script uses; read once — the splash is transient. + const isDark = document.documentElement.classList.contains("dark"); const [hold, setHold] = useState(() => elapsedSinceBoot() >= LOGO_CYCLE_MS); useEffect(() => { if (hold) return; @@ -20,21 +21,25 @@ export function SplashScreen() { return () => clearTimeout(timer); }, [hold]); return ( -
+
- +
- + GITS