Skip to content

Releases: ZcarecroW/CourseForge

CourseForge 5.3.0

Choose a tag to compare

@ZcarecroW ZcarecroW released this 31 Oct 03:54

Every page knows what every other page teaches. The outline now carries a
scope under each page - one line naming what that page and no other page
teaches - and every page is written from a course map that shows every page with
its scope. A page that needs a concept another page owns builds on it or names
that page, instead of explaining it again. The AI plans the scopes together when
it designs the outline; Plan missing scopes fills them in for an outline that
has none in one request. A page also sees the section headings of the pages
already written in its chapter, which is where plans drift.

A third of the prompt, and most of it cached. The page prompt used to carry
the whole outline - six hundred words per chapter - with every page. It now
carries the compact map and its own chapter, measured at 47,000 characters
instead of 139,000 per page on a course of 20 chapters × 7 pages. The shared part
comes first, is byte-identical from page to page, and is marked for each
provider's prompt cache: breakpoints for Anthropic, explicit cache points and a
cache key for OpenAI's gpt-5.6 and later, cache markers through OpenRouter, the
right order for Gemini and every gateway. On a live run with Anthropic the billed
input falls by a little over nine tenths. The cache breakpoint 5.2 set was on a
part of the prompt that differed for every page as soon as pages had tags, so it
paid the cache-write premium on every page and was never read back.

Connected clients read the map once. get_page_brief hands out the shared
context with ids, and a client that passes them back as known_context is not
sent the same text again. plan_page_scopes and update_page's new scope give
a client what the browser has.

The providers as they are today. OpenAI's GPT-6 family (gpt-6-sol,
gpt-6-luna, gpt-6-astra, gpt-6.1-sol, gpt-6.1-terra) was hidden from the
model picker and, typed in by hand, sent parameters it refuses; it is offered and
handled as the reasoning family it is. Gemini's batch queue knows gemini-4-argon
and gemini-3.8-flash; Anthropic's offline fallback names the current models.
Claude is now asked on API version 2026-10-21, which refuses temperature,
top_p and top_k on every model, so none is sent: the profile's temperature no
longer applies to Claude. Current Claude models already refused a custom value,
so pages they write read the same; Claude 4.6 and Haiku 4.5 now sample at their
default.

Libraries current. Vue 3.5.44, marked 18.1.2, DOMPurify 3.4.17, CodeMirror
(with the new @codemirror/streamparser it now needs), Shiki 4.5.0 and
Mermaid 12.2.0. Mermaid is vendored as its ES build: a flowchart in the preview
now fetches 863 KB instead of the 3.5 MB single file of 11 - or the 5.5 MB one
of 12. Diagrams keep the 11.x look and layout, in the preview and through
BookStackDev, because 12 changed four defaults that doubled a small flowchart.

Fixed. Saving or resetting the update token, repository or channel in
Settings ended in a fatal error after the change was written. A diagram, a
formula or highlighted code that arrived while you were on another tab was
thrown away, and the block stayed raw when you came back. A page stored over MCP
was left with a status nothing else uses. A clean clone on Windows failed two
tests before anything had been changed, because Git rewrote line endings; files
are now checked out exactly as committed.

Upgrading from 5.2 is a normal update: the database gains one column on first
start. Existing courses have no scopes until the outline is revised or Plan
missing scopes
is pressed; they are written as before, with the compact map,
in the meantime.

CourseForge 5.2.0

Choose a tag to compare

@ZcarecroW ZcarecroW released this 03 Sep 05:38
v5.2.0
b679d69

Publishing is a task, not a request. Pressing Publish writes a task down
and returns; the scheduler works it in slices of one tick each, and the tab can
be closed. A push interrupted half way - a wiki that stopped answering, a host
time limit, a process that died - is picked up again from the page it stopped
at, never from the start, with a growing pause between attempts and a ceiling
you can set. One wiki failing does not stop the others, and only the failed one
is tried again. On an installation with no scheduler the browser works a queued
task itself, one bounded slice per request, and says so.

