diff --git a/authoring_server.py b/authoring_server.py
index 50d8932..c1e2191 100755
--- a/authoring_server.py
+++ b/authoring_server.py
@@ -399,20 +399,30 @@ def list_pages() -> list[dict[str, Any]]:
pages = []
org_paths = []
if ROOT.exists():
- for path in ROOT.rglob("*.org"):
- rel_parts = path.relative_to(ROOT).parts
- if any(part in EXCLUDED_CONTENT_DIR_NAMES for part in rel_parts):
- continue
- org_paths.append(path)
- md_paths = list(LIMA_DIR.rglob("*.md")) if LIMA_DIR.exists() else []
+ try:
+ for path in ROOT.rglob("*.org"):
+ try:
+ rel_parts = path.relative_to(ROOT).parts
+ except ValueError:
+ continue
+ if any(part in EXCLUDED_CONTENT_DIR_NAMES for part in rel_parts):
+ continue
+ org_paths.append(path)
+ except OSError:
+ org_paths = []
+ try:
+ md_paths = list(LIMA_DIR.rglob("*.md")) if LIMA_DIR.exists() else []
+ except OSError:
+ md_paths = []
for path in sorted(org_paths + md_paths):
if path.name in GENERATED_CONTENT_NAMES or "sync-conflict" in path.name:
continue
try:
page = read_page(path)
- except UnicodeDecodeError:
+ parsed = parse_org_datetime(page.date)
+ timestamp = parsed.timestamp() if parsed else path.stat().st_mtime
+ except (OSError, UnicodeDecodeError, ValueError):
continue
- parsed = parse_org_datetime(page.date)
pages.append(
{
"path": page.path,
@@ -422,7 +432,7 @@ def list_pages() -> list[dict[str, Any]]:
"tags": page.tags,
"date": page.date,
"format": page.format,
- "timestamp": parsed.timestamp() if parsed else path.stat().st_mtime,
+ "timestamp": timestamp,
}
)
return sorted(pages, key=lambda item: item["timestamp"], reverse=True)
@@ -850,7 +860,12 @@ APP_HTML = r"""
async function api(path, options = {}) {
const res = await fetch(path, { headers: { "Content-Type": "application/json" }, ...options });
- const data = await res.json();
+ let data = null;
+ try {
+ data = await res.clone().json();
+ } catch (_err) {
+ data = { error: await res.text().catch(() => "") };
+ }
if (!res.ok) throw new Error(data.error || "Request failed");
return data;
}
@@ -1014,13 +1029,23 @@ APP_HTML = r"""
state.pages = await api("/api/pages");
renderPages();
} catch (err) {
- pagesBox.innerHTML = `
${html(err.message)}
`;
+ if (state.pages.length) {
+ renderPages();
+ saveMessage.textContent = `Could not refresh file list: ${err.message}`;
+ } else {
+ pagesBox.innerHTML = `${html(err.message)}
`;
+ }
}
}
async function refreshBuild() {
- state.build = await api("/api/build");
- renderStatus();
+ try {
+ state.build = await api("/api/build");
+ renderStatus();
+ } catch (err) {
+ statusBox.className = "status fail";
+ statusBox.textContent = `Could not refresh build status: ${err.message}`;
+ }
await refreshPages();
}
@@ -1323,7 +1348,10 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.write(body)
return
if parsed.path == "/api/pages":
- self.send_json(list_pages())
+ try:
+ self.send_json(list_pages())
+ except Exception as exc:
+ self.send_json({"error": str(exc)}, HTTPStatus.INTERNAL_SERVER_ERROR)
return
if parsed.path == "/api/page":
query = parse_qs(parsed.query)
diff --git a/posts/posts-list.org b/posts/posts-list.org
index 15e74bd..025867a 100755
--- a/posts/posts-list.org
+++ b/posts/posts-list.org
@@ -4,7 +4,7 @@
See the categories: @@html:Categories@@
* Posts:
-- [[file:career/career-list.org][Career List]] @@html:07-05-2026 16:28@@
+- [[file:career/career-list.org][Career List]] @@html:07-05-2026 16:31@@
- [[file:posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]] @@html:14-04-2026 16:36@@
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:11-03-2026 17:18@@ @@html:learning@@ @@html:notes@@
- [[file:career/javascript.org][Understands the Javascript language]] @@html:11-03-2026 16:52@@ @@html:learning@@ @@html:notes@@
diff --git a/tests/test_authoring_server.py b/tests/test_authoring_server.py
index a42c719..7ec39e8 100755
--- a/tests/test_authoring_server.py
+++ b/tests/test_authoring_server.py
@@ -172,6 +172,23 @@ class PageRenderingTests(AuthoringServerTestCase):
self.assertEqual(set(paths), {"blogs/keep.org", "home/notes.org", "lima/index.md"})
+ def test_list_pages_skips_files_that_fail_to_read(self):
+ good = self.blogs / "keep.org"
+ bad = self.blogs / "bad.org"
+ good.write_text("#+TITLE: Keep\n#+DATE: <2026-05-07 Thu 12:00>\n", encoding="utf-8")
+ bad.write_text("#+TITLE: Bad\n", encoding="utf-8")
+ original_read_page = server.read_page
+
+ def read_page(path):
+ if path == bad:
+ raise OSError("file disappeared")
+ return original_read_page(path)
+
+ with mock.patch.object(server, "read_page", side_effect=read_page):
+ paths = [page["path"] for page in server.list_pages()]
+
+ self.assertEqual(paths, ["blogs/keep.org"])
+
def test_relative_asset_path_is_calculated_from_lima_page_directory(self):
self.assertEqual(
server.relative_asset_path("lima/family/update.md", "assets/images/hzone/pic.png"),