Skip to content

Changelog

Complete change history of the GitPulse project - from first commit to latest updates. Format: Keep a Changelog.

[2026-07-11] - Archived courses are now viewable (read-only) 2026.7.3.post39

Added

  • Archived courses are now viewable (read-only). The dashboard "Archived Courses" list gained a View button that opens the course. Every course tab (overview / settings / activity / analytics / teachers) shows a read-only banner - "This course is archived, archived on … Reactivate it to make changes" - with a Reactivate shortcut for staff.
  • Archived courses are genuinely read-only. Course-scoped mutations (course sync-all, course & group run-checks, group sync, settings save, group creation) are rejected server-side with a clear 409 while the course is archived; the guarded action buttons are disabled and dimmed. Reactivation is unaffected.
  • Live read-only awareness. A course page polls its status (~1/min) and, if the course is archived in another tab or by another user, flips to read-only without a manual refresh.

Fixed

  • The dashboard archived-courses list no longer only offered Re-activate/Delete with no way to actually open an archived course.
  • Version 2026.7.3.post39; Service Worker cache gitpulse-v310.

[2026-07-11] - Semester-end auto-archive actually runs now 2026.7.3.post38

Fixed

  • Semester-end auto-archive actually runs now. Course Settings shows an "Auto-archive due / Auto-archives " badge and a "Skip automatic archiving" toggle, and the dashboard hints that ended courses will be archived - but nothing performed it since the scheduled sweep layer was removed (there is no cron/scheduler daemon). The sweep is back and runs opportunistically: the staff dashboard enqueues it at most once per hour (Redis lock), and the worker archives every active course whose semester ended more than the grace period ago, unless the course opted out via "Skip automatic archiving". Idempotent and fire-and-forget - never blocks the dashboard; reactivating a course opts it out of the next sweep, so an archive is easily reversible.
  • Dashboard hint copy is now truthful: an auto-enabled ended course reads "being auto-archived"; an opted-out one reads "auto-archive is turned off - archive manually".
  • Version 2026.7.3.post38; Service Worker cache gitpulse-v309.

[2026-07-11] - An explicitly chosen theme no longer flips to the system theme on its own 2026.7.3.post37

Fixed

  • An explicitly chosen theme no longer flips to the system theme on its own. The client trusted the account's rendered data-theme-preference over the local choice. When the account was still on the ambiguous system default (never overridden server-side, or an earlier persist POST failed) while this browser held an explicit light/dark, a reload re-resolved to system and followed the OS - so an explicit dark could switch to light by itself. The bootstrap now keeps a local explicit choice over a server system default and reconciles the account (re-persists) once the DOM is ready. An explicit light/dark set on another device still syncs across devices.
  • Version 2026.7.3.post37; Service Worker cache gitpulse-v308.

[2026-07-11] - A week with no expected checks no longer shows 100% + no skeleton avatar swap 2026.7.3.post36

Fixed

  • A week with no expected checks no longer shows 100%. The weekly compliance score for a week before the curriculum introduces any check was stored as a vacuous 1.0 (nothing to fail = "100%"), so the Weekly Breakdown showed e.g. Week 1 = 100% while its heatmap row was all-N/A. Such weeks now render as N/A ("-") for the overall score AND every category, and the trend line skips them - consistent with the heatmap. Applies to existing and future data.
  • The Profile skeleton avatar now matches the loaded avatar. During the skeleton phase the avatar was a bespoke initials circle and swapped to the photo once the content loaded. The skeleton now renders the real avatar via the same shared data-avatar-fallback component the dashboard/topbar use (photo, falling back to initials on error), so there is no swap.

Changed

  • The theme-switch reveal is slower and gentler (440 ms -> 620 ms) so the full-screen wipe feels smooth instead of an abrupt snap.
  • Version 2026.7.3.post36; Service Worker cache gitpulse-v307.

[2026-07-10] - Fix: the course Teachers page no longer renders as an empty broken shell 2026.7.3.post35