The publish log lives on the server. Every line a task says is written down
as it is said, with the wiki it belongs to, and shown whenever the tab is
opened - including the pushes that ran while nobody was looking. Stop and Retry
are yours; Retry carries on from where the task got to.

Each destination has its own card. The book, the shelf, pages published,
out of sync, never published, how the cross references resolve, and the last
outcome - per wiki, beside a card that speaks for all of them. Publish here,
Book only and Resolve links here act on one wiki.

Several invites at once. An invite carries a label - who it is for - and
any number may be open together. A code issued from the app is shown exactly
once and written to no file; only the first-run invite still goes into
INVITE-CODE.txt. Revoke one and the others stand.

A guided tour. It starts by itself the first time an account signs in and
walks through every screen and every setting, spotlighting each section as it
explains it. An administrator and a user get different tours. Cancel it at any
time; open it again from the button above the theme switch.

Administration › Security. CourseForge asks the server for its own private
files over HTTP and reports which came back - the data directory, the
database, the deny file, the configuration. Until every one is refused, every
field that would store a secret is locked and carries a red mark that leads to
the screen; the screen names the server it detected and gives the configuration
block for it, and the one arrangement that is safe everywhere. An administrator
who has verified the server by hand can accept the risk by typing a code shown
on the screen, which is recorded with their name.

A security review, applied. Addresses a profile carries are vetted before
they are called and never have their answers quoted back; the shared cURL
wrapper speaks HTTP only; a blank secret is carried forward only for the address
it was stored against; Host is used only when it looks like a host and
X-Forwarded-Host never; sessions use strict mode and are renewed on a password
change; password changes are throttled like sign-ins, in the browser and over
MCP; an account lock no longer shuts out the address the owner signs in from;
request bodies are capped; connection tokens minted over MCP die with the
connection that minted them and never carry a scope the account lacks; the MCP
origin check measures against the configured address; tool results are cut at
the size the tool declared; a look may load its libraries only from the module
CDNs or the installation itself; the BookStackDev endpoint varies its cache on
the header that decided, answers preflights only for allowed origins, and no
longer names the look in a refusal; embeds run sandboxed; the deny lists gained
.git, .github and .tmp; and a content security policy is sent.

Upgrading from 5.1 is a normal update: the database gains two tables and three
columns on first start. The first sign-in afterwards starts the tour once, and
the key fields stay locked until Administration › Security has been opened - the
check runs there by itself.

CourseForge 5.1.0

Choose a tag to compare

@ZcarecroW ZcarecroW released this 02 Sep 22:13
v5.1.0
84861e7

BookStackDev lives in CourseForge. The BookStack enhancements — Shiki code highlighting in every language it ships, Mermaid diagrams, MathJax formulas, link embeds, an audio player, external-link marks, a light/dark toggle, page styling — are configured on a new BookStackDev screen as looks: every value a card, every feature a switch, defaults exactly as the standalone script ships them. A look gives you one line for BookStack's custom head, served by bs.php, and that line works only on the wikis the look allows: the instances in your profiles that wear it, plus any origin you add by hand. One look serves any number of wikis. A conventions check compares each look with the prompts of the profiles using it and offers the wording that keeps them in agreement — on the look, in the profile's prompts and in the administration prompts.

The Structure tab is a split view. The outline is edited in the Markdown editor with its markers highlighted, and beside it is what Apply would make of it, parsed as you type: chapters, pages, what is new, moved or removed. The halves scroll together. The AI moved to a panel on the right, so the outline has the width.

Defaults you can start with. New courses ask for 3,000 to 15,000 words a page, five Anki cards and as many searches as the model sees fit; the Anthropic output ceiling is 32,000 tokens so a long page is not cut short. The time-zone setting is a fuzzy search over the whole list, and the Settings screen itself has a search box.

The Claude subscription provider is retired. CourseForge runs on ordinary web servers, which cannot hold a desktop sign-in. An account of that kind is kept and shown as retired so nothing referring to it breaks; give the profile an API account to write with.

