Changelog — JPKCom Desktop
Version history of JPKCom Desktop — the desktop-style web interface with windows, a dock and apps in plain JavaScript.
Version history of jpkcom-desktop (opens in a new tab). Entries are taken verbatim from the project's CHANGELOG.md. A hands-on guide with tips is available in the project guide.
1.4.0 (opens in a new tab) — 2026-10-09
Fixes for the service worker and for several desktops on one origin: an update check that stops as soon as a newer worker installs, legacy caches deleted only by the worker in charge, the holidays region file in the offline copy, an announcement for every update that keeps the cache name, an offline copy that repairs itself, and stored keys that stay apart when one namespace is another one's plus -…. For module authors: the descriptor field precache and store.owns()/store.claim(). Updating a site: see the upgrade notes below.
Upgrade notes
- Upload the new
sw.jswith the rest: the shell cache gets a new name once (new version), visitors are offered one reload. Nothing else is needed for the fixes;src/boot/preload.jsis unchanged. site/config.js: the comment ofnamespacechanged (a recommendation) — copy it if you keep your own file.- Several desktops on one origin whose namespaces overlap (
jpkdeskandjpkdesk-next): update every one of them to 1.4.0 — a desktop on 1.3.0 or earlier still counts the longer namespace's keys as its own and deletes them with "Reset everything". For new installations pick namespaces where neither is the other plus-…(desk-a,desk-b); a changed namespace starts with empty settings. - Site modules that import a file with a computed name (
import(`./parts/${name}.js`)) can list those files in the new descriptor fieldprecache: [...](docs/ARCHITECTURE.md§8) so that they are part of the offline copy; without it they are kept the first time they load online, as before. - Own code that read every
localStoragekey starting with<namespace>-should useDesk.store.names()orDesk.store.owns(key); modules that keep keys outside theirstoragedeclaration can declare them withDesk.store.claim()(docs/ARCHITECTURE.md§14 "Whose keys").
Added
- Module descriptor: new field
precache: [...](ARCHITECTURE §8) — files a module imports with a computed name, kept in the offline copy by the service worker. The loader ignores it. store.owns(key)andstore.claim(name | test)(ARCHITECTURE §14 "Whose keys"): the storage registry and consent declare their names, andstore.names()/usage()follow the rule. When in doubt a key is kept: a leftover of a removed module named likereader-langcan make "Reset everything" keep that module's leftovers.- New pure exports
ownKeys(keys, ns, known)(src/core/store.js, the rule behindowns()/names()) andstripComments(text)(tools/build-preload.mjs, the same function as insw.js).
Changed
- The shell cache holds two entries of the worker's own:
sw.js?files(the list of the last complete copy) and, between installation and activation only,sw.js?changed. install.jsno longer deletes legacy caches itself while a worker at the installation'ssw.jsURL controls the page: it asks that worker ({ type: 'desk:legacy-sweep' }), which sweeps only while it is in charge; an earlier worker at the same URL ignores the message. An uncontrolled page still sweeps itself.- The docs recommend namespaces where neither is the other plus
-…(desk-a,desk-b).
Fixed
- Service worker: an update check that is already fetching the new copy now stops as soon as a newer worker installs, also in the middle of the crawl — the requests in flight are aborted (also answers whose body is still arriving, and the wait for the browser's preloaded answer for
index.html), the half-written copy is deleted and never marked complete, and the pages hear nothing. In 1.3.0 a newer worker (an update, or a rollback to an earlier worker at the same URL) waited for the whole crawl — up to a minute on a slow server. When the check finds changed files it also lets the browser look atsw.jsfirst (and again before the copy is marked complete), so a new worker installs at once instead of after a crawl it would throw away. - Service worker: the caches named in
offline.legacyCachesare deleted only by the worker that is in charge of its registration — never while a newer worker is installing or waiting, never after another one took over, and checked again right before each delete. In 1.3.0, after a rollback to the earlier worker, the desktop deleted that worker's caches again: at a start of the desktop while the earlier worker was waiting, and from a desktop page still open in another tab about 30 s after the earlier worker took over (it has the same script URL). - Service worker: the region file of the public holidays (
config.holidays.region) is now part of the offline copy — offline the calendar showed no holidays and the console reported "region file could not be loaded". Its import is computed, so the crawl never saw it; the holidays descriptor now lists its region files in the new descriptor fieldprecache: [...]. - Service worker: a new
sw.jsorsite/config.jsthat keeps the cache name (a comment,offline.timeoutMs,offline.legacyCaches) now tells the open desktop "a new version is ready" whenever its installation changed code the desktop runs — also when the installation was interrupted and retried. In 1.3.0 the message only came when the old worker's update check happened to prepare the copy first. - Service worker: a start of the desktop while a new worker with the same cache name installs no longer moves an update the old worker had prepared over the newer files (a mix of old and new code).
- Service worker: the crawl ignores comments, so it no longer asks for
src/core/window.js(a 404 on every install and update) because of an example in a comment ofsrc/core/modules.js.tools/build-preload.mjsreads sources by the same rule. - Service worker: an offline copy that lost files — when another service worker of the site deletes every cache it does not know, the starts refill only what the pages ask for — is completed by the next update check (fast start,
offline.fastStart, the default), and so is an installation that could not reach every file. While the server still has the same code the missing files simply join the copy (no "new version" message). - Service worker: a file the server marks
no-storeorprivateno longer counts as a network failure in the crawl (an update that met one was dropped every time). - Service worker: a server error or rate limit (5xx, 408, 429) while fetching the copy now counts like a network failure: no update is marked complete with an old copy of a changed file, and an installation that met one is completed by the next check.
- Storage: a desktop whose namespace is another one's plus
-…on the same origin (jpkdesk-nextnext tojpkdesk) no longer counts as part of the other one. In 1.3.0 "Reset everything" in the desktopjpkdeskalso deleted every setting and every note ofjpkdesk-next, its storage figure (Settings and the terminal'sdf) counted them as its own, and their changes in another tab reached it as changes of its own keys. A desktop whose namespace is another one's plus-consent(jpkdesk-consent) even lost all its data to a reset of only the settings ofjpkdesk. A key now belongs to a desktop when it is one of the desktop's own names, or when no other desktop shows behind it. A desktop shows when it has stored a name that this desktop knows, such as a setting or a consent. Withdrawing all consents removes consent keys only. Backups were not affected.
1.3.0 (opens in a new tab) — 2026-10-08
Fixes and options from moving a real site onto 1.2: dock pins and desktop icons that follow renamed apps, a start language that follows the browser's order, a vault that stays hidden in Settings → Reset, every Fortune text replaceable, notification banners that show the app an article opens in, the desktop's own glyphs from a site icon set, code colours in the Reader without a flood of CSP reports, and a service worker that no longer holds back an update for 30 s. Updating a site: see the upgrade notes below.
Upgrade notes
- Upload the new
sw.jswith the rest: the shell cache gets a new name once (new version), visitors are offered one reload. - Run
npm run preloadonce if you keep your ownsrc/boot/preload.js: it is generated, and its start-language prediction changed (it now copiesmatchLanguage()ofsrc/core/i18n.js). site/config.jsgains commented entries foriconReplace,notify.labeland the newreaderkeys (keepStyles,styleScope,styleVars), and updated comments forsearch.pagefindandfortune.texts— copy them if you keep your own file; without them the defaults apply.- nginx: add
expires off;next toadd_header Cache-Control $desk_cache_control;in the server block. - Apache (optional): the 1.2.0
.htaccessalready sendsno-cacheand removesExpires; the new<IfModule mod_expires.c>block (ExpiresActive Off) ofdocs/server/apache.htaccessis extra hardening.ExpiresActiveneedsAllowOverride Indexes; the documentedAllowOverride FileInfo Indexes Options=Indexesalready includes it. - Pagefind: keep
'wasm-unsafe-eval'in the desktop's policy wheneversearch.pagefindis set, even if your bundle is served without a policy — Pagefind falls back to the page whenever its worker starts slowly. It only allows compiling WebAssembly and does not alloweval. - Visitors whose browser lists a regional variant of an offered language before another offered language (e.g.
de-AT, enon a de/en site) now start in that first language. A stored choice and?lang=are unaffected. - Code calling
i18n.displayName(code, inLang)with aninLangother thancodenow gets the name ininLang; calldisplayName(code)(ordisplayName(code, code)) for the endonym. - Sites that worked around duplicate desktop icons of aliases with override records such as
{ id: '<alias item id>', desktop: false }can remove them; they do no harm.desktop: true/dock: trueon an alias has no effect (asdock: truealready had none): put the flag on the app itself or on an override record of it. - An alias id in
config.dock.pinsnow pins its target, and the dock shows the target's name, icon and tint, not the alias's own. Exception: a visible alias of an app that cannot be pinned (e.g. of a hiddenwebwindow) stays pinned as the alias. Stored dock lists that hold alias ids are rewritten once on the first start (only when something changed); nothing to do. - A site that hid the vault's reset row with its own CSS (e.g.
.set-row:has(> input[data-key="rs-vault"]) { display: none }) should remove that rule — it would now also hide the row while the vault is unlocked. Own code that calledstorage.resetGroups()gets only the groups shown now; pass{ all: true }for every group. - A site that renamed the Fortune app can now also set
fortune.texts.askText,askText2,askLang,keys,web,errorandstorageLabel(list and placeholders:docs/packages/p11-fortune-site.md, "App texts"). A replacedaskTextmust keep naming who receives the request ({host}or the host written out), or a warning is shown. - Notifications: sites with a fixed
notify.applook the same. Sites withnotify.app: nullget app tiles in the banners automatically; setnotify.labelfor a "<label> · <app name>" meta line (a site that fixednotify.apponly to name its news section there can now keepapp: null). - Reader: code blocks with highlighting written as
style="color:…"now show their colours; setreader.keepStyles: falsefor the old look. A dual-theme highlighter's custom properties needreader.styleVarsplus a rule in the site stylesheet; the kept colours are inline declarations, so the rule needs!importantand must switch text and background together (page classes carry thec-prefix), e.g.:root[data-theme="dark"] .reader-page pre.c-shiki, :root[data-theme="dark"] .reader-page pre.c-shiki span { color: var(--shiki-dark) !important; background-color: var(--shiki-dark-bg) !important }. Colours set by the site stylesheet are not checked by the contrast guard. Content that relied on an inline colour outsidepre/code(or outside the configured scope) still loses it; inlinecodekeeps a colour only together with a background colour from the page. iconReplace: nothing to do without it. To use it, add the target icons to your site icon set, add the map tosite/config.js, runnpm run icons(the replaced Tabler ids stay insrc/icons/tabler.json purpose — they are the fallback when the set does not load) andnpm run validate. A pair whose key or target the browser does not know is warned about at start and dropped; the original glyph stays. An id used for two meanings is replaced in both (weathersleetandhailshareti-cloud-snow).
Added
- Replace the desktop's own glyphs (
iconReplaceinsite/config.js): map the icon ids the desktop draws itself — menu bar, window controls, settings, dock, weather conditions … — to icons of a site icon set ({ 'ti-settings': 'acme-cog', 'wc-close': 'acme-xmark' }) for one icon style throughout. Keys areti-…,tif-…,wc-…andtile-…ids (the author monogramjpkis not replaceable), values any known icon; one step, never chained.icon()andsymbolHref()resolve the map in one place, so core, modules, apps and site modules follow it without a change (docs/ARCHITECTURE.md§6, §13).npm run validatechecks every pair: an unknown key or target is an error, an id not built yet a warning ("run npm run icons"), a target missing from the set with its prefix an error naming the set, ati-/tif-typo "does not exist in Tabler Icons"; a chain is a warning. - Reader: code colours survive. Inside
reader.styleScope(defaultpre, code) a small allowlist of a page's inline styles is kept —color,background-color,font-style,font-weight,text-decoration-lineand, withreader.styleVars(e.g.'--shiki-'), custom properties of that prefix — with plain colours (hex,rgb(),hsl(), named) or fixed keywords only. The Reader re-serialises every value and sets it through CSSOM; the page's style text is never applied.reader.keepStyles: falsedrops every inline style as before. A contrast guard (2:1) drops kept colours that would hide text against their background (the tints ofmarkandkbdin between count). Inside an element that keeps a page's colours everything inherits the checked text colour, so no link or heading colour of the desktop lands on a background the page chose (links there are underlined); an element that keeps only a background gets the checked text colour written too. Areader.styleVarsprefix that starts with a word of the desktop's own tokens (--reader-,--text-,--accent-…) is refused with a warning. - Notifications:
notify.label— a text or{ lang: text }map (at most 60 characters) shown before the app name in the banners' meta line ("News · Blog");null(default) keeps the app name alone. - Fortune app:
config.fortune.textscan now replace every text that names or describes the app or its source — also the consent question (askText,askText2), the language sentence (askLang), the key hint (keys), the "Learn more" label (web), the error status (error) and the Backup/Reset label (storageLabel). Site texts may use the placeholders the app fills for their key (model.jsTEXT_PARAMS:askText{provider}{host},askLang{language},keys{space}{back}{next},error{host}), in plain,{ lang: text }and'@ns.key'form. - Reset groups can declare
visible()(descriptorresetGroups[],storage.registerGroup): a group is left out of Settings → Reset while it answers no and none of its keys holds data.storage.resetGroups()returns the shown groups,storage.resetGroups({ all: true })every group withshown. New bus event'storage:groups'{ id }tells Settings to redraw when the answer changes. - i18n: new named export
matchLanguage(offer, preferred)insrc/core/i18n.js(the browser-language match ofdetect());tools/build-preload.mjscopies its source intosrc/boot/preload.js. - Dock and desktop icons: new pure exports
pinTarget(id, get, canPin?)(src/shell/dock.js) anddesktopApps(list)(src/shell/desktop-icons.js);cleanPins(v, max, resolve?)takes an optional resolver.
Changed
- Server configurations: the Apache
.htaccessalso switchesmod_expiresoff for the desktop (ExpiresActive Offin<IfModule mod_expires.c><Files "*">) — hardening against a host-wideExpiresActive On; its header block already sentno-cacheand removedExpires. - i18n:
i18n.displayName(code, inLang)returns the name of the language ininLang(Intl.DisplayNames when the browser has data for that language, else the endonym, then the code); withoutinLangit is still the language's own name (meta.name). Language pickers (menu, toggle, settings row, terminallanglist) keep showing endonyms. - About: the Languages row names the offered languages in the current language ("Deutsch und Englisch" instead of "Deutsch und English").
- Notifications: with
notify.app: nulleach banner now shows the tile and name of the app its article opens in (the app the router picks, e.g. the page app with the longest URL prefix) instead of a generic bell and only the date; the bell remains for articles that open in a new tab. The summary banner ("And N more new articles") shows the app all new articles open in and opens it when it is their home (notify.app, a routed app that is not a page app, or a page app whose URL holds every article); otherwise it opens the newest article as before. Anotify.appthat cannot open (its module is missing) no longer lends its tile to the banners; they show the app the router actually opens. - Settings → Reset: "Reset everything" also resets the groups hidden at the moment; picks of a group that went hidden while Settings was open are dropped, and an open reset confirmation closes when the shown groups change, so a confirmed partial reset never runs as "everything". The backup window lists a hidden group only when the data holds it.
- Fortune app:
validateConfig()andnpm run validatewarn when a plain or language-map text infortune.textsuses a placeholder its key does not fill (e.g.{hots}). The text is kept as written. - Module loader: the contract (
docs/ARCHITECTURE.md§8) now states the load order. A module's config section is cleaned (validateConfig) before its declared parts are registered, so a declared value, such as a storage label getter, may readDesk.modules.config(id). - Manifest validator: an alias cycle (
a → b → a) is an error; none of its ids opens an app. - Documentation: the condition for
'wasm-unsafe-eval'with the Pagefind search is now stated exactly. Pagefind compiles its WebAssembly inpagefind-worker.js, under the policy sent with that file. It falls back to the page, under the desktop's policy, when the worker fails or has not started within 5 seconds, which can happen on a slow connection alone. The desktop's policy therefore needs it wheneversearch.pagefindis set, and so does the policy sent withpagefind-worker.js(the shipped server configurations send the desktop's policy with every file). Updated: README (en, de),docs/deploy.md§6 and §13, ARCHITECTURE §5/§6, the comments of all server configurations,site/config.js, and thenpm run servehelp. - The README copies of the server configurations are now checked against
docs/server/by the tests.
Fixed
- Dock: a visitor's own dock list keeps pins of an app whose id became an alias (an old id kept for old links after a rename). Stored pins and
config.dock.pinsresolve aliases to their target, duplicates this creates are dropped (the first keeps its place), and the resolved list is written back by the tab that read it. An alias the registry learns later (vault, module) is resolved onapps:change. Before, such a pin vanished silently. A visible alias of an app that cannot be pinned stays pinned as the alias whatever kind its target has (the rule asks whether an app may take a dock place at all, not whether its kind can open yet while modules load), and a visible alias whose target is not registered yet keeps its id until it is. - Desktop icons: an alias (e.g. a collection item that points at an app) no longer shows a second icon of its target — it inherited
desktop: true. The default dock pins already skipped aliases. - i18n: the start language follows the browser's language order (RFC 4647 lookup per entry): for each entry of
navigator.languagesthe exact tag, then the tag shortened subtag by subtag, then another region of the same language, before the next entry —de-AT, ennow starts in German, not English.src/boot/preload.jspredicts the same start language as the boot, so the locale hints match the language actually loaded. - Fortune app: "The texts are in {language}" names the language in the reader's language ("auf Englisch", not "auf English").
- Terminal:
lang <name>also accepts the language's name in the current language (lang englisch); the confirmation names the language in the current language. - Shell: until the target language's "Switch to …" phrase is loaded, the two-language toggle's fallback action names the target language in the current language instead of an unmarked endonym.
- Reader: no more CSP console reports while a page is parsed (
style="",<style>,<base>raised two report lines each — hundreds per page of highlighted code). Pages are parsed with theDOMParserof a removed same-originabout:blankiframe, whose document Chromium does not check; it is as inert as before and the sanitiser is unchanged. Other engines fall back to the window'sDOMParser. - Service worker: the second sweep of
offline.legacyCaches(about 30 s after an activation) no longer keeps an event open. In 1.2.0 the first request after an activation extended its event for 30 s, so a newer worker (an update or a rollback) could not activate until it ended. The follow-up now runs on a plain timer or on the first request after that moment, whichever comes first; a worker that a newer one replaced drops it. Old caches are still deleted on activation, about 30 s later, and at every start. - Service worker: the fast-start update check ends when a newer worker is installing or waiting, after its delay, before each batch of compares and before the crawl. It ran inside the start's event, so a newer worker had to wait for the whole compare and crawl.
- Server configurations: the nginx server block sets
expires off;. Otherwise anexpiresin nginx'shttp { }block addedExpiresand a secondCache-Controlwithmax-ageto the code, so browsers could mix module versions after an update. Caddy, Ferron and static-web-server needed no change.
Security
- The vault's "Private bookmarks" row in Settings → Reset appears only while the vault is unlocked or a login is kept on the device; visitors no longer learn that a vault exists, and no confirmation names it otherwise.
- Fortune app: a replaced consent question (
fortune.texts.askText) that names neither{host}nor a host of the online source is reported. The page reports it at'modules:ready';npm run validatereports it for the built-in providers. The text is kept. - The Reader's style allowlist refuses a whole
styleattribute that contains escapes, comments, quotes, braces,@or!important, and never keepsurl(),var(),calc(), image functions or any layout, position, size or display property (SECURITY.md, Reader section).
1.2.0 (opens in a new tab) — 2026-10-08
Fresh feeds and data with the fast start, site icon sets, manual pages per item, web windows anywhere on the site, a web overview app for collections, a Fortune app that a site can rename and run online only, and the clean removal of an earlier service worker's caches. Updating a site: see the upgrade notes below.
Upgrade notes
- Upload the new
sw.jswith the rest: the shell cache gets a new name once (version,iconSetsandoffline.fastStartare part of it), visitors are offered one reload. - Run
npm run preloadonce if you keep your ownsrc/boot/preload.js(the boot graph gainssrc/core/man.jsandsrc/core/icon-sets.js); CI'spreload:checkneeds it. site/config.jsgainsoffline.legacyCaches: [],iconSets(commented),fortune.localandfortune.textswith their comments — copy them if you keep your own file; without them the defaults apply and nothing changes.- Sites that set
offline.fastStart: falseonly because of their feeds can remove it: feeds, the fortune files and everything undersite/data/andsite/content/are fetched fresh first again. - Data is always the server's current version, code changes only as a whole: after a deploy a visitor may run the previous code against the new data for one session. Change a data format compatibly (add fields, do not rename or remove them; a new file name for an incompatible format), and deploy code that knows a new icon before data that names it.
- Data your own modules fetch at run time belongs under
site/data/(a file in a module's folder is code); scripts your modules import do not belong undersite/data/orsite/content/. Keep a Pagefind bundle outsidesite/(e.g.pagefind/at the root). - A site that replaced another service worker lists that worker's caches in
offline.legacyCaches(docs/deploy.md§11, "Replacing another service worker"); only such a site needs it. config.terminal.manUrl(a string) keeps working for the collections ofsite/apps.js; the vault's bookmarks no longer use it (they have no manual page). Items with a.md/.txtdocskeep printing it while they have nomanof their own; recommended: move the text twin tomanand letdocsbe the page (the Catalog opensdocs).- A
webUrlthat equals the collection'sbasePathused to only refocus the Catalog; it now opens in a new tab. To open the overview in a window, add a hidden web app ({ id: 'tools-web', kind: 'web', hidden: true, url: 'tools/' }) and setwebApp: 'tools-web'on the collection. A privatesite/that useswebAppstill loads on 1.1.0 (the field is ignored there). scopevalues starting with/on web apps were ignored before and take effect now (folders outside the desktop's). Links with#app=<id>&path=…open nothing in a browser that still runs a cached 1.1 desktop until the offered update is applied; open the link again after the update.- Sites that run
npm run validate:strictin CI may see new warnings:webUrlequal tobasePath, invalidwebUrl/allLabel/webLabelon a Catalog app or override record, missing manual files, large site icon sets. Fix the value (or addwebApp). - Visitors who agreed to the Fortune app's online source are asked once more (the agreement is now bound to the provider).
- A site module that wrote
<use href="#ti-x">itself must switch toDesk.icons.icon()orDesk.icons.symbolHref()(sprite symbols now have the DOM idi-<icon id>). npm run sealreadsconfig.iconSetsbelow the web root of--out(the project by default): when you seal into a deployment (--out <web>/site/vault/), the set must be in<web>/site/icon-sets/as well.- A private site repository that vendors the project's
.gitignoreand wants to track its icon set deletes thesite/icon-sets/*line or adds!site/icon-sets/<name>.json. - The terminal's
isRelPathis nowisSitePathofsrc/core/url.js(also refuses control characters).
Added
- Site icon sets — a site can bring its own icons next to Tabler: JSON files listed in
iconSetsofsite/config.js(e.g.site/icon-sets/duotone.json), ids'<prefix>-<name>'. They load before the modules (preload hint, kept offline by the service worker as part of the version), so tiles, dock, menus, search, the Catalog, the vault andbrand.glyphuse them like Tabler ids. Two-tone icons (k: 'd') paint their secondary layer with the new tokens--icon-duo-opacity(0.4) and--icon-duo-color.npm run validatechecks set ids and files,npm run sealaccepts set icons in vault data. The project ships no set, andsite/icon-sets/*is git-ignored; a set's licence is the site's business (docs/ARCHITECTURE.md§13). - Manual pages per item, collection and language —
manon collection items and collections (site/apps.js) names the text manual the terminal'sman <entry>prints: a path, a{ lang: path }map (for layouts{lang}cannot express) orfalse. Placeholders{slug}{id}{collection}{lang}; order: the item's ownman, its collection's, thenconfig.terminal.manUrl, which now also takes a{ lang: template }map (docs/ARCHITECTURE.md§7 "Manual pages"). The example site's "Writing pages" item ships a manual in English and German (man writing-pages). - Collections:
webApp— a collection (or its Catalog app) can name the app that its Catalog's "Overview on the web" button launches, e.g. a hiddenwebapp showing the collection's overview page at itsbasePath. It wins overwebUrlwhile it is available.npm run validatechecks the reference (docs/ARCHITECTURE.md§7). - Web windows anywhere on the site — web windows keep their in-frame location for start pages anywhere on the site, not only below the desktop's folder: an app at
/wiki/start/reopens on the page it showed after a reload.scopeon a web app may be root-absolute for folders outside the desktop (scope: '/wiki/') and is now also read on collection items. linkPaths: trueon a web app: "Copy link to this window" and the address bar keep its sub-page (#app=<id>&path=/<path>); the app's kind checks the path, a refused one opens the start page. Off by default.Desk.launch(id, { url })for an open web window loads that location while the window still shows (or is still loading) the page the desktop opened (otherwise the window is only shown, as before); a shared#app=<id>&path=…link therefore also wins over the location the visitor's saved session restores. This also applies to banner notifications and feed items that open a web app with a URL.- The
acceptUrl(app, path, from)hook learns where a path comes from:'session','launch'or'link'. - Removing an earlier service worker's caches:
config.offline.legacyCacheslists the cache names (exact orprefix*) of a service worker your site used before this desktop. They are deleted when the new worker takes over, again shortly after it, at every start and by Settings → Reset → "Offline copies". Names of this desktop's own scheme are never touched (docs/deploy.md§11). - Fortune app: modules add online sources declaratively (
fortuneProvidersin the descriptor), withoutrequires: ['fortune'];Desk.fortune.addProvider(def, { module })drops the provider when that module fails. - Fortune app:
config.fortune.local: falseruns the app online only (no local sayings, nothing precached, terminal command hidden, a clear message when no online source is available). - Fortune app:
config.fortune.textsreplaces the texts that name the app (buttons, status lines, the question, the label in Settings → Online services, the terminal help), so a renamed app reads consistently. Desk.icons.appGlyph(app, { cls, fallback }): an app's glyph without the tile (logo, mark or icon).Desk.icons.symbolHref(id): the<use>reference of an icon for site code that builds its own SVG.kit.copyWithFeedback()accepts functions forkeyanddoneKey.tools/browser-check.mjs --route path=fileserves a local file (e.g. a site module) for a check;tools/serve.mjs --extra <url-path>=<file>serves single files from outside the tree (tests, trials).- Service
deeplinks:hashOf(info), the pure hash builder behindhashFor(win). Desk.router.acceptPath()takes an array of start pages (one per language); the path qualifies when it qualifies for any of them.
Changed
- Server configurations: the Apache
.htaccessalso switchesmod_expiresoff for the desktop (ExpiresActive Offin<IfModule mod_expires.c><Files "*">) — hardening against a host-wideExpiresActive On; its header block already sentno-cacheand removedExpires. - Sprite symbols have the DOM id
i-<icon id>(was: the icon id), andicon()returns<use href="#i-<id>">. Code that wrotehref="#<icon id>"by hand usesDesk.icons.symbolHref(id)instead. Element ids starting withi-are reserved for the sprite. - Every icon definition is built through an allowlist (§5, §13): tags
path circle ellipse rect line polyline polygon, presentation attributes (including dash patterns,vector-effect,paint-order) and plain classes — for elements, for a pack's symbol attributesaand its viewBox. A runtime pack (Desk.icons.add) loses other tags (e.g.g),style,id,href,on*, and every value with a backslash (CSS escape) or a CSS function other than the transform and colour functions — so nourl(),src(),var()orenv()— with a console warning; an invalid viewBox becomes0 0 24 24. man <entry>without a manual — or whose manual file is missing (404/410, or an HTML page answered with 200) — says "<name> has no manual page." and offers the documentation or the entry itself, instead of an error. A failed request (offline, timeout, server error) is still reported as an error; a long manual is still cut short and printed, ascatdoes. An exact entry name always means that entry (no longer a prefix sibling's manual);man <name>matches a part of a name only among entries that have a manual.- Collection and
manUrltemplates no longer apply to items the vault adds — also when the vault creates the collection itself;manUrlapplies only to the site's own collections. An alias item no longer shows its target's manual.terminal.manUrlis merged as a whole like the other language maps of the config. - Deep links
#app=<id>&<name>=<value>with unknown parameters open the app instead of nothing. - Locations of web windows are refused when their raw path or the way a server may read it (escapes of unreserved characters such as
%2eor%61decoded once,%2f/%5cread as/,//collapsed) contains a dot segment (also..;x). Encoded slashes and empty segments that stay inside the app's folder are kept. - Fortune app: the card shows the app's glyph (logo, mark or icon of a site override) instead of the fixed cookie.
- Fortune app: the agreement to the online source is bound to the provider (id and hosts); when the provider changes, visitors are asked again.
Desk.fortune.source()returnsnullwithfortune.local: falseand no usable online source. A provider'snameor a categorylabelgiven as{ lang: text }must not contain empty strings (such providers are rejected, such categories dropped, with a warning); a provider names at most 8 hosts. npm run validatealso checkswebUrl,allLabelandwebLabelon Catalog apps and their override records (as warnings), warns when a collection'swebUrlequals itsbasePathand that address leads back to the collection's Catalog, checksmanandterminal.manUrland counts manual files missing for a collection's items (warnings), checksscope/linkPaths, the Fortune app'slocalandtexts, and the site icon sets.- Service worker:
offline.fastStartis part of the shell cache name — switching it starts a fresh offline copy. Copies of files the desktop read at runtime that are neither code nor data files (amanorcattext outsidesite/data/andsite/content/, an image) carry the response headerX-Desk-Copy: runtime.
Fixed
- Service worker: feeds and other data are fresh again. With
offline.fastStart(1.1.0) the feeds (notify.feeds, also outside the desktop's folder), the fortune files and everything undersite/data/andsite/content/were answered from the offline copy, and every change of one — a new feed item — made the worker fetch the whole desktop again and offer "A new version is ready". These data files are network first again (offline: the last copy), the update check skips them and a prepared update never contains them. Code stays part of the version wherever it lies:site/config.js,site/apps.js(also whensite.datapoints intosite/data/),site/theme.css, site icon sets, site modules and wallpapers (docs/ARCHITECTURE.md§14). - Service worker (fast start): any other file the desktop read at runtime that the install did not fetch (not a script, style or other code) is refreshed in place when it changes on the server and dropped when it is gone — instead of re-fetching the whole desktop and offering an update on every start.
- The Catalog's "Overview on the web" button no longer just refocuses the Catalog when
webUrlis the collection'sbasePath. That address now opens in a new tab (or name a window inwebApp). - Fortune app: no "unknown online source" warning on every start when a module adds the configured provider; the check now runs once at
'modules:ready'. - Docs:
docs/deploy.md§11 anddocs/packages/p12-pwa-deploy.mdlistoffline.fastStart.
Security
- Locations of web windows never reach a reserved folder (
config.vault.dir), also not written with percent escapes (/site/v%61ult/), even when the web app's folder orscopecontains it (1.1 accepted/desk/site/vault/…for a start page orscopeinsite/), nor the desktop's own folder in another letter case, nor itsindex.html. - Locations of web windows are also refused when a server could read them as a reserved folder or as the desktop through path parameters (
vault;x), Windows stream suffixes (vault::$INDEX_ALLOCATION) or trailing dots and spaces (vault.,vault%20). - A
scopethat is the installation root or a parent of it, a root-absolutescopeinside it, or one that is not a plain folder path is ignored with a warning. - Site icon sets and runtime icon packs pass an allowlist of SVG tags, attributes and values (see "Changed"); icon data never runs code.
1.1.0 (opens in a new tab) — 2026-10-08
Faster start, a house theme for site owners, site apps with their own texts, a live demo and a hardened supply chain. Updating a site: see the upgrade notes below.
Upgrade notes
index.htmlgained two lines:<link rel="stylesheet" href="site/theme.css">after the core CSS and<script src="src/boot/preload.js"></script>right aftersite/config.js— copy both into an adaptedindex.html, and copysite/theme.cssinto yoursite/(an empty file is fine).- Your own modules and site apps: run
npm run preloadafter adding or changing one (faster start; a stale file only costs speed). Code that callsDesk.media.open()/add()gets aPromise<number>now instead of a number. - Visitors with the service worker get updates one reload later: the desktop offers the reload (
config.offline.fastStart: falsefor the old behaviour). - Development tools need Node.js 24 or newer; install them with
npm ci(sfw npm cirecommended) and the headless browser withnpm run browsers.
Changed
- Server configurations: the Apache
.htaccessalso switchesmod_expiresoff for the desktop (ExpiresActive Offin<IfModule mod_expires.c><Files "*">) — hardening against a host-wideExpiresActive On; its header block already sentno-cacheand removedExpires. - Faster start: window code on demand — an app definition can name its window code with
load: () => import('./window.js')(a window kind withdefineKind(kind, { load })), its window-only CSS withwindowStyles. The boot no longer loads the editor, terminal, players, fortune, notes, todo, calculator, the panels' contents, Reader, viewer and Catalog windows; they come with the first window (spinner meanwhile).wm.open()still returns the window at once;win.readyand the event'window:ready'tell when its content is built (docs/ARCHITECTURE.md§8, §19).Desk.media.open/addnow returnPromise<number>;Desk.terminal.list()/has()know the built-in commands once the first terminal window opened. - Faster start: preload hints —
src/boot/preload.js(generated bynpm run preload, checked in CI) requests every file of the boot at once instead of one import level after the other; language metadata, strings and site data load side by side. Upgrade note:index.htmlloadssrc/boot/preload.jsright aftersite/config.js— copy that line into an adaptedindex.html. - Faster repeat visits: fast start from the offline copy — the service worker answers the desktop's own files from its copy and checks for a new version in the background; a complete new copy is prepared and the open desktop offers a reload.
config.offline.fastStart: falserestores "network first". - Supply chain — Node.js ≥ 24;
.npmrcwithignore-scripts,save-exact,engine-strict; exact versions of the two development tools (playwright-core 1.64.0);npm run browsersinstead ofnpx; GitHub Actions pinned to commit SHAs, CI on Node 24 withnpm ci --ignore-scriptsandnpm audit signatures; Socket Firewall Free (sfw npm …) recommended (CONTRIBUTING.md(opens in a new tab)).
Added
- House theme —
site/theme.cssin@layer themes, loaded before the first paint and kept offline; radius, glass and shadow tokens (families and part tokens) with unchanged defaults; full token reference indocs/theming.md. Upgrade note:index.htmllinkssite/theme.css— when updating an existing site, copy it into yoursite/(an empty file is fine) and do not delete it. - Live demo on GitHub Pages —
.github/workflows/pages.ymlpublishes the shipped example site, as it is and without a build step, to https://jpkcom.github.io/jpkcom-desktop/ (opens in a new tab) on every push tomain(or when started by hand). GitHub Pages cannot send response headers, so the workflow adds the production Content-Security-Policy as a<meta>tag to the published copy ofindex.html(withoutframe-ancestors, which a<meta>policy cannot carry). Copies made from the template publish only when they set the repository variablePAGEStotrue; otherwise the job is skipped (docs/deploy.md(opens in a new tab)). - Quickstart "Your own site in 10 minutes" — five steps from the template to a published site, in English (
docs/quickstart.md(opens in a new tab)) and German (docs/quickstart.de.md(opens in a new tab)). - Template repository — the repository is a GitHub template; the READMEs start with "Use this template" and link the live demo and the quickstart.
- Site apps with their own texts: a module or app in
site/modules/<id>/can keep its translations next to its code (descriptor fieldlocales);npm run i18n:checkchecks them like the desktop's own. - Example app "Hello" (
site/modules/hello/): a template for your own apps — window, texts with placeholder and plural, CSS, stored value with backup and reset, terminal commandhello.
1.0.0 (opens in a new tab) — 2026-10-06
Initial open-source release under the MIT License.
Added
- Foundation — vanilla JavaScript with native ES modules, no build step, no runtime dependencies, no CDN. A strict Content Security Policy (no inline script or style, no
eval, noinnerHTML); DOM is built withh()/s()andtextContent. Everything optional is a module described by a descriptor (apps, styles, i18n namespaces, storage keys, reset groups, trash types, online services, settings, shortcuts, terminal commands, search providers, file-drop handlers, context menus). A public, frozenwindow.JPKDeskAPI, a service registry and a bus with documented events (docs/ARCHITECTURE.md(opens in a new tab)). - Site folder — everything a site owner changes lives in
site/:config.js(every key optional and documented inline, validated with warnings and defaults),apps.js(apps, collections, menus, terminal files), Reader pages, fortunes, feeds, wallpapers and sealed vault files. A neutral example site ships and is meant to be replaced. - Any number of languages — German and English ship; a new language is a folder in
locales/. Fallback chain, named placeholders, CLDR plural forms,Intlformatting, right-to-left support, language toggle for two languages and a menu for more. - Window manager — window kinds (
web,app,nativebuilt in, more from modules), drag, resize, minimise, zoom, keyboard operation, configurable window controls (left/right, classic/minimal); edge snapping and a tile menu, a window overview, session restore and deep links to windows. - Shell — menu bar with arrow-key navigation, status area, clock and language switch, folding on phones; dock with pins, magnification and sizes; desktop icons; "All apps"; keyboard shortcuts; context menus; notification banners; file drops onto the desktop; boot, restart and shut-down screens.
- Panels — Settings (appearance, accent colours, online services, reset), Wallpaper (colours, gradients, pictures and generated SVG motifs), Backup and restore of the local data, Trash, About this desktop, How it works, and app installation.
- Content modules — Reader for HTML pages (fetched, sanitised against an allowlist and shown in a window), image viewer, Catalog for collections (bookmarks, tools, a portfolio …), quick search over apps and collections with optional Pagefind full-text search.
- Calendar, holidays, weather, notifications — a calendar popover under the clock, public holidays per region (Bavaria as an example), weather with two providers (Open-Meteo worldwide, Bright Sky for Germany), feed notifications from a JSON Feed per language.
- Vault — private bookmarks sealed with
tools/seal-vault.mjs(PBKDF2-HMAC-SHA-256 and AES-256-GCM), unlocked in the terminal, optionally kept on the device. - Apps — text editor with tabs, notes, tasks, calculator, a terminal with a command registry that modules extend, audio and video players for files from the device (tags and cover art read locally), and Fortune with local jokes and facts plus optional online sources.
- Privacy — every online service is off by default; the site has to switch it on and the visitor has to agree before the first request; consent can be withdrawn in Settings. No cookies, no referrer on third-party requests.
- Accessibility — menu bar as
menubar, windows as named dialogs, focus handling and restoration, one shared live region, reduced motion respected in CSS and JavaScript. - Branding and theming — JPK monogram and JPKCom logo as the default brand, replaceable through
site/config.js; they are the author's personal logo and not MIT (brand assets, see CREDITS.md (opens in a new tab)); dark and light themes, accent colours, tile tints and wallpapers configurable. - PWA and offline — service worker with an offline copy of the desktop and of Reader pages, web app manifest and app icons; works in the web root or any sub-folder.
- Deployment — a deployment guide (
docs/deploy.md(opens in a new tab)) and tested server configurations for Apache (.htaccess), nginx, Caddy, Ferron 2 and 3 and static-web-server (docs/server/(opens in a new tab)), all with the same security headers and commented opt-in lines. - Tools and tests — local server with the production headers (
npm run serve), icon subset builder and check, i18n check, site manifest validator, vault sealing tool, PWA icon renderer, headless browser check, and anode --testsuite for the pure parts of every package.