Fixed

  • The course "Teachers" page no longer renders as an empty broken shell. Its page script declared const GP at the top level, which collided with the global const GP that core.bundle.js already declares (SyntaxError: Identifier 'GP' has already been declared). A parse error aborts the whole file, so teacherManager() was never registered and every Alpine expression on the page failed. The script is now wrapped in an IIFE like every other page script. Added a regression test that fails if any pages/*.js declares GP at global scope.
  • Version 2026.7.3.post35; Service Worker cache gitpulse-v306.

[2026-07-10] - The page no longer looks stuck loading once it's done + immutable compliance history 2026.7.3.post34

Fixed

  • The page no longer looks stuck loading after it has fully rendered. When an avatar host black-holed the connection (TUKE VPN down, GitLab unreachable), the <img> fired neither load nor error and stayed pending forever - so the browser tab kept its loading spinner spinning even though every bit of page content was already on screen. Avatars now have an 8 s load watchdog: on expiry the request is aborted and the initials fallback is shown, so the tab stops spinning.
  • Compliance history is now immutable. Backfill used to recompute every past week from the current database state and upsert over it, so when GitLab data was later deleted (a branch or MR removed) a re-sync silently rewrote a student's historical score downward. Backfill now only fills weeks with no snapshot yet; a week that was already recorded stays as it was.
  • No more phantom "Week 21" after the semester ended. Weekly compliance snapshots are capped at the teaching window, the exam period closes via an effective end date even without an explicit end, and the charts (course trend line and the student trend/heatmap/radar) clip weeks past the window.
  • Faster pages: pruned the redundant metric_snapshots backlog (migration 010). post33 capped future growth but left ~480k historical duplicate rows - the real cause of the intermittent multi-second page loads. The migration keeps the newest row per (team, student, UTC week) and deletes the rest; the latest score is unchanged.
  • Charts are readable on the light theme. The compliance donut, the "Skill Profile" radar tooltip, and several chart tooltips and grids were hardcoded to a dark surface / near-white grid (dark-on-dark / invisible on the light card). They now take their colours from the shared, theme-aware Chart.js defaults.
  • Student page polish + smaller fixes: the loading skeleton matches the loaded MR cards and section headers 1:1; the commit table keeps the "Merge commit"/branch badges in fixed columns; the "Total Active Weight" block is restructured into a dedicated summary band; the top progress bar no longer sweeps backwards while fading out; a guarded modal Save button is no longer briefly clickable on open; the Run Checks checkbox focus outline is no longer clipped.
  • Version 2026.7.3.post34; Service Worker cache gitpulse-v305.

[2026-07-08] - Compliance snapshots no longer grow without bound (slow pages) 2026.7.3.post33

Fixed

  • Compliance snapshots no longer grow without bound (slow pages). Every sync / Run Checks INSERTed a brand-new metric_snapshots row per student - the unique key carries computed_at, so nothing ever deduped. Production had 506k rows, ~882 per student, pushing the per-group "latest snapshot per student" aggregate to 1.2 s (×8 groups on a course page) and making the student page load 800+ history rows. A recompute now keeps exactly one snapshot per (team, student, ISO week): it deletes the rows it supersedes - scoped to the same team, same week, and only the students being recomputed, so a partial recompute never drops another student's week - then writes the fresh ones in the same transaction. Current score and the weekly history chart are unchanged.
  • Version 2026.7.3.post33; Service Worker cache gitpulse-v304.

[2026-07-08] - Activity feed no longer reports every scope as a whole-group action 2026.7.3.post32

Fixed

  • "Follow the latest updates" no longer reports every scope as a whole-group action. A course-wide Run Checks fans out into one run_checks job per group, and only sync bursts were aggregated - so the feed listed each group's check separately and read as if the whole group ran repeatedly. Run-checks bursts now fold into a single "Ran checks for N groups" entry (same 5-minute clustering as "Synced N groups"), and a project-scoped run reports "Project checks completed" instead of the generic label with the group name attached.
  • Version 2026.7.3.post32; Service Worker cache gitpulse-v303.

[2026-07-08] - No phantom "Sync completed!" toast when running checks 2026.7.3.post31

Fixed

  • No more phantom "Sync completed!" toast when running Run Checks. A checks recompute emits an SSE update; the sync-stream saw no sync work but a stale _syncWasActive flag still fired _completeCourseSync() → a "Sync completed!" toast even though nothing synced. The terminal sync toast is now gated on a new _sawRealSyncRunning flag set only when a poll/SSE tick actually observes a sync running/finishing.
  • Removed the duplicate "Data updated" info toast after a job completes - the global job bar already shows the terminal toast, so the extra one just stacked a second toast.
  • Version 2026.7.3.post31; Service Worker cache gitpulse-v302.

[2026-07-08] - Student page skeleton adds the email row 2026.7.3.post30

Fixed

  • The student page loading skeleton now includes the email row in the profile identity block (mail icon + address), matching the loaded page for teachers/admins (can_edit). It was the one identity field missing between the username and the "View on GitLab" link, so the header shifted slightly when real data arrived.
  • Version 2026.7.3.post30; Service Worker cache gitpulse-v301.

[2026-07-08] - Per-group "Recomputing…" badge during Run Checks 2026.7.3.post29

Added

  • "Run Checks (all groups)" now shows a per-group "Recomputing…" badge on each group row while it runs, so the course page gives visible per-row feedback like a sync does (previously only the global progress bar moved). The badges clear when the rows refresh on completion.
  • Version 2026.7.3.post29; Service Worker cache gitpulse-v300.

[2026-07-08] - R09 (MR Approved) no longer breaks after merge - scores stay correct 2026.7.3.post28

Fixed

  • R09 (MR Approved) no longer drops to a false FAIL after an MR merges - this silently lowered compliance scores across all merged MRs. GitLab clears an MR's /approvals list once it merges and its branch is deleted, so a later sync overwrote the stored approvals with 0; with min_approvals >= 2 every merged MR then failed R09 (e.g. a student 0.860 -> 0.825, group averages fell too). R09 now counts distinct approvers from the permanent "approved this merge request" system notes (union the live list, netting any later "unapproved"), which survive the merge - so scores stay correct and are immune to the post-merge clobber. Re-run checks / sync to restore affected scores. Regression tests added.
  • Version 2026.7.3.post28; Service Worker cache gitpulse-v299.

[2026-07-08] - Student skeleton 1-to-1, single job-bar X, double top-bar fix 2026.7.3.post27

Fixed

  • Student page skeleton now matches the loaded page 1-to-1. The Compliance Progress loading state was badly out of sync: Score Trend was a tiny bar in a big empty box, Skill Profile a bare circle, and the "Check Progress by Week" heatmap + threshold pills + week-over-week summary were missing. The skeleton now fills the Score-Trend chart box, renders the threshold pills, a radar placeholder with the week label and the Current-Score / vs-Last-Week / Weakest-Area summary, plus the full R01–R13 × weeks heatmap.
  • Global job bar shows exactly one trailing control instead of two ambiguous X's: a running job shows the cancel-X (which confirms before stopping the process), a finished/failed job shows a dismiss-X to close the bar. Consistent in the full render and the in-place update.
  • Top progress bar no longer fires twice on a slow navigation. The network-activity reconciler used to force-finish the bar (snap to 100% + clear the hand-off state) during a full-page navigation - which has no fetch in flight - so the destination page popped a new bar from zero. It now skips finishing while navigating, so one continuous bar spans old page → new page.
  • Version 2026.7.3.post27; Service Worker cache gitpulse-v298.

[2026-07-07] - Strip the inert maintenance-jobs panel shell 2026.7.3.post26

Removed

  • Stripped the inert "scheduled / maintenance jobs" panel shell left over from post24. The jobs grid was no longer rendered, so this was dead scaffolding: removed the admin endpoints (/jobs/trigger, /jobs/retry, /scheduled-jobs, /jobs/active-scheduled), the empty admin_scheduled_registry, run_tracked_scheduled + sweep_tracking, and the front-end trigger/poll/SSE code in the admin-system bundle. Kept the live background-job history table, cancel, health, cache and stats. The useless per-row "Retry" button was removed with its dead endpoint.

Changed

  • custom_check schema: extracted a shared validate_rule_type helper so the create/update validators no longer duplicate the rule-type check.
  • Version 2026.7.3.post26; Service Worker cache updated to gitpulse-v297.

[2026-07-07] - Distinguish a template fork with no student commits 2026.7.3.post25

Added

  • Project detail now distinguishes a template/fork with no student commits yet from a genuinely empty repository. When the repo has template-author commits but no student has pushed their own work, the "Issues to Address" card shows a dedicated "Template / fork - no student commits yet" state (instead of the misleading "No repository data yet"), the Contributors card says "Only template authors so far", and the Commits stat renders "—" instead of a misleading "0". Detected consistently in both the full-page render and the live-data refresh. Slovak translations added.
  • Version 2026.7.3.post25; Service Worker cache updated to gitpulse-v296.

[2026-07-07] - Removal of the dead scheduled/periodic sweep-job layer 2026.7.3.post24

Removed

  • Removed the dead "scheduled / periodic sweep" job layer. There is no recurring scheduler and the admin maintenance-job catalog is empty, so these jobs could never fire: periodic_llm_analysis, periodic_telemetry_snapshot, sweep_dlq_auto_resurrect, cleanup_old_events, cleanup_old_email_logs, sweep_email_queue, sweep_auto_archive_ended_courses, sweep_unprocessed_events. Deleted their modules (sweep_jobs, sweep_cleanup, sweep_maintenance, sweep_events), the periodic functions in sweep_telemetry, the orphaned _delete_in_batches helper, the _FEATURE_FLAG_MAP entries, the activity-feed scheduled:* labels, and the admin retry legacy map. Tests updated.
  • The event/sync-driven jobs are unchanged: refresh_project_gitlab_intel (webhook intel refresh), sync_single_team_members (push member sync), and telemetry_snapshot_for_team. Email delivery is unaffected (enqueue_email enqueues deliver_email directly).

Note

  • Recovery paths that only the deleted jobs provided (DLQ resurrect, event retention, unprocessed-event replay, email-send retry) were already non-functional without a scheduler; a manual re-sync pulls current state, so no data is lost. The empty maintenance-panel shell stays inert and can be removed in a follow-up.
  • Version 2026.7.3.post24; Service Worker cache updated to gitpulse-v295.

[2026-07-07] - Logo animation fix and documentation cleanup 2026.7.3.post23

Fixed

  • The logo's ECG-pulse animation no longer jumps back to the start of the trace when the cursor leaves. Hovering used to swap the pulse animation-duration (3.8s -> 0.95s); reverting it on mouse-leave re-mapped the animation's elapsed time onto the longer timeline, snapping the moving segment backward. Hover now only brightens the pulse at a constant speed, so it continues smoothly from its current position.

Changed

  • Renamed app/workers/sweep_ci.py -> app/workers/sweep_events.py (the name was a leftover from the removed CI-failure feature, but it now only holds the webhook-event recovery sweep). Pure rename - the app.workers.sweep.* RQ string paths still resolve via re-exports.
  • Removed references to the three deleted sandbox features from the current docs (API spec, data model, requirements, rollout, checklist mapping, architecture tree, developer pages). Historical CHANGELOG/archive, thesis narrative, and migration-history pages are unchanged.
  • Version 2026.7.3.post23; Service Worker cache updated to gitpulse-v294.

[2026-07-07] - Complete removal of three unused sandbox features 2026.7.3.post22

Removed

  • Removed three unused sandbox features entirely (feature-flagged off by default, never enabled in production, zero rows in their tables): the git conflict simulator, the Docker build verification module, and the CI-failure scenario generator. Deleted their services, workers, models, API routes, templates, JS (_parts + rebuilt bundles), feature flags and orphan i18n strings; the course-settings config columns and the (empty) feature tables are dropped by new migration 009_drop_sandbox_features (idempotent IF EXISTS). No behaviour change for users.

Changed

  • Version 2026.7.3.post22; Service Worker cache updated to gitpulse-v293.

[2026-07-07] - "Issues to Address" empty state no longer vanishes + reverted the across-reloads cache (it flickered / showed stale data) 2026.7.3.post21

Fixed

  • The "Issues to Address" empty-state card no longer vanishes a second after load. On a live-data poll where the server can't determine the repo state it returns an empty issues_html (by design: "keep the current card"), but the client's legacy fallback then hid the card with d-none. Removed that dead legacy path - the persistent server-rendered empty state now stays.
  • Reverted the stale-while-revalidate lazy-content cache (post19/post20). It painted the last-seen content on refresh, but after a background sync it showed stale/empty content and flickered ("appears then disappears", "nothing shows after sync"). Pages now always render fresh from the server (a brief skeleton on refresh is the trade-off), so displayed data is always correct and never disappears. Stored data (members / contributors / stats) renders from the DB snapshot regardless of GitLab token state.

Changed

  • Version 2026.7.3.post21; Service Worker cache updated to gitpulse-v292.

Fixed

  • The lazy-content cache (post19) now actually engages. The cache-write step rejected any page whose real content contained a nested client-filled skeleton (charts, analysis cards, ...), so the page was never cached and F5 kept flashing the full-page skeleton. At htmx:afterSwap the content is always the real server response (the initial skeleton is already gone), so that guard was wrong - removed it.

Changed

  • The "GitLab token invalid" item in "Items that need your attention" now links to the token docs (/docs/gitlab/tokeny/) explaining how to rotate it, instead of System Settings - the token is a server env var, not an in-app setting, so System Settings could not fix it.
  • Version 2026.7.3.post20; Service Worker cache updated to gitpulse-v291.

[2026-07-06] - Lazy pages paint instantly on refresh (cache across reloads), not a skeleton 2026.7.3.post19

Changed

  • Lazy-loaded dashboard pages now paint instantly on a refresh / return instead of flashing the loading skeleton every time. A stale-while-revalidate cache (_parts/core/50-lazy-content-cache.js) stashes the last-seen rendered content per URL in sessionStorage (per-tab, memory-only - never written to disk, matching the no-store policy on /dashboard) and repaints it the instant the page loads, while htmx fetches fresh content in the background and swaps it in. Applies to every .gl-lazy-content[hx-trigger*="load"] page (course / team / project / student / dashboard / admin). The entrance reveal is suppressed on the revalidation swap (via data-swr-painted) so there is no double-reveal flash; cache entries are size-capped and 10-minute TTL-bounded. This is the client-side answer to "F5 re-runs everything" - /dashboard responses stay no-store (auth-page security).
  • Version 2026.7.3.post19; Service Worker cache updated to gitpulse-v290.

[2026-07-06] - No live GitLab calls while rendering pages, dashboard shows failed syncs and an invalid token 2026.7.3.post18

Fixed

  • The student page no longer makes live GitLab calls while rendering. It fetched up to 12 pipeline coverage details + a user-avatar lookup on every load, which stalled the page (and hung on an expired 401 token). It now renders purely from stored data - coverage is filled by sync + webhooks and reused from the existing SHA-based DB fallback; the avatar comes from the stored value or the URL-pattern fallback. Data reaches every page only via sync + webhooks, never a render-time API call.
  • "Follow the latest updates" now shows FAILED jobs (e.g. a sync blocked by an invalid token). The feed queried only completed jobs, so every failure was hidden; failed jobs now appear with an alert icon and a "… failed" label.
  • The Course Settings sticky action bar no longer leaves a thin lighter strip at the very bottom - it compensated an 8px page padding, but .gl-page-content's bottom padding is 12px, so the last ~4px showed through. Now compensated to 12px.

Added

  • "Items that need your attention" surfaces an "invalid GitLab token" item when the token is rejected (read from the shared cached health flag - no live probe on render), linking to System settings so it can be updated quickly.

Changed

  • Version 2026.7.3.post18; Service Worker cache updated to gitpulse-v289.

[2026-07-06] - Group/project Sync button no longer sticks without a timer, unified failure messages 2026.7.3.post17

Fixed

  • The group / project Sync button no longer gets stuck disabled with no cooldown timer after a failed sync (e.g. an expired 401 token). Root cause: the sync registers a global job, and completing that job with an error fired the process-lock release (unlockElement), which cleared the anti-spam cooldown that had just been applied - so the countdown vanished and the button could be left blocked. unlockElement no longer clears an active cooldown, nor re-enables a button a cooldown is still holding. The countdown now survives and the button self-recovers on every page (the course page only escaped the bug via its early token-return, which never registers a job).
  • Sync-failure messaging is now consistent across course / group / project. A shared GP.syncErrorMessage(err) returns the detailed "GitLab API token is invalid or expired (401)…" text on a 401 (already translated) instead of the bare "Sync failed" the group / project pages showed while the course page showed the full reason.

[2026-07-06] - Cached GitLab panels, no duplicate banner on the group page, simplified & unified sync 2026.7.3.post16

Fixed

  • Project sync wrongly reported "Group sync completed" on success - it now says "Sync completed", matching the entity being synced (group vs project).
  • The top loading strip no longer stays pinned at 100% when a new request begins during its fade-out. start() now resumes a finishing bar as one continuous indicator instead of leaving it stuck at the end and then popping a fresh bar.

Changed

  • The "Data may be outdated" webhook/staleness banner is no longer duplicated on the group (team) page - it is a course-level notice and already shown on the course page.
  • Group sync stripped of its dead sync-modal code. That modal was removed from the templates long ago (the global job bar owns all sync UI now), so its getElementById calls were no-ops; group sync is now bar-only like project sync and both use the same generic "Sync completed" / "Sync failed" messages.
  • GitLab intelligence panels (protected branches, contributors, repository statistics, milestones, releases, labels) are now under HTTP caching. Their responses carry Cache-Control: private, max-age=300 + a content-derived ETag, and the project page revalidates them with a conditional request (cache: no-cache) instead of the old no-store full re-download - so an unchanged snapshot returns a bodyless 304 and repeat visits/refreshes come from the browser cache.
  • Version 2026.7.3.post16; Service Worker cache updated to gitpulse-v287.

[2026-07-06] - Sync button no longer hangs on "Wait 1s" on other pages, smooth banners, GitLab-spec tables, cross-tab work 2026.7.3.post15

Fixed

  • The Sync / Run Checks button no longer freezes on "Wait 1s" after a failure on the team, project and per-group pages. Root cause: a race between the anti-spam cooldown and setButtonLoading over dataset.originalText when a request fails fast (< 400 ms, i.e. a 401 token rejection) - the deferred min-visible-duration reset fired mid-countdown and deleted the saved label, so the cooldown could not restore the button. The course page escaped it only via its early token-guard; every other page went through the racing path. Fixed in the shared core, so it is fixed on every page at once.
  • The failure cooldown countdown now shows on every sync trigger (course Sync All, per-group sync, team sync, project sync), not just some buttons.

Changed

  • Banners (the "Data may be outdated" / GitLab-token / sync-failed cards) now ease open and closed (animated height + opacity) instead of popping in and shoving the page content down. A dismissed banner now STAYS dismissed across tab switches, page reloads and other browser tabs (persisted in localStorage).
  • Data tables now follow the official GitLab Pajamas table spec 1:1: header and body cells both use 16px (gl-p-5) padding, and the condensed variant is exactly half (8px).
  • With the app open in several tabs, a single visible "leader" tab now owns the sync pollers / SSE streams (fail-open localStorage lease); the other tabs mirror state via storage events. This removes duplicate /sync/active requests, out-of-step progress bars and duplicate completion toasts across tabs. A lone tab always leads, so a single tab is never affected.
  • Version 2026.7.3.post15; Service Worker cache updated to gitpulse-v286.

[2026-07-04] - Cooldown no longer hangs on "Wait 1s", Slovak translation, on all Run Checks buttons 2026.7.3.post14

Fixed

  • The anti-spam button cooldown no longer hangs on "Wait 1s". The 200 ms countdown tick self-heals: when the time is up it clears the cooldown and restores the button, so a lost/throttled setTimeout can never leave it pinned.
  • "Wait" is now translated (Slovak Počkajte), so the cooldown countdown is no longer English-only on the Slovak UI.
  • The failure cooldown now covers every Run Checks button (course / team / project, via the shared runScopedChecksProcess), consistent with the Sync buttons.

Changed

  • Version 2026.7.3.post14; Service Worker cache updated to gitpulse-v285.

[2026-07-04] - Top loading strip: bulletproof reconcile against real network activity 2026.7.3.post13

Fixed

  • Top loading strip - a fundamentally different fix. Instead of chasing every way the event-driven pending counter can get stuck (a prevented submit like Import with an empty URL, missed/duplicated htmx events, a click that starts a "navigation" that never navigates), the bar is reconciled against REAL network activity: window.fetch and XMLHttpRequest are wrapped so a request-lifecycle counter (.finally / loadend always fire) tracks what is genuinely in flight, and a 250 ms reconciler finishes the bar ~1.2 s after the last real request completes - regardless of what the fragile event counter believes.

Changed

  • Version 2026.7.3.post13; Service Worker cache updated to gitpulse-v284.

[2026-07-04] - Sync button frees without refresh, top loading strip no longer hangs 2026.7.3.post12

Fixed

  • The Sync / Run Checks button now frees itself WITHOUT a page refresh. The ghost-expiry guard only ran while a job was visible in the bar, so a hidden ghost never got expired and the button stayed locked until a manual refresh. The guard now runs while ANY job exists; a ghost with no real backend job_id is retired ~15 s after load, and team/project syncs no longer inherit the 45-min course maxAge cap.
  • The top loading strip no longer hangs at ~99 %. A race let the generic htmx handler count a .gl-lazy-content request but skip its decrement, leaving the bar pinned for the full 120 s stuck-timeout. Lazy requests are now counted exclusively by the lazy bridge.
  • The job bar's grow-in is smoother (lowered target height so the transition spans the animation).

Changed

  • Cross-tab toasts are suppressed in background tabs; queued completion toasts are de-duplicated by job.
  • Version 2026.7.3.post12; Service Worker cache updated to gitpulse-v283.

[2026-07-04] - Neutral avatar, fixed activity feed, smooth banners 2026.7.3.post11

Fixed

  • The Sync / Run Checks button no longer sticks disabled on project & group pages (or any scope). The global job bar reconciled only course-level ghosts against /sync/active; a team-/project-scoped sync ghost restored from localStorage was skipped, so it kept the button process-locked for its full maxAge (up to 45 min) even when the server reported nothing active - and it survived refreshes because localStorage persists. Reconciliation now covers team/project ghosts too (matched by their owning course).
  • Avatar fallback initials are no longer on a purple disc - the top-bar and dashboard-greeting avatars now use a neutral slate (--gl-gray-700).
  • The "Follow the latest updates" feed no longer loses entries. A background refresh (a) never replaces a populated feed with the "no activity" empty state (a transient builder hiccup or a brief quiet window used to wipe every visible row), and (b) preserves the "Show all" expand state instead of collapsing back to 5.
  • Status strips no longer jolt the page when they appear. The global job bar eases open via a max-height transition + @starting-style (not a keyframe - the bar's deterministic removal contract is preserved), and the health banner grows in by max-height instead of sliding a full-height box in with translateY, so the content below makes room smoothly.

Changed

  • Version 2026.7.3.post11; Service Worker cache updated to gitpulse-v282.

[2026-07-03] - Sync button no longer sticks disabled after a refused sync 2026.7.3.post10

Fixed

  • The Sync / Run Checks button no longer sticks disabled after a refused sync. The global job bar marks a click optimistically as a "connecting" job before the server confirms a run; when the sync is refused (invalid/expired GitLab token, 401) or the page restored a stale pre-POST snapshot, that placeholder had no backend job behind it yet kept the button process-locked for the full 5-45 min staleness window - and inconsistently across project / team / course. The staleness guard now retires a "connecting" job that never got a real backend job_id after ~15s, so the button frees up quickly and identically on every scope. A genuine (even slow-queuing) sync always carries its job_id, so it is never affected.

Changed

  • Version 2026.7.3.post10; Service Worker cache updated to gitpulse-v281.

[2026-07-03] - Consistent avatar fallback across surfaces 2026.7.3.post9

Fixed

  • Avatar fallback is now consistent across surfaces. The dashboard greeting avatar used the old pattern (photo over a grey disc with a hidden letter), so it diverged from the top-bar avatar (fixed in post6) - one could flash an empty grey circle while the other showed the branded initial. The greeting avatar now uses the same layering (the initial is the always-present base and the GitLab photo overlays it), so both degrade identically.

Changed

  • Version 2026.7.3.post9; Service Worker cache updated to gitpulse-v280.

[2026-07-03] - Remove the standing invalid-token banner from project/group 2026.7.3.post8

Changed

  • Removed the persistent #gitlabTokenBanner that post7 added to the project and group pages. A bad/expired GitLab token no longer shows a standing banner there; the token error surfaces only when you actually run an action that needs it (Sync / Import), via the existing global progress bar + toast - one message, no standing clutter. (The project intel-card loading shimmer from post7 is kept.)
  • Version 2026.7.3.post8; Service Worker cache updated to gitpulse-v279.

[2026-07-03] - Consistent project/group behavior with the course page on an invalid GitLab token 2026.7.3.post7

Fixed

  • Project and group pages now handle an invalid/expired GitLab token the same way the course page always has: a single persistent #gitlabTokenBanner explains the 401 once, and the Sync / Run Checks buttons stay clickable instead of sticking disabled.
  • Clicking Sync with a known-bad token now short-circuits (no doomed background job that left the button stuck and double-reported the error as both a banner and a toast) - it surfaces one message.
  • Project GitLab-intelligence cards (Protected Branches / Contributors / Repository Stats) show a loading shimmer while a live fetch is in flight, instead of flashing a premature "No contributors yet" empty state.

Changed

  • Version 2026.7.3.post7; Service Worker cache updated to gitpulse-v278.

[2026-07-03] - Version consolidation and final release checks 2026.7.3.post6

Fixed

  • Health banner: the "GitLab server is currently unreachable" notice no longer reappears after you dismiss it (a successful app-health poll was silently clearing the dismissal flag it shared with the app-health banner), and its "Last connected" timer now ticks live instead of freezing at the value it had when first shown.
  • Top-bar avatar degrades to the user's initials instead of flashing an empty grey disc while the GitLab profile photo streams in or when GitLab is unreachable.

Changed

  • Version 2026.7.3.post6; Service Worker cache updated to gitpulse-v277.
  • Verified workspace code quality, ran full unit tests suite, and completed final release consolidation checks.

[2026-07-03] - Simplified category assignment in project members 2026.7.3.post5

Refactored

  • Simplified member category assignment in _members_table.html to eliminate duplicating tree branch logic for excluded members (consistent with the team details view).

Changed

  • Version 2026.7.3.post5; Service Worker cache updated to gitpulse-v276.

[2026-07-03] - Simplified empty state logic in project details 2026.7.3.post4

Refactored

  • Combined and simplified redundant empty_state macro call trees in project detail sub-sections: _docker_verification.html, _ci_scenarios.html, and _conflict_challenges.html.

Changed

  • Version 2026.7.3.post4; Service Worker cache updated to gitpulse-v275.

[2026-07-03] - Redundant cohort filtering cleanup and category assignment simplification 2026.7.3.post3

Refactored

  • Cleaned up redundant legacy Jinja fallback loop in _issues_to_address.html template. Now uses python backend computed scored_students exclusively, reducing file complexity.
  • Simplified member category assignment in _detail_content.html to eliminate duplicating tree branch logic for excluded members.

Changed

  • Version 2026.7.3.post3; Service Worker cache updated to gitpulse-v274.

[2026-07-03] - Refactored duplicate file tree buttons in student details 2026.7.3.post2

Refactored

  • Extracted duplicate GitLab diff-tree button blocks within the student details component template (student.html) into a single Jinja2 macro diff_file_tree_item, reducing duplicate markup in recursive file-tree nodes and diff body lists.

Changed

  • Version 2026.7.3.post2; Service Worker cache updated to gitpulse-v273.

Refactored

  • Extracted duplicate GitLab evidence link blocks within the AI Teamwork Analysis page template (_ai_analysis.html) into a single Jinja2 macro render_evidence_link, eliminating duplicate blocks across Strengths, Potential Issues, and Teamwork Quality sections.

Changed

  • Version 2026.7.3.post1; Service Worker cache updated to gitpulse-v272.

[2026-07-03] - Large-scale refactoring of Course Settings into modular partials 2026.7.3

Refactored

  • Decomposed and refactored the entire Course Settings template layout into modular partial includes (_basic_settings.html, _webhook_settings.html, _compliance_settings.html, _weekly_deadlines.html, _sandbox_settings.html, _ai_settings.html, _import_export_settings.html). This reduced the main course_settings.html template code from 580 lines down to under 100 lines.

Changed

  • Version 2026.7.3; Service Worker cache updated to gitpulse-v271.

[2026-07-03] - Settings refactoring and alert layout fix 2026.7.2.post17

Fixed

  • Layout wrapping of the "Scope cascade" alert box in the Course Settings page, preventing narrow-width wrapping.

Refactored

  • Extracted "Semester Settings" layout sections into reusable Jinja macros and partial templates (_semester_settings.html and _settings_form_macros.html) to minimize duplicate markup.

Changed

  • Version 2026.7.2.post17; Service Worker cache updated to gitpulse-v270.

[2026-07-02] - Green lint restored, students-only member tables, display-name project path 2026.7.2.post16

Fixed

  • Restored the green lint stage on main (the post15 commit failed CI): ruff format reformatting and a member_sync_service.py mypy fix (a reused loop variable was assigned an Optional).
  • Members and Group Members tables show STUDENTS only - never teachers, mentors, or a Project Manager. The issue-activity cohort could pull in a PM who authored issues; both lists now drop anyone flagged teacher/mentor.
  • The project header shows a GitLab-style display path (... / ApI 7 (Maťaš - Antypenko) / Chekanov ... - Míny) instead of the raw slug (.../api-7-ma-a-antypenko/mines-12770).
  • Version 2026.7.2.post16; Service Worker cache unchanged (gitpulse-v269).

[2026-07-02] - No contradictory sync toasts, issue-based cohort, cleaner Issues header 2026.7.2.post15

Fixed

  • Course "Sync All" no longer shows a contradictory pair of toasts. The optimistic green "Sync started..." fired before the request resolved, so a refused sync (invalid token / GitLab offline) showed "started" AND "failed" at once. The "Connecting to GitLab..." global bar already signals the start and resolves to progress or a single error - the premature toast is removed on the course-detail and course-analytics paths.
  • The project member cohort now also counts issue participation (author / assignee / co-assignees), not just merge-request activity, so a teammate who opened issues but has not authored an MR yet is no longer dropped ("4 shown but 5 on the team").
  • Issues to Address header shows only the actionable High-priority count; the Medium/Low pills read as a meaningless faint number ("8"/"45") next to it and were redundant with the chart + legend below.
  • Version 2026.7.2.post15; Service Worker cache gitpulse-v269.

[2026-07-02] - DB-backed project data and unified process progress 2026.7.2.post14

Fixed

  • Project detail and live refresh no longer fetch GitLab directly to decide members/statistics/contributors/protected branches. They read the stored gitlab_intel snapshot, derive older projects from stored MR activity, and refuse to fall back to the whole subgroup for multi-project teams. This prevents phantom students and wrong Issues-to-Address badges when the GitLab token is invalid or GitLab is offline.
  • Manual project sync now upserts the project's GitLab members into the owning group before issues/MRs/notes are processed, so direct project members no longer disappear until a full group sync runs.
  • Sync and Run Checks progress/error handling is shared across project, group, and course scopes: invalid-token/refused requests now finish as visible global-bar errors as well as toasts, and Run Checks uses one SSE/polling path.
  • Webhooks now enqueue a debounced refresh of the stored GitLab intelligence snapshot, keeping project intelligence DB-backed through sync/webhook mechanics.
  • Version 2026.7.2.post14; Service Worker cache gitpulse-v268.

[2026-07-02] - Members "Actions" header alignment and readable Issues pills 2026.7.2.post13

Fixed

  • Members table: the "Actions" header is right-aligned so it sits above its row buttons (the eye toggle + View) instead of drifting to the far left of the column. Applies to the live table and its loading skeleton.
  • Issues to Address header pills: the Medium (amber) and Low (blue) counts were getting lost against the dark card header - only two faint numbers ("45") were visible. They are now bold, outlined chips so each count and its label stay readable next to the High (red) pill.
  • Version 2026.7.2.post13; Service Worker cache gitpulse-v267.

[2026-07-02] - Sync button no longer sticks disabled on a refused sync 2026.7.2.post12

Fixed

  • The project Sync button no longer stays disabled (needing a refresh) when a sync is refused - e.g. an invalid/expired GitLab token returning 503. The handler registered a phantom "sync running" slot before the request was confirmed; a refused request never cleared it, so the process-lock kept the button disabled. The optimistic slot is removed - the real progress slot is registered only after the server returns a job id, and on failure the button re-enables with a clear message.
  • Version 2026.7.2.post12; Service Worker cache gitpulse-v266.

[2026-07-02] - Project members table shows the project, not the whole subgroup 2026.7.2.post11

Fixed

  • The project members table and its Issues-to-Address score cohort now scope to THIS project's students instead of every student in the parent subgroup. A GitPulse "team" maps to a GitLab subgroup that can hold many project repos, so one project's table was showing the whole section (~50) instead of the real ~5. When the live member fetch is unavailable (expired token / GitLab maintenance) the page recovers the real roster from stored data - the roster saved on the last sync, else derived from stored merge-request activity (authors, assignees, reviewers, approvers) - so it is correct even before a fresh sync.
  • Scoring counts a student as a project member regardless of GitLab membership source (direct OR inherited): a real contributor is no longer dropped from the group score just because their team-level source is 'inherited'. The score reflects the actual project cohort, not only the displayed rows.
  • Version 2026.7.2.post11; Service Worker cache unchanged (gitpulse-v265) - backend-only change.

[2026-07-02] - Alignment fix and warnings in skeletons 2026.7.2.post10

Fixed

  • Fixed Actions column alignment in both the loading skeleton and live members table to prevent buttons overflow.
  • Added GitLab active members warning note to student views of skeletons and live tables.
  • Version 2026.7.2.post10; Service Worker cache gitpulse-v265.

[2026-07-02] - Simplification of group and project members tables 2026.7.2.post9

Fixed

  • Simplified the Group Members and Project Members tables by removing the redundant "Source", "Role", and "Status" columns, and removing the filter bar (since only database student members of the group/project are displayed).
  • Colored the "Pipelines" column on the Group Members table to clearly identify success (green) vs failures (red), and added descriptive tooltips explaining the counts.
  • Version 2026.7.2.post9; Service Worker cache gitpulse-v264.

[2026-07-02] - Intelligence panels survive an offline GitLab 2026.7.2.post8

Added

  • The project GitLab Intelligence cards (protected branches, contributors, repository statistics) now persist a snapshot on every sync (gitlab_projects.gitlab_intel) and the page falls back to it per-card when the live GitLab fetch fails. These were the only data on the page with no stored fallback, so they were the only thing that broke to "GitLab is unreachable" when the token lapsed - now they keep showing the last-synced values (useful across the July 3-5 GitLab maintenance window).

Fixed

  • The intelligence cards no longer wipe themselves to "GitLab is unreachable" on a failed live refresh - the last snapshot stays visible.
  • The Groups table no longer overflows its card: deleted-group rows carry wide "Restore" / "Remove from deleted" buttons that forced the whole fixed-layout table to scroll sideways; those buttons now wrap within the actions column (active rows untouched).
  • Version 2026.7.2.post8; Service Worker cache gitpulse-v263; Alembic migration 008 adds the gitlab_intel column.

[2026-07-02] - Styled tooltips, sticky dismissals and aligned rows 2026.7.2.post7

Fixed

  • Tooltips are now always the styled component (Tippy) tooltip, never the browser's native title= popup: the btn/btn_icon macros, the info-card dismiss button, the course-settings check toggle and sync-lock save buttons, the login server-status dot, localized <time> timestamps, and the client-inserted Group row actions were all converted.
  • Dismissed banners stay dismissed: the info-card X remembers the dismissal for the session (keyed by banner + current state), so "sync failed" / "sync cancelled" / "data may be outdated" no longer reappears after navigating away and back.
  • The top loading bar and submit buttons no longer stick after a validation failure - e.g. Add Group with a 1-character name clears the progress bar and does not arm the anti-spam button-lock, so nothing hangs until a refresh.
  • New Group rows added after creation mirror the server table's responsive column classes (col-hide-sm/col-hide-md, centered Status) so they no longer shift out of alignment on narrow viewports; the first group in a course reloads to render the canonical table.
  • Added Slovak translations for the permanent-delete / "Remove from deleted" strings, returning missing translations to zero.
  • Version 2026.7.2.post7; Service Worker cache gitpulse-v262.

[2026-07-02] - Console, modals and shared role badges 2026.7.2.post6

Fixed

  • Course detail now ships valid JavaScript again: the deleted-group toggle binds correctly, Run Checks uses the shared confirmation callback flow, and the page no longer dies before exporting its actions.
  • The white avatar ring is scoped to the dashboard greeting avatar, while the topbar avatar stays borderless.
  • The user menu role pill now uses the shared role badge component and colors from User Management.
  • GitLab import and other modals reset their internal scroll position before opening, keep backdrops mounted under <body>, and do not delay the modal body scrollbar until after the first animation frame.
  • The PWA install handler no longer calls preventDefault() on Chromium's native install banner unless the custom prompt is explicitly enabled.
  • Version 2026.7.2.post6; Service Worker cache gitpulse-v261.

[2026-07-02] - Permanent deletion, user roles and layout fix 2026.7.2.post5

Added

  • Permanent deletion ("Remove from deleted") endpoints and UI actions for inactive groups and projects. Teachers/admins can now completely get rid of deleted groups or unlinked projects and all their associated records (memberships, snapshots, pipelines, MRs, commits, issues, LLM logs, and background jobs) from the system.

Fixed

  • Fixed mobile and tablet layout spacing and corners so the main content panel remains inset with 8px margins and rounded corners on small viewports rather than stretching full width.
  • Removed duplicated user avatar from the topbar dropdown menu card header.
  • Replaced the static visual-only "Coding my life" status box with a status badge showing the user's role (Admin, Teacher, Student) in the topbar user menu.
  • Removed the disabled "Deleted" button from the loading skeletons.
  • Version 2026.7.2.post5; Service Worker cache gitpulse-v260.

[2026-07-02] - Group-name validation, faster loads, opt-in dirty-check 2026.7.2.post4

Fixed

  • Add Group now validates the name: a blank or single-character value (e.g. a stray "ф") is rejected client-side and server-side (TeamCreate trims and requires >= 2 non-space characters), so you can no longer create a junk group that then vanishes.
  • Faster loads: the course sync-status endpoint and the SSE stream no longer probe GitLab over the network on each poll/connect - they read the cached token-health flag (no round-trip); the flag is still set on the first 401.
  • Dirty-check is now opt-in. Every modal with an input used to arm the "unsaved changes" guard, which fired on throwaway action modals (GitLab import). Auto-init now only covers modals marked data-dirty-check; genuine edit forms keep protection via explicit calls.
  • Version 2026.7.2.post4.

[2026-07-02] - Dismissible info-card banners 2026.7.2.post3

Added

  • Info-card banners can now be dismissed. The info_card macro takes dismissible=true and renders an X button; a delegated, CSP-safe click handler removes the card. Applied to the course "Some sync jobs failed", "Sync cancelled" and "Data may be outdated" banners.
  • Version 2026.7.2.post3.

[2026-07-02] - Restored GitLab-style layout + failed-banner flash fix 2026.7.2.post2

Fixed

  • Content-panel layout restored to the reliable GitLab model: .gl-page uses a fixed height (fills the viewport) and .gl-page-content uses flex: 1, so the panel is always full height and the content scrolls INTERNALLY (like GitLab's .js-static-panel / .panel-content-inner). The earlier max-height + flex: 0 1 auto experiment made the panel shrink to the loading skeleton ("half the page is missing") - reverted.
  • The "Some sync jobs failed" banner no longer flashes and vanishes after a second. The live update managed a generic .info-card-danger, so an SSE tick reporting 0 (its count uses a 2h window) removed the server-rendered banner. It now manages ONLY its own live card (#jsSyncFailedCard) and never touches the server banner or the token banner.
  • Version 2026.7.2.post2.

[2026-07-02] - Remaining process guard hardening 2026.7.2.post1

Fixed

  • GitLab import cancellation is no longer registered as unsaved form data; pressing Cancel stops the active import path without showing an unrelated before-unload prompt.
  • Queued CI failure scenarios and conflict challenges can now be cancelled before they mutate GitLab, and CI scenario polling only runs while rows are actually pending.
  • Retrying a failed scheduled/manual system job now creates a new tracked cancellable background job, so Follow the latest updates, Items that need your attention, and the global job bar all receive the same job id.
  • Active job checks now include the started state across sync, run-checks, import, course status, and scheduled jobs, preventing duplicate starts during the RQ handoff.
  • Live dashboard/project value flashes now use requestAnimationFrame instead of synchronous layout reads, removing another forced-reflow source from hot refresh paths.
  • Production monitoring no longer leaves Alertmanager in a restart loop; the container now starts with a valid internal absolute external URL and a YAML config that Alertmanager can parse without unsupported shell-style environment expansion.
  • Version 2026.7.2.post1; Service Worker cache gitpulse-v256.

[2026-07-02] - Cancellable processes and guarded GitLab import 2026.7.2

Fixed

  • GitLab import now rejects invalid group/project paths before streaming; typing non-ASCII input can no longer start an import or crash the import error handler.
  • Long-running processes now share a cancellable job contract: global cancel buttons stop sync, run-checks, Docker verification, scheduled jobs, LLM analysis, GitLab enrichment, frontend-only AI generation, and active imports. Cancelled jobs stay terminal, and workers no longer overwrite them with completed/failed after races.
  • Course sync and run-check status now reports cancelled groups separately and uses the newest run-check job per group, so old failed rows do not poison a later successful run.
  • Course detail SSE/logging and flash animations no longer create duplicate EventSources or forced reflow; the shell bottom spacing now matches GitLab-style content height without an empty footer gap.
  • Version 2026.7.2; Service Worker cache gitpulse-v255.

[2026-07-01] - Content panel shrinks to its content (no empty card) 2026.7.1.post5

Fixed

  • The content panel (.gl-page) now shrinks to its content on short pages (max-height instead of a fixed height) instead of always filling the viewport, so a page with only a few rows no longer shows a large empty rounded card below the table - matching GitLab's own panel. Tall pages still cap at the viewport and scroll internally.
  • Version 2026.7.1.post5.

[2026-07-01] - Token-invalid banner shows on page load 2026.7.1.post4

Fixed

  • Follow-up to post3: the "GitLab token invalid" banner now appears on page load (the SSE stream and sync-status warm the shared token-health flag with one cached probe) instead of only after the first failed click. Sync All short-circuits with a clear toast on the very first click - no "Connecting…" flash, and the button is never left in a loading state that needed a page refresh to clear.
  • Version 2026.7.1.post4.

[2026-07-01] - Banner instead of a disabled Sync button, test isolation 2026.7.1.post3

Fixed

  • The "GitLab token invalid" state no longer disables the Sync All button (it was unclear when it would re-enable and inconsistent with the still-working Import button). The banner is now the single clear explanation; clicking Sync All shows the same reason as a toast instead of a "Connecting…" bar that flashed and vanished a second later.
  • Test isolation: the shared GitLab token-health flag is reset around every test, so a mocked 401 no longer leaks into a later test and makes a guarded job skip — a failure that only surfaced in CI with live Redis.
  • Version 2026.7.1.post3.

[2026-07-01] - More resilient sync and app-wide GitLab token protection 2026.7.1.post2

Fixed

  • Sync no longer flaps ("finished in 2s, then restarted itself, in circles"). A failed sync used to re-raise into RQ's blind auto-retry, flipping the job running↔failed and making the course progress bar oscillate while the "Data may be outdated" banner popped mid-sync. Sync now uses a bounded, visible retry (3 attempts): the row stays active between attempts and shows Retrying (2/3) as a per-group RETRY 2/3 badge; the run ends with a clear "All syncs completed!" or a red "Sync finished with problems — N group(s) failed" instead of always reporting success.
  • Expanded job-history cards no longer break the table: the injected detail row was counted in the :nth-child zebra striping, flipping the shading of every row below it and painting a stripe over the detail panel. It is now excluded from the stripe/hover count.

Added

  • App-wide GitLab token protection. An invalid/expired token (401/403) is now detected the instant any GitLab call fails and shared through a cached health flag, so the whole app reacts to one failure instead of each flow independently hammering GitLab:
    • Sync, project-sync, Sync All and GitLab import are blocked up front with a clear "GitLab API token is invalid or expired (401)" message.
    • Token-using background jobs (issue/MR enrichment, membership sync, incremental project refresh) skip immediately when the token is known-bad and do not retry auth errors, stopping retry storms.
    • Recovery is automatic: any successful GitLab call clears the flag, so work resumes after a token rotation without a restart.
    • The course page shows a persistent "GitLab token invalid" banner and disables Sync All while the token is bad.
  • Version 2026.7.1.post2.

Fixed

  • Docs footer copyright corrected from 2024-2026 to 2026-2027 TUKE Košice.
  • SEO: language-switch links now use rel="nofollow" so search engines stop indexing those redirect URLs discovered on the public login page.
  • Refreshed the final stale example date in the app's course-settings help text (template + Slovak catalog kept in sync).

[2026-07-01] - Docs freshness: drop the removed Rubric feature, refresh example dates 2026.7.1

Changed

  • Removed the deleted "Rubric" feature from the Slovak teacher guides (dashboard, getting-started) so they match the English versions and the current app. Refreshed stale example dates and group paths across the GitLab, operator and developer guides to current-year examples, and updated the documented current version to 2026.7.1.

[2026-06-30] - Preload backdrop only where used, drop stray em dashes, refresh pilot dates 2026.6.30.post3

Fixed

  • Login/logout: the backdrop image was preloaded even on mobile, where the sheet paints no background, so the browser warned it was "preloaded but not used". The preload is now limited to screens at least 768px wide, where the background actually shows.

Changed

  • Refreshed the pilot-deployment guide: dates moved to 2026 and the inconsistent date formats were normalised. Removed stray em dashes from a couple of changelog lines.
  • Version 2026.6.30.post3.

[2026-06-30] - Real /favicon.ico so Google Search shows the site favicon 2026.6.30.post2

Fixed

  • Favicon in Google Search: the app served /favicon.ico as a 301 redirect to a PNG, which Google's favicon fetcher handles unreliably (the generic globe showed next to results). It now returns a real multi-resolution .ico with a 200, and the indexable login page lists it first. Google refreshes the favicon only on a home-page recrawl, so it can still take a few days to appear.

Changed

  • Version 2026.6.30.post2.

[2026-06-30] - Light-themed auth selectors/alerts, swipe-to-dismiss modals, coverage backfill 2026.6.30.post1

Fixed

  • Auth pages (login / logout): the language selector menu, flash alerts, toasts and the selected-language checkmark rendered dark on the white card when the visitor's saved theme was dark. The color-mode bootstrap now drives data-theme from the page's light lock, so every component on the auth pages renders light.
  • Test coverage reading "not reported" on student MR cards while GitLab showed a real percentage: the full team sync skipped already-finished pipelines, so test pipelines stored without coverage never backfilled. Sync now re-fetches terminal pipelines that ran tests but recorded no coverage.

Added

  • Mobile modals: the bottom-sheet drag handle is now interactive - swipe it (or the modal header) downward to dismiss. The gesture engages only from the top region so it never interferes with scrolling the modal body.

Changed

  • Version 2026.6.30.post1.

Fixed

  • Worker Redis configuration: resolved a RuntimeError during event processing tasks by initializing the shared Redis connection pool on RQ worker startup.
  • Course settings import script: removed obsolete imports and calls to gaming detection and rubric engines to prevent errors.
  • Observability: set OTLP tracer endpoint in development to point to the local Jaeger collector, enabling trace visualization locally.
  • Monitoring documentation: updated Slovak and English operator guides with production URLs, ports, and Caddy Basic Auth credentials for manual verification.

[2026-06-29] - Fix dev environment, Redis authentication, and Grafana redirects 2026.6.29.post12

Fixed

  • Dev Startup script (start-dev.ps1) error handling: removed $ErrorActionPreference = "Stop" which crashed the script on benign python/pip warnings during the docs container build.
  • Grafana redirect loop: resolved a connection refused issue on http://localhost:3000 by overriding GF_SERVER_ROOT_URL to http://localhost:3000/grafana/ in docker-compose.monitoring-dev.yml to prevent redirecting local dev requests to production HTTPS urls.
  • Cleaned up obsolete metrics: removed the stale "Report Generation Time", "Sweep Jobs", "Sweep Job Duration", and "Active Dashboard Sessions" dashboard panels, and removed the "ReportGenerationSlow" and "SweepJobFailing" alerts since their underlying functions do not exist in the codebase.
  • Grafana targets scraping: added metrics_path: /grafana/metrics for Grafana jobs in both prometheus.yml and prometheus.dev.yml to prevent Prometheus from failing on redirection loops.
  • Production Redis credentials: added missing REDIS_PASSWORD and configured authenticated REDIS_URL in .env.production to prevent startup crashes from mandatory production security/validation gates.

Changed

  • Version 2026.6.29.post12.

[2026-06-29] - Green lint stage and more accessible "Quick access" tabs 2026.6.29.post11

Fixed

  • Restored a green CI lint stage. Today's rapid polish commits landed four ruff violations and three ruff format drifts on main (pinned ruff==0.15.4), so the ruff-check/ruff-format gates were red: an f-string with no placeholders, an unsorted import block, a SIM108 if/else in the diff-context helper, and an over-long assert. Logic-preserving fixes; full unit suite (2512) stays green.

Changed

  • Accessibility: the "Quick access" Groups/Subgroups toggles are now proper tabs - type="button", role="tablist"/role="tab", and bound aria-selected for screen readers. Template-only.
  • Version 2026.6.29.post11.

[2026-06-29] - Remove N+1 in course-wide sync / run-checks 2026.6.29.post10

Changed

  • Backend: the "Run checks (all groups)" and "Sync all groups" endpoints no longer issue one SELECT per group to check for in-flight jobs - both now use a single batched lookup that is provably the same result. For a course with N groups this removes up to 2N redundant round-trips. No behavior change (full unit suite green).
  • Version 2026.6.29.post10.

[2026-06-29] - Preconnect to GitLab on the login page too 2026.6.29.post9

Changed

  • Extended the GitLab preconnect / dns-prefetch hint to the standalone login page - its primary button is the OAuth redirect to GitLab, so warming the connection first makes that redirect a round-trip faster. Resource hint only. No static asset or Service Worker cache change.
  • Version 2026.6.29.post9.

[2026-06-29] - Preconnect to GitLab for faster first contact 2026.6.29.post8

Changed

  • Frontend optimization: the base <head> now preconnects (+ dns-prefetch) to the GitLab origin (from the gitlab_url setting, not hardcoded). The login CTA redirects to GitLab for OAuth and authenticated pages load avatars + call its API, so warming the connection early makes that first contact a round-trip faster. Resource hint only - cannot affect rendering. No static asset or Service Worker cache change.
  • Version 2026.6.29.post8.

[2026-06-29] - Skip unused dashboard counts for students 2026.6.29.post7

Changed

  • Dashboard backend: the home page and /dashboard/stats no longer run the "total groups" and "total students" count queries for students - those values feed only the teacher/admin stat cards. Two fewer database round-trips on every student dashboard load and stats refresh. No visible change.
  • Backend-only release: no static assets changed, so the Service Worker cache stays at gitpulse-v247.
  • Version 2026.6.29.post7.

[2026-06-29] - Disable staggered cascade pending review 2026.6.29.post6

Changed

  • Disabled the staggered "by arrival" cascade reveal on the dashboard and student profile - it regressed those views in production. The reveal engine stays in the codebase and will be re-enabled only after it is visually verified in a browser.
  • Version 2026.6.29.post6; Service Worker cache gitpulse-v247.

[2026-06-29] - Staggered reveal on the student profile 2026.6.29.post5

Added

  • Extended the staggered "by arrival" reveal to the student profile: merge-request cards and reviews-given cards now cascade in one after another on first load, matching the dashboard.

Changed

  • Added a release version-consistency test asserting pyproject.version matches the newest entry of every changelog (root + docs SK/EN), so a release bump can no longer update one surface and leave the others behind.
  • Version 2026.6.29.post5; Service Worker cache gitpulse-v246.

[2026-06-29] - Staggered dashboard reveal, relative time, tour scroll fix 2026.6.29.post4

Added

  • Staggered "arrival" reveal: dashboard collections (KPI stats, course cards, "Items that need your attention", and the "Follow the latest updates" feed) now cascade in one after another - "by arrival" - instead of every item snapping in at once. Honors prefers-reduced-motion and runs only on the first reveal (lazy skeleton hand-off / initial render), never on live-poll refreshes (no blink). Table rows are intentionally excluded.
  • Relative "time ago" date formatting (e.g. "2 hours ago") for activity and timestamps, via a new client-side localize-time helper.

Fixed

  • Guided tour now recursively scrolls every scrollable ancestor to bring a step target into view, and skips table containers so the main page wrapper scrolls instead of an inner table.
  • The tour's "user row actions" step now highlights the buttons wrapper rather than the bare table cell.
  • Rolled back the earlier tour positioning/popover changes that regressed pointing at the target.
  • Removed duplicate subtitles on the student profile page.

Changed

  • Rebuilt JS/CSS bundles and minified assets.
  • Version 2026.6.29.post4; Service Worker cache gitpulse-v245. Also realigns the package version, which had stayed at post2 while post3 shipped only the SW cache + changelog.

[2026-06-29] - GitLab diff comments, thread headers, transparent email logo 2026.6.29.post3

Fixed

  • Inline diff code context now displays GitLab-style double line number columns (old & new line numbers side-by-side).
  • Inline diff range comments display correctly in thread footers (e.g. Comment on lines +51-53).
  • Discussion thread headers now include the author's GitLab username (@username) and the localized thread start time.
  • Status icons in .commit-message-cell are now inline and no longer wrap above the commit message link.
  • Guided tour popovers on desktop now correctly point to their highlighted targets with a themed arrow, rather than being forced to the bottom-right corner.

Changed

  • Changed the email notification logo to be transparent (no dark-gray background tile) and removed the box-shadow boundary for a cleaner, modern look.
  • Version 2026.6.29.post3; Service Worker cache gitpulse-v244.

[2026-06-29] - GitLab-style tanuki social cover 2026.6.29.post2

Changed

  • Reworked the Open Graph / social-share cover (og-image.png, docs og-cover.png) to the GitLab-style tanuki tile (orange tanuki on a light gradient rounded square) with the GitPulse ECG pulse overlaid, replacing the off-brand blue ECG square so link previews match the in-app logo. Home-screen/PWA icons and favicons were already the tanuki.
  • Version 2026.6.29.post2.

[2026-06-29] - Diff context hunk fix, inline-code spacing, favicon 2026.6.29.post1

Fixed

  • Diff comment code context no longer crosses a hunk boundary - it stays within the commented hunk (showing the whole compact added construct, GitLab-style) instead of jumping line numbers (e.g. 29 -> 49) and bleeding into the next function.
  • Inline code pills get real horizontal spacing so they no longer touch the surrounding words.
  • MR pipeline Test coverage row (icon, label, %, "from N jobs") sits on one shared midline instead of drifting to different vertical levels.
  • Regenerated the stale docs favicon.ico as a proper multi-resolution GitPulse tanuki icon and cache-busted the favicon links.
  • Guided tour opens a collapsed <details> around a step target only when navigating to that step, not during the page-load eligibility scan (fixes a regression that could auto-expand collapsed diff/code sections on load).

Changed

  • Version 2026.6.29.post1; Service Worker cache gitpulse-v243.

[2026-06-29] - Collapsible code context, cleaner discussions, tour fix 2026.6.29

Fixed

  • Inline code context in MR/review discussions is now collapsible and collapsed by default (GitLab-style Show code summary with file:line), so long threads stay light and no longer lag while scrolling.
  • Removed the misleading non-interactive new/old pill from the code-context toolbar; the side is already shown by the line gutter and the Comment on line +N footer.
  • Stopped rendering the fabricated 1 participants label on threads: the participant count now appears only when a thread genuinely has more than one participant.
  • Decluttered review threads: dropped the redundant per-reply "reply to …" cue (replies are already nested) and the duplicate copy-link icon on a thread's root note.
  • Inline code pills in MR/issue descriptions and comments no longer overlap adjacent lines.
  • Vertically centered MR card title rows and the reviews-given header so labels no longer sit above their icons.
  • Workflow ribbon dots render larger, clearer icons with a calmer hover lift (no row jump).
  • The guided tour now opens any collapsed <details> around a step target (changed-file tree, inline code context) before highlighting, so steps like Changed-file tree are no longer broken when a section is closed.
  • Removed the dark strip below the course-settings sticky save bar when the form is the last block on the page.

Changed

  • User-info modal: the per-group +N more project list is now an expandable/collapsible control (Show less) instead of a dead label.
  • Documentation Open Graph/Twitter preview now uses the branded GitPulse cover image (assets/og-cover.png).
  • Rebuilt bundles/minified assets; added Slovak translation for Show code; version 2026.6.29; Service Worker cache gitpulse-v242.

[2026-06-28] - Scoped MR diff jumps and code comments 2026.6.28.post1

Fixed

  • MR/review diff file anchor IDs are scoped per card, so changed-file tree clicks and comment jumps no longer hit another diff-file-* block when several MRs are visible.
  • Diff comment code context now uses one side-aware line-number gutter and exact Comment on line text, without the extra old/new number panel or incorrect visible-range footer.
  • Missing MR pipeline Test coverage is backfilled from GitLab pipeline details/jobs for visible student MRs when older local pipeline rows have coverage = null.
  • Removed the unreliable Response Rate diagnostic card and the duplicate diagnostic Test Coverage card, including matching skeleton placeholders.

Changed

  • Regenerated static bundles/minified assets after the student-detail fixes.

Release

  • Version 2026.6.28.post1; Service Worker cache gitpulse-v241.

[2026-06-28] - Local jumps and inline Test coverage 2026.6.28

Fixed

  • Copyable/hash links for student comments, MR cards, dashboard targets, and paginated tables now use the shared local .gl-page-content scroller contract instead of browser-level auto scrolling.
  • MR pipeline coverage is visible again as GitLab-like inline text (Test coverage ... from 1 job) without rendering a separate Test Coverage card/chip.
  • MR loading skeleton markup mirrors the live Pipeline/Tests plus inline coverage structure.

Release

  • Version 2026.6.28; Service Worker cache gitpulse-v240.

[2026-06-27] - Student profile skeletons, MR replies, and coverage accuracy 2026.6.27.post2

Fixed

  • Student profile loading now mirrors the live profile card, mobile GitLab/profile/date metadata is centered, and the duplicate profile divider is gone.
  • Student-detail jumps use the local page-content scroller contract from Issues to Address, avoiding global window-scroll layout jumps.
  • MR commit rows keep labels, ± stats, and dates in fixed grid columns so long content no longer spills across the page.
  • MR diff code context is rendered at the discussion-thread level with old/new gutters, target-row highlighting, copy action, and attached replies for a GitLab-like reading flow.
  • Pipeline coverage is parsed from GitLab pipeline/build payloads and same-SHA fallback pipelines, so rows show real values such as Test coverage 91.00% ... from 1 job instead of not reported.

Changed

  • GitPulse logo ECG hover motion is smoother, and the student profile/MR skeleton static assets were rebuilt.

Release

  • Version 2026.6.27.post2; Service Worker cache gitpulse-v239.

[2026-06-27] - GitLab-like MR replies, coverage visibility, and student profile polish 2026.6.27.post1

Fixed

  • Student MR discussions now render as linked GitLab-like threads with stable thread/comment anchors, reply-to jump links, collapsible replies, and buttons that jump to the matching diff file.
  • Pipeline rows show an explicit coverage fallback when GitLab does not report test coverage instead of silently hiding the signal.
  • MR branch commits show visible authors, in-app profile links where possible, and a Teacher commit badge when the commit belongs to a course teacher.
  • Student profile metrics, table skeleton alignment, Run Compliance Checks checkboxes, modal footer highlighting, and diff file-tree sticky behavior were tightened.

Release

  • Version 2026.6.27.post1; Service Worker cache gitpulse-v238.

[2026-06-27] - Componentized student-detail UI, modals, skeletons, and code comments 2026.6.27

Fixed

  • Member-table skeleton rows now use the same column contract as live rows, so Status/Checks placeholders no longer drift away from the headers.
  • Student MR diff navigation keeps the changed-file tree sticky, scrolls only its local panel, and avoids global item scrollIntoView.
  • Run Compliance Checks modal checkbox rows are stable without the checked-state inset stripe or global checkbox scale animation.
  • MR diff-note inline code context now has old/new gutters, side-aware target row matching, local horizontal scrolling, and a copy action.

Changed

  • Student profile metric tokens, Reviews Given cards, tour popovers, and student-detail tour steps now follow the shared component UI contract.
  • GitPulse logo motion uses a calmer ECG pulse and one subtle hover state without jitter in light or dark themes.
  • App source CSS letter-spacing declarations were normalized to 0 across shared and page styles.

Release

  • Version 2026.6.27; Service Worker cache gitpulse-v237.

[2026-06-26] - GitPulse branding, zero-inline UI contract, and exact MR jumps 2026.6.26

Fixed

  • Student Issues-to-MR chips now scroll inside the local page content and no longer invoke the global anchor scroller.
  • Dynamically opened modals and drawers re-initialize dirty-check guards before activation.
  • Diff comments can render GitLab-like nearby code context when stored MR diff data is available.

Changed

  • Visible Teamwork wording was normalized across the application and documentation while internal stored JSON/API keys remain stable.
  • The GitPulse logo is now a shared component stylesheet with a GitLab-inspired mark, light/dark contrast guards, and rebuilt PWA icons.
  • Template progress/size inline styles now use reusable component data attributes and centralized CSS-variable hydration.
  • Responsive table and skeleton contracts were tightened to match real content more consistently.

Release

  • Version 2026.6.26; Service Worker cache gitpulse-v236.

[2026-06-25] - System-wide HTMX cleanup, memory leak prevention, and safe scripts 2026.6.25

Fixed

  • Fixed memory leaks system-wide by implementing proper page teardown and lifecycle cleanup in GP.activePageCleanup(). All window/document event listeners, SSE streams (EventSource), and polling timers (setInterval/setTimeout) are fully torn down upon HTMX page swaps.
  • Converted all anonymous event handlers on document and window in standalone page controllers (admin-events.js, admin-users.js, course-activity.js, support-ticket-detail.js) to named functions so they can be explicitly removed from the global scope.
  • Prevented global lexical scope pollution by removing obsolete IIFE wrappers from page bundles and automating the IIFE generation at bundle time in build_bundle.py.
  • Extracted the pre-paint greeting script out of dashboard.html into a dedicated, clean, component-based static JavaScript module greeting-sync.js, leaving templates entirely free of inline executable scripts.

Release

  • Version 2026.6.25; Service Worker cache gitpulse-v235.

[2026-06-23] - Documentation Update and Compliance Rule Alignment

Added

  • Added explanation and details for rule R13 (Commit Quality), which evaluates the local Git workflow against GitLab's web editor (Web IDE).

Changed

  • Realigned the descriptions of compliance rules R01-R13 in the student guides (studenti/metriky.md and .en.md) to match the exact implementations in builtin_checks.json and engine_checks.py.
  • Updated compliance control tables in the teacher guides (ucitelia/zaciname.md and .en.md) and dashboard overviews (ucitelia/dashboard.md and .en.md) to include the R13 control and its default weight of 0%.
  • Aligned route counts in the API reference (api/index.md and .en.md) to the verified total of 222 route handlers.

[2026-06-20] - Responsive course management, CSP-safe Markdown, and current thesis docs 2026.6.20

Fixed

  • Course Management keeps every column in responsive card rows, adds value labels, and uses a stable search toolbar on narrow screens.
  • Markdown preview works with the Alpine CSP build without the forbidden x-html directive.
  • Markdown-editor and support-ticket scripts can initialise again after HTMX navigation without duplicate lexical-declaration failures.

Changed

  • The public thesis section now matches the third interim submission and does not claim results that have not yet been evaluated.
  • The design documents the current R01-R13 model, manual synchronisation instead of periodic GitLab sweeps, and two anonymised functional-prototype screenshots.

Release

  • Version 2026.6.20; Service Worker cache gitpulse-v234.

[2026-06-19] - User Management responsive table 2026.6.19.post2

Fixed

  • Search and the role filter now share a deterministic toolbar instead of wrapping at arbitrary offsets.
  • Column classes live on the real headers and cells, so widths, truncation, visibility, status, actions, and skeletons use one contract.
  • Human and service-account rows use compact icon actions and safely handle long usernames and email addresses.
  • Mobile cards use the username as the primary identity while keeping labelled metadata and actions aligned.
  • Live role and status updates preserve the same badge and status-indicator markup as server rendering.

Release

  • Version 2026.6.19.post2; Service Worker cache gitpulse-v232.

[2026-06-19] - Responsive table skeleton alignment 2026.6.19.post1

Fixed

  • Project and team member tables share one responsive column contract across real rows, loading skeleton rows, and JavaScript fallback rows.
  • Project detail skeletons derive rubric/check columns from the active checks list, so the loading matrix stays aligned with the real R01-R13 table.
  • Admin user skeleton rows use typed user columns instead of a generic table placeholder.
  • Skeleton rows now carry the same responsive data-label contract as real rows, so tablet and phone loading states render as label/value cards.
  • Admin user role filters and member filter bars now wrap inside the component on narrow screens without horizontal scroll or clipped controls.
  • Student merge request pipeline test status now renders as compact badges instead of raw text.
  • Issues to Address animations are disabled so they cannot replay after visibility or HTMX refresh changes.

Release

  • Version 2026.6.19.post1; Service Worker cache gitpulse-v231.

[2026-06-18] - Table skeletons, analytics freshness, and indexing policy 2026.6.18.post1

Fixed

  • Table skeletons now use typed cells for project rows, member rows, badges, status pills, check dots, and action buttons.
  • Admin and teacher dashboard summaries replace the low-value "Open MRs" card with active group coverage.
  • Course analytics and activity stale-data banners report partial current-week group snapshot coverage instead of implying the whole course was not synced.
  • Login/logout alerts use the shared GitLab-style alert component with a proper title API and a short synchronized logout fade.
  • Robots and sitemap responses explicitly block private dashboard crawling, allow the login page, include last-modified metadata, and set public cache headers.

Release

  • Version 2026.6.18.post1; Service Worker cache gitpulse-v229.

[2026-06-18] - Project sync reconciliation and guarded UI controls 2026.6.18

Fixed

  • Dashboard Quick access project rows open the project page instead of the parent group.
  • Group/course GitLab imports deactivate active database projects that no longer exist in the imported GitLab group tree, including orphaned team members when no active projects remain.
  • Process-action guards keep protecting sync/import/check/delete actions while bypassing passive filters, tabs, and dropdown-style controls.
  • Project and member table skeleton contracts stay aligned with the responsive table layout.
  • Production redeploy keeps AI teamwork analysis disabled in .env.production.

Changed

  • Back to Group links use transform-based icon/link motion instead of layout-moving gap changes.
  • GitLab import API and SSE completion payloads expose stale-project cleanup counts.

Release

  • Version 2026.6.18; Service Worker cache gitpulse-v228.

[2026-06-17] - Keycloak login alignment 2026.6.17

Changed

  • Aligned sign-in and sign-out pages layout, spacing, and dimensions with the Keycloak design.
  • Re-centered the logo title, set the card width to a compact 400 px, and removed unnecessary border radii for a sharp square-edge look.
  • Re-positioned and styled the language selector dropdown to be compact and styled with a semi-transparent black background.

Release

  • Version 2026.6.17; Service Worker cache gitpulse-v224.

[2026-06-16] - Email redesign, support ticket refinements, and emoji cleanup 2026.6.16

Added

  • Complete redesign of the email notification system with modern Inter typography, soft drop-shadows, and premium color gradients.
  • Comprehensive support tickets interface updates, including dynamic drag-and-drop file attachments in the ticket editor, dragover visual highlighting, and a Chrome double-browse fix.

Changed

  • Replaced obsolete "Reports" references with "Analytics" inside the welcome email template for teacher and admin roles.
  • Purged all raw emojis and AI-like emoticons from all active system documentation to establish a professional, publication-ready tone.
  • Incremented application version to 2026.6.16 and Service Worker cache namespace to gitpulse-v217.
  • Cleaned up obsolete helper and scratch scripts from the repository.

Release

  • Version 2026.6.16; Service Worker cache gitpulse-v217.

[2026-06-15] - Academic weeks, Pajamas dropdowns, and improved drag & drop 2026.6.15.post1

Added

  • Pajamas-aligned custom academic and exam period week calculation (1-13 and S1-S7) in Flatpickr based on the settings' semester start date.
  • Alpine.js-backed, Pajamas-styled custom dropdowns (select_field) for type and week selection on the assignments page.
  • Global window-level drag-and-drop overrides to prevent accidental browser navigation on missed file drops.
  • Pointer-events scoping on file upload areas to eliminate drag flickering.

Changed

  • Refactored Edit and Delete actions on the assignments list to use btn_icon_only to prevent text overlap in compact layouts.
  • Rebuilt static CSS/JS bundles and minified assets.

Fixed

  • Fixed dark-mode checkbox checkmark visibility and added premium spring-based transitions.
  • Fixed file upload areas to dispatch a standard change event on dropping, allowing automatic HTMX integration.
  • Removed redundant page-entrance card animations to prevent double-swaps.

Release

  • Version 2026.6.15.post1; Service Worker cache gitpulse-v209.

[2026-06-11] - Persistent appearance and responsive polish 2026.6.11.post7

Added

  • Light, Dark, and System modes backed by one semantic token layer without a first-paint theme flash.
  • The selected mode is persisted per account on the server and follows the user across browsers and devices.
  • The appearance tour opens the profile menu and targets the real switcher.
  • Installed-PWA state is verified through the browser instead of relying only on a local marker.
  • A shared mode switcher for the dashboard, login, and all error surfaces.
  • A static offline fallback using the same secure error surface.
  • A shared appearance step included automatically in every available product tour.
  • Server-owned Project, Course, and Group sync scope metadata in Job History.

Changed

  • Charts recolor on mode changes without a reload.
  • Middleware errors use one escaped, CSP-aware renderer.
  • Spinners use one component DOM/CSS contract and skeletons remain only in real HTMX lazy regions.
  • SSR tables render immediately without waiting for a JavaScript visibility guard.
  • The light shell, navigation, cards, tables, controls, skeletons, spinners, text, borders, shadows, and interaction states now follow current Pajamas semantic roles.
  • Historical whole-course sync jobs recover their course context and display the course name instead of being mislabelled as standalone group syncs.
  • Buttons, dropdowns, cards, and sections have visible semantic borders in both themes; search focus no longer resizes action toolbars.
  • The branded sign-in background is restored with a responsive card verified at a 390 px mobile viewport.

Removed

  • Generic page, row, and lazy-fragment entrance animations.
  • Dead skeleton branches and duplicate button spinner markup.
  • Dashboard hover translations and scale effects that added motion without state.

Release

  • Version 2026.6.11.post7; Service Worker cache gitpulse-v186.

[2026-06-10] - Pajamas Loader Alignment and Clean Skeletons 2026.6.10.post5

Added

  • Pajamas-aligned loaders and zero inline styles: Completely eliminated all inline style configurations for width, height, and border-radius within loaders.html. All sizing is now mapped to reusable Pajamas-aligned utility classes in loaders.css.
  • Fidelity-aligned Team Detail and Teachers Skeletons: Replaced custom/manual skeleton divs with macro components (badge, btn, btn_icon, btn_icon_only) in disabled states to guarantee pixel-perfect size matching and zero layout shift (CLS).

Fixed

  • GitLab project deletion and empty repo handling: Added automatic deactivation of projects whose GitLab repository was deleted (returning empty repository status rather than 404). Blanked stale roster scores to match "N/A" on empty/deleted repos.
  • Dynamic roster deactivation on project loss: Added auto-deactivation of team members when all active projects under their team are deleted/deactivated.
  • Member deactivation safety guard: Prevented dropping valid project members on partial GitLab API fetch failures, ensuring roster stability during intermittent GitLab API timeouts or 5xx errors.
  • Service Worker & Cache Bump: Version bumped to 2026.6.10.post5 with cache key incremented to gitpulse-v176.

[2026-06-09] - Complete removal of inline layout styles 2026.6.9.post2

Changed

  • Complete removal of inline layout styles: Eliminated all layout-related inline style="..." attributes (like flexbox, display, margins, padding, opacities, and pointer-events) from loaders.html. Migrated them to clean, Pajamas-aligned CSS utility classes in utilities.css.
  • Nicer form layouts: Replaced inline max-width configurations on form components with structured layout utility classes.
  • Service Worker & Cache Bump: Version bumped to 2026.6.9.post2 with cache key incremented to gitpulse-v174.

Fixed

  • Version mismatch alert notification: Updated the app-internal JSON changelog manifest so client browsers receive proper notifications when new versions are deployed.

[2026-06-09] - Seamless transitions and layout-shift free loading skeletons 2026.6.9.post1

Added

  • Dynamic table header rendering inside skeletons: Modified the sk_table macro to accept an optional headers list parameter, allowing static header rows to appear immediately during page loading states.
  • Enhanced sk_stat macro with label and icon arguments: The stats grid placeholders in Course Analytics, Course Activity, and Student Details now render actual labels and icons immediately instead of shimmering blocks, completely eliminating layout shifting.

Fixed

  • Seamless dropdown menu interaction: Fixed invalid CSS variables (var(--gl-dropdown-item-hover)) in dropdowns.css by mapping them to their correct tokens (var(--gl-dropdown-item-hover-bg), etc.), restoring smooth background transition effects for list hover states.
  • Skeleton loading alignment across all surfaces: Checked and fully refactored every single composed page skeleton (sk_dashboard, sk_course_detail_body, sk_analytics_inner, sk_course_activity_inner, sk_student_detail, _sk_teachers_inner) in loaders.html to align with the real server-rendered templates, using translated helper strings t() for static texts.

Changed

  • Bumped service-worker cache namespace to gitpulse-v173.

[2026-06-08] - Synchronized Refresh and Manual GitLab Sync 2026.6.8.post5

Changed

  • Manual refresh with full sync-all flow: Clicking the "Refresh" button on Course Analytics or Course Activity now triggers a real background GitLab sync via /api/v1/courses/{courseId}/sync-all (for staff). It features button loading states (spinner + text), integrates with the global job progress bar, auto-reloads charts and activity tables on completion, and applies a danger state to the button upon failure.
  • Removed temporary deploy logs: Cleaned up root-level temporary log files (deploy-post21.log, deploy-post22.log) from the repository workspace.

[2026-06-08] - Dashboard Polish and Bugfixes 2026.6.8.post4

Fixed

  • Exclusion of phantom sync rows: The "Follow the latest updates" feed on the dashboard no longer displays sync jobs that have no matching records in Job History.
  • Action column alignment in members table: The "View" button in the project members list is right-aligned to prevent layout jumping on rows without the exclusion toggle.

Changed

  • Job History refresh feedback: Manual refresh of the job history table now shows a visible loading state and provides support links on network failure.

[2026-06-08] - Analytics Calculations and Localization 2026.6.8.post3

Fixed

  • Course analytics student count: Fixed denominator in the course compliance checklist to calculate active non-instructor students only.
  • SyntaxError in script: Resolved a JavaScript SyntaxError in the week-explorer page controller.

Changed

  • Dashboard greeting subtitle and HTMX error states: Added a subtitle with user roles and localized date formatting, and appended support ticket links to HTMX lazy-loading failure panels.

[2026-06-08] - Synchronized Page Headers 2026.6.8.post2

Changed

  • Synchronized Team/Project headers: Headers (titles, back links, actions) now load directly inside the shell templates to prevent layout shifts.

[2026-06-08] - Skeleton Loader Overhaul 2026.6.8.post1

Removed

  • Page-level skeletons cleanup: Removed unused page-level skeleton macros and hid compliance grids on teacher profiles to prevent visual flashing.

Changed

  • The "Needs Attention" KPI card now redirects exactly to the element: when a single group is failing, it opens that group directly; with several it goes to the overview. With the earlier Students-card fix, no two KPI cards point to the same place.

[2026-06-05] - Student page: "Project" column removed 2026.6.5.post17

Changed

  • The "Project" column was completely removed from every table on the student-in-group page (commits, issues, MRs). On a single-group page the project never changes, so it was pure noise; the GitLab link stays on the commit/issue/MR number. The tables now focus on what is graded: the member's own work (DOING) vs peer code review (COMMUNICATING).
  • Deleting jobs from Job History now also clears them from "Follow the latest updates" (the feed's job rows share the same source).

[2026-06-05] - Consistent activity feed + smooth skeleton swap 2026.6.5.post16

Fixed

  • "Follow the latest updates" is now consistent with Job History - no more "phantom" sync/report rows that don't exist in Job History. Job-like activity comes from the same source as Job History; the audit log only adds genuine non-job events (course/group/member/settings/tickets).

Changed

  • Page content now settles smoothly into the skeleton's place after loading (GitLab style) instead of a hard pop.

[2026-06-05] - Cleaner dashboard + CI fix 2026.6.5.post15

Fixed

  • CI is green again - requirements.lock is regenerated on Linux (exactly as CI does), not on Windows.
  • The "Follow the latest updates" feed no longer shows "phantom" system entries (telemetry snapshots, leftover scheduled sweeps) - they remain only in the admin Job History.

Changed

  • The "Students" KPI card no longer points to the same place as "Users" - it opens the users list pre-filtered to students.

[2026-06-05] - Faster loads (longer cache) 2026.6.5.post14

Changed

  • The rendered-HTML cache now lives 5 minutes instead of 60 seconds. This does not affect freshness - the cache is invalidated precisely on every change (webhook or in-app action) and on every deploy. The result is far more instant loads with no risk of stale data.

[2026-06-05] - Complete webhook coverage 2026.6.5.post13

Changed

  • Confidential issues and notes are now processed just like public ones (previously silently ignored).
  • The webhook setup guide recommends enabling every relevant event and configuring the hook at the group/sub-group level - only group webhooks deliver Member and Subgroup events, which is how member add/remove and reassignment are captured automatically.

[2026-06-05] - Periodic jobs removed (purely webhook-driven) 2026.6.5.post12

Removed

  • All periodic (cron) jobs and the scheduler itself were removed (scheduler.py, the scheduler compose service, the rq-scheduler dependency). No more "ghost" jobs cluttering the job panel and zero background load on GitLab.
  • Every mention of periodic scheduling was purged from code and documentation.

Changed

  • The system is now purely webhook-driven + manual: data refreshes from GitLab events and explicit actions. Maintenance operations remain as manual jobs under System -> maintenance jobs.
  • Email-delivery retries are self-scheduling (Queue.enqueue_in, the worker's built-in RQ scheduler) - no separate service.

[2026-06-05] - Gentler spam-click protection 2026.6.5.post11

Fixed

  • Fast double-clicking a button that triggers an already-running action no longer shows a confusing red error. Instead a gentle blue "Please wait - the previous action is still running" notice appears and the duplicate request is silently ignored (a central fix, no per-button changes).

[2026-06-05] - Service Accounts table pagination 2026.6.5.post10

Changed

  • The Service Accounts (GitLab bots) table on the Users page now has its own pagination bar (10/20/50 per page), independent of the main users table and cooperating with column sorting. With many bots the page is no longer overwhelmed.

[2026-06-05] - Clearer student-in-group page 2026.6.5.post9

Changed

  • The loading skeleton of this page was rewritten to match the new structure exactly - it now includes both group dividers and the commits table has the correct column count, so the layout no longer shifts when content swaps in.
  • The student-in-group page is split into two clearly labelled parts: "Work by <member>" (commits, MRs and issues the member authored) and "Code review & communication" (review comments they wrote for peers, kept separate from feedback they received). A teacher reads the page top-to-bottom as a "doing + communicating" story.
  • The Project column in the tables now appears only when the group spans more than one active project; the standalone SHA column was removed (the GitLab link now sits on the commit title).

Fixed (post5-post7)

  • Fixed the duplicate "Done" state in the global job bar (idempotent against the "finishing" phase).
  • The greeting now renders server-side in the shell page so it no longer flashes; its duplicate stats sub-line was removed.
  • "Items that need your attention" no longer shows the confusing "N projects with stale data" line - the reminder is now grouped per course.

[2026-06-05] - Clickable dashboard KPIs + docs deep-clean 2026.6.5.post4

Changed

  • The dashboard KPI cards now navigate (admins to users/courses admin pages; teachers/students to their course/analytics). Clickable styling applies only to cards with a real target.

Removed

  • Removed the redundant System Settings stat strip (it duplicated the dashboard cards) and its dead JS.
  • Purged all remaining documentation references to removed features (rubric/gaming/async-report/roster-import) - zero remain.

[2026-06-05] - Retired schema/API tail cleanup 2026.6.5.post3

Removed

  • Added Alembic migration 049 to drop the remaining retired schema tail: the legacy student compliance snapshot column and orphaned history/report tables.
  • Removed the old telemetry PDF report endpoint, schemas, ORM model, generator service, and report-only tests.
  • Removed stale active docs, fixture JSON sections, support-form labels, login metadata, and i18n keys tied to retired grading, heuristic, report, and import subsystems.

Changed

  • Full course configuration examples now list only active import sections: course metadata, settings, built-in checks, custom checks, weekly deadlines, Docker lint config, and CI failure config.
  • App/docs metadata is 2026.6.5.post3; Service Worker cache namespace is gitpulse-v160.

[2026-06-05] - Removed scheduler master switch and dead scheduled fallbacks 2026.6.5.post2

Removed

  • Removed the retired scheduler master switch from Settings, Docker Compose, scheduler runtime, the tracked wrapper, admin scheduled registry, manual retry routing, tests, README files, and architecture docs.
  • Removed the remaining inactive scheduled fallback jobs: periodic metrics recompute, background-job cleanup, webhook verification, dead-project polling, metric-snapshot cleanup, audit-log cleanup, and expired webhook-secret cleanup. Data freshness stays on webhook/manual sync flows; metrics are recomputed inline by sync/event flows.
  • Removed stale activity-feed/global-job labels for already-deleted GitLab polling and discovery jobs so the UI cannot show retired scheduled processes.

Changed

  • Scheduler/admin catalog now shows only the 12 active scheduled jobs: event recovery, CI rollback, conflict expiry/verification, optional LLM, optional telemetry, DLQ resurrection, optional Docker re-verification, event retention, email queue/log retention, and semester auto-archive.
  • App/docs metadata is 2026.6.5.post2; static bundles were rebuilt and the Service Worker cache namespace is gitpulse-v159.

[2026-06-05] - Removed periodic GitLab polling & deep docs clean-up 2026.6.5.post1

Removed

  • Removed the periodic GitLab-polling scheduled jobs (periodic_enrich_all, periodic_enrich_users, sweep_stale_projects, sweep_discover_new_projects, sweep_discover_new_teams) and the now-empty sweep_enrichment.py / sweep_discover.py modules, plus their config intervals and admin registries. Data freshness now relies only on webhooks, manual sync, and the on-creation sync.

Changed

  • Deep documentation/thesis clean-up of retired grading/heuristic/intervention/polling references (developer structure docs, API reference, thesis abstract/keywords).

[2026-06-05] - Removed retired grading engine, retired heuristic detection, intervention flags & polling sweep 2026.6.5

Removed

  • Removed the retired grading grading engine, retired heuristic detection, the Intervention Flags analytics panel + flag endpoints, and the periodic membership-sync sweep completely (code, settings UI, API, DB columns/tables via migration 048). Manual sync, webhooks, activation sync, and import auto-sync stay; the evaluation-report/telemetry subsystem stays intact. The grading core is now just the configurable R01-R13 checks that evaluate each student.

Added

  • Global capture-phase button click-lock: repeat clicks within 600 ms are swallowed with one throttled "please wait" toast so every action gives feedback even when state does not change. UI toggles are exempt.

Fixed

  • The member-filter reset button in the loading skeleton now aligns right to match the real filter bar, and leftover retired grading/retired heuristic/flags skeleton placeholders were removed.

[2026-06-04] - Legacy global gates removal and System Health parity 2026.6.4.post4

Removed

  • Removed stale global switches for retired grading, manual interventions, heuristics, and GitLab polling automation from settings, template globals, API guards, scheduler metadata, docker-compose, tests, and docs.

Changed

  • System Health and scheduler metadata were aligned with the then-current transition state before the retired subsystems were fully removed.

Fixed

  • System Health checking skeleton now shows Database, Redis, and Session_store, matching the final operational card.
  • Team detail and course Activity skeletons were rechecked against the current SSR fragments after card removals.

[2026-06-04] - Roster import & export removal, grading-focused KPIs 2026.6.4.post3

Removed

  • Roster CSV import removed (students enter via GitLab sync) - the retired roster switch flag, roster_service, RosterImport model/schema, the upload/history API and UI, and the roster_import job-bar hooks. The Roster view + CSV export are unchanged.
  • Team CSV/JSON export removed (buttons + /teams/{id}/export/* endpoints).
  • Redundant cards removed - the course-detail KPI strip and the duplicate student Compliance Checks card.

Changed

  • Grading-focused dashboard KPIs - admin/teacher home cards swap the raw "Groups" count for Needs Attention (groups with failing checks).

Fixed

  • Week Analytics doughnut tooltip restored above the centre score.
  • Tighter student profile (removed the extra divider line).
  • No loading flash / click-spam - System Health holds a readable minimum checking state; buttons stay disabled for the whole request.
  • Skeleton loaders match the real pages (dropped deleted placeholders, flat profile meta, hollow Week Analytics ring).

[2026-06-04] - Retired Report removal and UI cleanup 2026.6.4.post2

Removed

  • The async "Retired Report" feature was removed end-to-end - team report API/worker, the dead retired_report service, the retired report switch flag with its cleanup_old_reports sweep and retention config, the generate_report job-bar hooks, the team-detail button, i18n strings and the tour step. The separate telemetry evaluation-report subsystem is untouched.
  • Redundant cards removed - course-activity Summary, analytics Compliance by Category (with its Chart.js renderer and backend roll-up) and team Group Summary.

Fixed

  • Consistent stat cards - the inconsistent stat-card--glow hover/focus highlight (on some cards, absent on others) was removed so every stat card shares one neutral hover plus the standard keyboard focus ring.
  • Flat profile facts - student Email / GitLab / Role moved from card tiles into a flat profile-meta row inside the profile card.
  • No loading flash, no click spam - setButtonLoading now holds the loading state for a minimum visible duration before resetting and keeps the button disabled for the whole in-flight window, centrally for every button (System Health, refreshes, syncs, ...).
  • Visible empty Week Analytics ring - its no-data state used 6%-opacity white (invisible on the dark background); it now uses a visible slate, like the activity pipeline doughnut.

[2026-06-04] - Analytics refresh and sync cache coherence 2026.6.4.post1

Fixed

  • Course analytics refresh now uses one SSR/cache-busted path. The stale evaluation-report surface, report Alpine factory, tour step, local report job hooks and report skeleton were removed from the analytics page and generated bundles. The refresh action reloads the SSR lazy fragment with a cache-busting parameter before falling back to the shared scroll-preserving reload helper.
  • Sync completion waits for fresh metrics before terminal SSE. Project syncs recompute team metrics inline before marking the sync completed, and webhook/metrics side effects invalidate page-cache fragments before notifying live clients.
  • Profile/stat cards keep useful interaction without entrance animations. Student profile metadata is back in compact cards, course KPI cards link to groups/roster anchors, and glow/process animations remain without duplicate card-entry effects.
  • Analytics and system micro-UX were tightened. The Week Analytics doughnut uses an HTML centre label with disabled chart tooltip hitboxes, and System Health checking keeps spinner/glow feedback without harsh blinking.
  • Release metadata is current for 2026.6.4.post1. Dashboard Latest changes, docs, generated bundles/minified siblings and the Service Worker cache namespace (gitpulse-v154) were refreshed.

[2026-06-04] - Unified sync completion and cache-busted refresh 2026.6.4

Fixed

  • Sync/check terminal states now follow one readable flow everywhere. Course, team, project syncs and run-check jobs enter shared Finishing first, remain visible across navigation, then emit the canonical Done row/toast exactly once.
  • SSR refreshes after data updates bypass stale page caches. reloadKeepScroll() now reloads through a cache-busting URL parameter, preserves scroll/toasts, and removes the parameter on boot so post-SSE or post-mutation refreshes read fresh server-rendered data.
  • No-change sync completions explain what happened. Project/team sync summaries now say when GitLab returned no changed data while metrics/cache refresh still completed; team summaries include commit counts when data did change.
  • Legacy appearance effects and duplicated profile cards were removed. Student profile metadata now lives in one compact header block and the affected course, project, support, student and team surfaces no longer use the removed glow/counter/card entrance animations.
  • Release metadata is current for 2026.6.4. Dashboard Latest changes, docs, generated bundles/minified siblings and the Service Worker cache namespace (gitpulse-v153) were refreshed.

[2026-06-03] - Settings live-update race removed 2026.6.3.post6

Fixed

  • Course Settings live updates no longer race old settings or stale page fragments. The settings endpoint now commits the new settings, invalidates affected course/team SSR fragments, then publishes settings_changed. Open course, team, project and settings pages that reload on that event therefore fetch committed data and fresh Redis page-cache bodies.
  • Post-update SSR reloads use the shared reload helper. Project sync completion and Support ticket fallback refreshes now call reloadKeepScroll() instead of raw window.location.reload(), preserving scroll and visible toasts across SSR reloads.
  • Release metadata is current for 2026.6.3.post6. Dashboard Latest changes, docs, generated bundles/minified siblings and the Service Worker cache namespace (gitpulse-v152) were refreshed.

[2026-06-03] - Dashboard live refresh cache coherence 2026.6.3.post5

Fixed

  • Dashboard live refreshes now use one coherent fan-out. SSE updates, global job-complete fallbacks and tab-return refreshes call one dashboard helper for stats, attention items, activity and sidebar badges. This keeps the home page from updating only one fragment after a sync.
  • Short-lived dashboard live caches are cleared before global SSE pushes. The dashboard notification publisher now invalidates dashboard:stats:* and dashboard:badges:* Redis keys before publishing dashboard:global, so an immediate post-sync refresh does not read stale 5-second cache values.
  • The Your courses loading skeleton now renders two cards. The skeleton matches the requested dashboard density instead of showing three placeholder course cards.
  • Release metadata is current for 2026.6.3.post5. Application/docs examples, docs-site latest, dashboard Latest changes, generated bundles and the Service Worker cache namespace (gitpulse-v151) were refreshed.

[2026-06-03] - Sync finishing stays readable across navigation 2026.6.3.post1

Fixed

  • Course sync finishing now stays readable across navigation before moving to Done. The global job bar restores finishing as its own state, keeps it visible for a minimum window, and then completes through the normal completeGlobalJob() path so the canonical "Sync completed!" row and toast are not swallowed during page bootstrap. /sync/active grace snapshots with running=0, queued=0 no longer delete/recreate the row or rewrite it back to running, and the course-detail page treats that zero-work active snapshot as Finishing instead of scheduling an immediate reload.
  • Completed single-stage welcome tours no longer reappear after DOM fingerprint drift. The legacy completion migration now scans the real prefixed gp_tour_v2: localStorage keys, so older per-stage completion records are promoted to the global completed key correctly. Tour-seen POSTs also use keepalive and retry after failed responses, so quick navigation after Done does not permanently lose server-side tour state.
  • Release metadata is current for 2026.6.3.post1. Application/docs package versions, docs-site latest, operator/developer version examples, dashboard Latest changes, and the Service Worker cache namespace (gitpulse-v147) were refreshed.

[2026-06-03] - Sync finishing survives navigation; completed tours stop re-prompting 2026.6.3

Fixed

  • Course sync finishing survives navigation and completed welcome tours stop re-prompting. This release introduced persistence for lastUpdate and finishingSince, plus welcome-tour completion migration/retry handling.

[2026-06-02] - Sync-locked buttons no longer collapse; course-card sync badge no longer stalls 2026.6.2.post24

Fixed

  • Process-locked buttons no longer collapse to mismatched widths during a sync. While a sync runs, mutating actions (e.g. Edit Course, Archive Course in the Course Actions menu) are process-locked, and the lock wraps each control in a <span class="gl-tip-wrap"> so the disabled-reason tooltip can fire on hover. That span had no styling and defaulted to display: inline, so it shrink-wrapped any full-width (btn-block) button down to its text width while sibling unlocked buttons stayed full-width - stacked menus rendered chaotically uneven button widths. The wrapper now mirrors a btn-block child's full-width block box.
  • Course-card sync badge can no longer get stranded on a stale "Finishing.../Syncing..." pill. A trailing gp:job-update arriving right after a job completed could re-label (or resurrect) a card badge whose job had already finished, leaving the card stuck on "Finishing..." while the global bar already showed "Sync completed!". The update handler now ignores events for jobs that are already terminal, and a defensive sweep clears any badge whose underlying global job is done/error on every completion and on tab refocus.

[2026-06-02] - GitLab Intelligence cards render server-side; no appearance animation 2026.6.2.post23

Fixed

  • The GitLab Intelligence cards now render fully on the server. The Protected Branches, Contributors and Repository Stats cards (and the Last Synced row) on the project page used to be emitted with - placeholders and then filled in by JavaScript on the first SSE connected event, with a fadeSlideUp entrance animation - so the above-the-fold stats visibly popped in. A new gl_intel_view is now computed in the route (mirroring the JS render logic: byte formatting, contributor merge/sort, stats parsing) and the template renders the card bodies directly. The entrance animation on the info bar and API panel cards was removed, and the dead client-side preload-JSON path (gp-gl-intelligence) plus the first-connect JS repaint were deleted. Live refreshes (sync / job-complete) still re-fetch and re-render as before.

[2026-06-02] - Pages stop flickering on load; one shared job-message formatter 2026.6.2.post22

Fixed

  • Course, team and project pages no longer flicker on load. The dashboard already skipped its first SSE connected refresh and trusted the server-rendered values; the three detail pages did not - they fired a full /live-data fetch + repaint the instant the SSE stream connected, so the freshly server-rendered cards/rows were briefly overwritten (the blank-then-JS flicker). All three now carry a _connectedOnce guard (mirroring the dashboard) that skips the first connected repaint and only refreshes on a genuine reconnect. Live SSE change events still repaint as before. On the project page the lazy GitLab-intelligence load still runs on first connect (it is not server-rendered, so there is nothing to flicker over).
  • Job progress messages now share one formatter everywhere; duplicated code removed. The admin view had its own _translateJobMsg / _jobTypeLabel / _JOB_TYPE_LABELS, while the global job bar showed the raw English result_summary (e.g. Synced N issues, N MRs...) untranslated. A single GP.formatJobMessage / GP.jobTypeLabel now lives in core/00-base.js; the global job bar formats at the display boundary (completion toast + live row) and the admin view delegates to it. The formatter is idempotent, so already-translated strings pass through unchanged.

[2026-06-02] - Deleting a group refreshes every stat, not just the project 2026.6.2.post21

Fixed

  • Deleting or restoring a whole group now refreshes every stat, not just the project view. Project unlink/restore already recomputed metrics and pushed an SSE nudge, but group delete/reactivate only invalidated the page cache - they never published an SSE event nor recomputed the snapshot. So after deleting a group the open course page kept its stale group list, summary cards, activity feed and analytics until a manual reload. Group delete and reactivate now commit explicitly, publish a team_deleted / team_reactivated SSE event on both the team and course channels, and enqueue a metrics recompute. The team and course pages treat these as structural events and re-render server-side (scroll preserved).

[2026-06-02] - Unlinking a project also recomputes the group summary 2026.6.2.post20

Fixed

  • Unlinking a project now updates the Group Summary numbers too. The summary cards (avg score, traffic-light status, pipeline pass rate) are served from the latest stored TeamMetricSnapshot, which is only rebuilt by a metrics recompute - never by the unlink itself. So the list/count updated but the headline numbers stayed frozen on the unlinked project's old contribution. Unlink and reactivate now enqueue a metrics recompute (the compliance engine already excludes is_active=False projects); when it lands it publishes a metrics_computed SSE nudge so the open team/course/dashboard pages refresh the cards live.

[2026-06-02] - Unlink/restore now truly refreshes live 2026.6.2.post19

Fixed

  • Unlink/restore now truly refreshes instead of only showing a toast. The dashboard SSE nudge was published from inside the request handler before the transaction committed (the session commits after the handler returns), so the client's instant live-data refetch raced the still-open transaction and re-read the project as active - the "Data updated" toast fired but nothing changed. Unlink and reactivate now commit explicitly and only then publish the SSE event (expire_on_commit=False keeps the captured ids valid).
  • The unlinked project row now disappears from the list. refreshTeamData() patched stat cards, member rows, and retired grading scores but never the server-rendered "Subgroups and Projects" list, so the unlinked row lingered. Structural project events (unlink / restore / link) now trigger a scroll-preserving server-side re-render on the team and course pages, so the list, the Deleted count, and the summary cards all reflect committed truth.

[2026-06-02] - Live refresh on unlink/restore, no duplicate progress bars 2026.6.2.post18

Fixed

  • Unlink/restore now updates the whole UI live. Unlinking or reactivating a project now publishes a dashboard SSE event for its team and course, so open dashboard, course, and analytics views refresh their stats cards, activity feed, and metrics immediately instead of waiting for a manual reload.
  • No more duplicate progress bars. A single sync/restore action could surface two rows in the global progress bar - a project-scoped "Syncing project..." row plus a course-level aggregate row from /sync/active. The /sync/active reconciler now suppresses the course aggregate while a project-/team-scoped sync row for the same course is already tracked, and the render pass collapses any remaining same-course rows into one as a safety net.

[2026-06-02] - Time-of-day greeting, live activity feed, active-only counters 2026.6.2.post17

Added

  • Time-of-day dashboard greeting. The home greeting shows "Good morning / Good afternoon / Good evening" by the viewer's local clock (server-translated, with an "Hi" fallback when JavaScript is off) and re-evaluates on tab-return.
  • Live "Follow the latest updates" feed. The feed is served from /dashboard/activity and refreshes live over the dashboard SSE stream (polling fallback retained), and now surfaces group/project deletion and restoration with their own icons, labels, and links.

Fixed

  • Headline counters count only active items. The "Groups" counter and the project counts behind per-team pipeline health filter is_active, so unlinked/deleted groups and projects (and GitLab deletion_scheduled tombstones) drop out immediately instead of inflating the cards.

Changed

  • Admin system "active jobs" grid is SSE-first. The scheduled-jobs panel reacts to the dashboard SSE stream instead of waiting up to 10 s, with the 10 s interval retained purely as a polling fallback.
  • Release metadata is current for 2026.6.2.post17. JS/CSS bundles and minified siblings were rebuilt, docs-site latest points to this release, dashboard Latest changes was refreshed, and the service-worker cache namespace is gitpulse-v139.

[2026-06-02] - Config import boundary and form validation cleanup 2026.6.2.post16

Fixed

  • Course config auto-import no longer depends on dashboard route internals. scripts/import_course_configs.py imports the dedicated course_settings_config config-application module directly, so deployment bootstrapping does not rely on private HTTP route re-exports and strict mypy is clean again.
  • Shared form components preserve their declared attributes. Bare text inputs now keep validation attributes (pattern, minlength, autofocus), bare checkboxes submit the requested value, and required custom dropdowns use a focusable validator control instead of an ignored required hidden input.
  • The local security scan wrapper reports production dependency risk. scripts/security_scan.py now uses timezone-aware timestamps, parses pip-audit JSON even when the tool emits advisory text, and audits requirements.lock by default instead of reporting unrelated packages from the developer virtualenv.

Changed

  • Release metadata is current for 2026.6.2.post16. JS/CSS bundles and minified siblings were rebuilt, docs-site latest points to this release, dashboard Latest changes was refreshed, and the service-worker cache namespace is gitpulse-v138.

[2026-06-02] - SSE helper contract and UI style cleanup 2026.6.2.post15

Fixed

  • The shared SSE job helper now has an explicit polling fallback contract. Manual fallback uses GP.apiCall instead of relying on an accidental global identifier, and completion callbacks are isolated so a page-level callback error cannot leave an EventSource or fallback poller running forever.
  • The global progress bar no longer persists removed ETA state. The ETA UI was already removed in favour of elapsed time, but the old state key still leaked through localStorage and cross-tab merges. That legacy state is now ignored and no longer written.
  • Small static inline styles moved into component CSS. Cookie banner links, dashboard quick-access avatar colours, the service-account summary header, and dropdown avatar dimensions now use classes instead of repeated inline declarations.

Changed

  • Release metadata is current for 2026.6.2.post15. docs-site latest points to this release, dashboard Latest changes was refreshed, and the service-worker cache namespace is gitpulse-v137.

[2026-06-02] - Diplomovka section synced with the current thesis repository 2026.6.2.post14

Added

  • The docs-site Thesis section now matches the current thesis text. Slovak chapters mirror the latest LaTeX source from dp.mykyta.olym/thesis, including the new 4. Solution design and 5. Conclusion pages, the refreshed thesis index, and Mermaid architecture/event-flow diagrams.
  • A repeatable thesis sync script was added. scripts/sync_thesis_docs.py converts the LaTeX chapters through Pandoc, handles project-specific glossary/reference/table/diagram macros, and removes the need for manual copy-paste updates.

Changed

  • Thesis goals, navigation and Latest changes are current for 2026.6.2.post14. The goals page links the completed design chapter, marks pilot deployment and impact evaluation as the next phase, docs-site latest points to 2026.6.2.post14, and the service-worker cache namespace is gitpulse-v136.

[2026-06-02] - Job SSE fallback dedup + current docs metadata 2026.6.2.post13

Fixed

  • The global progress bar no longer starts duplicate polling timers for the same background job after an SSE failure. streamJobProgress() already owns the SSE-to-polling fallback, but the global job bar also launched its legacy poller from the helper's error callback. That could close the helper while it was switching modes and leave duplicate /api/v1/jobs/{id} polling. The job bar now delegates fallback to the shared helper, and the helper re-checks whether it was closed before starting internal polling.
  • GitLab recovery toasts no longer appear under the main backend-health banner. A GitLab-only recovery toast during a broader backend outage was misleading, so the toast now fires only when the GitLab banner itself is still visible.
  • Documentation metadata is current again. Application/docs metadata is 2026.6.2.post13, docs-site latest points to the same version, the service-worker cache namespace is gitpulse-v135, dashboard Latest changes was refreshed, and stale docs references to 2026.6.2.post4 were updated.

[2026-06-02] - Dismissing a running sync keeps buttons locked + group-delete guardrails 2026.6.2.post12

Fixed

  • Dismissing a running sync no longer re-enables the buttons it locked. Clicking the X on a still-running job in the global progress bar deleted the job from the in-memory registry that the process-action guards read, so the Sync / Run Checks / Unlink / Delete Group buttons wrongly re-enabled mid-sync. Dismissing a running job now only hides its progress row - the job stays active so the buttons remain locked and the poller keeps tracking completion; it is removed normally once the sync actually finishes.
  • A project can no longer be restored while its group is deleted. Restoring a single project under a soft-deleted group would leave it orphaned (active yet hidden everywhere the group is hidden, and excluded from the course aggregate). The restore endpoint now rejects this with a clear message, and the project page shows a dedicated "This project's group is deleted" banner that points teachers to restore the whole group instead.

[2026-06-02] - One sync toast at the right time + CI lint restored 2026.6.2.post11

Fixed

  • A single project restore or sync no longer pops two "Sync completed" toasts. The per-project sync fired its own completion toast ("Sync completed") while the course-level aggregate fired another ("Sync completed!"). Their wording differs, so the 5-second text dedup missed them, and their job keys differ, so the session dedup missed them too - and the faster project toast appeared early, before the aggregate finished. Per-project / per-team completion toasts are now collapsed into the authoritative course-level aggregate toast, so one operation shows exactly one toast at the right time.
  • Restored the ruff lint CI gate. A helper script had unsorted imports and an unformatted body that failed the lint job; it is now sorted and formatted.

[2026-06-02] - One row per sync in the global progress bar 2026.6.2.post10

Fixed

  • The global progress bar no longer shows two rows for a single project restore or sync. Restoring or syncing one project registers a per-project "Syncing project..." entry, but the same work is also surfaced as a course-level aggregate ("DEVOPS: Syncing...") because the active-sync feed counts running jobs per course. The earlier dedup only collapsed the two while both were still running, so a project sync that finished first (common on tiny/empty repos) left a second "Sync completed" row. The per-project / per-team sync entry is now collapsed into the course aggregate for the same course regardless of completion state, so one operation shows exactly one row.

[2026-06-02] - Unlink/restore reflects instantly (page-cache invalidation) 2026.6.2.post9

Fixed

  • Unlinking or restoring a project or group now reflects instantly without leaving and re-entering the page. The detail pages serve their heavy content from a short-lived (60s) Redis page-fragment cache, but the unlink/restore endpoints never dropped that cache, so the in-place reload kept re-serving the previous (still-active or still-unlinked) body until the cache expired - which is why only navigating away and back appeared to work. The unlink and reactivate endpoints for both projects and groups now invalidate the affected project / group / course / student page caches immediately, so the reloaded page shows the new state right away. This also clears the phantom duplicate progress-bar row that appeared while the stale page re-registered the in-flight sync.

Fixed

  • Pages no longer fade, slide or blink when they load. Every entrance/appearance animation was removed - the cross-document page crossfade, the whole-page reveal, the staggered fade-in of lazy-loaded cards, and the alert/banner slide-in. Content paints instantly and stays static, so nothing flickers on load, reload or live refresh. The dead CSS and JS were deleted, not just disabled.
  • Unlinking or restoring a project now reflects immediately. The reload that flips the banner and action buttons was masked by the view-transition snapshot; with that gone the reload repaints cleanly and the new state shows right away instead of only after re-entering the project.
  • The global progress bar no longer shows a duplicate "Waiting in queue..." row. While a course-level sync runs, the redundant per-project/per-team entry for the same course is collapsed into the course aggregate.

[2026-06-02] - Restore sync completes and data refreshes in place 2026.6.2.post7

Fixed

  • Restoring a project no longer leaves the sync spinning forever. The reactivate endpoint returned a raw RQ job id, but the global progress bar polls GET /api/v1/jobs/{id} which resolves a job row - so the id 404'd forever and the "Syncing project..." bar never finished. The endpoint now creates a real background job and returns its id, so the bar completes.
  • The project page refreshes its data the moment a restore/sync finishes. Contributors, commits and pipelines are re-fetched in place, so you no longer have to leave the project and come back to see the updated numbers.
  • Small cards no longer flicker or "activate twice". The entrance animation on lazy-loaded panels (Contributors, Repository Stats, Protected Branches) now runs at most once per container instead of re-animating on every live refresh.

[2026-06-02] - Restore no longer freezes on "Restoring..." 2026.6.2.post6

Fixed

  • Restoring an unlinked project no longer freezes on "Restoring...". The previous reactivate flow waited for the background refresh job to finish before reloading; for an empty or slow repository that completion never arrived promptly, so the "Restoring..." button and the global progress bar hung for 20-40+ seconds and the page never reloaded. The project page now registers the refresh job in the persistent global progress bar and reloads immediately, so the project shows its restored state right away while the sync is tracked in the background.

Fixed

  • Unlinking a project or deleting a group no longer ejects you to another page. Both actions now reload the current page in place so you immediately see the "unlinked"/"archived" banner and the Restore button, instead of being redirected away ("kicked out").
  • Restoring a project surfaces the background sync and refreshes the data when it lands. The reactivate endpoint returns the enqueued refresh job id; the project page attaches it to the global progress bar, streams progress, and reloads only once the sync actually completes.
  • Empty repositories now state they are not counted. The empty-repo warning now explicitly says the project is excluded from the group average and rankings until the team pushes code.

[2026-06-02] - Tombstone projects and first-load animation flicker 2026.6.2.post4

Fixed

  • GitLab deletion_scheduled tombstone projects are no longer ingested or shown as restorable groups. When GitLab schedules a project for deletion it renames the path to <path>-<hash>-deletion_scheduled-<id> but keeps it visible in the group-projects API briefly. The discovery sweep now skips those tombstones and the course/team detail listings filter them out, so a project the teacher never deleted no longer appears as a ghost "Deleted" group with anomalous data.
  • Entrance animations and skeleton loaders no longer blink on first page load. The page-enter reveal now suppresses every descendant entrance animation during the reveal (instead of a hard-coded handful of classes) while leaving looping loader feedback - spinners, dots, bar/skeleton shimmer, status pulses - running.

Changed

  • Application/docs metadata bumped to 2026.6.2.post4; service-worker cache namespace bumped to gitpulse-v128; bundles and minified siblings rebuilt; dashboard Latest changes refreshed.

[2026-06-02] - Teacher restore of groups and projects 2026.6.2.post3

Added

  • Teachers can restore soft-deleted groups and unlinked projects. POST /api/v1/teams/{team_id}/reactivate and POST /api/v1/projects/{project_id}/reactivate are no longer admin-only; any teacher with course access can restore. Restoring a group precisely re-enables the members and projects deactivated by the most recent delete and enqueues a background sync so rankings, metrics and checks recover immediately.
  • Students keep read-only visibility of their own archived group's score. Deleted groups and unlinked projects stay reachable for their members with a clear archived banner explaining the data is frozen for reference, while staff see Delete/Unlink badges, restore buttons, and a toggle to show or hide archived rows.

Fixed

  • Deactivated groups and projects no longer inflate course aggregates. Contributor totals, green/red status counts, never-synced and stale-data indicators, and sync counters consider only active groups. The metrics worker, the project sync worker, and compute_team_metrics skip archived groups/projects so scoring, rankings and analytics exclude them consistently.

Changed

  • Application/docs metadata is 2026.6.2.post3; Service Worker cache is gitpulse-v127; bundles/minified siblings are rebuilt, Slovak translations are completed (0 missing strings), and restore endpoints return DTO-style JSON responses covered by unit tests.

Fixed

  • Dangerous group and project actions now have backend guards. DELETE /api/v1/teams/{team_id} and DELETE /api/v1/projects/{project_id} serialize through team/project mutation scopes and return 409 Conflict during active sync or run_checks, so direct API requests cannot bypass the UI guard and race score or project-state writers.
  • Delete Group also deactivates the group's projects. Soft-deleting a group now disables active GitLabProject rows together with active members, keeping dashboard state, sync discovery, and webhook processing aligned with the button contract.
  • Webhooks from unlinked projects are no longer processed. Authenticated GitLab webhooks for inactive projects return ignored without creating a WebhookEvent, so an unlinked project remains skipped until explicitly reactivated.
  • Course detail Delete Group uses the clicked button. Row removal no longer relies on a substring selector over data-action-args, and scoring toggle buttons now carry declarative sync run_checks process locks.
  • Application/docs metadata is 2026.6.2.post2; Service Worker cache is gitpulse-v126; bundles/minified siblings are rebuilt and destructive action guards are covered by focused unit/source-pattern tests.

[2026-06-02] - Latest changes limit and release-date guard 2026.6.2.post1

Fixed

  • Dashboard Latest changes now handles non-positive limits deterministically. _get_changelog_entries(limit=0) and negative limits return an empty list instead of Python negative slicing behaviour, where limit=-1 returned almost the whole widget feed.
  • Dashboard changelog release-date validation reads the package version. The test derives the current release day from the CalVer value in pyproject.toml, so the guard no longer duplicates a hand-maintained date.
  • Application/docs metadata is 2026.6.2.post1; Service Worker cache is gitpulse-v125; bundles/minified siblings are rebuilt and widget limit/date contracts are covered by unit tests.

[2026-06-02] - Team member badges without stray dots 2026.6.2

Fixed

  • Role and score badges in the team members table no longer render stray dots. Text-heavy cells in team-members-table still truncate with an ellipsis, but badge-only cells now use a dedicated member-badge-cell contract with text-overflow: clip, so Mentor, Maintainer and score pills do not get a dangling dot beside them.
  • Access-role badge mapping is single-source. The duplicated student/staff table branch ladder moved into the shared member_access_role_badge() component.
  • Application/docs metadata is 2026.6.2; Service Worker cache is gitpulse-v124; bundles/minified siblings are rebuilt and the badge-cell overflow contract is covered by unit tests.

[2026-06-01] - Docs-site nginx without 404 cache/index gaps 2026.6.1.post8

Fixed

  • The docs-site custom 404 page is no longer cacheable or indexable. The internal /404.html location now repeats the explicit Cache-Control: no-store, no-cache, must-revalidate header and adds X-Robots-Tag: noindex, nofollow, nosnippet, closing the gap caused by Nginx add_header inheritance rules inside location blocks.
  • The docs-site health endpoint is now an exact plain-text contract. /health is an exact location with default_type text/plain, so random /health... paths no longer look healthy and the endpoint does not rely on add_header for Content-Type.
  • Application/docs metadata is 2026.6.1.post8; Service Worker cache is gitpulse-v123; bundles/minified siblings are rebuilt and nginx header contracts are covered by unit tests.

[2026-06-01] - Local-timezone timestamps everywhere & no native pull-to-refresh 2026.6.1.post7

Fixed

  • Every timestamp now displays in your local timezone instead of UTC. "Last Synced" on the project page (missing the data-localize-time marker and using a timezone-less isoformat()), plus commit, issue, merge-request, CI-scenario, conflict, Docker-report, roster-import and analysis times, were showing raw server UTC. They now all render through one shared local_time() Jinja global that emits a canonical ...Z <time data-localize-time> element; localize-time.js converts it client-side and gained data-localize-format so custom formats (HH:MM, Mon DD, HH:MM) survive.
  • Chrome's native white pull-to-refresh spinner no longer fires on mobile / installed PWA. The overscroll suppression was set only on <body>, but the property must live on the root <html> to reach the viewport, so the native refresh kept showing next to the app's own indicator. The rule now also targets <html>.
  • Application/docs metadata is 2026.6.1.post7; Service Worker cache is gitpulse-v122; bundles/minified siblings are rebuilt.

[2026-06-01] - Flicker-free Last Updated column 2026.6.1.post6

Fixed

  • The groups-table "Last Updated" column no longer blinks between UTC and local time. The /sync-stream and /live-data SSE feeds shared the cell but rendered it differently, so it bounced between the server UTC stamp and the user-local time on every update. Both feeds now go through one ISO-keyed _renderLastUpdated() helper that only repaints when the underlying timestamp actually changes.
  • Application/docs metadata is 2026.6.1.post6; Service Worker cache is gitpulse-v121; bundles/minified siblings are rebuilt.

[2026-06-01] - Declarative actions without inline expression eval 2026.6.1.post5

Changed

  • Component actions no longer use data-action-expr or new Function. The shared frontend core only executes named data-action handlers with JSON arguments, and project empty-state buttons now call clickElementById through that structured contract.
  • Application/docs metadata is 2026.6.1.post5; Service Worker cache is gitpulse-v120; generated bundles and minified siblings were rebuilt.

[2026-06-01] - Immediate process guard refresh on job updates 2026.6.1.post4

Fixed

  • Process-action locks now react to gp:job-update. The shared guard refreshes when the global job bar starts or updates a sync/import/run-checks job, so conflicting buttons are disabled immediately instead of waiting for the fallback polling interval.
  • Application/docs metadata is 2026.6.1.post4; Service Worker cache is gitpulse-v119; generated bundles and minified siblings were rebuilt.

[2026-06-01] - Process guard root refresh and component cleanup 2026.6.1.post3

Changed

  • The unused process_status.html component was removed. Dashboard and course detail fragments now use the external import-stream.js module and the shared global-job/process-guard modules instead of importing legacy inline-JS macros.
  • GP.refreshProcessActionGuards(button) now evaluates the button itself. Single dynamically inserted controls are locked immediately during active sync/import/run-checks processes, even when no parent container is refreshed.
  • Application/docs metadata is 2026.6.1.post3; Service Worker cache is gitpulse-v118; generated bundles and minified siblings were rebuilt.

[2026-06-01] - Cross-document View Transition skip no longer logged 2026.6.1.post2

Fixed

  • Cross-document View Transitions no longer log AbortError: Transition was skipped. Course tabs and page links navigate cross-document, where the CSS @view-transition { navigation: auto } rule drives the crossfade. When a transition is skipped (a second tab is clicked before the first settles, the active tab is re-clicked, or the tab is hidden mid-navigation), the browser rejects the transition promises. They are now handled at the source via the pageswap and pagereveal navigation events instead of surfacing as an uncaught "(in promise)" rejection.
  • Application/docs metadata is 2026.6.1.post2; Service Worker cache is gitpulse-v117; generated bundles and minified siblings were rebuilt.

[2026-06-01] - External course-tabs module and component cleanup 2026.6.1.post1

Changed

  • Course tab keyboard navigation now lives in course-tabs.js. The shared course-navigation partial no longer embeds inline JavaScript; the behavior is included in core.bundle.js and covered by a regression test.
  • The shared native select component no longer exposes an inline onchange escape hatch. Current callers use declarative data/action contracts or Alpine bindings.
  • Application/docs metadata is 2026.6.1.post1; Service Worker cache is gitpulse-v116; generated bundles and minified siblings were rebuilt.

[2026-06-01] - Tooltip-without-hover fix 2026.6.1

Fixed

  • Tooltips no longer appear without hovering. Tippy's default trigger is mouseenter focus, so when a modal's focus-trap (or a mouse click) programmatically focused a button carrying data-tippy-content - most visibly the close (X) button - its tooltip popped open with no hover. Tooltips now suppress focus-triggered shows unless the focus is from the keyboard (:focus-visible), preserving accessibility while removing the spurious popups.
  • Application/docs metadata is 2026.6.1; Service Worker cache is gitpulse-v115.

[2026-05-31] - Dashboard cache safety and docs HTML fix 2026.5.31.post3

Fixed

  • Authenticated dashboard HTML is no longer stored by the Service Worker. The worker bypasses /dashboard navigations and its shared cache helpers refuse Cache-Control: no-store or private responses, matching the security middleware contract.
  • Logout now sends the documented browser cleanup header. GET /dashboard/logout deletes the session cookie and returns Clear-Site-Data: "cache", "storage".
  • The documentation logo override no longer creates nested anchors. The custom MkDocs Material partial renders only the image/icon content, leaving header and nav link ownership to the theme templates.
  • Application/docs metadata is 2026.5.31.post3; Service Worker cache is gitpulse-v114.

[2026-05-31] - Centralized documentation SEO policy 2026.5.31.post2

Changed

  • Documentation SEO signals are now managed in one place. The MkDocs hook hooks/seo_policy.py marks technical and internal sections (vyvojari, operatori, gitlab, bezpecnost, api, diplomovka, changelog, tags) as noindex, follow, nosnippet, so Google does not surface random runbooks, token examples, or the internal changelog as sitelinks. Public product, student/teacher onboarding, and legal pages remain indexable.
  • Sitemaps are split by ownership. Root /sitemap.xml belongs to the app and lists only /dashboard/login; /docs/sitemap.xml belongs to MkDocs and the hook removes noindex URLs from it. robots.txt explicitly lists both sitemaps, giving Google consistent signals without duplicate docs URLs in the root sitemap.

[2026-05-31] - Dashboard, analytics and admin-system SRP split 2026.5.31.post1

Changed

  • Split the dashboard, course-analytics and admin-system page controllers into Single-Responsibility source parts. Following the Admin Courses split, the three remaining ~1.1k-line page scripts are now authored as ordered _parts/<page>/NN-*.js fragments and concatenated verbatim back into the served file by the bundle builder. The output is byte-identical, so there is zero runtime change - only the source is now small and single-responsibility, matching every other split page controller.

[2026-05-31] - Admin Courses SRP split and cache refresh 2026.5.31

Changed

  • Admin Courses frontend now follows the page-controller SRP pattern. pages/admin-courses.js is generated from _parts/admin-courses/00-core.js, 10-lifecycle.js, and 20-events.js, so configuration, lifecycle mutations, and event delegation have separate owners while the served file remains a deterministic build artifact.
  • Course lifecycle row updates use shared UI contracts. Archive/activate/delete swaps now render the shared status-indicator markup, icon-only lifecycle buttons, refreshed tooltips, and scoped process guards after every dynamic DOM update.
  • Release metadata and static delivery refreshed. Version is 2026.5.31, bundles and .min siblings were rebuilt, dashboard Latest changes was updated, and Service Worker cache is gitpulse-v113.

[2026-05-30] - Animated scoring eye toggle, consistent tooltips, live recompute 2026.5.30.post11

Changed

  • Polished the scoring include/exclude (eye) control across the member tables. The eye / eye-off icon now animates with a springy pop when a student is toggled, the button stays visible and tinted while a row is excluded so the reversible state reads clearly, and hover/press add subtle scale feedback. The control now uses a consistent Tippy tooltip (no more duplicate native browser tooltip), and the excluded-row dimming transitions smoothly instead of snapping. Toggling a student still recalculates the group average, traffic light, contributor counts and retired grading scores live - no page reload. All motion respects prefers-reduced-motion.

[2026-05-30] - Fixed elapsed timer reset between navigations, and SW v101 2026.5.30.post10

Fixed

  • Fixed the progress-bar elapsed timer resetting on every page navigation. When a course-sync bar was recreated after navigating to another page, the new bar was stamped with the current clock time instead of the server-authoritative sync start time, so the seconds counter restarted from zero each navigation. The bar now anchors its elapsed timer to the server started_at_ms and keeps the earliest known start, so the timer stays monotonic across navigations.

[2026-05-30] - Monotonic task start time, SRP caching refactor, and SW v100 2026.5.30.post9

Fixed

  • Fixed progressive restarted times in sync job bar. Passive reconcilers (e.g. course card scans, dashboard polling updates) could restage the global progress bar as a new run, reset execution progress to null, and bump start time to the current clock time. The SSE progress indicators now correctly protect and retain monotonic start times, preventing seconds-counters in the progress panel from resetting to zero.

Changed

  • Refactored caching layers to enforce strict Single Responsibility Principle. Verified zero cross-tenant leak vectors on templated CSP nonces and CSRF tokens across page-fragment page caching endpoints.
  • Service Worker cache partition upgraded to v100. Static cache keys bumped to invalidate previous offline fragment cache baselines cleanly.

[2026-05-30] - Instant global progress bar with a "Connecting..." stage 2026.5.30.post8

Changed

  • The global progress bar now appears instantly with a "Connecting..." stage. Starting a course sync (Sync All), a project sync, or a sync from the course settings page used to surface the global progress bar only after the backend POST returned. With the GitLab reachability pre-flight (which retries and can take several seconds) the user saw no bar during that window and could re-click the button. The bar now shows an indeterminate "Connecting to GitLab..." spinner the moment the button is clicked, which also makes the existing re-click guard fire immediately so double submits are blocked. Once the sync is enqueued the bar promotes to the real "Syncing: X/Y groups..." message; if the request fails the error is shown directly in the bar (not just a transient toast). A safety timeout guarantees the connecting spinner can never hang.

[2026-05-30] - Sync reliability: GitLab pre-flight retry 2026.5.30.post7

Fixed

  • Sync no longer fails when GitLab answers slowly once. The sync entrypoints run a GitLab reachability pre-flight before enqueuing jobs. It used a single 4 s request with no retry, so one transient slow response returned 503 "GitLab server is currently unreachable" and the sync did nothing. The pre-flight now retries once (5 s per attempt, 0.4 s backoff) and only treats two consecutive failures as unreachable.

[2026-05-30] - Week Analytics fixes + telemetry typing 2026.5.30.post6

Fixed

  • Week Analytics opens on the latest week with data. Once teaching winds down students stop committing, so the current week is often empty while the weekly scoring job keeps carrying compliance forward. The explorer used to auto-select the current (empty) week and show "No GitLab events tracked this week", which looked like data loss. It now auto-selects the most recent week with real activity or a compliance trend. A prod DB probe confirmed no data loss - there were simply no commits after teaching week 12.
  • Exam-period weeks reachable and styled in Week Analytics. The week selector only ran 1..semester_weeks, so exam-period (skúškové obdobie) weeks had no pill and never showed the amber EXAM PERIOD treatment. The selector now extends through the exam period with amber "Exam" pills and an "Exam Period · Week N" chip in the detail header.
  • Course search empty state. Filtering the dashboard "Your courses" grid to a non-matching query now shows a proper empty-state component instead of a blank area.
  • Six telemetry models migrated to SQLAlchemy 2.0 typing. FlaggedTeamResolution, StudentComplianceSnapshot, ClicksAvoidedMetric, TeacherActivityMetric, TelemetryAggregate and EvaluationReport were rewritten from legacy Column(...) assignments to Mapped[] / mapped_column, matching the earlier SupportTicket migration. Pylance now infers attribute types correctly (e.g. flagged_at: datetime, week_number: int) instead of Column[X], eliminating the false-positive type errors in pilot_telemetry.py and the redundant cast(...) wrappers they required, plus a missing None guard before a datetime subtraction. No behaviour or database-schema change.

[2026-05-30] - Green container scan (protobuf CVE) 2026.5.30.post5

Fixed

  • Container security scan (Trivy) is green again. The transitive protobuf dependency was held at 4.25.9 by a protobuf<5.0 ceiling that also pinned opentelemetry-proto to the older 1.27 line. That protobuf build carried a fixable HIGH/CRITICAL vulnerability, CVE-2026-0994, failing the container-scan-api and container-scan-worker jobs. The ceiling was raised to protobuf>=5.29.6,<6.0, moving protobuf to the fixed 5.29.6 and letting OpenTelemetry resolve up to 1.42.1 (stable 1.x API - no app code change). requirements.lock was regenerated on Linux to match CI.

[2026-05-30] - Green CI pipeline + faster analytics 2026.5.30.post4

Fixed

  • CI pipeline is green again. Commit f0cb9816 shipped a requirements.lock generated on Windows, so it contained colorama (Windows-only) and was missing uvloop (Linux-only). CI runs on Linux (python:3.12-slim), so lockfile-check regenerated the lock, got the opposite package set, and the job went red. The lock is now generated on Linux (matching CI). mypy also flagged a leftover cast(int, ...) and a # type: ignore[assignment] that became redundant after the post3 SupportTicket typing - both removed - and home_changelog.py was reformatted with ruff format.
  • The course analytics page no longer hangs on first load. The heavy analytics fragment takes ~60-95 s to compute on a populous course, which exceeded the global 90 s htmx timeout - so after every deploy (the cache key includes GIT_SHA, i.e. a cold start) the first load hit htmx:timeout and "nothing loaded". That fragment's timeout is now 180 s and the page-cache TTL was raised from 60 s to 600 s, so once analytics is computed it stays warm and subsequent loads are instant. Version 2026.5.30.post4.

Fixed

  • Migrated the SupportTicket model to SQLAlchemy 2.0 typing. Legacy Column(...) assignments made Pylance infer every attribute as Column[X], producing 11 false-positive errors in the dashboard support route. The model now uses Mapped[...] / mapped_column(...) like its siblings. Pure typing change - ORM behaviour is identical; mypy and the targeted tests pass.
  • Made lockfile-check deterministic (again). exclude-newer pointed at a future instant, so each CI re-resolve picked up packages published "today" and requirements.lock drifted red. It is now pinned to a past instant, and a new .gitattributes forces requirements.lock to eol=lf so Windows CRLF commits cannot break the lint gate. Version 2026.5.30.post3.

[2026-05-30] - SRP split (largest source file -> JSON data) 2026.5.30.post2

Changed

  • Split the single largest source file by single-responsibility. src/app/api/dashboard/home_changelog.py (1511 lines) held ~1490 lines of hand-maintained "Latest changes" data inline. The data now lives in home_changelog_entries.json and the Python module is a thin localisation loader. Data is separated from logic; the dashboard widget and the changelog regression tests read the same entries with no behaviour change.
  • Version 2026.5.30.post2. Backend source-structure only - no template, JS or CSS changes, so bundles and the service-worker cache are unchanged.

[2026-05-30] - Continued SRP split (page controllers) 2026.5.30.post1

Changed

  • Continued the single-responsibility (SRP) source split to the next tier of large page controllers. pages/course-detail.js (1414 lines) and pages/team-detail.js (1216 lines) are now split into ordered, build-only _parts/ fragments that scripts/build_bundle.py concatenates verbatim: course-detail/ into 00-sync-stream / 10-teams-crud / 20-sync-all / 30-course-data, and team-detail/ into 00-core / 10-sync / 20-analysis-boot / 30-live. The served files are regenerated byte-for-byte identically (verified via SHA-256 and build_bundle.py --check), so there is zero runtime or behaviour change.

[2026-05-30] - SRP split of all 1500+ line files 2026.5.30

Changed

  • Split every 1500+ line source file by the single-responsibility principle (SRP) into smaller modules with no behaviour change. The front-end giants (core.js, global-job-bar.js, tour-engine.js, project-detail.js, course-settings.js and responsive.css) were broken into build-only parts under _parts/ folders that the build script concatenates back into an identical artifact (verified byte-for-byte on the minified bundle). Back-end API modules (courses, sync, telemetry, teams, projects, report/sync jobs) had their helpers extracted into neighbouring modules.

[2026-05-29] - Skeletons reuse real components 2026.5.29.post1

Changed

  • Loading skeletons now reuse the real page components instead of a parallel gl-skeleton-* layout. The skeleton primitives in components/loaders.html defined their own card / stat / table / form containers, each with a separate set of responsive media queries, which drifted from the live UI. The sk_stat, sk_stats, sk_card, sk_table and sk_form macros now emit the real component markup - .stat-card / .stats-grid / .card / table.data-table / .form-group - with .gl-skeleton shimmer bars in the leaf slots, so layout and responsiveness are inherited directly from the live components and can no longer diverge. Macro signatures are unchanged, so no call sites required edits.
  • Removed the now-dead skeleton CSS (.gl-skeleton-card, .gl-skeleton-stat, .gl-skeleton-table/-thead/-row with their nth-child variance and media queries, .gl-skeleton-stats-grid, .gl-skeleton-field/-label/-input). The .gl-skeleton primitive, .gl-skeleton-tr, .gl-skeleton-cards-grid and the .gl-skeleton-list* family are retained.

Release

  • Version 2026.5.29.post1. Template + CSS change only; bundles rebuilt. No backend changes.

[2026-05-29] - Deterministic lockfile-check 2026.5.29

Fixed

  • The lockfile-check CI job is now deterministic and no longer fails on unrelated upstream releases. The job runs uv pip compile pyproject.toml --extra otel -o requirements.lock and then git diff --exit-code requirements.lock. Because almost every dependency uses an open >= range, uv re-resolved to whatever was newest on PyPI at job time, so the lockfile "drifted" and the job went red the moment any direct or transitive dependency published a new version - even though nothing in the repository had changed. Added a [tool.uv] section with exclude-newer = "2026-05-30T00:00:00Z" to pin resolution to a fixed point in time. requirements.lock regenerates byte-identically and the gate stays reproducible until the timestamp is deliberately bumped.

Release

  • Version 2026.5.29. No service-worker cache bump: this is a CI/build-config change only, with no client-facing asset changes.

[2026-05-23] - Commit category UI polish 2026.5.23.post2

Fixed

  • Course Analytics checklist chart paints commit-category bars in the correct pink (#ec4899). After the R13 remap to commit in post1, the categoryColors map in course-analytics.js still keyed on the orphan quality: (the engine never emits that value anymore). R13 and any commit-category custom checks fell through to the custom purple fallback. Renamed the key to commit: (same pink) so the chart's category stripe matches the badge colour used elsewhere in the UI.
  • Course Settings filter toolbar got the missing Commit button. The filter bar above the checks table offered Issue, MR, Review, CI/CD but not Commit, so teachers could not narrow the table to R13 or custom checks with category="commit". Added the button via the existing filter_btn macro; the JS filter handler is data-driven and required no changes.

Release

  • Version 2026.5.23.post2, service-worker cache gitpulse-v99.

[2026-05-23] - Commit category across the whole stack 2026.5.23.post1

Fixed

  • Commit category is now first-class across the whole check stack. The Edit Check modal previously offered only five options (issue, mr, review, ci, custom); any stored check with category=commit rendered with an empty Category field because the value was not in the option list and the JS badge/label maps had no entry. Added ('commit', t('Commit')) to the dropdown and commit: entries to CATEGORY_BADGES (badge-purple) and CATEGORY_LABELS in course-settings.js.
  • Pydantic schemas and backend validators accept commit. Extended ALLOWED_CATEGORIES and the two Literal[...] unions in schemas/custom_check.py so the custom-check API no longer returns 422. Aligned the inline valid_categories set and the LLM suggestion docstring in api/dashboard/course_checks.py and the column comment in models/custom_check.py.
  • R13 (Commit Quality) is now mapped to the commit category. Previously _CHECK_CATEGORIES in compliance/engine.py mapped R13 to a one-off "quality" bucket that no other code recognised, so commit_score was always None in category breakdowns and pilot telemetry. Re-mapped to "commit", added "commit" to the always-present all_cats set in _compute_category_scores, mirrored the change in pilot_telemetry._FALLBACK_CHECK_CATEGORIES, and added "commit" to the iteration tuple in _derive_category_scores.

Release

  • Version 2026.5.23.post1, service-worker cache gitpulse-v98.

[2026-05-23] - Scoped button and modal guards 2026.5.23

Fixed

  • Buttons and modals now cover the remaining course/project write actions. Course archive/activate/delete, dashboard Add Group and course edit, course-settings scoring/config imports, admin course actions, and project unlink check the active course/team/project process scope before opening a modal or mutating data.
  • Process-lock click guard is fail-closed. Every element with data-process-lock-when is checked again at click time, not only by the 1.5s disabled-state poll, so stale button state cannot let a conflicting sync/import/run-check action through.
  • Dynamic buttons inherit scope. Buttons inserted after archive/activate operations are refreshed through the shared process guard without a page reload.

Release

  • Version 2026.5.23, service-worker cache gitpulse-v97.

[2026-05-22] - Spinner anomalies and toast recovery 2026.5.22.post7

Fixed

  • SVG spinners (.spinner, .spinner-inline) now set transform-origin: 50% 50% and transform-box: fill-box, so they no longer wobble around the top-left corner when the parent card composites a hover transform, and the animation stays smooth.
  • Lost completion toast during navigation. When a sync finished exactly during a navigation (SSE complete arrived between beforeunload and unload), _preserveFlashToast() had already snapshotted an empty toast container and the toast was lost. The _restoreJobs done/error/finishing branches now enqueue a completion replay toast on the next page load (session-scoped dedup blocks duplicates inside the same page-load) and the nav-replay window widened from 3s to 8s so slow navigations still surface the missed completion.

[2026-05-22] - Scoped process locks 2026.5.22.post6

Fixed

  • Scoped process locks now follow the course/team/project hierarchy. Per-team sync, per-project sync, and run-check jobs use stable scoped job keys, so active work in one group does not block unrelated courses or groups, while conflicting actions in the same team/project scope remain locked.
  • Course-level Run Checks stays locked until the real work finishes. Added an aggregate run-check status endpoint and wired the global job bar to poll it, preventing modals/buttons from unlocking immediately after fan-out enqueue while per-team jobs are still running.
  • Exclude from scoring is guarded consistently. Toggle buttons evaluate the current row's scope, use shared icons, and the backend takes the team advisory lock before checking active scoring jobs.

Release

  • Version 2026.5.22.post6, service-worker cache gitpulse-v95.

[2026-05-22] - Scope-aware imperative guards 2026.5.22.post5

Fixed

  • Imperative action guards now respect scope. Click handlers for per-team sync, per-project sync, run checks, and course-level sync-all/import/run-checks all pass a course/team/project scope to guardProcessAction, so the "Wait for the active sync to finish" toast no longer fires for actions in unrelated courses.
  • Dashboard Import buttons. Locked only by another active import, not by syncs in any course.
  • Course-detail skeleton. Renders all four action button placeholders (Add Group / Import from GitLab / Sync All / Run Checks) so it mirrors the loaded layout 1-to-1.

Release

  • Version 2026.5.22.post5, service-worker cache gitpulse-v94.

[2026-05-22] - Per-course sync independence 2026.5.22.post4

Fixed

  • Cross-course sync no longer blocks unrelated courses. The process guard now scopes locks to a specific course, team, and project. Starting a sync in one course leaves import and sync buttons in other courses fully usable; run-checks and sync still gate each other, but only within the same team or project; dashboard Import buttons are independent of per-course jobs. Every job-start call-site tags meta with courseId, teamId, projectId, and jobKind so guards can filter precisely.

Release

  • Version 2026.5.22.post4, service-worker cache gitpulse-v93.

[2026-05-22] - Process action guards 2026.5.22.post3

Fixed

  • Conflicting sync/import/run-check actions are blocked consistently. Buttons and modal triggers now use a shared process guard, and page handlers perform the same preflight before starting work programmatically.
  • Project sync uses the correct global-job API surface. The project page no longer destructures missing window.GP aliases, so active sync detection, progress registration, and completion cleanup run through the canonical global-job helpers.
  • Backend sync/check races now fail closed. Team/project/course sync and run-check endpoints share team-scope advisory locks and return 409 for conflicting active jobs instead of enqueuing overlapping work.
  • ASGI framework dependencies are hardened. FastAPI now starts at 0.136.1 and Starlette at 1.0.1, clearing the current Starlette PYSEC-2026-161 audit finding without loosening runtime compatibility.

Release

  • Application version is 2026.5.22.post3; Service Worker cache is gitpulse-v92.

[2026-05-22] - Scoring exclusion consistency 2026.5.22

Fixed

  • Exclude from scoring is coherent across team and project dashboards. The backend toggle now refuses active sync/run-checks races, uses a row lock, recomputes team metrics immediately, and invalidates team, course, project, and student page-cache scopes.
  • Project live data respects scoring exclusion. Excluded members no longer feed the project live-data scored cohort or Issues to Address refreshes.
  • The member toggle has one frontend owner. Team and project pages share the same handler, preserve inherited/manual/csv member categories when a student is included back into scoring, and block conflicting action while sync/import/ check runs are active.

Release

  • Application version is 2026.5.22; Service Worker cache is gitpulse-v89.

[2026-05-21] - Release housekeeping 2026.5.21.post26

Changed

  • Dashboard changelog dates are normalized. Latest changes no longer contains entries dated after the current 2026-05-21 release day, and the regression test now rejects future-dated dashboard changelog rows.
  • Deploy workspace hygiene is cleaner. Old ignored deploy-post*.log / redeploy.log files and the one-off ignored SRP split helper script were removed before redeployment.

Release

  • Application version is 2026.5.21.post26; Service Worker cache is gitpulse-v76.

[2026-05-21] - Sync cache invalidation module 2026.5.21.post25

Changed

  • Sync cache invalidation has a dedicated owner. _invalidate_dashboard_cache_after_sync moved from the large workers/sync_job.py worker into workers/sync_cache.py. Full-team and per-project sync still invalidate project, team, course, and student page-cache scopes, but the worker keeps less cache plumbing inline.
  • Release metadata is aligned with the current package baseline. Root/docs changelogs and version references now point at 2026.5.21.post25 and gitpulse-v75.

Tests

  • Retargeted the sync cache invalidation regression test to app.workers.sync_cache.

Release

  • Application version is 2026.5.21.post25; Service Worker cache is gitpulse-v75.

[2026-05-21] - Dashboard role-context module 2026.5.21.post3

Changed

  • Dashboard role-context helpers have a dedicated owner. Sidebar badge counts, greeting subtitles, student dashboard context, and admin dashboard context moved from dashboard/home.py into dashboard/home_context.py. home.py now focuses more tightly on page orchestration.
  • Context fast paths are covered. Student context returns the default zero-state without DB access when there are no active courses or no GitLab username, and student sidebar badges stay DB-free.

Release

  • Application version is 2026.5.21.post3; Service Worker cache is gitpulse-v53.

[2026-05-21] - Dashboard Latest changes split + course health module 2026.5.21.post2

Changed

  • Dashboard Latest changes has a dedicated owner. The hand-maintained release entries and locale selection moved from the large dashboard/home.py route into dashboard/home_changelog.py, keeping the home route focused on page orchestration instead of release-note data.
  • Course-card health SQL moved out of the route. Per-course RED-team health now lives in dashboard/home_course_health.py. The helper returns an empty result immediately when there are no active courses, avoiding unnecessary DB work for onboarding and empty states.

Tests

  • Added regression coverage for the empty active-course course-health path and retargeted dashboard changelog tests to home_changelog.py.

Release

  • Application version is 2026.5.21.post2; Service Worker cache is gitpulse-v52.

[2026-05-21] - Dashboard stats split + 429 asset cache busting 2026.5.21.post1

Changed

  • Dashboard home stat-card SQL has a dedicated module. Role-specific stat cards moved from the large dashboard/home.py route into dashboard/home_stats.py. RED-team counts and average compliance now share a single latest-team-snapshot subquery, and student dashboards with no active courses return zero-value cards without unnecessary DB queries.
  • Standalone 429 pages use deploy-hash static URLs. The rate-limit middleware cannot rely on Jinja static_url(), so its browser HTML now uses BUILD_HASH query strings for favicon, stylesheet, and illustration assets.
  • Supply-chain extras are narrower. Added idna>=3.15, removed the redundant safety optional scanner in favour of pip-audit + CycloneDX, and moved Locust into a dedicated load extra.

Tests

  • Added regression coverage for the no-active-course student stat-card path and for deploy-hash /static/... links on the browser rate-limit page.

Release

  • Application version is 2026.5.21.post1; Service Worker cache is gitpulse-v51.

[2026-05-19] - Page/static cache hardening 2026.5.19.post25

Fixed

  • Page-fragment cache variants are delimiter-safe. Variant parts are JSON-serialized before the BLAKE2b digest is computed, so values like ("a|b", "c") and ("a", "b|c") cannot collapse to the same cache key. The Redis namespace is bumped to gp:page:v2, forcing a safe cold start for existing fragments.
  • Static cache headers are canonical. /static/* responses replace any existing Cache-Control header instead of appending duplicates, merge Vary: Accept-Encoding with existing Vary values, and only use the one-year immutable cache profile when ?v= is a valid hex content hash.
  • Dashboard home cleanup. Removed unused stat-card parameters and stale locals from home.py; the support-ticket sidebar badge now clearly counts tickets in visible courses.

Changed

  • Application version is 2026.5.19.post25; Service Worker cache is gitpulse-v50.

[2026-05-18] - global-job-bar stability + webhook metrics debounce 2026.5.18.post24

Fixed

  • Progress bar no longer flickers right after a sync completes. Both the 30 s /sync/active watchdog and the htmx:load reconcile skip work for 3 s after completeGlobalJob fires. Previously a stale /sync/active snapshot or a navigation could re-add the just-faded bar momentarily.
  • Stale Last Synced is now visible in logs. _invalidate_dashboard_cache_after_sync failures are logged at WARNING (was DEBUG) in both the full-team-sync and per-project-sync paths, so Redis/SCAN hiccups no longer go silent.

Performance

  • Webhook bursts coalesce metrics-refresh enqueues per team. A burst of N events from the same team within 2 s now enqueues a single metrics_refresh job instead of N (Redis SET ... NX EX 2 debounce). The job is idempotent - coalescing loses no state. Removes most of the Redis chatter from CI pipelines that emit 10+ events in quick succession.

[2026-05-18] - Animated empty state + Diff column fix 2026.5.18.post23

Fixed

  • The Diff column in the MR table no longer shows - files. When files_changed, lines_added and lines_removed are all missing the cell renders an em-dash (-). If only the file count is missing but ± line counts are available, only the delta is shown.
  • Empty-state icons on the project detail page are animated like the rest of the dashboard. "Issues to Address -> No scored members yet" and "All passing" share the emptyStateFloat keyframes with the other .empty-state-icon elements, with a prefers-reduced-motion: reduce opt-out.

Changed

  • Dashboard Latest changes widget trimmed to the most recent 5 entries with one-paragraph summaries; the long history lives here and in docs-site/docs/changelog*.md.
  • Application and docs metadata are 2026.5.18.post23; Service Worker cache is gitpulse-v48.

[2026-05-18] - Sync cache invalidation + CSP cleanup 2026.5.18.post22

Fixed

  • Completed sync jobs now eagerly invalidate dashboard fragment caches. Team sync and per-project sync both drop the affected project, team, course/activity/analytics, and active-student cache scopes, so users do not receive stale HTML fragments after a job completes.
  • Remaining inline timers and the Course Settings LLM helper moved out of templates. The 401/429 redirect/countdown logic and LLM resource/prompt helper now run from external JS; CSS/minified assets were regenerated.

Changed

  • Application and docs metadata are 2026.5.18.post22; Service Worker cache is gitpulse-v47.

[2026-05-18] - Project-scoped sync in activity feed + semester audit 2026.5.18.post21

Added

  • The Follow the latest updates widget on the dashboard now distinguishes project-scoped sync runs. When a teacher clicks Sync directly on a specific project (not on the whole group, not via the global Sync all), the entry renders as "System - Project synced from GitLab", the detail shows the project name, and the link points to /dashboard/projects/{id} instead of the team page. Group-scoped and global syncs are unchanged. run_checks keeps appearing on every level it can be triggered (per-group and the per-course fan-out).

Fixed

  • System-wide semester scoping audit. Several aggregate / contributor queries were spanning all of history instead of the active semester window, which inflated counters and let stale project health linger across terms. Fixed:
  • course_detail, get_course_sync_status, and the /courses/{id}/sync-stream SSE builder now thread commit_since = semester_floor_utc(course.settings) through team_contributor_counts() and total_distinct_contributors().
  • The DB fallback in api/projects.py:get_contributors filters commits by the project's course semester floor.
  • The student -> primary-project mapping in dashboard/teams.py now respects the same _commit_window_clause as the rest of the team-detail page (a student who switched groups between semesters no longer remains mapped to the prior-semester project).
  • Per-team pipeline aggregates (total / passed / failed / latest_status) and the alternative course-pipeline path in dashboard/courses.py now both clamp by semester_floor_utc, so a successful run from last term no longer masks the real health signal.

Removed

  • Reverted an experimental project-level activity card on the project-detail page - the dashboard Follow the latest updates widget is the single source of truth; the project detail page does not duplicate it.

[2026-05-18] - Per-project Sync + precise SYNCING badges 2026.5.18.post17

Added

  • New endpoint POST /api/v1/projects/{project_id}/sync. The Sync button on the project detail page no longer syncs the whole team - it refreshes only that project's issues, MRs, MR notes, pipelines, and commits (default branch + per-MR source branches). Member sync and project discovery remain team-scoped (they only make sense at the group level). The endpoint takes an advisory lock on project_id, deduplicates against an in-flight team-wide sync for the same team, and writes a sync_project audit record.
  • Per-row SYNCING/QUEUED/FAILED badge on the team-detail Projects table. GET /api/v1/courses/{course_id}/sync-status now returns sync_status_by_project, so the team detail page highlights only the specific project rows whose sync is actually running - instead of lighting up every active row whenever any team-level sync runs.

Fixed

  • Phantom "½ groups done" counter during a single in-flight sync. The X/Y groups counter no longer includes project-scoped jobs (they have a separate project_sync_running_count) and now uses a 3-minute burst window for completed/failed - the old 2-hour cutoff in the denominator was the main cause of the misleading "½".
  • Migration 047_bg_job_project_id adds BackgroundJob.project_id (FK to gitlab_projects ON DELETE SET NULL) + index - the schema scaffolding for per-project sync tracking.

Technical

  • Service Worker cache: gitpulse-v46.

[2026-05-18] - Truthful sync toasts + SYNCING badge in tables 2026.5.18.post16

Fixed

  • False "Sync started!" toast on the project detail page. The Sync button used to show "Sync started!" on every click - even when the backend returned skipped_fresh (data already fresh), failed (Redis down), or an existing in-flight job. The handler now branches on the real status field and shows a truthful toast (info/danger/success).
  • Phantom "½ groups" slots in the global job bar after a one-off project-level sync. completeGlobalJob('sync', ...) is now invoked in every branch of the handler, so the deferred slot always closes.
  • Sync button unlocked too early - the old finally block released it right after the POST. The button now stays in loading state until the SSE stream emits complete / error for its specific job_id.

Added

  • Dynamic SYNCING / QUEUED / FAILED badges in the Status column of the Subgroups and Projects table on the group detail page. While the group is syncing (locally triggered or via a course-level Sync All), every active project row swaps its Active/Template badge for the live sync state. Original badges are restored when the sync settles. The mechanic mirrors the per-row status already present in the groups table on the course detail page.

[2026-05-18] - Project-scope Sync + removed duplicate Rotate Webhook Secret 2026.5.18.post15

Added

  • Sync button on the project detail page. Previously the only way to refresh a single project from GitLab was from the parent course/group view. The toolbar at /dashboard/projects/<uuid> now has its own Sync button next to Unlink, gated by can_edit and team, posting to POST /api/v1/teams/{team_id}/sync and surfacing progress through the global job bar exactly like the group-level sync.

Removed

  • Rotate Webhook Secret button + POST /api/v1/projects/{project_id}/rotate-secret endpoint. The per-project webhook secret rotation UI duplicated the multi-scope Settings -> Webhook Configurations flow (introduced in migration 041) without offering anything unique. Button, JS handler, FastAPI endpoint, Pydantic ProjectSecretRotated schema, tour step, contract test row, and security doc section are all gone. Project.webhook_secret_hash is still generated on import and verified by webhook delivery - only the manual rotation surface is retired.

Fixed

  • Shorter Unlink tooltip. The 4-sentence reactivation rules essay clipped on smaller viewports; collapsed to one line.

[2026-05-18] - Removed duplicate Sync Group Data modal 2026.5.18.post14

Fixed

  • Removed duplicate Sync Group Data modal. The full-screen sync modal opened on every Sync / Sync All click was duplicating the same Project N/15: ... synced (X%) progress already shown by the persistent global job bar at the top of the page. Two copies of the same line on screen at once was redundant. Progress is now owned exclusively by the global bar; modal markup (team/_modals.html) is retired and _DEFAULT_LOCAL_SELECTORS.sync in global-job-bar.js is gone too.
  • Sync button no longer overflows on mobile. triggerSync / triggerSyncAll used to call setButtonLoading(btn, true, 'Syncing...') which replaced the icon button's HTML with the literal Syncing... string. On narrow viewports that text bled outside the icon-sized button as /ncing. Dropped the loadingText argument - the button now keeps its original icon / label and gets a .loading class for the spinner state, matching every other action button.

[2026-05-18] - Student profile polish & loader contrast 2026.5.18.post13

Changed

  • Student profile polish. The yellow Run Checks button from post12 no longer floats above the page as an orphan strip - it now lives inside the Student Profile / My Profile / Teacher Profile card header next to the title, so it sits on a surface instead of the raw page background.
  • Project contributors widget canonical names. The Contributors widget on the project detail page now resolves matched git authors to Student.full_name (lookup by email, then by lowercased gitlab_username). Generic / shared display names like student no longer leak into the UI when a student's local git config is sloppy. The original git display name is preserved on the response payload as raw_name for auditing.
  • Skeleton loader contrast. .gl-skeleton now reads from the dedicated --gl-skeleton-bg / --gl-skeleton-shimmer tokens defined in tokens.css, and the shimmer overlay opacity goes from 0.05 to 0.12. Loader rows on the project / student detail pages are visibly contrasted against the page background instead of nearly invisible.

  • Run Checks now exists at all three levels of the GitPulse hierarchy (course -> group -> student). The group-level button has been around for a long time; this release adds the remaining two.
    • Course scope. The course action bar gets a yellow Run Checks (all groups) button next to Sync All. It calls a new POST /api/v1/courses/{course_id}/run-checks endpoint that enqueues a separate run_checks BackgroundJob for every active group in the course (same 10-minute dedup window as the team endpoint). Visible to anyone with can_edit on the course.
    • Student scope. The student profile gets the same yellow button. The compliance worker is still team-scoped (GitPulse stores one snapshot per student-on-team), so the button posts to /teams/{team_id}/run-checks and the student card refreshes via the existing team SSE channel once the job finishes.
    • _run_checks_modal.html gained a Course branch in its scope banner so dynamic context flows cleanly: Course -> Group -> Project -> Student.
  • Truthful "Rotate Webhook Secret" tooltip. The previous copy did not explain where the secret comes from. It now states that GitPulse auto-generates a webhook signing secret at project import time, the rotate action only matters when that secret may have leaked, and the old token keeps working for a 24-hour grace window so in-flight pushes from GitLab are not lost.
  • Truthful "Unlink from GitLab" tooltip and confirm dialog. The old text falsely promised the project would be "re-linked on the next sync." In reality _upsert_project respects manual deletion - a soft-deactivated project stays inactive forever unless an admin explicitly hits POST /projects/{id}/reactivate. Tooltip and confirm now say so plainly and emphasise that all data (commits, MRs, checks, snapshots) is preserved.
  • Role notes. User.role only takes three values: admin, teacher, student. There is no system-wide mentor role - team_member.is_mentor is a per-team flag for senior students who help out; they are excluded from team aggregate metrics but have no elevated permissions. All Run Checks UIs gate on can_edit (course owner / teacher with course membership / system admin), not on raw User.role.

[2026-05-18] - Skeleton background bleed fix + modal title icons + GitLab import UX 2026.5.18.post11

  • Skeleton loaders no longer flash blue/red banner colors during the loading state. Semantic notification classes (.member-api-note, .advisory-banner, .alert) reused inside [aria-busy="true"] skeleton trees were leaking their feature backgrounds (blue --info-bg, blue --gl-blue-950) through the placeholders. A defensive scope in loaders.css now forces neutral --surface-strong + --border-color for any semantic banner rendered inside a skeleton container - applied system-wide so future skeleton macros never inherit feature colors.
  • Modal title icons are now vertically centered against the label. .modal-title was a plain <h3> so leading icons sat at text baseline. Switched to display: inline-flex; align-items: center to fix every modal in the app (Edit Check, Settings, Import, Rotate, Edit Course, ...).
  • "Rotate Secret" project header button is now labeled "Rotate Webhook Secret" with an expanded tooltip explaining that a new GitLab webhook token is generated and the previous secret stays valid for a 24-hour grace window. Companion "Unlink" button relabeled to "Unlink from GitLab" with a tooltip describing automatic re-link on next sync.
  • Import from GitLab modal now exposes the same Quick preset dropdown and Semester start/end date pickers used by Create Course. Picking a TUKE FEI preset fills academic year, semester, and both dates in one click; the dates are persisted onto the imported course's settings so auto-archive and semester windowing work immediately without a follow-up "Edit Settings" step.
  • Run Compliance Checks modal scope banner now supports a student branch (priority: student -> project -> team) so a student-scoped Run Checks dialog will show the student's name instead of the parent team - groundwork for the upcoming student-scoped "Run Checks" header button.

[2026-05-18] - Skeleton loader DOM parity and feature-flag gating 2026.5.18.post10

  • Skeleton loaders for every lazy-loaded page (dashboard, course detail/activity/analytics/teachers, project detail, student detail, team detail, admin users, support tickets/detail/form) now mirror the live DOM exactly - same card counts, table column counts, toolbar layout and pagination - so the swap from skeleton to content no longer shifts the page.
  • Skeletons are runtime-condition aware. Project detail honors retired grading availability, feature_docker_verification, feature_ci_failure_generator, feature_conflict_simulator, feature_llm_suggestions and the GitLab button visibility; student detail honors retired grading availability and the own-profile gate; analytics honors the course-staff gate; dashboard honors the role split and changelog panel toggle; team detail honors student-privacy mode; course detail / teachers honor can_edit and can_see_admins. A loader for a disabled component no longer appears.
  • Service worker cache gitpulse-v39.
  • Local baseline: 2583 passed, 19 deselected.

[2026-05-17] - Quality gate and release metadata hygiene 2026.5.18.post9

  • src/app/api/dashboard/home.py is back in ruff format compliance; the change is formatting-only and does not alter behavior.
  • Release metadata, dashboard latest changes, docs-site versions.json, and local validation docs now match the 2026-05-17 audit.
  • Future-dated latest changelog entries were corrected; the current local test baseline is 2583 passed, 19 deselected.
  • dead_code_report.py, static artifact guard, and pip-audit remain clean for the currently installed environment.
  • Service worker cache gitpulse-v38.

[2026-05-17] - Static cache and CSP hygiene 2026.5.18.post8

  • Static asset caching now distinguishes content-hash URLs from direct static paths: /static/*?v=<hash> is cached for one year with immutable, while unversioned /static/* uses a 5-minute TTL.
  • Login and error-page inline JavaScript moved into external static modules (js/pages/login.js, js/error-actions.js).
  • redeploy.py now uses system OpenSSH with deploy keys or the SSH agent, rejects unknown host keys by default and no longer depends on Paramiko.
  • Python/docs dependency floors were raised for current pip-audit advisories, including cryptography, python-multipart, Pillow, Pygments, requests and urllib3.
  • Template metadata links now use static_url() / absolute_static_url() for local static assets where a content hash can be computed.
  • Service worker cache gitpulse-v37.

[2026-05-17] - Mobile responsive skeletons 2026.5.18.post6

  • sk_chart (vertical bars) scales down on <768px (96 px min-height, 20 px bars) and <480px (80 px, 14 px); axis ticks are trimmed.
  • sk_h_bars got a <360px breakpoint with a 56 px label column and 12 px bars; trailing axis ticks are hidden on small viewports.
  • Service worker cache gitpulse-v35.

[2026-05-17] - Mobile Week Analytics skeleton 2026.5.18.post5

  • sk_week_analytics skeleton was visually too tall on phones (240 px chart band + 140 px doughnut + 3 × 1.9em rows). Added <768px and <480px rules: chart 240->180->140 px, bars 140->64->44 px, doughnut 140->120->96 px, toolbar wraps, stat rows 1.9em->1.5em.
  • Service worker cache gitpulse-v34.

[2026-05-17] - Offline banner replaces false alarm 2026.5.18.post4

  • Health monitor now distinguishes client offline (navigator.onLine === false) from server unreachable. Offline state renders a neutral gray banner with a wifi-off icon and the message "You're offline. Some features may be unavailable until your connection is restored." instead of the misleading red "System is currently unreachable".
  • _performCheck short-circuits when offline (no fetch, no escalation to down).
  • window 'online' listener clears the dismissed flag, hides the offline banner, and triggers an immediate re-check + "Connection restored" toast.
  • Added wifi-off icon to the JS icon registry (GP.icons.get('wifi-off')).
  • Service worker cache gitpulse-v33.

[2026-05-17] - Pixel-parity skeleton parity 2026.5.18.post2

  • sk_h_bars (Teacher's Checklist) now uses a 170 px label column, 18 px bars and a 0-100 % axis tick row - matches the real horizontal bar chart proportions.
  • Compliance by Category placeholder switched from sk_chart to sk_h_bars(count=4) (real chart is also horizontal).
  • sk_chart rewritten as gl-skeleton-chart-wrap with shimmer bars (previously flat low-opacity bars were invisible on the dark surface) plus an axis tick row.
  • Service worker cache bumped to gitpulse-v31.

[2026-05-17] - Page-specific skeleton parity 2026.5.18.post1

Changed

  • Skeleton loaders now mirror real page structure more closely:
    • New sk_h_bars macro renders a horizontal-bar placeholder used for the Teacher's Checklist card (13 R01-R13 rows of varying fill).
    • New sk_legend macro renders the chart-category legend dots above the Teacher's Checklist.
    • New sk_week_pills macro renders the Week Navigator pill row.
  • The pipeline-health doughnut skeleton on the Activity page now uses a radial gradient (reads as a doughnut chart, not a flat disc).
  • Removed an orphaned to { opacity: 1; } block left over in loaders.css from the previous progressive-reveal purge.
  • Service worker cache version bumped to gitpulse-v30.

[2026-05-17] - Skeleton legacy cleanup 2026.5.18

Changed

  • Skeleton loaders were rewritten to a pure GitLab Pajamas shimmer with no progressive-reveal scaffolding. All skeleton primitives (text, stats, cards, tables, tabs, charts, lists, activity feeds, forms) render in one shot with a single Pajamas shimmer animation.
  • Removed legacy markers and dead controllers: classes gl-skeleton-progressive, gl-skeleton-deferred, gl-skeleton-row--deferred; attributes data-skeleton-progressive, data-skeleton-table, data-skeleton-total-items, data-skeleton-initial-items, data-skeleton-total-rows, data-skeleton-initial-rows, data-skeleton-delay; the initial, initial_lines, initial_rows, initial_bars, deferred kwargs on skeleton macros; the JS _initProgressiveSkeletons controller and its GP.initProgressiveSkeletons / GP.initProgressiveSkeletonTables exports; the @keyframes glSkeletonItemReveal rule and the .gl-skeleton-table.is-progressive-expanded .gl-skeleton-row--deferred reduced-motion selector.
  • Service-worker cache version is now gitpulse-v29 and the bundle/minified siblings were rebuilt.

[2026-05-17] - System-wide progressive skeleton reveal 2026.5.17.post4

Changed

  • GitLab/Pajamas-style progressive skeleton reveal now applies across shared skeleton primitives, not only tables: text blocks, stats grids, cards, tabs, charts, lists, activity feeds, key-value rows and forms reveal a light first batch and expand only when the lazy load remains visible.
  • Dashboard home stats and course cards now use the same progressive reveal contract so primary placeholders land first and secondary placeholders fill in without blank gaps.
  • Support ticket and support form skeletons no longer mix a spinner into the same content region. Structured placeholders now cover replies and follow-up fields, while spinners stay reserved for action/background states.
  • Course settings async tables now use progressive table skeletons for webhook configurations and compliance checks instead of centered spinner blocks.
  • The unused sk_content_spinner macro and its CSS were removed so loader primitives no longer advertise a stale page-content spinner path.
  • Application and documentation metadata now use 2026.5.17.post4.
  • Service-worker cache version is now gitpulse-v28.

[2026-05-17] - Progressive skeleton table loading 2026.5.17.post3

Changed

  • Shared dashboard table skeletons now follow a progressive loading contract: three rows are shown first, then the page-specific row count is revealed only when the lazy request remains visible long enough.
  • The quality-gates documentation now records the frontend loading contract: skeletons for progressively populated sections, spinners for actions and background work, and no mixed skeleton plus spinner state for the same region.
  • Scale load-test console status output now uses ASCII [PASS] / [FAIL] markers for Windows-safe checks.
  • Application and documentation metadata now use 2026.5.17.post3.
  • Service-worker cache version is now gitpulse-v27.

[2026-05-17] - Avatar loading and health version display 2026.5.17.post2

Fixed

  • Dashboard and top-bar avatars now load eagerly with high priority and use a neutral image background. Slow GitLab avatar responses no longer expose the purple initials fallback during page entry.
  • The Admin System health card displays the runtime version exactly as reported by /health (2026.5.17.post2) instead of adding a v prefix. The v... form is reserved for git tag names.
  • Service-worker cache version is now gitpulse-v26.

[2026-05-17] - CSP inline cleanup and release metadata 2026.5.17.post1

Changed

  • Application and docs metadata now use 2026.5.17.post1. The existing v2026.5.17 tag is left intact, so this release uses .post1 instead of rewriting tag history.
  • The dashboard shell no longer emits an inline service-worker registration script. Registration is handled by the shared core.js bundle.
  • Admin Events warning toasts are now driven by gp-page-config JSON and rendered by admin-events.js, removing the page's state-specific inline scripts.
  • The DLQ loading spinner uses the shared .gl-spinner-inline CSS utility instead of an inline style attribute.
  • Service-worker cache version is now gitpulse-v25.

Verified

  • Conservative dead-code scan reported no safe unreferenced candidates.
  • Typography cleanup scan reported no project-owned files to change.

[2026-05-16] - Semester scope and docs hygiene 2026.5.16.post2

Added

  • Course Settings form shows a permanent "Scope cascade" info notice reminding the operator that changing the semester window re-scopes every dashboard, sync job, compliance score, and report for this course.
  • Saving a semester date change opens a confirmation dialog listing the exact consequences (what will be hidden, what will reappear).
  • Student profile empty-states now mention the active semester window ("Showing current semester window: ...") so it is obvious why no commits / MRs / issues are shown.
  • The typography cleanup script now covers JSON, web manifests, extensionless deployment files and optional i18n catalogues, and normalizes smart punctuation, decorative arrows and box-drawing comment separators.

Changed

  • Application and docs metadata now use 2026.5.16.post2. Accidental future-dated metadata was corrected because the release date is May 16, 2026.
  • Sweep, enrichment and LLM jobs (enrich_issue, enrich_mr, llm_job, _enrich_all_async) explicitly filter out entities created before course.settings.semester_start_date. Pre-semester commits, MRs, issues and pipelines remain stored but are no longer re-processed or counted in current-semester metrics.
  • Admin / home / courses / teams dashboards share the same semester_floor_utc helper so the read path and worker path agree on the same lower bound.
  • The HTML5 date-input lower bound now auto-rolls each calendar year (semester_date_min = January 1 of today.year - 2) instead of the fixed 2020-01-01, preventing stale dates that would silently zero-out the visible scope.
  • Report and export downloads use the shared mobile-safe blob download helper.
  • Service-worker cache version is now gitpulse-v24.

Fixed

  • Report generation preserves teacher full names, counts distinct student improvements, reports compliance delta as percentage points, and downloads PDFs reliably on mobile browsers.
  • The top progress bar no longer starts before AJAX form handlers can prevent native submission, avoiding false progress flashes on report forms.

[2026-05-16] - Health version follow-up 2026.5.16.post1

Fixed

  • Application version is now 2026.5.16.post1.
  • Runtime containers now read project.version from pyproject.toml when installed package metadata is unavailable, so /health and metrics no longer report version="unknown" after source-checkout deployments.

[2026-05-16] - Course-scoped dashboard release 2026.5.16

Changed

  • Application version is now 2026.5.16.
  • Student detail activity is scoped to the active course window when a semester start date is configured. Imported repository history from older academic years is hidden from commit tables and line totals instead of inflating the current course view.
  • Weekly compliance computation and historical backfill now ignore pre-semester issues, merge requests, review activity, pipelines, and commits.
  • Member tables no longer duplicate global GitLab access roles beside the member name when the dedicated Role column already shows that value.

Fixed

  • Old 2021/2022 commits can no longer make a 2026 course show misleading commit quality or contribution totals.
  • The dashboard latest-changes feed no longer contains a future-dated entry.

[2026-05-15] - Declarative client actions release 2026.5.15.post2

Changed

  • Application version is now 2026.5.15.post2.
  • JavaScript-generated reload controls now use the shared data-action="reload-keep-scroll" contract instead of inline onclick handlers.
  • The read-only support-ticket reply hint now uses a CSS class for spacing instead of inline template styling.
  • Service-worker cache namespace bumped gitpulse-v6 -> gitpulse-v7, and generated bundles were refreshed.

Fixed

  • The offline fallback page no longer emits an inline click handler; retry uses a plain GET form.
  • Added static frontend tests that guard source JS and the service worker against new inline onclick= handlers.

[2026-05-15] - Login alert polish release 2026.5.15.post1

Changed

  • Application version is now 2026.5.15.post1.
  • Login-page authentication errors now use a reusable GitLab-style alert: dark surface, red top border, alert icon, dismiss action, and no inline styling.
  • Client-side login failures reuse the same alert DOM/CSS contract as server-rendered errors.
  • Service-worker cache namespace bumped gitpulse-v5 -> gitpulse-v6, and generated bundles were refreshed.

Fixed

  • Removed the old login-specific error block and invalid red fallback from the login stylesheet.
  • Added static tests for the login alert component contract.

[2026-05-15] - Version baseline and cache refresh

Changed

  • Application version is now CalVer 2026.5.15.
  • Version rationale: the repository started from a full-codebase initial commit, and the pre-release audit found 1875 commits, 47 Alembic migration files, and no historical release tags before this normalization commit. A reconstructed SemVer number would be arbitrary, while CalVer records the production release date honestly.
  • Service-worker cache namespace bumped gitpulse-v4 -> gitpulse-v5.
  • Documentation status, deployment examples, and dashboard latest changes were refreshed for the 2026-05-15 release.

Fixed

  • dead-code-report.txt is ignored as a generated local/CI artifact.
  • Confirmed the current static and dead-code guard scripts are shell-independent and do not rely on git ls-files.

[2026-05-14] - Production-safe cleanup release 0.1.1

Changed

  • Stabilized local and CI gates: test env normalization, shell-independent pre-push unit runner, Ruff pinned to 0.15.4, bundle drift check, static artifact guard, and non-blocking dead-code report.
  • Dashboard scheduler registry now mirrors the production scheduler: 26 registered jobs, including email log retention, expired previous webhook-secret cleanup, team discovery, and semester auto-archive.
  • OpenAPI schema hygiene: /dashboard/login GET has a stable operation id and HEAD is hidden from schema to prevent duplicate operation IDs.
  • Test suite is warning-clean under the default local gate: 2583 passed, 19 deselected.

Fixed

  • Removed stray tracked root artifact -w.
  • Bundle/vendor scripts now emit ASCII status markers so Windows cp1252 terminals do not crash.
  • Previous webhook secrets are accepted only until webhook_secret_previous_expires_at, then cleaned by scheduler.
  • Roster CSV import validates all rows before writes, avoiding partial commits.
  • Retired grading saves reject zero total category weight.
  • Redis session-store fallback now increments session_store_fallback_total and readiness probes the store only when enabled.

[2026-05-14 - root-cause patch] - Repeated re-auth on every visit fixed

Root cause

The app-level defaults in app/config.py were set correctly (persistent_sessions=True, 30-day idle, 90-day absolute), but docker-compose.yml lines 11-12 hard-coded the wrong defaults:

YAML
PERSISTENT_SESSIONS: ${PERSISTENT_SESSIONS:-false}
SESSION_MAX_AGE_SECONDS: ${SESSION_MAX_AGE_SECONDS:-28800}   # 8 hours

The production .env.production already set PERSISTENT_SESSIONS=true, but SESSION_MAX_AGE_SECONDS=28800 (8 h) meant the signed cookie's idle window expired in 8 hours - anyone visiting once a day was forced through the full GitLab OAuth redirect every time.

Fixed

  • docker-compose.yml defaults flipped: PERSISTENT_SESSIONS:-true, SESSION_MAX_AGE_SECONDS:-2592000 (30 days), and new SESSION_ABSOLUTE_MAX_AGE_SECONDS:-7776000 (90 days).
  • redeploy.py now injects all three values into .env.production on every deploy (idempotent sed/echo pattern, same as GIT_SHA). Auditable, drift-resistant.
  • .env and .env.production updated to the new 30 d / 90 d values as the next-deploy baseline.

Removed (dead code)

  • session_auth.verify_password() and session_auth.hash_password() - production runs exclusively on GitLab OAuth; these helpers had no callers outside the import smoke test. Removed along with the now-unused bcrypt import in session_auth.py (the bcrypt package is still used by webhook authentication, so the dependency stays in pyproject.toml).
  • Corresponding lines in tests/unit/test_module_imports.py removed.

Verified used (kept, NOT dead code)

The audit confirmed gitlab_oauth.refresh_access_token() IS used - invoked by dependencies.get_fresh_gitlab_token() (line 626) with a 5-minute buffer before token expiry. Silent OAuth refresh on telemetry/sync calls already works.


[2026-05-14] - Persistent sessions and full-system multi-tab sync

Changed (auth)

  • persistent_sessions default flipped to True - sessions now survive server restarts and redeploys. The user is no longer forced to re-authenticate every time a new build is deployed.
  • Idle TTL bumped 8 h -> 30 days, absolute TTL 24 h -> 90 days (session_max_age_seconds, session_absolute_max_age_seconds in app/config.py). Defence-in-depth: signed cookie + Redis opaque sid + Secure/HttpOnly/SameSite=Lax/__Host- prefix + UA fingerprint (optional) + explicit logout invalidation all remain.

Added (cross-tab sync - system-wide)

  • Locale (gp_lang) - storage event listener in locale-sync.js updates the gp_lang cookie and reloads sibling tabs the moment the user switches language anywhere.
  • Sidebar collapsed state - desktop collapse/expand mirrors live across all open tabs via the existing sidebar-collapsed localStorage key + new storage listener.
  • Cookie consent banner - BroadcastChannel('gp_cookie_consent_sync') hides the banner in every sibling tab the instant any tab accepts; dismiss() is now idempotent and the hiding animation class is cleared on completion.

Verified already-synced (untouched)

  • Tour completion (tour-engine.js), global job bar (global-job-bar.js), cross-toast broadcast (core.js), PWA install dismissal (pwa-install.js).

[2026-05-13 - round 3] - PWA install card: multi-tab sync, conflict avoidance, lifecycle polish

Added

  • Cross-tab synchronisation - BroadcastChannel('gp_pwa_install') plus localStorage storage event fallback. Dismissing or installing in one tab now propagates to every other open tab instantly.
  • Conflict avoidance - _conflictPresent() defers the card while #cookieConsent (visible), .gp-tour-welcome, .driver-active/.driver-popover, .modal.show/.gl-modal-overlay, or .gp-app-loader.is-visible is on-screen. An 800 ms re-poll loop reshows the card the moment the overlay disappears.
  • Page-synced lifecycle - visibilitychange pauses scheduling while the tab is hidden and reschedules on return (no more animation on a backgrounded tab).
  • Event hooks - listens for gp:cookie-consent, gp:tour:welcome:dismissed, gp:modal:close to re-attempt show immediately. tour-engine.dismissWelcomeCard() now dispatches gp:tour:welcome:dismissed after its 320 ms exit completes.
  • UX polish - Escape closes without persisting; BroadcastChannel cleanly closed on pagehide.

Changed (PWA copy)

  • Description text no longer claims "offline support" (the dashboard requires auth + DB and cannot work offline). New copy: "Install this app on your device for a faster, full-screen experience with quick access from your home screen."

[2026-05-13 - round 2] - JS modernisation and PWA bug fixes

Changed

  • pwa-install.js rewritten in modern ES6+: const/let, arrow functions, async/await, optional chaining, nullish coalescing.
  • core.js Alpine $mdShowPreview magic - replaced 6 var declarations (stack, i, node, refEl, rendered, rm) with const/let.

Fixed

  • Hide-timer race: _hide()'s setTimeout was not cancellable on re-show. Introduced module-level _hideTimer ref so the next show/hide cancels first.
  • Deprecated userChoice API: rewrote _triggerInstall with result?.outcome ?? (await prompt.userChoice?.catch(()=>null))?.outcome - handles both modern (Chrome 97+) and legacy two-step resolution shapes.
  • clearTimeout(t) inside fired callback: moved before scheduling so the timer is actually cancellable.

[2026-05-13] - PWA install prompt (GitLab Pajamas style)

Added

  • Custom PWA install card (pwa-install.js + pwa-install.css) - captures beforeinstallprompt, renders a polished bottom-right card with icon, title, description, Install and Not now actions, and an × close button.
  • 90-day dismissal memory (gp_pwa_install_dismissed_until in localStorage).
  • Reduced-motion respected (prefers-reduced-motion: reduce collapses to a fade-only transition).
  • Mobile - pinned to the bottom of the viewport, full-width.
  • Public API - window.GP.installApp.{ show, dismiss, isAvailable }.
  • i18n hooks - pwa.install.title, pwa.install.desc, pwa.install.cta, pwa.install.cancel, pwa.install.close (fallback strings provided).

[2026-05-12] - Toast dedup and z-index ladder alignment

Fixed

  • Duplicate sync-completion toasts - when a sync job finishes, the toast was sometimes posted twice (one from the SSE event, one from the terminal poll). Introduced sessionStorage key gp_job_completion_dedup_v1 to suppress duplicates within a session; auto-poll is skipped for terminal jobs.
  • Z-index conflicts - toast container was sometimes rendered under the app loader / cookie banner. Token ladder is now: --z-cookie-banner: 1040, --z-tour-welcome: 1045, --z-app-loader: 1050, PWA install card hardcoded to 1055, --z-toast: 1060.

[2026-05-11] - Tour engine dismiss fix and mobile responsiveness

Fixed

  • Tour - dismiss when clicking inside the sidebar on desktop/tablet: on desktop and tablet the sidebar is a permanent layout column - clicking any nav link, language switcher or Collapse button no longer dismisses the welcome card or an active tour. Guard added to the click listener: if (!_isMobile() && t.closest('#appSidebar, .gl-sidebar')) return;.
  • Tour - overly broad summary selector: the click handler used the bare summary CSS selector which matched every <summary> element on the page, including collapsible panels in forms (e.g. code-reference sections in Course Settings). Selector narrowed to details.gp-menu, details.dropdown, details.user-menu - .closest() already bubbles through <summary> children so no functionality was lost.
  • Tour popover - Android 3-button nav bar hiding Back/Next buttons: with viewport-fit=cover active the viewport extends behind the system nav bar, but env(safe-area-inset-bottom) is 0 for Android 3-button navigation. Two-phase fix: the bottom-sheet transform now subtracts env(safe-area-inset-bottom, 0px); browsers supporting dvh get a @supports (height: 100dvh) block with 100dvh that reliably excludes the system bar. Same treatment applied to the tablet portrait and landscape breakpoints.
  • Tour popover footer - minimum touch target: padding-bottom: max(env(safe-area-inset-bottom, 0px), 12px) guarantees at least 12 px clearance above the system bar even when env() returns 0. Added min-height: 44px (WCAG 2.5.5).
  • Welcome card - lifted above system bar: bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px)) raises the card by exactly the system bar height.
  • Tour popover - max-height dvh: max-height: 70dvh (with 70vh fallback) uses the dynamic viewport unit that excludes both the browser address bar and the system navigation bar.

[2026-05-10 - round 3] - Attribution chips: who actually did the work on MRs and Issues

Added

  • Attribution chips on the MR table (student detail): when an MR has no commits from the displayed student, an orange "No student commits" badge appears along with the names of the actual authors (blue = teacher, grey = peer). If the student committed together with others, a "Also by:" row shows the other contributors.
  • "Assigned to" column in Authored Issues table: shows who the issue is assigned to - blue badge for teacher, grey for peer, dash if nobody.
  • "Opened by" column in Assigned Issues table: shows who opened the issue - same colour logic.
  • Translations (sk.json): 6 new keys - Also by:, No student commits, tooltip for "No student commits", Opened by, Assigned to, Peer student.

Implementation

  • src/app/api/dashboard/students.py: batch SQL query via tuple_(Commit.project_id, Commit.ref).in_(branch_pairs) - no N+1; teacher_ids (UUID set) + teacher_gitlab_ids (int set) for correct teacher detection even without author_id; aliased(StudentModel) / aliased(TeamMember) for two outerjoin paths on Issues.
  • src/app/templates/partials/student/_merge_requests.html: attribution row section with not _student_commits / _other_commits conditions.
  • src/app/templates/partials/student/_detail_content.html: new "Assigned to" and "Opened by" columns.

[2026-05-10 - round 2] - "Branch Deleted" badge and commit recovery via MR API

Added

  • "Branch Deleted" badge on the student detail / MR table: when an MR's source branch was removed after merging (via GitLab force_remove_source_branch or manual deletion), the affected MR row shows a grey badge with a tooltip explaining commit recovery. Requires new DB column merge_requests.source_branch_deleted (migration 044).
  • Primary branch-deletion detection (mr_processor.py): GitLab returns force_remove_source_branch=true in both REST API responses and webhook payloads - the value is now stored in source_branch_deleted on every MR upsert.
  • Secondary detection (sync_job.py): the sync job tracks per-branch commit counts in the branch-ref loop; if the MR fallback loop recovers > 0 commits but the branch-ref returned 0 and the MR is merged, the system automatically marks source_branch_deleted = True - catching manual branch deletes where GitLab sends no flag.
  • Commit recovery via MR API (sync_job.py): after the branch-ref loop, a fallback loop calls GET /projects/:id/merge_requests/:iid/commits for every MR. This endpoint works even after branch deletion - GitLab preserves the MR commit list permanently.
  • Author-name attribution fallback (students.py): the commit query on the student detail page adds a fourth OR clause matching author_name = student.full_name - catches cases where the student's git client email doesn't match any roster entry but their display name does.
  • Translations (sk.json): "Branch Deleted": "Vetva zmazaná" plus Slovak tooltip.

Fixed

  • CI - mypy (students.py): bare : list annotation on _commit_clauses caused a type-arg error in strict mode -> annotation removed, mypy inference now resolves the correct type.
  • CI - ruff-format (students.py): multi-line .append(func.lower(...)) calls fit on a single line (< 100 chars) -> collapsed to single-line form.

[2026-05-10] - Commit attribution fix (email fallback + noreply prefix)

Fixed

  • Commit attribution email fallback (students.py): commit query on the student detail page now includes an author_email = student.email OR clause - commits with no resolved author_id still appear if the email matches.
  • GitLab noreply email <digits>+ prefix stripping (commit_processor.py): GitLab sometimes generates noreply addresses like 123456+username@users.noreply.gitlab.com; the numeric prefix is now stripped before comparison, so sync correctly attributes those commits.

[2026-05-09 - round 2] - Second pass on tour engine, DLQ loader and admin tables

Fixed

  • Tour engine v2 (gp_tour_v1 -> gp_tour_v2 storage prefix): a one-time, clean reset of the stale :completed keys that v1 wrote on every tour finish and that subsequently suppressed the welcome card across all stages of the multi-stage /dashboard/support page (course_id -> category -> form). Under v2 a bare completion key is only written when the user explicitly dismisses the welcome card (× or "Maybe later") - finishing a stage stores only the fingerprinted per-stage key, so navigating to the next stage of the same page re-prompts the tour as expected.
  • Tour engine - cross-tab coordination: a new storage event listener watches writes to the gp_tour_v2: namespace from sibling tabs. As soon as a user finishes or dismisses the tour in one tab, every other open tab immediately hides its welcome card and (if currently running) tears down its active Driver.js overlay.
  • Admin / Event Pipeline - DLQ loader: the spinner in the empty DLQ Events table appeared visually static. SVG defaults transform-origin to 0 0, so the icon was "rotating" around its top-left corner. .spin now uses transform-origin: 50% 50%; transform-box: fill-box;, fixing the rotation pivot across every page that consumes this utility class.
  • Admin / Users + DLQ tables - text clipping: Role ("Student") and Status ("Active") badges were getting ellipsis-truncated because td { overflow: hidden; text-overflow: ellipsis } applied to the whole table. Truncation is now scoped to text-only columns; Role / Status / Actions use white-space: nowrap; overflow: visible. Widths rebalanced: Actions 8 % -> 12 %, Email 22 % -> 18 %.
  • Admin / Users + DLQ tables - responsiveness: below 900 px (Users) and 1024 px (DLQ) the tables fall back to horizontal scrolling inside their .table-responsive wrapper instead of butchering headers ("GitLab User I...") or clipping action buttons.

[2026-05-09] - OAuth loop fix & UI polish

Fixed

  • OAuth login: end of the infinite "Session expired" loop after returning from GitLab. The callback now issues an explicit HTTP 303 See Other with Cache-Control: no-store, STATE_TTL was bumped from 600 s to 900 s, and the gp_returning cookie attributes were corrected so the browser reliably carries the session-id to the destination page.
  • select_field macro: literal u2014 no longer leaks into option labels. The tojson_attr filter now serializes JSON with ensure_ascii=False, so the em-dash (-) lands in the HTML attribute as a native UTF-8 character and no longer relies on a backslash escape that some downstream pass was stripping.
  • Tour - mobile popover flip: relaxed the auto-flip rule for the top sheet. The previous strict thresholds (≥ 40 % bottom-sheet overlap and ≤ 10 % top-sheet overlap) vetoed legitimate flips for fields in the lower viewport half (e.g. "Priority" on /dashboard/support), causing the popover to physically cover the field it was pointing at.
  • Tour - multi-stage pages: each /dashboard/support stage (course picker -> category -> form) now gets its own welcome card. Previously, completing one stage also wrote a "global" :completed key that permanently suppressed the welcome card on every other stage. The global key is now only written on explicit dismissal (× or "Maybe later").

[2026-05-07] - Tour Engine & UI Enhancements

Added

  • Admin Users Tour: A new step for "Service Accounts" has been added to clarify bot accounts and the analytics panel logic in System Settings.

Fixed

  • Course Settings: Fixed raw unicode \u2014 displaying in "Select a preset" dropdown placeholders due to Jinja/Alpine serialization issues by utilizing standard hyphens -.
  • Tour Engine: Overhauled completion edge-cases involving the max-reached step cache, successfully preventing complete tours from re-prompting users under wrong skipped classifications. HTMX race condition timeouts were also patched.
  • UI stability (Course Settings): Mitigated layout jumps on load by introducing seamless data-loading spinners instead of heavy skeleton tables.

[2026-05-06] - Service accounts (bots) as a first-class entity

Added

  • users.is_bot column + partial index (Alembic 042_user_is_bot): a BOOLEAN NOT NULL DEFAULT FALSE column on users with partial index ix_users_is_bot_false WHERE is_bot=false. Backfill matches the same patterns as is_bot_user() (project_*, group_*, *_bot, *-bot, ghost, alert-bot, support-bot). The previous regex/ilike-based runtime filter is replaced by a single indexed predicate, humans_only_clause().
  • is_bot set at every User-creation site: all 9 places where the app inserts a User/UserModel row (group import, OAuth first login, dev-mode auto-create, admin manual sync, student create, projects/teams auto-create from inherited members) now call is_bot_user(username, user_type=...) and persist the result. Service accounts are tagged once at entry, not heuristically re-filtered on every query.
  • "Service Accounts" stat card on the admin System Settings page plus a separate bots field in /dashboard/api/v1/admin/system/stats. The User Role Breakdown chart now counts humans only - bots are read from the dedicated card and never mixed into role analytics.
  • Service Accounts collapsible panel below the main User Management table. Bots cannot be edited or deactivated from the UI; only an Info button is rendered, opening the same User Info modal with a Service Account badge next to the role chip and the tooltip "GitLab service account / bot - not a human user". The skeleton (sk_admin_users) is aligned with the real layout: 4 filter pills (All/Admin/Teacher/Student) plus a placeholder for the service panel below the table.
  • Bots excluded from aggregate counts: dashboard_stats / home._build_stat_cards (Active Users / Students), member_sync_service.get_teachers_for_course (teacher whitelist), users.update_user last-admin guard, and the support assignee dropdown - all of them now ignore is_bot=true rows.

Fixed

  • The previous regex-based not_bot_user_clause() was a band-aid: the same logic was duplicated in Python (is_bot_user) and in SQL (an ilike-OR tree). It now exists only as a back-compat alias; new code should use humans_only_clause() / bots_only_clause() from app.services.user_filters.
  • sk_admin_users skeleton only showed 3 filter pills instead of 4 - the layout used to jump on lazy load when the real header arrived.

Changed

  • Translations (sk.json): added Service Account, Service Accounts, Bot, View service account details, GitLab bots - read-only, GitLab service account / bot - not a human user.

[2026-05-05] - Mobile-friendly tours + sync access control

Added

  • Tours fully responsive on mobile (static/css/tour.css): a new @media (max-width: 600px) block converts the popover into a full-width bottom sheet (max 70 vh) with a wrapping Back/Next footer, and stretches the welcome card to the viewport minus 0.75 rem margins. No more horizontal scroll or clipped popovers.
  • Tour engine auto-opens the mobile sidebar (static/js/tour-engine.js): when a step targets an element inside #appSidebar / .gl-sidebar on a mobile viewport (≤ 768 px), the engine adds mobile-open to the sidebar and visible to the overlay before measuring the target rect. The drawer closes itself in onDeselected (and onDestroyed) so subsequent body-targeted steps are not occluded. Without this fix, sidebar items kept a positive bounding rect even at translateX(-100%), so Driver.js drew the cut-out off-screen and users saw a dim overlay with nothing highlighted.

Fixed

  • Sync progress bar - student-without-courses visibility: /api/sync/active returned a per_course map containing every running sync system-wide, so students enrolled in zero courses saw activity from other courses. The endpoint now passes the response through _filter_courses_for_user (same logic as dashboard widgets), restricting non-admin users to courses they are allowed to see. Bearer-token callers and admins bypass the filter.
  • Multi-job count badge - Opera overlap: the badge was absolutely positioned in the top-right corner of #globalJobBar, which overlapped the job title in Opera at narrow viewports. The badge moved into a slim header strip above the bar items (bar.insertBefore(countBadge, bar.firstChild)) and now occupies its own row.

Changed

  • Documentation (docs-site): changelog entries added in changelog.md (SK) and changelog.en.md (EN). The dashboard "Latest changes" widget mirrors the same order via _CHANGELOG_ENTRIES in src/app/api/dashboard/home_changelog.py.

[2026-05-04] - Guided Tours

Added

  • Driver.js v1.3.6 vendored at static/vendor/driver.js/ (MIT, ~21 KB JS + 4 KB CSS, no CDN). Global entry point window.driver.js.driver({...}) ships on every authenticated page by default.
  • Tour engine (static/js/tour-engine.js): role-aware (student / teacher / admin), multi-language (en + sk), per-page detection from location.pathname, state persistence in localStorage under gp_tour_v1:<slug>@<role>:{completed,skipped,prompted}. Keys are role-scoped, so a student promoted to teacher receives the new tours.
  • Tour content registry (static/js/tour-content.js): 20 tours covering Dashboard, Course Detail / Settings / Roster / Teachers / Analytics / Activity, Team Detail, Project Detail, Student Profile, Admin Users / Courses / System, Retired grading Editor and Support - each fully localised.
  • First-visit welcome card: subtle floating card bottom-right with tour title, step count, time estimate and a "Take the tour / Maybe later" CTA. Auto-dismiss after 25 s, one-shot per role.
  • Help launcher in header: "Take a tour" and "Replay tours" items added to the existing user dropdown; Replay clears the persisted history.
  • Themed Driver.js popover (static/css/tour.css): matches design tokens (--surface-default, --primary, --shadow-lg), animated stage pulse via .gp-tour-pulse, reduced-motion safe.
  • Hard role guard: run(slug) refuses to launch a tour whose roles array does not include the current user's role - a safety net in case the content registry mistakenly registers a teacher-only tour on a student page.

Fixed

  • Run Compliance Checks modal - white footer strip: rcm-footer used background: var(--card-bg, var(--surface, #fff)) but neither token is defined in the app (only --surface-default, --surface-strong). The fallback #fff rendered a white sticky footer in the dark theme. Footer now uses --surface-strong and bleeds out through the --gl-spacing-5 modal padding so it aligns flush with the modal frame. rcm-group and rcm-item rewired to real tokens - checkbox rows now have a proper surface, hover state, focus ring and inset border stripe when checked.

Changed

  • Tour CSS uses real SVG icons / unicode dots instead of emoji (no rocket / wave / lightbulb / pin / stopwatch emojis). Welcome card meta row now shows two pill badges with coloured dots (primary for "steps", success for time).
  • Build pipeline: tour-engine.js and tour-content.js added to JS_MODULES in scripts/build_bundle.py; tour.css added to the static/styles.css manifest - minified .min siblings and gzip-precompressed bundles are generated automatically.

[2026-05-03] - Webhooks v2

Added

  • Multi-scope webhook configurations: Each course can now host any number of webhook secrets, each scoped to a GitLab Group, Sub-group, project list, or the whole course. Secrets are generated server-side, shown exactly once, and rotate with a 24h grace window. Audit log captures every create / rotate / revoke event.
  • Auto-detected GitLab group path: The course-level gitlab_group_path field is no longer manually edited - the system computes the longest common prefix from existing teams' gitlab_group_path values on save. Field removed from the UI to eliminate duplication with per-config scopes.
  • Archive-safe webhooks: When a course is archived, all of its WebhookConfiguration rows are now flipped to is_active=False so the auth lookup rejects further deliveries with 401. Rows are preserved (audit + rotation grace remain intact). On reactivation, configs stay deactivated by design - operators decide which scopes are still relevant.
  • Sortable webhook configurations table: All columns (Name / Scope / Status / Last delivery / Created) are now click-sortable with stable data-sort-value attributes; Last delivery and Created sort by ISO timestamp.
  • Tooltips on webhook configuration form actions: Cancel and Save buttons now carry tippy tooltips explaining their effect.
  • Active toggle visible in create flow: The "Active" checkbox now shows for both new and existing configurations (previously hidden in create mode), letting teachers stage a disabled config without an extra round-trip.

Fixed

  • Actions column squashed under long Scope strings: Pinned column width to min-width: 8rem and forced .action-cluster { flex-wrap: nowrap } so all three icon buttons stay on one row regardless of content.
  • Stray border line under collapsed "GitLab setup steps" card: Card header bottom border now turns transparent when Alpine collapses the body via x-show.
  • Secret input visual artifacts: Removed letter-spacing and any inherited dotted underline styling from .secret-reveal-input / .webhook-input so the readonly token reads cleanly without the dotty hover ghosts.
  • Reveal modal cramped key/value rows: Increased vertical padding and tightened label width on .secret-reveal-meta; replaced raw .alert.alert-info paragraph with the alert_block macro for consistent component styling.
  • Webhook config modal not opening: Inner .modal div carried class='d-none' which overrode openModal()'s .active class on the backdrop. Backdrop appeared but content stayed hidden. Removed d-none from all 3 webhook modals (webhookConfigEditModal, webhookSecretRevealModal, webhookConfigRevokeModal).
  • "Heads up" notice restyled to match other in-modal .alert.alert-info blocks (single-line callout, no bold lead-in).

Changed

  • Removed legacy api/group_webhooks.py, schemas/group_webhook.py, services/group_webhook.py and tests/unit/test_group_webhook_service.py (superseded by webhook_configurations). The legacy course-level secret stamping helper was inlined into api/webhooks.py; ROTATION_GRACE now lives in services/webhook_configurations.py.

[2026-05-03] - Late

Changed

  • Backend refactor (-60 inline imports, -73 net lines): Hoisted 60+ duplicate inline imports in admin.py, home.py, evaluation_report.py, pilot_telemetry.py to module level. Removed dead paths, fixed PERF401/PERF403 list/dict comprehensions, and best-effort swallow blocks (S110) now carry explicit noqa comments. No behavior change - ruff + mypy 1.11 remain green.
  • Course analytics page - fast-path: EvaluationReportService gained _collect_preview_data() which skips 3 expensive aggregates (compliance trends, support summary, weekly aggregates) on the analytics page load. PDF generation is unchanged.

Fixed

  • Accessibility (a11y) - <label> without associated form field:
  • In _analytics_content.html (PDF report form), four heading-style labels (Course Name, Semester, Teacher Name, Report Period) were converted from <label> to <span class="form-label"> because they describe readonly displays, not inputs.
  • In member_filters.html, _run_checks_modal.html and forms.html (toggle, bare checkbox, file-upload) wrapping <label> elements gained explicit for= attributes; checkboxes inside the run-checks grid received generated ids. Both implicit wrapping and explicit for now satisfy DevTools/axe.

[2026-05-03]

Changed

  • Student Details (Collapsibles): The first <details> section now automatically opens even for empty datasets. If a student has no assigned issues or MR discussions authored, the empty state displays instructional helper text instead of quietly collapsing the section.
  • Component Visual Adjustments:
  • Added an automatically rotating chevron indicator to the .summary-toggle element used in student detail breakdowns.
  • The Raw Check Details (JSON) view is now styled as a standard collapsible section with monospace pre formatting to seamlessly match the remainder of the dashboard.
  • Removed overlapping border notch on pagination-controls component placed inside specific inner details sections.

Fixed

  • GitLab Health Companion Text: The notification health banner has been stacked vertically with the global job progress bar to avoid UI shifting/overlapping. In turn, GL_FAIL_THRESHOLD duration before logging a GitLab outage was promoted to 3 consecutive failed polls.
  • Skeleton Table Loading Check (.gl-skeleton-table): Visual loader pills now better leverage flex widths per column using nth-child targeting, effectively removing visually monotonic blocks while maintaining compositor animations that do not obstruct the CPU main thread during initial parsing.
  • CSP fixes for <script> and <link> scripts: Inline onload triggers were refactored, and nonce variables injected into directly embedded configuration tags within all template boundaries, clearing remaining Strict CSP restrictions globally.
  • Markdown Escapes: CommonMark backslash escape handling before code and punctuation block parsing is now protected against stripping, ensuring safe literal rendering.

  • Edit User - Save changes spinner is now centered inside the button (inset:0; margin:auto).
  • Dashboard - duplicate rows for soft-deleted projects are hidden; projects sharing the same name across teams are disambiguated in the course tree.

Security

  • Absolute session TTL now enforced on stateless (cookie-only) sessions (AUTH-06b): the previous AUTH-06 fix added session_absolute_max_age_seconds only to the Redis-backed session store. Stateless cookies (fallback when Redis is unavailable) still relied on the sliding max_age alone - meaning an active user (or a stolen cookie) could renew indefinitely. get_current_session() now validates the ts (created-at) claim against session_absolute_max_age (default 24 h) on every session load, regardless of storage backend. Malformed ts values are treated as expired (fail-closed).
  • Rate-limit fails closed in ALL environments (AUTH-05b): the previous AUTH-05 fix only applied fail-closed to APP_ENV=production; development and test environments were still fail-open when Redis was unavailable. If a development .env file accidentally leaked to production (or a staging server was used as prod), rate limiting would silently degrade to no-op. Unified to HTTP 503 + Retry-After: 30 everywhere.
  • UA fingerprint always stored - even when User-Agent is empty (AUTH-08): previously, a missing or empty User-Agent header resulted in ua="" in the session cookie, which bypassed fingerprint checking entirely. An attacker with a stolen cookie could replay it from any client by simply omitting the header. Sessions now always carry a fingerprint; empty UA produces the distinct value "no_ua|no_ua" so the mismatch check fires even against header-stripping replay attacks.

Changed

  • Sweep enrich capped at 2 000 jobs per periodic run (SCALE-01): periodic_enrich_all previously enqueued enrichment jobs for every tracked issue and MR across all active projects without limit. At 567 students (~200-500 projects × 15-30 entities each) this could flood the queue with 5 000-15 000 jobs in a single burst, exhausting worker capacity and delaying higher-priority webhook processing. New config setting sweep_enrich_max_jobs_per_run (default 2 000) caps total enqueued jobs; remaining entities are picked up in the next 24 h cycle. The cap is logged at WARNING level when hit.

Fixed

  • Dead code removed in sweep_stale_projects: duplicate return 0 statement on line 821 (unreachable code after the first return) removed.

[2026-05-07]

Changed

  • Compliance engine - audit-driven recalibration of R02/R04/R05/R06/R07/R08/R10/R11/R12: a thorough logical audit of the 13-check system surfaced a series of cases where the default semantics drifted from the spirit of the ZSI assignment text and either unfairly penalised disciplined students or let real non-compliance through. This release realigns the engine with the "doing & communicating" course brief (4-5 person teams, separate feature branch per ticket, MR linked to issue, two reviewers, commit-ref-citing responses, Approve before merge, MR + ticket close together) so the engine now does what an experienced teacher would do during a manual check.
    • R02 (Branch naming) - failure detail also explains the protected-branch case (previously only "doesn't follow naming convention"). New text: "Branch '' is on a protected name or does not match the configured naming convention" - instantly clear whether the issue is a naming-pattern miss or a direct merge into master/main/develop. An empty branch_pattern no longer falls through (the check is just skipped).
    • R04 (MR linked to issue) - branch-name fallback + tighter bare-#N regex. Both enrich_mr.py and mr_processor.py now extract linked_issue_iid from the branch name (issue-7-foo, 7-add-login, feature/issue-12) when the MR description has no Closes #N. Combined with the text parser - explicit Closes #N wins, branch-IID is the fallback. The bare-reference pattern #N is tightened to be word-boundary anchored at both ends ((?<![\w-])#(\d+)(?!\d)), so v#10 and #10x no longer false-match. The MergeRequest.linked_issue_id FK is then resolved via gitlab_iid lookup -> R11 can pair the MR with its ticket.
    • R05 (MR description quality) - word counter ignores markdown structure. Previously an MR could pass the 10-word threshold with effectively empty content (sections like ## What, ## How, ## Why add up to ~9 tokens without describing anything). The _word_count helper now strips markdown headers, list markers, link syntax [text](url)->text, italic/bold sigils and filters out tokens shorter than 2 chars or without a \w character. Empty headings no longer pass.
    • R06 (Review Received) - counts only reviewers with meaningful feedback. Previously a reviewer could write "+1" or "ok" and R06 counted them. The engine now aggregates every comment per reviewer (notes_by_reviewer: dict[author_id, list[notes]]), joins them and runs is_meaningful_comment(joined, min_words=cfg.min_word_count). New cfg.require_meaningful flag (default True) can be turned off in the UI for courses with a lighter review culture. The detail string surfaces the number of reviewers with "short comments only" so the teacher can see why R06 fails even when 2 reviewers are present.
    • R07 (Code Review Given) - default min_words 30 -> 15 (model course_settings.min_review_word_count, helper is_meaningful_comment, migration 039). 30 words proved too high for typical per-line review comments ("You could use a dict comprehension here instead of a for-loop" is 9 words and a perfectly legitimate review). Aligns with the rest of the codebase (where min_review_word_count was already a fallback to 15) and with the ZSI text, which states no quantitative threshold. The migration only updates courses still on the legacy default of 30; teacher customisations are left untouched.
    • R08 (Review Response) - also counts standalone reviewer notes + softer defaults. Previously the engine treated only inline diff discussions (notes with a discussion_id) as "review threads"; standalone reviewer comments at MR level were ignored, so a student could fail to respond to "This PR looks fine but I'd like to see tests." and still pass R08. Now total_threads = inline_total + standalone_reviewer_notes and answered = inline_answered + min(standalone_author_notes, standalone_reviewer_notes). Default for cfg.require_commit_ref flipped True -> False (ZSI recommends commit-ref-citing but does not mandate it), cfg.pass_if_no_threads flipped False -> True (you can't respond to feedback that doesn't exist yet). When commit-ref is not required, partial scoring is the plain response_rate instead of the 50/50 weighting.
    • R10 (Merged by author) - also accepts a teammate merger. Previously allow_self_merge=True meant "the author MUST merge", which is semantically opposite of the ZSI text "assignment owner (or designated assignee) executed the merge". Default semantics are now permissive: passes when the author OR another resolved teammate (merged_by_id != None) performs the merge; only flags merges by an unknown user (admin, bot, external GitLab user). New explicit knobs: require_self_merge (default False - author must specifically click Merge) and forbid_self_merge (default False - reviewer-gates-merge model: someone OTHER than the author has to merge). Detail strings: "Merged by author", "Merged by a team member after approval", "Merged by an unknown user (not a team member)".
    • R11 (MR + Issue closed) - strict pairing via linked_issue_iid. Previously the check was decoupled: any merged MR + any closed issue was enough, so a student could pass even when issue #7 was still open and they had merged something completely unrelated. The engine now builds issues_by_iid = {i.gitlab_iid: i for i in ctx.issues_assigned}, looks up mr.linked_issue_iid for each merged MR, and only passes when the paired issue is closed. Fallback: when no merged MR has a linked_issue_iid (student forgot to write Closes #N and didn't name the branch), the engine falls back to the legacy any-issue-closed semantics so unlinked-but-closed pairs aren't penalised harder than before. Detail strings: "Merged MR's linked issue is closed", "Merged MR references an issue that is still open", "MR merged and assigned issue closed (no explicit link)", "MR merged but no assigned issue is closed".
    • R12 (Pipeline Health) - broader test-job detection. The "test" substring caught only "test", "pytest", "junit" so a pipeline running a job named "spec", "verify", "coverage", "jest", "mocha", "rspec" or "phpunit" was reported as "no test job" and R12 failed. The detector in pipeline_processor.py now scans the tuple ("test", "spec", "pytest", "junit", "jest", "mocha", "rspec", "phpunit", "verify", "coverage") against both name and stage, covering typical cross-language naming conventions.

Security

  • Absolute 24 h session-lifetime ceiling (AUTH-06): previously the session store implemented only a sliding TTL - an active user (or a stolen long-lived cookie) could keep refreshing the TTL indefinitely. Added session_absolute_max_age_seconds (default 24 h); the store stamps created_at on create() and rejects sessions older than the ceiling on load() regardless of how often the sliding TTL was refreshed. Bounds the blast radius of a stolen cookie.
  • Webhook post-commit side-effects now fail best-effort (EVT-01): the event processor used to commit the DB transaction and then run cache invalidation, immediate enrichment, metrics enqueue, and SSE notify. If any of those raised, the outer except called rollback - but the commit was already persisted, so event.processed=True remained and the side-effects were silently lost. Each side-effect now goes through _safe(coro, name) which catches exceptions, increments the Prometheus counter webhook_processed_total{status="sideeffect_failed:<name>"} and logs WARNING instead of bubbling.
  • Redis now requires a password even on the internal Docker network (SEC-03/SEC-04): previously Redis listened without authentication and REDIS_URL could be redis://redis:6379/0 with no password. An attacker with access to the Docker bridge (or a misconfigured port mapping) could read session blobs and queues. The app.config validator now refuses to start in APP_ENV=production when REDIS_URL lacks a password segment (redis://:PASSWORD@...). The Compose template enables --requirepass whenever REDIS_PASSWORD is set. Production now runs with a 40-char generated password, all 4 workers + scheduler + api reconnected.
  • FEATURE_RBAC=false in production hard-fails (AUTH-04): previously _check_required_roles only logged a warning and let the request through without role checks. A misconfigured flag would silently open every teacher-only endpoint. The validator now raises ValueError at startup so the deploy fails fast instead of fail-open.
  • DEBUG=true in production hard-fails (§7b): debug mode exposes stack traces and may auto-create admin users. Validator refuses to start.
  • Rate-limit fails closed in production (AUTH-05): when Redis was unavailable, dependencies.check_rate_limit swallowed the exception and allowed the request (graceful degradation). That meant an attacker could bypass rate limiting by knocking Redis down (or by brute-forcing during a Redis flap). Production now returns HTTP 503 with Retry-After: 30 and an rate_limit_unavailable_blocking log line. Dev and tests stay permissive.
  • SESSION_STRICT_UA_FINGERPRINT left opt-in (AUTH-02): UA-fingerprint is available as defence-in-depth against XSS / cookie-theft replay from a different OS / browser, but the default stays false. The UA hash is not stable across mobile <-> desktop switches or browser auto-updates, and a true default was kicking real users out with 401s in production. The combination of strict CSRF + IP binding + the new absolute session ceiling (AUTH-04/05/06) provides equivalent post-stolen-cookie protection without the false-positive cost.

Added

  • Healthcheck for the Caddy reverse proxy (INFRA-02): Compose definition now runs caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile (interval 30 s, retry 3, start_period 10 s). When a typo in Caddyfile would crash the config, the container is marked unhealthy instead of silently serving stale state.
  • JSON-file log rotation 20 MB × 5 (INFRA-04): postgres, redis, api, worker, scheduler, and caddy use the x-default-logging YAML anchor for default logging. Previously /var/lib/docker/containers/*/*.log could grow to tens of GB with verbose workers and exhaust the host disk.
  • Scheduler healthcheck via /proc/1/cmdline: the original pgrep -f failed because the slim worker image does not ship procps. We now use tr '\0' ' ' < /proc/1/cmdline | grep -q app.workers.scheduler - works in every Linux container without extra dependencies.
  • Integration tests run automatically on feature branches (CI-05): previously when: manual + allow_failure: true meant integration regressions were caught only when an MR was opened or main was merged. They now run on every feature-branch push.

