Skip to content

Changelog

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


[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 feature-flag aware. Project detail honors feature_rubric_engine, feature_docker_verification, feature_ci_failure_generator, feature_conflict_simulator, feature_llm_suggestions and the GitLab button visibility; student detail honors the rubric flag and the own-profile gate; analytics honors feature_intervention_flags; 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.
  • Rubric 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.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, Rubric 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 🚀 / 👋 / 💡 / 📍 / ⏱). 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 Expression Error: reportFormController is not defined: the 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 walks the new subtree and tries to evaluate reportFormController() before the script has loaded -> cascade startDate / endDate / durationText / dateError is not defined. The controller is now 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 / Generate 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, rubric 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, gaming 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
  • Add feature_auto_sync toggle to gate all GitLab-polling sweep jobs

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