Libraries current. Shiki 4, highlight.js 11, Mermaid 11, MathJax 4 and Font Awesome 7 on the wiki side, and an unlabelled code block is recognised by CourseForge's own detector before highlight.js is asked — which stops six lines of Python being coloured as a DNS zone file.

Upgrading from 5.0 is a normal update: the database gains one table on first start, and nothing else changes shape. bs.php in the install root must be reachable by the web, as cron.php already is; the nginx and Caddy examples in the documentation cover it as they stand.

Eight new MCP tools (list_bookstackdev_profiles, list_bookstackdev_options, get_bookstackdev_profile, create_bookstackdev_profile, update_bookstackdev_profile, delete_bookstackdev_profile, rotate_bookstackdev_link, check_bookstackdev_conventions) and bookstackdev_id on update_profile and add_bookstack_instance. Test suite: 392 passed.

CourseForge 5.0.0

Choose a tag to compare

@ZcarecroW ZcarecroW released this 02 Sep 19:06
v5.0.0
a010f50

A release about the interface: what it looks like, how it is organised, and whether it can be used without a mouse. No pipeline changes and no schema change — but a profile can now decide what every course written with it starts from, which is the one new thing under the surface.

An interface that can be read before it is read

Every screen, every card, every setting and every tab now carries a glyph from the Nucleo icon set, drawn to one grid and one stroke, so a list is skimmed by its pictures before a single label is read. A hundred icons, each named in assets/js/components/AppIcon.js by the Nucleo id it came from, inlined as SVG that takes the text colour around it — no font, no CDN, no flash of missing glyphs.

The sidebar is divided into Workspace and Administration, each destination has a hint, and the account entry at the foot shows who is signed in — avatar, name, role. The theme control has three positions — light, dark and Auto, which follows the system — instead of a toggle. Every screen opens with a line saying what it is for.

Settings gained an index down its side that follows the scroll, marks the group on screen, and counts what this installation has changed in each. Every setting has a glyph that says what kind of thing it is, and every switch is a switch.

Courses cards carry a tile that says where each course stands — being written, written but not published, published, a run open, a page failed — and an empty course list explains the whole application in five steps.

Course defaults look like the Details tab, because they are the same list

The fourteen elements and eight values a course can decide were drawn on the Settings screen as a plain list of settings rows that looked nothing like the Details tab the same elements are switched on a course with. They are now drawn as that tab draws them: the same rows, tiles, order and words. What differs is the control, and for a reason: every other level has three answers — on, off, or follow the level above — and this level has nothing above it, so its switch has two.

A profile decides what every course on it starts from

Until now the only way to give twenty courses the same house style was twenty Details tabs. A profile has a fourth tab, Content defaults, with the same editor as a course, one level up:

installation  →  profile  →  course  →  chapter  →  page

Only deviations are stored at every level, so a course that never disagreed with its profile follows it when it changes, and a course whose profile is deleted goes back to the installation. A course's Details tab says which of the two its "inherit" position points at, and one button opens the profile on the right tab.

Over MCP the same thing is set_profile_details (features as 1, -1 or 0; values as numbers or text; reset_all), with the crossed-lengths refusal every other door to that pair has. get_profile reports the decisions, and get_details on a course names profile as where an inherited answer comes from.

Usable without a mouse, and readable without sight

Every icon-only button has a name. Every tab bar is a tab list, every segmented control a radio group, every switch a switch, and the current navigation entry says so. Progress bars carry their numbers. A skip link takes the keyboard past the navigation. Error strips are announced. A decorative icon is hidden from assistive technology; one that stands for something on its own is named.

Upgrading

Nothing to do, and no schema change. A profile gains its content-defaults layer the first time it is saved, and an empty layer means exactly what no layer meant: whatever the installation says.


383 tests pass, 4 of them new in tests/profile-defaults.test.php. Every Vue template was compiled with the shipped Vue before release, and every screen was driven in a browser against a scratch installation — dark and light, desktop and phone width.

CourseForge 4.10.0

Choose a tag to compare

@ZcarecroW ZcarecroW released this 01 Sep 21:26
v4.10.0
2d4a2af