Improved

  • API memory limit 768 MB -> 1024 MB (INFRA-03): the PDF render pipeline plus concurrent LLM streams occasionally hit OOM kill. Worker bumped 512 -> 768 MB.
  • Sweep cleanup runs in batched deletes (DB-04): _cleanup_events_async, _cleanup_snapshots_async, and _cleanup_audit_async previously ran a single DELETE WHERE timestamp < cutoff statement. With 1 M+ rows that meant a long transaction that blocked replication slots and stalled vacuum. The new _delete_in_batches helper deletes in 5 K-row chunks with commits between, capped at 200 batches per run (~1 M rows / sweep). Subsequent runs drain the rest.
  • SBOM generation no longer allow_failure (CI-02): SBOM is a supply-chain compliance artefact. A silent fail would mean releases shipping without an auditable dependency listing.
  • Global APP_ENV: "test" in .gitlab-ci.yml: the unit-tests job had no explicit APP_ENV, so the new production validators caused Settings() to raise ValueError in tests. The global variable ensures only production-targeted jobs run in production mode.

Fixed

  • Global job-bar - ETA "2 min left" stuck while elapsed already passed 5+ minutes: _computeEta only fired on a new progress sample, so between batches the 1 Hz timer just incremented the elapsed clock while the time-left label kept the last computed value. ETA is now anchored to a wall-clock expectedFinishAt; the tick re-renders [data-job-eta] via _displayEta(job), which counts down to 0 and, on expiry of the original prediction, recomputes from a cumulative rate (totalElapsed / pct * (100-pct)). No flicker between renders, ETA decreases monotonically.
  • Sync-completion toasts missed when window minimized: sidebar.js was closing the dashboard SSE on visibilitychange->hidden. Server pushes for member_sync_complete therefore never reached the browser while the tab was minimized - _queuedToasts had nothing to flush on return. SSE now stays open (browsers throttle but still deliver), only the 60 s polling timer is paused.
  • Course Analytics - Alpine report-controller load order: the legacy report-form Alpine controller lived in pages/course-analytics.js, which was loaded via <script defer> inside the HTMX-swapped partial. HTMX clones script tags with the defer attribute, but dynamically-inserted scripts ignore defer semantics - they execute async after a fetch. Alpine meanwhile walked the new subtree and evaluated the controller before the script had loaded -> cascade startDate / endDate / durationText / dateError is not defined. The controller was then inlined as an x-data="{...}" literal directly on the <form> element - no load-order dependency at all.
  • Flaky test_tampered_signature_returns_none: itsdangerous' URLSafeSerializer (SHA-1) emits a 27-char base64 signature (162 bits, of which the trailing 2 are padding bits). Flipping only the last base64 char between "A" and "B" left the decoded HMAC bytes identical (the difference was in the padding bits). The test therefore reported in ~25% of pipelines that tampering didn't break the signature - truthfully, but it meant the test was measuring the wrong thing. We now mutate an interior char.

