Add STCourses design, research report and approved mockup
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
.superpowers/
|
||||
node_modules/
|
||||
*.log
|
||||
.env
|
||||
@@ -0,0 +1,959 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Homelab Academy</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&family=Literata:ital,opsz,wght@0,7..72,400;0,7..72,600;1,7..72,400&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
--ground:#12141a; --side:#181b22; --raised:#232733; --line:#262a33; --line2:#343947;
|
||||
--text:#d9dce3; --read:#d9d4c8; --head:#f2efe8; --muted:#a4abb8; --faint:#8c93a0;
|
||||
--accent:#f1b44c; --accent-dim:#8c7440; --on-accent:#12141a;
|
||||
--ok:#86c79f; --bad:#ee8f8f;
|
||||
--sans:"IBM Plex Sans",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
|
||||
--serif:"Literata",Georgia,"Times New Roman",serif;
|
||||
--mono:"IBM Plex Mono",ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;
|
||||
--top:52px; --sidew:296px; --fast:140ms;
|
||||
color-scheme:dark;
|
||||
}
|
||||
/* a course may set its own accent through its manifest; nothing else is themed */
|
||||
body[data-course="docker"]{--accent:#6ea8fe; --accent-dim:#3f5f8e}
|
||||
body[data-look="reading"]{--pf:var(--serif); --ps:19px; --pl:1.75; --pc:var(--read); --col:660px; --wide:900px; --tf:var(--serif); --h1:40px}
|
||||
body[data-look="work"]{--pf:var(--sans); --ps:15.5px; --pl:1.6; --pc:var(--text); --col:760px; --wide:1180px; --tf:var(--sans); --h1:28px}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
html{-webkit-text-size-adjust:100%}
|
||||
body{margin:0; background:var(--ground); color:var(--text); font:400 14px/1.5 var(--sans); -webkit-font-smoothing:antialiased}
|
||||
a{color:inherit}
|
||||
button,input,textarea{font:inherit; color:inherit}
|
||||
button{cursor:pointer}
|
||||
[hidden]{display:none !important}
|
||||
.tnum{font-variant-numeric:tabular-nums}
|
||||
:focus-visible{outline:2px solid var(--accent); outline-offset:2px}
|
||||
::selection{background:color-mix(in srgb,var(--accent) 35%,transparent)}
|
||||
kbd{font:500 11px/1 var(--mono); display:inline-grid; place-items:center; min-width:18px; height:18px; padding:0 4px; border:1px solid color-mix(in srgb,currentColor 38%,transparent); border-radius:4px}
|
||||
code,pre{font-family:var(--mono)}
|
||||
.sr{position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap}
|
||||
|
||||
/* reviewer strip: not product UI */
|
||||
.review{display:flex; align-items:center; gap:4px; padding:0 16px; height:34px; background:#0d0f13; border-bottom:1px dashed var(--line2); font-size:12px; color:var(--faint); overflow-x:auto; white-space:nowrap}
|
||||
.review b{font-weight:500; color:var(--muted); margin-right:8px}
|
||||
.review a{text-decoration:none; padding:3px 10px; border-radius:14px; color:var(--muted)}
|
||||
.review a:hover{color:var(--text)}
|
||||
.review a[aria-current]{background:var(--raised); color:var(--head)}
|
||||
.review .keys{margin-left:auto; padding-left:16px; display:flex; gap:6px; align-items:center}
|
||||
|
||||
/* header */
|
||||
.top{position:sticky; top:0; z-index:30; height:var(--top); display:flex; align-items:center; gap:14px; padding:0 16px; background:var(--ground); border-bottom:1px solid var(--line)}
|
||||
.org{flex:0 0 calc(var(--sidew) - 30px); font-weight:600; color:var(--head); text-decoration:none; font-size:14px; letter-spacing:.005em}
|
||||
.icon-btn{display:none; width:34px; height:34px; border-radius:18px; border:1px solid var(--line2); background:none; place-items:center; color:var(--muted)}
|
||||
.switch{position:relative}
|
||||
.switch-btn{display:flex; align-items:center; gap:10px; height:34px; padding:0 12px 0 5px; border:1px solid var(--line2); border-radius:18px; background:none; font-weight:500; font-size:13px; max-width:300px}
|
||||
.switch-btn:hover,.btn:hover,.icon-btn:hover{background:var(--raised)}
|
||||
.switch-btn .t{overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
|
||||
.switch-btn svg{color:var(--faint); flex:none}
|
||||
.menu{position:absolute; top:42px; left:0; width:330px; margin:0; padding:6px; list-style:none; background:var(--side); border:1px solid var(--line2); border-radius:14px}
|
||||
.menu a{display:flex; gap:12px; align-items:center; padding:8px 10px; border-radius:10px; text-decoration:none}
|
||||
.menu a:hover,.menu a:focus-visible{background:var(--raised)}
|
||||
.menu a[aria-current] .mt{color:var(--head)}
|
||||
.menu .mt{font-weight:500; font-size:13.5px}
|
||||
.menu .ms{display:block; font-size:12px; color:var(--faint)}
|
||||
.search{flex:1; max-width:380px; display:flex; align-items:center; gap:8px; height:34px; padding:0 8px 0 12px; border:1px solid var(--line2); border-radius:18px; color:var(--faint); margin-left:auto}
|
||||
.search:focus-within{border-color:var(--faint)}
|
||||
.search input{flex:1; min-width:0; border:0; background:none; outline:0; color:var(--text); font-size:13px}
|
||||
.search input::placeholder{color:var(--faint)}
|
||||
.seg{display:inline-flex; padding:2px; border:1px solid var(--line2); border-radius:20px; flex:none}
|
||||
.seg button{border:0; background:none; height:28px; padding:0 13px; border-radius:16px; font-size:12.5px; font-weight:500; color:var(--faint); transition:background var(--fast),color var(--fast)}
|
||||
.seg button:hover{color:var(--text)}
|
||||
.seg button[aria-pressed="true"]{background:var(--raised); color:var(--head)}
|
||||
|
||||
/* shell */
|
||||
.shell{display:flex; align-items:flex-start}
|
||||
.side{position:sticky; top:var(--top); flex:none; width:var(--sidew); height:calc(100vh - var(--top)); overflow-y:auto; background:var(--side); border-right:1px solid var(--line); overscroll-behavior:contain}
|
||||
body.no-side .side{display:none}
|
||||
.side-head{display:flex; gap:12px; padding:18px 16px 16px; border-bottom:1px solid var(--line)}
|
||||
.side-head h2{margin:0; font-size:14.5px; font-weight:600; color:var(--head); line-height:1.3}
|
||||
.side-head .src{font-size:12px; color:var(--faint); margin-top:2px}
|
||||
.prog{display:flex; align-items:center; gap:10px; margin-top:10px; font-size:12px; color:var(--muted)}
|
||||
.bar{display:block; flex:1; height:4px; border-radius:2px; background:var(--line); overflow:hidden}
|
||||
.bar i{display:block; height:100%; background:var(--accent); border-radius:2px}
|
||||
.bar.dim i{background:var(--accent-dim)}
|
||||
.mod{border-bottom:1px solid var(--line)}
|
||||
.mod-head{width:100%; display:grid; grid-template-columns:22px 1fr auto 12px; gap:6px; align-items:baseline; padding:11px 16px 7px; border:0; background:none; text-align:left; font-size:13.5px; color:var(--text)}
|
||||
.mod-head:hover{color:var(--head)}
|
||||
.mod-head .n{color:var(--faint); font-size:12px}
|
||||
.mod-head .c{color:var(--faint); font-size:12px}
|
||||
.mod-head svg{color:var(--faint); transition:transform var(--fast); align-self:center}
|
||||
.mod-head[aria-expanded="true"] svg{transform:rotate(90deg)}
|
||||
.mod.is-current .mod-head{color:var(--head); font-weight:500}
|
||||
.mod .bar{margin:0 16px 11px 44px; flex:none}
|
||||
.lessons{list-style:none; margin:-3px 0 0; padding:0 0 8px}
|
||||
.les{display:grid; grid-template-columns:22px 1fr auto; gap:6px; align-items:center; padding:6px 16px; font-size:13px; line-height:1.35; color:var(--muted); text-decoration:none}
|
||||
.les:hover{background:color-mix(in srgb,var(--raised) 60%,transparent); color:var(--text)}
|
||||
.les .m{width:8px; height:8px; border-radius:50%; border:1.5px solid var(--line2); justify-self:center}
|
||||
.les.done .m{border:0; width:12px; height:12px; color:var(--faint)}
|
||||
.les .d{font-size:12px; color:var(--faint)}
|
||||
.les[aria-current="page"]{background:var(--raised); color:var(--head); box-shadow:inset 2px 0 0 var(--accent)}
|
||||
.les[aria-current="page"] .m{border:0; background:var(--accent)}
|
||||
.nohit{padding:16px; color:var(--faint); font-size:13px}
|
||||
|
||||
/* main column */
|
||||
main{flex:1; min-width:0; padding:44px 40px 0}
|
||||
.col{max-width:var(--col); margin:0 auto}
|
||||
.wide{max-width:var(--wide); margin:0 auto}
|
||||
.chap{font-size:13px; font-weight:500; color:var(--muted); margin:0 0 10px}
|
||||
h1.title{font:600 var(--h1)/1.12 var(--tf); color:var(--head); margin:0; letter-spacing:-.012em; text-wrap:balance}
|
||||
.meta{display:flex; flex-wrap:wrap; gap:14px 30px; margin:22px 0 0; padding-bottom:22px; border-bottom:1px solid var(--line); align-items:end}
|
||||
.meta div{min-width:0}
|
||||
.meta dt{font-size:12px; color:var(--faint); margin-bottom:2px}
|
||||
.meta dd{margin:0; font-size:13.5px; color:var(--text)}
|
||||
.meta dd code{font-size:12.5px}
|
||||
.source{font-size:12.5px; color:var(--faint); margin:14px 0 0}
|
||||
.source strong{font-weight:500; color:var(--muted)}
|
||||
|
||||
.prose{font-family:var(--pf); font-size:var(--ps); line-height:var(--pl); color:var(--pc)}
|
||||
.prose p,.prose ul,.prose ol{margin:0 0 1em}
|
||||
.prose h2{font:600 1.32em/1.25 var(--tf); color:var(--head); margin:2em 0 .55em; letter-spacing:-.005em}
|
||||
.prose h3{font:600 1.08em/1.3 var(--tf); color:var(--head); margin:1.7em 0 .45em}
|
||||
.prose strong{font-weight:600; color:var(--head)}
|
||||
.prose li{margin-bottom:.35em; padding-left:.2em}
|
||||
.prose :not(pre)>code{font-size:.78em; padding:.08em .32em; background:color-mix(in srgb,var(--raised) 75%,transparent); border-radius:4px; color:var(--text)}
|
||||
pre.code{font-size:13.5px; line-height:1.6; background:var(--side); border:1px solid var(--line); border-radius:10px; padding:14px 16px; margin:0 0 1.2em; overflow-x:auto; color:var(--text); tab-size:2}
|
||||
.prose pre.code{font-size:13.5px}
|
||||
.lede{color:var(--head)}
|
||||
.aside{border-left:2px solid var(--line2); padding-left:18px; color:var(--muted); font-size:.92em; margin:0 0 1.2em}
|
||||
.aside strong{color:var(--text)}
|
||||
.c{color:var(--faint); font-style:italic}
|
||||
.k{color:var(--head); font-weight:500}
|
||||
.trk{display:none}
|
||||
body[data-track="js"] .trk[data-t="js"],body[data-track="scala"] .trk[data-t="scala"],body[data-track="fs"] .trk[data-t="fs"]{display:block}
|
||||
body[data-track="js"] span.trk[data-t="js"],body[data-track="scala"] span.trk[data-t="scala"],body[data-track="fs"] span.trk[data-t="fs"]{display:inline}
|
||||
|
||||
details.fold{border-top:1px solid var(--line); border-bottom:1px solid var(--line); margin:0 0 1.4em}
|
||||
details.fold+details.fold{margin-top:-1.4em; border-top:0}
|
||||
details.fold>summary{list-style:none; display:flex; align-items:center; gap:10px; padding:12px 2px; font:500 14px/1.4 var(--sans); color:var(--text); cursor:pointer}
|
||||
details.fold>summary::-webkit-details-marker{display:none}
|
||||
details.fold>summary svg{color:var(--faint); transition:transform var(--fast); flex:none}
|
||||
details.fold[open]>summary svg{transform:rotate(90deg)}
|
||||
details.fold>summary:hover{color:var(--head)}
|
||||
details.fold>summary .tier{margin-left:auto; font-size:12px; color:var(--faint); font-weight:400}
|
||||
details.fold>.in{padding:2px 0 14px 24px}
|
||||
.qs{counter-reset:q; list-style:none; padding:0}
|
||||
.qs li{counter-increment:q; position:relative; padding:.55em 0 .55em 2.2em; border-top:1px solid var(--line); margin:0}
|
||||
.qs li::before{content:counter(q); position:absolute; left:0; top:.55em; font:500 13px/2.1 var(--sans); color:var(--faint); font-variant-numeric:tabular-nums}
|
||||
|
||||
/* pills */
|
||||
.btn{display:inline-flex; align-items:center; gap:8px; height:34px; padding:0 15px; border:1px solid var(--line2); border-radius:18px; background:none; font-size:13px; font-weight:500; text-decoration:none; white-space:nowrap; transition:background var(--fast),color var(--fast),border-color var(--fast)}
|
||||
.btn.sm{height:28px; padding:0 11px; font-size:12.5px}
|
||||
.btn.primary{background:var(--accent); border-color:var(--accent); color:var(--on-accent)}
|
||||
.btn.primary:hover{background:color-mix(in srgb,var(--accent) 86%,#fff)}
|
||||
.btn.quiet{border-color:transparent; color:var(--muted)}
|
||||
.btn.quiet:hover{color:var(--text)}
|
||||
.btn:disabled{opacity:.45; cursor:default}
|
||||
|
||||
/* floating action pill */
|
||||
.dock{position:sticky; bottom:18px; z-index:10; display:flex; width:max-content; max-width:100%; margin:56px auto 18px; gap:4px; padding:5px; background:var(--raised); border:1px solid var(--line2); border-radius:24px; box-shadow:0 0 0 10px var(--ground)}
|
||||
.dock .btn{height:36px; border-color:transparent}
|
||||
.dock .btn:hover{background:var(--line2)}
|
||||
.dock .btn.primary:hover{background:color-mix(in srgb,var(--accent) 86%,#fff)}
|
||||
.dock .btn kbd{border-color:color-mix(in srgb,currentColor 45%,transparent)}
|
||||
.dock #done[aria-pressed="true"]{color:var(--accent)}
|
||||
.dock .sep{width:1px; background:var(--line2); margin:7px 2px}
|
||||
|
||||
/* video */
|
||||
.player{aspect-ratio:16/9; background:#000; border-radius:10px; overflow:hidden}
|
||||
.player video{display:block; width:100%; height:100%}
|
||||
.split{display:grid; grid-template-columns:minmax(0,1fr) 300px; gap:40px; margin-top:28px}
|
||||
.blockhead{display:flex; align-items:baseline; justify-content:space-between; gap:12px; margin:0 0 10px}
|
||||
.blockhead h2{margin:0; font:600 15px/1.3 var(--sans); color:var(--head)}
|
||||
.blockhead span{font-size:12px; color:var(--faint)}
|
||||
.notes{width:100%; min-height:140px; resize:vertical; background:var(--side); border:1px solid var(--line); border-radius:10px; padding:12px 14px; color:var(--text); font-size:14px; line-height:1.55}
|
||||
.notes:focus{outline:0; border-color:var(--faint)}
|
||||
.rows{list-style:none; margin:0; padding:0; border-top:1px solid var(--line)}
|
||||
.rows li{display:grid; grid-template-columns:20px minmax(0,1fr) auto; gap:10px; align-items:center; padding:10px 2px; border-bottom:1px solid var(--line); font-size:13.5px}
|
||||
.rows li svg{color:var(--faint)}
|
||||
.rows a{overflow-wrap:anywhere}
|
||||
.rows .sz{white-space:nowrap; font-size:12px; color:var(--faint)}
|
||||
.rows a{text-decoration:none}
|
||||
.rows a:hover{text-decoration:underline; text-underline-offset:3px}
|
||||
|
||||
/* slides */
|
||||
.stage{position:relative; aspect-ratio:16/9; background:#fff; border-radius:8px; overflow:hidden}
|
||||
.stage img{width:100%; height:100%; object-fit:cover; display:block}
|
||||
.stage:fullscreen{border-radius:0; background:#000; display:grid; place-items:center}
|
||||
.stage:fullscreen img{object-fit:contain}
|
||||
.deckbar{display:flex; align-items:center; gap:8px; margin:12px 0 0}
|
||||
.deckbar .count{font-size:13px; color:var(--muted); margin:0 6px; min-width:58px; text-align:center}
|
||||
.deckbar .gap{flex:1}
|
||||
.thumbs{display:flex; gap:8px; margin:16px 0 0; overflow-x:auto; padding:3px 3px 6px}
|
||||
.thumbs button{flex:none; width:104px; aspect-ratio:16/9; padding:0; border:1px solid var(--line2); border-radius:5px; overflow:hidden; background:#fff; opacity:.6; transition:opacity var(--fast)}
|
||||
.thumbs button:hover{opacity:.9}
|
||||
.thumbs button[aria-current="true"]{opacity:1; outline:2px solid var(--accent); outline-offset:2px}
|
||||
.thumbs img{width:100%; height:100%; object-fit:cover; display:block}
|
||||
.thumbs .more{flex:none; width:128px; aspect-ratio:16/9; border:1px dashed var(--line2); border-radius:5px; display:grid; place-items:center; text-align:center; font-size:11.5px; line-height:1.35; color:var(--faint); padding:6px}
|
||||
|
||||
/* code files */
|
||||
.files{display:grid; grid-template-columns:220px minmax(0,1fr); border:1px solid var(--line); border-radius:10px; overflow:hidden; margin:0 0 1.4em; font-family:var(--sans)}
|
||||
.tree{background:var(--side); border-right:1px solid var(--line); padding:8px 0; font-size:13px}
|
||||
.tree .dir{padding:5px 12px; color:var(--muted); display:flex; gap:7px; align-items:center}
|
||||
.tree .dir.sub{padding-left:26px}
|
||||
.tree button{display:flex; gap:7px; align-items:center; width:100%; border:0; background:none; text-align:left; padding:5px 12px 5px 26px; color:var(--muted); font-size:13px}
|
||||
.tree button.l2{padding-left:40px}
|
||||
.tree button:hover{color:var(--text)}
|
||||
.tree button[aria-current="true"]{background:var(--raised); color:var(--head)}
|
||||
.tree span.f{display:flex; gap:7px; align-items:center; padding:5px 12px 5px 26px; color:var(--faint)}
|
||||
.tree svg{flex:none; color:var(--faint)}
|
||||
.viewer{min-width:0; display:flex; flex-direction:column; background:var(--ground)}
|
||||
.tabs{display:flex; align-items:center; border-bottom:1px solid var(--line); padding:0 6px 0 0; overflow-x:auto}
|
||||
.tabs button{border:0; background:none; padding:10px 14px; font-size:12.5px; color:var(--faint); border-bottom:2px solid transparent; margin-bottom:-1px; white-space:nowrap; font-family:var(--mono)}
|
||||
.tabs button[aria-selected="true"]{color:var(--head); border-bottom-color:var(--accent)}
|
||||
.tabs .btn{margin-left:auto}
|
||||
.path{font-size:12px; color:var(--faint); padding:8px 16px 0}
|
||||
.viewer pre.code{border:0; border-radius:0; background:none; margin:0; counter-reset:ln}
|
||||
.ln{display:block; counter-increment:ln}
|
||||
.ln::before{content:counter(ln); display:inline-block; width:2.2em; margin-right:1em; text-align:right; color:#5b6170}
|
||||
.cmd{position:relative}
|
||||
.cmd .btn{position:absolute; top:8px; right:8px}
|
||||
|
||||
/* exercise */
|
||||
.ex{display:grid; grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr); gap:36px; align-items:start}
|
||||
.ex .prose{font-size:15px}
|
||||
.edit{border:1px solid var(--line); border-radius:10px; overflow:hidden; position:sticky; top:calc(var(--top) + 20px)}
|
||||
.edit-bar{display:flex; align-items:center; gap:6px; padding:6px 6px 6px 14px; background:var(--side); border-bottom:1px solid var(--line)}
|
||||
.edit-bar .f{font:500 12.5px var(--mono); color:var(--muted); margin-right:auto}
|
||||
.editor{display:grid; grid-template-columns:44px 1fr; background:var(--ground); height:270px}
|
||||
.gutter{margin:0; padding:12px 10px 12px 0; text-align:right; color:#5b6170; font:13.5px/1.6 var(--mono); overflow:hidden; user-select:none; background:var(--ground)}
|
||||
.editor textarea{border:0; resize:none; outline:0; background:none; padding:12px 14px 12px 6px; font:13.5px/1.6 var(--mono); color:var(--text); white-space:pre; overflow:auto; tab-size:2}
|
||||
.editor:focus-within{box-shadow:inset 0 0 0 1px var(--faint)}
|
||||
.results{border-top:1px solid var(--line); background:var(--side)}
|
||||
.res-sum{display:flex; align-items:center; justify-content:space-between; gap:10px; padding:10px 14px; font-size:13px; color:var(--muted); border-bottom:1px solid var(--line)}
|
||||
.res-sum b{font-weight:600; color:var(--head)}
|
||||
.res-list{list-style:none; margin:0; padding:0; max-height:300px; overflow:auto}
|
||||
.res-list li{padding:8px 14px; border-bottom:1px solid var(--line); font-size:13px}
|
||||
.res-list li:last-child{border-bottom:0}
|
||||
.res-row{display:grid; grid-template-columns:40px 1fr; gap:8px}
|
||||
.res-row .s{font-weight:600; font-size:12px}
|
||||
.pass .s{color:var(--ok)}
|
||||
.fail .s{color:var(--bad)}
|
||||
.res-row code{font-size:12.5px; color:var(--text)}
|
||||
.diff{display:grid; grid-template-columns:64px 1fr; gap:2px 8px; margin:6px 0 2px 48px; font-size:12.5px}
|
||||
.diff dt{color:var(--faint)}
|
||||
.diff dd{margin:0; font-family:var(--mono); color:var(--text)}
|
||||
.local{margin:20px 0 0; padding:12px 0 0; border-top:1px solid var(--line); font-size:13px; color:var(--muted)}
|
||||
.local strong{color:var(--text); font-weight:500}
|
||||
.local code{display:block; margin-top:6px; font-size:12.5px; color:var(--text)}
|
||||
|
||||
/* library */
|
||||
.lib-h{font:600 24px/1.2 var(--sans); color:var(--head); margin:0 0 4px}
|
||||
.lib-sub{color:var(--faint); margin:0 0 32px; font-size:13.5px}
|
||||
.sec-h{font:600 14px var(--sans); color:var(--muted); margin:0 0 10px}
|
||||
.cover{flex:none; align-self:flex-start; position:relative; width:var(--cw,44px); aspect-ratio:3/4; border-radius:6px; background:var(--cbg); color:var(--cfg); border:1px solid color-mix(in srgb,var(--cfg) 18%,transparent); overflow:hidden; font:600 calc(var(--cw,44px)*.34)/1 var(--sans); letter-spacing:-.03em; display:flex; align-items:flex-end; padding:calc(var(--cw,44px)*.12)}
|
||||
.cover.tiny{--cw:24px; border-radius:4px; font-size:9px}
|
||||
.continue{display:grid; grid-template-columns:auto 1fr auto; gap:20px; align-items:center; padding:18px 0; border-top:1px solid var(--line); border-bottom:1px solid var(--line); margin-bottom:44px}
|
||||
.continue .t{font-size:16px; font-weight:600; color:var(--head)}
|
||||
.continue .s{font-size:13.5px; color:var(--muted); margin:2px 0 10px}
|
||||
.continue .prog{max-width:360px; margin:0}
|
||||
.lib{border-top:1px solid var(--line)}
|
||||
.lib-row,.lib-cols{display:grid; grid-template-columns:minmax(0,1fr) 90px 210px 180px; gap:24px; align-items:center}
|
||||
.lib-cols{padding:0 0 8px; font-size:12px; color:var(--faint)}
|
||||
.lib-row{padding:16px 0; border-bottom:1px solid var(--line); text-decoration:none}
|
||||
.lib-row:hover .lt{color:var(--head); text-decoration:underline; text-underline-offset:3px; text-decoration-color:var(--line2)}
|
||||
.lib-main{display:flex; gap:16px; align-items:center; min-width:0}
|
||||
.lt{font-size:15px; font-weight:600; color:var(--text)}
|
||||
.ls{font-size:13px; color:var(--faint); margin-top:2px}
|
||||
.lnum{font-size:13.5px; color:var(--muted)}
|
||||
.tags{display:flex; flex-wrap:wrap; gap:5px}
|
||||
.tag{font-size:11.5px; color:var(--muted); border:1px solid var(--line2); border-radius:10px; padding:1px 8px}
|
||||
.lib-row .prog{margin:0}
|
||||
|
||||
.toast{position:fixed; left:50%; bottom:24px; transform:translateX(-50%); z-index:50; background:var(--raised); border:1px solid var(--line2); border-radius:18px; padding:9px 16px; font-size:13px; color:var(--text)}
|
||||
.scrim{display:none}
|
||||
|
||||
@media (max-width:1100px){
|
||||
.ex{grid-template-columns:minmax(0,1fr)}
|
||||
.edit{position:static}
|
||||
.split{grid-template-columns:1fr; gap:28px}
|
||||
.lib-row,.lib-cols{grid-template-columns:minmax(0,1fr) 70px 150px}
|
||||
.lib-row>.tags,.lib-cols>:nth-child(3){display:none}
|
||||
}
|
||||
@media (max-width:960px){
|
||||
.org{flex:none}
|
||||
.icon-btn{display:inline-grid}
|
||||
.side{position:fixed; top:var(--top); left:0; bottom:0; height:auto; z-index:40; transform:translateX(-100%); transition:transform 160ms ease-out}
|
||||
body.side-open .side{transform:none}
|
||||
body.side-open .scrim{display:block; position:fixed; left:0; right:0; bottom:0; z-index:35; background:rgb(10 11 15 / .6)}
|
||||
body.no-side .icon-btn{visibility:hidden; width:0; margin-right:-14px; border:0}
|
||||
main{padding:32px 24px 0}
|
||||
}
|
||||
@media (max-width:680px){
|
||||
:root{--top:52px}
|
||||
.top{gap:8px; padding:0 10px}
|
||||
.org{display:none}
|
||||
.search{display:none}
|
||||
.switch-btn{max-width:170px}
|
||||
.switch-btn .t{font-size:12.5px}
|
||||
.seg button{padding:0 10px}
|
||||
.review .keys{display:none}
|
||||
main{padding:26px 18px 0}
|
||||
body[data-look="reading"]{--ps:18px; --h1:31px}
|
||||
.meta{gap:12px 22px}
|
||||
.files{grid-template-columns:1fr}
|
||||
.tree{border-right:0; border-bottom:1px solid var(--line)}
|
||||
.continue{grid-template-columns:auto 1fr}
|
||||
.continue .btn{grid-column:1/-1; justify-self:start}
|
||||
.lib-row,.lib-cols{grid-template-columns:minmax(0,1fr) 92px; gap:14px}
|
||||
.lib-row>.lnum,.lib-cols>:nth-child(2){display:none}
|
||||
.lib-cols>:last-child{text-align:left}
|
||||
.menu{width:min(330px,calc(100vw - 20px)); left:-48px}
|
||||
.dock{bottom:12px}
|
||||
.dock .lbl-long{display:none}
|
||||
.thumbs button{width:84px}
|
||||
}
|
||||
@media (prefers-reduced-motion:reduce){*,*::before,*::after{transition:none !important; animation:none !important; scroll-behavior:auto !important}}
|
||||
</style>
|
||||
</head>
|
||||
<body data-look="reading" data-track="js" data-course="snake">
|
||||
|
||||
<nav class="review" aria-label="Mockup: lesson types">
|
||||
<b>Mockup: lesson types</b>
|
||||
<a href="#text">Text</a><a href="#video">Text + video</a><a href="#slides">Slides</a><a href="#code">Text + code files</a><a href="#exercise">Exercise</a><a href="#home">Library</a>
|
||||
<span class="keys"><kbd>[</kbd><kbd>]</kbd> lessons <kbd>/</kbd> search</span>
|
||||
</nav>
|
||||
|
||||
<header class="top">
|
||||
<button class="icon-btn" id="outlineBtn" aria-label="Course outline" aria-expanded="false" aria-controls="side"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M2 8h12M2 12h8"/></svg></button>
|
||||
<a class="org" href="#home">Homelab Academy</a>
|
||||
<div class="switch">
|
||||
<button class="switch-btn" id="courseBtn" aria-haspopup="true" aria-expanded="false" aria-controls="courseMenu"><span id="courseTile"></span><span class="t" id="courseName"></span><svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 3.5l3 3 3-3"/></svg></button>
|
||||
<ul class="menu" id="courseMenu" hidden></ul>
|
||||
</div>
|
||||
<label class="search"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5L14 14"/></svg><span class="sr">Search</span><input id="search" type="search" placeholder="Search this course" autocomplete="off"><kbd>/</kbd></label>
|
||||
<div class="seg" role="group" aria-label="Look"><button id="lookReading" aria-pressed="true">Reading</button><button id="lookWork" aria-pressed="false">Work</button></div>
|
||||
</header>
|
||||
|
||||
<div class="shell">
|
||||
<aside class="side" id="side" aria-label="Course outline"></aside>
|
||||
<div class="scrim" id="scrim"></div>
|
||||
<main id="main">
|
||||
<div id="view"></div>
|
||||
<div class="dock" id="dock" role="toolbar" aria-label="Lesson actions">
|
||||
<button class="btn quiet" id="prev" title="Previous lesson"><span class="sr">Previous lesson</span><kbd>[</kbd></button>
|
||||
<button class="btn quiet" id="done" aria-pressed="false"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M3 8.5l3.2 3L13 4.5"/></svg><span id="doneLbl">Mark done</span></button>
|
||||
<span class="sep" aria-hidden="true"></span>
|
||||
<button class="btn primary" id="next"><span>Next<span class="lbl-long"> lesson</span></span><kbd>]</kbd></button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div class="toast" id="toast" role="status" hidden></div>
|
||||
|
||||
<!-- 1. Text only -->
|
||||
<template id="tpl-text">
|
||||
<article class="col">
|
||||
<p class="chap">Module 3</p>
|
||||
<h1 class="title">Pure game rules</h1>
|
||||
<dl class="meta">
|
||||
<div><dt>Time</dt><dd>About 3–4 hours</dd></div>
|
||||
<div><dt>You edit</dt><dd><code class="trk" data-t="js">js/src/rules.js</code><code class="trk" data-t="scala">scala/src/Rules.scala</code><code class="trk" data-t="fs">fsharp/src/Rules.fs</code></dd></div>
|
||||
<div><dt>Needs</dt><dd>Modules 1 and 2</dd></div>
|
||||
<div><dt>Track</dt><dd><div class="seg" role="group" aria-label="Track" data-trackseg><button data-tr="js">JavaScript</button><button data-tr="scala">Scala 3</button><button data-tr="fs">F#</button></div></dd></div>
|
||||
</dl>
|
||||
<div class="prose" style="margin-top:30px">
|
||||
<h2 style="margin-top:0">Why this module</h2>
|
||||
<p>In a stateful Snake, the rules live in methods like <code>snake.move()</code>, <code>snake.changeDirection(d)</code> and <code>game.checkCollision()</code>. They are called in some order from a game loop, and each one reads and writes shared fields. The <em>order</em> of those calls is part of the rules, but it is written nowhere. Is the collision checked before or after the tail moves? Before or after the snake grows? You find out by reading the loop, or by watching bugs.</p>
|
||||
<p>In this module, every rule is a <strong>pure function from a state to a new state</strong>. <code>turn(state, dir)</code> returns the state after the player asks to turn. <code>advance(state)</code> returns the state one step later. Each function says, in one place, everything that happens in that step and in which order. You test it by handing it a state and comparing the result with the expected state; no timers, no setup, no mocks.</p>
|
||||
<p>Two of the rules hide real subtleties that many Snake implementations get wrong. You will meet them as puzzles, and reason them through before you read the explanations.</p>
|
||||
|
||||
<h2>Learning objectives</h2>
|
||||
<p>After this module you can:</p>
|
||||
<ul>
|
||||
<li><strong>implement</strong> game rules as total functions <code>State → State</code> using copy-with-changes;</li>
|
||||
<li><strong>explain</strong> why a rule returns the state unchanged instead of throwing when it does not apply;</li>
|
||||
<li><strong>reason</strong> about the order of events inside one step (move, grow, collide) and predict the result for a given board;</li>
|
||||
<li><strong>explain</strong> why <code>turn</code> must compare against <code>dir</code> (last moved) and not <code>nextDir</code>;</li>
|
||||
<li><strong>read</strong> a fixture case, draw it, and use it to debug.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Rules as functions State → State</h2>
|
||||
<p>Each rule has the same shape: it takes the whole game state (plus maybe some input) and returns a whole new game state.</p>
|
||||
<pre class="code">turn : (GameState, Direction) -> GameState
|
||||
advance : GameState -> GameState</pre>
|
||||
<p>Because the result is a full state, rules can be chained: <code>advance(turn(s, 'Up'))</code> is the state after turning up and moving. Because they are pure, a rule called with a frozen state from a fixture gives exactly the fixture’s expected state, every time.</p>
|
||||
|
||||
<h3>Total functions: “unchanged” is a valid answer</h3>
|
||||
<p>A function is <strong>total</strong> if it returns a result for every possible input. It never throws, never loops forever, never returns <code>undefined</code> by accident. Our rules follow one simple convention: <strong>when a rule does not apply, it returns the state unchanged.</strong> <code>turn</code> while paused returns the same state.</p>
|
||||
<p class="aside"><strong>From everyday programming:</strong> the usual alternative is a method that throws an <code>IllegalStateException</code> when it is called at the wrong time. A total rule behaves more like a disabled button in a user interface: pressing it is allowed, and nothing happens.</p>
|
||||
<p>A typical rule therefore starts with a <strong>guard</strong>:</p>
|
||||
<pre class="code trk" data-t="js"><span class="c">// (illustration on a different record)</span>
|
||||
const heal = (hero, amount) => {
|
||||
if (hero.hp <= 0) return hero <span class="c">// dead heroes cannot be healed: unchanged</span>
|
||||
return { ...hero, hp: Math.min(hero.maxHp, hero.hp + amount) }
|
||||
}</pre>
|
||||
<pre class="code trk" data-t="scala">def heal(hero: Hero, amount: Int): Hero =
|
||||
if hero.hp <= 0 then hero
|
||||
else hero.copy(hp = math.min(hero.maxHp, hero.hp + amount))</pre>
|
||||
<pre class="code trk" data-t="fs">let heal (hero: Hero) (amount: int) : Hero =
|
||||
if hero.Hp <= 0 then hero
|
||||
else { hero with Hp = min hero.MaxHp (hero.Hp + amount) }</pre>
|
||||
|
||||
<h2>Puzzle 1: can the snake bite its own tail?</h2>
|
||||
<p>Here is a 5 x 5 board. The snake has four segments, in a tight square. Head <code>H</code> at (1,1), then (1,2), (2,2), and the tail <code>T</code> at (2,1). It is about to move <code>Right</code>, onto the tail’s cell. There is no apple.</p>
|
||||
<pre class="code"> x=0 x=1 x=2 x=3 x=4
|
||||
y=0 . . . . .
|
||||
y=1 . H T . . snake (head first): (1,1) (1,2) (2,2) (2,1)
|
||||
y=2 . b b . . nextDir: Right
|
||||
y=3 . . . . .
|
||||
y=4 . . . . .</pre>
|
||||
<p><strong>Before reading on, decide:</strong> is this move a collision (game over), or legal? Draw what the board looks like after the move in each case. Then consider a second board: identical, except that there is an apple on (2,1), the cell where the tail is.</p>
|
||||
<details class="fold">
|
||||
<summary><svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M3.5 2l3 3-3 3"/></svg>Think about it first, then open the explanation</summary>
|
||||
<div class="in">
|
||||
<p>All segments of the snake move <strong>at the same time</strong>. In one step, the head enters a new cell and, if the snake is not eating, the tail leaves its cell. So in the first board, by the time the head arrives at (2,1), the tail has moved on. The move is <strong>legal</strong>, and the snake chases its own tail around the square.</p>
|
||||
<p>In the second board, the head lands on the apple, so the snake <strong>grows</strong>: the tail stays where it is. Now the head and the tail would occupy the same cell, which is a collision: game over.</p>
|
||||
<pre class="code">eating? yes -> body = the whole old snake (tail stays, the snake grows)
|
||||
no -> body = the old snake minus its last cell (tail leaves)
|
||||
collision if the new head is off the board, or is contained in body</pre>
|
||||
<p>This is why <code>advance</code> must first decide whether it eats, then compute the body, and only then check for collisions. The order is the rule.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<h2>Check your understanding</h2>
|
||||
<ol class="qs">
|
||||
<li>Why does <code>turn</code> return the state unchanged when paused, instead of throwing? Who would have to deal with the exception, and how would that change the rest of the program?</li>
|
||||
<li>Without looking: on a 5 x 5 board, the snake is (1,1) (1,2) (2,2) (2,1) heading Up with <code>nextDir = Right</code> and an apple at (2,1). What does <code>advance</code> return? Now the same with no apple.</li>
|
||||
<li>Explain to a friend, with a drawing, why <code>turn</code> must compare against <code>dir</code> and not <code>nextDir</code>. What sequence of key presses kills the snake in the wrong version?</li>
|
||||
<li>After <code>advance</code> crashes into a wall, the snake is unchanged but <code>dir</code> is updated. What invariant does that keep?</li>
|
||||
<li><code>advance</code> does not place a new apple after eating. Why is it a good idea to keep that out of <code>advance</code>?</li>
|
||||
<li>Why are the fixture boards 5 x 5 rather than the real 20 x 20? What does that tell you about storing <code>width</code> and <code>height</code> in the state?</li>
|
||||
</ol>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<!-- 2. Text + video -->
|
||||
<template id="tpl-video">
|
||||
<article class="wide">
|
||||
<p class="chap">Section 1: Introduction to Docker</p>
|
||||
<h1 class="title">Getting Started with Containerization</h1>
|
||||
<p class="source"><strong>Video 1.2</strong>, 0:48. Imported from a Packt video package on 28 October 2026; titles, order and files come from the package.</p>
|
||||
<div class="player" style="margin-top:22px"><video id="vid" controls preload="metadata" poster="video1_2-poster.jpg" src="docker-1-2.mp4"></video></div>
|
||||
<div class="split">
|
||||
<section>
|
||||
<div class="blockhead"><h2>Your notes</h2><button class="btn sm" id="stamp">Insert time <span class="tnum" id="stampT">0:00</span></button></div>
|
||||
<textarea class="notes" id="notesTa" aria-label="Your notes for this clip" placeholder="Only you see these. Insert time adds a link back to this point in the clip."></textarea>
|
||||
<div class="prose" style="margin-top:22px; font-size:calc(var(--ps) * .9)">
|
||||
<p>This clip sets up the question the rest of the section answers: why run an application in a container at all, rather than straight on a server or in a virtual machine. Clip 1.3 then opens the box and shows the two kernel features that make a container possible.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="blockhead"><h2>Files for this section</h2><span>Section 1</span></div>
|
||||
<ul class="rows" id="attach1"></ul>
|
||||
<p class="source">Section 1 has no code in the companion repo.</p>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<!-- 3. Slides -->
|
||||
<template id="tpl-slides">
|
||||
<article class="wide">
|
||||
<p class="chap">Module 2</p>
|
||||
<h1 class="title">Best Practices for Writing Functions</h1>
|
||||
<p class="source"><strong>Elle Krout</strong>, Pluralsight. Slide deck, 59 pages, imported from PDF.</p>
|
||||
<figure class="stage" id="stage" style="margin:22px 0 0"><img id="slideImg" alt=""></figure>
|
||||
<div class="deckbar">
|
||||
<button class="btn sm" id="sPrev" aria-label="Previous slide"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M7.5 2.5L4 6l3.5 3.5"/></svg></button>
|
||||
<span class="count tnum" id="sCount" aria-live="polite">1 / 59</span>
|
||||
<button class="btn sm" id="sNext" aria-label="Next slide"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M4.5 2.5L8 6 4.5 9.5"/></svg></button>
|
||||
<span class="gap"></span>
|
||||
<span class="source" style="margin:0">Arrow keys change slides</span>
|
||||
<button class="btn sm" id="sFull"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1.5 4.5v-3h3M7.5 1.5h3v3M10.5 7.5v3h-3M4.5 10.5h-3v-3"/></svg>Fullscreen</button>
|
||||
</div>
|
||||
<div class="thumbs" id="thumbs" role="group" aria-label="Slides"></div>
|
||||
<div class="col" style="margin-top:40px">
|
||||
<div class="prose">
|
||||
<h2 style="margin-top:0">Notes for this deck</h2>
|
||||
<p>The deck opens the module with one rule, <strong>keep code small</strong>, and then takes it apart three ways: a function should serve a single purpose, it should be idempotent, and it should depend on as little as possible.</p>
|
||||
<p>Slides 5 to 8 return to the same three boxes and fill one at a time. The point on slide 5 is the easiest to miss in practice: each bit of pathing (one route, one event source) should be its own function, so that a change to one path can never break another.</p>
|
||||
<p>Idempotent is the one to check in your own code. Serverless platforms retry: a function that runs twice for the same event must leave the world as if it had run once.</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<!-- 4. Text + code files -->
|
||||
<template id="tpl-code">
|
||||
<article class="wide"><div class="col">
|
||||
<p class="chap">Section 8: Containerizing Applications (Sample Project Using Python)</p>
|
||||
<h1 class="title">Writing the Dockerfile</h1>
|
||||
<p class="source"><strong>Text lesson</strong> between clips 8.2 and 8.4. Files come from the course’s companion repo.</p>
|
||||
<div class="prose" style="margin-top:28px">
|
||||
<p>NyxAcademy is the small Django site this section uses as its sample project. Started with <code>python manage.py runserver</code>, it works on your laptop because your laptop already has the right Python, the right Django and a database file next to the code. A container has none of that until the Dockerfile says so.</p>
|
||||
<p>The Dockerfile in the sample repo does three things in order: start from a slim Python image, install Django, copy the project in. Its last line hands over to <code>startup.sh</code>, which prepares the database and the static files every time the container starts, and then runs the server.</p>
|
||||
<p>Read both files side by side before you build. Notice that the server listens on <code>0.0.0.0</code>, not <code>127.0.0.1</code>: inside a container, localhost is the container itself, and the published port could never reach it.</p>
|
||||
</div></div>
|
||||
<div class="blockhead" style="margin-top:34px"><h2>Files for this section</h2><button class="btn sm" id="zip"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M6 1.5v6.5M3.2 5.5L6 8.2l2.8-2.7M1.5 10.5h9"/></svg>Download files for this section (zip)</button></div>
|
||||
<div class="files">
|
||||
<div class="tree" id="tree"></div>
|
||||
<div class="viewer">
|
||||
<div class="tabs" role="tablist" id="tabs"></div>
|
||||
<div class="path" id="fpath"></div>
|
||||
<pre class="code" id="fcode"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col"><div class="prose">
|
||||
<h2>Run it on your machine</h2>
|
||||
<p>From the folder you unzipped, build the image and start a container. Then open <code>localhost:8000</code>.</p>
|
||||
</div>
|
||||
<div class="cmd"><pre class="code" id="cmds">cd "S08 - Containerizing Applications/NyxAcademy-main"
|
||||
docker build -f Dockerfile.example -t nyxacademy .
|
||||
docker run --rm -p 8000:8000 nyxacademy</pre><button class="btn sm" data-copy="cmds">Copy</button></div>
|
||||
<div class="blockhead" style="margin-top:30px"><h2>Attachments</h2><span>Whole course</span></div>
|
||||
<ul class="rows" id="attach8"></ul>
|
||||
</div></article>
|
||||
</template>
|
||||
|
||||
<!-- 5. Exercise -->
|
||||
<template id="tpl-exercise">
|
||||
<article class="wide">
|
||||
<div class="ex">
|
||||
<div>
|
||||
<p class="chap">Module 3: Pure game rules</p>
|
||||
<h1 class="title">Exercise 3.1: <code style="font-size:.9em">move</code></h1>
|
||||
<div class="prose" style="margin-top:20px">
|
||||
<p>Return the cell one step from <code>p</code> in direction <code>dir</code>, where <code>dir</code> is <code>'Up'</code>, <code>'Down'</code>, <code>'Left'</code> or <code>'Right'</code>. The board’s <code>y</code> grows downward, so <code>Up</code> is <code>y - 1</code>.</p>
|
||||
<pre class="code">move({x: 2, y: 2}, 'Up') -> {x: 2, y: 1}
|
||||
move({x: 4, y: 2}, 'Right') -> {x: 5, y: 2}</pre>
|
||||
<p>It does <strong>not</strong> wrap around the edges: deciding what “off the board” means is <code>inBounds</code>’s job. And it is a rule, so it builds a new point; the point you were given must come back untouched.</p>
|
||||
<p><code>delta(dir)</code> and <code>point(x, y)</code> from Module 2 are already imported.</p>
|
||||
</div>
|
||||
<h2 class="sec-h" style="margin-top:26px">Hints</h2>
|
||||
<details class="fold"><summary><svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M3.5 2l3 3-3 3"/></svg>Nudge<span class="tier">1 of 3</span></summary><div class="in prose"><p><code>delta(dir)</code> gives you a point to add.</p></div></details>
|
||||
<details class="fold"><summary><svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M3.5 2l3 3-3 3"/></svg>Stronger hint<span class="tier">2 of 3</span></summary><div class="in prose"><p>Build a new point whose <code>x</code> is <code>p.x + d.x</code> and whose <code>y</code> is <code>p.y + d.y</code>, where <code>d = delta(dir)</code>.</p></div></details>
|
||||
<details class="fold"><summary><svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M3.5 2l3 3-3 3"/></svg>Near-solution<span class="tier">try the others first</span></summary><div class="in prose"><pre class="code">export const move = (p, dir) => { const d = delta(dir); return point(___, ___) }</pre></div></details>
|
||||
<p class="local"><strong>Runs in your browser — the full project runs on your machine.</strong> This page checks <code style="display:inline">move</code> against the five fixture cases plus a purity check. The whole module, in any track:<code>cd js && node --test "test/m03-*.test.js"</code></p>
|
||||
</div>
|
||||
<div class="edit">
|
||||
<div class="edit-bar"><span class="f">rules.js</span><button class="btn sm quiet" id="reset">Reset</button><button class="btn sm primary" id="run">Run tests <kbd>Ctrl ↵</kbd></button></div>
|
||||
<div class="editor"><pre class="gutter" id="gutter" aria-hidden="true"></pre><textarea id="code" spellcheck="false" aria-label="Code editor, rules.js"></textarea></div>
|
||||
<div class="results" id="results"></div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<!-- 6. Library -->
|
||||
<template id="tpl-home">
|
||||
<article class="wide" style="max-width:1080px">
|
||||
<h1 class="lib-h">Library</h1>
|
||||
<p class="lib-sub">Four courses on this server.</p>
|
||||
<h2 class="sec-h">Continue</h2>
|
||||
<div class="continue">
|
||||
<span class="cover" style="--cw:56px;--cbg:#272319;--cfg:#f1b44c">PS</span>
|
||||
<div><div class="t">Pure Snake</div><div class="s" id="contS"></div>
|
||||
<div class="prog"><span class="bar"><i id="contBar"></i></span><span class="tnum" id="contN"></span></div></div>
|
||||
<a class="btn primary" id="contBtn">Continue <kbd>]</kbd></a>
|
||||
</div>
|
||||
<h2 class="sec-h">Courses</h2>
|
||||
<div class="lib-cols"><span>Course</span><span>Lessons</span><span>Contains</span><span>Progress</span></div>
|
||||
<div class="lib" id="lib"></div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<template id="tpl-generic">
|
||||
<article class="col">
|
||||
<p class="chap" id="gChap"></p>
|
||||
<h1 class="title" id="gTitle"></h1>
|
||||
<div class="prose" style="margin-top:24px"><p style="color:var(--faint)">This lesson is not drawn in the mockup. The six lesson types are in the strip at the top; <kbd>[</kbd> and <kbd>]</kbd> keep moving through the course.</p></div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const ICON = {
|
||||
chev:'<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M3.5 2l3 3-3 3"/></svg>',
|
||||
check:'<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M3 8.5l3.2 3L13 4.5"/></svg>',
|
||||
file:'<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><path d="M4 1.5h5.5L13 5v9.5H4z"/><path d="M9.5 1.5V5H13"/></svg>',
|
||||
dir:'<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><path d="M1.5 3.5h5l1.5 1.5h6.5v8.5h-13z"/></svg>',
|
||||
pdf:'<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><path d="M3.5 1.5h6L13 5v9.5H3.5z"/><path d="M5.5 9h5M5.5 11.5h3.5"/></svg>'
|
||||
};
|
||||
const $ = (s, r = document) => r.querySelector(s);
|
||||
const esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
|
||||
/* ---------- course manifests (what an import or a repo would provide) ---------- */
|
||||
const SNAKE_MODS = ['Setup and how to use this course','Purity and immutability','The game as data','Pure game rules','Randomness without side effects','Folding time','Rendering as data','The IO type','Event streams','Wiring it together','Extensions, all pure','Capstone: Pong, by the method'];
|
||||
const SNAKE_EX = [0,4,4,4,3,3,3,4,3,4,3,1];
|
||||
const M3_EX = ['move','inBounds','turn','advance'];
|
||||
const snakeOutline = SNAKE_MODS.map((t, n) => {
|
||||
const ls = [{id:`s${n}-r`, t, d:'Text', view: n === 3 ? 'text' : null}];
|
||||
for (let k = 1; k <= SNAKE_EX[n]; k++) ls.push({id:`s${n}-e${k}`, t:`Exercise ${n}.${k}` + (n === 3 ? `: ${M3_EX[k-1]}` : ''), d:'Exercise', view: n === 3 && k === 1 ? 'exercise' : null});
|
||||
if (n > 0) ls.push({id:`s${n}-q`, t:'Check your understanding', d:'Text'});
|
||||
return {n, t, lessons: ls};
|
||||
});
|
||||
|
||||
const clips = (s, list) => list.map(([t, d, extra], i) => Object.assign({id:`d${s}-${i+1}`, t:`${s}.${i+1} ${t}`, d}, extra || {}));
|
||||
const STD = (s, mid) => clips(s, [['Topic Introduction','1:08'], ...mid, ['Summary','1:22']]);
|
||||
const dockerOutline = [
|
||||
{n:1, t:'Introduction to Docker', lessons: clips(1, [['Course Introduction','2:41'],['Getting Started with Containerization','0:48',{view:'video'}],['Containerization Core Concepts (Namespaces and Cgroups)','6:12'],['Available Container Toolsets','4:05'],['Docker at a Glance','3:37'],['Docker Architecture','5:20'],['Core Docker Objects (Images, Containers, Networks and Volumes)','7:46'],['Demonstration – Official Docker Documentation Walkthrough','6:58'],['Summary','1:52']])},
|
||||
{n:2, t:'Getting Started with Docker', lessons: clips(2, [['Topic Introduction','1:10'],['Docker Installation Methods','4:22'],['Demonstration – Installing Docker Engine on Linux','8:15'],['Docker Playground','3:40'],['Introduction to Docker CLI','6:05'],['Summary','1:20']])},
|
||||
{n:3, t:'Docker Images', lessons: STD(3, [['Image Layers','5:12'],['Demonstration – Pulling and Inspecting Images','7:30']])},
|
||||
{n:4, t:'Docker Containers', lessons: STD(4, [['Container Lifecycle','4:48'],['Demonstration – Running and Managing Containers','9:02']])},
|
||||
{n:5, t:'Building an Image with a Dockerfile', lessons: STD(5, [['Dockerfile Instructions','6:40'],['Demonstration – Building Your First Image','8:11']])},
|
||||
{n:6, t:'Docker Networking', lessons: STD(6, [['Network Drivers','5:55'],['Demonstration – Connecting Containers','7:18']])},
|
||||
{n:7, t:'Docker Volumes', lessons: STD(7, [['Volumes and Bind Mounts','5:03'],['Demonstration – Persisting Data','6:47']])},
|
||||
{n:8, t:'Containerizing Applications (Sample Project Using Python)', lessons: clips(8, [['Topic Introduction','1:05'],['The Sample Project: NyxAcademy','3:30'],['Writing the Dockerfile','Text',{view:'code'}],['Demonstration – Building and Running NyxAcademy','7:45'],['Summary','1:15']])},
|
||||
{n:9, t:'Docker Compose', lessons: STD(9, [['Compose File Basics','5:36'],['Demonstration – Multi-container NyxAcademy','9:20']])}
|
||||
];
|
||||
const serverlessOutline = [
|
||||
{n:1, t:'Course Overview', lessons:[{id:'v1-1', t:'Course overview', d:'4 pages'}]},
|
||||
{n:2, t:'Best Practices for Writing Functions', lessons:[{id:'v2-1', t:'Writing functions', d:'59 pages', view:'slides'}]},
|
||||
{n:3, t:'Best Practices for Serverless Architecture', lessons:[{id:'v3-1', t:'Architecture', d:'42 pages'}]},
|
||||
{n:4, t:'Best Practices for Security and Observability', lessons:[{id:'v4-1', t:'Security and observability', d:'37 pages'}]}
|
||||
];
|
||||
const systemicOutline = [
|
||||
{n:0, t:'Before you start', lessons:[{id:'y0-1', t:'Choose your track: Rust or TypeScript', d:'Text'},{id:'y0-2', t:'The naive engine', d:'Exercise'}]},
|
||||
{n:1, t:'Part I: Hearthstone', lessons:['Cards and zones','Triggers','Death phases','Auras','Text assembly'].map((t,i)=>({id:`y1-${i}`, t, d:'Text'}))},
|
||||
{n:2, t:'Part II: Baba Is You', lessons:['Rules as pushable text','Parsing','Movement','Win and lose'].map((t,i)=>({id:`y2-${i}`, t, d:'Text'}))},
|
||||
{n:3, t:'Part III: SpellGrid', lessons:['Combining both systems','Capstone: SpellGrid'].map((t,i)=>({id:`y3-${i}`, t, d:'Text'}))}
|
||||
];
|
||||
|
||||
const COURSES = {
|
||||
snake:{title:'Pure Snake', mono:'PS', cbg:'#272319', cfg:'#f1b44c', src:'Your course. Tracks: JavaScript, Scala 3, F#', types:['Text','Exercises'], outline:snakeOutline, start:'text'},
|
||||
docker:{title:'Mastering Docker Essentials – Hands-on DevOps', mono:'MD', cbg:'#18202d', cfg:'#6ea8fe', src:'Packt, Thinknyx Technologies. Imported video package', types:['Video','Text','Files'], outline:dockerOutline, start:'video'},
|
||||
serverless:{title:'Best Practices in Serverless', mono:'BS', cbg:'#2a1b24', cfg:'#f2709f', src:'Elle Krout, Pluralsight. Imported slides', types:['Slides'], outline:serverlessOutline, start:'slides'},
|
||||
systemic:{title:'Systemic Design', mono:'SD', cbg:'#192521', cfg:'#86c7a6', src:'Your course. Tracks: Rust, TypeScript', types:['Text','Exercises'], outline:systemicOutline, start:'l/y1-0'}
|
||||
};
|
||||
const VIEWS = {text:['snake','s3-r'], exercise:['snake','s3-e1'], video:['docker','d1-2'], code:['docker','d8-3'], slides:['serverless','v2-1']};
|
||||
|
||||
const done = new Set();
|
||||
snakeOutline.slice(0,3).forEach(m => m.lessons.forEach(l => done.add(l.id)));
|
||||
['d1-1','v1-1','y0-1','y0-2'].forEach(id => done.add(id));
|
||||
|
||||
const flat = c => COURSES[c].outline.flatMap(m => m.lessons);
|
||||
const findLesson = id => { for (const c in COURSES) { const l = flat(c).find(x => x.id === id); if (l) return [c, l]; } return [null, null]; };
|
||||
const href = l => '#' + (l.view || 'l/' + l.id);
|
||||
const pct = (ls) => ls.length ? Math.round(100 * ls.filter(l => done.has(l.id)).length / ls.length) : 0;
|
||||
const cover = (c, cls = '', cw) => `<span class="cover ${cls}" style="--cbg:${COURSES[c].cbg};--cfg:${COURSES[c].cfg}${cw ? ';--cw:' + cw + 'px' : ''}" aria-hidden="true">${COURSES[c].mono}</span>`;
|
||||
|
||||
/* ---------- state ---------- */
|
||||
const S = {course:'snake', lesson:'s3-r', view:'text', lookPref:'reading', open:new Set()};
|
||||
|
||||
/* ---------- sidebar ---------- */
|
||||
function renderSide() {
|
||||
const c = COURSES[S.course], q = $('#search').value.trim().toLowerCase();
|
||||
const all = flat(S.course);
|
||||
let h = `<div class="side-head">${cover(S.course, '', 36)}<div style="flex:1;min-width:0"><h2>${esc(c.title)}</h2><div class="src">${esc(c.src)}</div>
|
||||
<div class="prog"><span class="bar"><i style="width:${pct(all)}%"></i></span><span class="tnum">${pct(all)}%</span></div></div></div>`;
|
||||
let hits = 0;
|
||||
for (const m of c.outline) {
|
||||
const ls = q ? m.lessons.filter(l => l.t.toLowerCase().includes(q)) : m.lessons;
|
||||
if (q && !ls.length && !m.t.toLowerCase().includes(q)) continue;
|
||||
hits++;
|
||||
const cur = m.lessons.some(l => l.id === S.lesson);
|
||||
const open = q || S.open.has(S.course + m.n);
|
||||
const d = m.lessons.filter(l => done.has(l.id)).length, p = pct(m.lessons);
|
||||
h += `<div class="mod${cur ? ' is-current' : ''}"><button class="mod-head" aria-expanded="${!!open}" data-mod="${m.n}"><span class="n tnum">${m.n}</span><span>${esc(m.t)}</span><span class="c tnum">${d}/${m.lessons.length}</span>${ICON.chev}</button>
|
||||
<span class="bar${p === 100 || !cur ? ' dim' : ''}" aria-hidden="true"><i style="width:${p}%"></i></span>`;
|
||||
if (open) h += '<ol class="lessons">' + (q ? ls : m.lessons).map(l => `<li><a class="les${done.has(l.id) ? ' done' : ''}" href="${href(l)}"${l.id === S.lesson ? ' aria-current="page"' : ''}><span class="m">${done.has(l.id) ? ICON.check : ''}</span><span>${esc(l.t)}<span class="sr">${done.has(l.id) ? ', done' : l.id === S.lesson ? ', current' : ''}</span></span><span class="d tnum">${esc(l.d)}</span></a></li>`).join('') + '</ol>';
|
||||
h += '</div>';
|
||||
}
|
||||
if (!hits) h += `<p class="nohit">No lessons match “${esc(q)}”.</p>`;
|
||||
$('#side').innerHTML = h;
|
||||
}
|
||||
$('#side').addEventListener('click', e => {
|
||||
const b = e.target.closest('.mod-head'); if (!b) { if (e.target.closest('.les')) closeSide(); return; }
|
||||
const k = S.course + b.dataset.mod; S.open.has(k) ? S.open.delete(k) : S.open.add(k);
|
||||
renderSide(); $(`.mod-head[data-mod="${b.dataset.mod}"]`).focus();
|
||||
});
|
||||
|
||||
/* ---------- header ---------- */
|
||||
function renderHeader() {
|
||||
$('#courseTile').innerHTML = S.view === 'home' ? `<span style="width:24px;display:grid;place-items:center;color:var(--faint)">${ICON.dir}</span>` : cover(S.course, 'tiny');
|
||||
$('#courseName').textContent = S.view === 'home' ? 'All courses' : COURSES[S.course].title;
|
||||
$('#courseMenu').innerHTML = `<li><a href="#home"${S.view === 'home' ? ' aria-current="page"' : ''}><span style="width:24px;display:grid;place-items:center;color:var(--faint)">${ICON.dir}</span><span><span class="mt">Library</span><span class="ms">All four courses</span></span></a></li>` +
|
||||
Object.entries(COURSES).map(([k, c]) => `<li><a href="#${c.start}"${k === S.course && S.view !== 'home' ? ' aria-current="page"' : ''}>${cover(k, 'tiny')}<span><span class="mt">${esc(c.title)}</span><span class="ms">${esc(c.src.split('.')[0])}</span></span></a></li>`).join('');
|
||||
$('#search').placeholder = S.view === 'home' ? 'Search the library' : 'Search this course';
|
||||
document.querySelectorAll('.review a').forEach(a => a.toggleAttribute('aria-current', a.getAttribute('href') === '#' + S.view));
|
||||
}
|
||||
const menuBtn = $('#courseBtn'), menu = $('#courseMenu');
|
||||
function setMenu(open) { menu.hidden = !open; menuBtn.setAttribute('aria-expanded', open); if (open) menu.querySelector('a').focus(); }
|
||||
menuBtn.addEventListener('click', () => setMenu(menu.hidden));
|
||||
menu.addEventListener('click', e => { if (e.target.closest('a')) setMenu(false); });
|
||||
document.addEventListener('click', e => { if (!menu.hidden && !e.target.closest('.switch')) setMenu(false); });
|
||||
|
||||
function applyLook() {
|
||||
const look = S.view === 'exercise' ? 'work' : S.lookPref;
|
||||
document.body.dataset.look = look;
|
||||
$('#lookReading').setAttribute('aria-pressed', look === 'reading');
|
||||
$('#lookWork').setAttribute('aria-pressed', look === 'work');
|
||||
}
|
||||
$('#lookReading').onclick = () => { S.lookPref = 'reading'; if (S.view === 'exercise') toast('Exercises always use Work. Reading applies to text lessons.'); applyLook(); };
|
||||
$('#lookWork').onclick = () => { S.lookPref = 'work'; applyLook(); };
|
||||
|
||||
$('#search').addEventListener('input', () => S.view === 'home' ? renderLib() : renderSide());
|
||||
$('#outlineBtn').onclick = () => { const y = $('.top').getBoundingClientRect().bottom + 'px'; $('#side').style.top = $('#scrim').style.top = y; const o = document.body.classList.toggle('side-open'); $('#outlineBtn').setAttribute('aria-expanded', o); };
|
||||
function closeSide() { document.body.classList.remove('side-open'); $('#outlineBtn').setAttribute('aria-expanded', false); }
|
||||
$('#scrim').onclick = closeSide;
|
||||
matchMedia('(min-width: 961px)').addEventListener('change', e => { if (e.matches) $('#side').style.top = $('#scrim').style.top = ''; });
|
||||
|
||||
/* ---------- dock ---------- */
|
||||
function neighbours() { const ls = flat(S.course), i = ls.findIndex(l => l.id === S.lesson); return [ls[i-1], ls[i+1]]; }
|
||||
function renderDock() {
|
||||
const home = S.view === 'home'; $('#dock').hidden = home; if (home) return;
|
||||
const [p, n] = neighbours(), isDone = done.has(S.lesson);
|
||||
$('#prev').disabled = !p; $('#prev').title = p ? 'Previous: ' + p.t + ' ([)' : 'First lesson';
|
||||
$('#next').disabled = !n; $('#next').title = n ? 'Next: ' + n.t + ' (])' : 'Last lesson';
|
||||
$('#done').setAttribute('aria-pressed', isDone); $('#doneLbl').textContent = isDone ? 'Done' : 'Mark done';
|
||||
}
|
||||
const go = d => { const n = neighbours()[d]; if (n) location.hash = href(n); };
|
||||
$('#prev').onclick = () => go(0); $('#next').onclick = () => go(1);
|
||||
$('#done').onclick = () => { done.has(S.lesson) ? done.delete(S.lesson) : done.add(S.lesson); renderDock(); renderSide(); };
|
||||
|
||||
let toastT; function toast(msg) { const t = $('#toast'); t.textContent = msg; t.hidden = false; clearTimeout(toastT); toastT = setTimeout(() => t.hidden = true, 2600); }
|
||||
|
||||
/* ---------- router ---------- */
|
||||
function route() {
|
||||
let h = location.hash.slice(1) || 'text';
|
||||
if (document.fullscreenElement) document.exitFullscreen();
|
||||
let course, lesson, view = h, tpl = h;
|
||||
if (VIEWS[h]) [course, lesson] = VIEWS[h];
|
||||
else if (h === 'home') { course = S.course; lesson = null; }
|
||||
else if (h.startsWith('l/')) { [course] = findLesson(h.slice(2)); lesson = h.slice(2); view = 'lesson'; tpl = 'generic'; }
|
||||
if (!course) { location.hash = '#text'; return; }
|
||||
if (course !== S.course) $('#search').value = '';
|
||||
Object.assign(S, {course, lesson, view});
|
||||
document.body.dataset.course = course;
|
||||
document.body.classList.toggle('no-side', view === 'home');
|
||||
if (lesson) { const m = COURSES[course].outline.find(m => m.lessons.some(l => l.id === lesson)); S.open.add(course + m.n); }
|
||||
const v = $('#view'); v.replaceChildren($('#tpl-' + tpl).content.cloneNode(true));
|
||||
(INIT[tpl] || (() => {}))(v);
|
||||
applyLook(); renderHeader(); renderSide(); renderDock(); closeSide();
|
||||
const cur = $('.les[aria-current]'); if (cur) cur.scrollIntoView({block:'nearest'});
|
||||
document.title = (view === 'home' ? 'Library' : findLesson(lesson)[1].t) + ' – Homelab Academy';
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
const INIT = {};
|
||||
INIT.text = v => {
|
||||
const seg = $('[data-trackseg]', v);
|
||||
const sync = () => seg.querySelectorAll('button').forEach(b => b.setAttribute('aria-pressed', b.dataset.tr === document.body.dataset.track));
|
||||
seg.onclick = e => { const b = e.target.closest('button'); if (b) { document.body.dataset.track = b.dataset.tr; sync(); } };
|
||||
sync();
|
||||
};
|
||||
INIT.generic = () => {
|
||||
const [c, l] = findLesson(S.lesson), m = COURSES[c].outline.find(m => m.lessons.includes(l));
|
||||
$('#gChap').textContent = (c === 'docker' ? 'Section ' : c === 'systemic' ? '' : 'Module ') + (c === 'systemic' ? m.t : m.n + ': ' + m.t);
|
||||
$('#gTitle').textContent = l.t;
|
||||
};
|
||||
|
||||
const ATTACH = [['Thinknyx_Docker_CheatSheet.pdf','1.2 MB'],['Thinknyx_Mastering_Docker_Essentials_Handson_DevOps.pdf','8.6 MB']];
|
||||
const attachRows = () => ATTACH.map(([n, s]) => `<li>${ICON.pdf}<a href="#" data-dl="${n}">${n}</a><span class="sz tnum">${s}</span></li>`).join('');
|
||||
document.addEventListener('click', e => {
|
||||
const a = e.target.closest('[data-dl]'); if (a) { e.preventDefault(); toast('Mockup: would download ' + a.dataset.dl); }
|
||||
const c = e.target.closest('[data-copy]'); if (c) copy($('#' + c.dataset.copy).textContent, c);
|
||||
});
|
||||
function copy(text, btn) {
|
||||
const ok = () => { const o = btn.textContent; btn.textContent = 'Copied'; setTimeout(() => btn.textContent = o, 1400); };
|
||||
(navigator.clipboard ? navigator.clipboard.writeText(text) : Promise.reject()).then(ok, () => {
|
||||
const t = document.createElement('textarea'); t.value = text; document.body.append(t); t.select();
|
||||
try { document.execCommand('copy'); ok(); } catch (_) {} t.remove();
|
||||
});
|
||||
}
|
||||
|
||||
INIT.video = v => {
|
||||
$('#attach1', v).innerHTML = attachRows();
|
||||
const vid = $('#vid', v), ta = $('#notesTa', v);
|
||||
const fmt = s => Math.floor(s / 60) + ':' + String(Math.floor(s % 60)).padStart(2, '0');
|
||||
vid.addEventListener('timeupdate', () => $('#stampT').textContent = fmt(vid.currentTime));
|
||||
$('#stamp', v).onclick = () => {
|
||||
const ins = `[${fmt(vid.currentTime)}] `, p = ta.selectionStart;
|
||||
ta.value = ta.value.slice(0, p) + (p && ta.value[p-1] !== '\n' ? '\n' : '') + ins + ta.value.slice(ta.selectionEnd);
|
||||
ta.focus(); ta.selectionStart = ta.selectionEnd = ta.value.indexOf(ins, p) + ins.length;
|
||||
};
|
||||
};
|
||||
|
||||
const SLIDES = 8, PAGES = 59; let slide = 0;
|
||||
INIT.slides = v => {
|
||||
slide = 0;
|
||||
$('#thumbs', v).innerHTML = Array.from({length:SLIDES}, (_, i) => `<button data-s="${i}" aria-label="Slide ${i+1}"><img src="deck-0${i+1}.png" alt="" loading="lazy"></button>`).join('') +
|
||||
`<span class="more">Pages 9–${PAGES} not in this mockup</span>`;
|
||||
$('#thumbs', v).onclick = e => { const b = e.target.closest('[data-s]'); if (b) showSlide(+b.dataset.s); };
|
||||
$('#sPrev', v).onclick = () => showSlide(slide - 1);
|
||||
$('#sNext', v).onclick = () => showSlide(slide + 1);
|
||||
$('#sFull', v).onclick = () => document.fullscreenElement ? document.exitFullscreen() : $('#stage').requestFullscreen().catch(() => toast('Fullscreen is not available here'));
|
||||
showSlide(0);
|
||||
};
|
||||
const SLIDE_ALT = ['Title: Best Practices in Serverless, Elle Krout','Section: Best Practices for Writing Functions','Keep Code Small: single purpose, idempotent, limit dependencies','Keep Code Small','Keep Code Small: each bit of pathing should be its own function','Keep Code Small','Keep Code Small','Keep Code Small: reuse functions'];
|
||||
function showSlide(i) {
|
||||
if (i < 0 || i >= SLIDES) { if (i >= SLIDES) toast(`Pages ${SLIDES + 1}–${PAGES} are not part of this mockup`); return; }
|
||||
slide = i; const img = $('#slideImg');
|
||||
img.src = `deck-0${i+1}.png`; img.alt = `Slide ${i+1}: ${SLIDE_ALT[i]}`;
|
||||
$('#sCount').textContent = `${i+1} / ${PAGES}`;
|
||||
$('#sPrev').disabled = i === 0;
|
||||
document.querySelectorAll('#thumbs [data-s]').forEach(b => b.setAttribute('aria-current', +b.dataset.s === i));
|
||||
const cur = $(`#thumbs [data-s="${i}"]`); if (cur) cur.scrollIntoView({block:'nearest', inline:'nearest'});
|
||||
}
|
||||
|
||||
const REPO = 'S08 - Containerizing Applications/NyxAcademy-main/';
|
||||
const FILES = {
|
||||
'Dockerfile.example':{lang:'docker', src:`# NyxAcademy: the Django sample app from Section 8
|
||||
FROM python:3.11-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \\
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip install --no-cache-dir "django>=4.2,<5"
|
||||
|
||||
COPY . .
|
||||
RUN chmod +x startup.sh
|
||||
|
||||
EXPOSE 8000
|
||||
CMD ["./startup.sh"]`},
|
||||
'startup.sh':{lang:'sh', src:`#!/bin/sh
|
||||
# Runs every time the container starts
|
||||
set -e
|
||||
|
||||
python manage.py migrate --noinput
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
# 0.0.0.0, not 127.0.0.1: the published port must reach it
|
||||
exec python manage.py runserver 0.0.0.0:8000`},
|
||||
'NyxAcademy/settings.py':{lang:'py', src:`# Excerpt: the lines the container depends on
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
DEBUG = os.environ.get("DJANGO_DEBUG", "1") == "1"
|
||||
ALLOWED_HOSTS = ["localhost", "127.0.0.1", "0.0.0.0"]
|
||||
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = BASE_DIR / "staticfiles"`}
|
||||
};
|
||||
function hl(src, lang) {
|
||||
return esc(src).split('\n').map(line => {
|
||||
let l = line;
|
||||
if (/^\s*#/.test(l)) l = `<span class="c">${l}</span>`;
|
||||
else if (lang === 'docker') l = l.replace(/^(FROM|RUN|COPY|WORKDIR|ENV|EXPOSE|CMD)\b/, '<span class="k">$1</span>');
|
||||
else if (lang === 'py') l = l.replace(/\s(#.*)$/, ' <span class="c">$1</span>');
|
||||
return `<span class="ln">${l || ' '}</span>`;
|
||||
}).join('');
|
||||
}
|
||||
INIT.code = v => {
|
||||
$('#attach8', v).innerHTML = attachRows();
|
||||
const f = (p, cls = '') => `<button class="${cls}" data-f="${p}">${ICON.file}${p.split('/').pop()}</button>`;
|
||||
const s = n => `<span class="f">${ICON.file}${n}</span>`;
|
||||
$('#tree', v).innerHTML = `<div class="dir">${ICON.dir}NyxAcademy-main</div>${f('Dockerfile.example')}${s('manage.py')}${f('startup.sh')}
|
||||
<div class="dir sub">${ICON.dir}NyxAcademy</div>${f('NyxAcademy/settings.py', 'l2')}<span class="f" style="padding-left:40px">${ICON.file}urls.py</span>
|
||||
<div class="dir sub">${ICON.dir}templates</div><div class="dir sub">${ICON.dir}static</div>`;
|
||||
$('#tabs', v).innerHTML = Object.keys(FILES).map(p => `<button role="tab" data-f="${p}">${p.split('/').pop()}</button>`).join('') + '<button class="btn sm quiet" id="copyFile" style="font-family:var(--sans)">Copy</button>';
|
||||
v.querySelector('.files').addEventListener('click', e => { const b = e.target.closest('[data-f]'); if (b) openFile(b.dataset.f); });
|
||||
$('#copyFile', v).onclick = e => copy(FILES[openFile.cur].src, e.currentTarget);
|
||||
$('#zip', v).onclick = () => toast('Mockup: would download “S08 - Containerizing Applications.zip” (7 files, 41 KB)');
|
||||
openFile('Dockerfile.example');
|
||||
};
|
||||
function openFile(p) {
|
||||
openFile.cur = p;
|
||||
document.querySelectorAll('#tabs [data-f]').forEach(b => b.setAttribute('aria-selected', b.dataset.f === p));
|
||||
document.querySelectorAll('#tree [data-f]').forEach(b => b.setAttribute('aria-current', b.dataset.f === p));
|
||||
$('#fpath').textContent = 'From the sample repo: ' + REPO + p;
|
||||
$('#fcode').innerHTML = hl(FILES[p].src, FILES[p].lang);
|
||||
}
|
||||
|
||||
/* ---------- exercise: tests run in this page ---------- */
|
||||
const STUB = `import { delta, point } from './model.js'
|
||||
|
||||
// Exercise 3.1: the cell one step from p in direction dir.
|
||||
// y grows downward, so 'Up' is y - 1.
|
||||
export const move = (p, dir) => {
|
||||
const d = delta(dir)
|
||||
p.x = p.x + d.x
|
||||
p.y = p.y + d.y
|
||||
return p
|
||||
}
|
||||
`;
|
||||
let draft = STUB;
|
||||
const fmtP = o => o && typeof o === 'object' ? `{x: ${o.x}, y: ${o.y}}` : String(o);
|
||||
function runTests(src) {
|
||||
const prelude = "const delta = d => ({Up:{x:0,y:-1}, Down:{x:0,y:1}, Left:{x:-1,y:0}, Right:{x:1,y:0}})[d];\nconst point = (x, y) => ({x, y});\n";
|
||||
const body = src.replace(/^\s*import .*$/gm, '').replace(/\bexport\s+/g, '');
|
||||
let move;
|
||||
try { move = new Function(prelude + body + '\n;return typeof move === "function" ? move : undefined;')(); }
|
||||
catch (e) { return {error: e.name + ': ' + e.message}; }
|
||||
if (!move) return {error: 'No function named move. Keep the line “export const move = (p, dir) => …”.'};
|
||||
const cases = [['move-up',{x:2,y:2},'Up',{x:2,y:1}],['move-down',{x:2,y:2},'Down',{x:2,y:3}],['move-left',{x:2,y:2},'Left',{x:1,y:2}],['move-right',{x:2,y:2},'Right',{x:3,y:2}],['move-does-not-wrap-at-the-edge',{x:4,y:2},'Right',{x:5,y:2}]];
|
||||
const out = cases.map(([name, p, d, exp]) => {
|
||||
try { const got = move({...p}, d); return {name, call:`move(${fmtP(p)}, '${d}')`, ok: !!got && got.x === exp.x && got.y === exp.y, exp: fmtP(exp), got: fmtP(got)}; }
|
||||
catch (e) { return {name, call:`move(${fmtP(p)}, '${d}')`, ok:false, exp: fmtP(exp), got: 'threw ' + e.message}; }
|
||||
});
|
||||
const input = {x:2, y:2}; let r;
|
||||
try { r = move(input, 'Up'); } catch (e) { r = e; }
|
||||
const pure = input.x === 2 && input.y === 2 && r !== input;
|
||||
out.push({name:'move-leaves-its-input-unchanged', call:"const p = {x: 2, y: 2}; move(p, 'Up')", ok: pure, exp:'p is still {x: 2, y: 2}', got: r === input ? `p became ${fmtP(input)}, and move returned p itself` : `p is ${fmtP(input)}`});
|
||||
return {out};
|
||||
}
|
||||
function showResults(res) {
|
||||
const el = $('#results');
|
||||
if (!res) { el.innerHTML = `<div class="res-sum"><span>Not run yet. Six checks: five fixture cases and one purity check.</span></div>`; return; }
|
||||
if (res.error) { el.innerHTML = `<div class="res-sum"><span><b>The code did not load.</b> ${esc(res.error)}</span></div>`; return; }
|
||||
const n = res.out.filter(t => t.ok).length, all = n === res.out.length;
|
||||
el.innerHTML = `<div class="res-sum" aria-live="polite"><span><b class="tnum">${n} of ${res.out.length}</b> passed${all ? '. Mark the exercise done when you are ready.' : ''}</span></div><ul class="res-list">` +
|
||||
res.out.map(t => `<li class="${t.ok ? 'pass' : 'fail'}"><div class="res-row"><span class="s">${t.ok ? 'Pass' : 'Fail'}</span><code>${esc(t.name)}</code></div>` +
|
||||
(t.ok ? '' : `<dl class="diff"><dt>Call</dt><dd>${esc(t.call)}</dd><dt>Expected</dt><dd>${esc(t.exp)}</dd><dt>Actual</dt><dd>${esc(t.got)}</dd></dl>`) + '</li>').join('') + '</ul>';
|
||||
}
|
||||
INIT.exercise = v => {
|
||||
const ta = $('#code', v), gut = $('#gutter', v);
|
||||
const lines = () => { gut.textContent = ta.value.split('\n').map((_, i) => i + 1).join('\n'); gut.scrollTop = ta.scrollTop; };
|
||||
ta.value = draft; lines();
|
||||
ta.addEventListener('input', () => { draft = ta.value; lines(); });
|
||||
ta.addEventListener('scroll', () => gut.scrollTop = ta.scrollTop);
|
||||
ta.addEventListener('keydown', e => {
|
||||
if (e.key === 'Tab' && !e.shiftKey) { e.preventDefault(); ta.setRangeText(' ', ta.selectionStart, ta.selectionEnd, 'end'); draft = ta.value; lines(); }
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); $('#run').click(); }
|
||||
if (e.key === 'Escape') ta.blur();
|
||||
});
|
||||
$('#run', v).onclick = () => showResults(runTests(ta.value));
|
||||
$('#reset', v).onclick = () => { ta.value = draft = STUB; lines(); showResults(null); toast('Code reset to the starting stub'); };
|
||||
showResults(null);
|
||||
};
|
||||
|
||||
/* ---------- library ---------- */
|
||||
function renderLib() {
|
||||
const q = $('#search').value.trim().toLowerCase();
|
||||
const rows = Object.entries(COURSES).filter(([, c]) => !q || (c.title + c.src).toLowerCase().includes(q));
|
||||
$('#lib').innerHTML = rows.map(([k, c]) => {
|
||||
const ls = flat(k), p = pct(ls);
|
||||
return `<a class="lib-row" href="#${c.start}"><span class="lib-main">${cover(k, '', 44)}<span style="min-width:0"><span class="lt">${esc(c.title)}</span><span class="ls" style="display:block">${esc(c.src)}</span></span></span>
|
||||
<span class="lnum tnum">${ls.length}</span><span class="tags">${c.types.map(t => `<span class="tag">${t}</span>`).join('')}</span>
|
||||
<span class="prog"><span class="bar${p === 0 ? '' : ''}"><i style="width:${p}%"></i></span><span class="tnum" style="min-width:3ch;text-align:right">${p}%</span></span></a>`;
|
||||
}).join('') || `<p class="nohit">No courses match “${esc(q)}”.</p>`;
|
||||
}
|
||||
function nextUp() { const m = snakeOutline.find(m => m.lessons.some(l => !done.has(l.id))), l = m.lessons.find(l => !done.has(l.id)); return [m, l]; }
|
||||
INIT.home = () => {
|
||||
const [m, l] = nextUp(), d = m.lessons.filter(x => done.has(x.id)).length;
|
||||
$('#contS').textContent = `Module ${m.n}, ${m.t}. Next up: ${l.t === m.t ? 'the reading' : l.t}`;
|
||||
$('#contBar').style.width = pct(m.lessons) + '%';
|
||||
$('#contN').textContent = `${d} of ${m.lessons.length} in this module`;
|
||||
$('#contBtn').href = href(l);
|
||||
renderLib();
|
||||
};
|
||||
|
||||
/* ---------- keyboard ---------- */
|
||||
document.addEventListener('keydown', e => {
|
||||
const t = e.target, typing = t.closest && t.closest('input, textarea, [contenteditable]');
|
||||
if (e.key === 'Escape') { if (!menu.hidden) { setMenu(false); menuBtn.focus(); } else if (t.id === 'search') { t.value = ''; t.dispatchEvent(new Event('input')); t.blur(); } closeSide(); return; }
|
||||
if (typing || e.ctrlKey || e.metaKey || e.altKey) return;
|
||||
if (e.key === ']') { e.preventDefault(); S.view === 'home' ? location.hash = href(nextUp()[1]) : go(1); }
|
||||
else if (e.key === '[') { e.preventDefault(); go(0); }
|
||||
else if (e.key === '/') { e.preventDefault(); const s = $('#search'); if (getComputedStyle(s.closest('.search')).display === 'none') return; s.focus(); }
|
||||
else if (S.view === 'slides' && (e.key === 'ArrowRight' || e.key === 'ArrowLeft') && !(t.closest && t.closest('video'))) { e.preventDefault(); showSlide(slide + (e.key === 'ArrowRight' ? 1 : -1)); }
|
||||
});
|
||||
|
||||
window.addEventListener('hashchange', route);
|
||||
route();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,333 @@
|
||||
# STCourses: design
|
||||
|
||||
Date: 2026-10-30. Status: draft for approval.
|
||||
|
||||
STCourses is a self-hosted course platform that one person or one organisation runs for its own learners. It mixes three things: the reading comfort of Packt's book reader, the structured video courses of Pluralsight, and boot.dev's habit of learning by doing small exercises. Course content lives in plain files that are the single source of truth. A browser studio edits those files. AI features are optional and off by default.
|
||||
|
||||
Background research on existing open-source platforms is in `reports/Open source course platforms.md`. None of them fits: they are too heavy (Open edX), too dated (Moodle), put key features in paid tiers (LearnHouse), or keep content in a database. That is why this is a new build.
|
||||
|
||||
## 1. Who it is for, and why
|
||||
|
||||
- **Homelabbers.** Host your own courses, plus course material you already own, such as a Packt video package or a Pluralsight slide PDF, in one place that tracks your progress.
|
||||
- **Universities and companies.** Run your own catalogue of self-made courses, with your own sign-in, on your own hardware.
|
||||
- **Course authors.** Write in Markdown in a git repo, or in a browser studio. Both edit the same files.
|
||||
|
||||
Each install has **one owner** (a person or an organisation). It is not multi-tenant.
|
||||
|
||||
The two reference courses are the quality bar. Everything in this design must work for them without rewriting them:
|
||||
|
||||
- `/home/user/functional-programming-course` (Pure Snake): 12 modules, three language tracks (JS, Scala 3, F#) that share JSON fixtures, stub files the learner fills in, three hint tiers per exercise, "check your understanding" questions, and a `MENTOR.md` that tells an AI tutor how to behave.
|
||||
- `/home/user/systemic-design-course`: Rust and TypeScript tracks, scenario-named tests, a workspace template, module gating, the Observe → Predict → Spec → Build → Name it → Expansion Drop pattern, and an Astro site.
|
||||
|
||||
Real imported material is in `/home/user/example_course_content`:
|
||||
|
||||
- a Packt video package: a zip with about 50 MP4s and an old HTML player whose `index.html` holds the section and clip titles and their order;
|
||||
- the companion code repo, with one folder per section, PDF slides, a cheat sheet and a Django sample app;
|
||||
- a 59-page Pluralsight slide PDF.
|
||||
|
||||
This material is licensed to the user. Nothing from it is committed to this repo.
|
||||
|
||||
## 2. Goals, non-goals, anti-goals
|
||||
|
||||
**Goals**
|
||||
|
||||
1. Comfortable lessons of several types, in one consistent interface:
|
||||
- long-form text;
|
||||
- text with video;
|
||||
- slides attached to the video or section they belong to;
|
||||
- text with downloadable code files;
|
||||
- small in-browser exercises.
|
||||
2. Course content is files on disk, versioned with git. The database never holds course content.
|
||||
3. Importing purchased material produces an ordinary course folder. Supported sources: video packages, slide PDFs and code repos or zips.
|
||||
4. A browser studio that edits course files and commits them.
|
||||
5. Opt-in AI:
|
||||
- a course-aware assistant for learners;
|
||||
- a studio generator that drafts documents, exercises and knowledge checks.
|
||||
6. Easy self-hosting: one Docker image, SQLite by default, and external PostgreSQL as an option.
|
||||
|
||||
**Non-goals for now**
|
||||
|
||||
- Payments, storefronts, marketplaces, prices, ratings and revenue share.
|
||||
- Multi-tenancy, meaning several separate organisations on one install.
|
||||
- Running full course projects on the server. Learners run projects on their own machine. Only small exercises run in the browser.
|
||||
- Mandatory video transcoding and DRM.
|
||||
- SCORM, xAPI, cmi5 and LTI. They may come later, for example LTI so universities can embed courses. Nothing in this design should block them.
|
||||
|
||||
**Anti-goals** (failure, even if it technically works)
|
||||
|
||||
- Course content stored only in database tables, or in a proprietary format.
|
||||
- Idle memory use above 2 GB.
|
||||
- The visual style the user has rejected: glowing gradients, card grids, decorative accents, and interface elements themed to one course's subject (for example a snake-shaped progress bar).
|
||||
- An AI mentor that silently hands out solutions when the course asks it not to, or an AI that publishes generated material without an author reviewing it.
|
||||
- Unofficial redistribution of licensed imports. Imported courses are private by default.
|
||||
|
||||
## 3. Constraints and decided technology
|
||||
|
||||
These were decided with the user:
|
||||
|
||||
- **Runtime:** Bun, following the conventions in the `CLAUDE.md` that `bun init` generates:
|
||||
- serving and routing: `Bun.serve()` with routes and WebSockets, and HTML imports for the frontend;
|
||||
- databases: `bun:sqlite` for SQLite and `Bun.sql` for Postgres;
|
||||
- tooling: `bun test`, `bun install`, `bunx`, `Bun.file`, and `` Bun.$ `` for shell commands;
|
||||
- not used: Express, Vite, dotenv, `pg`, `better-sqlite3`.
|
||||
- **Language:** TypeScript throughout.
|
||||
- **Frontend:** a React single-page app served by `Bun.serve` through HTML imports. Next.js was considered and rejected. Its memory use leaves less of the 2 GB budget, it duplicates Bun's own server and bundler, and most pages are interactive and behind sign-in. If public pages need to be visible to search engines later, render just those pages with React's `renderToReadableStream` inside `Bun.serve`.
|
||||
- **Database:**
|
||||
- SQLite by default;
|
||||
- external PostgreSQL through one connection-string setting;
|
||||
- user data only (section 7);
|
||||
- both databases are supported and tested in CI.
|
||||
- **Course history:** git, through the `git` command line.
|
||||
- **Sign-in:** OAuth/OIDC, SAML, social logins and username/password. The user suggested Better Auth (or Passport.js).
|
||||
- **Memory:** at most 2 GB at idle for the whole deployment, including the database when Postgres runs in the same compose file.
|
||||
- **License:** AGPL-3.0.
|
||||
- **Deployment:** one Docker image, plus an optional docker-compose file that adds Postgres.
|
||||
- **Repository:** `https://gitea.lucaswinther.info/lucasw89/STCourses.git`, default branch `main`.
|
||||
|
||||
## 4. The learner experience
|
||||
|
||||
Visual reference: `docs/design/mockups/platform-v1.html`. It is a click-through mockup that the user approved in direction. Its media files are not committed, so the video and slides appear blank when it is opened from this repo.
|
||||
|
||||
### 4.1 Visual direction (approved)
|
||||
|
||||
- **Theme.**
|
||||
- Dark by default: ground `#12141a`, side surfaces `#181b22`, raised `#232733`, hairlines `#262a33`/`#343947`.
|
||||
- A light theme is designed separately, not created by inverting the dark one.
|
||||
- **Accent.**
|
||||
- One accent colour: amber `#f1b44c`, with a dim variant `#8c7440`.
|
||||
- It is used only for: the current item, progress, and the primary next action.
|
||||
- A course can set its own accent in its manifest. The Docker course in the mockup uses blue `#6ea8fe`.
|
||||
- **Type.**
|
||||
- IBM Plex Sans for all interface text.
|
||||
- Literata only for lesson titles and reading text: about 19px with line height 1.75, in a centred column about 660px wide.
|
||||
- IBM Plex Mono for code.
|
||||
- Fonts are bundled locally, not loaded from Google, so the platform works offline.
|
||||
- **Structure.** Pill-shaped controls, rows separated by thin rules, and thin 4px progress bars. No gradients, glows, card grids, or labels in all caps.
|
||||
- **Course-neutral interface.** A course brings only a cover image (or a generated monogram) and an optional accent colour.
|
||||
- **Reading / Work toggle.** Reading is the roomy serif column. Work is denser sans text in a wider column. Exercise pages always use Work.
|
||||
- **Floating action bar.** A pill at the bottom of the reading column holds "Previous", "Mark done" and "Next lesson".
|
||||
- **Keyboard shortcuts.** Only the expected ones: arrow keys in the slide viewer, Escape to close overlays, and normal Tab focus. No `[`/`]` or `/` shortcuts and no shortcut hints. They may return later as an optional setting.
|
||||
- **Responsive and accessible.** Works down to phone width, with visible focus and respect for reduced-motion settings.
|
||||
|
||||
### 4.2 Library (home)
|
||||
|
||||
The current mockup is "good and simple" but lacks the pull of browsing for your next course, the way Kickstarter browsing feels. The library has these parts:
|
||||
|
||||
- **Featured course.** The owner picks it. It shows a large cover, a one-line pitch and a Start button.
|
||||
- **Continue row.** Lessons you have in progress.
|
||||
- **Browsable rows.** "New in the library", then one row per collection. Each row is a horizontal strip of covers with thin progress bars.
|
||||
- **Collections.** Defined by the owner, for example "Functional programming", "Game design" and "DevOps". A course can belong to several.
|
||||
- **Filters.** By format (text, video, slides, exercises), length, and "in progress".
|
||||
- **Search** across course titles, descriptions and lesson titles.
|
||||
- **Cover images.**
|
||||
- Use the course's own artwork when it has some.
|
||||
- Otherwise use a frame taken from its first video, or its first slide, at import.
|
||||
- Otherwise use a typographic monogram.
|
||||
- **No storefront signals.** No prices, ratings or "bestseller" badges.
|
||||
- **Scale.** It should feel right with 5 courses (a homelab) and with several hundred (a company or university).
|
||||
|
||||
### 4.3 Lesson types
|
||||
|
||||
1. **Text lesson.**
|
||||
- Markdown with headings, code blocks, figures, callouts and folded "think about it first" sections.
|
||||
- Hint tiers render as folded sections, opened in order.
|
||||
- Multi-track courses show a track switcher (JS / Scala 3 / F#) that swaps the code samples and file names.
|
||||
2. **Video lesson.**
|
||||
- A video player (seeking needs HTTP range requests) with a transcript, if one exists, and personal notes.
|
||||
- The notes have an "insert timestamp" action that links back to that moment in the video.
|
||||
3. **Slides belong to something.**
|
||||
- A slide deck is never a standalone lesson. It is attached to the video, lesson or section it belongs to.
|
||||
- On a video lesson, the deck is a companion panel next to or under the video, and you can page through it on its own.
|
||||
- On a section, the deck appears as part of that section.
|
||||
- The viewer has a 16:9 stage, thumbnails, previous/next, arrow keys, fullscreen, and a page counter.
|
||||
- Keeping the slide in step with the video's time is a later nice-to-have.
|
||||
4. **Notes on slides.**
|
||||
- Notes attach to a specific slide page.
|
||||
- Coming back to the slide shows its note.
|
||||
- All notes for a lesson, whether on slides or video timestamps, can be read together as one list.
|
||||
5. **Code files.**
|
||||
- A file tree and tabbed code viewer of the course's companion code for that section, with copy buttons.
|
||||
- "Download files for this section" gives a zip.
|
||||
- A command block shows how to run it locally.
|
||||
- An attachments list holds PDFs and cheat sheets.
|
||||
6. **In-browser exercise.**
|
||||
- Instructions with a small editor (CodeMirror is acceptable).
|
||||
- Tests run in the browser in a sandbox, a Web Worker or sandboxed iframe, never in the page itself.
|
||||
- The first version supports JavaScript and TypeScript. Other languages (Python via Pyodide, and later others) come through a pluggable runner interface.
|
||||
- Results show pass/fail per test, with expected and actual values. Hint tiers and a Reset button are included.
|
||||
- The page shows the local command for running the full project.
|
||||
- Exercises in tracks without a browser runner show "run locally" instead of an editor.
|
||||
|
||||
### 4.4 Progress
|
||||
|
||||
- Per-lesson state: not started, in progress or done. For video, the position reached also counts.
|
||||
- Progress rolls up to modules and to the course.
|
||||
- "Mark done" is manual for text and slides. Video counts as done when watched near the end. An exercise counts as done when all its tests pass.
|
||||
|
||||
## 5. Course format on disk
|
||||
|
||||
Phase 0 (section 10) pins down the exact schema. These principles are fixed:
|
||||
|
||||
- **A course is a folder,** normally its own git repo, under a configured courses directory. It has a manifest file at its root that gives:
|
||||
- id, title, pitch, authors, cover, accent and collections;
|
||||
- visibility, tracks and source/import info;
|
||||
- an ordered list of modules and lessons, or a convention that derives the order from file names.
|
||||
- **Lessons are Markdown files** with a small front matter block (type, title, duration, attachments, linked deck, linked video and track variants). Lesson types are listed in section 4.3.
|
||||
- **Attachments live with the course:** videos, PDFs, images and code. Large binaries may live outside git, in a configured media directory, and be referenced by path and checksum. Phase 0 decides between that and Git LFS.
|
||||
- **Slide decks are PDF files.** The manifest or lesson front matter links each deck to its video, lesson or section. Page images and extracted text are derived caches, not source.
|
||||
- **Exercises** declare the starting code, tests, hint tiers and solution per track. The solution is visible only to authors.
|
||||
- **Mentor rules:** a course may ship AI tutor instructions, such as Pure Snake's `MENTOR.md`, referenced from the manifest.
|
||||
- **Compatibility.**
|
||||
- The two reference courses must load by adding only a manifest and front matter, without moving or rewriting their existing files.
|
||||
- Their existing tooling (test scripts, `tools/*.mjs`) must keep working.
|
||||
- **Derived data lives in a cache directory that can be rebuilt from the files:** search index, slide images and text, transcripts, video posters and AI embeddings.
|
||||
|
||||
## 6. Accounts, roles and visibility
|
||||
|
||||
**Roles** (my proposal):
|
||||
|
||||
- **Owner/admin:** settings, users, sign-in providers, AI configuration and imports.
|
||||
- **Author:** studio access, either to all courses or per course.
|
||||
- **Learner.**
|
||||
|
||||
**Visibility per course, set by the author:**
|
||||
|
||||
- public: anyone, no sign-in;
|
||||
- signed-in: any user;
|
||||
- restricted: named users or groups.
|
||||
|
||||
Imported courses default to restricted, visible only to the importer.
|
||||
|
||||
**Sign-in:**
|
||||
|
||||
- OIDC/OAuth (including social providers), SAML and username/password.
|
||||
- Group membership from SAML/OIDC claims can drive the restricted visibility.
|
||||
|
||||
## 7. Data stored in the database (user data only)
|
||||
|
||||
The database holds:
|
||||
|
||||
- users, sessions, sign-in links, roles and groups;
|
||||
- per-user progress, lesson state and video position;
|
||||
- notes, each attached to a lesson plus an optional video timestamp or slide page;
|
||||
- exercise attempts (latest code per track) and results;
|
||||
- AI settings: the server provider configuration, and each user's own API key stored encrypted (BYOK);
|
||||
- AI conversation history (optional, user-deletable);
|
||||
- library curation: featured course and collections. If the owner prefers, these can instead live in a settings file. That is the builder's call.
|
||||
|
||||
The same schema and migrations run on SQLite and Postgres. Deleting the database loses user data only, never courses.
|
||||
|
||||
## 8. Import
|
||||
|
||||
Import turns material the user owns into an ordinary course folder, writing a manifest and front matter. It never edits the original files; it copies them or references them in place. Sources in the first version:
|
||||
|
||||
- **Packt video package (zip).** Read the player's `index.html` for sections and clip titles and their order, and map them to `videos/videoN_M.mp4`. Each clip becomes a video lesson.
|
||||
- **Companion code repo (zip or git URL).** Folders such as `S08 - Containerizing Applications` are matched to sections, by number and by name. PDFs inside them become decks or attachments.
|
||||
- **Slide PDF (for example from Pluralsight).** It becomes a deck attached to a chosen lesson or section. Its text is extracted for search and AI.
|
||||
- **Folder of videos** (the "point at a folder" homelab case). It becomes one lesson per file, ordered naturally by file name.
|
||||
|
||||
Import runs as a background job with visible progress. It produces cover candidates (a video frame, the first slide). Transcription is an optional, separate job using an external Whisper-style service. The user has `faster-whisper-rs` and `au_whisper_web` locally, which may fit. Transcription is never part of the main process's idle footprint.
|
||||
|
||||
Imported material is licensed to its owner. The platform makes imports restricted by default and never exposes import sources publicly unless the owner changes visibility.
|
||||
|
||||
## 9. Studio and AI
|
||||
|
||||
### 9.1 Studio
|
||||
|
||||
- Browser editing of the manifest, lessons (Markdown with a live preview in the real lesson renderer), deck links, attachments and exercises.
|
||||
- Every save writes files and makes a git commit attributed to the author.
|
||||
- Work can happen on a draft branch and be published by merging.
|
||||
- External edits (VS Code, git pull) appear in the studio. Files remain the truth.
|
||||
- Conflicts between studio edits and external edits must be detected and shown, never silently overwritten.
|
||||
|
||||
### 9.2 Opt-in AI
|
||||
|
||||
AI is off by default and has two ways to be configured:
|
||||
|
||||
- **Server provider, set by the admin** for everyone: Anthropic, any OpenAI-compatible endpoint (including a self-hosted Ollama), or others through a provider interface.
|
||||
- **Bring your own key (BYOK):** a learner or author adds their own key, used only for their own requests.
|
||||
|
||||
**Learner assistant**
|
||||
|
||||
- It is course-aware. It retrieves from the course files, transcripts, slide text, the companion code and the learner's own notes and progress.
|
||||
- It gets tools: the platform's own course and notes tools, plus MCP servers configured by the admin, globally or per course.
|
||||
- It answers questions, explains and quizzes.
|
||||
- **Mentor mode.**
|
||||
- The course's mentor rules (for example "never write the solution") apply by default.
|
||||
- The learner may switch them off.
|
||||
- When they are off, the interface says so plainly, for example "Mentor mode off: I'll give full answers".
|
||||
|
||||
**Studio generator**
|
||||
|
||||
- It drafts documents, exercises (with tests and hint tiers) and knowledge-check questions from material the author can access: videos via transcripts, text, slides and code.
|
||||
- It follows the house style of the reference courses:
|
||||
- Pure Snake's module shape: why, objectives, concepts, a puzzle before the explanation, exercises with nudge / stronger hint / near-solution, and check questions;
|
||||
- Systemic Design's Observe → Predict → Spec → Build → Name it → Expansion Drop cycle;
|
||||
- shared fixtures across tracks.
|
||||
- Output is always files on a draft branch, or marked as drafts, for an author to review. Nothing is published automatically.
|
||||
|
||||
## 10. Delivery approach (decided with the user)
|
||||
|
||||
- **Phase 0, the contract (one agent, sequential).** Produce:
|
||||
- the course format schema and validator;
|
||||
- the database schema and migrations (SQLite and Postgres);
|
||||
- the internal module interfaces;
|
||||
- the repo layout, as one Bun workspace with packages;
|
||||
- the shared design tokens;
|
||||
- manifests for both reference courses;
|
||||
- fixture courses for tests.
|
||||
- **Phase 1, four parallel agents, each in its own git worktree, building against the contract:**
|
||||
1. learner app: library, reader, video and slide viewers, exercises, notes, progress, sign-in;
|
||||
2. importer;
|
||||
3. studio;
|
||||
4. AI: assistant and studio generator.
|
||||
- **Phase 2, integration.** Merge branch by branch. Run everything against Pure Snake, Systemic Design and the real Docker and Serverless material (kept outside the repo). Fix what breaks, and check the 2 GB idle budget.
|
||||
|
||||
## 11. Left to the builder
|
||||
|
||||
These are the builder's call:
|
||||
|
||||
- the exact manifest file format (YAML or JSON) and file layout within the principles in section 5;
|
||||
- the client router (React Router or TanStack Router);
|
||||
- the editor component;
|
||||
- the search engine (SQLite FTS5 against Postgres full-text search, or an in-process index);
|
||||
- the PDF rendering library;
|
||||
- the Markdown pipeline;
|
||||
- how background jobs run, as long as they run in the same process or one small worker;
|
||||
- whether Better Auth fits Bun's database clients directly or needs a thin adapter. If it is a poor fit, choose an equivalent and record why.
|
||||
|
||||
## 12. Decisions settled after the builder review (designer's calls)
|
||||
|
||||
The decisions below were made by the designer, not by the user, to settle questions a fresh builder raised.
|
||||
|
||||
- **Lesson granularity.**
|
||||
- A manifest lesson entry points at a file, or at a file plus a heading anchor.
|
||||
- A Pure Snake module file is one text lesson. Each of its exercises (for example "Exercise 3.1 (move)") is a separate exercise page, addressed by anchor. The hints in the module's `## Hints` section are matched to exercises by their labels.
|
||||
- Progress is tracked per manifest entry, so per exercise.
|
||||
- **Metadata location.**
|
||||
- All course metadata lives in the manifest. Lesson front matter is optional, never required.
|
||||
- This keeps Systemic Design's `site/scripts/sync-content.mjs` and the Astro site working unchanged.
|
||||
- **Module gating** is an optional manifest setting per course: `gating: sequential`. It is off by default. When on, the reader locks a module until the previous one is done. Systemic Design turns it on.
|
||||
- **Git model.**
|
||||
- Learners see the course's published branch (default `main`), read from a separate worktree that the platform manages. Unpublished drafts never reach learners.
|
||||
- The studio works in one worktree per author per draft branch. Publishing merges the draft into the published branch.
|
||||
- The platform watches the published worktree and reloads when it changes, after an external commit or a pull.
|
||||
- Pushing to and pulling from a course's remote happen only when an author clicks "Sync", and only if a remote is configured. Nothing syncs automatically.
|
||||
- Merge conflicts are shown in the studio and resolved by the author. The platform never resolves them automatically.
|
||||
- **Roles and access.**
|
||||
- The roles in section 6 stand.
|
||||
- Only the owner can make an imported course public or signed-in visible.
|
||||
- Restricted visibility uses platform groups, which can be filled from SAML/OIDC group claims.
|
||||
- **Light theme.** Version 1 ships dark only. The design tokens are structured so a light theme can be added later as a second set of values.
|
||||
- **Exercise completion when there is no in-browser runner.** The exercise counts as done when the learner marks it done. A "verify locally" command is shown, and its passing output can be pasted back as optional evidence.
|
||||
|
||||
## 13. Decided by the user after the builder review
|
||||
|
||||
- **The reference courses' exercises run locally in v1.**
|
||||
- Pure Snake and Systemic Design exercises do not run in the browser. Their exercise pages show the instructions, hint tiers, the local test command and "Mark done".
|
||||
- The in-browser runner (section 4.3) only targets new, small exercises authored for it.
|
||||
- TODO (future feature): a richer browser runner for project-style exercises. It needs a `node:test`/`node:assert` shim, a virtual file system for fixtures, multi-file module graphs, and learner code that carries over between modules.
|
||||
- **Manifests for existing course repos live in STCourses, not in those repos (option B).**
|
||||
- The platform supports an overlay manifest stored on the platform side, alongside the course, and pointing at an unmodified course repo.
|
||||
- The manifests for the reference courses are created this way. Their repos are never written to.
|
||||
- A course repo may still contain its own manifest. That is the normal case for courses created in the studio. If both exist, the overlay manifest wins.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Build a new core from proven parts
|
||||
|
||||
**No existing open-source project fits all three audiences (homelabbers, universities, companies) well enough to adopt as-is, and none is a sensible fork target. The best path is to build a new, modern core that is multi-tenant from day one and standards-first, and to assemble it from mature open-source building blocks.** Open edX is the closest functional match. It has a modern React frontend, certificates, Tutor packaging and plugin-based multi-tenancy. It is also the heaviest to operate, is AGPL-licensed, changes its frontend architecture every release until mid-2027, and has deprecated its own ecommerce service. The newer projects each miss something essential. Frappe Learning has the broadest fully open feature set but weak standards support. LearnHouse has the best authoring UX, but payments, SSO, SCORM and multi-org are paid Enterprise features. ClassroomIO has pivoted to B2B customer training. CourseLit has nearly stalled. Moodle is easy to self-host but dated. Canvas's public repository has had no pushes since April 2026. The gaps are what make the product worth building: a combined LTI 1.3 platform and tool, cmi5 and xAPI with an embedded learning record store (LRS, which stores learner activity records), Open Badges 3.0 credentials, a native HLS video pipeline, hands-on labs, connectors to commercial course catalogs, and a "point at a folder" library for homelabbers. Two market events make the timing good: the Coursera–Udemy merger and Pluralsight's lender takeover both raise vendor-continuity risk for organizations that rely on them. Throughout this report, claims that the research notes marked as unverified background knowledge are labelled that way.
|
||||
|
||||
## Twelve candidates exist, but none covers homelab to enterprise
|
||||
|
||||
The candidates fall into three tiers. The first tier is institutional LMSs (Moodle, Open edX, Canvas, Sakai, ILIAS, Chamilo). The second is modern lightweight platforms (Frappe Learning, LearnHouse, ClassroomIO, CourseLit). The third is WordPress plugins (Tutor LMS, LearnPress, Sensei, LifterLMS, MasterStudy) and small niche projects. The table summarizes the state as of 30 October 2026. Cells marked † come from background knowledge that the research did not re-verify in 2026.
|
||||
|
||||
| Platform | License | Stack | Health (Oct 2026) | Multi-tenancy | Standards | Commerce | SSO | Self-host ease |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| **Open edX** | AGPL-3.0 | Django + React MFEs, via Tutor | 8.2k stars, 1,254 commits/yr, Verawood Jul 2026 | eox-tenant plugin (AGPL, niche) | LTI block, SCORM XBlock†, xAPI analytics (Aspects)† | Legacy service deprecated, replacement unclear | SAML permanent in Verawood; OAuth2/OIDC† | Heavy (Docker/K8s, ~8 GB†) |
|
||||
| **Moodle** | GPL-3.0 | PHP monolith | 7.6k stars, 3,228 commits/yr, 5.3 LTS Oct 2026 | Paid Workplace, or open-source IOMAD | LTI 1.3 certified†, SCORM 1.2 strong, 2004 partial† | Payment subsystem; PayPal in core, Stripe via plugin† | OIDC/LDAP in core, SAML via plugin† | Easiest |
|
||||
| **Canvas OSS** | AGPL-3.0 | Rails + React | **No pushes since 30 Apr 2026** | Sub-accounts (not isolated)† | Strong LTI/CC/QTI† | Proprietary add-on† | SAML/LDAP/OIDC/CAS† | Hard; no official Docker image |
|
||||
| **Sakai** | ECL-2.0 (permissive) | Java/Tomcat (JDK 17) | 823 commits/yr, 25.2 | Sites† | Strong LTI 1.3, CC, QTI† | Weak† | Strong SAML/Shibboleth† | Moderate |
|
||||
| **ILIAS** | GPL-3.0 | PHP | 2,188 commits/yr, three supported branches | n/a | Strongest: SCORM 2004, xAPI/cmi5, LTI† | Limited shop† | Strong SAML† | Easy |
|
||||
| **Chamilo 3.0** | GPL-3.0 | Symfony + Vue 3 + Tailwind | 5,950 commits/yr; stable since Sep 2026 | Hierarchical multi-URL | Improved SCORM 2004 runtime | BuyCourses plugin with subscriptions | n/a | Easy |
|
||||
| **Frappe Learning** | AGPL-3.0 | Frappe (Python) + Vue + MariaDB | 3.4k stars, 96 contributors, v2.68.0 | Not documented | SCORM *import* only | Built-in payment gateway | Frappe OAuth† | Easy (`easy-install.py`) |
|
||||
| **LearnHouse** | AGPL-3.0 + Enterprise License | Next.js + FastAPI + Postgres + Yjs | 2.4k stars, 1.4.3; single lead maintainer | **Enterprise only** | SCORM **Enterprise only** | **Enterprise only** | **Enterprise only** | Easy (CLI with backup/doctor) |
|
||||
| **ClassroomIO** | AGPL-3.0 (commercial licenses sold) | SvelteKit + Hono + Postgres | 1.7k stars, very active; pivoted to B2B | Multi-org academies | None found | Paid tiers | Enterprise only | Fragile (MinIO breakage) |
|
||||
| **CourseLit** | AGPL-3.0 | Next.js + MongoDB | **1 commit in 90 days**, 15 contributors | n/a | None found | Stripe | n/a | Weak; paid MediaLit media service |
|
||||
|
||||
Sources for the table: GitHub repositories for [edx-platform](https://github.com/openedx/edx-platform), [moodle](https://github.com/moodle/moodle), [canvas-lms](https://github.com/instructure/canvas-lms), [sakai](https://github.com/sakaiproject/sakai), [ILIAS](https://github.com/ILIAS-eLearning/ILIAS), [chamilo-lms](https://github.com/chamilo/chamilo-lms), [frappe/lms](https://github.com/frappe/lms), [learnhouse](https://github.com/learnhouse/learnhouse), [classroomio](https://github.com/classroomio/classroomio) and [courselit](https://github.com/codelitdev/courselit); the [Verawood dev/op notes](https://docs.openedx.org/en/latest/community/release_notes/verawood/dev_op_release_notes.html); the [Chamilo v3.0.0 notes](https://github.com/chamilo/chamilo-lms/releases/tag/v3.0.0); and [selfhosting.sh](https://selfhosting.sh/best/education/).
|
||||
|
||||
Licensing narrows the options before any feature comparison. **Almost every modern candidate is AGPL-3.0**, and so are Open edX and Canvas. The AGPL requires anyone running a modified version as a hosted service to publish their source changes. GPLv3 (Moodle, ILIAS, Chamilo) only requires source when the software is distributed, so a privately modified SaaS fork is allowed. Sakai's ECL-2.0 permits proprietary forks. These readings come from background knowledge of the license terms, not legal advice. The other main differences are in health. Open edX, Moodle, Chamilo, ILIAS and Frappe Learning all have strong release trains. **Canvas's silence after April 2026 stands out:** the repository recorded 4,314 commits in the preceding year, so six silent months is a serious warning sign. It could be a mirroring pause, though, and no Instructure announcement was found ([canvas-lms](https://github.com/instructure/canvas-lms)). A secondary source says Instructure restructured Canvas into Core, Plus and Next tiers in April 2026 and still offers a self-hosted OSS edition. That source is unverified ([bit4learn](https://bit4learn.com/pt/?p=56)).
|
||||
|
||||
The WordPress plugins are actively shipped. Tutor LMS v4.2.0 and Sensei 4.26.4 both came out in autumn 2026. All of them use a freemium model where certificates, subscriptions and advanced grading sit in paid Pro add-ons ([themeum/tutor](https://github.com/themeum/tutor); [Automattic/sensei](https://github.com/Automattic/sensei); [learningrevolution.net](https://www.learningrevolution.net/best-wordpress-lms-plugins/)). They suit solo creators but do not give a multi-tenant, standards-driven foundation. Among the niche projects, Pupilfirst (last release October 2024) and Leemons (last push May 2025) look stalled. **Richie** (MIT, Django CMS) is worth studying as a reference for catalog and portal UX ([openfun/richie](https://github.com/openfun/richie)). awesome-selfhosted's "Learning and Courses" list still omits all four modern platforms, a sign of how young that tier is ([awesome-selfhosted](https://github.com/awesome-selfhosted/awesome-selfhosted#learning-and-courses)).
|
||||
|
||||
## Open edX comes closest, but operating it is expensive
|
||||
|
||||
Open edX is the obvious "build on top" candidate. It is designed for MOOC-scale catalogs, ships built-in certificates, and offers many extension points: XBlocks, Django plugins, hooks and filters, and frontend plugin slots ([fulcrum.rocks](https://fulcrum.rocks/blog/how-to-choose-lms-between-moodle-canvas-openedx); [Verawood notes](https://docs.openedx.org/en/latest/community/release_notes/verawood/dev_op_release_notes.html)). Verawood, the 22nd community release, shipped on 30 July 2026. It added an AI Extensions Framework, a new Instructor Dashboard, and PostgreSQL support for fresh installs. Tutor v22.0.3 packaged it by 27 October ([Verawood product notes](https://docs.openedx.org/en/latest/community/release_notes/verawood/feature_release_notes.html); [tutor](https://github.com/overhangio/tutor)).
|
||||
|
||||
The cost shows up in Open edX's own release notes. **By Xylon (June 2027), every frontend is being converted to a new "frontend-base" architecture.** The older server-side filters are being replaced by frontend slots. Legacy Libraries are deprecated and must be migrated. Aspects analytics v4 is a breaking upgrade. Search is pinned to Typesense ≥ v30. A course-ID uniqueness migration can fail with an IntegrityError ([Verawood dev/op notes](https://docs.openedx.org/en/latest/community/release_notes/verawood/dev_op_release_notes.html)). The release working group held Verawood back until test completion passed 80% ([BTR notes](https://openedx.atlassian.net/wiki/spaces/COMM/pages/6497271814/BTR+2026-06-22+Meeting+notes)). Any UI customization therefore has to be redone every six months until mid-2027.
|
||||
|
||||
**Commerce is the decisive weakness for a Udemy-style product.** The legacy Django Oscar ecommerce service was "last tagged for the Redwood release", and maintainers say "we are not fixing bugs or developing new features for it". Its replacement, the Commerce Coordinator, was shaped around 2U's needs, and the research could not confirm it is usable by others ([ecommerce deprecated docs](https://docs.openedx.org/en/ulmo/site_ops/install_configure_run_guide/ecommerce-deprecated/index.html); [DEPR #22](https://discuss.openedx.org/t/deprecation-removal-ecommerce-service-depr-22/6839)). Multi-tenancy depends on eduNEXT's eox-tenant, which is maintained (v14.4.0, June 2026) but has only 13 stars ([eox-tenant](https://github.com/eduNEXT/eox-tenant)). Open edX's own installation docs expect operators to diagnose system problems from the command line ([install docs](https://docs.openedx.org/en/ulmo/site_ops/install_configure_run_guide/installation/index.html)). The often-quoted 4–8 GB RAM floor is unverified background knowledge. Either way, Open edX is a poor fit for a homelabber who wants a single `docker compose up`. Forking edx-platform would mean absorbing its six-monthly breaking changes under the AGPL. Building on top of it through plugins and frontend slots is the only realistic Open edX route.
|
||||
|
||||
The other institutional LMSs serve better as references or niche bases. **Moodle** is the easiest to self-host and has a deep plugin ecosystem. Version 5.3 LTS shipped in October 2026 ([moodle.org](https://moodle.org/course/cms/7135/next)). Its UI improvements are evolutionary, though ([3rdwavemedia](https://elearning.3rdwavemedia.com/blog/whats-new-in-moodle-5-1/7254/)), and a modern single-page frontend would mean rewriting the UI on top of its web services. Its isolated multi-tenancy is the paid Workplace edition, which partners estimate at about $12k/yr for 1,000 users. The free alternative is IOMAD ([compono](https://www.compono.com/articles/moodle-workplace-pricing-hidden-costs-2026); [e-learndesign](https://www.e-learndesign.scot/moodle-quick-guides/moodle-vs-multi-tenancy-moodle/)). **Chamilo 3.0** is the dark horse. It uses Symfony with Vue 3 and Tailwind and offers hierarchical multi-URL tenancy. It also has a BuyCourses plugin with monthly and yearly subscriptions, improved SCORM 2004 runtime fields, certificate expiry, and an MCP server for AI clients. However, it only became stable in September 2026, and its contributors are concentrated around the BeezNest company ([v3.0.0 notes](https://github.com/chamilo/chamilo-lms/releases/tag/v3.0.0)). **ILIAS and Sakai** reportedly have the strongest university standards support: ILIAS for SCORM 2004 and cmi5, and Sakai for LTI, which Dr. Chuck led. That support is unverified background knowledge. Both are university-oriented and do not offer a consumer-grade catalog UX. **Canvas** still leads on usability ratings ([selfhosting.sh](https://selfhosting.sh/best/education/)). Its video, New Quizzes, Catalog and Credentials products are reportedly proprietary (unverified background), and its repository is silent, so treat it as a UX and API reference only.
|
||||
|
||||
## Modern challengers either paywall or omit enterprise features
|
||||
|
||||
The modern platforms have the UX and stacks the user wants. Each one, though, leaves out or charges for a feature that universities and companies need.
|
||||
|
||||
**Frappe Learning** is the healthiest. It is backed by Frappe Technologies, has 96 contributors, and releases v2.x builds almost weekly (v2.68.0 on 28 October 2026). It has no paid tier inside the app because revenue comes from hosting ([frappe/lms](https://github.com/frappe/lms)). Its open feature set is the broadest of the four modern platforms. It covers programs/learning paths, batches (cohorts) with seat limits, Zoom and Meet live classes, quizzes, assignments, and auto-issued certificates. It also supports SCORM upload and payments for courses, batches or certificates ([Frappe Learning docs](https://docs.frappe.io/learning)). Its limits are structural. **It only imports SCORM**, and SCORM export is still an open request (#1781). Sales tax at checkout is missing (#2349). Multi-tenancy is undocumented. The whole Frappe/MariaDB stack comes along with it, and the v15→v16 upgrade broke the stable Docker image (#2350) ([#1781](https://github.com/frappe/lms/issues/1781); [#2349](https://github.com/frappe/lms/issues/2349); [#2350](https://github.com/frappe/lms/issues/2350)). Note that one 2026 article calls Frappe LMS "MIT". GitHub reports AGPL-3.0, so the article is wrong ([lmspedia](https://lmspedia.org/best-free-open-source-lms-platforms/)).
|
||||
|
||||
**LearnHouse** has the most modern authoring experience. It offers a Notion-like Tiptap editor, real-time collaboration, code execution with auto-grading in 30+ languages, AI playgrounds and an AI tutor. Its self-host CLI is also the friendliest, with `setup`, `backup` and `doctor` commands ([learnhouse](https://github.com/learnhouse/learnhouse)). But it is **explicitly open-core**. Its README marks Payments, SSO and Multi-Org as "(Enterprise)". The repository ships only SCORM stubs under `ee-stub`, and the pricing page puts SCORM, RBAC, audit logs, white-labelling and the self-hosted option in the Enterprise tier ([repo tree](https://github.com/learnhouse/learnhouse/tree/main/apps/web/ee-stub); [pricing](https://www.learnhouse.app/pricing)). Forking it means rebuilding exactly the features the target audiences need, from a project led by one maintainer. Open bugs such as "Subscriptions are not working" (#1182) add to the risk ([#1182](https://github.com/learnhouse/learnhouse/issues/1182)).
|
||||
|
||||
**ClassroomIO** has multi-org academies on custom domains, a REST API, webhooks and an MCP server. It now describes itself as "an intelligent customer training platform" for SaaS companies. SSO is in its Enterprise tier, and the company sells commercial licenses on top of the AGPL ([classroomio](https://github.com/classroomio/classroomio); [pricing](https://classroomio.com/pricing)). Self-hosters have hit broken bundled MinIO images and video transcription stuck "in progress" ([issues](https://github.com/classroomio/classroomio/issues?q=self-host)). **CourseLit** describes itself as a Teachable/Thinkific alternative with Stripe and a page builder. It has had one commit in three months, its users have been asking for multi-language support since 2024 (#508), and its media handling goes through the paid MediaLit service unless you self-host MediaLit ([courselit](https://github.com/codelitdev/courselit); [#508](https://github.com/codelitdev/courselit/issues/508)).
|
||||
|
||||
The pattern is the same across all four. **The research found no evidence of LTI 1.3 in any of them** (code search was rate-limited, so this is unconfirmed). None has xAPI or cmi5. **None is a multi-instructor marketplace with revenue share.** In two of them, the SSO that companies require is paid.
|
||||
|
||||
## Every project falls short on standards, video and labs
|
||||
|
||||
Purchased content shapes the design more than the inspiration brands suggest. **Commercial libraries are almost never handed over as files.** They appear inside the customer's LMS through catalog sync and launch links, and progress flows back over xAPI or APIs. LinkedIn Learning integrates through LTI 1.3 with all three Advantage services. Deep Linking can launch a specific video segment, Assignment and Grade Services syncs grades, and Names and Roles Provisioning syncs rosters ([Microsoft Learn](https://learn.microsoft.com/th-th/linkedin/learning/sso-auth/sso-docs/lti-13-implementation)). Udemy Business sends near-real-time xAPI statements to a customer LRS over OAuth, **but only on its Enterprise plan** ([Udemy Business xAPI](https://business-support.udemy.com/hc/articles/4405026170135)). Go1 exports SCORM only through a paid add-on ([Go1 help](https://help.go1.com/en/articles/4030866-export-scorm-from-go1-to-your-external-hris-or-lms)). The industry pattern is "metadata local, media remote". A platform that hosts purchased courses therefore needs five pieces: an **LTI 1.3 Advantage platform (consumer)**, a **SCORM 1.2/2004 player**, an **OAuth-protected xAPI LRS endpoint**, **catalog connectors**, and identity matching through SSO. Universities also need the reverse role, an **LTI 1.3 tool provider**, so the platform's courses can embed in Canvas, Moodle, Blackboard or Brightspace. That requirement now has a deadline: Blackboard requires LTI 1.3 by **30 September 2027** ([Blackboard community](https://community.blackboard.com/home/blogs/retirement-of-lti-11-support-in-blackboard-lms-2026-08-27)).
|
||||
|
||||
The standards themselves are stable targets. xAPI is now **IEEE 9274.1.1-2023**, published under Apache-2.0, and "xAPI 2.0" turns several earlier recommendations into requirements ([IEEE SA](https://standards.ieee.org/ieee/9274.1.1/7321); [xapi.com](https://xapi.com/blog/what-vendors-can-do-to-prepare-for-xapi-2-0)). cmi5 is the modern launch profile built on xAPI. Its production "Quartz" release is from about 2016, and no newer ADL release was found ([Learning Guild](https://www.learningguild.com/articles/adl-cmi5-committee-announces-release-of-quartz-version-of-cmi5-specification)). QTI 3.0 (May 2022) is still current, and Common Cartridge 1.4 is listed as "Candidate Final" ([1EdTech QTI](https://www.1edtech.org/standards/qti/versions); [1EdTech CC](https://1edtech.org/standards/cc)). Open Badges 3.0 and CLR 2.0 are "native verifiable credentials, as defined by the W3C" ([1EdTech CLR FAQ](https://1edtech.org/clr/faq)). Their final dates (June 2024 and roughly March 2025) come only from secondary sources. OB 3.0 uses a different data model from OB 2.0, so it should be designed in from the start rather than retrofitted. The research found **no mature open-source OB 3.0 issuer**. Badgr's open fork has only partial 3.0 support ([Open edX forum](https://discuss.openedx.org/t/common-questions-about-badging-and-verifiable-credentials-in-open-edx-teak-ulmo/18434)).
|
||||
|
||||
Video is a gap in every candidate. No platform was found with native HLS transcoding, though this is unverified for the large LMSs. LearnDash's own documentation warns that serving video from the app server does not scale ([LearnDash](https://learndash.com/support/kb/cloud/uncategorized/video-management-best-practices/)). The storage layer changed in 2026 as well. **MinIO's community repository was archived on 25 April 2026** and is "no longer maintained". Community binaries stopped in October 2025 ([minio/minio](https://github.com/minio/minio); [It's FOSS](https://itsfoss.com/news/minio-moves-away-from-open-source/)). That is exactly what broke ClassroomIO self-hosts. New designs should target "any S3" and test against **Garage** (AGPLv3, in production at Deuxfleurs since 2020), whose maintainers consider a swappable S3 backend an "aggregate of independent works" ([Garage](https://github.com/deuxfleurs-org/garage); [issue #840](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/840)). Full DRM (Widevine, FairPlay, PlayReady) needs vendor licenses (unverified background), so signed URLs plus AES-128 HLS is the realistic open default.
|
||||
|
||||
Hands-on labs are the biggest open-source gap and the most valuable differentiator. Pluralsight bought Next Tech and A Cloud Guru to get them ([Wikipedia](https://en.wikipedia.org/wiki/Pluralsight)). **Kasm's Community Edition cannot be used commercially**: its terms limit it to testing, non-profit and non-commercial use with 5 concurrent sessions ([Kasm license](https://kasm.com/docs/latest/license.html)). The research found no feature-complete open-source equivalent of Instruqt or Killercoda.
|
||||
|
||||
The homelab audience has its own gap. The only "Plex for courses" project found, **uCourse**, is unfinished and appears dormant since late 2023. Jellyfin and similar media servers have no idea of course structure or per-lesson completion ([uCourse](https://codeberg.org/vlfldr/ucourse)). The project should position this mode around content users own or are licensed to keep. Ingesting ripped Udemy or Pluralsight downloads likely violates those vendors' terms, though the primary terms-of-service text was not retrieved. Meanwhile, commercial platforms have made AI role-play and in-course tutors standard. Coursera reports its Coach on 97% of courses, and Udemy creators published more than 3,100 Role Plays in four weeks. Both figures are company-reported ([Gulf Business](https://gulfbusiness.com/en/2026/education/coursera-ceo-greg-hart-on-ai-and-learning-trends/); [Business Wire](https://www.businesswire.com/news/home/20250528283452/en/Udemy-Launches-AI-Powered-Role-Play-to-Coach-Professionals-on-Business-and-Communication-Skills)). An open platform could match this with a pluggable LLM backend and keep learner data in-house.
|
||||
|
||||
## Build new and reuse proven parts
|
||||
|
||||
**The recommendation is to build a new core and not fork.** Forking fails on every candidate. Open edX would bring six-monthly breaking changes and AGPL obligations without fixing its commerce or its operational weight. LearnHouse would leave you rebuilding its Enterprise features from a single-maintainer codebase. Frappe Learning ties you to the Frappe framework and MariaDB and would still need LTI, xAPI, cmi5 and multi-tenancy added. CourseLit is stalling. Building on top of an existing project is defensible only for a narrower goal. An institution that needs a MOOC portal in 2027 should deploy Open edX via Tutor (or a paid host) and extend it with plugins. A team that wants a working MVP in weeks could prototype on Frappe Learning or Chamilo 3.0. Neither of these reaches the stated goal: one modern codebase that a homelabber can run on a small box and a company can run as a multi-tenant SaaS.
|
||||
|
||||
The new core should own what no project does well. That means tenant-aware data (a single Postgres with tenant IDs and row-level security, with the same code path for single-tenant homelab mode, per background-knowledge patterns), a Git-friendly Markdown/MDX course format, LTI 1.3 in both directions, commerce and B2B seat licensing, catalog connectors and the lab orchestrator. Everything else should come from existing projects. Items marked † were not re-verified in 2026.
|
||||
|
||||
| Need | Reuse | License | Note |
|
||||
|---|---|---|---|
|
||||
| SCORM 1.2/2004 runtime | **scorm-again** | MIT | Full Simple Sequencing in v3; AICC dropped; no cmi5 ([GitHub](https://github.com/jcputney/scorm-again)) |
|
||||
| xAPI LRS | Yet Analytics **SQL LRS**† | Apache-2.0† | Postgres/SQLite; xAPI 2.0 conformance claimed ([GitHub](https://github.com/yetanalytics/lrsql)). Learning Locker is GPL and commercial-first |
|
||||
| cmi5 | ADL **CATAPULT**† as reference and test suite | Apache-2.0† | You build the player yourself ([GitHub](https://github.com/adlnet/CATAPULT)) |
|
||||
| LTI 1.3 tool side | **pylti1.3**† (Python) or **ltijs**† (Node, single maintainer) | MIT / Apache-2.0† | Platform-side libraries are scarce; use Moodle and Open edX code as reference ([pylti1.3](https://github.com/dmitry-viskov/pylti1.3); [ltijs](https://github.com/Cvmcosta/ltijs)) |
|
||||
| Identity | **Keycloak**† (or Zitadel†, reportedly AGPL since v3) + **SATOSA**† for eduGAIN | Apache-2.0† | App speaks only OIDC; the IdP handles per-tenant SAML and SCIM ([Keycloak](https://www.keycloak.org); [SATOSA](https://github.com/IdentityPython/SATOSA)) |
|
||||
| Video | **FFmpeg** + **Shaka Packager** + **hls.js/Vidstack**†; optionally **PeerTube** runners† | LGPL/BSD/Apache/MIT† | Captions via **faster-whisper** or **whisper.cpp** (MIT)† ([Shaka Packager](https://github.com/shaka-project/shaka-packager); [faster-whisper](https://github.com/SYSTRAN/faster-whisper)) |
|
||||
| Object storage | **Garage** (homelab) or **SeaweedFS**† / Ceph RGW† | AGPLv3 / Apache-2.0† | Do not bundle MinIO ([Garage](https://github.com/deuxfleurs-org/garage)) |
|
||||
| Search and recommendations | **Meilisearch**† or **Typesense**† + **pgvector**† | MIT† / GPL-3.0† / PostgreSQL | ([pgvector](https://github.com/pgvector/pgvector)) |
|
||||
| Payments and billing | **Stripe Billing + Tax**† (or a merchant of record); **Lago** (AGPL) or **Kill Bill** (Apache-2.0) only if usage billing becomes core | | Seat-pool licensing is a build item ([Lago](https://github.com/getlago/lago); [Kill Bill](https://killbill.io)) |
|
||||
| Interactive content | **h5p-standalone** (MIT)† | MIT† | Lumi's Node library is GPL-3.0† ([h5p-standalone](https://github.com/tunapanda/h5p-standalone)) |
|
||||
| Labs | **code-server**†, **JupyterHub** + ltiauthenticator†, **Kata/gVisor** or **Firecracker**†, **xterm.js/ttyd**, **Guacamole**† | MIT/BSD/Apache† | Avoid Kasm CE for commercial use ([JupyterHub ltiauthenticator](https://github.com/jupyterhub/ltiauthenticator)) |
|
||||
| Community | **Discourse**† as a companion | | Optional integration |
|
||||
|
||||
The project's own license should be chosen deliberately. Picking AGPL-3.0 would match the ecosystem (Open edX, Frappe, LearnHouse, Lago) and deter closed SaaS forks. It would also let you reuse AGPL components freely and keep a dual-license route open for commercial revenue, the way ClassroomIO does. Picking Apache-2.0 would maximize adoption by companies. In that case, keep AGPL and GPL components (Garage, Typesense, Zitadel, Lumi H5P) behind swappable network boundaries.
|
||||
|
||||
Build in this order. First, a single-binary or small-compose homelab mode with folder ingestion, progress tracking and HLS. Second, SCORM, an embedded LRS and the LTI tool provider, which is what universities need. Third, the LTI platform role, catalog connectors, SCIM and seat licensing for companies. Labs and AI role-play come last, because they cost the most to build and differentiate the most.
|
||||
|
||||
## Conclusion
|
||||
|
||||
The research shows that this is less about the course player than about the integration layer that sits between everything else. Commercial vendors charge extra for interoperability: xAPI on Udemy's Enterprise plan, export as a Go1 add-on. Meanwhile, the open-source projects with modern UX either paywall standards and SSO or never built them. A vendor-neutral hub that speaks LTI 1.3 in both directions, runs SCORM and cmi5, and keeps progress in the customer's own LRS has a clear purpose, especially as the Coursera–Udemy merger and Pluralsight's lender takeover make organizations wary of depending on one vendor ([Business Wire](https://www.businesswire.com/news/home/20251217155893/en/Coursera-to-Combine-with-Udemy-to-Empower-the-Global-Workforce-with-Skills-for-the-AI-Era); [Wikipedia](https://en.wikipedia.org/wiki/Pluralsight)). The homelab "Plex for courses" mode is the cheapest way to build community traction, and labs are where self-hosting turns a cost centre into an advantage.
|
||||
|
||||
Several points remain uncertain and should be checked before committing. Nobody in this research ran any candidate hands-on, so Chamilo 3.0 and Frappe Learning deserve a one-week spike each as benchmarks. Canvas OSS's future is unknown. Most standards-support claims for Moodle, ILIAS, Sakai and Canvas are unverified background knowledge and should be checked against the 1EdTech product directory. The 2026 status of LTI libraries, SQL LRS, Keycloak SCIM and Zitadel's license also needs re-confirming. Community sentiment from Reddit and Hacker News could not be retrieved, so demand for the homelab mode is inferred from tools like uCourse and Udeler rather than measured.
|
||||
@@ -0,0 +1,236 @@
|
||||
# Building Blocks and Standards for a New Self-Hostable Course Platform (state as of Oct 2026)
|
||||
|
||||
Method note: Budget was about 15 tool calls. Items marked **[verified 2026-10]** were checked against a live source in this session. Items marked **[unverified, pre-2026 knowledge]** cite the canonical primary URL but were NOT re-fetched this session. The report writer should treat those as likely-but-unconfirmed and flag them.
|
||||
|
||||
## 1. E-learning standards status (SCORM, xAPI/IEEE 9274.1.1, cmi5, LTI 1.3/Advantage, QTI 3, Common Cartridge, Open Badges 3.0/CLR 2.0, CASE): which matter for purchased content and university integration?
|
||||
|
||||
### Takeaway
|
||||
SCORM 1.2/2004 is frozen but is still the main format for off-the-shelf content libraries, so a platform that imports purchased content must run it. xAPI is now a formal IEEE standard (9274.1.1-2023, "xAPI 2.0"), and cmi5 is the modern packaging/launch profile on top of it. LTI 1.3/Advantage is required for university LMS integration, and Blackboard is retiring LTI 1.0/1.1 (LTI 1.3 required by 30 Sep 2027). Open Badges 3.0 and CLR 2.0 (W3C Verifiable Credentials) are the credential standards to target. QTI 3.0 and Common Cartridge are lower priority except for university course exchange.
|
||||
|
||||
### Cited Findings
|
||||
- **xAPI / IEEE 9274.1.1-2023 [verified 2026-10]:** The IEEE board approved it on 2023-03-30 and published it on 2023-10-06. It is listed as an "Active Standard" under the Apache-2.0 open-source license. It specifies a JSON data model and a REST API between activities and a Learning Record Store (LRS). — [IEEE SA 9274.1.1](https://standards.ieee.org/ieee/9274.1.1/7321)
|
||||
- ADL calls it the first open-source standard in IEEE history and says it became a standard via IEEE LTSC on 10 Oct 2023. — [ADL news, Nov 2023](https://adlnet.gov/news/2023/11/22/xAPI-Officially-Becomes-a-Published-IEEE-Standard)
|
||||
- In xAPI 2.0, several "strong recommendations" from 1.0.3 became requirements, and the text was edited for clarity. Vendors need to prepare for 2.0 separately from 1.0.3. — [xapi.com (Rustici) blog](https://xapi.com/blog/what-vendors-can-do-to-prepare-for-xapi-2-0)
|
||||
- The IEEE page also references a 2025 ISO/IEC/IEEE edition (39274-1-1-2025). This was not independently verified. — [IEEE SA 9274.1.1](https://standards.ieee.org/ieee/9274.1.1/7321)
|
||||
- **cmi5:** ADL released the production "Quartz" version of cmi5 after the developer-validation "Sandstone" version. This release is from about 2016; I found no newer ADL cmi5 release in the search. — [Learning Guild](https://www.learningguild.com/articles/adl-cmi5-committee-announces-release-of-quartz-version-of-cmi5-specification)
|
||||
- Rustici added cmi5 to SCORM Cloud, SCORM Engine and SCORM Driver. iSpring publishes cmi5. Some authoring tools (for example, ActivePresenter in an older forum post) did not. This shows that cmi5 authoring support is common in major tools but not universal. — [Rustici](https://rusticisoftware.com/?p=21736); [iSpring](https://www.ispringsolutions.com/blog/?p=9069); [Atomi forum](https://talk.atomisystems.com/t/support-for-cmi5/3154)
|
||||
- **LTI 1.3 [verified 2026-10]:** Blackboard is retiring LTI 1.0/1.1 and requires LTI 1.3 by 30 Sep 2027. New registrations tighten from 1 Jan 2027. Canvas tools are migrating to LTI 1.3 (May 2025 note). — [Blackboard community](https://community.blackboard.com/home/blogs/retirement-of-lti-11-support-in-blackboard-lms-2026-08-27); [Canvas community](https://community.canvaslms.com/t5/The-Product-Blog/Update-your-Impact-Course-Reports-to-the-LTI-1-3-version/ba-p/581663)
|
||||
- **QTI 3.0:** Released in May 2022 and still the latest version; I found no later release. — [1EdTech QTI versions](https://www.1edtech.org/standards/qti/versions); [1EdTech blog "Finally Final: QTI 3.0"](https://www.1edtech.org/blog/finally-final-the-qti-30-release)
|
||||
- **Common Cartridge 1.4:** Listed by 1EdTech as "Candidate Final" and ready to implement. Its K-12 profile bundles LTI 1.3 and QTI 3.0. The status is undated. — [1EdTech Common Cartridge](https://1edtech.org/standards/cc)
|
||||
- **Open Badges 3.0:** Approved as a final 1EdTech spec in June 2024. This date comes from a secondary source, and 1EdTech's own page was not fetched. — [POK guide (secondary)](https://www.pok.tech/blog/posts/open-badge-3-0-complete-guide-digital-credentials)
|
||||
- 1EdTech says OB 3.0 and CLR 2.0 are "native verifiable credentials, as defined by the W3C." — [1EdTech CLR FAQ](https://1edtech.org/clr/faq)
|
||||
- CLR 2.0 bundles multiple achievements into one signed ClrCredential aligned with OB 3.0. One secondary source dates its release to March 2025. — [Forasoft (secondary)](https://www.forasoft.com/learn/elearning-video/articles-elearning/certificates-badges-verifiable-credentials)
|
||||
- 1EdTech runs an OB 3.0 conformance and certification program for both issuers and hosts/displayers. Certification requires more than the spec strictly mandates. — [1EdTech OB 3.0 Cert Guide](https://standards.1edtech.org/open-badges/specifications/standards/v3p0/cert)
|
||||
- **CASE:** The 1EdTech Competencies and Academic Standards Exchange spec is used for machine-readable competency frameworks. — [1EdTech standards roadmap](https://www.1edtech.org/standards/roadmap) [unverified, pre-2026 knowledge for CASE details]
|
||||
|
||||
### Inferences
|
||||
- **Importing purchased content:** You need SCORM 1.2 plus SCORM 2004 (3rd/4th ed.) runtime as the floor. Most content libraries also offer LTI 1.3 delivery, where the vendor hosts the content, and some offer cmi5/xAPI. An LTI 1.3 *platform* (consumer) role is therefore as important as the SCORM player for B2B content. AICC is legacy and can likely be skipped. Note that scorm-again v3 dropped AICC.
|
||||
- **University integration:** The new platform should act as an LTI 1.3 *tool*, so universities can embed courses in Canvas, Moodle, Blackboard or Brightspace. It needs Advantage services: Names and Roles Provisioning Services (NRPS), Assignment and Grade Services (AGS), and Deep Linking. Common Cartridge export is a nice-to-have, and QTI 3 import/export matters only for assessment portability.
|
||||
- **Credentials:** Target OB 3.0 (and optionally CLR 2.0) issuance from the start. OB 2.0 is legacy, and OB 3.0 is a different data model (VC-based), so retrofitting later is costly.
|
||||
- **Analytics:** Emit xAPI statements internally, for example to an embedded LRS, so the platform can satisfy cmi5 and enterprise analytics requests.
|
||||
|
||||
### Gaps
|
||||
- I could not confirm the official 1EdTech dates for OB 3.0 final and CLR 2.0 final from 1EdTech pages directly, only from secondary sources.
|
||||
- I found no 2025–2026 change to cmi5 or SCORM. I also could not verify whether ADL has moved cmi5 into IEEE (an IEEE P9274.x cmi5 work item has been discussed historically).
|
||||
- The 2025 ISO/IEC/IEEE 39274-1-1 edition is unverified.
|
||||
- LTI Advantage service versions and any "LTI 1.3 + Dynamic Registration" adoption status were not checked this session.
|
||||
|
||||
## 2. OSS runtimes and libraries: SCORM players, cmi5 players, LRSs, LTI libraries, QTI tools, Open Badges issuers
|
||||
|
||||
### Takeaway
|
||||
The building blocks are uneven. scorm-again (MIT) is a solid SCORM 1.2/2004 runtime with sequencing, but it has no cmi5/xAPI. LRS options exist: Yet Analytics SQL LRS (Apache-2.0), plus Learning Locker (GPL-3.0, still public but apparently low-activity). LTI libraries exist for Node, Python and PHP. OB 3.0 open-source issuers are immature, and Badgr's OB 3.0 support is only partial.
|
||||
|
||||
### Cited Findings
|
||||
- **scorm-again [verified 2026-10]:**
|
||||
- License and scope: MIT. Supports SCORM 1.2 and SCORM 2004, including a "complete implementation" of IMS Simple Sequencing (navigation requests, rollup and sequencing rules) in v3.0.0.
|
||||
- Dropped and missing standards: AICC was removed in v3.0.0. cmi5/xAPI are *not* supported and are listed only as "potential" roadmap items.
|
||||
- Offline: offline support is a v3 feature still under development.
|
||||
- Source: [GitHub jcputney/scorm-again](https://github.com/jcputney/scorm-again)
|
||||
- **Learning Locker [verified 2026-10]:**
|
||||
- Status and license: the GitHub repo is not archived and is GPL-3.0. Its README points to the commercial "Learning Locker Data Cloud". Releases and packages are empty, and no deprecation notice is shown.
|
||||
- Activity: 3,254 commits, 72 open issues, 6 open PRs.
|
||||
- Source: [GitHub LearningLocker/learninglocker](https://github.com/LearningLocker/learninglocker)
|
||||
- Learning Locker v2 (Sept 2017) was ADL-certified as xAPI-conformant and moved from PHP to Node.js. The newest Learning Pool open-source news I found dates from 2019. — [Learning Pool v2 announcement](https://learningpool.com/learning-locker-open-source-v2-is-here/); [Learning Pool "A New Direction"](https://learningpool.com/blog/learning-locker-a-new-direction)
|
||||
- **Badgr:** Badgr is described as a free and open-source Open Badges platform. A third-party comparison (July 2026) lists Badgr's open-educational-badges as covering OB 1.1, 2.0 and 3.0, but with *partial* issue/verify support. **edubadges** is a maintained open-source fork of Badgr. Open edX's Teak/Ulmo releases document badging and verifiable-credential options. — [Canvas Badgr overview](https://learn.canvas.net/courses/2739/modules/items/239349); [Open edX forum](https://discuss.openedx.org/t/common-questions-about-badging-and-verifiable-credentials-in-open-edx-teak-ulmo/18434)
|
||||
- **Yet Analytics SQL LRS** (Apache-2.0, Postgres/SQLite, xAPI 1.0.3 and 2.0 conformance claimed) — [GitHub yetanalytics/lrsql](https://github.com/yetanalytics/lrsql) [unverified, pre-2026 knowledge]
|
||||
- **TRAX LRS** (Laravel/PHP; open-source edition, commercial "extended") — [traxlrs.com](https://traxlrs.com) [unverified, pre-2026 knowledge]
|
||||
- **Veracity Learning** LRS has a free/community tier but is not open source. — [lrs.io](https://lrs.io) [unverified, pre-2026 knowledge]
|
||||
- **ADL open-source cmi5 tooling:** the ADL "CATAPULT" project provides a cmi5 player prototype and a conformance test suite (Apache-2.0). — [GitHub adlnet/CATAPULT](https://github.com/adlnet/CATAPULT) [unverified, pre-2026 knowledge]
|
||||
- **Pipwerks SCORM API wrapper** is the *content-side* wrapper, not an LMS runtime. It is MIT-licensed and old but stable. — [GitHub pipwerks/scorm-api-wrapper](https://github.com/pipwerks/scorm-api-wrapper) [unverified, pre-2026 knowledge]
|
||||
- **LTI libraries** [unverified, pre-2026 knowledge]:
|
||||
- **ltijs** (Node, Apache-2.0, tool side; maintenance has historically been single-maintainer) — [GitHub Cvmcosta/ltijs](https://github.com/Cvmcosta/ltijs)
|
||||
- **pylti1.3** (Python, MIT, tool side; Django/Flask adapters) — [GitHub dmitry-viskov/pylti1.3](https://github.com/dmitry-viskov/pylti1.3)
|
||||
- **1EdTech/packbackbooks lti-1-3-php-library** (PHP, tool side) — [GitHub packbackbooks/lti-1-3-php-library](https://github.com/packbackbooks/lti-1-3-php-library)
|
||||
- Platform-side (consumer) OSS libraries are scarcer. Moodle's and Open edX's code is the main reference.
|
||||
- **QTI tooling** [unverified, pre-2026 knowledge]: the TAO platform (OAT, GPL-2.0) is the main open-source QTI engine. 1EdTech publishes QTI 3 examples and validators to members. — [TAO](https://www.taotesting.com)
|
||||
|
||||
### Inferences
|
||||
- **SCORM path:** scorm-again covers the "play purchased SCORM" requirement in the browser. The team must still build the backend: package import, manifest parsing, the CMI data store, and ideally sequencing persistence.
|
||||
- **cmi5 path:** cmi5 requires building launch and session logic on top of an LRS, using ADL CATAPULT as a reference and test suite. Rustici Engine is the only turnkey all-standards engine, and it is commercial.
|
||||
- **Licenses:** SQL LRS (Apache-2.0) is the lowest-risk embeddable LRS. Learning Locker is GPL-3.0 and appears effectively commercial-first, so it is a weaker bet.
|
||||
- **Badges:** An OB 3.0 issuer is probably a build item (VC signing plus JSON-LD context plus 1EdTech certification), possibly using generic W3C VC libraries.
|
||||
|
||||
### Gaps
|
||||
- I did not verify scorm-again's latest version or release date, or the activity of ltijs and pylti1.3 in 2025–2026.
|
||||
- I did not verify SQL LRS's current version or license, TRAX's status, or whether Learning Locker has had commits since 2023.
|
||||
- Badgr's current corporate owner and status (it was acquired by Instructure in 2021) and its open-source repo activity were not checked this session.
|
||||
- I found no mature open-source OB 3.0 issuer confirmed this session.
|
||||
|
||||
## 3. Video pipeline: transcoding/HLS/DASH, players, captions (Whisper), DRM, S3-compatible storage (MinIO changes, Garage, SeaweedFS)
|
||||
|
||||
### Takeaway
|
||||
The storage landscape changed sharply. MinIO's community repo was archived on 25 Apr 2026 and is no longer maintained; it went source-only in Oct 2025. New builds should therefore use Garage (AGPLv3), SeaweedFS, Ceph RGW or a community fork. The transcode/packaging/player stack (FFmpeg + Shaka Packager + Shaka/Video.js/Vidstack) is mature OSS. DRM cannot be fully open source, because Widevine, PlayReady and FairPlay need vendor licenses and CDMs; only ClearKey/AES-128 HLS is open.
|
||||
|
||||
### Cited Findings
|
||||
- **MinIO [verified 2026-10]:**
|
||||
- Repo status: the GitHub repo was "archived by the owner on Apr 25, 2026". The README says "THIS REPOSITORY IS NO LONGER MAINTAINED".
|
||||
- License and distribution: source is AGPLv3. "The MinIO community edition is now distributed as source code only", and legacy binaries "will not receive updates".
|
||||
- Alternatives MinIO points to: AIStor Free (standalone, free license) and AIStor Enterprise.
|
||||
- Source: [GitHub minio/minio](https://github.com/minio/minio)
|
||||
- MinIO timeline per secondary sources:
|
||||
- Console removed from the community edition in 2025 (sources disagree whether February or May).
|
||||
- Docker images and binaries stopped in Oct 2025.
|
||||
- README moved to maintenance mode on 3 Dec 2025.
|
||||
- Marked "no longer maintained" in Feb 2026.
|
||||
- A fork, pgsty/minio, restores the console and binaries.
|
||||
- Sources: [vonng blog](https://blog.vonng.com/en/db/minio-resurrect/); [It's FOSS](https://itsfoss.com/news/minio-moves-away-from-open-source/); [Elest.io](https://blog.elest.io/minio-is-in-maintenance-mode-your-guide-to-s3-compatible-storage-alternatives/)
|
||||
- Conflict: one source cites Feb 2026 archival, but GitHub shows an archive date of 25 Apr 2026. I treat the GitHub banner as authoritative for the final archive date.
|
||||
- **Garage:**
|
||||
- What it is: an AGPLv3 S3-compatible store for small, geo-distributed self-hosted deployments, run in production by Deuxfleurs since 2020. The crate is at v2.4.x on docs.rs.
|
||||
- Maintainers' view on AGPL: if Garage could be swapped for another S3 store, the result is an "aggregate of independent works" and not subject to the license restriction. This is not legal advice.
|
||||
- Sources: [GitHub deuxfleurs-org/garage](https://github.com/deuxfleurs-org/garage); [Garage issue #840](https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/840); [docs.rs garage 2.4.1](https://docs.rs/crate/garage/2.4.1)
|
||||
- **SeaweedFS** (Apache-2.0, S3 gateway, good for many small files) — [GitHub seaweedfs/seaweedfs](https://github.com/seaweedfs/seaweedfs) [unverified, pre-2026 knowledge]
|
||||
- **Ceph RGW** (LGPL; heavy but proven at university scale) — [ceph.io](https://ceph.io) [unverified, pre-2026 knowledge]
|
||||
- **Transcoding and packaging** [unverified, pre-2026 knowledge]:
|
||||
- FFmpeg (LGPL/GPL depending on build flags) — [ffmpeg.org legal](https://ffmpeg.org/legal.html)
|
||||
- Shaka Packager (BSD-3, HLS/DASH/CMAF plus CENC encryption) — [GitHub shaka-project/shaka-packager](https://github.com/shaka-project/shaka-packager)
|
||||
- **Players** [unverified, pre-2026 knowledge]:
|
||||
- Shaka Player (Apache-2.0, DASH/HLS plus EME DRM) — [GitHub shaka-project/shaka-player](https://github.com/shaka-project/shaka-player)
|
||||
- Video.js (Apache-2.0; v10 is merging with Vidstack/Plyr/Media Chrome efforts) — [videojs.com](https://videojs.com)
|
||||
- Vidstack (MIT) — [vidstack.io](https://www.vidstack.io)
|
||||
- hls.js (Apache-2.0) — [GitHub video-dev/hls.js](https://github.com/video-dev/hls.js)
|
||||
- **PeerTube** as a video backend (AGPLv3; HLS transcoding, runners for remote transcoding, plugin API; built-in Whisper-based automatic transcription since v6.2/v7) — [joinpeertube.org](https://joinpeertube.org); [GitHub Chocobozzz/PeerTube](https://github.com/Chocobozzz/PeerTube) [unverified, pre-2026 knowledge]
|
||||
- **Captions** [unverified, pre-2026 knowledge]:
|
||||
- OpenAI Whisper (MIT, weights MIT) — [GitHub openai/whisper](https://github.com/openai/whisper)
|
||||
- faster-whisper (MIT, CTranslate2) — [GitHub SYSTRAN/faster-whisper](https://github.com/SYSTRAN/faster-whisper)
|
||||
- whisper.cpp (MIT) — [GitHub ggml-org/whisper.cpp](https://github.com/ggml-org/whisper.cpp)
|
||||
- **DRM** [unverified, pre-2026 knowledge]:
|
||||
- Widevine requires a license agreement with Google, FairPlay requires Apple's FPS deployment package, and PlayReady requires a Microsoft license.
|
||||
- Open-source *packaging* (Shaka Packager) and *players* (Shaka, Video.js plus EME) exist, but the key/license server and CDM are proprietary or licensed.
|
||||
- Sources: [Widevine](https://www.widevine.com); [Apple FairPlay Streaming](https://developer.apple.com/streaming/fps/)
|
||||
|
||||
### Inferences
|
||||
- **DRM need:** For a course platform, DRM is usually overkill except for licensed third-party video. Most self-hosted platforms use signed URLs, short-lived tokens, AES-128 HLS and watermarking, all of which are fully open.
|
||||
- **Storage choice:**
|
||||
- Garage: AGPL, simple, multi-site, suits homelab users.
|
||||
- SeaweedFS: Apache-2.0, performant.
|
||||
- Ceph: enterprise and university scale.
|
||||
- Design for "any S3" and do not bundle MinIO.
|
||||
- **Build vs. reuse:** PeerTube can serve as a turnkey transcode and stream backend (its runners and transcription are reusable), but it is AGPL and federated-video-oriented. A thin custom FFmpeg/Shaka pipeline may fit better if you need multi-tenant access control.
|
||||
|
||||
### Gaps
|
||||
- I did not verify SeaweedFS's license, PeerTube's 2026 version and transcription features, or the Video.js v10/Vidstack merger status this session.
|
||||
- The current AIStor Free license terms were not examined.
|
||||
- No open-source multi-DRM license server of production quality was confirmed. Some exist for ClearKey only.
|
||||
|
||||
## 4. Auth and identity: Keycloak, Authentik, Zitadel, Ory, Authelia (OIDC/SAML/SCIM, eduGAIN/Shibboleth)
|
||||
|
||||
### Takeaway
|
||||
No source was fetched this session; all items are from pre-2026 knowledge. For universities, SAML 2.0 SP support with metadata-aggregate (eduGAIN/InCommon) ingestion is the key requirement. Keycloak (CNCF, Apache-2.0) is the safest default. Authentik and Zitadel are viable for SAML and OIDC. Authelia is not an IdP broker for SAML federations.
|
||||
|
||||
### Cited Findings
|
||||
- **Keycloak** [unverified, pre-2026 knowledge]:
|
||||
- License and scope: Apache-2.0, a CNCF incubating project. It supports OIDC, SAML 2.0 and identity brokering.
|
||||
- Federation: it has no native bulk eduGAIN metadata-aggregate import. Community extensions and SATOSA/Shibboleth proxies are typically used.
|
||||
- SCIM: added as an experimental feature in recent releases (26.x).
|
||||
- Sources: [keycloak.org](https://www.keycloak.org); [CNCF Keycloak](https://www.cncf.io/projects/keycloak/)
|
||||
- **Authentik** [unverified, pre-2026 knowledge]: MIT core (with enterprise features under a separate license). Supports OIDC, SAML, LDAP, RADIUS, and SCIM *outbound* provisioning. — [goauthentik.io](https://goauthentik.io)
|
||||
- **Zitadel** [unverified, pre-2026 knowledge]: relicensed from Apache-2.0 to AGPL-3.0 in 2025 (v3). Multi-tenant "organizations" model with OIDC, SAML and SCIM 2.0 server. — [zitadel.com](https://zitadel.com); [GitHub zitadel/zitadel](https://github.com/zitadel/zitadel)
|
||||
- **Ory** [unverified, pre-2026 knowledge]: Kratos/Hydra are Apache-2.0. SAML and SCIM are mainly in Ory Network/enterprise, not the OSS core. — [ory.sh](https://www.ory.sh)
|
||||
- **Authelia** [unverified, pre-2026 knowledge]: Apache-2.0, a forward-auth portal and OIDC provider. It does not do SAML. — [authelia.com](https://www.authelia.com)
|
||||
- **SATOSA** (GÉANT/IdentityPython, Apache-2.0) is the common SAML-to-OIDC proxy used in R&E federations, and Shibboleth SP is the reference SP. — [GitHub IdentityPython/SATOSA](https://github.com/IdentityPython/SATOSA); [shibboleth.net](https://shibboleth.net)
|
||||
|
||||
### Inferences
|
||||
- **Recommended pattern:** the app speaks only OIDC to an embedded or companion IdP (Keycloak or Zitadel). That IdP handles enterprise SAML/OIDC per tenant, and SATOSA or a Keycloak federation extension handles eduGAIN.
|
||||
- **Provisioning:** SCIM inbound (an Entra ID/Okta push to the platform) is what companies want for seat management. Verify each candidate's *inbound* SCIM server support.
|
||||
|
||||
### Gaps
|
||||
- Nothing in this section was verified this session, including Zitadel's AGPL change date, Keycloak's SCIM status, and Authentik's SCIM direction.
|
||||
- Whether any candidate natively consumes eduGAIN metadata aggregates with MDQ in 2026 is unknown.
|
||||
|
||||
## 5. Commerce: payments, billing, B2B seat licensing, taxes/VAT
|
||||
|
||||
### Takeaway
|
||||
Use Stripe (or Paddle/Lemon Squeezy as merchant of record) for payments and tax. Open-source billing engines exist: Lago (AGPL-3.0, usage and subscriptions) and Kill Bill (Apache-2.0, subscriptions). Hyperswitch (payment orchestration) is likely Apache-2.0 but unverified. Seat-based B2B licensing (seat pools, assignment, true-ups) is application logic that the team must build.
|
||||
|
||||
### Cited Findings
|
||||
- **Lago:** AGPL-3.0 per its GitHub. Lago's 2022 post says AGPL was chosen to deter commercial forks that do not contribute back. — [GitHub getlago/lago](https://github.com/getlago/lago); [Lago blog](https://getlago.com/blog/open-source-licensing-and-why-lago-chose-agplv3)
|
||||
- **Kill Bill:** Apache-2.0 (per a third-party listing updated Aug 2026). — [ideaproof Kill Bill](https://ideaproof.io/open-source/project/kill-bill); [killbill.io](https://killbill.io)
|
||||
- **OpenMeter** (Apache-2.0) and Flexprice (AGPL-3.0) are other OSS billing and metering options. This comes from a vendor-adjacent comparison. — [Flexprice glossary](https://flexprice.io/glossary/open-source-billing)
|
||||
- **Hyperswitch** (Juspay): open-source payment switch/orchestrator, believed Apache-2.0. — [GitHub juspay/hyperswitch](https://github.com/juspay/hyperswitch) [unverified, pre-2026 knowledge]
|
||||
- **Stripe Tax and Stripe Billing** support per-seat pricing, VAT/OSS (EU One-Stop-Shop) calculation and invoicing for B2B. — [stripe.com/tax](https://stripe.com/tax) [unverified, pre-2026 knowledge]
|
||||
|
||||
### Inferences
|
||||
- **Self-hosted operators** (homelab, universities) mostly do not need commerce. Make commerce a pluggable module.
|
||||
- **SaaS operators selling to companies:** Stripe Billing plus Stripe Tax, or a merchant of record for EU VAT simplicity, is far cheaper than self-hosting Lago or Kill Bill, unless usage-based billing (for example, lab hours) becomes core.
|
||||
|
||||
### Gaps
|
||||
- Hyperswitch's license and 2026 status are unverified.
|
||||
- I found no open-source "B2B seat license manager" component. It is likely a build item.
|
||||
|
||||
## 6. Hands-on labs and interactive content (Kasm, code-server, JupyterHub, Coder, Killercoda-like, Firecracker, Instruqt alternatives, H5P)
|
||||
|
||||
### Takeaway
|
||||
Hands-on labs are the hardest differentiator to reuse. Kasm's Community Edition is *not* usable commercially: it is limited to non-commercial use and 5 concurrent sessions. The open-source primitives are code-server, JupyterHub, Coder, Firecracker/Kata/gVisor and ttyd/xterm.js, which the team must orchestrate itself. H5P (GPL-3.0/MIT parts) can be embedded standalone.
|
||||
|
||||
### Cited Findings
|
||||
- **Kasm Workspaces [verified 2026-10]:**
|
||||
- Community Edition terms: "free to download and install for testing, non-profits and non-commercial activities", limited to 5 concurrent sessions. The EULA bars revenue-generating business use.
|
||||
- Paid tiers: licensed per named user or concurrent session.
|
||||
- Sources: [Kasm license docs](https://kasm.com/docs/latest/license.html); [Kasm trust: community edition](https://trust.kasmweb.com/community-edition)
|
||||
- **code-server** (Coder, MIT) — [GitHub coder/code-server](https://github.com/coder/code-server) [unverified, pre-2026 knowledge]
|
||||
- **Coder** (AGPL-3.0 core, Terraform-defined workspaces; premium features licensed) — [GitHub coder/coder](https://github.com/coder/coder) [unverified, pre-2026 knowledge]
|
||||
- **JupyterHub** (BSD-3; Kubernetes via Zero to JupyterHub; widely used at universities; LTI authenticator exists) — [jupyterhub.readthedocs.io](https://jupyterhub.readthedocs.io); [GitHub jupyterhub/ltiauthenticator](https://github.com/jupyterhub/ltiauthenticator) [unverified, pre-2026 knowledge]
|
||||
- **Firecracker** (AWS, Apache-2.0 microVMs, used by Fly.io and others) — [firecracker-microvm.github.io](https://firecracker-microvm.github.io) [unverified, pre-2026 knowledge]
|
||||
- **Isolation runtimes:** Kata Containers (Apache-2.0) and gVisor (Apache-2.0) are sandbox runtimes for running untrusted learner workloads on Kubernetes. — [katacontainers.io](https://katacontainers.io); [gvisor.dev](https://gvisor.dev) [unverified, pre-2026 knowledge]
|
||||
- **Killercoda and Instruqt** are proprietary SaaS. **iximiuz Labs** is likewise proprietary. I found no feature-complete open-source Instruqt equivalent this session.
|
||||
- **Browser terminal/desktop pieces:** ttyd (MIT), xterm.js (MIT) and Apache Guacamole (Apache-2.0) are the browser terminal/desktop primitives. — [GitHub xtermjs/xterm.js](https://github.com/xtermjs/xterm.js); [guacamole.apache.org](https://guacamole.apache.org) [unverified, pre-2026 knowledge]
|
||||
- **H5P:**
|
||||
- Licensing: the core libraries are mixed MIT/GPL, and many content-type libraries are MIT.
|
||||
- Embedding: h5p-standalone (MIT) and Lumi's h5p-nodejs-library (GPL-3.0) allow embedding without Drupal, WordPress or Moodle.
|
||||
- Interop: H5P emits xAPI statements.
|
||||
- Sources: [h5p.org](https://h5p.org); [GitHub Lumieducation/H5P-Nodejs-library](https://github.com/Lumieducation/H5P-Nodejs-library); [GitHub tunapanda/h5p-standalone](https://github.com/tunapanda/h5p-standalone) [unverified, pre-2026 knowledge]
|
||||
|
||||
### Inferences
|
||||
- **Lab engine:** A Pluralsight-style lab engine (provision, check task completion, tear down, quota) is the largest *build* item. The likely stack is Kubernetes plus Kata/gVisor or Firecracker plus ttyd/xterm.js or code-server, with Guacamole for GUI. Coder or JupyterHub can cover dev-environment and notebook labs directly.
|
||||
- **Kasm:** using it for a commercial platform requires a paid license.
|
||||
- **H5P license risk:** the GPL-3.0 node library could matter if the platform is distributed under a non-GPL license.
|
||||
|
||||
### Gaps
|
||||
- Coder's license split (AGPL vs. enterprise) and its 2026 status were not verified.
|
||||
- H5P library licenses and Lumi's status were not verified this session.
|
||||
- I did not search for newer OSS lab platforms from 2025–2026.
|
||||
|
||||
## 7. Search/recommendations, multi-tenancy patterns, and authoring formats (Markdown/MDX course-as-code, Git-backed)
|
||||
|
||||
### Takeaway
|
||||
Nothing in this section was verified this session. The lightweight search engines Meilisearch and Typesense, or OpenSearch at scale, cover catalog search. Postgres plus pgvector covers semantic recommendations. Course-as-code (Markdown/MDX in Git) is well proven by docs frameworks, but I found no standard course schema, so the team must define one.
|
||||
|
||||
### Cited Findings
|
||||
- **Search engines** [unverified, pre-2026 knowledge]:
|
||||
- Meilisearch (MIT community edition; enterprise features under a separate license since v1.x) — [GitHub meilisearch/meilisearch](https://github.com/meilisearch/meilisearch)
|
||||
- Typesense (GPL-3.0) — [GitHub typesense/typesense](https://github.com/typesense/typesense)
|
||||
- OpenSearch (Apache-2.0, Linux Foundation) — [opensearch.org](https://opensearch.org)
|
||||
- **pgvector** (PostgreSQL license) for embeddings-based "similar courses" — [GitHub pgvector/pgvector](https://github.com/pgvector/pgvector) [unverified, pre-2026 knowledge]
|
||||
- **Course-as-code prior art** [unverified, pre-2026 knowledge]:
|
||||
- Open edX OLX (XML course export), which is not Markdown — [Open edX OLX docs](https://docs.openedx.org)
|
||||
- MDX (MIT) and frameworks such as Astro Starlight and Docusaurus (MIT) — [mdxjs.com](https://mdxjs.com)
|
||||
|
||||
### Inferences
|
||||
- **Multi-tenancy:** the common pattern is a single DB with a tenant_id column plus row-level security in Postgres, optionally with per-tenant IdP realms or organizations (Keycloak realms, Zitadel orgs). Homelab single-tenant and SaaS multi-tenant modes should use the same code path.
|
||||
- **Content model:** a Git-backed, Markdown/MDX course format (lessons, quizzes as YAML/QTI-lite, lab definitions) is a good differentiator for the homelab and dev audience. Add importers for SCORM, cmi5 and Common Cartridge so bought content still fits.
|
||||
|
||||
### Gaps
|
||||
- Meilisearch's and Typesense's current licenses were not verified this session.
|
||||
- I did not research existing course-as-code schemas or tools from 2025–2026 (for example, any OSS "courses in Git" projects).
|
||||
- Recommendation-engine OSS beyond pgvector was not researched.
|
||||
@@ -0,0 +1,139 @@
|
||||
# Commercial Course Platforms (Pluralsight, Coursera, Udemy, LinkedIn Learning, LXPs): Features, Content Integration, and Gaps for a Self-Hosted Open-Source Alternative
|
||||
|
||||
_Research date: 2026-10-30. About 18 tool calls. Several primary vendor pages returned HTTP 403 (Business Wire, the Udemy Business help center article "Customer news April 2026"), so some facts come from search-result snippets of those pages. Items marked [UNVERIFIED-BACKGROUND] come from general knowledge and were not confirmed by a source in this session. Do not state them as fact without checking them first._
|
||||
|
||||
## (a) Core feature set and UX patterns as of 2025–2026
|
||||
|
||||
### Takeaway
|
||||
By 2026 the commercial baseline is a large catalog plus AI layers on top of it: an AI tutor or coach inside courses, AI role-play for soft skills, and AI-generated or AI-personalized learning paths. Skills benchmarking (e.g. Pluralsight Skill IQ/Role IQ), hands-on labs/sandboxes, and enterprise admin features (SSO, analytics, LMS/LXP integration) sit on top of that. Video-player basics (speed, transcripts, offline download in the mobile apps) are table stakes.
|
||||
|
||||
### Cited Findings
|
||||
- **Coursera Coach (AI tutor):** CEO Greg Hart said in 2026 that Coach is available on 97% of Coursera courses. Newer additions are "Coach Dialogues", which lets instructors embed Coach in a course for one-on-one Socratic questioning, and "Role Play", where Coach plays the other party in 60 role-play activities. These figures are company-reported. — [Gulf Business interview with Greg Hart, 2026](https://gulfbusiness.com/en/2026/education/coursera-ceo-greg-hart-on-ai-and-learning-trends/); [Coursera blog: Coach for educators](https://blog.coursera.org/announcing-ai-powered-capabilities-enabling-educators-to-use-coursera-coach-to-deliver-interactive-personalized-instruction)
|
||||
- **Udemy AI Role Play (launched May 28, 2025):** a set of instructor-designed conversation simulations with real-time coaching for interviews, conflict resolution, and giving feedback. Organizations can build custom scenarios. Udemy says creators published more than 3,100 Role Plays within four weeks. It came out of Udemy's "Innovation Studio". — [Business Wire press release](https://www.businesswire.com/news/home/20250528283452/en/Udemy-Launches-AI-Powered-Role-Play-to-Coach-Professionals-on-Business-and-Communication-Skills); [Training Industry](https://trainingindustry.com/press-release/artificial-intelligence/udemy-launches-ai-powered-role-play-to-coach-professionals-on-business-and-communication-skills/)
|
||||
- **Udemy Q2 2025 earnings release:** lists Role Play among recent launches, alongside a Lummi integration for AI-generated design assets. — [SEC EX-99.1 Q2 2025](https://www.sec.gov/Archives/edgar/data/0001607939/000160793925000123/q22025pressrelease.htm)
|
||||
- **Udemy Business enablement kit (April 2026):** groups Role Play, "AI-Powered Learning Paths", and an "AI Assistant". This comes from a search snippet; the page itself returned 403. — [Udemy Business: Customer news April 2026](https://business-support.udemy.com/hc/en-us/articles/39926187376919-Customer-news-April-2026)
|
||||
- **LinkedIn Learning AI role-play:** a learning-designer post lists it alongside Coursera Role Play as a practice-conversation tool that gives feedback afterward. The same post says there is little independent research on these tools and that trials so far show mixed results. This is a secondary source. — [search snippet, LinkedIn post (Andrea Filatro feed)](https://br.linkedin.com/in/andrea-filatro); [eWeek: AI tools to learn faster 2026](https://www.eweek.com/de/news/ai-tools-learn-any-skill-faster/)
|
||||
- **Pluralsight Skill IQ / Role IQ:** Pluralsight uses these to benchmark team skills and measure proficiency. — [AppsRunTheWorld profile](https://www.appsruntheworld.com/hcm-top-500-software-vendors/pluralsight/)
|
||||
- **Pluralsight labs:** Pluralsight acquired Next Tech in January 2021, a provider of cloud-hosted environments for development, data science, and ML labs, and A Cloud Guru, a cloud-skills training provider, in July 2021. — [Wikipedia: Pluralsight](https://en.wikipedia.org/wiki/Pluralsight)
|
||||
- **Pluralsight 2025 AI products:** an aggregator says Pluralsight rolled out an "AI Sandbox" for safe LLM and gen-AI practice and expanded its AI Skills paths, and lists a "Skills + Flow" tier with anchor pricing of $45/month or $449/year. This is a low-authority aggregator and is not confirmed. — [Subger: Pluralsight](https://subger.com/en/service/pluralsight)
|
||||
- **Docebo:** has added community features (bought PeerBoard, April 2023), generative-AI path tailoring (bought Edugo.AI, June 2023), and training-impact surveys (bought forMetris, November 2020). This shows LXPs bundling community, AI, and impact measurement. These deals are older (2020–2023). — [Business Wire: PeerBoard](https://businesswire.com/news/home/20230404005528/en/Docebo-Expands-External-Training-Offering-With-Acquisition-of-PeerBoard); [Seeking Alpha: Edugo.AI](https://seekingalpha.com/news/3979119-docebo-boost-ai-capability-through-edugoai-acquisition); [Training Industry: forMetris](https://trainingindustry.com/press-release/learning-technologies/docebo-acquires-formetris-to-expand-learning-impact-capabilities/)
|
||||
- **Minimum feature set in self-hosted course-platform requirement specs:** persistent progress tracking across devices ("resume where you left off"), a visual completion bar, subtitle support, a course builder, assessments, and certificates. — [Freelancer project spec](https://www.freelancer.in/projects/web-development/full-online-course-platform-build); [RaftLabs: How to build an LMS 2026](https://www.raftlabs.com/blog/how-to-build-an-lms)
|
||||
|
||||
### Inferences
|
||||
- The new 2025–2026 frontier is **AI conversational practice (role-play) and an in-course AI tutor grounded in course content**. All three major consumer/B2B players (Coursera, Udemy, LinkedIn Learning) have shipped role-play. An open-source platform could offer this with a pluggable LLM backend (local via Ollama/vLLM, or a BYO API key). That fits privacy-sensitive universities and companies that do not want learner data sent to a vendor's AI.
|
||||
- **Skills benchmarking plus a skills taxonomy** (Skill IQ/Role IQ) is a key B2B differentiator because it ties learning to workforce planning. An open-source version could use open taxonomies. ESCO and O*NET are examples [UNVERIFIED-BACKGROUND: suitability not checked].
|
||||
- **Hands-on labs are expensive to run** (Pluralsight bought Next Tech and A Cloud Guru for them). A self-hosted platform is well placed here: homelabbers and universities already run their own compute, so labs could run as containers or VMs on local infrastructure (e.g. Docker/K8s, code-server/Jupyter) instead of a metered vendor cloud.
|
||||
- [UNVERIFIED-BACKGROUND, not checked this session] Common UX patterns across these products: playback speed 0.5–2x, searchable interactive transcripts, timestamped notes and bookmarks, offline downloads in the mobile apps only (DRM-protected), weekly goals and streaks (LinkedIn Learning, Coursera), shareable certificates (LinkedIn profile integration, Coursera/Credly-style badges), Q&A per lecture (Udemy), and graded peer review plus university degrees (Coursera). Verify before citing.
|
||||
|
||||
### Gaps
|
||||
- No primary source fetched for LinkedIn Learning AI coaching features, Pluralsight's 2025–2026 AI features, Degreed, 360Learning (known for collaborative/peer authoring [UNVERIFIED-BACKGROUND]), or TalentLMS. A search for LXP news was polluted by "LXP Industrial Trust" (a REIT).
|
||||
- No verified 2026 details on instructor revenue-share terms (Udemy marketplace, Udemy Business royalty pool). Udemy changed instructor revenue share in the past, but this was not verified in this session.
|
||||
- No verified detail on SCIM or HRIS connectors (Workday, SuccessFactors) for each vendor.
|
||||
|
||||
## (b) How organizations bring purchased or third-party content into their own LMS/LXP, and what licensing restricts
|
||||
|
||||
### Takeaway
|
||||
Purchased libraries are almost never handed over as files. They are **surfaced inside the customer's LMS through catalog sync plus launch links** (LTI 1.3 deep linking, or SCORM/AICC "wrapper" stubs that call back to the vendor), while **progress flows back via xAPI or APIs**. Video stays on the vendor's servers. The main exception found is Go1's paid SCORM/xAPI export add-on. For an open-source platform, "hosting purchased courses" realistically means being a good **LTI 1.3 tool consumer, SCORM/xAPI player, and catalog-API aggregator**, not storing licensed MP4s. Self-owned or legally downloaded personal files are a separate case.
|
||||
|
||||
### Cited Findings
|
||||
- **LinkedIn Learning uses LTI 1.3 with Canvas, Moodle, Blackboard, and D2L.** It supports all three LTI Advantage services:
|
||||
- Deep Linking lets course creators select content, including a link that launches a specific segment of a video.
|
||||
- Assignments and Grades handles grade sync.
|
||||
- Names and Roles Provisioning handles roster sync.
|
||||
|
||||
Admins enable LTI and provision keys in the LinkedIn Learning admin "Authenticate" settings. — [Microsoft Learn: LinkedIn Learning LTI 1.3 implementation](https://learn.microsoft.com/th-th/linkedin/learning/sso-auth/sso-docs/lti-13-implementation); [LTI 1.3 Canvas guide](https://learn.microsoft.com/tr-tr/linkedin/learning/sso-auth/sso-docs/sso-lti-13-canvas); [LTI 1.3 Moodle guide](https://learn.microsoft.com/it-it/linkedin/learning/sso-auth/sso-docs/sso-lti-13-moodle)
|
||||
- **Canvas workflow for LinkedIn Learning (Stevens Institute):** instructors find LinkedIn Learning content inside Canvas and embed it via a deep link. The recommended method is an Assignment with the "External Tool" submission type. — [Stevens support article](https://support.stevens.edu/support/solutions/articles/19000105005)
|
||||
- **Limitation:** LinkedIn's Canvas guide says educators cannot deep link to LinkedIn Learning content on class webpages, wikis, or other external systems. The deep link works only through the LMS LTI launch. — [LinkedIn Learning Canvas/LTI guide](https://learn.microsoft.com/de-de/linkedin/learning/sso-auth/sso-docs/sso-lti-canvas)
|
||||
- **Udemy Business xAPI integration:**
|
||||
- Course and learning-path progress and completion statements go to the customer's LMS/LXP in near real time.
|
||||
- It is only available on the **Enterprise plan**.
|
||||
- Setup: the LMS registers Udemy Business as an API application. The admin then enters the OAuth token URL, client ID, client secret, and xAPI statement URL in Udemy Business.
|
||||
- Learners are identified by email by default. Alternatively, an account object is used, which requires the LMS to pass an `lmsUserID` during SSO.
|
||||
- Failed statements are retried twice a day for 5 days.
|
||||
- Not all LMS/LXP systems support xAPI.
|
||||
|
||||
— [Udemy Business: xAPI integration](https://business-support.udemy.com/hc/articles/4405026170135); [Udemy Business help section](https://business-support.udemy.com/hc/en-us/sections/115001553827-Supporting-My-Users)
|
||||
- **Udemy Business has a REST API** (catalog and reporting) — [Udemy Business API Reference](https://business-support.udemy.com/hc/articles/360005792753-Udemy-Business-API-Reference). Third-party LMSs use it to integrate Udemy Business. Two examples are the [IMC Learning Suite Udemy connector](https://doc-en.scheer-imc.com/FuncRef/udemy) and [Schoox "Integrating Udemy Business into your Academy"](https://sigmasupport.schoox.com/hc/en-us/articles/16663502324759).
|
||||
- **Udemy Business custom SCORM/AICC content:** files must be hosted on a secured (https) server that the customer organization manages, then embedded via iframe or direct link. — [Udemy Business: SCORM and AICC content](https://business-support.udemy.com/hc/en-us/articles/13292991199127-SCORM-and-AICC-content-Leadership-Academy)
|
||||
- **Go1 content export:** admins can export selected Content Hub resources as SCORM packages (with a CSV of course details, delivered by email), but only if the **SCORM and xAPI export add-on** is enabled. — [Go1 help: export SCORM to your HRIS/LMS](https://help.go1.com/en/articles/4030866-export-scorm-from-go1-to-your-external-hris-or-lms)
|
||||
- **Go1, July 30, 2026:** added SCORM export of customer-created content when SCORM integration is enabled. — [Go1 integration partner product updates](https://help.go1.com/en/articles/11005352-integration-partner-product-updates)
|
||||
- **Go1, March 23, 2026:** launched a Schoox/UKG integration covering activation, content search, sync, enrollment tracking, and retirement. Go1 Learn appears as an embedded pop-out inside Schoox. — [Go1 integration partner product updates](https://help.go1.com/en/articles/11005352-integration-partner-product-updates)
|
||||
- **Go1 lists integrations** with Docebo, Elmo, Kineo, MindTickle, PeopleStreme, and others. This is a third-party directory. — [GetApp: Go1](https://www.getapp.com/training-software/a/go1-lms)
|
||||
- **Consumer Udemy:** sharing subscribed-course content is described as prohibited under Udemy's Terms of Use. This is a secondary source; primary ToS text was not retrieved. — [Udeler guide](https://www.primebook.in/blog/how-to-download-udemy-videos-using-udeler)
|
||||
- **University LMS terms (2U-run programs)** typically prohibit mirroring materials on other servers without permission. This is a general analogue, not a vendor contract. — [e.g. USC/2U Terms](https://2usc.rossieronline.usc.edu/terms)
|
||||
|
||||
### Inferences
|
||||
- The industry pattern is **"metadata local, media remote"**: catalog sync via API, launch via LTI or a SCORM stub, and completion back via xAPI, LTI AGS, or a reporting API. An open-source platform targeting universities and companies needs:
|
||||
1. An LTI 1.3 Advantage **consumer/platform** role (deep linking, AGS, NRPS), to embed LinkedIn Learning and others.
|
||||
2. A SCORM 1.2/2004 player and AICC support.
|
||||
3. An xAPI **LRS endpoint** with OAuth, so Udemy Business, Go1, and others can push statements.
|
||||
4. A pluggable catalog-connector framework (Udemy Business API, LinkedIn Learning API, Go1 API, Coursera for Business).
|
||||
5. User identity matching by email or an external LMS user ID, propagated through SSO.
|
||||
- Being an **LTI tool provider** as well lets the platform's own courses embed into Canvas or Moodle at universities.
|
||||
- **Licensed content gating:** vendor integrations are often limited to top tiers (Udemy xAPI requires the Enterprise plan; Go1 export is an add-on). Organizations pay extra for interoperability. An open-source hub that handles these standards for free has a clear value proposition, but it still depends on the customer holding the right vendor tier.
|
||||
- "Host purchased courses" for **individuals and homelabbers** realistically means organizing content they own or are licensed to keep: DRM-free purchases (many indie course sellers ship MP4/ZIP), their own recordings, or open courseware (MIT OCW etc.). Ingesting ripped Udemy or Pluralsight downloads is likely a ToS violation, and the project should avoid positioning around it. [Legal assessment not verified; no primary ToS text was retrieved.]
|
||||
|
||||
### Gaps
|
||||
- Primary Udemy Business, LinkedIn Learning, Pluralsight, and Coursera for Business contract or ToS text on self-hosting video, caching, or offline use was **not retrieved** (pages 403'd or did not surface). The claim "no self-hosting of video files" rests on inference from the integration architecture, not quoted license text.
|
||||
- Coursera for Business/Campus LMS integration specifics (LTI, Canvas/Blackboard, SCORM) and Pluralsight LMS connectors were not researched in this session.
|
||||
- No AICC support statement was found for LinkedIn Learning.
|
||||
|
||||
## (c) Unmet needs, pain points, and demand for self-hosted alternatives
|
||||
|
||||
### Takeaway
|
||||
There is a real but under-served niche for a "Plex for courses": existing projects are few and immature, and individuals currently use VLC or MPV with no progress tracking. Institutional open-source LMSs (Moodle, Open edX, Chamilo) are seen as heavy or frustrating despite free licenses. Market turmoil (Coursera–Udemy merger, Pluralsight lender takeover) adds vendor-concentration and continuity risk that a self-hosted platform can address.
|
||||
|
||||
### Cited Findings
|
||||
- **uCourse (Codeberg)** was built because legally downloaded video courses can be played in MPV or VLC, but that approach lacks library management, progress tracking, and streaming on the go. It treats each sub-folder as a course and runs via Docker. Its README says it is unfinished; the last commits appear to be from late 2023 (dormant). — [uCourse on Codeberg](https://codeberg.org/vlfldr/ucourse)
|
||||
- **Other generic options** are not course-aware (no per-lesson completion, no course structure):
|
||||
- HomeBrewPlex is a lightweight Node "Plex replacement". — [HomeBrewPlex](https://git.jrtechs.net/jrtechs/jrtechs-HomeBrewPlex/src/branch/master/package.json)
|
||||
- Jellyfin, Streama, and Frames are general self-hosted video servers. — [Jellyfin](https://railway.com/deploy/jellyfin-media-server.md); [Streama](https://best-of-web.builder.io/library/streamaserver/streama); [selfhostyourself video servers](https://selfhostyourself.com/categories/video-media-servers/page/3)
|
||||
- **Download tools such as Udeler and udemy-dl** save courses into per-course folders. This shows a user base holding local course libraries that need a player and organizer. — [Udeler guide](https://en.ubunlog.com/download-udemy-course-videos-with-udeler/); [udemy-dl on PyPI](https://pypi.org/project/udemy-dl/0.2.0/)
|
||||
- **Open-source LMS sentiment:** a CTO-oriented guide warns that "teams choosing Moodle or Open edX because the licence is free often end up frustrated". This is opinion. — [Coursebox: best open-source LMS](https://www.coursebox.ai/blog/best-open-source-learning-management-systems); see also [Geekflare: Moodle alternatives](https://geekflare.com/software/open-source-lms-moodle-alternatives/)
|
||||
- **Chamilo** is the most-cited Moodle alternative (GPL), with progress analytics, gamification, and certificates. It is a PHP/MariaDB stack. — [AlternativeTo: Moodle alternatives](https://alternativeto.net/software/moodle/?p=3); [Railway Chamilo template](https://railway.com/deploy/chamilo.md)
|
||||
- **Self-hosted video does not scale** on typical web hosting, per LearnDash. This is a pain point for self-hosted course platforms that serve video from the app server and points to a need for HLS transcoding and object storage. — [LearnDash video management best practices](https://learndash.com/support/kb/cloud/uncategorized/video-management-best-practices/)
|
||||
- **Coursera–Udemy merger:**
|
||||
- Announced December 17, 2025 as an all-stock deal. Udemy holders receive 0.800 COUR shares per share, for an implied combined equity value of about $2.5B.
|
||||
- The combined company operates under the Coursera name, with Greg Hart as CEO and Andrew Ng as chairman.
|
||||
- A Built In page says it closed on May 11, 2026; this is the only closing source found and was not independently confirmed.
|
||||
|
||||
— [Business Wire announcement](https://www.businesswire.com/news/home/20251217155893/en/Coursera-to-Combine-with-Udemy-to-Empower-the-Global-Workforce-with-Skills-for-the-AI-Era); [GovTech](https://www.govtech.com/education/higher-ed/coursera-and-udemy-to-merge-in-2-5b-deal); [Built In Chicago (closing claim)](https://www.builtinchicago.org/company/udemy/faq/stability-growth)
|
||||
- **Pluralsight:**
|
||||
- Vista Equity took it private for $3.5B in April 2021.
|
||||
- Vista wrote off its full value in May 2024.
|
||||
- In August 2024, lenders led by Blue Owl Capital and Ares Management took full ownership, wiping out Vista's equity.
|
||||
- Co-founder and CEO Aaron Skonnard left in April 2024 and Chris Walters succeeded him. Wikipedia's infobox lists Erin Gajdalo as CEO, which conflicts with the body text and is unresolved.
|
||||
- In July 2024 it announced a 17% workforce cut and a headquarters move from Utah to Westlake, Texas. A Dallas Innovates piece (July 2025) notes two CEO changes since the take-private.
|
||||
|
||||
— [Wikipedia: Pluralsight](https://en.wikipedia.org/wiki/Pluralsight); [Dallas Innovates](https://dallasinnovates.com/tag/launchcode/)
|
||||
- **Earlier Pluralsight layoffs:** about 400 employees (about 20%) in December 2022 (older). — [Business Standard](https://www.business-standard.com/article/companies/online-education-company-for-it-services-pluralsight-lays-off-400-employees-122121300142_1.html)
|
||||
|
||||
### Inferences
|
||||
- **Vendor concentration and continuity risk is rising.** Udemy is folding into Coursera, and Pluralsight went through a distressed lender takeover. Organizations that built LMS integrations against Udemy Business APIs, or that rely on Pluralsight, face roadmap and pricing uncertainty. This argues for a vendor-neutral, self-hosted hub that aggregates multiple content sources via open standards (LTI/SCORM/xAPI/cmi5), so customers can switch providers without losing learner history. Progress data can live in the organization's own LRS.
|
||||
- **Differentiators by audience:**
|
||||
- **Homelabbers and individuals:** zero-config "point at a folder" ingestion (auto-detect course/section/lesson from folder and file names, subtitles, PDFs/attachments), resume and progress, playback speed, notes and bookmarks, transcripts via local Whisper, mobile/PWA streaming with offline sync, multi-user households, Docker one-liner, low resource use. Existing options (uCourse, Jellyfin) do not cover this well.
|
||||
- **Universities:**
|
||||
- LTI 1.3 tool provider and consumer.
|
||||
- Canvas/Moodle coexistence rather than replacement.
|
||||
- SSO via SAML/Shibboleth/eduGAIN [UNVERIFIED-BACKGROUND requirement].
|
||||
- Data sovereignty (GDPR/FERPA).
|
||||
- Accessibility (captions, WCAG).
|
||||
- Local AI tutors so student data is not sent to vendors.
|
||||
- Open-standard credentials such as Open Badges 3.0 / W3C VCs [UNVERIFIED-BACKGROUND].
|
||||
- **Companies:**
|
||||
- SSO and SCIM.
|
||||
- HRIS sync.
|
||||
- Catalog connectors for Udemy Business, LinkedIn Learning, and Go1.
|
||||
- xAPI LRS.
|
||||
- Skills taxonomy and assessments (Skill IQ-like).
|
||||
- Compliance-training assignment and due dates.
|
||||
- Manager dashboards.
|
||||
- Self-hosted labs.
|
||||
- AI role-play using a company-chosen LLM.
|
||||
- **Pricing pain:** vendors gate interoperability behind top tiers (Udemy Enterprise plan for xAPI; Go1 export add-on). An OSS hub could provide that layer for free.
|
||||
|
||||
### Gaps
|
||||
- **Reddit and HN threads were not found** by this session's searches. Two `site:reddit.com` searches returned no threads. Direct quotes from r/selfhosted or r/homelab ("self-hosted Udemy", "Plex for courses") and from HN ("Ask HN: Moodle alternative") remain unverified. Follow-up: search reddit.com directly, or GitHub topics such as "course-player" / "local-course-player".
|
||||
- **No 2025–2026 primary data on:** LXP pricing (Degreed, Docebo, 360Learning, TalentLMS), Pluralsight 2025–2026 price changes or a Flow divestiture, or Coursera post-merger product consolidation (e.g. whether Udemy Business has been rebranded).
|
||||
- **Not researched:** other GitHub "local course player" projects. Searches surfaced only uCourse; projects may exist under other names.
|
||||
- **Coursera–Udemy closing date** (May 11, 2026) rests on one secondary source.
|
||||
@@ -0,0 +1,239 @@
|
||||
# Established open-source LMS platforms (Open edX, Moodle, Canvas, Sakai, ILIAS, Chamilo) as a base, fork target, or reference for a self-hostable modern course platform
|
||||
|
||||
Research date: 2026-10-30. Method: web search and fetch, plus live GitHub API queries run on 2026-10-30 (repo metadata, release/tag lists, and `stats/participation` commit counts for the last 52 weeks). Budget was about 15 tool calls, so many feature-level details (standards, SSO, video) could NOT be re-verified this session. Those details appear only under "Inferences", marked **[background knowledge, unverified 2026]**, or under Gaps. The report writer should treat them accordingly.
|
||||
|
||||
## Q1. License, governance, backing org, release cadence, latest release (late 2026), GitHub health
|
||||
|
||||
### Takeaway
|
||||
All six are copyleft or permissive open source and all are active in late 2026, with one exception: the public Canvas repo has had no pushes since 30 Apr 2026. Moodle (GPLv3), Open edX (AGPLv3) and Chamilo (GPLv3) have the healthiest release trains. Open edX shipped Verawood (Jul 2026). Moodle 5.3 LTS shipped Oct 2026. Chamilo skipped a stable "2.0" and shipped v3.0.0 in Sep 2026. Two licenses carry network-use obligations: AGPL (Open edX, Canvas, Tutor) affects SaaS forks, whereas GPL (Moodle, ILIAS, Chamilo) does not trigger on network use. Sakai's ECL-2.0 is permissive (Apache-derived).
|
||||
|
||||
### Cited Findings
|
||||
**GitHub snapshot (queried 2026-10-30 via GitHub API)**
|
||||
| Repo | License | Stars / forks | Last push | Commits, last 52 wks | Latest tags/releases |
|
||||
|---|---|---|---|---|---|
|
||||
| openedx/edx-platform | AGPL-3.0 | 8,211 / 4,378 | 2026-10-29 | 1,254 | named releases are branches/tags (see Verawood below) |
|
||||
| moodle/moodle | GPL-3.0 | 7,561 / 7,690 | 2026-10-21 | 3,228 | v5.3.0 (after rc1, rc2), v5.2.4 |
|
||||
| instructure/canvas-lms | AGPL-3.0 | 6,889 / 3,057 | **2026-04-30** | 4,314 (all before May) | release/2026-05-20.143, v5.14.2 |
|
||||
| sakaiproject/sakai | ECL-2.0 | 1,240 / 1,019 | 2026-10-29 | 823 | 25.2, 25.1, 25.0 |
|
||||
| ILIAS-eLearning/ILIAS | GPL-3.0 | 505 / 429 | 2026-10-29 | 2,188 | v11.5, v10.12, v9.24 (all 2026-10-06); v12.0_alpha tag |
|
||||
| chamilo/chamilo-lms | GPL-3.0 | 1,019 / 565 | 2026-10-29 | 5,950 | v3.0.1 (2026-09-19), v3.0.0 (2026-09-09), v1.11.42 (2026-10-28) |
|
||||
| overhangio/tutor | AGPL-3.0 | 1,130 / 508 | 2026-10-29 | n/a | v22.0.3 (2026-10-27); v21.0.10 and v20.0.6 still patched |
|
||||
Sources: [edx-platform](https://github.com/openedx/edx-platform), [moodle](https://github.com/moodle/moodle), [canvas-lms](https://github.com/instructure/canvas-lms), [sakai](https://github.com/sakaiproject/sakai), [ILIAS](https://github.com/ILIAS-eLearning/ILIAS), [chamilo-lms](https://github.com/chamilo/chamilo-lms), [tutor](https://github.com/overhangio/tutor)
|
||||
|
||||
**Open edX**
|
||||
- Verawood is the 22nd community release. The official notes say it shipped on 30 July 2026. Earlier doc versions and a third-party article said June 2026, so it slipped — [Verawood release notes](https://docs.openedx.org/en/latest/community/release_notes/verawood.html); [edunext versions article](https://www.edunext.co/articles/latest-open-edx-versions/) (gives June 2026, which conflicts)
|
||||
- The release working group refused to ship Verawood until test completion was above 80% and no blockers remained — [BTR 2026-06-22 meeting notes](https://openedx.atlassian.net/wiki/spaces/COMM/pages/6497271814/BTR+2026-06-22+Meeting+notes)
|
||||
- Release/verawood.1 was tagged, with Tutor v22.0.0 packaging following — [discuss.openedx.org announcement](https://discuss.openedx.org/t/new-release-open-edx-verawood-1-is-out/19183). Tutor v22.0.3 was out by 2026-10-27 — [tutor releases](https://github.com/overhangio/tutor)
|
||||
- The cadence is two named releases a year: Ulmo (Dec 2025), then Verawood (mid-2026). Upcoming names are Willow, then Xylon (June 2027) — [edunext](https://www.edunext.co/articles/latest-open-edx-versions/); [Verawood dev/op notes](https://docs.openedx.org/en/latest/community/release_notes/verawood/dev_op_release_notes.html)
|
||||
- Verawood product highlights: more granular team management, an AI Extensions Framework, a new Studio sidebar, and a new Instructor Dashboard — [Verawood product notes](https://docs.openedx.org/en/latest/community/release_notes/verawood/feature_release_notes.html)
|
||||
|
||||
**Moodle**
|
||||
- Moodle 5.1 was released 6 Oct 2025. Bug fixes run until 5 Oct 2026 — [MoodleDocs FR version history](https://docs.moodle.org/fr/Historique_des_versions)
|
||||
- Moodle 5.2 was released 20 Apr 2026 as a standard (non-LTS) release. One mirror lists 29 May, which conflicts — [installatron feed](https://installatron.com/updatefeed/moodle_lms_5_2); [Moodle integration forum](https://moodle.org/mod/forum/view.php?id=7966)
|
||||
- Moodle 5.3 is the new LTS. A core-dev post gives the release date as 5 Oct 2026, and the GitHub tag v5.3.0 exists. With 5.3 out, 5.0 is out of support and 5.1 is security-only. 4.5 (the previous LTS) has security support until 4 Oct 2027 — [moodle.org integration post](https://moodle.org/course/cms/7135/next); [endoflife article](https://endoflife.ai/article-moodle-50-eol-october-2026); [moodle GitHub tags](https://github.com/moodle/moodle)
|
||||
- The cadence is two major releases a year (April and October), with every fourth release designated LTS — inferred from the 5.0 → 5.3 sequence above
|
||||
|
||||
**Canvas LMS**
|
||||
- License is AGPL-3.0. The public repo's last push was 2026-04-30 (the last commits are translation and accessibility tweaks), although release tags dated 2026-05-20 exist. No public announcement of discontinuation was found — [canvas-lms repo](https://github.com/instructure/canvas-lms); [ossinsight](https://ossinsight.io/analyze/instructure/canvas-lms)
|
||||
- A secondary source (Portuguese review site, unverified) reports that in April 2026 Instructure restructured Canvas into three tiers (Core, Plus, Next) with AI features, and that the open-source self-hosted version still exists — [bit4learn](https://bit4learn.com/pt/?p=56)
|
||||
|
||||
**Sakai**
|
||||
- Sakai 25 moved to JDK 17. Sakai 25.2 is a maintenance release with 237 improvements and 4 security fixes (Dr. Chuck's blog, June 2026) — [dr-chuck.com June 2026](https://www.dr-chuck.com/csev-blog/2026/06/)
|
||||
- License is ECL-2.0 (Educational Community License, which is Apache-2.0-derived and permissive) — [sakai repo](https://github.com/sakaiproject/sakai)
|
||||
|
||||
**ILIAS**
|
||||
- ILIAS maintains three parallel supported branches: on 2026-10-06 it shipped v11.5, v10.12 and v9.24, and a v12.0_alpha tag exists — [ILIAS repo](https://github.com/ILIAS-eLearning/ILIAS); [newreleases v11.3](https://newreleases.io/project/github/ILIAS-eLearning/ILIAS/release/v11.3)
|
||||
|
||||
**Chamilo**
|
||||
- Chamilo 2.0 alpha 1 ("Babaorum") came out 2024-07-20 with the warning "should not be used in production unless you have a full-time dedicated IT team" — [chamilo changelog mirror, alpha 1](https://forger.sitiv.fr/chamilo/chamilo-lms/raw/tag/v2.0.0-alpha.1/public/documentation/changelog.html)
|
||||
- There is no stable 2.0 tag. The project went v3.0.0-RC.1, then beta.1 (2026-08-13), then v3.0.0 (2026-09-09), then v3.0.1 (2026-09-19). The legacy 1.11.x line is still being patched (v1.11.42, 2026-10-28) — [chamilo-lms releases](https://github.com/chamilo/chamilo-lms)
|
||||
- I found no explanation for jumping from 2.0 to 3.0. Historically the "2.0" name was taken by the separate Chamilo LCMS codebase, which named its next version "3.0" — [BeezNest 2010 post](https://beeznest.com/nl/node/615) (this is old and may not be the current reason)
|
||||
- Backing org: the Chamilo Association, with BeezNest as the main commercial publisher — [beeznest](https://beeznest.com/nl/node/1811)
|
||||
|
||||
### Inferences
|
||||
- AGPL (Open edX, Canvas, Tutor, eox-tenant) requires publishing source modifications to users of a hosted SaaS fork. GPLv3 (Moodle, ILIAS, Chamilo) only requires source on distribution, so a modified SaaS fork can stay private, although plugins distributed to customers must be GPL. Sakai's ECL permits proprietary forks. **[background knowledge of license terms; not legal advice]**
|
||||
- Open edX governance: the Axim Collaborative (non-profit, formed after 2U acquired edX in 2021) stewards the project, and community working groups run releases (the BTR group cited above). **[background knowledge, unverified 2026]**
|
||||
- Moodle governance: Moodle HQ (Moodle Pty Ltd, a B-Corp) controls core, and certified Moodle Partners fund it. **[background knowledge]**
|
||||
- Sakai is governed by the Apereo Foundation. ILIAS is governed by the ILIAS open source e-Learning e.V. (Germany), whose membership is mostly German/Swiss universities. **[background knowledge]**
|
||||
- Canvas: the commit counter (4,314 in 52 weeks) shows that before 30 Apr 2026 Instructure was mirroring internal commits at a high rate. Six months of silence afterwards is a serious red flag for anyone planning to build on Canvas OSS, though it could be a mirroring pause rather than a policy change. This needs verification.
|
||||
- By commit volume Chamilo is very active (5,950 commits). Its contributor base is narrow and BeezNest-centric, and stars are low (about 1k).
|
||||
|
||||
### Gaps
|
||||
- No official Instructure statement on the open-source repo after April 2026 was found. Contributor counts (unique humans) per repo were not retrieved; only commit totals were.
|
||||
- Official Moodle 5.3 release notes were not fetched directly. The date comes from a moodle.org forum post plus the GitHub tag.
|
||||
- The exact date of Sakai 25.0 was not found.
|
||||
|
||||
## Q2. Tech stack, architecture, and operability (Docker/Tutor, resources, homelab vs. scale)
|
||||
|
||||
### Takeaway
|
||||
Moodle, ILIAS and Chamilo are PHP monoliths that can run on a single small VM. Open edX is a Python/Django platform plus many React MFEs, best run via Tutor (Docker Compose or Kubernetes), and it is the heaviest to operate. Canvas is a Rails plus React monolith with no official Docker production image and a heavy build. Sakai is a large Java/Tomcat webapp.
|
||||
|
||||
### Cited Findings
|
||||
**Open edX**
|
||||
- Verawood made new MFEs the default (Catalog MFE and Instructor Dashboard MFE). Willow will remove the legacy catalog — [Verawood dev/op notes](https://docs.openedx.org/en/latest/community/release_notes/verawood/dev_op_release_notes.html)
|
||||
- The frontend architecture is still changing. Verawood is the first release to support "frontend-base", and "by Xylon (June 2027), all Open edX MFEs will have been converted to frontend-base apps". This is ongoing churn for anyone who customizes UI — [same](https://docs.openedx.org/en/latest/community/release_notes/verawood/dev_op_release_notes.html)
|
||||
- In Verawood, "PostgreSQL is now supported as a backend (fresh installs only)". MySQL/MariaDB was historically the only option. Search requires Typesense Server >= v30.0. Analytics (Aspects v4.0.0, which uses Python 3.12) is a breaking upgrade. Course IDs became case-insensitive-unique, and the migration can fail with IntegrityError on duplicates — [same](https://docs.openedx.org/en/latest/community/release_notes/verawood/dev_op_release_notes.html)
|
||||
- Built-in XBlocks (including the LTI block) were extracted to `xblocks-core`, and Legacy Libraries are deprecated in favour of Content Libraries — [same](https://docs.openedx.org/en/latest/community/release_notes/verawood/dev_op_release_notes.html)
|
||||
- Tutor is the community-supported Docker distribution "suited for both production and development". The official docs expect operators to be comfortable with the CLI and with diagnosing system problems — [Open edX installation docs](https://docs.openedx.org/en/ulmo/site_ops/install_configure_run_guide/installation/index.html)
|
||||
- The pre-Tutor native install had "very high complexity" and no upgrade path. Tutor's upgrade commands were created to address that — [discuss: native installation thread](https://discuss.openedx.org/t/lets-talk-about-the-native-installation/3269); [Tutor FAQ](https://docs.tutor.edly.io/faq.html); [OEP-45 decision](https://docs.openedx.org/projects/openedx-proposals/en/latest/_sources/architectural-decisions/oep-0045/decisions/0001-tutor-as-replacement-for-edx-configuration.rst)
|
||||
- Comparison blogs describe it as "a microservices platform, not a PHP app" with a DevOps burden far greater than Moodle's — [selfhosting.sh](https://selfhosting.sh/best/education/); [fulcrum.rocks](https://fulcrum.rocks/blog/how-to-choose-lms-between-moodle-canvas-openedx) (secondary/vendor sources)
|
||||
|
||||
**Moodle**
|
||||
- Moodle 5.1 restructured the codebase into a new `/public` webroot directory and added a URL routing engine — [Open LMS 5.0/5.1 highlights](https://support.openlms.net/hc/en-us/articles/25767173553180-Moodle-5-0-And-5-1-Essential-Release-Highlights)
|
||||
- Comparison sources consider it the easiest to self-host, via Docker (the Bitnami image is commonly cited) — [thefrontkit Moodle alternatives 2026](https://thefrontkit.com/blogs/moodle-alternatives-2026); [selfhosting.sh](https://selfhosting.sh/best/education/). Railway offers a one-click template — [railway](https://railway.com/deploy/moodle)
|
||||
|
||||
**Canvas**
|
||||
- "requires building from source — there's no official Docker image" and "requires 6+ GB RAM for asset compilation during setup". The open-source core is described as the hardest of the three (with Moodle and Open edX) to operate independently — [selfhosting.sh](https://selfhosting.sh/best/education/); [fulcrum.rocks](https://fulcrum.rocks/blog/how-to-choose-lms-between-moodle-canvas-openedx) (secondary sources)
|
||||
- Primary language is Ruby — [GitHub](https://github.com/instructure/canvas-lms)
|
||||
|
||||
**Sakai**
|
||||
- Java, upgraded to JDK 17 in Sakai 25 — [dr-chuck.com](https://www.dr-chuck.com/csev-blog/2026/06/); [GitHub](https://github.com/sakaiproject/sakai)
|
||||
|
||||
**Chamilo**
|
||||
- 3.0 (formerly the 2.0 branch) is based on Chamilo 1.11.10. It uses Symfony as the main controller, "except for some legacy tools not yet converted", with Vue.js (Vue 3) and TailwindCSS on the frontend — [changelog mirror](https://forger.sitiv.fr/chamilo/chamilo-lms/src/commit/28c536db3e6315905f991aba9cbd0f4e0c7a50b8/public/documentation/changelog.html); [BeezNest](https://beeznest.com/nl/node/1811)
|
||||
- v3.0.0 added DB/web server/PHP caching metrics in admin, signed update-package verification, and an "experimental web update feature" — [v3.0.0 release notes](https://github.com/chamilo/chamilo-lms/releases/tag/v3.0.0)
|
||||
|
||||
### Inferences
|
||||
- **[background knowledge, unverified 2026]** Open edX: edx-platform (LMS plus CMS/Studio) is a Django monolith. The surrounding services are MySQL, MongoDB (course structure and blob storage, historically), Redis, Meilisearch/Typesense, Celery workers, Caddy, and many React MFEs. Tutor's often-quoted minimum is about 4 GB RAM / 2 CPU with 8 GB recommended. In practice a single-node homelab is possible but uses 8 GB or more. Tutor's k8s mode (`tutor k8s`) serves scale.
|
||||
- **[background knowledge]** Moodle 5.x needs PHP 8.2+ and runs on PostgreSQL, MySQL, MariaDB or MSSQL. It is a classic server-rendered monolith with Mustache templates and AMD/ES JS, and needs a cron job. It runs comfortably on 2 GB RAM for small sites. Moodle HQ ships `moodle-docker` for development only, with no official production image.
|
||||
- **[background knowledge]** Canvas needs PostgreSQL, Redis, and a delayed-jobs worker, and serves a React/InstUI frontend. Real-time features, RCE, Canvas Studio and New Quizzes are separate services, several of them closed.
|
||||
- **[background knowledge]** ILIAS is PHP plus MySQL/MariaDB and is server-rendered with its own "KitchenSink" UI framework. It has no official Docker production image, though community images exist.
|
||||
- Ranking for homelab single-node: Moodle ≈ Chamilo ≈ ILIAS (easy) > Sakai (moderate, JVM memory) > Open edX via Tutor (heavy but scripted) > Canvas OSS (hard, unofficial).
|
||||
|
||||
### Gaps
|
||||
- Tutor's official minimum RAM/CPU could not be retrieved; the docs fetch returned empty and the repo path lookup failed.
|
||||
- No primary-source resource numbers were found for Sakai, ILIAS or Chamilo 3.
|
||||
|
||||
## Q3. Extensibility (plugins, theming, APIs, headless) and multi-tenancy
|
||||
|
||||
### Takeaway
|
||||
Moodle has the deepest plugin ecosystem, and Open edX the most "platform-like" extension points (XBlocks, Django plugins, hooks/filters, frontend slots). Canvas is extended mainly through LTI and its REST/GraphQL API rather than in-code plugins. Multi-tenancy comes in different forms: Open edX through eox-tenant (eduNEXT, AGPL), Moodle through commercial Workplace or open-source IOMAD, and Chamilo through "multi-URL", which became hierarchical in 3.0.
|
||||
|
||||
### Cited Findings
|
||||
- **Open edX extension points:** frontend plugin slots (for example `org.openedx.frontend.slot.instructorDashboard.tabs.v1`) replace older server-side filters such as `org.openedx.learning.instructor.dashboard.render.started.v1`. That is evidence of a hooks/filters plus slots extension model that is still changing. Verawood also adds an "AI Extensions Framework" — [Verawood dev/op notes](https://docs.openedx.org/en/latest/community/release_notes/verawood/dev_op_release_notes.html); [product notes](https://docs.openedx.org/en/latest/community/release_notes/verawood/feature_release_notes.html)
|
||||
- **Open edX AuthZ RBAC** is opt-in for course authoring, and the legacy permission system is planned for removal in Xylon — [Verawood dev/op notes](https://docs.openedx.org/en/latest/community/release_notes/verawood/dev_op_release_notes.html)
|
||||
- **eox-tenant** (Open edX multi-tenancy plugin by eduNEXT): AGPL-3.0, 13 stars, latest release v14.4.0 on 2026-06-30. It is maintained but niche — [eduNEXT/eox-tenant](https://github.com/eduNEXT/eox-tenant)
|
||||
- **Moodle Workplace** offers multi-tenancy with "completely isolated" tenants, each with its own look, structure, users and learning entities. It is sold only through partners, with no public price. Partner estimates are about $6k/yr for 250 users, $12k/yr for 1,000 users, and $42k/yr for 10,000 users — [enovation](https://www.enovation.ie/unlock-the-power-of-multi-tenancy-with-moodle-workplace/); [compono 2026](https://www.compono.com/articles/moodle-workplace-pricing-hidden-costs-2026) (estimates, vendor content)
|
||||
- **IOMAD** is free, open-source multi-tenant Moodle maintained by E-Learn Design. It supports per-tenant delegation of admin and reporting — [e-learndesign](https://www.e-learndesign.scot/moodle-quick-guides/moodle-vs-multi-tenancy-moodle/)
|
||||
- **MoodleCloud** reportedly no longer has a permanent free plan. Paid tiers run about $130–200/yr for 50 users, with a 750-user cap, no custom plugins, and no multi-tenancy — [compono](https://www.compono.com/articles/moodle-workplace-pricing-hidden-costs-2026); [synergy-learning Sept 2026](https://synergy-learning.com/blog/moodlecloud-vs-moodle-workplace-what-are-the-differences/) (secondary sources)
|
||||
- **Chamilo 3.0** added "hierarchical URLs and a global multi-URL panel" (its multi-portal/tenant mechanism), an MCP server for AI clients (course creation, documents, tests, learning paths, progress, tickets), and an "external API authentication mechanism for external plugins". It also re-enabled all 1.11.x plugins — [v3.0.0 release notes](https://github.com/chamilo/chamilo-lms/releases/tag/v3.0.0). BeezNest claims a "full REST API" — [BeezNest](https://beeznest.com/nl/node/1811) (vendor marketing)
|
||||
- **Moodle** AI subsystem: 5.1 added DeepSeek as a provider alongside OpenAI and others — [Open LMS highlights](https://support.openlms.net/hc/en-us/articles/25767173553180-Moodle-5-0-And-5-1-Essential-Release-Highlights)
|
||||
|
||||
### Inferences
|
||||
- **[background knowledge, unverified 2026]**
|
||||
- Moodle has about 2,000 plugins in its directory across roughly 50 plugin types (mod, block, auth, enrol, local, theme, paygw, etc.), plus a Web Services REST/SOAP API (function-based, token auth). It is not truly headless, though the Moodle App uses that API.
|
||||
- Open edX has REST APIs across services and a "headless" pattern used by some providers, but no GraphQL.
|
||||
- Canvas has a very complete, well-documented REST API plus partial GraphQL, and accounts/sub-accounts give hierarchical (not fully isolated) tenancy. It is the best API for headless use, but its OSS future is uncertain.
|
||||
- Sakai has a tool/webapp plugin model and REST APIs, with tenancy through "sites".
|
||||
- ILIAS has a plugin slot system and SOAP, with REST still limited.
|
||||
|
||||
### Gaps
|
||||
- Moodle plugin counts, Canvas GraphQL coverage, and Sakai/ILIAS API details were not verified this session.
|
||||
|
||||
## Q4. Content standards (SCORM, xAPI, cmi5, LTI 1.3, QTI, Common Cartridge)
|
||||
|
||||
### Takeaway
|
||||
Only partially verified this session. Chamilo 3.0 made notable SCORM 2004 runtime improvements, and Open edX moved its LTI block into `xblocks-core`. The other standards claims below are background knowledge.
|
||||
|
||||
### Cited Findings
|
||||
- Chamilo 3.0 added SCORM 2004 `adl.nav.request` (except jump/choice), per-objective `progress_measure`, `cmi.interactions.n.description`, learner_preference fields, and a setting to auto-complete bad SCOs. Its course export targets `.mbz`, the Moodle backup format — [v3.0.0 release notes](https://github.com/chamilo/chamilo-lms/releases/tag/v3.0.0)
|
||||
- Open edX's LTI Block was extracted to `xblocks-core`, and the built-in code is deprecated — [Verawood dev/op notes](https://docs.openedx.org/en/latest/community/release_notes/verawood/dev_op_release_notes.html)
|
||||
|
||||
### Inferences
|
||||
- **[background knowledge, unverified 2026]**
|
||||
- **Moodle:** SCORM 1.2 is solid but SCORM 2004 support is partial. It is an LTI 1.3/Advantage-certified tool and platform. It imports/exports IMS Common Cartridge, has no native xAPI/cmi5 player (plugins and Logstore xAPI exist), and imports QTI only via plugins.
|
||||
- **Open edX:** SCORM via the community/Overhang `openedx-scorm-xblock`. LTI 1.3 consumer via the LTI XBlock, plus LTI provider support. Aspects (ClickHouse/Superset) is xAPI-based analytics. Weak on CC/QTI.
|
||||
- **Canvas:** strong LTI 1.3/Advantage (Instructure is a major IMS member), Common Cartridge and QTI import/export. SCORM requires a separate (historically hosted) service.
|
||||
- **Sakai:** LTI 1.3 is strong (Dr. Chuck led LTI work). It supports Common Cartridge and QTI in Samigo, and SCORM through a player tool.
|
||||
- **ILIAS:** strong certified SCORM 2004 support, with xAPI/cmi5 objects (ILIAS was an early cmi5 adopter), LTI consumer and provider, and QTI in tests.
|
||||
|
||||
### Gaps
|
||||
- No primary-source verification of IMS certification status (LTI Advantage, CC, QTI) per platform in 2026. IMS's product directory should be checked.
|
||||
|
||||
## Q5. Video, commerce/payments, SSO, certificates, learning paths/programs, assessments, analytics
|
||||
|
||||
### Takeaway
|
||||
Commerce is weakest exactly where a "Udemy-style" platform needs it most. Open edX deprecated its legacy ecommerce service, last tagged for Redwood, and the replacement (Commerce Coordinator) remains unclear. Moodle has payment-gateway plugins. Chamilo has a BuyCourses plugin with subscriptions. Video transcoding/HLS is not native to any of these platforms (unverified).
|
||||
|
||||
### Cited Findings
|
||||
- **Open edX ecommerce:** legacy ecommerce (Django Oscar) was deprecated (DEPR #22), with the 1 Apr 2022 deprecation target missed. Docs say it was "last tagged for the Redwood release" and "We are not fixing bugs or developing new features for it." The replacement was meant to be the Commerce Coordinator, a modular Django service with plugin apps for payment-provider webhooks, driven by 2U's needs. Stripe was planned for Palm — [DEPR #22 thread](https://discuss.openedx.org/t/deprecation-removal-ecommerce-service-depr-22/6839); [Oscar stack deprecation](https://discuss.openedx.org/t/oscar-based-ecommerce-stack-deprecation/13760); [ecommerce deprecated docs](https://docs.openedx.org/en/ulmo/site_ops/install_configure_run_guide/ecommerce-deprecated/index.html); [Commerce Coordinator talk 2023](https://openedx.atlassian.net/wiki/download/attachments/3734077463/Commerce+Coordinator+Lightning+Talk+-+Open+edX+Conference+2023+-+Phil+Shiu.pdf?version=1)
|
||||
- **Open edX Verawood:** the Learning MFE's notifications panel was replaced by an "Upgrade" widget. Notifications are now on by default, with digests via Celery. Custom video storage settings in edx-val are deprecated in favour of Django `STORAGES`. The temporary SAML toggle was removed (SAML support is now permanent) — [Verawood dev/op notes](https://docs.openedx.org/en/latest/community/release_notes/verawood/dev_op_release_notes.html)
|
||||
- **Chamilo 3.0:**
|
||||
- Gradebook and certificates: certificate expiry dates and reminders, plus customizable grading modes
|
||||
- Exercises: a progressive adaptive mode and an OnlyOffice editor
|
||||
- Commerce: monthly and yearly service tabs in the BuyCourses plugin
|
||||
- Learning paths: date-based access
|
||||
- AI: an AI course analyzer, a "Student Success AI Coach", and Anthropic Claude as a provider
|
||||
- Source: [v3.0.0 release notes](https://github.com/chamilo/chamilo-lms/releases/tag/v3.0.0)
|
||||
- **Moodle 5.1:** TinyMCE usability changes, activity permalinks, and course-level AI controls — [Catalyst](https://www.catalyst-au.net/blog/improved-educators-experience-moodle-5-1); [enovation](https://www.enovation.ie/moodle-5-1-refining-the-moodle-5-experience/)
|
||||
|
||||
### Inferences
|
||||
- **[background knowledge, unverified 2026]**
|
||||
- **Moodle:**
|
||||
- Payments: core payment subsystem since 3.10, with a PayPal gateway in core and Stripe and others as plugins. "Enrolment on payment" is per course, with no cart or bundles natively.
|
||||
- SSO: SAML via auth_saml2 (Catalyst), OAuth2/OIDC in core, LDAP in core.
|
||||
- Paths and certificates: Workplace adds programs and certifications; open-source Moodle uses course completion, competencies, and the Custom Certificate (mod_customcert) plugin.
|
||||
- Analytics: core reports plus Report Builder.
|
||||
- Video: plain file or embed. Captioning and transcoding need external services or plugins (e.g. Kaltura or Panopto, both commercial).
|
||||
- **Open edX:**
|
||||
- Video: edx-val with an external video pipeline (VEDA was edX-internal). Most self-hosters embed YouTube/Vimeo or S3 HLS.
|
||||
- Certificates: built-in. Programs require course-discovery and credentials services.
|
||||
- Analytics: Aspects, built on ClickHouse and Superset.
|
||||
- SSO: third-party auth (SAML, OAuth2/OIDC) built in.
|
||||
- **Canvas:** SAML, LDAP, OIDC and CAS in core. Canvas Studio (video), New Quizzes, Catalog (commerce), Credentials and Canvas Data are proprietary or SaaS-only, so the OSS edition lacks them.
|
||||
- **Sakai and ILIAS:** strong in SAML/LDAP/Shibboleth for universities, weak in commerce. ILIAS has a shop module, historically limited.
|
||||
|
||||
### Gaps
|
||||
- Current state of the Commerce Coordinator (is it released, and is it usable outside 2U?) was not verified. No details on a Tutor Stripe plugin were found.
|
||||
- No primary verification of HLS/transcoding capabilities on any platform.
|
||||
|
||||
## Q6. UX modernity and operator pain points
|
||||
|
||||
### Takeaway
|
||||
Canvas is still regarded as having the best learner/teacher UX. Moodle remains functional but dated, despite the 4.x/5.x refreshes. Open edX's UX is modernizing but is in permanent MFE migration churn. Chamilo 3.0 offers a fresh Vue 3 UI, though it is less proven. Reddit/selfhosted first-hand reports were not retrieved.
|
||||
|
||||
### Cited Findings
|
||||
- Canvas is "consistently" rated higher for usability than Moodle by students and teachers — [selfhosting.sh](https://selfhosting.sh/best/education/); [ibl.ai comparison](https://ibl.ai/resources/comparisons/canvas-vs-moodle.md) (secondary/vendor)
|
||||
- Moodle 5.1's UI/UX changes are evolutionary, covering TinyMCE, course building and activity menus — [3rdwavemedia](https://elearning.3rdwavemedia.com/blog/whats-new-in-moodle-5-1/7254/)
|
||||
- Chamilo 2.0/3.0 redesigned the main interface with a retractable left menu, Vue 3 and Tailwind — [interoperable-europe Chamilo news](https://interoperable-europe.ec.europa.eu/collection/education-culture-and-sport/solution/chamilo-lms-fr/news/chamilo-celebre-une-etape-cle-de-son-aventure-open-source); [changelog mirror](https://forger.sitiv.fr/chamilo/chamilo-lms/src/commit/28c536db3e6315905f991aba9cbd0f4e0c7a50b8/public/documentation/changelog.html)
|
||||
- **Open edX operator pain, evidenced in primary release notes:**
|
||||
- legacy frontends removed release by release
|
||||
- MFE → frontend-base conversion through mid-2027
|
||||
- breaking analytics (Aspects v4)
|
||||
- Typesense version pinning
|
||||
- MariaDB table-lock migrations (edx-enterprise UUID conversion)
|
||||
- localStorage keys renamed without migration
|
||||
- Legacy Libraries deprecated, with forced migration
|
||||
- the release slipped on test coverage
|
||||
- Sources: [Verawood dev/op notes](https://docs.openedx.org/en/latest/community/release_notes/verawood/dev_op_release_notes.html); [BTR notes](https://openedx.atlassian.net/wiki/spaces/COMM/pages/6497271814/BTR+2026-06-22+Meeting+notes)
|
||||
- **Canvas OSS pain:** no official Docker image and a heavy build, plus the repo has been silent since April 2026 — [selfhosting.sh](https://selfhosting.sh/best/education/); [GitHub](https://github.com/instructure/canvas-lms)
|
||||
|
||||
### Inferences
|
||||
- Commonly reported complaints, **[background knowledge, not re-verified with 2026 forum threads]**:
|
||||
- Moodle: cluttered admin UX, a steep settings learning curve, and plugins breaking across major upgrades. The 5.1 `/public` move breaks web-server configs.
|
||||
- Open edX: the Tutor upgrade ritual twice a year, theming across many MFEs being laborious, and Studio authoring being clunky. Many small operators end up on paid hosting (e.g. eduNEXT, Edly, OpenCraft).
|
||||
|
||||
### Gaps
|
||||
- No r/selfhosted, r/moodle or discuss.openedx.org operator threads from 2025–2026 were retrieved (the search returned none). This should be filled by a follow-up search.
|
||||
|
||||
## Q7. Fit assessment: base, fork target, or reference
|
||||
|
||||
### Takeaway
|
||||
For a Pluralsight/Udemy-style, self-hostable, customizable course platform, the six options sort as follows:
|
||||
- **Open edX:** the strongest base for a "course marketplace/MOOC" model. It has modern React MFEs, XBlocks, certificates, Tutor, and multi-tenancy via eox-tenant. The trade-offs are heavy ops, AGPL, and commerce you must build or bolt on.
|
||||
- **Moodle:** best as a reference or for plugin-based customization. It is easy to run, but its UX/architecture is the opposite of "modern course catalog".
|
||||
- **Canvas OSS:** a risky fork target given repo inactivity and proprietary pieces. Best used as a UX/API reference.
|
||||
- **Sakai and ILIAS:** university-centric references, with ILIAS strongest on standards (unverified).
|
||||
- **Chamilo 3.0:** a dark-horse lightweight base. It is newly stable (Sep 2026), PHP/Symfony/Vue, GPL, has multi-URL tenancy, a BuyCourses plugin and an MCP server, but its maturity is unproven.
|
||||
|
||||
### Cited Findings
|
||||
- Open edX is suited to MOOC-scale delivery, while Moodle is the lowest-friction self-host — [fulcrum.rocks](https://fulcrum.rocks/blog/how-to-choose-lms-between-moodle-canvas-openedx); [selfhosting.sh](https://selfhosting.sh/best/education/)
|
||||
- See the Q1–Q6 citations for the facts underlying each judgment.
|
||||
|
||||
### Inferences
|
||||
- A fork of edx-platform is ill-advised. It moves fast, its own release notes show breaking changes every six months, and AGPL applies. Building on top of it via plugins/MFE slots, or as a headless backend, is the realistic path.
|
||||
- Moodle's GPL allows private SaaS modifications, but its server-rendered architecture makes a "modern SPA" front end a rewrite on top of web services.
|
||||
- Chamilo 3.0 warrants a hands-on evaluation as the lightest modern-stack option.
|
||||
|
||||
### Gaps
|
||||
- No hands-on validation was done. Fit judgments are synthesized from the cited facts plus flagged background knowledge.
|
||||
@@ -0,0 +1,165 @@
|
||||
# Modern / lightweight / niche open-source course platforms (state as of 30 Oct 2026)
|
||||
|
||||
Method note: GitHub numbers were pulled live from the GitHub REST API (via `gh`) on 2026-10-30. "Commits in the last 90 days" means commits on the default branch since 2026-07-30, capped at 100 (100 = "at least 100"). Feature claims come from READMEs, official docs and pricing pages. Community and Reddit sentiment was hard to find (see Gaps). WordPress.org install counts could not be fetched because the API call was blocked in this environment.
|
||||
|
||||
## 1. Governance and health: license, backing, business model, cadence, activity, abandonment signals
|
||||
|
||||
### Takeaway
|
||||
Four modern projects are clearly active in late 2026, all under AGPL-3.0: Frappe Learning (most mature and fastest moving), LearnHouse (fast moving, but explicitly open-core), ClassroomIO (very active, but pivoted to B2B "customer training" SaaS) and CourseLit (activity has slowed sharply since July 2026, effectively one maintainer). Forma LMS is alive but slow. Gibbon is active, but it is school-management software rather than a course platform. The WordPress plugins are all actively shipped and run on a freemium "Pro add-on" model.
|
||||
|
||||
### Cited Findings
|
||||
**GitHub snapshot (2026-10-30)**, from the [GitHub API](https://api.github.com/repos/frappe/lms):
|
||||
|
||||
| Project | Repo | License | Stars | Forks | Contributors | Commits last 90d | Latest release | Primary lang |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| Frappe Learning (Frappe LMS) | [frappe/lms](https://github.com/frappe/lms) | AGPL-3.0 | 3,353 | 1,565 | 96 | ≥100 | v2.68.0 (2026-10-28) | TypeScript/Vue + Python |
|
||||
| LearnHouse | [learnhouse/learnhouse](https://github.com/learnhouse/learnhouse) | AGPL-3.0 + separate Enterprise License | 2,433 | 592 | 47 | ≥100 | 1.4.3 (2026-10-25) | Python + TS |
|
||||
| ClassroomIO | [classroomio/classroomio](https://github.com/classroomio/classroomio) | AGPL-3.0 | 1,735 | 409 | 47 | ≥100 | tag v1.0.0 (commit 2026-07-07), no GitHub Releases | TypeScript |
|
||||
| CourseLit | [codelitdev/courselit](https://github.com/codelitdev/courselit) | AGPL-3.0 | 1,290 | 267 | 15 | **1** | v0.74.2 GH release (2026-07-18); commits tagged v0.74.4 (2026-07-22) | TypeScript |
|
||||
| Forma LMS | [formalms/formalms](https://github.com/formalms/formalms) | GPL (not detected by GitHub) | 37 | 26 | 66 | **0** | 4.1.43 version bump (2026-04-16); last push 2026-07-17 | PHP |
|
||||
| Gibbon | [GibbonEdu/core](https://github.com/GibbonEdu/core) | GPL-3.0 | 635 | 427 | 62 | 75 | v30.0.01 (2026-02-06) | PHP |
|
||||
| Tutor LMS (free core) | [themeum/tutor](https://github.com/themeum/tutor) | GPL-3.0 | 129 | 97 | 65 | ≥100 | v4.2.0 (2026-10-28) | PHP |
|
||||
| LearnPress | [LearnPress/LearnPress](https://github.com/LearnPress/LearnPress) | (GPL per WP.org; not detected by GitHub) | 274 | 144 | 64 | ≥100 | no GH releases | PHP |
|
||||
| Sensei LMS | [Automattic/sensei](https://github.com/Automattic/sensei) | GPL-2.0 | 596 | 216 | 135 | ≥100 | 4.26.4 (2026-09-29) | PHP |
|
||||
| LifterLMS (extra WP option) | [gocodebox/lifterlms](https://github.com/gocodebox/lifterlms) | GPL-3.0 | 211 | — | — | — | 10.3.2 (2026-10-09) | PHP |
|
||||
| MasterStudy LMS | not on GitHub under `stylemixthemes/masterstudy-lms-learning-management-system` (404); distributed through WordPress.org | GPL (WP.org) | — | — | — | — | — | PHP |
|
||||
|
||||
- Frappe Learning was started in 2021 by Frappe so it could launch Mon.School for FOSS United, because "Moodle … didn't feel right." It is built on Frappe Framework plus Frappe UI (Vue). Monetisation is managed hosting on Frappe Cloud. — [frappe/lms README](https://github.com/frappe/lms)
|
||||
- License conflict: one 2026 comparison article calls Frappe LMS "MIT". The GitHub repo reports **AGPL-3.0**, so treat the article as wrong. — [lmspedia (via search)](https://lmspedia.org/best-free-open-source-lms-platforms/); contradicted by [GitHub frappe/lms](https://github.com/frappe/lms)
|
||||
- LearnHouse is **open-core**. Its README says "AGPL-3.0. Enterprise features are available under a separate Enterprise License" and marks Payments, SSO and Multi-Org as "(Enterprise)". The repo contains an `apps/web/ee-stub` directory with SCORM stubs (`ee-stub/app/api/scorm`, `ScormActivity.tsx`), so SCORM is an EE feature too. The project is led by a single author/maintainer, "Sweave (Badr B.)". — [learnhouse README](https://github.com/learnhouse/learnhouse); [repo tree](https://github.com/learnhouse/learnhouse/tree/main/apps/web/ee-stub)
|
||||
- LearnHouse cloud pricing: Free plan; Standard "from $49/mo" (200 members, payments at 0% fee); Pro (certifications, custom domains, API access, versioning, boards/playgrounds); Enterprise custom (audit logs, SSO/OIDC, SCORM, RBAC, multi-tenancy, white-label, **self-hosted option**). The page does not say which tier features ship in the AGPL build. — [LearnHouse pricing](https://www.learnhouse.app/pricing)
|
||||
- LearnHouse v1.0 was described by its maker as "our first stable release" (Product Hunt). — [Product Hunt (via search)](https://www.producthunt.com/p/learnhouse/learnhouse-2)
|
||||
- ClassroomIO now positions itself as "an intelligent customer training platform that helps SaaS companies use education to grow their business". Its use cases are customer education, partner training and employee training. — [classroomio README](https://github.com/classroomio/classroomio)
|
||||
- ClassroomIO pricing: Basic $0 (20 students, no video upload, ClassroomIO branding); Early Adopters $35/mo (10K students, video upload, certificates, custom branding/domain, limited API); Enterprise custom (SSO, unlimited API). For "on-prem deployment, SSO, or a commercial license", the page says to contact them. In other words, the company sells commercial licenses on top of AGPL. — [ClassroomIO pricing](https://classroomio.com/pricing)
|
||||
- ClassroomIO has a `v1.0.0` tag (commit 2026-07-07) but publishes no GitHub Releases. Its LICENSE file was created 2023-12-08. — [GitHub API tags/commits](https://github.com/classroomio/classroomio/tags)
|
||||
- CourseLit pitches itself as "an open source alternative to Teachable, Thinkific, Podia". Revenue comes from a cloud version (14-day trial) and the paid **MediaLit** media service. — [courselit README](https://github.com/codelitdev/courselit)
|
||||
- CourseLit activity: the last default-branch commits were v0.74.3/v0.74.4 (2026-07-18/22) and then a single docs-link fix on 2026-10-24. 256 issues are open. Only 15 contributors in total. — [GitHub API commits](https://github.com/codelitdev/courselit/commits/main)
|
||||
- Forma LMS: latest commits are "bump version to 4.1.43" and "safe SCORM path repair" (2026-04-16), with a merge on 2026-07-17. — [GitHub formalms commits](https://github.com/formalms/formalms/commits/master)
|
||||
- awesome-selfhosted "Learning and Courses" currently lists Canvas, Chamilo, Digiscreen, Digitools, edX, Gibbon, Helium, ILIAS, INGInious, Moodle, Open eClass, OpenOLAT, QST, RELATE and RosarioSIS. It does **not** list Frappe Learning, LearnHouse, ClassroomIO or CourseLit. — [awesome-selfhosted README](https://github.com/awesome-selfhosted/awesome-selfhosted#learning-and-courses)
|
||||
- Gibbon is described as a "flexible school management platform designed to make life better for teachers, students, parents and leaders" (GPL-3.0, PHP). — [awesome-selfhosted](https://github.com/awesome-selfhosted/awesome-selfhosted#learning-and-courses)
|
||||
|
||||
### Inferences
|
||||
- Frappe Learning has the healthiest governance profile. It has a commercial backer (Frappe Technologies), a near-weekly release train (v2.68.0), about 96 contributors, and no paid EE tier inside the app; revenue comes from hosting.
|
||||
- LearnHouse is the most "modern UX" candidate, but the features a commercial course platform needs (payments, SSO, SCORM, multi-org/white-label) are closed EE. A fork would have to rebuild them. It also has bus-factor risk around one lead maintainer.
|
||||
- ClassroomIO's pivot to B2B customer training, together with its AI/MCP focus, makes it less suited to an Udemy-style marketplace. Its AGPL plus commercial-license model also suggests a CLA or dual-licensing posture (CLA not verified).
|
||||
- CourseLit shows early signs of stagnation (one commit in three months, one main developer). It is not abandoned yet, but it is a risky base.
|
||||
- Forma LMS's GitHub mirror (37 stars) has low community visibility.
|
||||
|
||||
### Gaps
|
||||
- Could not verify whether ClassroomIO or LearnHouse require a CLA.
|
||||
- LearnPress and MasterStudy repo licenses are not detected by GitHub. WordPress.org active-install counts were not retrieved because the API call was blocked.
|
||||
- Funding status of Frappe Technologies, ClassroomIO and CodeLit was not researched.
|
||||
|
||||
## 2. Tech stack, architecture, self-hosting and homelab suitability
|
||||
|
||||
### Takeaway
|
||||
LearnHouse (Next.js + FastAPI + Postgres/Redis + Yjs collab server, with a first-party CLI) and Frappe Learning (Frappe/Python + MariaDB + Vue, with an `easy-install.py` Docker deploy) have the most polished self-host stories. ClassroomIO (SvelteKit + Hono + Postgres/Redis + MinIO) has had self-host breakages. CourseLit (Next.js + MongoDB) depends on MediaLit for media.
|
||||
|
||||
### Cited Findings
|
||||
- **Frappe Learning**: Frappe Framework (Python, full-stack) with a Frappe UI (Vue) frontend. Production self-host is `wget https://frappe.io/easy-install.py` followed by `python3 easy-install.py deploy --image=ghcr.io/frappe/lms --version=stable --app=lms --sitename …`, described as "production-ready … in about 5 minutes". A docker-compose dev setup is also provided. — [frappe/lms README](https://github.com/frappe/lms)
|
||||
- Frappe Learning operator pain points among open issues: "stable Docker image regression: LMS app missing after Frappe v15→v16 upgrade, no migration" (#2350, May 2026); a request for a "Production-ready Deployment Guide … using Coolify" (#1434); and branch-naming alignment with Frappe/ERPNext (#1592). — [frappe/lms #2350](https://github.com/frappe/lms/issues/2350), [#1434](https://github.com/frappe/lms/issues/1434), [#1592](https://github.com/frappe/lms/issues/1592)
|
||||
- **LearnHouse**: monorepo. `apps/web` is Next.js, React, TailwindCSS and Tiptap. `apps/api` is FastAPI, SQLModel and Alembic. `apps/collab` is Hocuspocus/Yjs over WebSocket. `apps/cli` is Node. Self-host with `npx learnhouse@latest setup`, a wizard that generates config and starts services, plus `start/stop/update/logs/backup/doctor` commands. A Dockerfile ships in the repo root. — [learnhouse README](https://github.com/learnhouse/learnhouse)
|
||||
- **ClassroomIO**: SvelteKit, Hono, PostgreSQL, Better Auth and TailwindCSS. Monorepo apps are `website`, `api` (PDF/video processing, email), `dashboard` (the LMS) and `docs`, plus `apps/jobs`. Dev needs Node ≥20.19.3, pnpm v10 and Docker (Postgres + Redis). `docker-compose.yaml`, `docker-compose.images.yaml` and `classroomio.sh` are in the repo root. — [classroomio README](https://github.com/classroomio/classroomio)
|
||||
- ClassroomIO self-host issues: "[self-hosting] Bundled MinIO images no longer exist; fresh installs and upgrades fail" (closed, Sept 2026); "Uploaded lesson video stays on 'Transcript generation in progress' forever" (open, Oct 2026); and an open issue about reducing dashboard production memory on Render. — [GitHub issues search](https://github.com/classroomio/classroomio/issues?q=self-host), [#1331](https://github.com/classroomio/classroomio/issues/1331)
|
||||
- **CourseLit**: pnpm monorepo with a Next.js app (`apps/web`), MongoDB (migrations are run as ad-hoc Node/mongoose scripts) and a Vercel deploy button. Media goes through **MediaLit**, "a paid service", unless you self-host a MediaLit instance via `MEDIALIT_SERVER`. — [courselit README](https://github.com/codelitdev/courselit)
|
||||
- CourseLit operator issues: "Self hosted instance fails while sending auth code" (open since 2024-10); "Community post deletion does not delete media from MediaLit" (open). — [courselit issues](https://github.com/codelitdev/courselit/issues?q=medialit)
|
||||
- Forma LMS and Gibbon are classic PHP/MySQL LAMP apps. — [GitHub API language field](https://github.com/formalms/formalms), [Gibbon](https://github.com/GibbonEdu/core)
|
||||
|
||||
### Inferences
|
||||
- For homelabbers, LearnHouse's CLI (with backup and doctor commands) and Frappe's easy-install are the lowest-friction options. Frappe drags in the whole Frappe bench/MariaDB/Redis stack, and major-version upgrades (v15→v16) have broken Docker images.
|
||||
- ClassroomIO's multi-service stack (dashboard + api + jobs + Postgres + Redis + object storage + AI transcription) is the heaviest of the four for a homelab.
|
||||
- CourseLit's MongoDB plus hand-run migration scripts is the weakest operational story.
|
||||
|
||||
### Gaps
|
||||
- No measured RAM or CPU footprints were found for any project.
|
||||
- Helm or Kubernetes support was not checked.
|
||||
|
||||
## 3. Features: authoring, video, assessments, certificates, paths, cohorts, commerce, SSO, multi-tenancy, standards (SCORM/xAPI/LTI), i18n, API
|
||||
|
||||
### Takeaway
|
||||
Frappe Learning has the broadest *fully open* feature set: SCORM import, payments, programs/paths, batches/cohorts, live classes, quizzes, assignments and certificates. LearnHouse has the richest modern authoring (Notion-like blocks, code execution, real-time collaboration, AI), but payments, SSO, SCORM and multi-org are EE-gated. ClassroomIO focuses on cohorts, learning paths, multi-org academies, REST API, webhooks and MCP, with SSO gated to Enterprise. CourseLit is a creator-commerce tool (Stripe, landing-page builder, digital downloads, email) with thin assessment features and no i18n.
|
||||
|
||||
### Cited Findings
|
||||
- **Frappe Learning**:
|
||||
- Course structure is three levels: course, chapter, lesson.
|
||||
- Batches (cohorts) have dates, seat limits and instructors. Live classes are scheduled through Zoom or Google Meet.
|
||||
- Quizzes support single-choice, multiple-choice and open-ended questions, with evaluators reviewing results. Assignments are submitted as PDFs or documents.
|
||||
- Certificates use a built-in or custom template and can be auto-issued or issued in bulk per batch.
|
||||
- **Programs** group courses into learning paths.
|
||||
- **SCORM package upload** as a chapter.
|
||||
- A payment gateway configured under Settings → Payment can charge for courses, batches or certificates.
|
||||
- Sources: [Frappe Learning docs](https://docs.frappe.io/learning); [README](https://github.com/frappe/lms)
|
||||
- Frappe Learning open requests: SCORM *export* (#1781) and sales-tax support at checkout (#2349). — [#1781](https://github.com/frappe/lms/issues/1781), [#2349](https://github.com/frappe/lms/issues/2349)
|
||||
- Frappe Learning ships a `crowdin.yml` (community translations). — [repo root](https://github.com/frappe/lms)
|
||||
- **LearnHouse**:
|
||||
- Open features: courses, a block-based Notion-like editor (Tiptap), collections (bundles), assignments, discussions/forums, podcasts, analytics, AI "Playgrounds" (generated interactive simulations and diagrams), real code execution with auto-grading in 30+ languages, real-time collaborative whiteboards, an AI tutor, auto-generated certificates, user groups, SEO, and custom branding/landing pages/theming.
|
||||
- **Enterprise-only**: Payments, SSO (OAuth), Multi-Org.
|
||||
- Sources: [learnhouse README](https://github.com/learnhouse/learnhouse)
|
||||
- LearnHouse SCORM, RBAC, audit logs and white-label are listed as Enterprise. — [pricing](https://www.learnhouse.app/pricing)
|
||||
- Open LearnHouse bugs include "Subscriptions are not working" (#1182, Oct 2026) and "Magic Link With 500" (#1183). — [#1182](https://github.com/learnhouse/learnhouse/issues/1182), [#1183](https://github.com/learnhouse/learnhouse/issues/1183)
|
||||
- **ClassroomIO**:
|
||||
- AI course builder, cohorts, branded certificates, team collaboration and 10+ languages.
|
||||
- Branded academies on custom domains, plus "Manage Organizations" (multi-org).
|
||||
- Learner dashboard, learning paths, and embedded video, slides and documents.
|
||||
- REST API and webhooks; Zapier is "coming soon".
|
||||
- AI lesson tutor and an MCP server.
|
||||
- Sources: [classroomio README](https://github.com/classroomio/classroomio)
|
||||
- On ClassroomIO's free cloud tier, video upload and certificates are excluded and SSO is Enterprise. — [pricing](https://classroomio.com/pricing)
|
||||
- ClassroomIO open issues show API and certificate gaps: no welcome email for API-added students (#1364), and certificate eligibility is not re-checked after grading (#1365). — [#1364](https://github.com/classroomio/classroomio/issues/1364), [#1365](https://github.com/classroomio/classroomio/issues/1365)
|
||||
- **CourseLit**:
|
||||
- Course authoring, student management, Stripe payments, a website/landing-page builder, digital downloads, blogs, and custom widgets.
|
||||
- Community posts with reply-by-email (added in v0.74.x).
|
||||
- Sources: [courselit README](https://github.com/codelitdev/courselit), [commit log](https://github.com/codelitdev/courselit/commits/main)
|
||||
- CourseLit's most-upvoted open issues: multi-language support (#508, open since 2024), subtitle support in video lessons (#610), and section re-ordering in courses (#719). — [#508](https://github.com/codelitdev/courselit/issues/508), [#610](https://github.com/codelitdev/courselit/issues/610), [#719](https://github.com/codelitdev/courselit/issues/719)
|
||||
- Forma LMS work continues on SCORM (the 2026-04 commit is "safe SCORM path repair"). — [formalms commits](https://github.com/formalms/formalms/commits/master)
|
||||
- **WordPress plugins** (affiliate-style sources; verify on vendor sites):
|
||||
- Tutor LMS: Pro adds a certificate builder, memberships/subscriptions and automated grading; add-on bundles are cited "from $199/yr".
|
||||
- LearnPress: free core, but payments, certificates, drip and advanced assessments may need paid extensions.
|
||||
- Sensei: best with WooCommerce; Pro cited at $179/yr.
|
||||
- MasterStudy: the free version includes a visual builder, PayPal/Stripe, quizzes and memberships; Pro adds Zoom/Meet, a certificate builder, grading and analytics.
|
||||
- Sources: [learningrevolution.net](https://www.learningrevolution.net/best-wordpress-lms-plugins/), [profilepress.com](https://profilepress.com/best-wordpress-lms-plugins/), [wbcomdesigns](https://wbcomdesigns.com/learndash-vs-learnpress-vs-tutor-lms/)
|
||||
|
||||
### Inferences
|
||||
- **SCORM/xAPI/LTI support is weak across the modern set.** Frappe only *imports* SCORM, and LearnHouse SCORM is EE. I found no evidence of SCORM or xAPI in ClassroomIO or CourseLit, and no evidence of LTI 1.3 in any of the four (LTI not verified; see Gaps). Universities needing LTI would have to build it, or use a library such as the MIT [scorm-again](https://github.com/jcputney/scorm-again) runtime (361 stars, active 2026-10) for SCORM playback.
|
||||
- **OIDC/SAML SSO is paid in LearnHouse and ClassroomIO.** In Frappe Learning, SSO comes from Frappe Framework's social-login/OAuth support (not verified in this session).
|
||||
- None of the four is a true "Udemy-style multi-instructor marketplace" with revenue share out of the box. CourseLit and MasterStudy come closest to creator commerce.
|
||||
|
||||
### Gaps
|
||||
- GitHub code-search counts were attempted but rate-limited. The "lti" hits were substring noise, so LTI support is **unverified** for all projects.
|
||||
- Native mobile apps: none were found for the four modern projects. Not exhaustively verified.
|
||||
- Video handling details (HLS transcoding, DRM) were not documented in the sources reviewed. ClassroomIO does processing in its `api` service, and CourseLit delegates to MediaLit.
|
||||
|
||||
## 4. Other newer / niche projects (2023–2026) and adjacent tools
|
||||
|
||||
### Takeaway
|
||||
GitHub topic searches turned up few serious new entrants beyond the big four. Notable extras are Richie (an MIT Django CMS front-end for course catalogs), iblai/lms (MIT, "skills intelligence"), Pupilfirst (stalled), Leemons (stalled), EaseLMS (tiny), course-tencent-cloud (Chinese market, GPL-2.0) and many tutorial-grade "udemy-clone" repos with no production value.
|
||||
|
||||
### Cited Findings
|
||||
- **Richie** (openfun/richie): "An opensource CMS to build education portals", MIT, 317 stars, v3.5.1 (2026-08-18), pushed 2026-10-29. A 2021 Ask HN thread described it as a modern Django+React LMS. — [GitHub](https://github.com/openfun/richie); [Ask HN: Why is there no good open-source LMS?](https://news.ycombinator.com/item?id=29275470)
|
||||
- **iblai/lms**: "Skills intelligence platform — discover courses, track competencies, earn credentials", MIT, 208 stars, release skills-v0.100.0 on 2026-10-29. — [GitHub](https://github.com/iblai/lms)
|
||||
- **Pupilfirst**: asynchronous online-school LMS, 980 stars. Last release v2024.2.1 (2024-10-09), last push 2025-12-09, so it is likely stalling. — [GitHub](https://github.com/pupilfirst/pupilfirst)
|
||||
- **Leemons**: "learning experience platform", 290 stars. Last release 1.23.0 (2025-02), last push 2025-05-14, so it looks dormant. — [GitHub](https://github.com/leemonade/leemons)
|
||||
- **course-tencent-cloud**: GPL-2.0 Chinese paid-knowledge/online-school system, 1,175 stars, v2.0.0 (2026-09-16), Docker deploy. — [GitHub](https://github.com/xiaochong0302/course-tencent-cloud)
|
||||
- **EaseLMS**: self-host or hosted; video lessons, quizzes, certificates, Stripe and Flutterwave. The GitHub repo enyojoo/easelms has 8 stars (pushed 2026-06). — [Product Hunt](https://www.producthunt.com/products/easelms?launch=easelms); [GitHub](https://github.com/enyojoo/easelms)
|
||||
- Other repos found under topics:
|
||||
- Topic `lms`: RELATE (inducer/relate, 437 stars, active), AnubisLMS (382 stars, MIT, pushed 2026-09), academico (MIT, Laravel/Filament school management), ulearn (MIT, Laravel 5.8 + React 16, dated), plus guillermoscript/lms-front (MIT, 42 stars).
|
||||
- Topic `udemy-clone`: manjurulhoque/laravel-udemy-clone (116 stars).
|
||||
- All are small or hobby-scale.
|
||||
- Source: [GitHub topic search: lms](https://github.com/topics/lms), [udemy-clone](https://github.com/topics/udemy-clone), [course-platform](https://github.com/topics/course-platform)
|
||||
- An Oct 2025 Adobe community post announced a "modern open source LMS built for the corporate world", but its source was not yet public. — [Adobe community](https://community.adobe.com/t5/captivate-discussions/finally-a-modern-open-source-lms-built-for-the-corporate-world/td-p/15534361)
|
||||
- Hacker News: no recent threads specifically on LearnHouse, ClassroomIO, CourseLit or Frappe LMS were found. The older threads are "Ask HN: Why is there no good open-source LMS?" and a 2023 Moodle thread. — [HN 29275470](https://news.ycombinator.com/item?id=29275470), [HN 36753363](https://news.ycombinator.com/item?id=36753363)
|
||||
|
||||
### Inferences
|
||||
- Adjacent tools (from background knowledge, **not verified this session**):
|
||||
- Ghost memberships with Stripe can gate video and article content but has no progress tracking.
|
||||
- Discourse works as a community/discussion layer.
|
||||
- Docusaurus or MkDocs sites suit free, text-first courses but lack enrolment.
|
||||
- Kolibri (Learning Equality) is offline-first K-12 content distribution.
|
||||
- Oppia is interactive-lesson authoring for free education.
|
||||
- None is a direct base for a Pluralsight/Udemy-style product, but Discourse (community) and Docusaurus/MkDocs (text-heavy course content) are realistic *complements*.
|
||||
- The real candidate pool for a fork or base is small: Frappe Learning (most complete open feature set), LearnHouse (best UX/editor, open-core), ClassroomIO (B2B, multi-org), and CourseLit (creator commerce, stagnating). Richie is a reference for catalog/portal UX.
|
||||
|
||||
### Gaps
|
||||
- Reddit r/selfhosted threads on these platforms could not be surfaced by search, so community sentiment is under-sourced.
|
||||
- I did not verify current activity or licensing for Kolibri, Oppia, Ghost or Discourse.
|
||||
- LearnDash is noted as proprietary per the task brief. I did not re-verify its 2026 pricing.
|
||||
Reference in New Issue
Block a user