Eleven readers went over the whole application with one instruction — find what is actually wrong, not what could be tidier — and this release is what they found. Twenty-odd fixes and no features. The ones worth knowing about, in the order of the damage they did:

Stopping a batch threw away the pages it had already written

"Stop this run" on a batch asked the provider to cancel and then closed the run on the spot. But a cancel is asynchronous everywhere, and a batch keeps whatever it answered before the request landed — pages that were paid for. Nothing could download them while the batch was winding down, and nothing ever tried afterwards, because a closed run is never polled. On OpenRouter, which has no cancel route at all, the run was closed while the batch ran on, billing, with nobody left to collect it.

The run now stays open until the provider has said its last word. The next check collects the pages that were finished, releases only the ones that never ran, and closes the run then. A provider that cannot be stopped says so, and its run stays open for the same reason.

A book created twice, once per retry

A course's first publish created its book, attached it to the shelf, and then wrote the id down. A deleted shelf or a token not allowed to edit shelves failed the middle step, the id went with the exception, and the next push created another book — one orphan per attempt. The id is now written down the moment the book exists.

A setting saved, and lost to a page load

Reading data/config.json rewrote it, on every request, whenever the file held a prompts key at its top level — any installation that has overridden a single prompt — and took no lock doing it. A read just before an administrator's save wrote its stale copy back just after. Reading no longer writes anything; the one-time reduction of a 3.x document happens under the same lock as every other write.

Deleting a profile stranded the batch running under it

The Profiles screen deleted without looking; the next poll of a run under that profile found no credentials and wrote the run off, while the batch ran on at the provider. A profile with an open run under it is now refused, on both surfaces.

The course editor was the one screen that never asked

Every other screen asks "Leave without saving?". The course editor did not — one click on the sidebar or the back arrow and an unsaved page, outline or destinations list was gone. All five tabs now ask. Rewrite everything asks before it starts from the two primary buttons as it always did from the third, and an outline edit is no longer discarded when a title is renamed on the Content tab.

Smaller, and still real

  • create_course over MCP let an administrator bind a course to somebody else's profile; it is refused, as the browser refuses it.
  • update_profile ignored preset_key, the per-kind extras and bookstack_name when adding a profile's first account or instance — every profile made in the browser.
  • gpt-5-chat-latest was taken for a reasoning model and had its temperature stripped.
  • The Claude subscription account passed the model id to the command line unchecked; anything that is not a model id is refused first.
  • Chinese and Japanese word counts counted a sentence as one word. Each character counts.
  • A four-backtick fence quoting a three-backtick example was closed at the example.
  • A placeholder inside a value — a page quoting its own {{tags}} — had the real tag list written into it before the model saw it.
  • A course update refused for its shelf had already renamed the course.
  • Publishing one page or chapter from the Content tab reported success when one of several wikis had failed.
  • Creating a second account with a generated password, or a second connection, replaced the card holding the first password or token. Both wait now until the card is acknowledged.
  • Mermaid, MathJax and the editor's language modes stayed failed for the session after one non-network failure.
  • A batch the provider accepted but the database could not record is withdrawn from the provider rather than left running unknown.
  • tools/deploy.php --full --prune pruned nothing; the development router could be walked past with /x/../; two workers migrating the same column no longer answer one with a 500.

Upgrading

Nothing to do, and no schema change.


379 tests pass, 14 of them new in tests/review-fixes.test.php, each checked to fail against the previous code. Verified in a browser against a scratch database with a fake model and a fake BookStack behind it: outline, pages and a published book on the old code, then the new dialogs and guards on the fixed one.

CourseForge 4.9.0

Choose a tag to compare

@ZcarecroW ZcarecroW released this 01 Sep 17:53
v4.9.0
44210d3

A word count said a finished page was empty

list_pages reported seven finished pages of a course as 194, 283, 954, 1177, 2036, 2586 and 2905 words long. They were 3,893, 4,677 and more. Nothing had been lost — the count was wrong, and wrong in the direction that destroys work: a model reading that list sees pages that look like failed generations and rewrites them, spending money to replace text that was already there.

