This commit is contained in:
@@ -32,7 +32,7 @@ flowchart LR
|
|||||||
| `posts/` | Longer-lived posts; `posts/career/` has its own index | Source, except generated list pages |
|
| `posts/` | Longer-lived posts; `posts/career/` has its own index | Source, except generated list pages |
|
||||||
| `home/` | Utility and dashboard-style pages | Source, except `categories.org` |
|
| `home/` | Utility and dashboard-style pages | Source, except `categories.org` |
|
||||||
| `play/` | Interactive and experimental pages | Source |
|
| `play/` | Interactive and experimental pages | Source |
|
||||||
| `rl/` | Browser-local Resource Loader SPA | Source |
|
| `rl/` | Server-synced Resource Loader SPA | Source |
|
||||||
| `lima/` | Org or Markdown pages with special attachment handling | Source, except `lima-list.org` |
|
| `lima/` | Org or Markdown pages with special attachment handling | Source, except `lima-list.org` |
|
||||||
| `assets/` | Browser code, styles, fonts, images, and content manifests | Mostly source; some manifests/scripts are generated externally |
|
| `assets/` | Browser code, styles, fonts, images, and content manifests | Mostly source; some manifests/scripts are generated externally |
|
||||||
| `lisp/` | Modular Emacs Lisp publishing system | Source |
|
| `lisp/` | Modular Emacs Lisp publishing system | Source |
|
||||||
@@ -275,12 +275,13 @@ The published HTML is static, but some browser modules rely on same-origin APIs:
|
|||||||
| Notes board | `pages/notes.js` | `/api/notes` |
|
| Notes board | `pages/notes.js` | `/api/notes` |
|
||||||
| Wird tracker | `pages/wird-tracker.js` | `/api/wird`, `/api/wird/motalah`, `/api/calibre/books` |
|
| Wird tracker | `pages/wird-tracker.js` | `/api/wird`, `/api/wird/motalah`, `/api/calibre/books` |
|
||||||
| Competency board | `pages/competency-status-board.js` | `/api/competencies/items...` |
|
| Competency board | `pages/competency-status-board.js` | `/api/competencies/items...` |
|
||||||
|
| Resource Loader | `pages/resource-loader.js` | `/api/resource-loader`, `/api/resource-loader/thumbnails/:id` |
|
||||||
|
|
||||||
These APIs are not implemented here. Production routing must serve `output/` and proxy `/api/` to the appropriate backend. When changing an endpoint contract, coordinate the static client and its external service.
|
These APIs are not implemented here. Production routing must serve `output/` and proxy `/api/` to the appropriate backend. When changing an endpoint contract, coordinate the static client and its external service.
|
||||||
|
|
||||||
The hidden-details feature uses browser `localStorage`/`sessionStorage` for visit and discovery state and may call Open-Meteo after explicit geolocation interaction. Its friendly content source is `assets/content/hidden-details.json`; `assets/scripts/features/hidden-details.js` is generated by the external hidden-narrative authoring workflow. Treat both files as a pair and do not hand-edit only the generated JavaScript.
|
The hidden-details feature uses browser `localStorage`/`sessionStorage` for visit and discovery state and may call Open-Meteo after explicit geolocation interaction. Its friendly content source is `assets/content/hidden-details.json`; `assets/scripts/features/hidden-details.js` is generated by the external hidden-narrative authoring workflow. Treat both files as a pair and do not hand-edit only the generated JavaScript.
|
||||||
|
|
||||||
The Resource Loader at `/rl/` is intentionally backend-free. It seeds Work and Personal Study vaults from `assets/content/resource-loader.json`, then stores library metadata and retained PDFs in IndexedDB under `zxh-resource-loader`. JSON exports contain metadata and reading sessions, not PDF binaries. Its page code lives in `assets/scripts/pages/resource-loader.js`; the separately testable schema/progress operations live in `resource-loader-model.js` and run with `make test-js`.
|
The Resource Loader at `/rl/` loads and saves metadata through the same-origin `/api/resource-loader` backend, with ETags protecting concurrent edits. Generated cover thumbnails use `/api/resource-loader/thumbnails/:id`. IndexedDB under `zxh-resource-loader` remains an offline cache and retains imported PDF binaries only on the device where they were added. JSON exports contain metadata and reading sessions, not PDF binaries. Its page code lives in `assets/scripts/pages/resource-loader.js`; the separately testable schema/progress/sync operations live in `resource-loader-model.js` and run with `make test-js`.
|
||||||
|
|
||||||
## Authoring service
|
## Authoring service
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,37 @@
|
|||||||
return normaliseLibrary(merged);
|
return normaliseLibrary(merged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function newestByTimestamp(remote, local) {
|
||||||
|
const remoteStamp = String(remote.updatedAt || remote.createdAt || "");
|
||||||
|
const localStamp = String(local.updatedAt || local.createdAt || "");
|
||||||
|
return clone(localStamp >= remoteStamp ? local : remote);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeSyncItems(remoteItems, localItems, mergeMatch) {
|
||||||
|
const items = new Map(remoteItems.map((item) => [item.id, clone(item)]));
|
||||||
|
localItems.forEach((local) => {
|
||||||
|
const remote = items.get(local.id);
|
||||||
|
items.set(local.id, remote ? mergeMatch(remote, local) : clone(local));
|
||||||
|
});
|
||||||
|
return Array.from(items.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeForSync(remoteLibrary, localLibrary) {
|
||||||
|
const remote = normaliseLibrary(remoteLibrary);
|
||||||
|
const local = normaliseLibrary(localLibrary);
|
||||||
|
const vaults = mergeSyncItems(remote.vaults, local.vaults, (remoteVault, localVault) => {
|
||||||
|
const vault = newestByTimestamp(remoteVault, localVault);
|
||||||
|
vault.folders = mergeSyncItems(remoteVault.folders, localVault.folders, newestByTimestamp);
|
||||||
|
vault.resources = mergeSyncItems(remoteVault.resources, localVault.resources, (remoteResource, localResource) => {
|
||||||
|
const resource = newestByTimestamp(remoteResource, localResource);
|
||||||
|
resource.sessions = mergeSyncItems(remoteResource.sessions || [], localResource.sessions || [], newestByTimestamp);
|
||||||
|
return resource;
|
||||||
|
});
|
||||||
|
return vault;
|
||||||
|
});
|
||||||
|
return normaliseLibrary({ schemaVersion: SCHEMA_VERSION, vaults });
|
||||||
|
}
|
||||||
|
|
||||||
function progressFor(resource) {
|
function progressFor(resource) {
|
||||||
const sessions = Array.isArray(resource.sessions) ? resource.sessions : [];
|
const sessions = Array.isArray(resource.sessions) ? resource.sessions : [];
|
||||||
const furthest = sessions.reduce((maximum, session) => {
|
const furthest = sessions.reduce((maximum, session) => {
|
||||||
@@ -218,6 +249,7 @@
|
|||||||
validateLibrary,
|
validateLibrary,
|
||||||
normaliseLibrary,
|
normaliseLibrary,
|
||||||
mergeLibraries,
|
mergeLibraries,
|
||||||
|
mergeForSync,
|
||||||
progressFor,
|
progressFor,
|
||||||
addVault,
|
addVault,
|
||||||
renameVault,
|
renameVault,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
const DB_VERSION = 1;
|
const DB_VERSION = 1;
|
||||||
const META_STORE = "metadata";
|
const META_STORE = "metadata";
|
||||||
const ATTACHMENT_STORE = "attachments";
|
const ATTACHMENT_STORE = "attachments";
|
||||||
|
const API_URL = "/api/resource-loader";
|
||||||
const $ = (selector) => document.querySelector(selector);
|
const $ = (selector) => document.querySelector(selector);
|
||||||
const $$ = (selector) => Array.from(document.querySelectorAll(selector));
|
const $$ = (selector) => Array.from(document.querySelectorAll(selector));
|
||||||
const state = {
|
const state = {
|
||||||
@@ -22,6 +23,10 @@
|
|||||||
pendingImport: null,
|
pendingImport: null,
|
||||||
pdfUrls: new Map(),
|
pdfUrls: new Map(),
|
||||||
thumbnailUrls: new Map(),
|
thumbnailUrls: new Map(),
|
||||||
|
uploadedThumbnails: new Set(),
|
||||||
|
serverEtag: null,
|
||||||
|
syncQueue: Promise.resolve(),
|
||||||
|
eventsBound: false,
|
||||||
};
|
};
|
||||||
let pdfJsPromise;
|
let pdfJsPromise;
|
||||||
|
|
||||||
@@ -54,6 +59,7 @@
|
|||||||
jsonInput: $("#rl-json-input"),
|
jsonInput: $("#rl-json-input"),
|
||||||
loading: $("#rl-loading"),
|
loading: $("#rl-loading"),
|
||||||
toast: $("#rl-toast"),
|
toast: $("#rl-toast"),
|
||||||
|
syncStatus: $("#rl-sync-status"),
|
||||||
};
|
};
|
||||||
|
|
||||||
function openDatabase() {
|
function openDatabase() {
|
||||||
@@ -86,21 +92,93 @@
|
|||||||
function putAttachment(record) { return request(ATTACHMENT_STORE, "readwrite", (store) => store.put(record)); }
|
function putAttachment(record) { return request(ATTACHMENT_STORE, "readwrite", (store) => store.put(record)); }
|
||||||
function deleteAttachment(id) { return id ? request(ATTACHMENT_STORE, "readwrite", (store) => store.delete(id)) : Promise.resolve(); }
|
function deleteAttachment(id) { return id ? request(ATTACHMENT_STORE, "readwrite", (store) => store.delete(id)) : Promise.resolve(); }
|
||||||
|
|
||||||
|
function setSyncStatus(status, label) {
|
||||||
|
if (!els.syncStatus) return;
|
||||||
|
els.syncStatus.dataset.state = status;
|
||||||
|
els.syncStatus.querySelector("small").textContent = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchServerLibrary() {
|
||||||
|
const response = await fetch(API_URL, { cache: "no-store", credentials: "same-origin" });
|
||||||
|
if (response.status === 404) return null;
|
||||||
|
if (!response.ok) throw new Error(`Server library request failed (${response.status}).`);
|
||||||
|
const library = Model.normaliseLibrary(await response.json());
|
||||||
|
state.serverEtag = response.headers.get("ETag");
|
||||||
|
return library;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadLocalThumbnails(library) {
|
||||||
|
const ids = Array.from(new Set(library.vaults.flatMap((vault) => vault.resources.map((resource) => resource.thumbnailId).filter(Boolean))));
|
||||||
|
await Promise.all(ids.map(async (id) => {
|
||||||
|
if (state.uploadedThumbnails.has(id)) return;
|
||||||
|
const attachment = await getAttachment(id);
|
||||||
|
if (!attachment || !attachment.blob || attachment.kind !== "thumbnail") return;
|
||||||
|
const response = await fetch(`${API_URL}/thumbnails/${encodeURIComponent(id)}`, {
|
||||||
|
method: "PUT",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: { "Content-Type": attachment.type || attachment.blob.type || "image/jpeg" },
|
||||||
|
body: attachment.blob,
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`Thumbnail upload failed (${response.status}).`);
|
||||||
|
state.uploadedThumbnails.add(id);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushServerLibrary(library, allowMerge) {
|
||||||
|
setSyncStatus("syncing", "syncing…");
|
||||||
|
const headers = { "Content-Type": "application/json" };
|
||||||
|
if (state.serverEtag) headers["If-Match"] = state.serverEtag;
|
||||||
|
const response = await fetch(API_URL, {
|
||||||
|
method: "PUT",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(library),
|
||||||
|
});
|
||||||
|
if (response.status === 412 && allowMerge) {
|
||||||
|
const remote = await fetchServerLibrary();
|
||||||
|
const merged = Model.mergeForSync(remote, library);
|
||||||
|
state.library = merged;
|
||||||
|
await setMetadata("library", merged);
|
||||||
|
if (state.eventsBound) render();
|
||||||
|
toast("Changes from another device were merged.");
|
||||||
|
return pushServerLibrary(merged, false);
|
||||||
|
}
|
||||||
|
if (!response.ok) throw new Error(`Server save failed (${response.status}).`);
|
||||||
|
state.serverEtag = response.headers.get("ETag");
|
||||||
|
await uploadLocalThumbnails(library);
|
||||||
|
setSyncStatus("synced", "synced to server");
|
||||||
|
}
|
||||||
|
|
||||||
async function saveLibrary() {
|
async function saveLibrary() {
|
||||||
await setMetadata("library", state.library);
|
await setMetadata("library", Model.normaliseLibrary(state.library));
|
||||||
|
state.syncQueue = state.syncQueue.catch(() => {}).then(async () => {
|
||||||
|
try {
|
||||||
|
await pushServerLibrary(Model.normaliseLibrary(state.library), true);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Resource Loader server sync is unavailable.", error);
|
||||||
|
setSyncStatus("offline", "offline · saved here");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return state.syncQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadLibrary() {
|
async function loadLibrary() {
|
||||||
const saved = await getMetadata("library");
|
const saved = await getMetadata("library");
|
||||||
if (saved) return Model.normaliseLibrary(saved);
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/assets/content/resource-loader.json", { cache: "no-store" });
|
const remote = await fetchServerLibrary();
|
||||||
if (!response.ok) throw new Error("Seed library could not be loaded.");
|
if (remote) {
|
||||||
return Model.normaliseLibrary(await response.json());
|
setSyncStatus("synced", "synced to server");
|
||||||
} catch (error) {
|
await setMetadata("library", remote);
|
||||||
console.warn(error);
|
return remote;
|
||||||
return Model.createSeed();
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Resource Loader server is unavailable; using this browser's cache.", error);
|
||||||
|
setSyncStatus("offline", "offline · using cache");
|
||||||
|
}
|
||||||
|
if (saved) return Model.normaliseLibrary(saved);
|
||||||
|
const response = await fetch("/assets/content/resource-loader.json", { cache: "no-store" });
|
||||||
|
if (response.ok) return Model.normaliseLibrary(await response.json());
|
||||||
|
return Model.createSeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentVault() {
|
function currentVault() {
|
||||||
@@ -216,8 +294,17 @@
|
|||||||
if (!id) return null;
|
if (!id) return null;
|
||||||
if (state.thumbnailUrls.has(id)) return state.thumbnailUrls.get(id);
|
if (state.thumbnailUrls.has(id)) return state.thumbnailUrls.get(id);
|
||||||
const attachment = await getAttachment(id);
|
const attachment = await getAttachment(id);
|
||||||
if (!attachment || !attachment.blob) return null;
|
let blob = attachment && attachment.blob;
|
||||||
const url = URL.createObjectURL(attachment.blob);
|
if (!blob) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/thumbnails/${encodeURIComponent(id)}`, { credentials: "same-origin" });
|
||||||
|
if (response.ok) blob = await response.blob();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Thumbnail ${id} is unavailable.`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!blob) return null;
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
state.thumbnailUrls.set(id, url);
|
state.thumbnailUrls.set(id, url);
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
@@ -718,7 +805,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function clearData() {
|
async function clearData() {
|
||||||
if (!confirm("Clear every Resource Loader vault, record, session, and retained PDF from this browser?")) return;
|
if (!confirm("Clear this browser's offline cache and retained PDFs? Server metadata and thumbnails will remain available.")) return;
|
||||||
closeDialog(els.storageDialog);
|
closeDialog(els.storageDialog);
|
||||||
state.db.close();
|
state.db.close();
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
@@ -730,6 +817,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindEvents() {
|
function bindEvents() {
|
||||||
|
state.eventsBound = true;
|
||||||
els.vaultSelect.addEventListener("change", () => { state.vaultId = els.vaultSelect.value; state.folderId = "all"; render(); });
|
els.vaultSelect.addEventListener("change", () => { state.vaultId = els.vaultSelect.value; state.folderId = "all"; render(); });
|
||||||
[els.search, els.typeFilter, els.statusFilter, els.tagFilter, els.sort].forEach((control) => control.addEventListener(control === els.search ? "input" : "change", renderResources));
|
[els.search, els.typeFilter, els.statusFilter, els.tagFilter, els.sort].forEach((control) => control.addEventListener(control === els.search ? "input" : "change", renderResources));
|
||||||
$("#rl-view-list").addEventListener("click", () => { state.view = "list"; localStorage.setItem("rl-view", state.view); renderResources(); });
|
$("#rl-view-list").addEventListener("click", () => { state.view = "list"; localStorage.setItem("rl-view", state.view); renderResources(); });
|
||||||
@@ -763,6 +851,7 @@
|
|||||||
state.pdfUrls.forEach((url) => URL.revokeObjectURL(url));
|
state.pdfUrls.forEach((url) => URL.revokeObjectURL(url));
|
||||||
state.thumbnailUrls.forEach((url) => URL.revokeObjectURL(url));
|
state.thumbnailUrls.forEach((url) => URL.revokeObjectURL(url));
|
||||||
});
|
});
|
||||||
|
window.addEventListener("online", () => saveLibrary());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initialise() {
|
async function initialise() {
|
||||||
|
|||||||
@@ -158,6 +158,8 @@
|
|||||||
.rl-summary small { color: var(--rl-muted); font-size: .72rem; font-weight: 750; text-transform: uppercase; letter-spacing: .06em; }
|
.rl-summary small { color: var(--rl-muted); font-size: .72rem; font-weight: 750; text-transform: uppercase; letter-spacing: .06em; }
|
||||||
.rl-summary .rl-summary__privacy { justify-content: flex-end; }
|
.rl-summary .rl-summary__privacy { justify-content: flex-end; }
|
||||||
.rl-summary__privacy span { color: #4a8a5b; font-size: .8rem; }
|
.rl-summary__privacy span { color: #4a8a5b; font-size: .8rem; }
|
||||||
|
.rl-summary__privacy[data-state="syncing"] span { color: #a87831; }
|
||||||
|
.rl-summary__privacy[data-state="offline"] span { color: #a13f35; }
|
||||||
|
|
||||||
.rl-command-bar {
|
.rl-command-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -937,3 +937,10 @@ at <ScriptBlock>, /home/zaine/master-folder/org-platform/org_web/build-logs/gite
|
|||||||
2026-08-20T11:32:52.2510149+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
|
2026-08-20T11:32:52.2510149+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
|
||||||
2026-08-20T11:32:52.3284574+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
|
2026-08-20T11:32:52.3284574+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
|
||||||
2026-08-20T11:32:52.6620246+01:00 [INFO] Sent authoring server test notification.
|
2026-08-20T11:32:52.6620246+01:00 [INFO] Sent authoring server test notification.
|
||||||
|
2026-08-20T11:34:32.1361264+01:00 [INFO] Prepared current build status for zaine/org_web: severity=Info, status=success
|
||||||
|
2026-08-20T11:34:32.1453561+01:00 [INFO] Fetching recent Gitea Actions runs for zaine/org_web.
|
||||||
|
2026-08-20T11:34:32.6512992+01:00 [ERROR] Failed to analyze job 841: Cannot bind argument to parameter 'LogText' because it is an empty string.
|
||||||
|
2026-08-20T11:34:33.2869130+01:00 [INFO] Sent build status notification with 1 embed(s).
|
||||||
|
2026-08-20T11:34:33.3081787+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
|
||||||
|
2026-08-20T11:34:33.4006115+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
|
||||||
|
2026-08-20T11:34:33.7249113+01:00 [INFO] Sent authoring server test notification.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
||||||
|
|
||||||
* Posts:
|
* Posts:
|
||||||
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">20-08-2026 01:01</span>@@
|
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">20-08-2026 11:34</span>@@
|
||||||
- [[file:career/cross-site-scripting-xss.org][Cross-Site Scripting (XSS)]] @@html:<span class="post-date">01-06-2026 10:21</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/cross-site-scripting-xss.org][Cross-Site Scripting (XSS)]] @@html:<span class="post-date">01-06-2026 10:21</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
|
|||||||
10
rl/index.org
10
rl/index.org
@@ -7,9 +7,9 @@
|
|||||||
<main class="rl-app" id="resource-loader" aria-labelledby="rl-title">
|
<main class="rl-app" id="resource-loader" aria-labelledby="rl-title">
|
||||||
<header class="rl-hero">
|
<header class="rl-hero">
|
||||||
<div class="rl-hero__copy">
|
<div class="rl-hero__copy">
|
||||||
<p class="rl-eyebrow">Private browser library</p>
|
<p class="rl-eyebrow">Private synced library</p>
|
||||||
<h1 id="rl-title">Resource Loader</h1>
|
<h1 id="rl-title">Resource Loader</h1>
|
||||||
<p>Keep books, papers, articles, and videos together. Your vaults stay in this browser.</p>
|
<p>Keep books, papers, articles, and videos together. Metadata and covers follow you across devices.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rl-hero__actions">
|
<div class="rl-hero__actions">
|
||||||
<label class="rl-vault-picker" for="rl-vault-select">
|
<label class="rl-vault-picker" for="rl-vault-select">
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<div><span id="rl-stat-active">0</span><small>active</small></div>
|
<div><span id="rl-stat-active">0</span><small>active</small></div>
|
||||||
<div><span id="rl-stat-backlog">0</span><small>backlog</small></div>
|
<div><span id="rl-stat-backlog">0</span><small>backlog</small></div>
|
||||||
<div><span id="rl-stat-completed">0</span><small>completed</small></div>
|
<div><span id="rl-stat-completed">0</span><small>completed</small></div>
|
||||||
<div class="rl-summary__privacy"><span aria-hidden="true">●</span><small>stored locally</small></div>
|
<div class="rl-summary__privacy" id="rl-sync-status" data-state="syncing"><span aria-hidden="true">●</span><small>connecting…</small></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rl-command-bar" aria-label="Resource actions">
|
<section class="rl-command-bar" aria-label="Resource actions">
|
||||||
@@ -165,8 +165,8 @@
|
|||||||
<dialog class="rl-dialog" id="rl-storage-dialog">
|
<dialog class="rl-dialog" id="rl-storage-dialog">
|
||||||
<div class="rl-dialog__shell rl-dialog__shell--small">
|
<div class="rl-dialog__shell rl-dialog__shell--small">
|
||||||
<header class="rl-dialog__head"><div><p class="rl-eyebrow">This browser</p><h2>Local storage</h2></div><button type="button" class="rl-close" data-close-dialog aria-label="Close">×</button></header>
|
<header class="rl-dialog__head"><div><p class="rl-eyebrow">This browser</p><h2>Local storage</h2></div><button type="button" class="rl-close" data-close-dialog aria-label="Close">×</button></header>
|
||||||
<div class="rl-dialog__body"><p id="rl-storage-summary">Calculating storage use…</p><p class="rl-help">Export JSON regularly. Clearing browser data removes vault metadata and retained PDFs.</p><button type="button" id="rl-request-persistence">Request persistent storage</button></div>
|
<div class="rl-dialog__body"><p id="rl-storage-summary">Calculating storage use…</p><p class="rl-help">Metadata and thumbnails sync to the server. PDFs remain only in the browser where they were imported.</p><button type="button" id="rl-request-persistence">Request persistent storage</button></div>
|
||||||
<footer class="rl-dialog__foot"><button type="button" class="rl-danger" id="rl-clear-data">Clear Resource Loader data</button><button type="button" data-close-dialog>Close</button></footer>
|
<footer class="rl-dialog__foot"><button type="button" class="rl-danger" id="rl-clear-data">Clear this browser's cache & PDFs</button><button type="button" data-close-dialog>Close</button></footer>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
|||||||
284
rl/resource-loader-2026-08-20.json
Normal file
284
rl/resource-loader-2026-08-20.json
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"vaults": [
|
||||||
|
{
|
||||||
|
"id": "vault-work",
|
||||||
|
"name": "Work",
|
||||||
|
"colour": "#526d82",
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"id": "d277fa97-1f4a-4386-aba3-d1b9990020f3",
|
||||||
|
"name": "Books",
|
||||||
|
"createdAt": "2026-08-19T14:26:23.797Z",
|
||||||
|
"updatedAt": "2026-08-19T14:26:23.797Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d35a57a9-8090-4f06-beff-9d3013dada38",
|
||||||
|
"name": "Articles",
|
||||||
|
"createdAt": "2026-08-19T14:38:08.613Z",
|
||||||
|
"updatedAt": "2026-08-19T14:38:08.613Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "31e35b5e-3ba4-4f70-8075-5ef95ee6d1c1",
|
||||||
|
"title": "Quick Start Kubernetes",
|
||||||
|
"creators": [
|
||||||
|
"Nigel Poulton"
|
||||||
|
],
|
||||||
|
"type": "book",
|
||||||
|
"format": "pdf",
|
||||||
|
"status": "active",
|
||||||
|
"folderId": "d277fa97-1f4a-4386-aba3-d1b9990020f3",
|
||||||
|
"pageCount": 102,
|
||||||
|
"durationMinutes": null,
|
||||||
|
"url": "",
|
||||||
|
"publisher": "XeTeX 0.99998",
|
||||||
|
"published": "20230714",
|
||||||
|
"tags": [],
|
||||||
|
"description": "",
|
||||||
|
"notes": "",
|
||||||
|
"attachmentId": "4f55a614-1f1d-467c-a6ec-6a8e4710d6a7",
|
||||||
|
"thumbnailId": "22a8f1f9-3284-4b11-b2ee-76a0439deb4d",
|
||||||
|
"sessions": [
|
||||||
|
{
|
||||||
|
"id": "92b5f660-9f41-4c11-8662-64307062ac9e",
|
||||||
|
"date": "2026-08-19",
|
||||||
|
"start": 0,
|
||||||
|
"end": 25,
|
||||||
|
"unit": "pages",
|
||||||
|
"note": "",
|
||||||
|
"createdAt": "2026-08-19T14:26:05.106Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"createdAt": "2026-08-19T14:25:50.144Z",
|
||||||
|
"updatedAt": "2026-08-19T14:26:31.349Z",
|
||||||
|
"attachmentState": "reattach-required-after-import"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6c3206c8-9421-40e8-8157-783bcef4c2d8",
|
||||||
|
"title": "You Won’t Make It. It Makes You.",
|
||||||
|
"creators": [],
|
||||||
|
"type": "article",
|
||||||
|
"format": "web",
|
||||||
|
"status": "backlog",
|
||||||
|
"folderId": "d35a57a9-8090-4f06-beff-9d3013dada38",
|
||||||
|
"pageCount": null,
|
||||||
|
"durationMinutes": null,
|
||||||
|
"url": "https://medium.com/westenberg/you-wont-make-it-it-makes-you-5580fb0ae28c?sharedUserId=nworb",
|
||||||
|
"publisher": "Medium",
|
||||||
|
"published": "",
|
||||||
|
"tags": [],
|
||||||
|
"description": "",
|
||||||
|
"notes": "",
|
||||||
|
"attachmentId": null,
|
||||||
|
"thumbnailId": null,
|
||||||
|
"sessions": [],
|
||||||
|
"createdAt": "2026-08-19T14:29:00.526Z",
|
||||||
|
"updatedAt": "2026-08-19T14:38:13.471Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "818c243e-4a38-4392-88a5-5622b8d45730",
|
||||||
|
"title": "Agile Testing A Practical Guide for Testers and Agile Teams by Lisa Crispin and Janet Gregory",
|
||||||
|
"creators": [
|
||||||
|
"Lisa Crispin",
|
||||||
|
"Janet Gregory"
|
||||||
|
],
|
||||||
|
"type": "book",
|
||||||
|
"format": "physical",
|
||||||
|
"status": "shelf",
|
||||||
|
"folderId": "d277fa97-1f4a-4386-aba3-d1b9990020f3",
|
||||||
|
"pageCount": 573,
|
||||||
|
"durationMinutes": null,
|
||||||
|
"url": "",
|
||||||
|
"publisher": "iText 2.1.4 (by lowagie.com)",
|
||||||
|
"published": "20110315",
|
||||||
|
"tags": [],
|
||||||
|
"description": "",
|
||||||
|
"notes": "",
|
||||||
|
"attachmentId": "2b489f35-1d4c-4086-996d-10ae9d7892d9",
|
||||||
|
"thumbnailId": "93d3c67b-7c39-476b-b3c6-144aac8be448",
|
||||||
|
"sessions": [],
|
||||||
|
"createdAt": "2026-08-20T11:12:31.407Z",
|
||||||
|
"updatedAt": "2026-08-20T11:12:31.407Z",
|
||||||
|
"attachmentState": "reattach-required-after-import"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "801d464e-e6d2-4a97-b9c5-904de80336ff",
|
||||||
|
"title": "Clean Code A Handbook Of Agile Software Craftsmanship Robert C. Martin",
|
||||||
|
"creators": [],
|
||||||
|
"type": "book",
|
||||||
|
"format": "physical",
|
||||||
|
"status": "shelf",
|
||||||
|
"folderId": "d277fa97-1f4a-4386-aba3-d1b9990020f3",
|
||||||
|
"pageCount": 462,
|
||||||
|
"durationMinutes": null,
|
||||||
|
"url": "",
|
||||||
|
"publisher": "",
|
||||||
|
"published": "20080916",
|
||||||
|
"tags": [],
|
||||||
|
"description": "",
|
||||||
|
"notes": "",
|
||||||
|
"attachmentId": "3302329d-ca36-4028-ab36-ca89c84dbe9f",
|
||||||
|
"thumbnailId": "e727a0e3-0d0e-4e17-a394-d77eee7606ca",
|
||||||
|
"sessions": [],
|
||||||
|
"createdAt": "2026-08-20T11:12:51.287Z",
|
||||||
|
"updatedAt": "2026-08-20T11:12:51.287Z",
|
||||||
|
"attachmentState": "reattach-required-after-import"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "968f0e4e-aec2-4ed1-8aac-372b609ae594",
|
||||||
|
"title": "Code Complete, Second Edition eBook",
|
||||||
|
"creators": [
|
||||||
|
"Steve McConnell"
|
||||||
|
],
|
||||||
|
"type": "book",
|
||||||
|
"format": "physical",
|
||||||
|
"status": "shelf",
|
||||||
|
"folderId": "d277fa97-1f4a-4386-aba3-d1b9990020f3",
|
||||||
|
"pageCount": 952,
|
||||||
|
"durationMinutes": null,
|
||||||
|
"url": "",
|
||||||
|
"publisher": "Acrobat Distiller 5.0 (Windows)",
|
||||||
|
"published": "20130506",
|
||||||
|
"tags": [
|
||||||
|
"V413HAV"
|
||||||
|
],
|
||||||
|
"description": "",
|
||||||
|
"notes": "",
|
||||||
|
"attachmentId": "f105daf1-fb4e-4661-8a26-61f7a82d2f06",
|
||||||
|
"thumbnailId": "0a9e1ffa-55e4-41fb-b6c3-ef34320ec123",
|
||||||
|
"sessions": [],
|
||||||
|
"createdAt": "2026-08-20T11:13:05.046Z",
|
||||||
|
"updatedAt": "2026-08-20T11:13:05.046Z",
|
||||||
|
"attachmentState": "reattach-required-after-import"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1afa8601-5db0-4e37-932b-3c00ccf13614",
|
||||||
|
"title": "Head First: Design Patterns",
|
||||||
|
"creators": [
|
||||||
|
"Freeman",
|
||||||
|
"Eric"
|
||||||
|
],
|
||||||
|
"type": "book",
|
||||||
|
"format": "physical",
|
||||||
|
"status": "shelf",
|
||||||
|
"folderId": "d277fa97-1f4a-4386-aba3-d1b9990020f3",
|
||||||
|
"pageCount": 867,
|
||||||
|
"durationMinutes": null,
|
||||||
|
"url": "",
|
||||||
|
"publisher": "calibre 3.15.0 [https://calibre-ebook.com]",
|
||||||
|
"published": "20180121",
|
||||||
|
"tags": [
|
||||||
|
"COMPUTERS / Software Development & Engineering / General"
|
||||||
|
],
|
||||||
|
"description": "<p>This edition of Head First Design Patterns—now updated for Java 8—shows you the tried-and-true, road-tested patterns used by developers to create functional, elegant, reusable, and flexible software. By the time you finish this book, you’ll be able to take advantage of the best design practices and experiences of those who have fought the beast of software design and triumphed.</p>",
|
||||||
|
"notes": "",
|
||||||
|
"attachmentId": "414d1ac4-6934-4191-83de-4164ef4f4e1c",
|
||||||
|
"thumbnailId": "0df8ad55-2811-4342-a411-471a5f2e3e1e",
|
||||||
|
"sessions": [],
|
||||||
|
"createdAt": "2026-08-20T11:13:14.758Z",
|
||||||
|
"updatedAt": "2026-08-20T11:13:14.758Z",
|
||||||
|
"attachmentState": "reattach-required-after-import"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9c9f2902-583b-4d36-96c8-13d0911c5b8e",
|
||||||
|
"title": "Grokking Algorithms",
|
||||||
|
"creators": [
|
||||||
|
"Aditya Y. Bhargava"
|
||||||
|
],
|
||||||
|
"type": "book",
|
||||||
|
"format": "pdf",
|
||||||
|
"status": "shelf",
|
||||||
|
"folderId": "d277fa97-1f4a-4386-aba3-d1b9990020f3",
|
||||||
|
"pageCount": 258,
|
||||||
|
"durationMinutes": null,
|
||||||
|
"url": "",
|
||||||
|
"publisher": "Adobe PDF Library 11.0; modified using iText 2.1.7 by 1T3XT",
|
||||||
|
"published": "20160501",
|
||||||
|
"tags": [],
|
||||||
|
"description": "",
|
||||||
|
"notes": "",
|
||||||
|
"attachmentId": "b3ae5709-316d-4f02-a810-7515dfa9d073",
|
||||||
|
"thumbnailId": "2fa590f1-3198-4c6f-bef6-ac90df1d51a3",
|
||||||
|
"sessions": [],
|
||||||
|
"createdAt": "2026-08-20T11:13:23.182Z",
|
||||||
|
"updatedAt": "2026-08-20T11:13:23.182Z",
|
||||||
|
"attachmentState": "reattach-required-after-import"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cc4084e8-ee45-485c-b71c-903e896c55db",
|
||||||
|
"title": "The Clean Coder: A Code of Conduct For Professional Programmers",
|
||||||
|
"creators": [
|
||||||
|
"Robert C. Martin"
|
||||||
|
],
|
||||||
|
"type": "book",
|
||||||
|
"format": "physical",
|
||||||
|
"status": "shelf",
|
||||||
|
"folderId": "d277fa97-1f4a-4386-aba3-d1b9990020f3",
|
||||||
|
"pageCount": 244,
|
||||||
|
"durationMinutes": null,
|
||||||
|
"url": "",
|
||||||
|
"publisher": "3-Heights(TM) PDF Security Shell 4.8.25.2 (http://www.pdf-tools.com) / pdcat (www.pdf-tools.com)",
|
||||||
|
"published": "20110428",
|
||||||
|
"tags": [],
|
||||||
|
"description": "",
|
||||||
|
"notes": "",
|
||||||
|
"attachmentId": "18670834-863f-47fc-ab4b-c2f3f963e2bf",
|
||||||
|
"thumbnailId": "90d29780-d110-4525-8efe-cd333b836fc9",
|
||||||
|
"sessions": [],
|
||||||
|
"createdAt": "2026-08-20T11:13:32.620Z",
|
||||||
|
"updatedAt": "2026-08-20T11:13:32.620Z",
|
||||||
|
"attachmentState": "reattach-required-after-import"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dd1265d0-00a7-44ad-8f35-9a0fb14a657c",
|
||||||
|
"title": "The Toyota Way - Jeffrey Liker",
|
||||||
|
"creators": [
|
||||||
|
"Jeffrey Liker"
|
||||||
|
],
|
||||||
|
"type": "book",
|
||||||
|
"format": "pdf",
|
||||||
|
"status": "completed",
|
||||||
|
"folderId": "d277fa97-1f4a-4386-aba3-d1b9990020f3",
|
||||||
|
"pageCount": 151,
|
||||||
|
"durationMinutes": null,
|
||||||
|
"url": "",
|
||||||
|
"publisher": "calibre 3.22.1 [https://calibre-ebook.com]",
|
||||||
|
"published": "20190513",
|
||||||
|
"tags": [],
|
||||||
|
"description": "",
|
||||||
|
"notes": "",
|
||||||
|
"attachmentId": "0ad4f3e8-303b-4872-ae61-a3c323f510a6",
|
||||||
|
"thumbnailId": "6f17fcf2-0ce5-41be-b25e-9d3192b6146c",
|
||||||
|
"sessions": [
|
||||||
|
{
|
||||||
|
"id": "1112c865-2990-4f8d-858a-b3fa18f05001",
|
||||||
|
"date": "2026-08-20",
|
||||||
|
"start": 0,
|
||||||
|
"end": 151,
|
||||||
|
"unit": "pages",
|
||||||
|
"note": "",
|
||||||
|
"createdAt": "2026-08-20T11:14:03.816Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"createdAt": "2026-08-20T11:13:46.554Z",
|
||||||
|
"updatedAt": "2026-08-20T11:14:08.850Z",
|
||||||
|
"attachmentState": "reattach-required-after-import"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"createdAt": "2026-08-19T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-08-20T11:14:08.850Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "vault-personal-study",
|
||||||
|
"name": "Personal Study",
|
||||||
|
"colour": "#9b6b43",
|
||||||
|
"folders": [],
|
||||||
|
"resources": [],
|
||||||
|
"createdAt": "2026-08-19T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-08-19T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"exportedAt": "2026-08-20T11:15:11.246Z"
|
||||||
|
}
|
||||||
104
sitemap.org
104
sitemap.org
@@ -82,13 +82,13 @@ flowchart TD
|
|||||||
n30 --> n38
|
n30 --> n38
|
||||||
n39["Tag: update"]
|
n39["Tag: update"]
|
||||||
n30 --> n39
|
n30 --> n39
|
||||||
n40["Tag: insights"]
|
n40["Tag: education"]
|
||||||
n30 --> n40
|
n30 --> n40
|
||||||
n41["Tag: emacs"]
|
n41["Tag: insights"]
|
||||||
n30 --> n41
|
n30 --> n41
|
||||||
n42["Tag: education"]
|
n42["Tag: reading"]
|
||||||
n30 --> n42
|
n30 --> n42
|
||||||
n43["Tag: reading"]
|
n43["Tag: emacs"]
|
||||||
n30 --> n43
|
n30 --> n43
|
||||||
n44["Tag: maths"]
|
n44["Tag: maths"]
|
||||||
n30 --> n44
|
n30 --> n44
|
||||||
@@ -160,36 +160,36 @@ flowchart TD
|
|||||||
n69 --> n77
|
n69 --> n77
|
||||||
n78["Marginalia Machine"]
|
n78["Marginalia Machine"]
|
||||||
n69 --> n78
|
n69 --> n78
|
||||||
n79{{"rl"}}
|
n79{{"home"}}
|
||||||
root --> n79
|
root --> n79
|
||||||
n80["Resource Loader"]
|
n80["Countdown"]
|
||||||
n79 --> n80
|
n79 --> n80
|
||||||
n81{{"home"}}
|
n81["Backlog"]
|
||||||
root --> n81
|
n79 --> n81
|
||||||
n82["Countdown"]
|
n82["Competency Status Board"]
|
||||||
n81 --> n82
|
n79 --> n82
|
||||||
n83["Backlog"]
|
n83["hidden-soul"]
|
||||||
n81 --> n83
|
n79 --> n83
|
||||||
n84["Competency Status Board"]
|
n84["Wird Tracker"]
|
||||||
n81 --> n84
|
n79 --> n84
|
||||||
n85["hidden-soul"]
|
n85["Contact"]
|
||||||
n81 --> n85
|
n79 --> n85
|
||||||
n86["Wird Tracker"]
|
n86["Service"]
|
||||||
n81 --> n86
|
n79 --> n86
|
||||||
n87["Contact"]
|
n87["Notes"]
|
||||||
n81 --> n87
|
n79 --> n87
|
||||||
n88["Service"]
|
n88["Categories"]
|
||||||
n81 --> n88
|
n79 --> n88
|
||||||
n89["Notes"]
|
n89{{"guide"}}
|
||||||
n81 --> n89
|
n79 --> n89
|
||||||
n90["Categories"]
|
n90["Setup"]
|
||||||
n81 --> n90
|
n89 --> n90
|
||||||
n91{{"guide"}}
|
n91["Wird Tracker — Technical Guide"]
|
||||||
n81 --> n91
|
n89 --> n91
|
||||||
n92["Setup"]
|
n92{{"rl"}}
|
||||||
n91 --> n92
|
root --> n92
|
||||||
n93["Wird Tracker — Technical Guide"]
|
n93["Resource Loader"]
|
||||||
n91 --> n93
|
n92 --> n93
|
||||||
click n1 "index.html" "Home"
|
click n1 "index.html" "Home"
|
||||||
click n2 "recently-updated.html" "Recently Updated"
|
click n2 "recently-updated.html" "Recently Updated"
|
||||||
click n4 "blogs/blogs-intro.html" "Blogs Introduction"
|
click n4 "blogs/blogs-intro.html" "Blogs Introduction"
|
||||||
@@ -213,10 +213,10 @@ flowchart TD
|
|||||||
click n37 "tags/website.html" "Tag: website"
|
click n37 "tags/website.html" "Tag: website"
|
||||||
click n38 "tags/life.html" "Tag: life"
|
click n38 "tags/life.html" "Tag: life"
|
||||||
click n39 "tags/update.html" "Tag: update"
|
click n39 "tags/update.html" "Tag: update"
|
||||||
click n40 "tags/insights.html" "Tag: insights"
|
click n40 "tags/education.html" "Tag: education"
|
||||||
click n41 "tags/emacs.html" "Tag: emacs"
|
click n41 "tags/insights.html" "Tag: insights"
|
||||||
click n42 "tags/education.html" "Tag: education"
|
click n42 "tags/reading.html" "Tag: reading"
|
||||||
click n43 "tags/reading.html" "Tag: reading"
|
click n43 "tags/emacs.html" "Tag: emacs"
|
||||||
click n44 "tags/maths.html" "Tag: maths"
|
click n44 "tags/maths.html" "Tag: maths"
|
||||||
click n46 "posts/posts-intro.html" "Posts Introduction"
|
click n46 "posts/posts-intro.html" "Posts Introduction"
|
||||||
click n47 "posts/posts-list.html" "Posts List"
|
click n47 "posts/posts-list.html" "Posts List"
|
||||||
@@ -249,18 +249,18 @@ flowchart TD
|
|||||||
click n76 "play/terminal.html" "Archive Terminal"
|
click n76 "play/terminal.html" "Archive Terminal"
|
||||||
click n77 "play/study.html" "Study Lamp"
|
click n77 "play/study.html" "Study Lamp"
|
||||||
click n78 "play/poem.html" "Marginalia Machine"
|
click n78 "play/poem.html" "Marginalia Machine"
|
||||||
click n80 "rl/index.html" "Resource Loader"
|
click n80 "home/countdown.html" "Countdown"
|
||||||
click n82 "home/countdown.html" "Countdown"
|
click n81 "home/backlog.html" "Backlog"
|
||||||
click n83 "home/backlog.html" "Backlog"
|
click n82 "home/status.html" "Competency Status Board"
|
||||||
click n84 "home/status.html" "Competency Status Board"
|
click n83 "home/hidden-soul.html" "hidden-soul"
|
||||||
click n85 "home/hidden-soul.html" "hidden-soul"
|
click n84 "home/wird-tracker.html" "Wird Tracker"
|
||||||
click n86 "home/wird-tracker.html" "Wird Tracker"
|
click n85 "home/contact.html" "Contact"
|
||||||
click n87 "home/contact.html" "Contact"
|
click n86 "home/services.html" "Service"
|
||||||
click n88 "home/services.html" "Service"
|
click n87 "home/notes.html" "Notes"
|
||||||
click n89 "home/notes.html" "Notes"
|
click n88 "home/categories.html" "Categories"
|
||||||
click n90 "home/categories.html" "Categories"
|
click n90 "home/guide/setup.html" "Setup"
|
||||||
click n92 "home/guide/setup.html" "Setup"
|
click n91 "home/guide/wird-tracker-guide.html" "Wird Tracker — Technical Guide"
|
||||||
click n93 "home/guide/wird-tracker-guide.html" "Wird Tracker — Technical Guide"
|
click n93 "rl/index.html" "Resource Loader"
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
* Pages
|
* Pages
|
||||||
@@ -303,10 +303,10 @@ flowchart TD
|
|||||||
- [[file:tags/website.org][Tag: website]]
|
- [[file:tags/website.org][Tag: website]]
|
||||||
- [[file:tags/life.org][Tag: life]]
|
- [[file:tags/life.org][Tag: life]]
|
||||||
- [[file:tags/update.org][Tag: update]]
|
- [[file:tags/update.org][Tag: update]]
|
||||||
- [[file:tags/insights.org][Tag: insights]]
|
|
||||||
- [[file:tags/emacs.org][Tag: emacs]]
|
|
||||||
- [[file:tags/education.org][Tag: education]]
|
- [[file:tags/education.org][Tag: education]]
|
||||||
|
- [[file:tags/insights.org][Tag: insights]]
|
||||||
- [[file:tags/reading.org][Tag: reading]]
|
- [[file:tags/reading.org][Tag: reading]]
|
||||||
|
- [[file:tags/emacs.org][Tag: emacs]]
|
||||||
- [[file:tags/maths.org][Tag: maths]]
|
- [[file:tags/maths.org][Tag: maths]]
|
||||||
- posts
|
- posts
|
||||||
- [[file:posts/posts-intro.org][Posts Introduction]]
|
- [[file:posts/posts-intro.org][Posts Introduction]]
|
||||||
@@ -342,8 +342,6 @@ flowchart TD
|
|||||||
- [[file:play/terminal.org][Archive Terminal]]
|
- [[file:play/terminal.org][Archive Terminal]]
|
||||||
- [[file:play/study.org][Study Lamp]]
|
- [[file:play/study.org][Study Lamp]]
|
||||||
- [[file:play/poem.org][Marginalia Machine]]
|
- [[file:play/poem.org][Marginalia Machine]]
|
||||||
- rl
|
|
||||||
- [[file:rl/index.org][Resource Loader]]
|
|
||||||
- home
|
- home
|
||||||
- [[file:home/countdown.org][Countdown]]
|
- [[file:home/countdown.org][Countdown]]
|
||||||
- [[file:home/backlog.org][Backlog]]
|
- [[file:home/backlog.org][Backlog]]
|
||||||
@@ -357,3 +355,5 @@ flowchart TD
|
|||||||
- guide
|
- guide
|
||||||
- [[file:home/guide/setup.org][Setup]]
|
- [[file:home/guide/setup.org][Setup]]
|
||||||
- [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]]
|
- [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]]
|
||||||
|
- rl
|
||||||
|
- [[file:rl/index.org][Resource Loader]]
|
||||||
@@ -43,6 +43,23 @@ test("merge replaces matching ids and keeps unrelated records", () => {
|
|||||||
assert.ok(merged.vaults[0].resources.some((item) => item.id === "resource-2"));
|
assert.ok(merged.vaults[0].resources.some((item) => item.id === "resource-2"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("server sync keeps newer fields and sessions from both devices", () => {
|
||||||
|
const remote = library([resource({
|
||||||
|
title: "Remote title",
|
||||||
|
updatedAt: "2026-01-02T00:00:00.000Z",
|
||||||
|
sessions: [{ id: "remote-session", end: 20, createdAt: "2026-01-02T00:00:00.000Z" }],
|
||||||
|
})]);
|
||||||
|
const local = library([resource({
|
||||||
|
title: "Newer local title",
|
||||||
|
updatedAt: "2026-01-03T00:00:00.000Z",
|
||||||
|
sessions: [{ id: "local-session", end: 30, createdAt: "2026-01-03T00:00:00.000Z" }],
|
||||||
|
})]);
|
||||||
|
const merged = Model.mergeForSync(remote, local);
|
||||||
|
const synced = merged.vaults[0].resources[0];
|
||||||
|
assert.equal(synced.title, "Newer local title");
|
||||||
|
assert.deepEqual(synced.sessions.map((session) => session.id).sort(), ["local-session", "remote-session"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("vault and folder operations preserve records safely", () => {
|
test("vault and folder operations preserve records safely", () => {
|
||||||
let data = Model.addVault(library(), "Second", "#abcdef");
|
let data = Model.addVault(library(), "Second", "#abcdef");
|
||||||
assert.equal(data.vaults.length, 2);
|
assert.equal(data.vaults.length, 2);
|
||||||
|
|||||||
Reference in New Issue
Block a user