[2026-04-29]

Added

  • Course-staff notification on new support ticket: previously only the ticket submitter received an e-mail (acknowledgement). Teachers / owners of the course only found out about new tickets by manually checking the dashboard. New notify_ticket_new_to_staff template (subject [GitPulse] New ticket #{id} ({course_code}) - {title}) is now sent to every active owner + teacher of the course (excluding the submitter) from both branches of submit_support_ticket (dynamic-form and fixed-column). Protected by an idempotency key ticket_new_staff:<ticket_id>:<email>, exponential-backoff retry, and the 2-minute sweep fallback (api/telemetry.py, services/email_notifications.py)

Fixed

  • Privacy leak: internal notes were e-mailed to the submitter: notify_ticket_comment was e-mailing the submitter even when is_internal=true (a staff-only private note - e.g. "this student is lying, ignore"). Added an explicit not payload.is_internal guard before notifying the submitter. Internal notes now go strictly to the assigned teacher / staff
  • Avatars no longer get stuck on letter fallback after laptop sleep: when the laptop sleeps or Wi-Fi briefly drops, the browser fires an error event on every in-flight <img> avatar. The previous handler immediately hid the <img> and showed the letter fallback - with no recovery logic whatsoever. After a single sleep, EVERY avatar across the app (header, dashboard greeting, student profile, support tickets, course teachers, search) silently degraded to letter placeholders until a hard reload. Fix in core.js: save the original src on first error; when !navigator.onLine, defer the fallback decision (mark data-avatar-pending="1") instead of showing the letter; a new retry-all listener on online + pageshow (BFCache) + visibilitychange->visible with a 250 ms defer (to let DNS / Wi-Fi settle after wake) and a cache-buster re-fetches all pending / fallback avatars in one batch. Centralized - works across the whole app, not just one page
  • Real double-submit race in forms - some actions fired 2 POSTs per click: the global form interceptor in core.js ran on the capture phase and called preventDefault() + fetch(), while page-level handlers on the bubble phase ALSO called preventDefault() + fetch() on the same form. Result: two HTTP POSTs per click. Affected: dismiss LLM error (project detail), login, the Alpine @submit course-teacher assignment form. Fix: extended skip-rules - the global handler now leaves alone forms with data-submit-action, @submit / @submit.prevent (Alpine), data-no-boundary="1", or HTMX attributes. login.html was marked data-no-boundary="1"
  • E-mail scheduler stuck for 25+ hours - end of the "no e-mail ever arrived" mystery: the scheduler container, after a long uptime, stopped registering periodic jobs; on start it logged only "Scheduler registered 5 periodic jobs" instead of the full set. sweep_email_queue was never invoked -> rows in email_log stayed pending forever. After redeploy.py (which recreates containers), the scheduler correctly registers all jobs and a smoke-test e-mail flowed through the pipeline in 1.6 s

Improved

  • Caddy configuration - Caddy 2.x deprecation warnings cleaned up: basicauth directive renamed to basic_auth for /grafana, /prometheus, /jaeger (×3); removed the redundant header_up X-Forwarded-Proto {scheme} (×2 - Caddy's reverse_proxy passes it automatically). After rebuild no deprecation / style warnings remain in caddy logs; basic_auth still enforces 401 without credentials

[2026-04-28]

Improved

  • Student profile - MR and issue notes now render markdown: review comments in partials/student/_comments.html previously displayed only with # / ! / @ references linkified, with the rest as raw text. They now go through the render_gl_description filter, which has been extended to support fenced code blocks (```c ...```) - content is escaped, wrapped in <pre class="gl-desc-pre"><code class="language-X">...</code></pre>, and gets its own CSS. Headings (##), **bold**, _italic_, bullet/numbered lists, inline `code`, and fenced blocks all behave like the GitLab web UI now
  • Markdown renderer - no more huge gaps between sections: newlines are now stripped around BOTH opening and closing block tags (<h6> ... </h6>, <pre>, <ul>, <ol>), so a blank line between a heading and a paragraph no longer renders as a visible double <br> gap. Safety cap: 3+ consecutive <br> tags collapse to 2
  • Note list (components/lists.html) - markdown render: the secondary note-list-item component (legacy path) now uses the same render_gl_description filter as the student detail page so no GitLab comment surface is left rendering raw escaped text
  • Globally unified course-sync banner format: five different code paths produced four slightly different formats of the same Syncing... text (SSE detail / SSE fallback / legacy poll / registerServerSyncs / _reconcileCourseJobs). A single helper _courseSyncMsg(name, running, queued, synced, total) in global-job-bar.js now produces ONE format everywhere: "NAME: Syncing X/Y groups done (R active, Q queued)", gracefully dropping the X/Y and parens when data is unknown. Helper is exposed on window._courseSyncMsg, dashboard and course-detail page scripts call the same code - no more flicker between formats on every SSE tick or htmx settle

[2026-04-27]

Fixed

  • CI dependency-scan job - no more spurious "Job failed": the second pip-audit run (against the full installed environment, not only requirements-lock.txt) was flagging vulnerabilities in CI tooling - pip itself (CVE-2026-3219, no fix released yet) and transitive deps of cyclonedx-bom/pip-audit - which are not part of our application dependency closure. Added || true to the informational scan plus a comment; the requirements-lock.txt audit (actual runtime deps) stays strict and continues to publish the SBOM artifact
  • Dashboard - false "Process timed out - check results manually" toast during long syncs: two compounding bugs. (1) _jobMaxAge for course-sync jobs (UUID-like or numeric IDs) was 10 minutes; a real 8-group course sync routinely runs 15-25 minutes, so the staleness guard fired mid-flight. Bumped to 45 minutes. (2) The previous "passive deferGlobalJob must not overwrite live message" fix early-returned without calling showGlobalJob, so _autoStartPolling was never invoked -> lastUpdate stopped being refreshed and the guard fired on healthy jobs. Passive branch now starts polling when no poller is attached (global-job-bar.js)

[2026-04-25]

Improved

  • Per-team Status pill on the course page - flips instantly on Sync All click: optimistic update in triggerSyncAll() writes a QUEUED badge to cells[4] of every team row and "Sync queued..." into the Last Updated column before the server responds. _refreshCourseData() no longer overwrites SYNCING/QUEUED badges during the SSE race; updateTeamRow(team) owns all state transitions (course-detail.js)
  • /sync-all responds in ~100 ms (previously ~20 s): team discovery (/projects/:id/users calls) moved into a background task _discover_course_teams_async - the endpoint queues immediately and the frontend no longer blocks on GitLab rate limits (api/sync.py)
  • Pilot Telemetry Service lint - cast(date, ...) cleanup: unified datetime imports, renamed shadowing loop variables, ruff/mypy strict clean (services/pilot_telemetry.py)
  • No more progress-text flicker on dashboard course cards: Last Updated / progress text stabilised during SSE updates

[2026-04-24]

Fixed

  • Compliance report PDF - no more orphaned headings or clipped rows: removed page-break-after: avoid on <h2> and break-inside: avoid on large narrative / attention blocks in @media print. In A4 landscape that combination made WeasyPrint push section headings onto an empty page whenever the following content did not fit in the remaining space. Only row-level break-inside: avoid is kept, so individual table rows are never sliced in half (src/app/workers/report_job.py)
  • Pilot Evaluation - realistic counts: instead of a raw MetricSnapshot row count (which grew on every webhook), we now count distinct (team_id, student_id, week_bucket) tuples joined through TeamMember with is_active + excluded_from_scoring=False - teachers, peer mentors and excluded members are no longer included. Dropped the or fallback that silently leaked the global count when rpt_course_count was zero. Weeks-covered now falls back to len(compliance_trends.weeks) so the KPI no longer reads 0 when the trend shows 5 weeks. Click multiplier reduced to 2×checks + 4×flags (src/app/services/evaluation_report.py)

Improved

  • Project detail - action buttons match the group/course header size: the project page action buttons (Open in GitLab, Rotate Secret, Unlink, Run Checks) no longer use size='sm' - they now line up with Link Project / Sync / Run Checks / Export CSV / Export JSON / Retired Report / Delete on the group page. Visual consistency across Course -> Group -> Project
  • @mentions in MR descriptions on the student page render as names: the linkify_gl_refs filter now accepts an optional username_map (gitlab_username -> full_name) and substitutes the readable name for any mention matched against a team member (e.g. Reviewers: Andrii Betsa instead of Reviewers: @sm361rw). Unknown @mentions still render as the plain handle. Touched templates: partials/student/_merge_requests.html (MR description), partials/student/_comments.html (MR notes)
  • Compliance report - thesis-style redesign: numbered sections 1-9, Table 1..N captions, page-number footer, empty-state paragraphs per section, compact "Group Overview" instead of the 4-card grid, deduplicated executive summary and recommended actions

[2026-04-23]

Added

  • Per-course skip_auto_archive opt-out (migration 036): new boolean on course_settings shields manually reactivated courses from being re-archived on the next sweep run. course_service.activate_course() sets it to True automatically, so a course revived post-end-date doesn't vanish again the next day. Teachers can toggle it from the course settings "Semester dates" section
  • Presets wired into Create Course and Clone Course modals: the Quick preset selector now fills academic_year, semester (winter/summer), and both semester_start_date / semester_end_date with one click - teachers no longer have to open Settings after creating a course for auto-archiving to kick in. POST /api/v1/courses/ accepts optional semester_start_date / semester_end_date directly at creation time
  • SemesterPreset now carries a semester field (winter / summer) matching Course.semester vocabulary, removing the need to manually sync preset choice with the Winter/Summer selector
  • Full config export/import now carries skip_auto_archive as well - config-import.js validates and surfaces "Skip auto-archive: yes" in the import summary
  • Automatic shutdown of automation after semester end: new daily scheduled job sweep_auto_archive_ended_courses (queue low, 24 h interval) flips courses.is_active=False for courses whose semester_end_date + semester_grace_days has passed. Because all 23 existing periodic sweeps already filter on Course.is_active.is_(True), this single flag flip immediately stops: periodic_enrich_all, periodic_membership_sync, periodic_metrics_all, periodic_llm_analysis, periodic_telemetry_snapshot, periodic_verify_webhooks, periodic_docker_reverify, periodic_enrich_users, sweep_stale_projects, sweep_dead_projects, sweep_discover_new_projects, sweep_discover_new_teams, plus all cleanup sweeps. Result: zero GitLab API calls for finished semesters (migration 035_semester_end_date.py)
  • New column course_settings.semester_end_date (DATE NULL): explicit "last teaching / exam day". For courses without an explicit semester_end_date, the helper derives it from semester_start_date + semester_default_length_weeks (default 14) - protects against forgotten courses running indefinitely
  • Semester quick-presets for TUKE FEI KPI - Informatika (all years of study): new module app.utils.semester_presets + endpoint GET /dashboard/semester-presets ships 4 presets: Winter 2025/2026, Summer 2025/2026, Winter 2026/2027, Summer 2026/2027. Course-settings UI dropdown "Quick preset" one-click fills both dates (Monday of week 1 + last exam day); teachers can still tweak individual dates. The calendar is identical across 1st, 2nd and 3rd year - TUKE FEI uses a single academic calendar for the whole Informatika study program
  • New settings in app.config: semester_auto_archive_enabled (feature flag, default True), semester_grace_days (default 14 days - buffer for grade finalization, late MRs, final report exports), semester_default_length_weeks (default 14, fallback for courses without semester_end_date)
  • Helper module app.utils.semester: two pure functions - get_effective_semester_end(cs, default_length_weeks) and is_semester_over(cs, grace_days, default_length_weeks, today=None). Explicit semester_end_date takes precedence; fallback is semester_start_date + N weeks. Open-ended courses (neither start nor end) are never auto-archived - they can only be archived manually via the admin UI
  • Full config export/import cycle now also carries semester_end_date in the /settings/full-config JSON (both export and import branches), so presets transfer cleanly between instances and cloned courses
  • 14 new unit tests in tests/unit/test_semester.py covering: empty settings, open-ended course, within-grace, past-grace, fallback-to-start+N-weeks, preset uniqueness, bilingual labels, JSON shape
  • Slovak translations added: Dátum konca semestra (Semester End Date), Koniec semestra, Dátumy semestra, Rýchla predvoľba (Quick preset), - Vyberte predvoľbu -, Končí (Ends), Posledný deň výučby / skúšok..., TUKE FEI - Zimný/Letný semester 2025/2026 a 2026/2027 (Informatika, všetky ročníky), and more

Fixed

  • Weekly Score Trend & Weekly Breakdown - fabricated W1-W7 rows for teams active from mid-semester onwards: chain of five linked bugs that caused the Score Trend line to show a bogus W1=100 % -> W7=0 % -> W8+ real shape, Skill Profile to render a stale W7 (0 %) radar and the Weekly Breakdown to list seven empty red rows triggering false teacher/student alarm:
    • (a) pilot_telemetry.get_student_compliance_history primary path (when StudentComplianceSnapshot rows existed) never returned the assembled data - the return filled was missing - so control fell through to the MetricSnapshot fallback, which only knew the latest computed_at (W10). Fix: added return filled (commit bd189ee8)
    • (b) compliance.engine.backfill_compliance_history fabricated StudentComplianceSnapshot rows for weeks 1..N even for teams whose GitLab project did not yet exist (-> W1=100 % "default ok" + W2..W7=0 %). Fix: detect the team's first-activity week from MIN(Commit.committed_at) and skip earlier weeks (commit 3e6fe1b1)
    • © heterogeneous tuple for col, model in ((Commit.committed_at, Commit), (MergeRequest.created_at, MergeRequest), ...) failed CI strict-mypy due to union-type inference. Fix: split into three explicit select(func.min(...)) queries (commit 52611892)
    • (d) Result[Any].rowcount is absent from the SQLAlchemy async stubs -> CI mypy attr-defined failure. Fix: getattr(deleted, "rowcount", 0) or 0 (commit c289c5b1)
    • (e) hidden root cause: GitLab auto-creates an initial README commit at project-creation time with committed_at ≈ project creation time, so MIN(Commit.committed_at) returned W1 for every team and the anti-fabrication logic from (b) was a no-op. Fix: first_activity is now derived from MIN(MergeRequest.created_at) and MIN(Issue.created_at) (genuine intentional student actions); commits are only used as a fallback and only commits whose author_id matches TeamMember.student_id (so the bootstrap/system commit is ignored) (commit 56a9387f)
    • (f) post-deploy audit fix: even after the pre-activity StudentComplianceSnapshot DELETE, the API still synthesized W1..(first_activity-1) as null stub rows because of for wk in range(1, max_week+1) - Weekly Breakdown rendered them as empty em-dash rows. Fix: range starts at min(populated_weeks), so chart/heatmap/table only show weeks with real data - no fabricated numbers, no misleading empty rows
  • Net effect: for a team that started work in W8 with current activity in W10, the UI now honestly shows only W8, W9, W10 - no misleading W1=100 % (false green) or W2-W7=0 % (false red)

[2026-04-22]

Added

  • Job bar - "Finishing..." state: the global progress bar no longer appears frozen at "0 running, 0 queued" during the server's 15-second post-batch grace window. When the client sees running==0 && queued==0 while the server still reports has_active_sync=true, the bar switches to a finishing status with t('Finishing...') instead of showing a misleadingly empty counter. Applied to both the SSE branch and the legacy poll fallback in global-job-bar.js
  • i18n - covered card states & job-bar fallbacks: every hardcoded English fallback in global-job-bar.js ('Analyzing...', 'Generating report...', 'Running compliance checks...', 'Importing roster...', 'Exporting data...', 'Processing...') is now wrapped in t(...) with matching keys added to sk.json. The scripts/find_missing_translations.py audit now reports 0 missing keys

Added

  • Issues to Address -> interactive chart: the static list of failing checks has been replaced by a Chart.js horizontal bar chart - X axis is % of group affected, bars are severity-coloured (red High / orange Medium / blue Low) with a 900 ms staggered entrance animation. Hover tooltip shows count/weight/severity, clicking a bar expands a detail panel with the affected students rendered as a responsive grid of cards, each click-through to the member row in the table. Reuses the IntersectionObserver scroll-reveal pattern and a <noscript> fallback. Chart.js is loaded on project-detail only via extra_head
  • Issues-to-Address detail panel: new card with severity-coloured left border, uppercase severity pill, × close button, tabular-nums stats (affected/total · share · weight) and students rendered as hover-highlighted cards in a responsive grid. Legend re-styled as pill chips (instead of italic hint) for consistency with the rest of the component system
  • Quick Access - informative items: each sidebar entry now shows a traffic-light dot from the latest compliance snapshot, the score in %, and member count from TeamMetricSnapshot instead of just the name (the sidebar is now informative, not decorative)
  • Slovak UI translations - full coverage: added 99 missing translations to sk.json (sync error messages, confirmation dialogs, toast messages, retired grading editor, admin-users page, user-info modal, project reactivation, CI scenarios). New audit script scripts/find_missing_translations.py scans all t(...) calls across JS and templates and reports missing keys - added to the CI checklist
  • Typed exceptions for GitLab client: new GitLabPaginationTruncatedError class (subclass of GitLabAPIError) - pagination truncation no longer returns partial data silently but raises, so per-project try/except marks only that project as failed and the sweep retries it later

Fixed

  • Mobile responsiveness - tables & profile card: (1) profile header .profile-identity lacked min-width:0 and the "View on GitLab" link overflowed the card on narrow viewports - added flex:1+min-width:0, overflow-wrap:anywhere and align-self:flex-start+nowrap on the link. (2) Project members table had <td class="text-nowrap"> on the name cell, causing the sticky column to stretch and cover the R01-R13 check cells - on mobile text-nowrap is overridden to white-space:normal + max-width:14rem + wrapped role badges. (3) Course teams table (7 columns) now hides the tree-toggle and Last Updated columns on mobile so Avg Score / Status / Actions remain visible without horizontal scrolling. (4) Check cells .check-pass/.check-fail/.check-na switched to vertical-align:middle - ticks/crosses no longer cling to the top of multi-line name rows
  • Click on student name in Issues to Address no longer scrolls page to top: scrollIntoView also scrolled outer scroll containers (viewport) causing the fixed topbar to visibly shift. The scroll now targets .gl-page-content exclusively and computes scrollTop + boundingRect offset so the target row centers inside the scrollable zone while surrounding chrome (topbar, breadcrumb, sidebar) stays put
  • Weekly compliance heatmap - historical weeks no longer empty: two linked bugs were wiping past weeks on every sync. (a) compliance.engine.backfill_compliance_history ignored as_of_week and always wrote the current week number to StudentComplianceSnapshot.week_number. (b) pilot_telemetry.capture_team_snapshots upserted via ON CONFLICT DO UPDATE with a stripped raw_metrics payload (no checks_detail), overwriting the 13-check breakdown the engine just persisted. Fix: engine now honours as_of_week; telemetry sweep derives category scores via _derive_category_scores(checks_detail) and writes a superset. 170 unit tests pass

  • Quick Access "Subgroups and Projects" tab empty: the SQL query for qa_projects was sorted by latest_computed and limited to 20 rows, but those 20 rows were the same teams already shown in Groups (team:project is ~1:1), so after Python-side dedup 0 projects remained and the tab always showed an empty state. Now team_id NOT IN is pushed into SQL WHERE, so the 5-row limit is spent on teams NOT already surfaced in Groups; if Groups covers every team (≤5 total), a fallback shows the top-5 projects regardless

  • Course Analytics - auto-scrolled page on load: initial selectWeek(currentWeek) called pill.scrollIntoView({block:'nearest'}), which browsers interpreted as scrolling the entire document vertically (when the target was below the fold). Initial select now skips scroll (scrollPill:false), and user-driven clicks use weekPills.scrollBy({left, behavior:'smooth'}) scoped to the horizontal pill strip only
  • LLM modal - guard against calling endpoints without an API key: all LLM modals now check both the feature flag and API-key presence before showing and return a friendly error instead of a 500 from the upstream call
  • GitLab 429 (rate limit) - silent data loss: after retries were exhausted the client returned None, which callers treated as "not found" (404) - sync jobs marked as completed with missing issues/MRs/pipelines, dashboard showed wrong metrics. Now raises GitLabAPIError(429) with Retry-After in the message
  • GitLab pagination (10 000+ items) - silent data loss: when hitting max_pages=100 cap with a full final page (meaning GitLab has more data), the client returned a partial list and only logged a warning. Dashboard would show "completed" with under-counted numbers. Now raises GitLabPaginationTruncatedError with structured fields (path, max_pages, items_fetched) for Grafana alerting
  • Webhook rate-limit IP spoofing (DoS): orphan-webhook rate limiter read X-Forwarded-For directly from headers, bypassing the ProxyHeadersMiddleware trust boundary - attacker could rotate spoofed IPs to bypass 10/min limit and flood orphan_webhook_events. Now uses request.client.host which the middleware already validated against trusted_hosts
  • Webhook backpressure - silent check failure: except: enqueue anyway silently hid backpressure-module failures / Redis outages. Fallback preserved (best-effort enqueue), but now logs logger.warning with exc_info - operators see degraded mode instead of false confidence
  • Global job bar - interval leak on tab close: _syncWatchdogId (5 s polling of /sync/active) was not cleared in the beforeunload handler - interval lived until GC after tab close or logout. Now clearInterval alongside _elapsedTimerId

[2026-04-21]

Changed

  • Sync UI - single source of truth: removed the redundant inline "Sync in progress" info-card from the course Overview, Roster and Teachers tabs - the persistent global progress bar is now the sole sync indicator, eliminating duplicate-UI flashes and double-animation during page transitions (-312 lines of dead UI code, including the SyncStatusBanner component and its pollers)
  • Sync freshness guard (up-to-date check): POST /api/v1/courses/{id}/sync-all and POST /api/v1/teams/{id}/sync now skip teams whose last successful sync completed within the 5-minute freshness window - response carries already_fresh: N, single-team returns status: "skipped_fresh"; pass ?force=true to bypass. Prevents hammering the GitLab API when Sync All is clicked repeatedly or across multiple teachers viewing the same course. Frontend prompts for force re-sync when everything is fresh instead of silently no-op-ing

Fixed

  • Global progress bar - cross-page discovery of active syncs: new GET /api/v1/sync/active endpoint returns in-progress course syncs; the global bar now calls it on page load, so the bar also appears on Admin -> Users / System / Dashboard when a sync was started in another tab, device, or by a scheduler
  • Global progress bar - race conditions: removed silent bar removal during the 2-second restore window when /sync-status transiently returned has_active_sync=false during a state transition; jobs now carry a serverConfirmedRunning flag, and the silent-remove guard fires only for genuine redirect-flash cases
  • Issues to Address - direct project members only: card was iterating the full team roster (e.g. 37/37) instead of only direct GitLab project members (e.g. 5/5) - live-data endpoint now calls get_project_members(include_inherited=False) matching full-page render
  • Issues to Address - dynamic refresh: card now re-renders server-side on every change (exclude toggle, role change, check run) via Jinja partial
  • Issues to Address - synthetic/inherited member leak: teachers, mentors, inherited and synthetic GitLab-only members excluded from scored cohort
  • CSS bleed system-wide fix: global details summary::before rules scoped to opt-in .details-accordion class; duplicate unicode ▶ chevron removed from pages/course.css

[2026-04-20]

Added

  • Comprehensive mobile responsiveness overhaul
  • GitHub-style zebra tables + changelog sidebar widget
  • Comprehensive changelog from all 1347 commits since repo creation

Fixed

  • Auth resilience - login page for new users, next-URL preservation, crash protection
  • Redesign OG image - clean centered layout for Google previews
  • Prevent skeleton flash + lighten heavy table skeletons
  • Rebuild bundles + ruff format auth.py (fix CI lint)
  • Prevent zebra-stripe flash when toggling member filters
  • Clean empty CSS ruleset + add restripeTable to no-paginator filter path
  • Fully suppress transition on clickable rows during restripe
  • Zebra tables without animations - remove transitions on rows, instant hover (GitHub style)
  • Propagate zebra/hover backgrounds to <td> for overflow:hidden tables
  • Use solid opaque colors for table zebra stripes
  • Remove opacity from row-page-enter animation to prevent color mixing during filter/pagination

Changed

  • GitLab-style skeleton loaders - lighter, mixed with spinners

Infrastructure

  • Code quality audit - var->let/const, dead code removal, cache fixes

[2026-04-13 - 2026-04-17]

Added

  • Periodic team discovery sweep + lint fixes
  • Email-based teacher/mentor detection + early sync progress
  • Improve OG image + add skeleton loader to User Management
  • Add peer mentor detection and filter
  • Production-grade email notification system with retry queue

Fixed

  • Email enrichment propagates to User + filter inherited teachers from contributors
  • Whitelist contributor filter - show only team student commits
  • Whitelist contributor counts on course/team dashboard too
  • Remove deprecated code, fix labels, inherited members modal, subtle flash
  • Resolve mypy arg-type errors in _stats.py narrowing
  • Lower coverage threshold to 30, fix arena email caching and timeouts
  • Replace asyncio.TimeoutError with builtin TimeoutError (ruff UP041)
  • Add arena email fallback to single-user enrich, suppress global bar flash on redirect
  • Auto-create User records for inherited members, remove teacher/mentor opacity
  • Batch enrich Arena fallback, course count from groups, inherited member import
  • Inherited member import, toast visibility, course cards in modal
  • Course display with code/year/semester for membership-derived courses, add academic_year+semester to membership API, fix ruff-format
  • Arena enrichment for User records during import, role badge for derived courses, elevate Arena logs to warning
  • Mypy User.gitlab_username type + strip course name from card sync badge
  • Stale sync after redeploy + smooth global bar entrance
  • Create User records during member sync (not only on initial import)
  • Ensure User record for existing active members during sync
  • Limit User creation during sync to teachers only
  • Enrich User emails from Student records and Arena during sync
  • Sync_all discovers new teams + Arena enrichment shared httpx client
  • Arena enrichment 30s per-request timeout, concurrency 2
  • Update scheduler test counts for job #23 + cross-team project dup guard
  • Pass course_id to sync worker for early progress toasts
  • Add toasts to enrich button + welcome-back toast after login
  • Reduce lazy skeleton reveal delay from 400ms to 150ms
  • Rewrite admin users skeleton to match page structure + ruff format
  • Remove corrupted duplicate in sk_admin_users skeleton
  • Defer admin-users paginator/filter init for lazy-shell loading
  • Skeleton pagination to match 3-column layout (info | buttons | per-page)
  • Update all skeleton pagination to 3-column layout (info|buttons|per-page)
  • Skeleton search width, pagination inside card-body, mobile dropdown clamping
  • Dropdown portal right-aligned, support tickets skeleton structure
  • Dropdown portal conflict with global handler - use data-gl-portal marker
  • Keep per-page dropdown visible when pageSize=all so user can switch back
  • Skeleton card structure, pagination DRY, pageSize localStorage cache
  • Add per-page dropdown to admin/system Job History pagination
  • Mobile responsiveness, optimize scheduler & telemetry
  • Stop spinner reset and button color flash on mobile
  • Pull-to-refresh false trigger on table scroll + toast reliability + lint
  • Completion toast suppressed when global bar visible
  • Ruff E501 in scheduler.py (line too long)
  • Member table, enrichment gate, contributor exclusion
  • Global bar dedup, contributor exclusion in project detail
  • Complete mentor support across all views and scoring paths
  • Exclude mentors from all student count queries
  • Preserve is_mentor and excluded_from_scoring on team reassignment
  • Increase worker heartbeat freshness window to match RQ TTL (420s)
  • Enrich PRIVATE emails during sync and periodic sweep
  • Member table bugs - filter empty state, badge count, sorting, inherited visibility, commit-based email enrichment
  • Arena email enrichment, mentor badge, blue focus, progress bar course name, student count labels
  • Dashboard Students stat now counts User accounts (matches admin breakdown)
  • Deduplicate role badges, global bar flash, blue row selection
  • Prevent global bar flash during skeleton loader phase
  • Ruff-check unused import + ruff-format (fixes CI pipeline)
  • Dashboard JS created duplicate at-risk badge using old class names
  • Show more description text in course card (3-line clamp, no Jinja truncate)
  • Global job bar hidden on team page when course sync is running
  • Sorting/filter conflict + welcome email on first login
  • Rebuild JS bundle for sorting/filter changes
  • Correct CTA links in welcome emails (404 paths)
  • Remove all emoji characters from email notifications
  • Replace deforming loader icon with smooth circle spinner
  • Show text status badges instead of dots in group activity table
  • Prevent page-size dropdown from being clipped by pagination container
  • Replace custom page-size dropdown with native select
  • Restore custom page-size dropdown with fixed positioning
  • Portal page-size dropdown to document.body to escape overflow
  • Smart dropdown direction - open downward when space allows
  • Remove unused Request import (ruff F401)
  • Tabs overflow, footer theme, remove icons, accuracy fixes
  • Tabs scroll + footer light theme with explicit colors
  • Shorten tab names, remove tab icons, light footer, full-width tabs
  • Footer light mode with !important to override Material theme
  • Footer text/link colors visible in light mode
  • Stronger no-store cache headers to prevent stale proxy/browser cache
  • Job history table responsive headers + zebra striping on filter/sort
  • Replace broken Sync Status modal with toast notifications
  • Update tests for queue-based email system + fix ruff lint

Changed

  • Replace var with const/Set/Map in JS, use begin_nested savepoints instead of rollback, add logger.debug for inherited member fetches, consolidate imports, fix line lengths
  • Reuse ClientPaginator for admin Job History (server-side mode)
  • Revert: remove commit-based email enrichment - Arena API already handles this
  • Privacy: comprehensive GitLab-quality privacy statement with TOC, tables, GDPR rights, retention schedule, cookie details, supervisory authority

Security

  • Fix header issues from security scan
  • Add privacy policy page, CSP upgrade-insecure-requests, sitemap update

UI / UX

  • Redesign: course card - clean layout like top companies

Infrastructure

  • Config: pass EXCLUDED_CONTRIBUTOR_NAMES through docker-compose
  • Config: remove unused ARENA_EMAIL_API_TOKEN from compose (endpoint is public)

Documentation

  • Comprehensive audit - fix counts, intervals, add 19 missing models
  • Full documentation audit and actualization
  • Move legal pages to docs-site, add student guide, open docs access

[2026-04-09 - 2026-04-12]

Added

  • Smart stale data notifications with webhook activity detection
  • Webhook attention items + fix activity feed showing team actions
  • Premium Week Analytics with interactive Chart.js charts
  • I18n: add 17 missing Slovak translations for Week Analytics
  • Auto-expand current week in Week Analytics on page load
  • I18n: add 87 missing Slovak translations
  • Add Week Analytics skeleton loader to analytics page
  • Redesign report generation form with component-based UI
  • Simplify sub-project rows to links only, remove per-project metrics
  • Detect template repos, unify team page contributor count
  • Stale data warning banner for course and team pages
  • Comprehensive i18n, job history improvements, LLM config diagnostics
  • Expose Grafana/Prometheus/Jaeger via Caddy with basic auth
  • Enrich Configuration Preview with detailed settings, LLM status, course metadata
  • Install OpenTelemetry packages in Docker image and init tracing in workers
  • GitLab-style course sub-navigation + update course configs

Fixed

  • Enrich activity feed with background_jobs + fix audit commit in sync endpoints
  • Ruff lint + format errors (line length, unused import, alias)
  • Remove broken effective-start detection, replace weekly summary with week navigator
  • Analytics & activity quality audit - responsive, empty states, data consistency
  • Show empty state message in week bar chart when activity is 0
  • Pill focus outline follows rounded shape
  • Fully suppress rectangular focus outline on week pills
  • Remove focus ring from week pills entirely
  • Remove blue glow shadow from active week pill
  • Ruff-format compliance + suppress stale-data toast on report generation
  • Widen GitLab ID column, add webhook setup info to Event Pipeline page
  • Smart webhook detection on events page, rebuild CSS bundle
  • Use toast instead of alert banner for webhook warning on events page
  • Code quality improvements across admin module
  • CI pipeline failures - lint script, admin early-return in _session_caller
  • Mobile responsiveness - activity tables scroll, instructors layout, danger zone overlap
  • Redesign instructors mobile layout - CSS Grid 2-row compact (GitHub/GitLab style)
  • Mobile responsive - students table scroll, sidebar overflow, compact action buttons
  • Sidebar panel overflow:hidden, action buttons 32px matching btn base height
  • Distinct icons for team actions - code for JSON, bar-chart-2 for Report
  • Stretch action buttons evenly across full width on mobile
  • Uniform 36px square buttons in mobile action toolbar
  • Quick Access projects tab overflow + skeleton loader matches action toolbar
  • Quick Access ellipsis - move overflow:hidden to list level, not item
  • Quick Access ellipsis - remove negative margins, add overflow:hidden to .gl-qa-text
  • Truncate long project paths server-side (45 chars) + display:block on qa spans
  • Quick Access: filter empty/template teams, fix CSS text-overflow
  • Use student_id instead of id in TeamMember count
  • Add display:block + flex:1 for CSS text-overflow ellipsis in Quick Access
  • Use width:0+flex-grow:1 for reliable text-overflow ellipsis, cap sidebar on mobile
  • Commits table + heatmap mobile responsive, badge inline positioning
  • Pin trivy to 0.62.1 (latest tag removed from Docker Hub), guard publish upload
  • Team contributors count uses distinct commit authors for consistency with sub-projects
  • Live-data and SSE endpoints also use distinct commit authors for contributor counts
  • Show dash instead of 0% for projects with no resolved authors
  • Resolve ruff lint errors - unused import and per-file-ignores for utility scripts
  • Sub-projects inherit team avg score and traffic light when no commit authors resolved
  • Hide stale-data banner when sync is already in progress
  • Template detection now also checks author_gitlab_id against team students
  • Exclude teachers from contributor counts + robust compliance chart loading
  • Contributor count excludes teacher by name + compliance data_attrs as dict
  • Mypy types, dedup teacher imports, improve group messages, quick access, compliance visuals
  • Mypy list[Any] type, never-synced count uses sync status not metrics
  • MR empty state msg, improve semester settings, remove review requirements card
  • Align course configs with assignment requirements + fix sk.json broken quote
  • Remove C01/C02 custom checks, fix weights to sum 1.00
  • Resolve mypy and ruff lint errors in LLM status endpoint
  • Monitoring proxy + code quality cleanup
  • Rebuild CSS bundle (config-preview-details styles were missing)
  • Config preview sub-items layout (flex-basis + baseline align)
  • Hide AI Analysis Configuration card when LLM feature is disabled
  • Support form dropdown clipped by overflow:hidden on .gh-category-fields
  • Full monitoring stack - Grafana Failed to fetch, Jaeger DS, enhanced dashboard
  • Remove orphaned duplicate alerts in alerts.yml
  • Add basic_auth for Grafana metrics scraping by Prometheus
  • Correct rq_failed_jobs metric name in dashboard and alerts
  • Align DevOps config with ZSI, remove sergej.chodarev from teachers
  • Rebuild CSS bundle from sources + fix ZSI description
  • Update DevOps description with test (10b) + team project (10b)
  • Align course descriptions with official kurzy.kpi pages
  • Unify Refresh buttons across all course pages
  • Unify dashboard header buttons - remove size='sm' to match course pages
  • Break SSE read loop on done event to prevent spurious network error toast
  • Remove extra closing brace that broke JS bundle parsing
  • Break outer while loop in readImportSSEStream after done event
  • Concurrent email enrichment + activity feed labels/dedup
  • Remove commit sync date filter + fix CI lint + show 0 for empty sub-projects
  • R08 and R13 no longer auto-pass with zero student activity
  • Update R13 test to expect fail with zero commits
  • Progress timer ticks every 1s + threshold labels positioned at actual percentages
  • Send SSE done event before sync-enqueue to prevent network error toast
  • Default member filter shows Direct + Inherited to match team contributor count
  • Exclude inherited members from team student count on course overview

Changed

  • Deduplicate components, consolidate JS utils, update docs
  • Consolidate _lock alias into GP destructuring across 14 page JS files
  • Deduplicate code patterns, validate thresholds, clean translations
  • Unify contributor counts via shared _stats helper, remove fake per-project score fallback
  • Lift tab nav to shell pages - instant tabs, ARIA a11y, keyboard nav, body-only skeletons

UI / UX

  • Fix ruff-format for teams.py and reset_db_remote.py

Tests

  • Force-show all 3 stale data scenarios for visual review

[2026-03-30 - 2026-04-04]

Added

  • Search panel - mobile close btn, GitLab avatars, user->admin nav with auto-search
  • PWA support + rewrite login animations (no conflicts)
  • SSR-direct page entrance animation - staggered contentEnter for pages without skeleton loaders
  • Edge-to-edge mobile cards + pull-to-refresh
  • Comprehensive SEO - Google Search Console, OG, sitemap, robots.txt
  • Comprehensive email notifications + refactor to shared template
  • Non-anonymous tickets + auth-only submission + SMTP setup
  • Add proper error handling for 401/session expired
  • Wire support form skeleton + sys audit cleanup
  • Add 346 missing Slovak translations for support, fix form overflow CSS, improve skeleton
  • Support ticket system with comments, filters, and management\n\n- Add migration 031: ticket lifecycle columns (status, priority, category,\n assigned_to, resolved_at, updated_at) + support_comments table\n- Add SupportComment model with author, content, is_internal fields\n- Add ticket management API: PUT ticket, GET/POST comments\n- Add filtering by status/priority/category/search in ticket list\n- Rewrite support_tickets.html: filter bar, expandable detail rows,\n inline status/priority/category management, comments section\n- Replace all Unicode arrows with SVG icon components\n- Replace emojis with icon() macro calls throughout\n- Rename FEATURE_FEEDBACK_FORMS to FEATURE_SUPPORT_FORMS in docs\n- Update architecture doc: feedback.css -> support.css\n- Add btn_link_icon for back navigation and pagination controls\n- All linters pass (ruff, mypy), 2382 tests pass"
  • GitHub-style support system redesign
  • SSE real-time updates, email notifications, GitHub-identical UI
  • Redesign support filters with animated selects + category-specific form fields
  • Add alternating row backgrounds for visual consistency
  • Ticket detail - component-based layout, assignee user search
  • Dashboard integration - attention items, activity feed, sidebar badges, error page links
  • Pass SMTP env vars through docker-compose to containers
  • Component-based Subgroups & Projects table + Groups child rows
  • Auto-enrich user profiles from GitLab after config import
  • Persistent feedback links with CRUD, form picker, expiry, and link management
  • Move Feedback Links to Feedback Forms page + gl-dropdown form picker + context default forms
  • Add expand/collapse arrows to groups table with member preview
  • Expandable tree view for team projects + UI fixes
  • Arena email API fallback + filter bots from instructor lists
  • Security hardening - secret rotation, per-user tokens, retired heuristic detectors, CSRF UA binding

Fixed

  • Add PNG favicon for Google Search + shorten meta description
  • Redirect /favicon.ico to PNG instead of SVG
  • Slovak translations (podpora), mobile responsive improvements, email cleanup
  • Login page mobile - keep desktop look (background, centered card)
  • Ruff format + remove deploy job from CI pipeline
  • Login language switcher 404 - use /dashboard/login path
  • Login mobile - keep card contained like Keycloak (remove max-width:100%)
  • Login mobile - full-screen card like Keycloak (edge-to-edge white, no dark bg)
  • Login mobile - center content, visible lang selector on white bg
  • SW CORS error on OAuth redirect - skip navigate requests
  • Remove /dashboard/ from SW pre-cache - redirects to OAuth when unauthenticated
  • Remove SSR entrance guard - caused blank page on slow connections
  • Prevent skeleton flash on fast HTMX loads
  • Mobile card styling + PTR indicator hidden by default
  • Rewrite PTR to match GitLab blue circle design
  • Rewrite PTR and mobile card styling (manual)
  • Clean PTR rewrite - fix cancelable touchmove + Material spinner
  • Instant mobile logout + suppress native PTR on login page
  • Regenerate OG image with proper GitPulse branding
  • Clean OG image rewrite - minimalist pixel-perfect layout
  • Render login page for search-engine crawlers instead of OAuth redirect
  • Detect Lighthouse/PageSpeed bot in crawler regex
  • Ruff lint + accessibility (main landmark, color contrast)
  • Allow HEAD method on SEO routes (robots.txt, sitemap.xml, verification)
  • Restructure robots.txt - unblock /dashboard/login for Google
  • Simplify robots.txt - remove dashboard disallow entirely
  • Support HEAD on /dashboard/login + detect Google-InspectionTool
  • CI mypy errors, remove sidebar badge, fix login lang auto-login
  • Wire comprehensive email enrichment with GitLab API + Arena fallback
  • Support form double toast, markdown preview, asterisk notice, email notification
  • Proper double-toast fix (data-no-boundary), XSS prevention in markdown, hide info-card on success
  • Rebuild JS bundle, fix mypy dict type in gitlab_group_import
  • Remove temp SMTP scripts causing ruff lint failures
  • Add Arena API token auth + Arena fallback in student enrichment
  • Improve student enrichment with username search fallback
  • Replace anonymous message with logged-in user info on support form
  • Rebuild JS bundle and use proper 401 SVG from GitLab design
  • Rewrite renderMarkdown() in support-form & ticket-detail
  • Ticket comments 500, emails, comment permissions
  • Duplicate comments, missing icons, email avatar, notifications, responsiveness
  • Admin-system crash, duplicate emails, 401 UA mismatch
  • SSE real-time timeline update, touch updated_at on comment, skeleton loaders
  • Perfect 1:1 skeleton loaders for support pages, update email v4 with arena/SMTP/SSE changes
  • Restrict textarea.form-input to vertical resize only
  • Render info_card HTML correctly in support form (use call block)
  • Complete feedback->support rename, fix .env config bug and data.feedback attribute bug
  • CI lint failures + SSE security + dashboard auth guards + docs
  • Support page layout to match standard dashboard structure
  • Remove sidebar duplication, drop support/manage page, fix filter styling
  • Full-width form, move info card, expand category-specific fields
  • Improve support form - reorder notices, add padding, expand category fields
  • Dropdown z-index/transparency bug - replace opacity transition with class-based enter/leave
  • Add id=description to textarea and for=description to label (a11y)
  • Correct RateLimitDep usage and add empty-string UUID validator for SupportCreate
  • Tickets table matching admin pattern + dropdown transparency
  • Rewrite support tickets page with proper table/filter components
  • Cast UUID to string for tojson serialization
  • Improve toolbar dropdown hover with button-like bg highlight
  • Move filters to card header with search (User Management pattern)
  • Use subtle rgba hover for data-table rows across all pages
  • Ticket detail - proper two-column layout, fix sidebar overflow, self-contained assignee dropdown
  • Alpine expression error, form corners, sidebar heading colors, assignee reload, responsive
  • RenderMarkdown via vanilla JS init instead of Alpine expressions - prevents quote escaping errors
  • Correct SSE endpoint URL - remove stale /sse/ prefix
  • Enable assignee unassign + form accessibility (id/label)
  • Change timeline labels to spans (no associated form field)
  • Deep-link URLs, email notifications, assigned-by info, 403 page
  • Harden teacher membership sync - upgrade student memberships, elevate User.role, enrich avatars
  • Resolve ruff lint/format failures in teacher membership sync
  • Course Teachers count includes admins, filter-pill descender clipping, config preview spacing
  • Create User records for teacher_usernames during config import
  • Ruff lint errors in reset_prod_db.py, tighter config preview spacing
  • Section count toast mismatch + dashboard config preview spacing
  • Add error handlers for unexpected scenarios + fix mismatches
  • Active column width, unify config-import component
  • Remove stale _fullConfig ref, add comprehensive config validations
  • Pass disabledFeatures to config-import in course-settings
  • Prevent SSE task cancellation after import commit + add keepalive pings
  • Filter bot users from teacher discovery + clear config on import success
  • Fast config import, modal close, admin-teacher filter
  • Correct cell indices in course-detail.js (group names overwritten by member count)
  • Student privacy - hide metrics, restrict profile access, fix tree layout
  • Student profiles, LLM error display, table layout, global job bar coverage
  • Clean LLM error display, role-appropriate suggestions, proper icon
  • Error card dynamic injection + CI test fix
  • Skip job history for zero-work scheduled jobs + fix mypy
  • Groups child-row dashes, member name overflow, LLM error card bugs
  • Restore Groups Status column + LLM error card delayed swap
  • Build LLM error card client-side, show project last_synced_at, fix worker MissingGreenlet
  • Per-project contributors, inline SVG icons, toast-delayed error card, dismiss empty state
  • Error card delay 6s, per-project avg score, remove Active badge, single-MR retry
  • Real per-project avg score, error card without toast, better error classification
  • Role-based error messages + classify MissingGreenlet as transient
  • MissingGreenlet root cause + role-based messages + MR links in analysis
  • LLM error classifier order, global progress bar deferral, per-project status badges
  • Replace en dash with hyphen in llm_job comment (ruff RUF003)
  • Use forceCloseModal after successful import to avoid spurious Discard confirm
  • Lint errors (ruff/mypy) + UI consistency for feedback links
  • Cast UUID to text in feedback-links join to fix 503 error
  • Add for attribute to form picker label (a11y)
  • Fix Form picker dropdown not opening
  • Align tokens and modals 1:1 with GitLab Pajamas
  • Weekly deadlines table uses full width, no wasted space
  • Align table th, toggle token, pagination padding to GitLab Pajamas
  • Pixel-perfect GitLab Pajamas 1:1 alignment (round 5)
  • Align backgrounds and borders with GitLab dark theme
  • Remove max-width cap on main-container - content fills full page width
  • Pixel-perfect GitLab Pajamas component alignment
  • Align status dots, pagination radius, table padding, card/modal footer to GitLab
  • Project links to internal pages, arena JSON API with identities
  • Even column distribution across all tables (colgroups + table-layout:fixed)
  • Ruff lint/format errors in arena_email (UP037, E501, N806)
  • Show empty-state message when filter pills yield 0 results
  • Hide pagination when filter yields 0 rows, clean empty-state styling
  • Add !important to .d-none utility to override .pagination-controls display:flex
  • Prevent scrollbar flash on table pages by deferring page-initialised with double-rAF and hiding pagination-controls pre-init
  • Suppress row animation and lock container height during filter/sort to prevent scrollbar flash
  • Add missing column width classes for compliance checks table and fix weekly deadlines layout
  • Improve weekly deadlines table layout and prevent modal scrollbar flash
  • Unify scrollbar styling across modals, drawers, tables to match main content
  • Switch checksTable to auto layout on mobile to prevent column overflow
  • Hide Active/Code columns on small mobile screens, constrain Actions column width
  • Wire per-user token auth, fix ruff format, enable event pipeline
  • Resolve ruff lint/format failures, rename remaining instructor refs in i18n, fixtures and docs
  • Move Resurrect button right, fix duplicate toasts, let pagination stretch
  • Move Resurrect Selected to card header top-right, footer only pagination
  • Move pagination inside card body, hide in empty state
  • Add table-empty-row class to all empty/loading/error rows, hide pagination in empty state
  • Use table-empty-state with icon for empty DLQ, matching courses/users pages
  • Map event.replay/resurrect/processed/failed in activity feed with icons, labels, details, URLs
  • Align Configuration Preview icon, expose currentUsername to Alpine, fix modal close animation
  • Configuration Preview spacing, add fg855ni to ZSI config, fix DLQ Type column wrapping
  • Sync teacher_usernames from config import to CourseMembership

Changed

  • Unified premium animation system - staggered entrance, no conflicts
  • Zero-lag PTR - all passive listeners + rAF batching
  • Audit fixes - deduplicate afterSwap handler, fix deploy script PS5.1 compat
  • Comprehensive best-practice audit
  • Audit: full system refactor - security, a11y, i18n, dedup, dead code
  • Deep rename feedback->support across entire codebase
  • Redesign: pixel-perfect GitHub Support UI with markdown editor, thread view, expanded categories
  • GitHub-identical support UI - reply on top, reverse chronological comments, remove thread connectors
  • Unify table styles and replace inline styles with CSS classes
  • Ticket detail page - use page_header, remove custom action bar
  • Simplify forms, fix dropdown, sortable table
  • Rename Feedback subsystem to Support/Ticketing system
  • Revert: undo UI changes
  • Revert: restore to d03fd4f (align tokens and modals 1:1 with GitLab Pajamas)
  • Unify role system (instructor->teacher), drop instructor_usernames column, R13 push enqueue
  • Eliminate all 'instructor' terminology - rename to 'teacher' everywhere

UI / UX

  • Align UI system to GitLab Pajamas design tokens
  • Align remaining components to GitLab Pajamas
  • Revert: restore main-container max-width layout
  • Pixel-perfect GitLab Pajamas alignment - focus ring 1px/3px, form padding 8px/12px, line-height 1rem, triple-ring focus, card radius 8px, modal radius 12px, table cell 12px/16px

Infrastructure

  • Remove test support script
  • Remove temp verify_fix script
  • Remove junk files and harden .gitignore
  • Remove debug scripts
  • Remove utility scripts with hardcoded credentials

Documentation

  • Comprehensive update - R13, 30 migrations, 2382 tests, CourseMembership

Tests

  • Add unit tests for email notifications, api utils, constants, and type modules

[2026-03-23 - 2026-03-29]

Added

  • Clickable member rows + context-aware back navigation
  • Group members table navigates to project context
  • Role separation - force logout on change, hide docs from students, fix attention items
  • Suggest full sync after course settings save
  • Redesign instructors page (GitLab-style invite bar + member rows)
  • Real GitLab avatars + wider layout
  • Clickable instructor rows with User Info modal, Owner badge
  • GitLab-style Source & Role columns in member tables
  • GitLab reference linkification + reduce member-api-note spacing
  • Role-based sidebar + thesis feedback card
  • Add R13 Commit Quality check - web editor detection
  • Simplify: remove intervention list, risk groups, all groups; disable event pipeline/roster; add instructors lazy loading
  • Redesign dashboard cards, header, timeline grid, profile menu
  • Quick Access sidebar, health for teacher, scheduler feature-flag gating
  • Single-user enrich button in User Info modal
  • Comprehensive UX improvements from teacher review
  • Add 295 missing Slovak translations
  • Overhaul global search - students scope, role badges, direct links, docs
  • Drastically reduce GitLab API polling - webhook-first approach
  • Extend activity-window filter to ALL remaining GitLab polling jobs
  • Webhook edge-case hardening - orphan projects, backpressure, immediate enrichment, delivery health
  • System-wide resilience - sweep retry, pagination limit, Prometheus enrichment metrics
  • Gate GitLab-polling sweep jobs through the scheduled-jobs master switch

Fixed

  • Member profile - dynamic role title, no-activity guard, table scroll
  • Member profile UX - back nav to group, instructor-aware empty states
  • Prevent infinite login loop when allowlist user changes role
  • Dashboard: fix status dots, reporter role warning, pipeline health overflow
  • Fix CI lint + Groups 0: match Student by gitlab_user_id
  • Fix OAuth: GitLabUser.id not gitlab_user_id
  • Elevate dashboard role for course owners/instructors, remove redundant stat tooltip
  • Rewrite modal CSS to match GitLab Pajamas exactly
  • Silence editCheck console warning and aria-hidden focus conflict
  • Quick Access shows real teams, System health enriched
  • Quick Access Groups/Subgroups tabs, interactive timeline weeks, skeleton blue bar fix
  • Lint errors (ruff/mypy), timeline semester_weeks order, project name display
  • Sidebar layout, project names, timeline nav, telemetry gating
  • Round avatars, attention link to course, instructor layout
  • Avatar backgrounds hardcoded, groups sort by RED, instructor panels stretch
  • Revert email, muted avatar colors 24px, restore email docs
  • Language switch revert, new analytics favicon, DTA refs cleanup
  • Cache-bust extra.js/css/favicon, reduce nginx cache, remove dead locale persistence
  • Fix language revert, favicon flash, and canonical URLs
  • Restore missing