Skip to main content

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)

Moodle5.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
StackUbuntu 24.04, Apache 2.4 + PHP-FPM 8.3, MySQL 8.4.6 (port 3366), Redis 7 — host details on hannibal
URLhttps://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
Authenticationshibboleth (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/
Datamoodle_data, ~130 GB logical after the 2026-09-05 purge (filedir 78 GB + course backups 49 GB, block-shared on btrfs: ~80 GB physical)
Databasemoodle_isc, ~600 MB, 508 tables. Nightly automysqlbackup dump at 06:25 into db_backups/ — a restore never touches the live DB
Cronadmin/cli/cron.php every minute from the www-data crontab
Execution serversnot on the box: the VPL jail is vpl.isc-vs.chenv-vpl.jcloud.ik-server.com, the CodeRunner Jobe server env-6007632.jcloud.ik-server.com:4000 — both on Infomaniak Jelastic Cloud
Outbound mailstraight to mail.infomaniak.com:465 as mailer@isc-vs.ch; settings in mdl_config, not config.php. Cannot use the rack relay
Backups & DRDS923 nightly pull, versioned rumba copy, standing mirror VM 107 — disaster recovery
Scriptsprovisioning/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

TaskHow
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 managerprovisioning/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 upgradeRehearse 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 updatere-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 mirrorprovisioning/drp/learn-mirror-refresh.sh on rumba (runbook)
Before any change to /srvread-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.sh moves one line in theme/mb2nl/lib/lib_scss.php and 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.sh puts 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-face block 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 uses split("|") 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_mb2nlganaidga4, was G-RVBXPZ5X34) is empty. Query strings are kept so the course viewed is visible, sesskey is stripped before sending. Previous values in moodle_data/isc-analytics-backup-2026-09-10.txt; revert = the two cfg.php --set back, then purge_caches.php. Since 2026-09-11 the tag also carries data-performance="true", which adds the Core Web Vitals of each page to what it reports — set and reverted by provisioning/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. With cdn = 1 they 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 = 2G at 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 -i shows the CLI config; use php-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_* in config.php, store redis in moodle_data/muc/config.php; Redis capped at 512 MB, allkeys-lru, igbinary), so mdl_sessions and moodle_data/sessions are no longer written. provisioning/learn/learn-redis-enable.sh is the idempotent setup, also used on the mirror after a refresh. $CFG->enable_read_only_sessions was 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 .mbz twice — in moodle_data/backup/moodle_courses/ and in the course's file area under filedir — but btrfs shares the blocks. Retention is backup_auto_max_kept = 3 for 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, never rm in filedir. 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 — check filedir on 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 delete moodledata/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:

beforeafter
Moodle front page, PageSpeed mobile46 — LCP 20.2 s, 8 507 kB53 — LCP 13.0 s, 2 774 kB
Course page, server render (Apache %D)361 ms median, 765 ms p95
Static files and imagesre-fetched on every viewfrom cache, 0 ms server wait
isc.hevs.ch/landing/, PageSpeed mobile8199

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 loaderearly fetch
Frontpage tabs and cards, fast connection0.63–0.75 s0.49–0.67 s
Frontpage tabs and cards, PageSpeed mobile emulation (CPU ×4, 1.6 Mbit/s, 150 ms)31.6–31.8 s4.3–4.7 s
Course sidebar panel, logged in, fast connection1.29 s0.78–0.84 s
Course sidebar panel, mobile emulation32.2 s4.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.ch is in the vsnet zone (SInf), learn.isc-vs.ch and hannibal.isc-vs.ch in ours (Infomaniak). The root of isc.hevs.ch serves the ISC hub; /learn is the Moodle alias.
  • learn.isc-vs.ch has its own vhost and its own certbot lineage and 301s to https://isc.hevs.ch/learn/.
warning
Keep the learn DNS record

It is the SSH, nightly-backup and DRP handle for hannibal. Removing it breaks the backups.