The count ran the Markdown through strip_tags first. Markdown is not HTML: List<String>, Map<String, Object> and a written-out <div class="card"> are prose about code, and strip_tags removes each of them along with everything up to the next >. Worse, a single a<b with no later > anywhere on the page is an unterminated tag, so the rest of the document is discarded — a 2,408-word page came back as two words.

It now counts the page as it is stored, which is what get_page and the browser's own tree have always counted. Three surfaces answering "how long is this page" differently is worse than any one of them being wrong, because the difference reads as information.

Everything the browser can do, a connected client can do

That was the claim this project makes about its MCP surface. It was audited screen by screen against the tool registry, and it was untrue in eighteen places. This release closes them: thirteen new tools, and arguments added to five that existed.

Profiles

update_profile names the BookStack instance of a profile that has one, and would add one only when there was none — so a profile could be given its first instance and never a second, and the same for AI accounts. 4.7 made publishing a course into several wikis at once a headline feature, and a client setting an installation up over MCP could configure exactly one of them.

  • add_bookstack_instance, delete_bookstack_instance, add_ai_account, delete_ai_account — the Add and Remove buttons the browser has always had beside those two lists. Adding is its own verb rather than a special case of update, because an argument that edits when an id is given and creates when it is not turns a mistyped ai_id into a second account holding the key that was meant to replace the first. Removing an instance a course publishes to is refused with the names of those courses unless you confirm; removing the last AI account of a profile is refused outright.
  • preset_key on create_profile, add_ai_account and update_profile. The provider picker offers two dozen rows — Groq, Together, Fireworks, DeepSeek, a local llama.cpp — and most are the same OpenAI-compatible driver at a different endpoint. The tools took only the six raw driver kinds underneath them. organization, cli_path, site_url and site_name are now settable too.
  • set_model_slot sets one slot alone: its account, its model, its temperature, its token ceiling. update_profile writes both together, which is right for a profile with one account and wrong the moment it has two — the outline is one call and can afford the expensive model, while the pages are hundreds.
  • set_profile_prompts writes a profile's prompt overrides, which were readable and not writable, and list_prompt_slots reads the library naming them without needing an administrator. Overriding a slot for one profile is not the same power as rewriting it for everybody.
  • update_profile also finally takes typography, ai_name and bookstack_name.

Administration and content

  • revoke_invite, because an invite could be issued and not withdrawn.
  • get_cron_url, because the scheduler URL could only be read by rotating the secret in it first — which stops every scheduler already calling it.
  • list_detail_overrides answers "what does this course override, anywhere" in one call instead of one per page.
  • publish_course with part: "book" — the book and its shelf without the five hundred pages under it, which is what a renamed course needs.
  • generate_page with extra_context, which the browser writes in the same click that generates.

Connections

AccountTools said in its own docblock that no tool would ever create a connection, because a token able to mint another could mint a wider one and every narrowing on the Connect screen would become a formality. That argument is about escalation, not about creation — so the rule is now written down instead of the absence.

create_my_connection may issue nothing the calling connection does not already hold. An omitted scope list means "the same as mine" rather than "everything the account allows" — which is the trap, since that is what an empty list means by the time it is stored — and a group the caller lacks is refused by name rather than quietly dropped, because a token that silently does less than was asked for is worse than one that refuses. A connection can copy itself or make something smaller, never something larger.

The browser has no such limit, because a person signed in with a password is the account rather than a delegation of it. rename_my_connection and list_scopes go with it.

What is deliberately still absent

Signing in, redeeming an invite and first-run setup all create or resume the very credential a tool call needs before it can happen at all. revoke_my_connection is the nearest thing to signing out, and it is permanent rather than resumable. This is now written in docs.md beside the parity claim, so the next audit does not rediscover them as gaps.

Upgrading

Nothing to do, and no schema change. Existing connections keep the scopes they were issued with.


365 tests pass, 21 of them new. tests/mcp-parity.test.php carries the audit as a map of capability to tool name, so a tool renamed or dropped fails the suite rather than decaying quietly; the word-count test asserts the three surfaces agree rather than that any one of them is right, and fails against the old code.

