ISC Learn
The Moodle platform of the ISC programme, at https://isc.hevs.ch/learn/. It runs on the hannibal Infomaniak VPS (the server), outside the rack on purpose: it has to survive a loss of room 23N307. A migration to managed hosting is proposed (Aug 2026).
This page is the current state of the service. Dated work lives once, in history & operations; open items in ops todo → ISC Learn.
At a glance (September 2026)
| Moodle | 5.2.2+ (build 20260903, since 2026-09-05). 5.2 is supported until 2027-04-05; the next step is 5.3 LTS, released 2026-10-05 |
| Stack | Ubuntu 24.04, Apache 2.4 + PHP-FPM 8.3, MySQL 8.4.6 (port 3366), Redis 7 — host details on hannibal |
| URL | https://isc.hevs.ch/learn (= $CFG->wwwroot). isc.hevs.ch (vsnet zone) and learn.isc-vs.ch are both CNAMEs to hannibal.isc-vs.ch → 185.143.103.222 |
| Authentication | shibboleth (SWITCH edu-ID through mod_shib; the SP key in /etc/shibboleth is what SWITCH knows isc.hevs.ch by) plus manual accounts. Site admin: admin_moodle_isc |
| Code | /srv/www/learn.isc-vs.ch/moodle_isc (~450 MB). Moodle 5.1+ layout: the web root is moodle_isc/public, config.php and the CLI scripts (admin/cli/cron.php, upgrade.php, cfg.php) stay at the tree root, plugins live under public/ |
| Data | moodle_data, ~130 GB logical after the 2026-09-05 purge (filedir 78 GB + course backups 49 GB, block-shared on btrfs: ~80 GB physical) |
| Database | moodle_isc, ~600 MB, 508 tables. Nightly automysqlbackup dump at 06:25 into db_backups/ — a restore never touches the live DB |
| Cron | admin/cli/cron.php every minute from the www-data crontab |
| Execution servers | not on the box: the VPL jail is vpl.isc-vs.ch → env-vpl.jcloud.ik-server.com, the CodeRunner Jobe server env-6007632.jcloud.ik-server.com:4000 — both on Infomaniak Jelastic Cloud |
| Outbound mail | straight to mail.infomaniak.com:465 as mailer@isc-vs.ch; settings in mdl_config, not config.php. Cannot use the rack relay |
| Backups & DR | DS923 nightly pull, versioned rumba copy, standing mirror VM 107 — disaster recovery |
| Scripts | provisioning/learn/ (Moodle side), provisioning/hannibal/ (host), provisioning/drp/ (mirror); ISC-HEI/moodle-isc-admin-scripts for the yearly course archive and the semester registrations |
Routine operations
| Task | How |
|---|---|
| Register the semester's students (September, February) | Registering students — first run 2026-09-09 |
| Yearly course archive (move-up, summer) | moodle-isc-admin-scripts |
| Plugin updates reported by the plugin manager | provisioning/learn/plugin-updates.sh: edit its list, dry-run, run on the mirror, then hannibal. It snapshots /srv, checks the moodle.org md5s, keeps the previous directories under /srv/plugin-updates/<date>/prev/ and runs upgrade.php. No maintenance mode needed at that size (last round) |
| Moodle upgrade | Rehearse on VM 107 first, then provisioning/learn/moodle-upgrade-cutover-2026-09-05.sh adapted: btrfs snapshot + dump, new tree built beside the live one, CLI maintenance, directory swap, upgrade.php --non-interactive, cache purge (5.2 run, 118 s of maintenance page) |
| After a New Learning theme update | re-run provisioning/learn/mb2nl-fxwidth-patch.sh; check the frontpage and a course page: the early-fetch scripts key on theme class names and web-service names |
| Refresh the DR mirror | provisioning/drp/learn-mirror-refresh.sh on rumba (runbook) |
Before any change to /srv | read-only btrfs snapshot: btrfs subvolume snapshot -r /srv /srv/.snapshots/<name> (it pins the freed space until deleted). Before a change with user impact, the Infomaniak data snapshot as well — 2 h 30 to create (change process) |
Plugins and theme
23 non-standard plugins: VPL, CodeRunner and its question behaviour, the New Learning theme
mb2nl with its local_mb2* companions and filter_mb2shortcodes, filter_filtercodes, synhi,
syntaxhighlighter, tiny_c4l, tool_redis, report_coursesize, local_bulkenrol,
local_mailtest, local_staticpage, block_mb2azcourses.
The code tree is read-only for the web server (since 2026-09-07): no group- or world-writable
path under moodle_isc/; the web-installed plugins used to be 777. Plugin installs and updates
go through plugin-updates.sh, never the web UI. Reversal lists are in
/root/moodle-gw-2026-09-07.txt and /root/moodle-ow-2026-09-07.txt; moodle_data stays
www-data 770.
New Learning patches to re-apply after every theme update
The theme is New Learning 12.2.7 (September 2026). One patch lives in a theme file and is lost on update:
- Page width — the theme reads its SCSS defaults before the site settings, so the boxed
layout ignores Page width (1270 instead of 1600 px, header narrower than the content).
provisioning/learn/mb2nl-fxwidth-patch.shmoves one line intheme/mb2nl/lib/lib_scss.phpand purges the caches. Reported upstream: todo. The inline render of the toggle sidebar's course panel (2026-09-05 to 09) was a second such patch; it was retired on 2026-09-09 for the early-fetch scripts, which live in settings.provisioning/learn/mb2nl-tgsdb-chome-restore.shputs the function back to upstream if the patched file ever comes back from a backup.
A second fix is a setting, not a file, so it survives updates: the toggle sidebar's first-paint
geometry. The theme declares --tgsdb_cheight: 0 without a unit, so calc(100% - 0) is invalid
and the column fell back to its content height until tgsdb.js wrote 0px. A CSS block (a 0px
override and a flex column) is the last section of provisioning/learn/frontpage-2a/frontpage-2a.scss,
appended to the theme's Custom CSS setting by provisioning/learn/mb2nl-tgsdb-css.sh
(idempotent, --remove takes it out). If the column jumps again after an update, a class was
renamed or the setting was reset: compare against scss/theme/theme-toggle-sidebar.scss and re-run.
Frontpage design
Since 2026-09-06 (log) the landing page follows the ISC
sites' visual language (ISCHeader-style header, five-petal strip, Inter / Manrope / Fira Code, the
site's magenta rgb(212, 19, 103)); the reference for the header and strip is curriculum.isc-vs.ch.
Nothing in the code: CSS appended to New Learning's Custom CSS setting, three font settings, a
script appended to Moodle's Additional HTML footer (the header surtitle with its link back to the
hub, and the course code split from the course title on the cards, since New Learning prints
101.1 - Name as one string), and a builder page « ISC / Learn — design 2a » carrying the
frontpage flag (mpage = -1); the previous page « toto » is kept intact in
mdl_local_mb2builder_pages.
Since 2026-09-09: an ambient background (twelve small petal-coloured shapes drifting behind the
whole page, as on the hub landing page, desktop only; the layer is the first child of <body> with
no z-index, a negative one paints under the body ground; it is sized to the document by a ResizeObserver on the
#page columns, since neither <body> nor #page follows the content height and the content keeps moving after
load; the shapes start on a 120 px grid over the visible
ground, each in the cell farthest from the others), no 30 px .pagelayout-b::after under the footer
(a theme pseudo-element, kept on the other pages), course and blog blocks shown as soon as their cards exist with each image
fading in on its own load, the petal mark
running off the hero's bottom-right corner, a single 44 px rhythm between blocks (carried by the
section titles and the row margins set in apply-frontpage-2a.php, nothing else), and the hero's
edu-ID button opening the header's login window (#header-modal-login, the same Bootstrap modal
as the « Connexion » button). The navigation has two root entries, « Infos » and « ISC Hub »
(provisioning/learn/menu-hub-2026-09-09.php; root-level icons must be FontAwesome, the Remix set
is not loaded there).
Source and deploy script: provisioning/learn/frontpage-2a/ — deploy.sh hannibal redeploys
after an edit, deploy.sh --revert hannibal restores the values the first run saved to
moodle_data/isc-frontpage-2a-backup.json. Rehearse on VM 107 first (the default target). The
content scripts beside it (team-menu-2026-09-06.php, menu-infos-2026-09-06.php,
footer-usermenu-2026-09-06.php, course-images.php) each take --revert.
Traps:
- dart-sass writes a UTF-8 BOM at the start of any CSS holding non-ASCII text; appended to the theme SCSS it fails Moodle's compiler, which then serves its bare fallback CSS (the script strips it).
- On hannibal the first web compile after a purge still used the previous Custom CSS, so the script purges the theme a second time.
- New Learning ignores its third Google font, hence the
@font-faceblock for Fira Code. - Chrome drops the gradient fills of SVG paths animated individually, so the petal mark is animated as a whole.
- No square brackets in the HTML elements of the builder page: saving the page by hand in the
builder editor replaces every
[…]by a "no shortcodes" notice, which broke the hero's typewriter script on 2026-09-07 (the script now usessplit("|")and.at(i)). - A New Learning update can rename the classes the CSS targets: check the frontpage after each one.
Theme AJAX requested at HTML parse time
New Learning builds the semester tabs, the blog row, « Mes cours » and, on course pages, the toggle
sidebar's course panel by AJAX, and only fires those requests once first.js is parsed and its own
scripts have run: 0.6 s after the HTML on a fast connection, after the load event on a slow one
(32 s under PageSpeed's mobile throttling). Since 2026-09-09
(log) two inline scripts send the same web-service
requests with fetch() as soon as the HTML is parsed and inject the same markup:
provisioning/learn/frontpage-2a/loader.html, last element of the builder page, for the frontpage
blocks, and a block of header.html (the Additional HTML footer, every page) for the course panel.
The theme's own loaders then find nothing to do on the frontpage (the script renames the
mb2-pb-coursetabs class and removes data-i2load before they run, and handles the tab clicks
itself); on course pages the theme still makes its call and re-renders the identical panel. The
same footer script also sets the course header image (.page-header-img, a data-bg background
the theme's lazy.js would set after first.js) and any lazy image already in the viewport, at
parse time. Both scripts are bracket-free (builder editor) and hand back to the theme if a request fails. What can
break them: an update renaming those classes or the web services theme_mb2nl_load_coursetabs,
theme_mb2nl_load_items, theme_mb2nl_course_chome.
Settings worth knowing
- Audience statistics: Umami, not Google (since 2026-09-10): the fleet's tag sits in
Appearance → Additional HTML → head (
additionalhtmlhead), website id in ISC web sites; the theme's Google Analytics field (theme_mb2nl→ganaidga4, wasG-RVBXPZ5X34) is empty. Query strings are kept so the course viewed is visible,sesskeyis stripped before sending. Previous values inmoodle_data/isc-analytics-backup-2026-09-10.txt; revert = the twocfg.php --setback, thenpurge_caches.php. Since 2026-09-11 the tag also carriesdata-performance="true", which adds the Core Web Vitals of each page to what it reports — set and reverted byprovisioning/learn/learn-umami-performance.sh. Service page: audience statistics. - Syntax highlighter filter,
cdn = 0(since 2026-09-09): the filter loads highlight.js and its stylesheet from the plugin's own directory on every page. Withcdn = 1they came from cdnjs.cloudflare.com, a render-blocking stylesheet on a third origin in the<head>of every page, code or not. - Performance settings (since 2026-09-05): InnoDB buffer pool 2 GB
(
innodb_buffer_pool_size = 2Gat the end of/etc/mysql/mysql.conf.d/mysqld.cnf; the database sits entirely in memory); OPcache 256 MB / 30 000 files / revalidate 60 s in/etc/php/8.3/fpm/conf.d/99-isc-opcache.ini, so a code change is picked up within a minute with no PHP-FPM restart (php -ishows the CLI config; usephp-fpm8.3 -i); the event log (logstore_standard) kept 365 days, trimmed by the nightly cleanup task around 05:00 — the value must be one the admin UI lists, otherwise the Standard log page shows "invalid current value" and saving resets the retention to never. PHP sessions and the MUC application cache live in the local Redis (session_redis_*inconfig.php, storeredisinmoodle_data/muc/config.php; Redis capped at 512 MB,allkeys-lru, igbinary), somdl_sessionsandmoodle_data/sessionsare no longer written.provisioning/learn/learn-redis-enable.shis the idempotent setup, also used on the mirror after a refresh.$CFG->enable_read_only_sessionswas tried twice and reverted: Moodle refuses to start while any MUC definition in session mode resolves to the session store. - Course backups: the automated backup runs Sundays 23:00 and stores each
.mbztwice — inmoodle_data/backup/moodle_courses/and in the course's file area underfiledir— but btrfs shares the blocks. Retention isbackup_auto_max_kept = 3for courses Moodle still backs up; a course it skips (hidden, or unmodified for 30 days, i.e. everything in Archives) is never pruned, so a retention change there is a one-off pass over both locations — file-area entries through the File API, neverrminfiledir. Agreed rule: 3 versions for the year just ended and for active courses, 1 for older archives (2026-09-05 purge).file_storage::content_exists()is deprecated in Moodle 5.0 and always returns false — checkfilediron disk instead. - File-system repository aliases: the current courses alias their files through two repository
instances in pmudry's user context (
101,205.1), which course copies inherit and no course deletion can turn into frozen copies. Never deletemoodledata/repository/mui: the archived 22-23 to 25-26 courses still alias it (2026-09-09). - Router: Moodle's router check reports the 5.1+ router as not configured (URLs without
r.php). The site does not need it; open item in the todo. - Automated backups and the mirror: a restored copy believes it is the real site and mails students within minutes — the mirror is triple-muzzled for that reason (DRP).
Performance — where it stands (September 2026)
After the 2026-09-05 and 2026-09-08 passes:
| before | after | |
|---|---|---|
| Moodle front page, PageSpeed mobile | 46 — LCP 20.2 s, 8 507 kB | 53 — LCP 13.0 s, 2 774 kB |
Course page, server render (Apache %D) | — | 361 ms median, 765 ms p95 |
| Static files and images | re-fetched on every view | from cache, 0 ms server wait |
isc.hevs.ch/landing/, PageSpeed mobile | 81 | 99 |
The theme's AJAX-built blocks no longer wait for first.js (884 AMD modules, 4.7 MB
decompressed, ~760 ms to parse): the early-fetch scripts request them at parse
time. Headless Chromium, 2026-09-09, time until the block is in the DOM:
| theme's own loader | early fetch | |
|---|---|---|
| Frontpage tabs and cards, fast connection | 0.63–0.75 s | 0.49–0.67 s |
| Frontpage tabs and cards, PageSpeed mobile emulation (CPU ×4, 1.6 Mbit/s, 150 ms) | 31.6–31.8 s | 4.3–4.7 s |
| Course sidebar panel, logged in, fast connection | 1.29 s | 0.78–0.84 s |
| Course sidebar panel, mobile emulation | 32.2 s | 4.9 s |
The section TOC of a course page (courseTocLoad) is still filled by the theme's loader. The
floor under the mobile numbers is the theme stylesheet: 1.7 MB decompressed, and the browser
executes no script of the body before it has loaded, about 4 s at 1.6 Mbit/s
(todo). Measured and ruled out: OPcache, Redis (89 MB of
512 MB), the database (no missing index, only cosmetic column drift from mb2 plugins), server load
(0.5), network (TTFB 70–310 ms) and the automated backups (23:00 only). The server-side cache
policy and PHP-FPM pool are on the hannibal page.
Names
isc.hevs.chis in the vsnet zone (SInf),learn.isc-vs.chandhannibal.isc-vs.chin ours (Infomaniak). The root ofisc.hevs.chserves the ISC hub;/learnis the Moodle alias.learn.isc-vs.chhas its own vhost and its own certbot lineage and 301s tohttps://isc.hevs.ch/learn/.
learn DNS recordIt is the SSH, nightly-backup and DRP handle for hannibal. Removing it breaks the backups.