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 inloaders.cssnow forces neutral--surface-strong+--border-colorfor 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-titlewas a plain<h3>so leading icons sat at text baseline. Switched todisplay: inline-flex; align-items: centerto 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
studentbranch (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_suggestionsand the GitLab button visibility; student detail honors the rubric flag and the own-profile gate; analytics honorsfeature_intervention_flags; dashboard honors the role split and changelog panel toggle; team detail honors student-privacy mode; course detail / teachers honorcan_editandcan_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.pyis back inruff formatcompliance; 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, andpip-auditremain 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 withimmutable, 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.pynow 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-auditadvisories, includingcryptography,python-multipart,Pillow,Pygments,requestsandurllib3. - 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_barsgot a<360pxbreakpoint 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_analyticsskeleton was visually too tall on phones (240 px chart band + 140 px doughnut + 3 × 1.9em rows). Added<768pxand<480pxrules: 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 awifi-officon 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". _performCheckshort-circuits when offline (no fetch, no escalation todown).window 'online'listener clears the dismissed flag, hides the offline banner, and triggers an immediate re-check + "Connection restored" toast.- Added
wifi-officon 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_charttosk_h_bars(count=4)(real chart is also horizontal). sk_chartrewritten asgl-skeleton-chart-wrapwith 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_barsmacro renders a horizontal-bar placeholder used for the Teacher's Checklist card (13 R01–R13 rows of varying fill). - New
sk_legendmacro renders the chart-category legend dots above the Teacher's Checklist. - New
sk_week_pillsmacro renders the Week Navigator pill row.
- New
- 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 inloaders.cssfrom 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; attributesdata-skeleton-progressive,data-skeleton-table,data-skeleton-total-items,data-skeleton-initial-items,data-skeleton-total-rows,data-skeleton-initial-rows,data-skeleton-delay; theinitial,initial_lines,initial_rows,initial_bars,deferredkwargs on skeleton macros; the JS_initProgressiveSkeletonscontroller and itsGP.initProgressiveSkeletons/GP.initProgressiveSkeletonTablesexports; the@keyframes glSkeletonItemRevealrule and the.gl-skeleton-table.is-progressive-expanded .gl-skeleton-row--deferredreduced-motion selector. - Service-worker cache version is now
gitpulse-v29and 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_spinnermacro 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 avprefix. Thev...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 existingv2026.5.17tag is left intact, so this release uses.post1instead of rewriting tag history. - The dashboard shell no longer emits an inline service-worker registration script. Registration is handled by the shared
core.jsbundle. - Admin Events warning toasts are now driven by
gp-page-configJSON and rendered byadmin-events.js, removing the page's state-specific inline scripts. - The DLQ loading spinner uses the shared
.gl-spinner-inlineCSS 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 beforecourse.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_utchelper 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 oftoday.year - 2) instead of the fixed2020-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.versionfrompyproject.tomlwhen installed package metadata is unavailable, so/healthand metrics no longer reportversion="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 inlineonclickhandlers. - 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
1875commits, 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.txtis 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/loginGET 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_totaland 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 | |
|---|---|
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.ymldefaults flipped:PERSISTENT_SESSIONS:-true,SESSION_MAX_AGE_SECONDS:-2592000(30 days), and newSESSION_ABSOLUTE_MAX_AGE_SECONDS:-7776000(90 days).redeploy.pynow injects all three values into.env.productionon every deploy (idempotentsed/echopattern, same asGIT_SHA). Auditable, drift-resistant..envand.env.productionupdated to the new 30 d / 90 d values as the next-deploy baseline.
Removed (dead code)¶
session_auth.verify_password()andsession_auth.hash_password()- production runs exclusively on GitLab OAuth; these helpers had no callers outside the import smoke test. Removed along with the now-unusedbcryptimport insession_auth.py(thebcryptpackage is still used by webhook authentication, so the dependency stays inpyproject.toml).- Corresponding lines in
tests/unit/test_module_imports.pyremoved.
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_sessionsdefault flipped toTrue- 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_secondsinapp/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) -storageevent listener inlocale-sync.jsupdates thegp_langcookie 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-collapsedlocalStoragekey + newstoragelistener. - 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')pluslocalStoragestorageevent 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-visibleis on-screen. An 800 ms re-poll loop reshows the card the moment the overlay disappears. - Page-synced lifecycle -
visibilitychangepauses 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:closeto re-attempt show immediately.tour-engine.dismissWelcomeCard()now dispatchesgp:tour:welcome:dismissedafter its 320 ms exit completes. - UX polish - Escape closes without persisting;
BroadcastChannelcleanly closed onpagehide.
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.jsrewritten in modern ES6+:const/let, arrow functions,async/await, optional chaining, nullish coalescing.core.jsAlpine$mdShowPreviewmagic - replaced 6vardeclarations (stack, i, node, refEl, rendered, rm) withconst/let.
Fixed¶
- Hide-timer race:
_hide()'ssetTimeoutwas not cancellable on re-show. Introduced module-level_hideTimerref so the next show/hide cancels first. - Deprecated
userChoiceAPI: rewrote_triggerInstallwithresult?.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) - capturesbeforeinstallprompt, 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_untilinlocalStorage). - Reduced-motion respected (
prefers-reduced-motion: reducecollapses 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
sessionStoragekeygp_job_completion_dedup_v1to 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
summaryselector: the click handler used the baresummaryCSS selector which matched every<summary>element on the page, including collapsible panels in forms (e.g.code-referencesections in Course Settings). Selector narrowed todetails.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=coveractive the viewport extends behind the system nav bar, butenv(safe-area-inset-bottom)is 0 for Android 3-button navigation. Two-phase fix: the bottom-sheet transform now subtractsenv(safe-area-inset-bottom, 0px); browsers supportingdvhget a@supports (height: 100dvh)block with100dvhthat 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 whenenv()returns 0. Addedmin-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(with70vhfallback) 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 viatuple_(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 withoutauthor_id;aliased(StudentModel)/aliased(TeamMember)for two outerjoin paths on Issues.src/app/templates/partials/student/_merge_requests.html: attribution row section withnot _student_commits/_other_commitsconditions.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_branchor manual deletion), the affected MR row shows a grey badge with a tooltip explaining commit recovery. Requires new DB columnmerge_requests.source_branch_deleted(migration044). - Primary branch-deletion detection (
mr_processor.py): GitLab returnsforce_remove_source_branch=truein both REST API responses and webhook payloads - the value is now stored insource_branch_deletedon 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 ismerged, the system automatically markssource_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 callsGET /projects/:id/merge_requests/:iid/commitsfor 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 matchingauthor_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: listannotation on_commit_clausescaused atype-argerror 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 anauthor_email = student.emailOR clause - commits with no resolvedauthor_idstill appear if the email matches. - GitLab noreply email
<digits>+prefix stripping (commit_processor.py): GitLab sometimes generates noreply addresses like123456+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_v2storage prefix): a one-time, clean reset of the stale:completedkeys that v1 wrote on every tour finish and that subsequently suppressed the welcome card across all stages of the multi-stage/dashboard/supportpage (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
storageevent listener watches writes to thegp_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-originto0 0, so the icon was "rotating" around its top-left corner..spinnow usestransform-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") andStatus("Active") badges were getting ellipsis-truncated becausetd { overflow: hidden; text-overflow: ellipsis }applied to the whole table. Truncation is now scoped to text-only columns;Role/Status/Actionsusewhite-space: nowrap; overflow: visible. Widths rebalanced:Actions8 % -> 12 %,Email22 % -> 18 %. - Admin / Users + DLQ tables - responsiveness: below 900 px (Users) and 1024 px (DLQ) the tables fall back to horizontal scrolling inside their
.table-responsivewrapper 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_TTLwas bumped from 600 s to 900 s, and thegp_returningcookie attributes were corrected so the browser reliably carries the session-id to the destination page. select_fieldmacro: literalu2014no longer leaks into option labels. Thetojson_attrfilter now serializes JSON withensure_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/supportstage (course picker -> category -> form) now gets its own welcome card. Previously, completing one stage also wrote a "global":completedkey 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
\u2014displaying 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
skippedclassifications. 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_botcolumn + partial index (Alembic042_user_is_bot): aBOOLEAN NOT NULL DEFAULT FALSEcolumn onuserswith partial indexix_users_is_bot_false WHERE is_bot=false. Backfill matches the same patterns asis_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_botset at every User-creation site: all 9 places where the app inserts aUser/UserModelrow (group import, OAuth first login, dev-mode auto-create, admin manual sync, student create, projects/teams auto-create from inherited members) now callis_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
botsfield 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
Infobutton is rendered, opening the same User Info modal with aService Accountbadge 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_userlast-admin guard, and the support assignee dropdown - all of them now ignoreis_bot=truerows.
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 (anilike-OR tree). It now exists only as a back-compat alias; new code should usehumans_only_clause()/bots_only_clause()fromapp.services.user_filters. sk_admin_usersskeleton 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): addedService 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-sidebaron a mobile viewport (≤ 768 px), the engine addsmobile-opento the sidebar andvisibleto the overlay before measuring the target rect. The drawer closes itself inonDeselected(andonDestroyed) so subsequent body-targeted steps are not occluded. Without this fix, sidebar items kept a positive bounding rect even attranslateX(-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/activereturned aper_coursemap 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 inchangelog.md(SK) andchangelog.en.md(EN). The dashboard "Latest changes" widget mirrors the same order via_CHANGELOG_ENTRIESinsrc/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 pointwindow.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 fromlocation.pathname, state persistence inlocalStorageundergp_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 whoserolesarray 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-footerusedbackground: var(--card-bg, var(--surface, #fff))but neither token is defined in the app (only--surface-default,--surface-strong). The fallback#fffrendered a white sticky footer in the dark theme. Footer now uses--surface-strongand bleeds out through the--gl-spacing-5modal padding so it aligns flush with the modal frame.rcm-groupandrcm-itemrewired 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.jsandtour-content.jsadded toJS_MODULESinscripts/build_bundle.py;tour.cssadded to thestatic/styles.cssmanifest - minified.minsiblings 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_pathfield is no longer manually edited - the system computes the longest common prefix from existing teams'gitlab_group_pathvalues 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
WebhookConfigurationrows are now flipped tois_active=Falseso 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-valueattributes; 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: 8remand 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-spacingand any inherited dotted underline styling from.secret-reveal-input/.webhook-inputso 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-infoparagraph with thealert_blockmacro for consistent component styling. - Webhook config modal not opening: Inner
.modaldiv carriedclass='d-none'which overrodeopenModal()'s.activeclass on the backdrop. Backdrop appeared but content stayed hidden. Removedd-nonefrom all 3 webhook modals (webhookConfigEditModal,webhookSecretRevealModal,webhookConfigRevokeModal). - "Heads up" notice restyled to match other in-modal
.alert.alert-infoblocks (single-line callout, no bold lead-in).
Changed¶
- Removed legacy
api/group_webhooks.py,schemas/group_webhook.py,services/group_webhook.pyandtests/unit/test_group_webhook_service.py(superseded bywebhook_configurations). The legacy course-level secret stamping helper was inlined intoapi/webhooks.py;ROTATION_GRACEnow lives inservices/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.pyto module level. Removed dead paths, fixed PERF401/PERF403 list/dict comprehensions, and best-effort swallow blocks (S110) now carry explicitnoqacomments. No behavior change -ruff+mypy 1.11remain green. - Course analytics page - fast-path:
EvaluationReportServicegained_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.htmlandforms.html(toggle, bare checkbox, file-upload) wrapping<label>elements gained explicitfor=attributes; checkboxes inside the run-checks grid received generatedids. Both implicit wrapping and explicitfornow 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-toggleelement used in student detail breakdowns. - The Raw Check Details (JSON) view is now styled as a standard collapsible section with monospace
preformatting to seamlessly match the remainder of the dashboard. - Removed overlapping border notch on
pagination-controlscomponent placed inside specific innerdetailssections.
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_THRESHOLDduration 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 usingnth-childtargeting, 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: Inlineonloadtriggers were refactored, andnoncevariables 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_secondsonly to the Redis-backed session store. Stateless cookies (fallback when Redis is unavailable) still relied on the slidingmax_agealone - meaning an active user (or a stolen cookie) could renew indefinitely.get_current_session()now validates thets(created-at) claim againstsession_absolute_max_age(default 24 h) on every session load, regardless of storage backend. Malformedtsvalues 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.envfile accidentally leaked to production (or a staging server was used as prod), rate limiting would silently degrade to no-op. Unified toHTTP 503 + Retry-After: 30everywhere. - UA fingerprint always stored - even when User-Agent is empty (AUTH-08): previously, a missing or empty
User-Agentheader resulted inua=""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_allpreviously 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 settingsweep_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 atWARNINGlevel when hit.
Fixed¶
- Dead code removed in
sweep_stale_projects: duplicatereturn 0statement on line 821 (unreachable code after the firstreturn) 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 emptybranch_patternno longer falls through (the check is just skipped). - R04 (MR linked to issue) - branch-name fallback + tighter bare-
#Nregex. Bothenrich_mr.pyandmr_processor.pynow extractlinked_issue_iidfrom the branch name (issue-7-foo,7-add-login,feature/issue-12) when the MR description has noCloses #N. Combined with the text parser - explicitCloses #Nwins, branch-IID is the fallback. The bare-reference pattern#Nis tightened to be word-boundary anchored at both ends ((?<![\w-])#(\d+)(?!\d)), sov#10and#10xno longer false-match. TheMergeRequest.linked_issue_idFK is then resolved viagitlab_iidlookup -> 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,## Whyadd up to ~9 tokens without describing anything). The_word_counthelper 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\wcharacter. 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 runsis_meaningful_comment(joined, min_words=cfg.min_word_count). Newcfg.require_meaningfulflag (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_words30 -> 15 (modelcourse_settings.min_review_word_count, helperis_meaningful_comment, migration039). 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 (wheremin_review_word_countwas 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. Nowtotal_threads = inline_total + standalone_reviewer_notesandanswered = inline_answered + min(standalone_author_notes, standalone_reviewer_notes). Default forcfg.require_commit_refflipped True -> False (ZSI recommends commit-ref-citing but does not mandate it),cfg.pass_if_no_threadsflipped False -> True (you can't respond to feedback that doesn't exist yet). When commit-ref is not required, partial scoring is the plainresponse_rateinstead of the 50/50 weighting. - R10 (Merged by author) - also accepts a teammate merger. Previously
allow_self_merge=Truemeant "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) andforbid_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 buildsissues_by_iid = {i.gitlab_iid: i for i in ctx.issues_assigned}, looks upmr.linked_issue_iidfor each merged MR, and only passes when the paired issue isclosed. Fallback: when no merged MR has alinked_issue_iid(student forgot to writeCloses #Nand 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 inpipeline_processor.pynow scans the tuple("test", "spec", "pytest", "junit", "jest", "mocha", "rspec", "phpunit", "verify", "coverage")against bothnameandstage, covering typical cross-language naming conventions.
- R02 (Branch naming) - failure detail also explains the protected-branch case (previously only "doesn't follow naming convention"). New text: "Branch '
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 stampscreated_atoncreate()and rejects sessions older than the ceiling onload()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
exceptcalled rollback - but the commit was already persisted, soevent.processed=Trueremained and the side-effects were silently lost. Each side-effect now goes through_safe(coro, name)which catches exceptions, increments the Prometheus counterwebhook_processed_total{status="sideeffect_failed:<name>"}and logsWARNINGinstead of bubbling. - Redis now requires a password even on the internal Docker network (SEC-03/SEC-04): previously Redis listened without authentication and
REDIS_URLcould beredis://redis:6379/0with no password. An attacker with access to the Docker bridge (or a misconfigured port mapping) could read session blobs and queues. Theapp.configvalidator now refuses to start inAPP_ENV=productionwhenREDIS_URLlacks a password segment (redis://:PASSWORD@...). The Compose template enables--requirepasswheneverREDIS_PASSWORDis set. Production now runs with a 40-char generated password, all 4 workers + scheduler + api reconnected. FEATURE_RBAC=falsein production hard-fails (AUTH-04): previously_check_required_rolesonly logged a warning and let the request through without role checks. A misconfigured flag would silently open every teacher-only endpoint. The validator now raisesValueErrorat startup so the deploy fails fast instead of fail-open.DEBUG=truein 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_limitswallowed 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 returnsHTTP 503withRetry-After: 30and anrate_limit_unavailable_blockinglog line. Dev and tests stay permissive. SESSION_STRICT_UA_FINGERPRINTleft 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 staysfalse. The UA hash is not stable across mobile <-> desktop switches or browser auto-updates, and atruedefault 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 inCaddyfilewould 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, andcaddyuse thex-default-loggingYAML anchor for default logging. Previously/var/lib/docker/containers/*/*.logcould grow to tens of GB with verbose workers and exhaust the host disk. - Scheduler healthcheck via
/proc/1/cmdline: the originalpgrep -ffailed because the slim worker image does not ship procps. We now usetr '\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: truemeant 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_asyncpreviously ran a singleDELETE WHERE timestamp < cutoffstatement. With 1 M+ rows that meant a long transaction that blocked replication slots and stalled vacuum. The new_delete_in_batcheshelper 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 explicitAPP_ENV, so the new production validators causedSettings()to raiseValueErrorin 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:
_computeEtaonly fired on a new progress sample, so between batches the 1 Hz timer just incremented the elapsed clock while thetime-leftlabel kept the last computed value. ETA is now anchored to a wall-clockexpectedFinishAt; 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.jswas closing the dashboard SSE onvisibilitychange->hidden. Server pushes formember_sync_completetherefore never reached the browser while the tab was minimized -_queuedToastshad 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 inpages/course-analytics.js, which was loaded via<script defer>inside the HTMX-swapped partial. HTMX clones script tags with thedeferattribute, but dynamically-inserted scripts ignoredefersemantics - they execute async after a fetch. Alpine meanwhile walks the new subtree and tries to evaluatereportFormController()before the script has loaded -> cascadestartDate / endDate / durationText / dateError is not defined. The controller is now inlined as anx-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_stafftemplate (subject[GitPulse] New ticket #{id} ({course_code}) - {title}) is now sent to every activeowner+teacherof the course (excluding the submitter) from both branches ofsubmit_support_ticket(dynamic-form and fixed-column). Protected by an idempotency keyticket_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_commentwas e-mailing the submitter even whenis_internal=true(a staff-only private note - e.g. "this student is lying, ignore"). Added an explicitnot payload.is_internalguard 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
errorevent 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 incore.js: save the originalsrcon first error; when!navigator.onLine, defer the fallback decision (markdata-avatar-pending="1") instead of showing the letter; a new retry-all listener ononline+pageshow(BFCache) +visibilitychange->visiblewith 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.jsran on the capture phase and calledpreventDefault() + fetch(), while page-level handlers on the bubble phase ALSO calledpreventDefault() + fetch()on the same form. Result: two HTTP POSTs per click. Affected: dismiss LLM error (project detail), login, the Alpine@submitcourse-teacher assignment form. Fix: extended skip-rules - the global handler now leaves alone forms withdata-submit-action,@submit/@submit.prevent(Alpine),data-no-boundary="1", or HTMX attributes.login.htmlwas markeddata-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_queuewas never invoked -> rows inemail_logstayedpendingforever. Afterredeploy.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:
basicauthdirective renamed tobasic_authfor/grafana,/prometheus,/jaeger(×3); removed the redundantheader_up X-Forwarded-Proto {scheme}(×2 - Caddy's reverse_proxy passes it automatically). After rebuild no deprecation / style warnings remain incaddylogs; 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.htmlpreviously displayed only with#/!/@references linkified, with the rest as raw text. They now go through therender_gl_descriptionfilter, 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 samerender_gl_descriptionfilter 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)inglobal-job-bar.jsnow 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 onwindow._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-scanjob - no more spurious "Job failed": the secondpip-auditrun (against the full installed environment, not onlyrequirements-lock.txt) was flagging vulnerabilities in CI tooling -pipitself (CVE-2026-3219, no fix released yet) and transitive deps ofcyclonedx-bom/pip-audit- which are not part of our application dependency closure. Added|| trueto the informational scan plus a comment; therequirements-lock.txtaudit (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)
_jobMaxAgefor 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 "passivedeferGlobalJobmust not overwrite live message" fix early-returned without callingshowGlobalJob, so_autoStartPollingwas never invoked ->lastUpdatestopped 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 aQUEUEDbadge tocells[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-allresponds in ~100 ms (previously ~20 s): team discovery (/projects/:id/userscalls) 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: unifieddatetimeimports, 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: avoidon<h2>andbreak-inside: avoidon 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-levelbreak-inside: avoidis kept, so individual table rows are never sliced in half (src/app/workers/report_job.py) - Pilot Evaluation - realistic counts: instead of a raw
MetricSnapshotrow count (which grew on every webhook), we now count distinct(team_id, student_id, week_bucket)tuples joined throughTeamMemberwithis_active+excluded_from_scoring=False- teachers, peer mentors and excluded members are no longer included. Dropped theorfallback that silently leaked the global count whenrpt_course_countwas zero. Weeks-covered now falls back tolen(compliance_trends.weeks)so the KPI no longer reads0when the trend shows 5 weeks. Click multiplier reduced to2×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 usesize='sm'- they now line up withLink Project / Sync / Run Checks / Export CSV / Export JSON / Generate Report / Deleteon the group page. Visual consistency across Course -> Group -> Project - @mentions in MR descriptions on the student page render as names: the
linkify_gl_refsfilter now accepts an optionalusername_map(gitlab_username -> full_name) and substitutes the readable name for any mention matched against a team member (e.g.Reviewers: Andrii Betsainstead ofReviewers: @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..Ncaptions, 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_archiveopt-out (migration036): new boolean oncourse_settingsshields manually reactivated courses from being re-archived on the next sweep run.course_service.activate_course()sets it toTrueautomatically, 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 bothsemester_start_date/semester_end_datewith one click - teachers no longer have to open Settings after creating a course for auto-archiving to kick in.POST /api/v1/courses/accepts optionalsemester_start_date/semester_end_datedirectly at creation time SemesterPresetnow carries asemesterfield (winter/summer) matchingCourse.semestervocabulary, removing the need to manually sync preset choice with the Winter/Summer selector- Full config export/import now carries
skip_auto_archiveas well -config-import.jsvalidates 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(queuelow, 24 h interval) flipscourses.is_active=Falsefor courses whosesemester_end_date + semester_grace_dayshas passed. Because all 23 existing periodic sweeps already filter onCourse.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 (migration035_semester_end_date.py) - New column
course_settings.semester_end_date(DATE NULL): explicit "last teaching / exam day". For courses without an explicitsemester_end_date, the helper derives it fromsemester_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+ endpointGET /dashboard/semester-presetsships 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, defaultTrue),semester_grace_days(default14days - buffer for grade finalization, late MRs, final report exports),semester_default_length_weeks(default14, fallback for courses withoutsemester_end_date) - Helper module
app.utils.semester: two pure functions -get_effective_semester_end(cs, default_length_weeks)andis_semester_over(cs, grace_days, default_length_weeks, today=None). Explicitsemester_end_datetakes precedence; fallback issemester_start_date + N weeks. Open-ended courses (neitherstartnorend) are never auto-archived - they can only be archived manually via the admin UI - Full config export/import cycle now also carries
semester_end_datein the/settings/full-configJSON (both export and import branches), so presets transfer cleanly between instances and cloned courses - 14 new unit tests in
tests/unit/test_semester.pycovering: 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_historyprimary path (whenStudentComplianceSnapshotrows existed) never returned the assembled data - thereturn filledwas missing - so control fell through to the MetricSnapshot fallback, which only knew the latestcomputed_at(W10). Fix: addedreturn filled(commitbd189ee8) - (b)
compliance.engine.backfill_compliance_historyfabricatedStudentComplianceSnapshotrows 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 fromMIN(Commit.committed_at)and skip earlier weeks (commit3e6fe1b1) - © 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 explicitselect(func.min(...))queries (commit52611892) - (d)
Result[Any].rowcountis absent from the SQLAlchemy async stubs -> CI mypyattr-definedfailure. Fix:getattr(deleted, "rowcount", 0) or 0(commitc289c5b1) - (e) hidden root cause: GitLab auto-creates an initial README commit at project-creation time with
committed_at ≈ project creation time, soMIN(Commit.committed_at)returned W1 for every team and the anti-fabrication logic from (b) was a no-op. Fix:first_activityis now derived fromMIN(MergeRequest.created_at)andMIN(Issue.created_at)(genuine intentional student actions); commits are only used as a fallback and only commits whoseauthor_idmatchesTeamMember.student_id(so the bootstrap/system commit is ignored) (commit56a9387f) - (f) post-deploy audit fix: even after the pre-activity
StudentComplianceSnapshotDELETE, the API still synthesized W1..(first_activity-1) asnullstub rows because offor wk in range(1, max_week+1)- Weekly Breakdown rendered them as empty em-dash rows. Fix: range starts atmin(populated_weeks), so chart/heatmap/table only show weeks with real data - no fabricated numbers, no misleading empty rows
- (a)
- 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==0while the server still reportshas_active_sync=true, the bar switches to afinishingstatus witht('Finishing...')instead of showing a misleadingly empty counter. Applied to both the SSE branch and the legacy poll fallback inglobal-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 int(...)with matching keys added tosk.json. Thescripts/find_missing_translations.pyaudit 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 viaextra_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
TeamMetricSnapshotinstead 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 scriptscripts/find_missing_translations.pyscans allt(...)calls across JS and templates and reports missing keys - added to the CI checklist - Typed exceptions for GitLab client: new
GitLabPaginationTruncatedErrorclass (subclass ofGitLabAPIError) - pagination truncation no longer returns partial data silently but raises, so per-projecttry/exceptmarks only that project as failed and the sweep retries it later
Fixed¶
- Mobile responsiveness - tables & profile card: (1) profile header
.profile-identitylackedmin-width:0and the "View on GitLab" link overflowed the card on narrow viewports - addedflex:1+min-width:0,overflow-wrap:anywhereandalign-self:flex-start+nowrapon 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 mobiletext-nowrapis overridden towhite-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-naswitched tovertical-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:
scrollIntoViewalso scrolled outer scroll containers (viewport) causing the fixed topbar to visibly shift. The scroll now targets.gl-page-contentexclusively and computesscrollTop + boundingRectoffset 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_historyignoredas_of_weekand always wrote the current week number toStudentComplianceSnapshot.week_number. (b)pilot_telemetry.capture_team_snapshotsupserted via ON CONFLICT DO UPDATE with a strippedraw_metricspayload (nochecks_detail), overwriting the 13-check breakdown the engine just persisted. Fix: engine now honoursas_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_projectswas sorted bylatest_computedand 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. Nowteam_id NOT INis pushed into SQLWHERE, 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)calledpill.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 useweekPills.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 ascompletedwith missing issues/MRs/pipelines, dashboard showed wrong metrics. Now raisesGitLabAPIError(429)withRetry-Afterin the message - GitLab pagination (10 000+ items) - silent data loss: when hitting
max_pages=100cap 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 raisesGitLabPaginationTruncatedErrorwith structured fields (path,max_pages,items_fetched) for Grafana alerting - Webhook rate-limit IP spoofing (DoS): orphan-webhook rate limiter read
X-Forwarded-Fordirectly from headers, bypassing theProxyHeadersMiddlewaretrust boundary - attacker could rotate spoofed IPs to bypass 10/min limit and floodorphan_webhook_events. Now usesrequest.client.hostwhich the middleware already validated againsttrusted_hosts - Webhook backpressure - silent check failure:
except: enqueue anywaysilently hid backpressure-module failures / Redis outages. Fallback preserved (best-effort enqueue), but now logslogger.warningwithexc_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 thebeforeunloadhandler - interval lived until GC after tab close or logout. NowclearIntervalalongside_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
SyncStatusBannercomponent and its pollers) - Sync freshness guard (up-to-date check):
POST /api/v1/courses/{id}/sync-allandPOST /api/v1/teams/{id}/syncnow skip teams whose last successful sync completed within the 5-minute freshness window - response carriesalready_fresh: N, single-team returnsstatus: "skipped_fresh"; pass?force=trueto 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/activeendpoint 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-statustransiently returnedhas_active_sync=falseduring a state transition; jobs now carry aserverConfirmedRunningflag, 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::beforerules scoped to opt-in.details-accordionclass; duplicate unicode ▶ chevron removed frompages/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