Verified over real HTTP against a scratch database, through api/mcp.php with a minted token: 97 tools listed, list_pages and get_page agreeing at 494 words on the page shape that used to report two, a second BookStack instance and a Groq preset account added to a profile with no secret in either answer, the page slot pointed at the new account while the outline slot kept its own temperature, and a connection narrowed to two groups copying itself, refusing to widen itself, and not being shown the admin tools at all.

CourseForge 4.8.0

Choose a tag to compare

@ZcarecroW ZcarecroW released this 01 Sep 16:45
v4.8.0
c6c9abb

The punctuation pass reads a quotation mark the way a reader does

4.6 taught CourseForge to set a generated page's punctuation the way its language sets it. It read each mark by position — a mark after a space opens, a mark after a word closes — and position turned out not to be enough, twice.

**"Wort"** came out as **"Wort“**. A Markdown asterisk stood where a space should have been, the opening rule declined the mark on its account, and the closing rule took it — so a bold quotation ended with two closing marks, and the first one was still straight. And correct French came out wrong: in « bonjour » et, the closing guillemet has a space on both sides, exactly like an opening one, so every » was turned back into a « — on text that had been right before the pass ran.

A mark is now decided by three signals, in order.

  • Position first, with Markdown emphasis stepped over on the way to it, so a quotation with asterisks around it is still a quotation.
  • Nesting depth second, which is what tells a closing guillemet from an opening one when both of its neighbours are spaces and position has nothing left to say.
  • The glyph last, for a mark that only ever points one way.

And what gets written is not what was read: a closing mark takes the pair its own opening mark took. That is why „Wort" — the thing the models actually write — comes out as a pair at all, and why a double quotation inside a double quotation comes out as „… ‚…‘ …“ whichever character the model typed. The depth is forgotten at every blank line, heading, list item and table row, so one unbalanced mark costs its own paragraph instead of every paragraph after it.

The rest of the pass grew with it. None of this is AI: it is rules, it runs in milliseconds, and it costs nothing.

  • An em dash between two words becomes a spaced en dash in the languages that set one that way — the single most reliable thing a model writing German reproduces from English.
  • 1990-2000 becomes a span, while 2026-09-01, T-34 and 1-2-3 are left alone.
  • 5" after a digit with no quotation open is a measurement and becomes 5″, while the " closing "42" is still a quotation mark, because the depth tells them apart.
  • don't, '90s, rock 'n' roll and dogs' bowls each get the apostrophe they want, and none of them is mistaken for a quotation.
  • Four spaces become one, a space before a comma goes, ( innen ) closes on its content, three blank lines become one, and trailing whitespace goes — except the two spaces that are a line break in Markdown, and except inside a table row, whose padding is what makes its source readable.
  • HTML comments and characters escaped on purpose join fenced blocks, inline spans, links, addresses, tags, formulas and table alignment rows as things that are lifted out and put back untouched.
  • Polish, Czech, Slovak, Hungarian, Russian, Ukrainian, Greek, Swedish and Finnish now get their own marks rather than English ones.

…and it can be pointed at a course that is already written

All of that runs when a page is stored, which was no help at all to the pages stored before it existed, stored with the setting switched off, or stored by 4.6 and 4.7 with the bugs above in them.

Course → Settings → Punctuation now runs the same rules over a course that is already finished. One chapter and its pages have their own button beside Publish chapter; one page has one in the inspector.

It asks first. The button runs the entire pass with the writing switched off and says what would change — "of 3 pages and 2 chapters, this would change 1 chapter and the course description", with the first forty of them named — and only then offers to go through with it. The number in the dialog is the number that changes, because it comes from the same run.

It runs whatever the profile says, and that is the point rather than an oversight. The per-profile switch answers "correct the pages I am about to generate"; this button is somebody standing in front of a course they already have. A setting that turned the second question into a no would be a setting that refuses an instruction.

It corrects every page's Markdown and title, every chapter's title and description, and the book title and description, and rewrites the outline from the titles afterwards — the same repair renaming a page has always made, and necessary because applying a structure matches by title. A page's extra context and the course's research notes are deliberately left out: they are instructions to the writer, not text anybody reads.

