diff --git a/assets/images/hzone/2026-05-09-screen-shot-2025-11-10-at-18-02-10-fullpage.png b/assets/images/hzone/2026-05-09-screen-shot-2025-11-10-at-18-02-10-fullpage.png
new file mode 100644
index 0000000..3aba68e
Binary files /dev/null and b/assets/images/hzone/2026-05-09-screen-shot-2025-11-10-at-18-02-10-fullpage.png differ
diff --git a/assets/scripts/notes.js b/assets/scripts/notes.js
index 9e14baf..3496ef4 100755
--- a/assets/scripts/notes.js
+++ b/assets/scripts/notes.js
@@ -3,6 +3,8 @@
const form = document.getElementById("notes-form");
const authorInput = document.getElementById("note-author");
const contentInput = document.getElementById("note-content");
+ const authorFilter = document.getElementById("notes-author-filter");
+ let notesCache = [];
if (!wall) return;
@@ -34,6 +36,53 @@
return el;
}
+ function normaliseAuthor(author) {
+ return (author || "").trim();
+ }
+
+ function populateAuthorFilter(notes) {
+ if (!authorFilter) return;
+
+ const previousValue = authorFilter.value;
+ const authors = Array.from(
+ new Set(notes.map(note => normaliseAuthor(note.author_name)).filter(Boolean))
+ ).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
+
+ authorFilter.innerHTML = '';
+
+ authors.forEach(author => {
+ const option = document.createElement("option");
+ option.value = author;
+ option.textContent = author;
+ authorFilter.appendChild(option);
+ });
+
+ authorFilter.value = authors.includes(previousValue) ? previousValue : "";
+ }
+
+ function renderNotes() {
+ wall.innerHTML = "";
+
+ const selectedAuthor = authorFilter ? authorFilter.value : "";
+ const notes = selectedAuthor
+ ? notesCache.filter(note => normaliseAuthor(note.author_name) === selectedAuthor)
+ : notesCache;
+
+ if (notesCache.length === 0) {
+ wall.innerHTML = "
No notes yet.
";
+ return;
+ }
+
+ if (notes.length === 0) {
+ wall.innerHTML = "No notes for this author.
";
+ return;
+ }
+
+ notes.forEach(note => {
+ wall.appendChild(renderNote(note));
+ });
+ }
+
async function loadNotes() {
wall.innerHTML = "Loading notes…
";
@@ -42,16 +91,9 @@
if (!res.ok) throw new Error("Failed to fetch notes");
const notes = await res.json();
- wall.innerHTML = "";
-
- if (notes.length === 0) {
- wall.innerHTML = "No notes yet.
";
- return;
- }
-
- notes.forEach(note => {
- wall.appendChild(renderNote(note));
- });
+ notesCache = notes;
+ populateAuthorFilter(notesCache);
+ renderNotes();
} catch (err) {
console.error(err);
wall.innerHTML = "Could not load notes.
";
@@ -98,5 +140,9 @@
form.addEventListener("submit", submitNote);
}
+ if (authorFilter) {
+ authorFilter.addEventListener("change", renderNotes);
+ }
+
loadNotes();
})();
diff --git a/assets/styles/style.css b/assets/styles/style.css
index 3131913..37316f2 100755
--- a/assets/styles/style.css
+++ b/assets/styles/style.css
@@ -443,6 +443,38 @@ body.no-sidenotes {
NOTES BOARD
========================= */
+#notes-tools {
+ display: flex;
+ align-items: end;
+ justify-content: flex-end;
+ gap: 0.65rem;
+ margin: 1.25rem 0 0;
+}
+
+#notes-tools label {
+ color: var(--muted);
+ font-size: 0.75rem;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+#notes-author-filter {
+ min-width: min(220px, 100%);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--surface);
+ color: var(--fg);
+ font: inherit;
+ font-size: 0.9rem;
+ line-height: 1.2;
+ padding: 0.45rem 0.65rem;
+}
+
+#notes-author-filter:focus {
+ outline: 2px solid color-mix(in oklab, var(--accent) 45%, transparent);
+ outline-offset: 2px;
+}
+
#notes-wall {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
@@ -458,7 +490,9 @@ body.no-sidenotes {
.note {
position: relative;
padding: 1rem 1rem 0.9rem;
- background: --code-bg;
+ background: var(--surface);
+ color: var(--fg);
+ border: 1px solid var(--border);
border-radius: 8px;
box-shadow:
0 2px 6px rgba(0, 0, 0, 0.08),
@@ -466,7 +500,9 @@ body.no-sidenotes {
transform: rotate(var(--note-tilt, 0deg));
transition:
transform 0.15s ease,
- box-shadow 0.15s ease;
+ box-shadow 0.15s ease,
+ background-color 0.2s ease,
+ border-color 0.2s ease;
}
@@ -511,7 +547,7 @@ body.no-sidenotes {
align-items: baseline;
font-size: 0.7rem;
margin-bottom: 0.5rem;
- color: #555;
+ color: var(--muted);
}
.note-author {
@@ -533,7 +569,7 @@ body.no-sidenotes {
font-size: 0.95rem;
line-height: 1.45;
white-space: pre-wrap;
- color: --fg;
+ color: var(--fg);
}
/* =========================
@@ -545,6 +581,11 @@ body.no-sidenotes {
grid-template-columns: 1fr;
}
+ #notes-tools {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
.note {
transform: none;
}
@@ -567,7 +608,9 @@ body.no-sidenotes {
#notes-form {
width: 100%;
max-width: 420px;
- background: --bg;
+ background: var(--surface);
+ color: var(--fg);
+ border: 1px solid var(--border);
padding: 1.25rem 1.25rem 1.1rem;
border-radius: 10px;
box-shadow:
@@ -600,7 +643,7 @@ body.no-sidenotes {
font-size: 0.7rem;
letter-spacing: 0.08em;
text-transform: uppercase;
- color: #666;
+ color: var(--muted);
margin-bottom: 0.25rem;
}
@@ -613,12 +656,13 @@ body.no-sidenotes {
width: 100%;
border: none;
background: transparent;
+ color: var(--fg);
font-family: inherit;
font-size: 0.95rem;
line-height: 1.45;
padding: 0.3rem 0;
margin-bottom: 0.9rem;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid var(--border);
}
#notes-form textarea {
@@ -630,12 +674,12 @@ body.no-sidenotes {
#notes-form input:focus,
#notes-form textarea:focus {
outline: none;
- border-bottom-color: #999;
+ border-bottom-color: var(--accent);
}
/* placeholder */
#notes-form ::placeholder {
- color: #aaa;
+ color: var(--muted);
}
/* =========================
@@ -651,14 +695,15 @@ body.no-sidenotes {
text-transform: uppercase;
border-radius: 999px;
border: none;
- background: #222;
- color: #fff;
+ background: var(--fg);
+ color: var(--bg);
cursor: pointer;
float: right;
}
#notes-form button:hover {
- background: #000;
+ background: var(--accent);
+ color: var(--surface);
}
#notes-form button:disabled {
diff --git a/authoring_server.py b/authoring_server.py
index ad93576..bec1ea7 100755
--- a/authoring_server.py
+++ b/authoring_server.py
@@ -12,14 +12,15 @@ import subprocess
import sys
import threading
import time
-import cgi
from dataclasses import dataclass, field
from datetime import datetime
+from email.parser import BytesParser
+from email.policy import default as email_default_policy
from email.utils import formatdate
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
-from typing import Any
+from typing import Any, Callable
from urllib.parse import parse_qs, urlparse
@@ -272,12 +273,16 @@ class BuildQueue:
job.started_at = time.time()
job.message = "Publishing site and search index."
self._current = job
- ok, message, log = run_build_commands()
+ def append_log(chunk: str) -> None:
+ with self._lock:
+ job.log = (job.log + chunk)[-200000:]
+
+ ok, message, log = run_build_commands(append_log)
with self._lock:
job.finished_at = time.time()
job.ok = ok
job.message = message
- job.log = log
+ job.log = log[-200000:]
self._recent.append(job)
self._recent = self._recent[-20:]
self._current = None
@@ -643,7 +648,7 @@ def save_upload(filename: str, payload: bytes, page_path: str = "") -> dict[str,
absolute_url = f"https://zainezq.com/{rel}"
relative_url = relative_asset_path(page_path, rel)
is_markdown = page_path.endswith(".md")
- insert_text = f"" if is_markdown else f"[[{relative_url}]]"
+ insert_text = f"" if is_markdown else f"[[{relative_url}]]"
return {
"url": absolute_url,
"relativeUrl": relative_url,
@@ -654,7 +659,35 @@ def save_upload(filename: str, payload: bytes, page_path: str = "") -> dict[str,
}
-def run_build_commands() -> tuple[bool, str, str]:
+def parse_upload_form(content_type: str, body: bytes) -> tuple[str, bytes, str]:
+ if not content_type.lower().startswith("multipart/form-data"):
+ raise ValueError("Uploads must use multipart/form-data.")
+ message = BytesParser(policy=email_default_policy).parsebytes(
+ f"Content-Type: {content_type}\r\nMIME-Version: 1.0\r\n\r\n".encode("utf-8") + body
+ )
+ if not message.is_multipart():
+ raise ValueError("Upload form is not multipart.")
+
+ filename = ""
+ payload = b""
+ page_path = ""
+ for part in message.iter_parts():
+ name = part.get_param("name", header="content-disposition")
+ if name == "attachment":
+ filename = part.get_filename() or ""
+ payload = part.get_payload(decode=True) or b""
+ elif name == "pagePath":
+ raw_value = part.get_payload(decode=True) or b""
+ page_path = raw_value.decode(part.get_content_charset() or "utf-8", errors="replace")
+
+ if not filename:
+ raise ValueError("No attachment was uploaded.")
+ if not payload:
+ raise ValueError("Attachment is empty.")
+ return filename, payload, page_path
+
+
+def run_build_commands(log_callback: Callable[[str], None] | None = None) -> tuple[bool, str, str]:
venv_python = ROOT / ".venv" / "bin" / "python"
venv_pip = ROOT / ".venv" / "bin" / "pip"
commands = [["emacs", "-Q", "--script", "build-site.el"]]
@@ -667,20 +700,33 @@ def run_build_commands() -> tuple[bool, str, str]:
)
commands.append([str(venv_python), "search-index-json.py"])
combined = []
+
+ def append_log(text: str) -> None:
+ combined.append(text)
+ if log_callback:
+ log_callback(text)
+
ok = True
for command in commands:
- combined.append(f"$ {' '.join(command)}\n")
- proc = subprocess.run(
+ append_log(f"$ {' '.join(command)}\n")
+ env = os.environ.copy()
+ env["PYTHONUNBUFFERED"] = "1"
+ proc = subprocess.Popen(
command,
cwd=ROOT,
+ env=env,
text=True,
- stdout=subprocess.PIPE,
+ bufsize=1,
stderr=subprocess.STDOUT,
+ stdout=subprocess.PIPE,
)
- combined.append(proc.stdout)
- if proc.returncode != 0:
+ assert proc.stdout is not None
+ for line in proc.stdout:
+ append_log(line)
+ return_code = proc.wait()
+ if return_code != 0:
ok = False
- combined.append(f"\nCommand exited with {proc.returncode}.\n")
+ append_log(f"\nCommand exited with {return_code}.\n")
break
message = "Build complete. The site output and search index were regenerated." if ok else "Build failed. Check the log below."
return ok, message, "".join(combined)
@@ -1058,9 +1104,21 @@ APP_HTML = r"""
return String(value).replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char]));
}
+ function previewImageUrl(src) {
+ const value = String(src || "");
+ if (value.startsWith("https://author.zainezq.com/assets/")) {
+ return value.replace("https://author.zainezq.com/assets/", "https://zainezq.com/assets/");
+ }
+ const relativeAsset = value.match(/^(?:\.\.\/)+assets\/(.+)$/);
+ if (relativeAsset) return `https://zainezq.com/assets/${relativeAsset[1]}`;
+ if (value.startsWith("/assets/")) return `https://zainezq.com${value}`;
+ if (value.startsWith("assets/")) return `https://zainezq.com/${value}`;
+ return value;
+ }
+
function renderInlineMarkdown(value) {
let out = html(value);
- out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '
');
+ out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt, src) => `
`);
out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
out = out.replace(/\*\*([^*]+)\*\*/g, '$1');
out = out.replace(/\*([^*]+)\*/g, '$1');
@@ -1173,14 +1231,15 @@ APP_HTML = r"""
}
async function refreshBuild() {
+ const wasRunning = Boolean(state.build && state.build.running);
try {
state.build = await api("/api/build");
renderStatus();
+ if (wasRunning && !state.build.running) await refreshPages();
} catch (err) {
statusBox.className = "status fail";
statusBox.textContent = `Could not refresh build status: ${err.message}`;
}
- await refreshPages();
}
editor.title.addEventListener("input", () => {
@@ -1224,6 +1283,7 @@ APP_HTML = r"""
const saved = await api("/api/page", { method: "POST", body: JSON.stringify(payload) });
setForm(saved);
saveMessage.textContent = "Saved. Build queued.";
+ await refreshPages();
await refreshBuild();
} catch (err) {
saveMessage.textContent = err.message;
@@ -1416,6 +1476,7 @@ APP_HTML = r"""
pageLinkPicker.hidden = false;
pageLinkSelect.focus();
}
+ showPageLinkPicker();
function insertSelectedPageLink() {
const page = state.pages.find((item) => item.path === pageLinkSelect.value);
@@ -1509,24 +1570,12 @@ class Handler(BaseHTTPRequestHandler):
def do_POST(self) -> None:
if self.path == "/api/upload":
try:
- form = cgi.FieldStorage(
- fp=self.rfile,
- headers=self.headers,
- environ={
- "REQUEST_METHOD": "POST",
- "CONTENT_TYPE": self.headers.get("Content-Type", ""),
- },
+ length = int(self.headers.get("Content-Length", "0"))
+ filename, payload, page_path = parse_upload_form(
+ self.headers.get("Content-Type", ""),
+ self.rfile.read(length),
)
- field = form["attachment"] if "attachment" in form else None
- if field is None or not getattr(field, "filename", ""):
- raise ValueError("No attachment was uploaded.")
- payload = field.file.read()
- if not payload:
- raise ValueError("Attachment is empty.")
- page_path = ""
- if "pagePath" in form:
- page_path = str(form["pagePath"].value or "")
- self.send_json(save_upload(field.filename, payload, page_path))
+ self.send_json(save_upload(filename, payload, page_path))
except Exception as exc:
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
return
diff --git a/blogs/blogs-list.org b/blogs/blogs-list.org
old mode 100755
new mode 100644
diff --git a/home/backlog.org b/home/backlog.org
old mode 100755
new mode 100644
index b8d5498..eae2199
--- a/home/backlog.org
+++ b/home/backlog.org
@@ -1,16 +1,16 @@
#+TITLE: Backlog
#+OPTIONS: num:nil toc:nil
#+DATE: <2025-11-08 Sat 11:08>
-#+FILETAGS: :emacs:website:
-#+NO_SIDENOTES: t
+#+filetags: :emacs:website:
#+COMMENTS: t
#+SLUG: backlog
-
* TODO
- Give lima accounts to all the services
- Figure out a solution for the shared calendar
- change the font
+- Use zero trust cloudflare instead of nginx auth
+
* DOING
- Database permissions (roles, accounts and schemas)
- how pipelines work
diff --git a/home/notes.org b/home/notes.org
index 19f9b63..430821c 100755
--- a/home/notes.org
+++ b/home/notes.org
@@ -5,6 +5,13 @@
#+SLUG: notes
#+BEGIN_EXPORT html
+
+
+
+
+
diff --git a/lima/index.md b/lima/index.md
old mode 100755
new mode 100644
index ad3430f..ad47b50
--- a/lima/index.md
+++ b/lima/index.md
@@ -9,3 +9,8 @@ I made quite a few changes. It all started when I realised that not everyone kno
- I probably didn’t explain it as well, but you can create folders and files under the `lima-website` directory. The files must have a `.md` extension at the end (markdown).
- One other thing, a more personal one: what do you think of all this? am i doing too much? am i doing too little? am i overengineering things? am i forcing you to do something you dont wanna do? these little trinkets work for me, but i’m not sure if it would work for someone else, so at any point if you have reservations, let me know okay?
+
+
+
+
+This is a test
diff --git a/posts/career/career-list.org b/posts/career/career-list.org
index dc34d42..4e470f7 100755
--- a/posts/career/career-list.org
+++ b/posts/career/career-list.org
@@ -13,9 +13,9 @@ See the categories: @@html:Categories@@
- [[file:restful-api.org][Restful API]] @@html:15-02-2026 23:00@@ @@html:learning@@ @@html:notes@@
** January 2026
-- [[file:database-permissions.org][Database Permissions, Roles, and Accounts]] @@html:18-01-2026 23:00@@ @@html:learning@@ @@html:notes@@
- [[file:monitoring-and-logging.org][Monitoring and Logging]] @@html:18-01-2026 23:00@@ @@html:learning@@ @@html:notes@@
- [[file:pipelines.org][Pipelines and how they work (as well as CI/CD)]] @@html:18-01-2026 23:00@@ @@html:learning@@ @@html:notes@@
+- [[file:database-permissions.org][Database Permissions, Roles, and Accounts]] @@html:18-01-2026 23:00@@ @@html:learning@@ @@html:notes@@
** December 2025
- [[file:probation-objectives.org][Probation Objectives:]] @@html:08-12-2025 17:55@@ @@html:review@@ @@html:notes@@
@@ -23,13 +23,13 @@ See the categories: @@html:Categories@@
** November 2025
- [[file:airflow.org][Datamarts, Airflow and DAG's]] @@html:15-11-2025 18:37@@ @@html:learning@@ @@html:notes@@
- [[file:normalisation.org][Benefits of Normalisation]] @@html:10-11-2025 18:04@@ @@html:learning@@ @@html:notes@@
-- [[file:management-of-self.org][Management of self training]] @@html:06-11-2025 22:01@@ @@html:learning@@ @@html:notes@@
- [[file:wireframe-designs.org][Wireframe Designs]] @@html:06-11-2025 22:01@@ @@html:learning@@ @@html:notes@@
+- [[file:management-of-self.org][Management of self training]] @@html:06-11-2025 22:01@@ @@html:learning@@ @@html:notes@@
- [[file:requirements-features.org][Requirements, features, user stories, tasks, walking skeletons]] @@html:06-11-2025 22:01@@ @@html:learning@@ @@html:notes@@
- [[file:career-intro.org][Career Introduction]] @@html:06-11-2025 21:29@@ @@html:introduction@@
- [[file:invest-principles.org][Invest Principles]] @@html:06-11-2025 21:08@@ @@html:learning@@ @@html:notes@@
-- [[file:retrospectives.org][Retrospectives]] @@html:05-11-2025 20:46@@ @@html:learning@@ @@html:notes@@
- [[file:lean.org][Lean]] @@html:05-11-2025 20:46@@ @@html:learning@@ @@html:notes@@
+- [[file:retrospectives.org][Retrospectives]] @@html:05-11-2025 20:46@@ @@html:learning@@ @@html:notes@@
** October 2025
- [[file:owasp.org][OWASP Top Ten]] @@html:19-10-2025 13:21@@ @@html:learning@@ @@html:notes@@
diff --git a/posts/posts-list.org b/posts/posts-list.org
index 7deb1b4..25438f7 100644
--- a/posts/posts-list.org
+++ b/posts/posts-list.org
@@ -4,12 +4,7 @@
See the categories: @@html:Categories@@
* Posts:
-- [[file:career/career-list.org][Career List]] @@html:09-05-2026 01:18@@
-- [[file:posts-list.sync-conflict-20260509-011812-NE5VEIB.org][Posts List]] @@html:09-05-2026 01:18@@
-- [[file:posts-list.sync-conflict-20260509-011752-NE5VEIB.org][Posts List]] @@html:09-05-2026 01:17@@
-- [[file:posts-list.sync-conflict-20260509-011732-NE5VEIB.org][Posts List]] @@html:09-05-2026 01:17@@
-- [[file:posts-list.sync-conflict-20260509-005434-VT6366A.org][Posts List]] @@html:09-05-2026 00:54@@
-- [[file:posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]] @@html:14-04-2026 16:36@@
+- [[file:career/career-list.org][Career List]] @@html:09-05-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@@
- [[file:career/restful-api.org][Restful API]] @@html:15-02-2026 23:00@@ @@html:learning@@ @@html:notes@@
diff --git a/recently-updated.org b/recently-updated.org
index 68e0d47..0c0f9f9 100644
--- a/recently-updated.org
+++ b/recently-updated.org
@@ -2,18 +2,13 @@
#+OPTIONS: toc:nil num:nil
* Recently Updated (top 26 files)
-- [[file:posts/posts-list.sync-conflict-20260509-011812-NE5VEIB.org][Posts List]] @@html:2026-05-09 01:18@@
-- [[file:sitemap.sync-conflict-20260509-011812-NE5VEIB.org][Sitemap]] @@html:2026-05-09 01:17@@
-- [[file:recently-updated.sync-conflict-20260509-011802-NE5VEIB.org][Recently Updated]] @@html:2026-05-09 01:17@@
-- [[file:posts/posts-list.sync-conflict-20260509-011752-NE5VEIB.org][Posts List]] @@html:2026-05-09 01:17@@
-- [[file:sitemap.sync-conflict-20260509-011752-NE5VEIB.org][Sitemap]] @@html:2026-05-09 01:17@@
-- [[file:posts/posts-list.sync-conflict-20260509-011732-NE5VEIB.org][Posts List]] @@html:2026-05-09 01:17@@
-- [[file:sitemap.sync-conflict-20260509-011732-NE5VEIB.org][Sitemap]] @@html:2026-05-09 01:17@@
-- [[file:sitemap.sync-conflict-20260509-005434-VT6366A.org][Sitemap]] @@html:2026-05-09 00:54@@
-- [[file:posts/posts-list.sync-conflict-20260509-005434-VT6366A.org][Posts List]] @@html:2026-05-09 00:54@@
-- [[file:recently-updated.sync-conflict-20260509-005434-VT6366A.org][Recently Updated]] @@html:2026-05-09 00:54@@
-- [[file:home/login.org][Login]] @@html:2026-05-09 00:46@@
-- [[file:home/guide/setup.org][Setup]] @@html:2026-05-09 00:33@@
+- [[file:home/notes.org][Notes]] @@html:2026-05-09 15:05@@
+- [[file:home/wird-tracker.org][Wird Tracker]] @@html:2026-05-09 01:18@@
+- [[file:home/status.org][Competency Status Board]] @@html:2026-05-09 01:18@@
+- [[file:home/services.org][Service]] @@html:2026-05-09 01:18@@
+- [[file:home/contact.org][Contact]] @@html:2026-05-09 01:18@@
+- [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]] @@html:2026-05-09 01:18@@
+- [[file:home/guide/setup.org][Setup]] @@html:2026-05-09 01:18@@
- [[file:blogs/2026/05-may/ai-datacamp-08-05.org][AI Datacamp]] @@html:2026-05-08 12:49@@
- [[file:blogs/2026/05-may/using-codex-07-05-26.org][Using Codex]] @@html:2026-05-07 16:12@@
- [[file:blogs/2026/05-may/03-05-week-review.org][[03-05-2026] - Weekly Review]] @@html:2026-05-03 12:00@@
@@ -22,9 +17,14 @@
- [[file:blogs/2026/04-april/april-almost-over-28-04.org][April is almost over...]] @@html:2026-04-28 15:33@@
- [[file:blogs/2026/04-april/26-04-week-review.org][[26-04-2026] - Weekly Review]] @@html:2026-04-26 12:00@@
- [[file:blogs/2026/04-april/adding-more-wird-22-04-26.org][Adding another Wird]] @@html:2026-04-22 16:04@@
-- [[file:home/wird-tracker.org][Wird Tracker]] @@html:2026-04-22 11:58@@
- [[file:blogs/2026/04-april/sitting-outside-in-the-sun-21-04-26.org][Sitting in the sun]] @@html:2026-04-21 13:01@@
- [[file:blogs/2026/04-april/19-04-week-review.org][[19-04-2026] - Weekly Review]] @@html:2026-04-19 12:00@@
- [[file:blogs/2026/04-april/ending-the-week-17-04-26.org][End of week thoughts]] @@html:2026-04-17 16:17@@
- [[file:blogs/2026/04-april/16-04-26.org][Rambles]] @@html:2026-04-16 16:02@@
-- [[file:posts/posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]] @@html:2026-04-14 16:36@@
+- [[file:blogs/2026/04-april/comparison-14-04.org][Comparison is the thief of joy]] @@html:2026-04-14 09:55@@
+- [[file:blogs/2026/04-april/12-04-week-review.org][[12-04-2026] - Weekly Review]] @@html:2026-04-12 12:00@@
+- [[file:blogs/2026/04-april/starting-new-rotation-08-04-26.org][Starting new rotation]] @@html:2026-04-08 16:15@@
+- [[file:blogs/2026/04-april/action-plan-07-04-26.org][Action plan to change teams]] @@html:2026-04-07 10:04@@
+- [[file:blogs/2026/04-april/weekly-target-06-04-26.org][[06-04-2026] - Weekly target]] @@html:2026-04-06 22:53@@
+- [[file:blogs/2026/04-april/making-notes-through-org-noter-06-05.org][Making Notes through Org Noter]] @@html:2026-04-06 14:30@@
+- [[file:blogs/2026/04-april/05-04-week-review.org][[05-04-2026] - Weekly Review]] @@html:2026-04-05 12:00@@
diff --git a/requirements.txt b/requirements.txt
index 6f83e94..b8ba271 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
beautifulsoup4
lxml
+legacy-cgi
diff --git a/sitemap.org b/sitemap.org
index 2c74332..e1aef81 100644
--- a/sitemap.org
+++ b/sitemap.org
@@ -1,25 +1,19 @@
#+TITLE: Sitemap
- [[file:index.org][Home]]
-- [[file:recently-updated.sync-conflict-20260509-005434-VT6366A.org][Recently Updated]]
-- [[file:sitemap.sync-conflict-20260509-005434-VT6366A.org][Sitemap]]
-- [[file:sitemap.sync-conflict-20260509-011732-NE5VEIB.org][Sitemap]]
-- [[file:sitemap.sync-conflict-20260509-011752-NE5VEIB.org][Sitemap]]
-- [[file:recently-updated.sync-conflict-20260509-011802-NE5VEIB.org][Recently Updated]]
-- [[file:sitemap.sync-conflict-20260509-011812-NE5VEIB.org][Sitemap]]
- [[file:wip.org][Work in progress]]
- [[file:recently-updated.org][Recently Updated]]
- tags
- - [[file:tags/review.sync-conflict-20260328-203248-VT6366A.org][Tag: review]]
- [[file:tags/life.sync-conflict-20260417-233423-VT6366A.org][Tag: life]]
+ - [[file:tags/review.sync-conflict-20260328-203248-VT6366A.org][Tag: review]]
- [[file:tags/introduction.org][Tag: introduction]]
- [[file:tags/learning.org][Tag: learning]]
- [[file:tags/notes.org][Tag: notes]]
- [[file:tags/review.org][Tag: review]]
- [[file:tags/website.org][Tag: website]]
- - [[file:tags/update.org][Tag: update]]
- [[file:tags/life.org][Tag: life]]
- [[file:tags/education.org][Tag: education]]
+ - [[file:tags/update.org][Tag: update]]
- [[file:tags/insights.org][Tag: insights]]
- [[file:tags/emacs.org][Tag: emacs]]
- [[file:tags/maths.org][Tag: maths]]
@@ -32,28 +26,22 @@
- [[file:lima/lima-list.org][Lima]]
- home
- [[file:home/countdown.org][Countdown]]
- - [[file:home/contact.org][Contact]]
- [[file:home/backlog.org][Backlog]]
- - [[file:home/notes.org][Notes]]
- - [[file:home/services.org][Service]]
- [[file:home/status.org][Competency Status Board]]
- [[file:home/wird-tracker.org][Wird Tracker]]
- - [[file:home/login.org][Login]]
+ - [[file:home/contact.org][Contact]]
+ - [[file:home/services.org][Service]]
+ - [[file:home/notes.org][Notes]]
- [[file:home/categories.org][Categories]]
- guide
- - [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]]
- [[file:home/guide/setup.org][Setup]]
+ - [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]]
- blogs
- [[file:blogs/blogs-intro.org][Blogs Introduction]]
- [[file:blogs/publish-pages.org][How to publish pages using Org Publish]]
- [[file:blogs/blogs-list.org][Blogs List]]
- posts
- [[file:posts/posts-intro.org][Posts Introduction]]
- - [[file:posts/posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]]
- - [[file:posts/posts-list.sync-conflict-20260509-005434-VT6366A.org][Posts List]]
- - [[file:posts/posts-list.sync-conflict-20260509-011732-NE5VEIB.org][Posts List]]
- - [[file:posts/posts-list.sync-conflict-20260509-011752-NE5VEIB.org][Posts List]]
- - [[file:posts/posts-list.sync-conflict-20260509-011812-NE5VEIB.org][Posts List]]
- [[file:posts/posts-list.org][Posts List]]
- career
- [[file:posts/career/solid-principles.org][SOLID Principles]]
diff --git a/tags/learning.org b/tags/learning.org
index ef43c84..fa15dfe 100755
--- a/tags/learning.org
+++ b/tags/learning.org
@@ -5,16 +5,16 @@
- [[file:../posts/career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]]
- [[file:../posts/career/javascript.org][Understands the Javascript language]]
- [[file:../posts/career/restful-api.org][Restful API]]
-- [[file:../posts/career/database-permissions.org][Database Permissions, Roles, and Accounts]]
- [[file:../posts/career/monitoring-and-logging.org][Monitoring and Logging]]
- [[file:../posts/career/pipelines.org][Pipelines and how they work (as well as CI/CD)]]
+- [[file:../posts/career/database-permissions.org][Database Permissions, Roles, and Accounts]]
- [[file:../posts/career/airflow.org][Datamarts, Airflow and DAG's]]
- [[file:../posts/career/normalisation.org][Benefits of Normalisation]]
-- [[file:../posts/career/management-of-self.org][Management of self training]]
- [[file:../posts/career/wireframe-designs.org][Wireframe Designs]]
+- [[file:../posts/career/management-of-self.org][Management of self training]]
- [[file:../posts/career/requirements-features.org][Requirements, features, user stories, tasks, walking skeletons]]
- [[file:../posts/career/invest-principles.org][Invest Principles]]
-- [[file:../posts/career/retrospectives.org][Retrospectives]]
- [[file:../posts/career/lean.org][Lean]]
+- [[file:../posts/career/retrospectives.org][Retrospectives]]
- [[file:../posts/career/owasp.org][OWASP Top Ten]]
- [[file:../posts/career/solid-principles.org][SOLID Principles]]
diff --git a/tags/notes.org b/tags/notes.org
index 6d9fc80..3e2f335 100755
--- a/tags/notes.org
+++ b/tags/notes.org
@@ -5,17 +5,17 @@
- [[file:../posts/career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]]
- [[file:../posts/career/javascript.org][Understands the Javascript language]]
- [[file:../posts/career/restful-api.org][Restful API]]
-- [[file:../posts/career/database-permissions.org][Database Permissions, Roles, and Accounts]]
- [[file:../posts/career/monitoring-and-logging.org][Monitoring and Logging]]
- [[file:../posts/career/pipelines.org][Pipelines and how they work (as well as CI/CD)]]
+- [[file:../posts/career/database-permissions.org][Database Permissions, Roles, and Accounts]]
- [[file:../posts/career/probation-objectives.org][Probation Objectives:]]
- [[file:../posts/career/airflow.org][Datamarts, Airflow and DAG's]]
- [[file:../posts/career/normalisation.org][Benefits of Normalisation]]
-- [[file:../posts/career/management-of-self.org][Management of self training]]
- [[file:../posts/career/wireframe-designs.org][Wireframe Designs]]
+- [[file:../posts/career/management-of-self.org][Management of self training]]
- [[file:../posts/career/requirements-features.org][Requirements, features, user stories, tasks, walking skeletons]]
- [[file:../posts/career/invest-principles.org][Invest Principles]]
-- [[file:../posts/career/retrospectives.org][Retrospectives]]
- [[file:../posts/career/lean.org][Lean]]
+- [[file:../posts/career/retrospectives.org][Retrospectives]]
- [[file:../posts/career/owasp.org][OWASP Top Ten]]
- [[file:../posts/career/solid-principles.org][SOLID Principles]]
diff --git a/tests/test_authoring_server.py b/tests/test_authoring_server.py
index 9e314db..69aae75 100755
--- a/tests/test_authoring_server.py
+++ b/tests/test_authoring_server.py
@@ -103,6 +103,24 @@ class UtilityTests(AuthoringServerTestCase):
self.assertEqual(server.image_dimensions(gif, ".gif"), (320, 200))
self.assertIsNone(server.image_dimensions(b"not an image", ".png"))
+ def test_parse_upload_form_reads_attachment_and_page_path(self):
+ boundary = "----authoring-test"
+ content_type = f"multipart/form-data; boundary={boundary}"
+ body = (
+ f"--{boundary}\r\n"
+ 'Content-Disposition: form-data; name="pagePath"\r\n\r\n'
+ "lima/index.md\r\n"
+ f"--{boundary}\r\n"
+ 'Content-Disposition: form-data; name="attachment"; filename="photo.png"\r\n'
+ "Content-Type: image/png\r\n\r\n"
+ ).encode("utf-8") + b"image bytes\r\n" + f"--{boundary}--\r\n".encode("utf-8")
+
+ filename, payload, page_path = server.parse_upload_form(content_type, body)
+
+ self.assertEqual(filename, "photo.png")
+ self.assertEqual(payload, b"image bytes")
+ self.assertEqual(page_path, "lima/index.md")
+
class PageRenderingTests(AuthoringServerTestCase):
def test_render_org_writes_metadata_and_body(self):
@@ -244,6 +262,22 @@ class PageRenderingTests(AuthoringServerTestCase):
self.assertEqual(saved["path"], "assets/images/blogs/2026-05-07-lunch.jpg")
self.assertEqual(saved["insertText"], "[[../../../assets/images/blogs/2026-05-07-lunch.jpg]]")
+ def test_save_upload_inserts_absolute_public_url_for_markdown(self):
+ with mock.patch.object(server, "datetime") as datetime_mock:
+ datetime_mock.now.return_value = datetime(2026, 5, 9, 9, 30)
+ saved = server.save_upload(
+ "Screen Shot.png",
+ b"image bytes",
+ "lima/index.md",
+ )
+
+ self.assertEqual(saved["path"], "assets/images/hzone/2026-05-09-screen-shot.png")
+ self.assertEqual(saved["relativeUrl"], "../assets/images/hzone/2026-05-09-screen-shot.png")
+ self.assertEqual(
+ saved["insertText"],
+ "",
+ )
+
def test_save_upload_rejects_disallowed_extensions(self):
with self.assertRaises(ValueError):
server.save_upload("shell.php", b"