A page that needed nothing is not written at all — its timestamp does not move and it does not go newly out of sync with a wiki over a change that never happened. A page that did change does differ from what BookStack holds, and says so until the next publish.

Over MCP

fix_typography, with course_id, an optional chapter_id or page_id (a page wins over a chapter, which wins over the whole course), an optional language to set the text as something other than the course's own, and preview to be told what would change without changing it. It is flagged destructive and idempotent, which is exactly what it is: it rewrites a whole course in place, and doing it twice is doing it once.

Upgrading

Nothing to do, and no schema change. Existing pages are left exactly as they are until somebody asks for them to be corrected. Profiles that correct new pages go on doing so, with the better rules.

344 tests pass, 21 of them new. Verified in a browser against a scratch database seeded with the wrong kind of German: the page button, the chapter button and the course dialog, the preview naming what it would change and the second run saying there was nothing left, code fences and links and table padding surviving all of it, and the outline coming back rewritten from the corrected titles.

CourseForge 4.7.0

Choose a tag to compare

@ZcarecroW ZcarecroW released this 01 Sep 13:19
v4.7.0
e81c85b

A course can be published into more than one BookStack

A course used to have one destination. Choosing a second one meant a second course: the same outline, the same brief, the same pages generated and paid for twice, and two things to keep in step by hand for the rest of their lives.

Course → Publish → Destinations is now a list. Add as many BookStack instances as the course's profile defines — a staging wiki and a live one, the wikis of two departments, a customer's install and your own — each with its own shelf. Publish everything goes to all of them; each one also has a button of its own, for the wiki that was behind.

A destination has a switch, and it is the difference between pausing one and removing one. Off keeps everything already published there on record and simply leaves it out of the next push. Removing forgets the book, so publishing to that instance again later would create a second book beside the first — the dialog says so before it does it, and pointing a destination at a different instance costs the same thing and asks the same question.

Each destination holds its own book, and that is not a detail: the books have different ids and different slugs, and a page's cross references point inside the wiki it is in, so the same page is written slightly differently into each. A push knows what it last sent to each wiki separately, so the wiki that was already current is skipped there and rewritten here, and nothing is ever duplicated anywhere.

A wiki that is down does not stop the others — its failure is named in the log, the rest still go out, and only a push where every destination failed is an error.

The counts fold across the destinations that are on, which is what makes them worth reading: published means published in every one of them, out of sync means at least one would be written to. Add a second destination to a finished course and the whole course lights up as work outstanding, because it is.

Over MCP

set_publish_targets writes the list, get_publish_status reports every destination with the book it holds and how much of the course is in it, and publish_course takes an instances list to push to some of them. update_course's bookstack_instance still works and still means what it meant in 4.6 — it replaces the course's first destination, so a client that switches from one wiki to another does not quietly end up publishing to both.

Upgrading

Nothing to do, and nothing is republished. Schema version 8 turns the destination each course already has into the first entry of its list, carrying its book, its shelf and its fingerprints, so the first push after the upgrade sends exactly what it would have sent before. The columns that destination used to live in are still written, as a copy of the first entry, so a rollback to 4.6 finds its book where it left it.

Fixes

Found by an adversarial review of this change:

  • A refused save of the destinations list re-seeded the form from the server, throwing away the edit the person was being asked to correct — and the list stayed marked unsaved after a successful save, which left every button that waits for a saved list disabled.
  • Switching a row's instance kept the previous wiki's shelf and name, and would have forgotten its book with no dialog at all.
  • update_course took a shelf for a course with no destination to put it on, wrote nothing, and reported it as changed.
  • A push naming only malformed target ids fanned out to every destination instead of failing; PUT projects/{id}/targets read a missing field as an empty list and deleted every destination.
  • The destinations list could not be saved at all once the profile stopped defining one of the instances it already published to.
  • Clearing bs_instance_id deleted the destination and the record of its book. In 4.6 it was a pause, so it is now refused with the reason.
  • A paused destination reported zero pages published however many it held, and get_publish_status counted a page published to one wiki of two as never published.
  • publish_items had no index for its three cascades, so every deleted page scanned the table for the whole installation.

323 tests pass.

CourseForge 4.6.0

Choose a tag to compare

@ZcarecroW ZcarecroW released this 01 Sep 03:07
v4.6.0
cb2037d

Punctuation set the way the language sets it

A model writing German opens a quotation with „ and closes it with a straight ". The two are one keystroke apart in the training data, it is not wrong enough to notice while reading one page, and it is wrong on every page of a five-hundred-page book — exactly the kind of error worth fixing with code rather than with a sentence in a prompt.

A generated page now has its punctuation set before it is stored: German gets „Wort“, French its guillemets and the narrow spaces its typography asks for, Spanish and Italian their own, English curly marks and a real apostrophe in don't. Every language gets ... as one character and a spaced hyphen as a dash.

It never touches anything that is not prose — fenced blocks, inline spans, links and their targets, bare addresses, HTML tags, formulas, a table's alignment row and CourseForge's own cross-reference and cloze markers are lifted out before the rules run and put back exactly as they were. It reads position rather than order, which is what makes the half-corrected pair the models actually produce come out right, and it is idempotent.

Each profile decides for itself; app.typography is what a new profile starts from, and it is on.

The baseline every course starts from is yours to set

Every content element and value now appears on Administration → Settings under Course defaults, one field each. Set Learning objectives on, or a house minimum length, or a standing audience, and every course that has not decided otherwise follows. They are ordinary settings: only what you change is written, Reset hands the decision back to the release, and set_settings over MCP reaches them by the same keys. They are generated from the details catalogue, so an element added later arrives with its field already in place.

The research briefing has both halves

Course → Details → Research is now the same split the Content tab gives a page: Markdown on the left, rendered on the right, edit / split / preview, and the same linked scrolling.

Fixes

  • One course prompt, in both places it shows. The Structure and Settings tabs each held their own copy and only re-read it when a different course was opened, so saving in one left the other stale — and Save changes would push the stale value back over the one just saved. Both now follow the stored value, without dragging an unsaved outline or course name with them, and without overwriting an edit typed into the other and not yet saved.
  • An invite can be taken back. Revoke, beside the open invite, closes the row and deletes INVITE-CODE.txt in one step; the code stops working immediately and no invite is left open. Recorded as invite.revoke.
  • Settings could store a Minimum length above a Maximum length, which every other door to that pair already refuses.
  • The Markdown editor's read-only lock set contenteditable and nothing else, so Backspace, Enter and undo still edited a document that was supposed to be held still.

291 tests pass.

CourseForge 4.5.1

Choose a tag to compare

@ZcarecroW ZcarecroW released this 01 Sep 01:10
v4.5.1
837a177

Browser testing on a real six-hundred-word outline found a bug that the unit tests had not, and it is squarely in what 4.5.0 was for. If you installed 4.5.0, take this one.

A description that reads like a sentence is read as one

4.5.0 escapes a description paragraph that opens 3. Install the plugin on the way out and reads the escape back off, so anything CourseForge wrote itself round-trips correctly. But nothing escapes an outline written freehand — and freehand is exactly what a model and an MCP client produce. The net underneath was a length ceiling of 200 characters, and the paragraph that actually turns up is one ordinary sentence of about a hundred and ten.

On a genuine 835-word book description and three 607-word chapter descriptions, that produced:

  • a chapter and three pages nobody asked for, and
  • a book description cut to 266 of its 835 words — each discarded paragraph having been read as structure instead.

A title is now told from a paragraph by shape as well as by length: prose ends in terminal punctuation and runs past a title's worth of words. Both signals must hold, so a long title with no full stop is still a title, and What is new in 7.2? is still a page.

Verified in the browser against real data: three chapters, twelve pages, 834 of 835 words kept, every paragraph intact — and re-applying the stored 16,732-character outline is byte-for-byte idempotent, with the written page keeping its text.

Also

  • The Research panel offered "Clear research" over an empty box, which described nothing that could happen.

271 tests pass.