weekly updates
This commit is contained in:
2
Makefile
2
Makefile
@@ -4,7 +4,7 @@ VENV := .venv
|
||||
PY := $(VENV)/bin/python
|
||||
PIP := $(VENV)/bin/pip
|
||||
|
||||
all: fix-permissions clean-output clean-venv build search
|
||||
all: fix-permissions clean-output build search clean-venv
|
||||
|
||||
build:
|
||||
@echo "Building project..."
|
||||
|
||||
BIN
assets/images/blogs/2026-03-12-journeys-branch.png
Normal file
BIN
assets/images/blogs/2026-03-12-journeys-branch.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/images/blogs/2026-03-18-slack-dag-manager.png
Normal file
BIN
assets/images/blogs/2026-03-18-slack-dag-manager.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
BIN
assets/images/reviews/timesheets/2026-03-15-timesheet.png
Normal file
BIN
assets/images/reviews/timesheets/2026-03-15-timesheet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
assets/images/reviews/timesheets/timesheet-08-03-26.png
Normal file
BIN
assets/images/reviews/timesheets/timesheet-08-03-26.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -77,17 +77,17 @@ function buildCommentTree(comments) {
|
||||
const byId = {};
|
||||
const roots = [];
|
||||
|
||||
comments.forEach(c => {
|
||||
c.children = [];
|
||||
byId[c.id] = c;
|
||||
comments.forEach(comment => {
|
||||
comment.children = [];
|
||||
byId[comment.id] = comment;
|
||||
});
|
||||
|
||||
comments.forEach(c => {
|
||||
if (c.parent_id) {
|
||||
const parent = byId[c.parent_id];
|
||||
if (parent) parent.children.push(c);
|
||||
comments.forEach(comment => {
|
||||
if (comment.parent_id) {
|
||||
const parent = byId[comment.parent_id];
|
||||
if (parent) parent.children.push(comment);
|
||||
} else {
|
||||
roots.push(c);
|
||||
roots.push(comment);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -112,7 +112,7 @@ function renderComments(comments) {
|
||||
}
|
||||
|
||||
const tree = buildCommentTree(comments);
|
||||
tree.forEach(c => list.appendChild(createComment(c)));
|
||||
tree.forEach(comment => list.appendChild(createComment(comment)));
|
||||
}
|
||||
|
||||
|
||||
@@ -153,8 +153,8 @@ async function postComment(author, content, parentId = null) {
|
||||
function wireCommentForm() {
|
||||
const form = document.getElementById("comment-form");
|
||||
|
||||
form.addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
form.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
|
||||
const authorInput = form.querySelector("input[name='author']");
|
||||
const textarea = form.querySelector("textarea[name='content']");
|
||||
@@ -167,7 +167,7 @@ function wireCommentForm() {
|
||||
const ok = await postComment(author, content);
|
||||
if (ok) {
|
||||
textarea.value = "";
|
||||
loadComments(); // same pattern as your Kanban board
|
||||
loadComments();
|
||||
} else {
|
||||
alert("Failed to post comment");
|
||||
}
|
||||
|
||||
@@ -101,33 +101,36 @@ function populateMobileControls(items) {
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById("move-confirm").addEventListener("click", async () => {
|
||||
const itemId = document.getElementById("move-item").value;
|
||||
const from = document.getElementById("move-from").value;
|
||||
const to = document.getElementById("move-to").value;
|
||||
const moveConfirmBtn = document.getElementById("move-confirm");
|
||||
if (moveConfirmBtn) {
|
||||
moveConfirmBtn.addEventListener("click", async () => {
|
||||
const itemId = document.getElementById("move-item").value;
|
||||
const from = document.getElementById("move-from").value;
|
||||
const to = document.getElementById("move-to").value;
|
||||
|
||||
if (!itemId || !to) {
|
||||
alert("Select an item and target column");
|
||||
return;
|
||||
}
|
||||
if (!itemId || !to) {
|
||||
alert("Select an item and target column");
|
||||
return;
|
||||
}
|
||||
|
||||
if (from === to) {
|
||||
alert("Item is already in that column");
|
||||
return;
|
||||
}
|
||||
if (from === to) {
|
||||
alert("Item is already in that column");
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/competencies/items/${itemId}/state`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ state: to }),
|
||||
const res = await fetch(`/api/competencies/items/${itemId}/state`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ state: to }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
loadBoard();
|
||||
} else {
|
||||
alert("Failed to move competency");
|
||||
}
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
loadBoard();
|
||||
} else {
|
||||
alert("Failed to move competency");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let draggedItemId = null;
|
||||
|
||||
|
||||
557
assets/scripts/wird-tracker.js
Normal file
557
assets/scripts/wird-tracker.js
Normal file
@@ -0,0 +1,557 @@
|
||||
/* ============================================================
|
||||
wird-tracker.js
|
||||
============================================================ */
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const API = "/api/wird";
|
||||
|
||||
|
||||
// type: "count" | "juz" | "min" | "rating"
|
||||
const DAILY_WIRD = {
|
||||
durood: { label: "Durood", type: "count", unit: "count", target: 500 },
|
||||
istighfar: { label: "Istighfar", type: "count", unit: "count", target: 200 },
|
||||
quran: { label: "Quran", type: "juz", unit: "juz", target: 3 },
|
||||
muraqabah: { label: "Muraqabah", type: "min", unit: "min", target: 10 },
|
||||
wuqoof_qalbi: { label: "Wuquf Qalbi", type: "rating", unit: "", target: 3 },
|
||||
};
|
||||
|
||||
const WIRD_META = {
|
||||
...DAILY_WIRD,
|
||||
shaykh_meeting: { label: "Meeting w/ Shaykh", type: "meeting", unit: "meeting", target: 1 },
|
||||
};
|
||||
|
||||
const RATING_LABELS = {
|
||||
1: "distracted",
|
||||
2: "scattered",
|
||||
3: "present",
|
||||
4: "attentive",
|
||||
5: "absorbed",
|
||||
};
|
||||
|
||||
const MEETING_CYCLE_DAYS = 21;
|
||||
|
||||
const $ = (sel, ctx = document) => ctx.querySelector(sel);
|
||||
const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];
|
||||
const isoDate = (d = new Date()) => d.toISOString().slice(0, 10);
|
||||
|
||||
function daysBetween(a, b) {
|
||||
return Math.round((new Date(b) - new Date(a)) / 86400000);
|
||||
}
|
||||
function addDays(iso, n) {
|
||||
const d = new Date(iso); d.setDate(d.getDate() + n); return isoDate(d);
|
||||
}
|
||||
function fmtDisplay(iso) {
|
||||
return new Date(iso + "T00:00:00").toLocaleDateString("en-GB", {
|
||||
day: "numeric", month: "short", year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function fmtValue(type, val) {
|
||||
const meta = WIRD_META[type];
|
||||
if (!meta) return val;
|
||||
if (meta.type === "rating") return val ? `${RATING_LABELS[val] || val} (${val}/5)` : "—";
|
||||
if (meta.type === "meeting") return val >= 1 ? "✓ attended" : "✗ missed";
|
||||
return `${Number(val).toLocaleString()} ${meta.unit}`;
|
||||
}
|
||||
|
||||
function statusClass(type, val) {
|
||||
const meta = WIRD_META[type];
|
||||
if (!meta || !val || val <= 0) return "";
|
||||
if (meta.type === "rating") {
|
||||
if (val >= 4) return "over";
|
||||
if (val >= 3) return "done";
|
||||
return "miss";
|
||||
}
|
||||
if (val >= meta.target * 1.1) return "over";
|
||||
if (val >= meta.target) return "done";
|
||||
return "miss";
|
||||
}
|
||||
|
||||
let allEntries = [];
|
||||
let todayMap = {};
|
||||
let meetingLog = [];
|
||||
let chart = null;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
setTodayLabel();
|
||||
await loadAll();
|
||||
renderToday();
|
||||
renderMeetingPanel();
|
||||
renderHistory();
|
||||
renderChart();
|
||||
bindControls();
|
||||
});
|
||||
|
||||
function setTodayLabel() {
|
||||
$("#today-date").textContent = new Date().toLocaleDateString("en-GB", {
|
||||
weekday: "long", day: "numeric", month: "long", year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
try {
|
||||
const r = await fetch(`${API}/entries`);
|
||||
if (!r.ok) throw new Error(r.status);
|
||||
allEntries = await r.json();
|
||||
buildTodayMap();
|
||||
buildMeetingLog();
|
||||
} catch (e) {
|
||||
console.error("Wird API error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function buildTodayMap() {
|
||||
const today = isoDate();
|
||||
todayMap = {};
|
||||
for (const e of allEntries) {
|
||||
if (e.date !== today) continue;
|
||||
// For ratings, take the latest entry (last log wins), not a sum
|
||||
if (WIRD_META[e.wirdType]?.type === "rating") {
|
||||
const existing = todayMap[e.wirdType];
|
||||
if (!existing || e.createdAt > existing.createdAt) {
|
||||
todayMap[e.wirdType] = Number(e.value);
|
||||
}
|
||||
} else {
|
||||
todayMap[e.wirdType] = (todayMap[e.wirdType] || 0) + Number(e.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildMeetingLog() {
|
||||
meetingLog = allEntries
|
||||
.filter(e => e.wirdType === "shaykh_meeting" && Number(e.value) >= 1)
|
||||
.sort((a, b) => b.date.localeCompare(a.date));
|
||||
}
|
||||
|
||||
async function postEntry(payload) {
|
||||
const r = await fetch(`${API}/entries`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
return r.json();
|
||||
}
|
||||
|
||||
function renderToday() {
|
||||
const wrap = $("#wird-cards");
|
||||
wrap.innerHTML = "";
|
||||
let done = 0;
|
||||
const types = Object.keys(DAILY_WIRD);
|
||||
|
||||
for (const type of types) {
|
||||
const meta = DAILY_WIRD[type];
|
||||
const val = todayMap[type] || 0;
|
||||
const cls = statusClass(type, val);
|
||||
if (cls === "done" || cls === "over") done++;
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = `wird-card ${cls}`;
|
||||
card.dataset.type = type;
|
||||
|
||||
if (meta.type === "rating") {
|
||||
card.innerHTML = buildRatingCard(meta, val);
|
||||
} else {
|
||||
const pct = Math.min((val / meta.target) * 100, 100);
|
||||
card.innerHTML = `
|
||||
<div class="card-name">${meta.label}</div>
|
||||
<div class="card-meta">
|
||||
${fmtValue(type, val)}
|
||||
<span style="opacity:.5"> / ${meta.target.toLocaleString()} ${meta.unit}</span>
|
||||
</div>
|
||||
<div class="card-bar-wrap">
|
||||
<div class="card-bar-fill" style="width:${pct}%"></div>
|
||||
</div>
|
||||
<button class="card-log-btn">+ log</button>
|
||||
`;
|
||||
}
|
||||
|
||||
card.querySelector(".card-log-btn").addEventListener("click", () => openModal(type));
|
||||
wrap.appendChild(card);
|
||||
}
|
||||
|
||||
const pct = Math.round((done / types.length) * 100);
|
||||
$("#today-progress-fill").style.width = pct + "%";
|
||||
$("#today-progress-label").textContent = `${done} / ${types.length} complete`;
|
||||
}
|
||||
|
||||
function buildRatingCard(meta, val) {
|
||||
// 5 pips, filled up to current rating
|
||||
const pips = [1,2,3,4,5].map(n => {
|
||||
const filled = val >= n;
|
||||
const isMin = n === meta.target;
|
||||
return `<span class="card-pip ${filled ? "filled" : ""} ${isMin ? "min-marker" : ""}"
|
||||
title="${RATING_LABELS[n]}"></span>`;
|
||||
}).join("");
|
||||
|
||||
const lbl = val ? `${RATING_LABELS[val]} <span style="opacity:.45">(${val}/5)</span>` : "not logged";
|
||||
|
||||
return `
|
||||
<div class="card-name">${meta.label}</div>
|
||||
<div class="card-pips">${pips}</div>
|
||||
<div class="card-meta card-rating-lbl">${lbl}</div>
|
||||
<button class="card-log-btn">+ log</button>
|
||||
`;
|
||||
}
|
||||
|
||||
const modal = $("#log-modal");
|
||||
const form = $("#log-form");
|
||||
const typeEl = $("#log-type");
|
||||
const valEl = $("#log-value");
|
||||
const dateEl = $("#log-date");
|
||||
const notesEl = $("#log-notes");
|
||||
const ratingEl = $("#log-rating-value");
|
||||
|
||||
// Pip click handlers
|
||||
let selectedRating = 0;
|
||||
$$("#modal-pips .modal-pip").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
selectedRating = parseInt(btn.dataset.val);
|
||||
ratingEl.value = selectedRating;
|
||||
$$("#modal-pips .modal-pip").forEach(b => {
|
||||
const v = parseInt(b.dataset.val);
|
||||
b.classList.toggle("selected", v <= selectedRating);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function openModal(preType = null) {
|
||||
form.reset();
|
||||
selectedRating = 0;
|
||||
ratingEl.value = "";
|
||||
$$("#modal-pips .modal-pip").forEach(b => b.classList.remove("selected"));
|
||||
dateEl.value = isoDate();
|
||||
if (preType) typeEl.value = preType;
|
||||
updateModalFields();
|
||||
modal.showModal();
|
||||
}
|
||||
|
||||
function openMeetingModal() {
|
||||
form.reset();
|
||||
dateEl.value = isoDate();
|
||||
typeEl.value = "shaykh_meeting";
|
||||
updateModalFields();
|
||||
modal.showModal();
|
||||
}
|
||||
|
||||
function updateModalFields() {
|
||||
const type = typeEl.value;
|
||||
const isRating = type === "wuqoof_qalbi";
|
||||
const isMeeting = type === "shaykh_meeting";
|
||||
|
||||
$("#value-group").style.display = (!isRating && !isMeeting) ? "" : "none";
|
||||
$("#rating-group").style.display = isRating ? "" : "none";
|
||||
$("#shaykh-group").style.display = isMeeting ? "" : "none";
|
||||
|
||||
if (!isRating && !isMeeting) {
|
||||
const meta = DAILY_WIRD[type];
|
||||
if (meta) valEl.placeholder = `Target: ${meta.target} ${meta.unit}`;
|
||||
}
|
||||
}
|
||||
|
||||
typeEl.addEventListener("change", updateModalFields);
|
||||
$("#modal-cancel").addEventListener("click", () => modal.close());
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const type = typeEl.value;
|
||||
const isRating = type === "wuqoof_qalbi";
|
||||
const isMtg = type === "shaykh_meeting";
|
||||
|
||||
if (isRating && !ratingEl.value) {
|
||||
alert("Please select a rating.");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
wirdType: type,
|
||||
date: dateEl.value,
|
||||
value: isRating ? Number(ratingEl.value)
|
||||
: isMtg ? 1
|
||||
: Number(valEl.value),
|
||||
notes: notesEl.value.trim() || null,
|
||||
};
|
||||
|
||||
try {
|
||||
const saved = await postEntry(payload);
|
||||
allEntries.push(saved);
|
||||
buildTodayMap();
|
||||
buildMeetingLog();
|
||||
renderToday();
|
||||
renderMeetingPanel();
|
||||
renderHistory();
|
||||
renderChart();
|
||||
modal.close();
|
||||
} catch (err) {
|
||||
alert("Could not save entry: " + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
function renderMeetingPanel() {
|
||||
const today = isoDate();
|
||||
const last = meetingLog[0] || null;
|
||||
const lastDate = last ? last.date : null;
|
||||
const nextDate = lastDate ? addDays(lastDate, MEETING_CYCLE_DAYS) : null;
|
||||
const daysToNext = nextDate ? daysBetween(today, nextDate) : null;
|
||||
|
||||
let statusHtml = "";
|
||||
if (!lastDate) {
|
||||
statusHtml = `<span class="meeting-status pending">No meetings logged yet</span>`;
|
||||
} else if (daysToNext > 0) {
|
||||
statusHtml = `<span class="meeting-status ok">Next due in <strong>${daysToNext}</strong> day${daysToNext !== 1 ? "s" : ""} — ${fmtDisplay(nextDate)}</span>`;
|
||||
} else if (daysToNext === 0) {
|
||||
statusHtml = `<span class="meeting-status due">Due today</span>`;
|
||||
} else {
|
||||
const ov = Math.abs(daysToNext);
|
||||
statusHtml = `<span class="meeting-status overdue"><strong>${ov}</strong> day${ov !== 1 ? "s" : ""} overdue — expected ${fmtDisplay(nextDate)}</span>`;
|
||||
}
|
||||
|
||||
const cycles = buildCycleInsights();
|
||||
const cycleRows = cycles.map(c => `
|
||||
<tr>
|
||||
<td>${fmtDisplay(c.start)} → ${fmtDisplay(c.end)}</td>
|
||||
<td>${c.met
|
||||
? `<span class="badge done">✓ met — ${fmtDisplay(c.metOn)}</span>`
|
||||
: c.ongoing
|
||||
? `<span class="badge">ongoing</span>`
|
||||
: `<span class="badge miss">✗ missed</span>`}
|
||||
</td>
|
||||
<td style="font-family:monospace;font-size:.7rem;color:var(--muted)">${c.met ? `day ${c.dayOfCycle}` : "—"}</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
|
||||
$("#meeting-panel-body").innerHTML = `
|
||||
<div id="meeting-status-row">
|
||||
<div>
|
||||
<span class="meeting-meta-lbl">Last meeting</span>
|
||||
<span class="meeting-meta-val">${lastDate ? fmtDisplay(lastDate) : "—"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="meeting-meta-lbl">Next expected</span>
|
||||
<span class="meeting-meta-val">${nextDate ? fmtDisplay(nextDate) : "—"}</span>
|
||||
</div>
|
||||
<div id="meeting-status-msg">${statusHtml}</div>
|
||||
</div>
|
||||
<div id="meeting-cycles">
|
||||
<h3>3-week cycles</h3>
|
||||
${cycles.length === 0
|
||||
? `<p style="color:var(--muted);font-size:.88rem">Log at least one meeting to see cycle insights.</p>`
|
||||
: `<table class="meeting-cycle-table">
|
||||
<thead><tr><th>Cycle window</th><th>Status</th><th>When</th></tr></thead>
|
||||
<tbody>${cycleRows}</tbody>
|
||||
</table>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function buildCycleInsights() {
|
||||
if (meetingLog.length === 0) return [];
|
||||
const dates = [...meetingLog].reverse().map(e => e.date);
|
||||
const today = isoDate();
|
||||
const cycles = [];
|
||||
let start = dates[0];
|
||||
while (start <= today) {
|
||||
const end = addDays(start, MEETING_CYCLE_DAYS - 1);
|
||||
const hit = dates.find(d => d >= start && d <= end);
|
||||
cycles.push({
|
||||
start, end,
|
||||
met: !!hit,
|
||||
metOn: hit || null,
|
||||
dayOfCycle: hit ? daysBetween(start, hit) + 1 : null,
|
||||
ongoing: end > today,
|
||||
});
|
||||
start = addDays(end, 1);
|
||||
}
|
||||
return cycles.reverse();
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
const tbody = $("#history-body");
|
||||
tbody.innerHTML = "";
|
||||
|
||||
const sorted = [...allEntries]
|
||||
.filter(e => e.wirdType !== "shaykh_meeting")
|
||||
.sort((a, b) => b.date.localeCompare(a.date));
|
||||
|
||||
if (sorted.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="5" class="loading-cell">No entries yet.</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const e of sorted) {
|
||||
const meta = WIRD_META[e.wirdType] || {};
|
||||
const cls = statusClass(e.wirdType, e.value);
|
||||
const badgeTxt = cls === "over" ? (meta.type === "rating" ? "flourishing" : "above")
|
||||
: cls === "done" ? (meta.type === "rating" ? "present" : "met")
|
||||
: cls === "miss" ? (meta.type === "rating" ? "below" : "below")
|
||||
: "—";
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td style="font-family:monospace;font-size:.75rem">${e.date}</td>
|
||||
<td>${meta.label || e.wirdType}</td>
|
||||
<td style="font-family:monospace;font-size:.82rem">${fmtValue(e.wirdType, e.value)}</td>
|
||||
<td><span class="badge ${cls}">${badgeTxt}</span></td>
|
||||
<td style="color:var(--muted);font-size:.82rem">${e.notes || ""}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
function renderChart() {
|
||||
const type = $("#trend-type").value;
|
||||
const days = parseInt($("#trend-range").value);
|
||||
const meta = WIRD_META[type];
|
||||
const isRating = meta.type === "rating";
|
||||
const now = new Date();
|
||||
|
||||
const labels = [];
|
||||
const vals = [];
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const d = new Date(now);
|
||||
d.setDate(d.getDate() - i);
|
||||
const iso = isoDate(d);
|
||||
labels.push(iso.slice(5));
|
||||
|
||||
const dayEntries = allEntries.filter(e => e.date === iso && e.wirdType === type);
|
||||
if (isRating) {
|
||||
// Use the last logged rating for the day
|
||||
const last = dayEntries.sort((a,b) => a.createdAt > b.createdAt ? 1 : -1).pop();
|
||||
vals.push(last ? Number(last.value) : 0);
|
||||
} else {
|
||||
vals.push(dayEntries.reduce((s, e) => s + Number(e.value), 0));
|
||||
}
|
||||
}
|
||||
|
||||
const nonZero = vals.filter(v => v > 0);
|
||||
const avg = nonZero.length
|
||||
? (isRating
|
||||
? (nonZero.reduce((a,b)=>a+b,0)/nonZero.length).toFixed(1)
|
||||
: Math.round(nonZero.reduce((a,b)=>a+b,0)/nonZero.length))
|
||||
: 0;
|
||||
const max = nonZero.length ? Math.max(...nonZero) : 0;
|
||||
const streak = calcStreak(type);
|
||||
|
||||
if (isRating) {
|
||||
$("#trend-stats").innerHTML = `
|
||||
<div class="stat-chip"><span class="stat-val">${avg}</span><span class="stat-lbl">avg rating</span></div>
|
||||
<div class="stat-chip"><span class="stat-val">${RATING_LABELS[max] || "—"}</span><span class="stat-lbl">best day</span></div>
|
||||
<div class="stat-chip"><span class="stat-val">${streak}d</span><span class="stat-lbl">streak e ${meta.target}</span></div>
|
||||
<div class="stat-chip"><span class="stat-val">${nonZero.length}</span><span class="stat-lbl">days logged</span></div>
|
||||
`;
|
||||
} else {
|
||||
$("#trend-stats").innerHTML = `
|
||||
<div class="stat-chip"><span class="stat-val">${Number(avg).toLocaleString()}</span><span class="stat-lbl">avg / day</span></div>
|
||||
<div class="stat-chip"><span class="stat-val">${Number(max).toLocaleString()}</span><span class="stat-lbl">best day</span></div>
|
||||
<div class="stat-chip"><span class="stat-val">${streak}d</span><span class="stat-lbl">current streak</span></div>
|
||||
<div class="stat-chip"><span class="stat-val">${meta.target.toLocaleString()}</span><span class="stat-lbl">daily target</span></div>
|
||||
`;
|
||||
}
|
||||
|
||||
const cs = getComputedStyle(document.documentElement);
|
||||
const accent = cs.getPropertyValue("--wird-accent").trim() || "#7c5c3a";
|
||||
const gridColor = cs.getPropertyValue("--border").trim() || "#d7d7d7";
|
||||
const tickColor = cs.getPropertyValue("--muted").trim() || "#666";
|
||||
|
||||
const ctx = $("#trend-chart").getContext("2d");
|
||||
if (chart) chart.destroy();
|
||||
|
||||
chart = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: meta.label,
|
||||
data: vals,
|
||||
borderColor: accent,
|
||||
backgroundColor: accent + "22",
|
||||
fill: true,
|
||||
tension: .35,
|
||||
pointRadius: 3,
|
||||
pointHoverRadius: 5,
|
||||
},
|
||||
{
|
||||
label: "Target",
|
||||
data: vals.map(() => meta.target),
|
||||
borderColor: "rgba(74,124,89,.5)",
|
||||
borderDash: [5,4],
|
||||
pointRadius: 0,
|
||||
fill: false,
|
||||
tension: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: ctx => {
|
||||
const v = ctx.parsed.y;
|
||||
if (isRating && ctx.datasetIndex === 0)
|
||||
return `${meta.label}: ${RATING_LABELS[v] || v} (${v}/5)`;
|
||||
return `${ctx.dataset.label}: ${v}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: gridColor + "22" },
|
||||
ticks: { color: tickColor, font: { family: "monospace", size: 10 }, maxTicksLimit: 10 },
|
||||
},
|
||||
y: {
|
||||
grid: { color: gridColor + "22" },
|
||||
ticks: {
|
||||
color: tickColor,
|
||||
font: { family: "monospace", size: 10 },
|
||||
// For rating chart: show labels instead of numbers
|
||||
...(isRating ? {
|
||||
min: 0, max: 5, stepSize: 1,
|
||||
callback: v => RATING_LABELS[v] || (v === 0 ? "" : v),
|
||||
} : {}),
|
||||
},
|
||||
beginAtZero: true,
|
||||
...(isRating ? { min: 0, max: 5 } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function calcStreak(type) {
|
||||
const meta = WIRD_META[type];
|
||||
let streak = 0;
|
||||
const now = new Date();
|
||||
for (let i = 0; i < 365; i++) {
|
||||
const d = new Date(now);
|
||||
d.setDate(d.getDate() - i);
|
||||
const iso = isoDate(d);
|
||||
const entries = allEntries.filter(e => e.date === iso && e.wirdType === type);
|
||||
let total;
|
||||
if (meta.type === "rating") {
|
||||
const last = entries.sort((a,b) => a.createdAt > b.createdAt ? 1 : -1).pop();
|
||||
total = last ? Number(last.value) : 0;
|
||||
} else {
|
||||
total = entries.reduce((s, e) => s + Number(e.value), 0);
|
||||
}
|
||||
if (total >= meta.target) streak++;
|
||||
else if (i > 0) break;
|
||||
}
|
||||
return streak;
|
||||
}
|
||||
|
||||
function bindControls() {
|
||||
$("#open-log-btn").addEventListener("click", () => openModal());
|
||||
$("#log-meeting-btn").addEventListener("click", openMeetingModal);
|
||||
$("#trend-type").addEventListener("change", renderChart);
|
||||
$("#trend-range").addEventListener("change", renderChart);
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -317,7 +317,7 @@ body.no-sidenotes {
|
||||
.note {
|
||||
position: relative;
|
||||
padding: 1rem 1rem 0.9rem;
|
||||
background: #fffef8;
|
||||
background: --code-bg;
|
||||
border-radius: 8px;
|
||||
box-shadow:
|
||||
0 2px 6px rgba(0, 0, 0, 0.08),
|
||||
@@ -326,6 +326,7 @@ body.no-sidenotes {
|
||||
transition:
|
||||
transform 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
|
||||
}
|
||||
|
||||
/* subtle randomness */
|
||||
@@ -391,6 +392,7 @@ body.no-sidenotes {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.45;
|
||||
white-space: pre-wrap;
|
||||
color: --fg;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
@@ -424,7 +426,7 @@ body.no-sidenotes {
|
||||
#notes-form {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background: #fffef8;
|
||||
background: --bg;
|
||||
padding: 1.25rem 1.25rem 1.1rem;
|
||||
border-radius: 10px;
|
||||
box-shadow:
|
||||
|
||||
BIN
assets/styles/wird-tracker.css
Normal file
BIN
assets/styles/wird-tracker.css
Normal file
Binary file not shown.
@@ -1,28 +0,0 @@
|
||||
#+TITLE: 2025 List
|
||||
#+OPTIONS: toc:nil num:nil
|
||||
|
||||
See the categories: @@html:<a href="../../home/categories.html">Categories</a>@@
|
||||
|
||||
* 2025
|
||||
|
||||
** December 2025
|
||||
- [[file:12-december/28-12-week-review.org][[28-12-2025] - Weekly Review]] @@html:<span class="post-date">28-12-2025 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:12-december/21-12-week-review.org][[21-12-2025] - Weekly Review]] @@html:<span class="post-date">21-12-2025 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:12-december/14-12-week-review.org][[14-12-2025] - Weekly Review]] @@html:<span class="post-date">09-12-2025 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:12-december/07-12-week-review.org][[07-12-2025] - Weekly Review]] @@html:<span class="post-date">07-12-2025 20:34</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
|
||||
** November 2025
|
||||
- [[file:11-november/30-11-week-review.org][[30-11-2025] - Weekly Review]] @@html:<span class="post-date">30-11-2025 17:09</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:11-november/23-11-week-review.org][[23-11-2025] - Weekly Review]] @@html:<span class="post-date">17-11-2025 18:05</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:11-november/16-11-week-review.org][[16-11-2025] - Weekly Review]] @@html:<span class="post-date">10-11-2025 17:44</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:11-november/09-11-week-review.org][[09-11-2025] - Weekly Review]] @@html:<span class="post-date">09-11-2025 20:08</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:11-november/02-11-week-review.org][[02-11-2025] - Weekly Review]] @@html:<span class="post-date">02-11-2025 00:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
|
||||
** August 2025
|
||||
- [[file:08-august/third-time.org][Third Time]] @@html:<span class="post-date">28-08-2025 17:08</span>@@ @@html:<a href="/tags/insights.html"> <span class="post-tag">insights</span> </a>@@
|
||||
- [[file:08-august/benefits-of-reading.org][Benefits of Reading]] @@html:<span class="post-date">14-08-2025 23:36</span>@@ @@html:<a href="/tags/reading.html"> <span class="post-tag">reading</span> </a>@@ @@html:<a href="/tags/insights.html"> <span class="post-tag">insights</span> </a>@@
|
||||
- [[file:08-august/hilberts.hotel.org][Hilbert's Hotel]] @@html:<span class="post-date">11-08-2025 18:39</span>@@ @@html:<a href="/tags/maths.html"> <span class="post-tag">maths</span> </a>@@ @@html:<a href="/tags/insights.html"> <span class="post-tag">insights</span> </a>@@
|
||||
- [[file:08-august/spending-the-whole-day-on-this-website.org][09-08-2025: Website Changes]] @@html:<span class="post-date">09-08-2025 00:00</span>@@ @@html:<a href="/tags/emacs.html"> <span class="post-tag">emacs</span> </a>@@ @@html:<a href="/tags/website.html"> <span class="post-tag">website</span> </a>@@
|
||||
- [[file:08-august/what-do-i-want-to-do-with-emacs.org][What Do I Want To Do With Emacs]] @@html:<span class="post-date">08-08-2025 00:00</span>@@ @@html:<a href="/tags/emacs.html"> <span class="post-tag">emacs</span> </a>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:08-august/wacom-with-arch.org][Wacom With Arch]] @@html:<span class="post-date">08-08-2025 00:00</span>@@ @@html:<a href="/tags/insights.html"> <span class="post-tag">insights</span> </a>@@
|
||||
- [[file:08-august/zettelkasten.org][Zettelkasten Method]] @@html:<span class="post-date">07-08-2025 00:00</span>@@ @@html:<a href="/tags/education.html"> <span class="post-tag">education</span> </a>@@
|
||||
15
blogs/2026/03-march/08-03-week-review.org
Normal file
15
blogs/2026/03-march/08-03-week-review.org
Normal file
@@ -0,0 +1,15 @@
|
||||
#+TITLE: [08-03-2026] - Weekly Review
|
||||
#+OPTIONS: num:nil
|
||||
#+DATE: <2026-03-08 Sun 12:00>
|
||||
#+filetags: :review:
|
||||
#+COMMENTS: t
|
||||
#+SLUG: 08-03-26-week-review
|
||||
|
||||
* Review
|
||||
Another work week done. This week was spent mostly working on the Journeys upgrade (again), but we are nearing completion. Towards the end of the week we had to drop what we were doing to focus on getting Site Visits to live. I hope that In'sha'Allah I can do the deployment process for Journeys as there is a lot of learning potential.
|
||||
|
||||
Unfortunately Hyper-V broke on my laptop, so I have to go in the office next monday to get it repaired.
|
||||
|
||||
* Timesheet
|
||||
|
||||
[[../../../assets/images/reviews/timesheets/timesheet-08-03-26.png]]
|
||||
20
blogs/2026/03-march/15-03-week-review.org
Normal file
20
blogs/2026/03-march/15-03-week-review.org
Normal file
@@ -0,0 +1,20 @@
|
||||
#+TITLE: [15-03-2026] - Weekly Review
|
||||
#+OPTIONS: num:nil
|
||||
#+DATE: <2026-03-15 Sun 12:00>
|
||||
#+filetags: :review:
|
||||
#+COMMENTS: t
|
||||
#+SLUG: 15-03-26-week-review
|
||||
|
||||
* Review
|
||||
|
||||
This week was a four day work week. I had no plans on booking the Friday off but a message came saying we were invited to perform Nafl I'tikaaf. Thus, it hit me that this is the last Friday of Ramadan so I should make the most of it. In'sha'Allah I plan to stay until Monday after Fajr (writing this as of <2026-03-12 Thu>).
|
||||
|
||||
I finally finished the upgrades for Journeys Web API. I released a PR on <2026-03-12 Thu> and should get to resolving the comments on Monday In'sha'Allah.
|
||||
|
||||
Over 100 commits on this single PR...
|
||||
|
||||
[[../../../assets/images/blogs/2026-03-12-journeys-branch.png]]
|
||||
|
||||
* Timesheet
|
||||
|
||||
[[../../../assets/images/reviews/timesheets/2026-03-15-timesheet.png]]
|
||||
8
blogs/2026/03-march/feeling-sleepy.org
Normal file
8
blogs/2026/03-march/feeling-sleepy.org
Normal file
@@ -0,0 +1,8 @@
|
||||
#+TITLE: Feeling extremely sleepy
|
||||
#+OPTIONS: num:nil
|
||||
#+DATE: <2026-03-16 Mon 16:18>
|
||||
#+filetags: :life:
|
||||
#+COMMENTS: t
|
||||
#+SLUG: feeling-sleepy
|
||||
|
||||
One thing I realised about myself is that if I feel /sleepy/ (as in running on fumes), then I will literally go unconscious, as I can't tell the difference between reality and dream.
|
||||
10
blogs/2026/03-march/fixing-the-dag-18-03.org
Normal file
10
blogs/2026/03-march/fixing-the-dag-18-03.org
Normal file
@@ -0,0 +1,10 @@
|
||||
#+TITLE: DAG fixes
|
||||
#+OPTIONS: num:nil
|
||||
#+DATE: <2026-03-18 Wed 15:01>
|
||||
#+filetags: :life:
|
||||
#+COMMENTS: t
|
||||
#+SLUG: fixing-the-dag-18-03
|
||||
|
||||
What an interesting morning. So yesterday, my manager sent me a DM asking to look into the DAG stuff (was failing for a few days due to the restructuring of SQ).
|
||||
|
||||
[[../../../assets/images/blogs/2026-03-18-slack-dag-manager.png]]
|
||||
10
blogs/2026/03-march/oversleeping-16-03.org
Normal file
10
blogs/2026/03-march/oversleeping-16-03.org
Normal file
@@ -0,0 +1,10 @@
|
||||
#+TITLE: Oversleeping and missing a meeting...
|
||||
#+OPTIONS: num:nil
|
||||
#+DATE: <2026-03-16 Mon 11:21>
|
||||
#+filetags: :life:
|
||||
#+COMMENTS: t
|
||||
#+SLUG: oversleeping-16-03
|
||||
|
||||
Just this morning, I went to sleep at 6 am, due to spending the night elsewhere, and had some urgent things to do. I ended up waking up at 9:30, which meant the daily standup just finished. Never again.
|
||||
|
||||
Sleeping in and missing a meeting is a negative million kind of feeling.
|
||||
9
blogs/2026/03-march/setting-a-wird-tracker-19-03.org
Normal file
9
blogs/2026/03-march/setting-a-wird-tracker-19-03.org
Normal file
@@ -0,0 +1,9 @@
|
||||
#+TITLE: Setting a Wird Tracker
|
||||
#+OPTIONS: num:nil
|
||||
#+DATE: <2026-03-19 Thu 13:15>
|
||||
#+filetags: :life:
|
||||
#+COMMENTS: t
|
||||
#+SLUG: setting-a-wird-tracker-19-03
|
||||
|
||||
I decided it was time to use the extensibility of this website to add an [[../../../home/wird-tracker.org][Awrad tracker]], which would serve as a place for me to track the litanies on a daily basis. I used claude to generate the architecture and to give the frontend and backend for it. It did a good job {{{sidenote(1, It also generated a comprehensive documentation here https://zainezq.com/home/guide/wird-tracker-guide.html)}}}.
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#+TITLE: 2026 List
|
||||
#+OPTIONS: toc:nil num:nil
|
||||
|
||||
See the categories: @@html:<a href="../../home/categories.html">Categories</a>@@
|
||||
|
||||
* 2026
|
||||
|
||||
** March 2026
|
||||
- [[file:03-march/01-03-week-review.org][[01-03-2026] - Weekly Review]] @@html:<span class="post-date">01-03-2026 12:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
|
||||
** February 2026
|
||||
- [[file:02-february/27-02-26.org][Journeys rambles again...]] @@html:<span class="post-date">27-02-2026 17:12</span>@@ @@html:<a href="/tags/life.html"> <span class="post-tag">life</span> </a>@@
|
||||
- [[file:02-february/26-02-26.org][Starting the Journeys Upgrade]] @@html:<span class="post-date">26-02-2026 17:32</span>@@ @@html:<a href="/tags/life.html"> <span class="post-tag">life</span> </a>@@
|
||||
- [[file:02-february/24-02-26.org][Integration tests failing (sob)]] @@html:<span class="post-date">24-02-2026 16:55</span>@@ @@html:<a href="/tags/life.html"> <span class="post-tag">life</span> </a>@@
|
||||
- [[file:02-february/22-02-week-review.org][[22-02-2026] - Weekly Review]] @@html:<span class="post-date">22-02-2026 12:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:02-february/15-02-week-review.org][[15-02-2026] - Weekly Review]] @@html:<span class="post-date">15-02-2026 12:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:02-february/08-02-week-review.org][[08-02-2026] - Weekly Review]] @@html:<span class="post-date">08-02-2026 12:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:02-february/01-02-week-review.org][[01-02-2026] - Weekly Review]] @@html:<span class="post-date">01-02-2026 12:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:02-february/third-meeting.org][Third Meeting with lima :)]] @@html:<span class="post-date">01-02-2026 12:00</span>@@ @@html:<a href="/tags/life.html"> <span class="post-tag">life</span> </a>@@
|
||||
|
||||
** January 2026
|
||||
- [[file:01-january/25-01-week-review.org][[25-01-2026] - Weekly Review]] @@html:<span class="post-date">25-01-2026 12:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:01-january/18-01-week-review.org][[18-01-2026] - Weekly Review]] @@html:<span class="post-date">18-01-2026 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:01-january/11-01-week-review.org][[11-01-2026] - Weekly Review]] @@html:<span class="post-date">11-01-2026 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:01-january/04-01-week-review.org][[04-01-2026] - Weekly Review]] @@html:<span class="post-date">04-01-2026 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
@@ -3,37 +3,54 @@
|
||||
|
||||
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
||||
|
||||
* Blogs:
|
||||
- [[file:2026/2026-list.org][2026 List]] @@html:<span class="post-date">08-03-2026 17:33</span>@@
|
||||
- [[file:2025/2025-list.org][2025 List]] @@html:<span class="post-date">08-03-2026 17:33</span>@@
|
||||
* 2026
|
||||
|
||||
** March 2026
|
||||
- [[file:2026/03-march/setting-a-wird-tracker-19-03.org][Setting a Wird Tracker]] @@html:<span class="post-date">19-03-2026 13:15</span>@@ @@html:<a href="/tags/life.html"> <span class="post-tag">life</span> </a>@@
|
||||
- [[file:2026/03-march/fixing-the-dag-18-03.org][DAG fixes]] @@html:<span class="post-date">18-03-2026 15:01</span>@@ @@html:<a href="/tags/life.html"> <span class="post-tag">life</span> </a>@@
|
||||
- [[file:2026/03-march/feeling-sleepy.org][Feeling extremely sleepy]] @@html:<span class="post-date">16-03-2026 16:18</span>@@ @@html:<a href="/tags/life.html"> <span class="post-tag">life</span> </a>@@
|
||||
- [[file:2026/03-march/oversleeping-16-03.org][Oversleeping and missing a meeting...]] @@html:<span class="post-date">16-03-2026 11:21</span>@@ @@html:<a href="/tags/life.html"> <span class="post-tag">life</span> </a>@@
|
||||
- [[file:2026/03-march/15-03-week-review.org][[15-03-2026] - Weekly Review]] @@html:<span class="post-date">15-03-2026 12:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2026/03-march/08-03-week-review.org][[08-03-2026] - Weekly Review]] @@html:<span class="post-date">08-03-2026 12:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2026/03-march/01-03-week-review.org][[01-03-2026] - Weekly Review]] @@html:<span class="post-date">01-03-2026 12:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
|
||||
** February 2026
|
||||
- [[file:2026/02-february/27-02-26.org][Journeys rambles again...]] @@html:<span class="post-date">27-02-2026 17:12</span>@@ @@html:<a href="/tags/life.html"> <span class="post-tag">life</span> </a>@@
|
||||
- [[file:2026/02-february/26-02-26.org][Starting the Journeys Upgrade]] @@html:<span class="post-date">26-02-2026 17:32</span>@@ @@html:<a href="/tags/life.html"> <span class="post-tag">life</span> </a>@@
|
||||
- [[file:2026/02-february/24-02-26.org][Integration tests failing (sob)]] @@html:<span class="post-date">24-02-2026 16:55</span>@@ @@html:<a href="/tags/life.html"> <span class="post-tag">life</span> </a>@@
|
||||
- [[file:2026/02-february/22-02-week-review.org][[22-02-2026] - Weekly Review]] @@html:<span class="post-date">22-02-2026 12:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2026/02-february/15-02-week-review.org][[15-02-2026] - Weekly Review]] @@html:<span class="post-date">15-02-2026 12:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2026/02-february/08-02-week-review.org][[08-02-2026] - Weekly Review]] @@html:<span class="post-date">08-02-2026 12:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2026/02-february/01-02-week-review.org][[01-02-2026] - Weekly Review]] @@html:<span class="post-date">01-02-2026 12:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2026/02-february/third-meeting.org][Third Meeting with lima :)]] @@html:<span class="post-date">01-02-2026 12:00</span>@@ @@html:<a href="/tags/life.html"> <span class="post-tag">life</span> </a>@@
|
||||
- [[file:2026/02-february/01-02-week-review.org][[01-02-2026] - Weekly Review]] @@html:<span class="post-date">01-02-2026 12:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
|
||||
** January 2026
|
||||
- [[file:2026/01-january/25-01-week-review.org][[25-01-2026] - Weekly Review]] @@html:<span class="post-date">25-01-2026 12:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2026/01-january/18-01-week-review.org][[18-01-2026] - Weekly Review]] @@html:<span class="post-date">18-01-2026 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2026/01-january/11-01-week-review.org][[11-01-2026] - Weekly Review]] @@html:<span class="post-date">11-01-2026 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2026/01-january/04-01-week-review.org][[04-01-2026] - Weekly Review]] @@html:<span class="post-date">04-01-2026 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
* 2025
|
||||
|
||||
** December 2025
|
||||
- [[file:2025/12-december/28-12-week-review.org][[28-12-2025] - Weekly Review]] @@html:<span class="post-date">28-12-2025 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2025/12-december/21-12-week-review.org][[21-12-2025] - Weekly Review]] @@html:<span class="post-date">21-12-2025 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2025/12-december/14-12-week-review.org][[14-12-2025] - Weekly Review]] @@html:<span class="post-date">09-12-2025 12:12</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2025/12-december/07-12-week-review.org][[07-12-2025] - Weekly Review]] @@html:<span class="post-date">07-12-2025 20:34</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
|
||||
** November 2025
|
||||
- [[file:2025/11-november/30-11-week-review.org][[30-11-2025] - Weekly Review]] @@html:<span class="post-date">30-11-2025 17:09</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2025/11-november/23-11-week-review.org][[23-11-2025] - Weekly Review]] @@html:<span class="post-date">17-11-2025 18:05</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2025/11-november/16-11-week-review.org][[16-11-2025] - Weekly Review]] @@html:<span class="post-date">10-11-2025 17:44</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2025/11-november/09-11-week-review.org][[09-11-2025] - Weekly Review]] @@html:<span class="post-date">09-11-2025 20:08</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:publish-pages.org][How to publish pages using Org Publish]] @@html:<span class="post-date">08-11-2025 10:57</span>@@ @@html:<a href="/tags/website.html"> <span class="post-tag">website</span> </a>@@
|
||||
- [[file:2025/11-november/02-11-week-review.org][[02-11-2025] - Weekly Review]] @@html:<span class="post-date">02-11-2025 00:00</span>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
|
||||
** August 2025
|
||||
- [[file:2025/08-august/third-time.org][Third Time]] @@html:<span class="post-date">28-08-2025 17:08</span>@@ @@html:<a href="/tags/insights.html"> <span class="post-tag">insights</span> </a>@@
|
||||
- [[file:2025/08-august/benefits-of-reading.org][Benefits of Reading]] @@html:<span class="post-date">14-08-2025 23:36</span>@@ @@html:<a href="/tags/reading.html"> <span class="post-tag">reading</span> </a>@@ @@html:<a href="/tags/insights.html"> <span class="post-tag">insights</span> </a>@@
|
||||
- [[file:2025/08-august/hilberts.hotel.org][Hilbert's Hotel]] @@html:<span class="post-date">11-08-2025 18:39</span>@@ @@html:<a href="/tags/maths.html"> <span class="post-tag">maths</span> </a>@@ @@html:<a href="/tags/insights.html"> <span class="post-tag">insights</span> </a>@@
|
||||
- [[file:2025/08-august/spending-the-whole-day-on-this-website.org][09-08-2025: Website Changes]] @@html:<span class="post-date">09-08-2025 00:00</span>@@ @@html:<a href="/tags/emacs.html"> <span class="post-tag">emacs</span> </a>@@ @@html:<a href="/tags/website.html"> <span class="post-tag">website</span> </a>@@
|
||||
- [[file:2025/08-august/what-do-i-want-to-do-with-emacs.org][What Do I Want To Do With Emacs]] @@html:<span class="post-date">08-08-2025 00:00</span>@@ @@html:<a href="/tags/emacs.html"> <span class="post-tag">emacs</span> </a>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2025/08-august/wacom-with-arch.org][Wacom With Arch]] @@html:<span class="post-date">08-08-2025 00:00</span>@@ @@html:<a href="/tags/insights.html"> <span class="post-tag">insights</span> </a>@@
|
||||
- [[file:2025/08-august/what-do-i-want-to-do-with-emacs.org][What Do I Want To Do With Emacs]] @@html:<span class="post-date">08-08-2025 00:00</span>@@ @@html:<a href="/tags/emacs.html"> <span class="post-tag">emacs</span> </a>@@ @@html:<a href="/tags/review.html"> <span class="post-tag">review</span> </a>@@
|
||||
- [[file:2025/08-august/zettelkasten.org][Zettelkasten Method]] @@html:<span class="post-date">07-08-2025 00:00</span>@@ @@html:<a href="/tags/education.html"> <span class="post-tag">education</span> </a>@@
|
||||
- [[file:blogs-intro.org][Blogs Introduction]] @@html:<span class="post-date">06-08-2025 00:00</span>@@ @@html:<a href="/tags/introduction.html"> <span class="post-tag">introduction</span> </a>@@
|
||||
@@ -60,6 +60,7 @@
|
||||
<link rel=\"stylesheet\" href=\"/assets/styles/misc.css\" />
|
||||
<link rel=\"stylesheet\" href=\"/assets/styles/toc.css\" />
|
||||
<link rel=\"stylesheet\" href=\"/assets/styles/media.css\" />
|
||||
<link rel=\"stylesheet\" href=\"/assets/styles/wird-tracker.css\" />
|
||||
|
||||
<script src=\"/assets/scripts/script.js\" defer></script>
|
||||
<script src=\"/assets/scripts/lunr.js\" defer></script>
|
||||
@@ -71,6 +72,7 @@
|
||||
<script src=\"/assets/scripts/svg-pan-zoom.min.js\" defer></script>
|
||||
<script src=\"/assets/scripts/gallery-init.js\" defer></script>
|
||||
<script src=\"/assets/scripts/sitemap-interactive.js\" defer></script>
|
||||
<script src=\"/assets/scripts/wird-tracker.js\" defer></script>
|
||||
"
|
||||
)
|
||||
|
||||
@@ -96,8 +98,7 @@
|
||||
<a href=\"/\"> <img src=\"/assets/images/gr.png\" alt=\"Site Logo\" class=\"banner-logo\" /> </a>
|
||||
<nav>
|
||||
<a href=\"/\">Home | </a>
|
||||
<a href=\"/blogs/2026/2026-list.html \">2026 | </a>
|
||||
<a href=\"/blogs/blogs-list.html\">Blogs | </a>
|
||||
<a href=\"/blogs/blogs-list.html \">Blogs | </a>
|
||||
<a href=\"/posts/career/career-list.html\">Career | </a>
|
||||
<a href=\"https://zone.zainezq.com\">Dashboard</a>
|
||||
|
||||
@@ -401,7 +402,7 @@ A file has comments if:
|
||||
:sitemap-filename "blogs-list.org"
|
||||
:sitemap-title "Blogs List"
|
||||
:sitemap-style list
|
||||
:sitemap-function z/blogs-sitemap
|
||||
:sitemap-function z/blogs-grouped-sitemap
|
||||
:sitemap-sort-files anti-chronologically
|
||||
:html-head ,z-shared-head)
|
||||
|
||||
@@ -421,37 +422,39 @@ A file has comments if:
|
||||
:sitemap-sort-files anti-chronologically
|
||||
:html-head ,z-shared-head)
|
||||
|
||||
("org-2025"
|
||||
:base-directory ,(site-path "blogs/2025/")
|
||||
:publishing-directory ,(site-path "output/blogs/2025/")
|
||||
:recursive t
|
||||
:base-extension "org"
|
||||
:publishing-function z/z-publish-to-html
|
||||
:html-preamble ,z-preamble
|
||||
:html-postamble ,z-postamble
|
||||
:auto-sitemap t
|
||||
:sitemap-filename "2025-list.org"
|
||||
:sitemap-title "2025 List"
|
||||
:sitemap-style list
|
||||
:sitemap-function z/2025-sitemap
|
||||
:sitemap-sort-files anti-chronologically
|
||||
:html-head ,z-shared-head)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ("org-2025" ;;
|
||||
;; :base-directory ,(site-path "blogs/2025/") ;;
|
||||
;; :publishing-directory ,(site-path "output/blogs/2025/") ;;
|
||||
;; :recursive t ;;
|
||||
;; :base-extension "org" ;;
|
||||
;; :publishing-function z/z-publish-to-html ;;
|
||||
;; :html-preamble ,z-preamble ;;
|
||||
;; :html-postamble ,z-postamble ;;
|
||||
;; :auto-sitemap t ;;
|
||||
;; :sitemap-filename "2025-list.org" ;;
|
||||
;; :sitemap-title "2025 List" ;;
|
||||
;; :sitemap-style list ;;
|
||||
;; :sitemap-function z/2025-sitemap ;;
|
||||
;; :sitemap-sort-files anti-chronologically ;;
|
||||
;; :html-head ,z-shared-head) ;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
("org-2026"
|
||||
:base-directory ,(site-path "blogs/2026/")
|
||||
:publishing-directory ,(site-path "output/blogs/2026/")
|
||||
:recursive t
|
||||
:base-extension "org"
|
||||
:publishing-function z/z-publish-to-html
|
||||
:html-preamble ,z-preamble
|
||||
:html-postamble ,z-postamble
|
||||
:auto-sitemap t
|
||||
:sitemap-filename "2026-list.org"
|
||||
:sitemap-title "2026 List"
|
||||
:sitemap-style list
|
||||
:sitemap-function z/2026-sitemap
|
||||
:sitemap-sort-files anti-chronologically
|
||||
:html-head ,z-shared-head)
|
||||
;; ("org-2026"
|
||||
;; :base-directory ,(site-path "blogs/2026/")
|
||||
;; :publishing-directory ,(site-path "output/blogs/2026/")
|
||||
;; :recursive t
|
||||
;; :base-extension "org"
|
||||
;; :publishing-function z/z-publish-to-html
|
||||
;; :html-preamble ,z-preamble
|
||||
;; :html-postamble ,z-postamble
|
||||
;; :auto-sitemap t
|
||||
;; :sitemap-filename "2026-list.org"
|
||||
;; :sitemap-title "2026 List"
|
||||
;; :sitemap-style list
|
||||
;; :sitemap-function z/2026-sitemap
|
||||
;; :sitemap-sort-files anti-chronologically
|
||||
;; :html-head ,z-shared-head)
|
||||
|
||||
("org-career"
|
||||
:base-directory ,(site-path "posts/career/")
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
- [[file:../tags/emacs.org][@@html:<span class="post-tag">emacs</span>@@]] (2)
|
||||
- [[file:../tags/insights.org][@@html:<span class="post-tag">insights</span>@@]] (4)
|
||||
- [[file:../tags/introduction.org][@@html:<span class="post-tag">introduction</span>@@]] (3)
|
||||
- [[file:../tags/learning.org][@@html:<span class="post-tag">learning</span>@@]] (14)
|
||||
- [[file:../tags/life.org][@@html:<span class="post-tag">life</span>@@]] (4)
|
||||
- [[file:../tags/learning.org][@@html:<span class="post-tag">learning</span>@@]] (16)
|
||||
- [[file:../tags/life.org][@@html:<span class="post-tag">life</span>@@]] (8)
|
||||
- [[file:../tags/maths.org][@@html:<span class="post-tag">maths</span>@@]] (1)
|
||||
- [[file:../tags/notes.org][@@html:<span class="post-tag">notes</span>@@]] (15)
|
||||
- [[file:../tags/notes.org][@@html:<span class="post-tag">notes</span>@@]] (17)
|
||||
- [[file:../tags/reading.org][@@html:<span class="post-tag">reading</span>@@]] (1)
|
||||
- [[file:../tags/review.org][@@html:<span class="post-tag">review</span>@@]] (20)
|
||||
- [[file:../tags/review.org][@@html:<span class="post-tag">review</span>@@]] (22)
|
||||
- [[file:../tags/website.org][@@html:<span class="post-tag">website</span>@@]] (2)
|
||||
332
home/guide/wird-tracker-guide.org
Normal file
332
home/guide/wird-tracker-guide.org
Normal file
@@ -0,0 +1,332 @@
|
||||
#+TITLE: Wird Tracker — Technical Guide
|
||||
#+OPTIONS: toc:t num:t
|
||||
#+SLUG: wird-tracker-guide
|
||||
|
||||
* Overview
|
||||
|
||||
The wird tracker is a full-stack feature built on top of the existing
|
||||
org-publish site. It consists of four layers:
|
||||
|
||||
- *Database* — a PostgreSQL table (~wird_entries~) storing every log event
|
||||
- *Backend* — a Spring Boot controller (~WirdController~) exposing a REST API at ~/api/wird/~
|
||||
- *Frontend JS* — ~wird-tracker.js~ handles all rendering and API calls
|
||||
- *Org page* — ~wird-tracker.org~ defines the HTML structure via ~#+BEGIN_EXPORT html~
|
||||
|
||||
The page has no sidenotes (~#+NO_SIDENOTES: t~), so ~body.no-sidenotes~ in
|
||||
~styles.css~ automatically widens the content area. ~wird-tracker.css~
|
||||
complements ~styles.css~ and defers to its CSS variables, so dark mode
|
||||
works without any extra work.
|
||||
|
||||
* File Locations
|
||||
|
||||
| File | Where it lives | Purpose |
|
||||
|--------------------------+---------------------------------------------+----------------------------------|
|
||||
| ~wird-tracker.org~ | your org source directory | page structure, HTML injection |
|
||||
| ~wird-tracker.css~ | ~static/css/wird-tracker.css~ | component styles |
|
||||
| ~wird-tracker.js~ | ~static/js/wird-tracker.js~ | all frontend logic |
|
||||
| ~WirdEntry.java~ | ~src/.../model/WirdEntry.java~ | JPA entity |
|
||||
| ~CreateWirdEntryDTO.java~ | ~src/.../dto/CreateWirdEntryDTO.java~ | request body shape |
|
||||
| ~WirdEntryRepository.java~ | ~src/.../repository/WirdEntryRepository.java~ | Spring Data queries |
|
||||
| ~WirdService.java~ | ~src/.../service/WirdService.java~ | business logic |
|
||||
| ~WirdController.java~ | ~src/.../controller/WirdController.java~ | REST endpoints |
|
||||
| ~wird_schema.sql~ | wherever you keep your SQL scripts | initial DB setup |
|
||||
|
||||
* Database Schema
|
||||
|
||||
** Tables
|
||||
|
||||
The main table is ~wird_entries~. Each row is a single log event — not
|
||||
one row per day. This means you can log 200 durood in the morning and 300
|
||||
in the evening; they aggregate to 500 on the frontend.
|
||||
|
||||
#+BEGIN_SRC sql
|
||||
CREATE TABLE wird_entries (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
wird_type VARCHAR NOT NULL,
|
||||
date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
value NUMERIC(10,2) NOT NULL CHECK (value >= 0),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
#+END_SRC
|
||||
|
||||
The ~wird_targets~ table stores daily minimums per wird type. It has an
|
||||
~effective_from~ column so you can change targets over time without
|
||||
losing history.
|
||||
|
||||
#+BEGIN_SRC sql
|
||||
CREATE TABLE wird_targets (
|
||||
id SERIAL PRIMARY KEY,
|
||||
wird_type VARCHAR NOT NULL,
|
||||
target NUMERIC NOT NULL,
|
||||
effective_from DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
UNIQUE (wird_type, effective_from)
|
||||
);
|
||||
#+END_SRC
|
||||
|
||||
There is also a convenience view ~wird_daily_totals~ which aggregates
|
||||
entries per day per type. It is not queried by the backend currently but
|
||||
is useful for ad-hoc psql inspection.
|
||||
|
||||
** Note on the Enum
|
||||
|
||||
The schema was originally written with a PostgreSQL ~CREATE TYPE wird_type AS ENUM~.
|
||||
This was dropped in favour of plain ~VARCHAR~ because Hibernate 6 cannot
|
||||
bind a ~String~ to a Postgres enum column without a custom ~PGobject~
|
||||
converter, which itself has compatibility issues with Hibernate 6's type
|
||||
system. ~VARCHAR~ with application-level validation is simpler and
|
||||
equally safe.
|
||||
|
||||
* REST API
|
||||
|
||||
All endpoints are under ~/api/wird/~.
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------+-----------------------------+---------------------------------------------|
|
||||
| ~GET~ | ~/api/wird/entries~ | All entries, newest first |
|
||||
| ~GET~ | ~/api/wird/entries/today~ | Today's entries only |
|
||||
| ~GET~ | ~/api/wird/entries/range~ | Entries between ~?from=YYYY-MM-DD&to=...~ |
|
||||
| ~GET~ | ~/api/wird/entries/type/:t~ | Entries for one type in a date range |
|
||||
| ~POST~ | ~/api/wird/entries~ | Create a new entry |
|
||||
|
||||
The ~POST~ body shape is:
|
||||
|
||||
#+BEGIN_SRC json
|
||||
{
|
||||
"wirdType": "durood",
|
||||
"date": "2026-03-19",
|
||||
"value": 500,
|
||||
"notes": "after fajr"
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
All fields except ~notes~ are required. ~date~ defaults to today on the
|
||||
backend if omitted, but the frontend always sends it explicitly.
|
||||
|
||||
* Frontend Architecture
|
||||
|
||||
** Key constants
|
||||
|
||||
At the top of ~wird-tracker.js~ there are two objects you will edit most often:
|
||||
|
||||
#+BEGIN_SRC javascript
|
||||
const DAILY_WIRD = {
|
||||
durood: { label: "Durood", unit: "count", target: 500 },
|
||||
istighfar: { label: "Istighfar", unit: "count", target: 200 },
|
||||
quran: { label: "Qurʾān", unit: "juz", target: 3 },
|
||||
muraqabah: { label: "Murāqabah", unit: "min", target: 20 },
|
||||
wuqoof_qalbi: { label: "Wuqūf Qalbī", unit: "min", target: 15 },
|
||||
};
|
||||
|
||||
const MEETING_CYCLE_DAYS = 21;
|
||||
#+END_SRC
|
||||
|
||||
~DAILY_WIRD~ drives the today cards, the progress bar count, the trend
|
||||
chart dropdown, and the history table. ~WERD_META~ is a superset of
|
||||
~DAILY_WIRD~ that also includes ~shaykh_meeting~ — used for labelling
|
||||
history entries and formatting values.
|
||||
|
||||
** Rendering pipeline
|
||||
|
||||
On page load, the sequence is:
|
||||
|
||||
1. ~loadAll()~ — fetches ~/api/wird/entries~, populates ~allEntries~
|
||||
2. ~buildTodayMap()~ — aggregates today's entries into ~todayMap~ (type → total)
|
||||
3. ~buildMeetingLog()~ — filters ~allEntries~ to attended shaykh meetings
|
||||
4. ~renderToday()~ — draws the 5 wird cards and progress bar
|
||||
5. ~renderMeetingPanel()~ — draws the meeting status, next due date, cycle table
|
||||
6. ~renderHistory()~ — populates the history table (meetings excluded)
|
||||
7. ~renderChart()~ — draws the Chart.js trend line for the selected wird
|
||||
|
||||
After any ~POST~ (new entry), steps 2–7 all re-run so the page updates
|
||||
without a reload.
|
||||
|
||||
** Shaykh meeting cycle logic
|
||||
|
||||
~buildCycleInsights()~ works by anchoring cycles to the date of your very
|
||||
first logged meeting and walking forward in 21-day windows until today.
|
||||
For each window it checks whether any attended meeting falls within it.
|
||||
This means the cycle boundaries are stable — they do not shift when you
|
||||
log a new meeting. If you want cycles to reset from the most recent
|
||||
meeting instead, change the anchor line:
|
||||
|
||||
#+BEGIN_SRC javascript
|
||||
// Current: anchored to first ever meeting
|
||||
const first = allMeetingDates[0];
|
||||
|
||||
// Alternative: rolling window from most recent
|
||||
const first = addDays(allMeetingDates[allMeetingDates.length - 1], 0);
|
||||
#+END_SRC
|
||||
|
||||
* How To: Common Tasks
|
||||
|
||||
** Change a daily target
|
||||
|
||||
Targets are currently hard-coded in ~DAILY_WIRD~ in ~wird-tracker.js~.
|
||||
Change the ~target~ value for the relevant entry:
|
||||
|
||||
#+BEGIN_SRC javascript
|
||||
durood: { label: "Durood", unit: "count", target: 700 },
|
||||
#+END_SRC
|
||||
|
||||
If you want targets to come from the database instead (so you can change
|
||||
them without redeploying), the ~wird_targets~ table already supports this.
|
||||
You would need to add a ~/api/wird/targets~ endpoint in ~WirdController~
|
||||
and fetch it in ~loadAll()~, then replace the hard-coded ~target~ values
|
||||
with the fetched ones.
|
||||
|
||||
** Add a new wird type
|
||||
|
||||
There are four places to update:
|
||||
|
||||
1. *~wird-tracker.js~* — add an entry to ~DAILY_WIRD~:
|
||||
|
||||
#+BEGIN_SRC javascript
|
||||
tawbah: { label: "Tawbah", unit: "count", target: 100 },
|
||||
#+END_SRC
|
||||
|
||||
2. *~wird-tracker.org~* — add an ~<option>~ to the modal ~<select>~:
|
||||
|
||||
#+BEGIN_SRC html
|
||||
<option value="tawbah">Tawbah</option>
|
||||
#+END_SRC
|
||||
|
||||
3. *~wird-tracker.org~* — add an ~<option>~ to the trend chart ~<select>~:
|
||||
|
||||
#+BEGIN_SRC html
|
||||
<option value="tawbah">Tawbah</option>
|
||||
#+END_SRC
|
||||
|
||||
4. *~wird_targets~ table* — insert a default target (optional but tidy):
|
||||
|
||||
#+BEGIN_SRC sql
|
||||
INSERT INTO wird_targets (wird_type, target)
|
||||
VALUES ('tawbah', 100);
|
||||
#+END_SRC
|
||||
|
||||
No backend changes are needed — ~WirdController~ accepts any string as
|
||||
~wirdType~ and stores it as-is.
|
||||
|
||||
** Change the shaykh meeting cycle length
|
||||
|
||||
One line in ~wird-tracker.js~:
|
||||
|
||||
#+BEGIN_SRC javascript
|
||||
const MEETING_CYCLE_DAYS = 21; // change to e.g. 14
|
||||
#+END_SRC
|
||||
|
||||
** Edit or delete an entry
|
||||
|
||||
There is currently no edit/delete UI. You can do it directly in psql:
|
||||
|
||||
#+BEGIN_SRC sql
|
||||
-- Find the entry
|
||||
SELECT * FROM wird_entries
|
||||
WHERE wird_type = 'durood' AND date = '2026-03-19'
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- Delete by id
|
||||
DELETE FROM wird_entries WHERE id = 42;
|
||||
|
||||
-- Correct a value
|
||||
UPDATE wird_entries SET value = 350 WHERE id = 42;
|
||||
#+END_SRC
|
||||
|
||||
If you want a delete button in the UI, the backend needs a ~DELETE~
|
||||
endpoint:
|
||||
|
||||
#+BEGIN_SRC java
|
||||
@DeleteMapping("/entries/{id}")
|
||||
public ResponseEntity<Void> deleteEntry(@PathVariable Long id) {
|
||||
repo.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
Then in the JS, add a delete button to each history row and call:
|
||||
|
||||
#+BEGIN_SRC javascript
|
||||
await fetch(`${API}/entries/${id}`, { method: "DELETE" });
|
||||
#+END_SRC
|
||||
|
||||
** Inspect data directly
|
||||
|
||||
The ~wird_daily_totals~ view is useful for quick summaries:
|
||||
|
||||
#+BEGIN_SRC sql
|
||||
-- Today's totals
|
||||
SELECT wird_type, total, log_count
|
||||
FROM wird_daily_totals
|
||||
WHERE date = CURRENT_DATE;
|
||||
|
||||
-- Last 7 days of durood
|
||||
SELECT date, total
|
||||
FROM wird_daily_totals
|
||||
WHERE wird_type = 'durood'
|
||||
AND date >= CURRENT_DATE - INTERVAL '7 days'
|
||||
ORDER BY date DESC;
|
||||
|
||||
-- Check whether you met target each day
|
||||
SELECT date, wird_type, total,
|
||||
CASE WHEN total >= t.target THEN 'met' ELSE 'missed' END AS status
|
||||
FROM wird_daily_totals w
|
||||
JOIN wird_targets t USING (wird_type)
|
||||
WHERE t.effective_from = (
|
||||
SELECT MAX(effective_from) FROM wird_targets t2
|
||||
WHERE t2.wird_type = w.wird_type
|
||||
AND t2.effective_from <= w.date
|
||||
)
|
||||
ORDER BY date DESC, wird_type;
|
||||
#+END_SRC
|
||||
|
||||
* Extensibility Notes
|
||||
|
||||
** Adding a weekly/monthly summary endpoint
|
||||
|
||||
The backend is structured to make this easy. Add a method to
|
||||
~WirdEntryRepository~ using a ~@Query~ and expose it via a new
|
||||
~@GetMapping~ in ~WirdController~. The JS can then call it and render
|
||||
an additional panel without touching anything else.
|
||||
|
||||
** Making targets configurable via the DB
|
||||
|
||||
The ~wird_targets~ table already has ~effective_from~, which means you
|
||||
can version targets over time. A query like the one in the last section
|
||||
above shows the pattern for joining targets to entries correctly —
|
||||
finding the most recent target that was in effect on a given date.
|
||||
|
||||
** Adding authentication
|
||||
|
||||
Currently the API has no auth — it is assumed the page is on a
|
||||
personal/private site. If you ever need to restrict writes, the cleanest
|
||||
approach given the existing Spring Boot setup is to add a simple API key
|
||||
check in a ~HandlerInterceptor~ that only applies to ~POST~ and ~DELETE~
|
||||
methods on ~/api/wird/~.
|
||||
|
||||
** Porting the frontend to a proper framework
|
||||
|
||||
The JS is a self-contained IIFE with no build step, which suits the
|
||||
org-publish workflow. If you ever move to a build pipeline, the logic
|
||||
maps cleanly onto a React component tree:
|
||||
~<TodayPanel>~, ~<MeetingPanel>~, ~<TrendChart>~, ~<HistoryTable>~ — each
|
||||
taking ~allEntries~ as a prop and deriving their state from it.
|
||||
|
||||
* Deployment Checklist
|
||||
|
||||
When you deploy changes, the steps depend on what you changed:
|
||||
|
||||
| Changed file | Action needed |
|
||||
|------------------------+------------------------------------------------------------|
|
||||
| ~wird-tracker.org~ | Re-run org-publish; the HTML will be regenerated |
|
||||
| ~wird-tracker.css~ | Copy to ~static/css/~; hard-refresh browser cache |
|
||||
| ~wird-tracker.js~ | Copy to ~static/js/~; hard-refresh browser cache |
|
||||
| Any ~*.java~ file | Rebuild and restart the Spring Boot jar |
|
||||
| SQL schema changes | Run the migration manually in psql; restart Spring Boot |
|
||||
|
||||
For CSS/JS cache busting during development, append a query string to
|
||||
the ~<link>~ and ~<script>~ tags in the ~#+BEGIN_EXPORT html~ block:
|
||||
|
||||
#+BEGIN_SRC html
|
||||
<link rel="stylesheet" href="/css/wird-tracker.css?v=2">
|
||||
<script src="/js/wird-tracker.js?v=2"></script>
|
||||
#+END_SRC
|
||||
@@ -2,7 +2,18 @@
|
||||
#+OPTIONS: toc:nil num:nil
|
||||
|
||||
* Recently Updated (top 26 files - per lima's request)
|
||||
- [[file:posts/career/restful-api.org][Restful API]] @@html:<span class="post-date">2026-03-08 17:23</span>@@
|
||||
- [[file:home/wird-tracker.org][Wird Tracker]] @@html:<span class="post-date">2026-03-19 16:05</span>@@
|
||||
- [[file:blogs/2026/03-march/setting-a-wird-tracker-19-03.org][Setting a Wird Tracker]] @@html:<span class="post-date">2026-03-19 13:57</span>@@
|
||||
- [[file:index.org][Home Page]] @@html:<span class="post-date">2026-03-19 13:10</span>@@
|
||||
- [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]] @@html:<span class="post-date">2026-03-19 12:43</span>@@
|
||||
- [[file:blogs/2026/03-march/fixing-the-dag-18-03.org][DAG fixes]] @@html:<span class="post-date">2026-03-19 10:42</span>@@
|
||||
- [[file:blogs/2026/03-march/feeling-sleepy.org][Feeling extremely sleepy]] @@html:<span class="post-date">2026-03-19 10:41</span>@@
|
||||
- [[file:blogs/2026/03-march/oversleeping-16-03.org][Oversleeping and missing a meeting...]] @@html:<span class="post-date">2026-03-16 12:17</span>@@
|
||||
- [[file:blogs/2026/03-march/15-03-week-review.org][[15-03-2026] - Weekly Review]] @@html:<span class="post-date">2026-03-12 16:31</span>@@
|
||||
- [[file:blogs/2026/03-march/08-03-week-review.org][[08-03-2026] - Weekly Review]] @@html:<span class="post-date">2026-03-12 16:17</span>@@
|
||||
- [[file:posts/career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">2026-03-11 17:20</span>@@
|
||||
- [[file:posts/career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">2026-03-11 17:19</span>@@
|
||||
- [[file:posts/career/restful-api.org][Restful API]] @@html:<span class="post-date">2026-03-08 17:37</span>@@
|
||||
- [[file:home/status.org][Competency Status Board]] @@html:<span class="post-date">2026-03-08 16:28</span>@@
|
||||
- [[file:blogs/2026/02-february/27-02-26.org][Journeys rambles again...]] @@html:<span class="post-date">2026-03-07 16:33</span>@@
|
||||
- [[file:blogs/2026/03-march/01-03-week-review.org][[01-03-2026] - Weekly Review]] @@html:<span class="post-date">2026-03-07 12:45</span>@@
|
||||
@@ -14,17 +25,6 @@
|
||||
- [[file:blogs/2026/02-february/26-02-26.org][Starting the Journeys Upgrade]] @@html:<span class="post-date">2026-02-26 17:37</span>@@
|
||||
- [[file:blogs/2026/02-february/24-02-26.org][Integration tests failing (sob)]] @@html:<span class="post-date">2026-02-24 17:00</span>@@
|
||||
- [[file:home/backlog.org][Backlog]] @@html:<span class="post-date">2026-02-23 16:40</span>@@
|
||||
- [[file:index.org][Home Page]] @@html:<span class="post-date">2026-02-23 12:37</span>@@
|
||||
- [[file:blogs/2026/02-february/15-02-week-review.org][[15-02-2026] - Weekly Review]] @@html:<span class="post-date">2026-02-23 10:02</span>@@
|
||||
- [[file:home/services.org][Service]] @@html:<span class="post-date">2026-02-11 13:26</span>@@
|
||||
- [[file:blogs/2026/02-february/08-02-week-review.org][[08-02-2026] - Weekly Review]] @@html:<span class="post-date">2026-02-11 12:20</span>@@
|
||||
- [[file:blogs/2026/02-february/01-02-week-review.org][[01-02-2026] - Weekly Review]] @@html:<span class="post-date">2026-02-11 12:20</span>@@
|
||||
- [[file:blogs/2026/01-january/25-01-week-review.org][[25-01-2026] - Weekly Review]] @@html:<span class="post-date">2026-02-11 11:43</span>@@
|
||||
- [[file:blogs/2026/01-january/18-01-week-review.org][[18-01-2026] - Weekly Review]] @@html:<span class="post-date">2026-01-18 23:05</span>@@
|
||||
- [[file:blogs/2026/01-january/11-01-week-review.org][[11-01-2026] - Weekly Review]] @@html:<span class="post-date">2026-01-17 21:23</span>@@
|
||||
- [[file:posts/career/airflow.org][Datamarts, Airflow and DAG's]] @@html:<span class="post-date">2026-01-17 21:17</span>@@
|
||||
- [[file:blogs/2025/12-december/21-12-week-review.org][[21-12-2025] - Weekly Review]] @@html:<span class="post-date">2026-01-17 21:09</span>@@
|
||||
- [[file:blogs/2025/12-december/28-12-week-review.org][[28-12-2025] - Weekly Review]] @@html:<span class="post-date">2026-01-17 21:08</span>@@
|
||||
- [[file:blogs/2026/01-january/04-01-week-review.org][[04-01-2026] - Weekly Review]] @@html:<span class="post-date">2026-01-17 21:08</span>@@
|
||||
- [[file:posts/career/probation-objectives.org][Probation Objectives:]] @@html:<span class="post-date">2026-01-17 21:06</span>@@
|
||||
- [[file:posts/career/invest-principles.org][Invest Principles]] @@html:<span class="post-date">2026-01-04 17:15</span>@@
|
||||
159
home/wird-tracker.org
Normal file
159
home/wird-tracker.org
Normal file
@@ -0,0 +1,159 @@
|
||||
#+TITLE: Wird Tracker
|
||||
#+OPTIONS: toc:nil num:nil
|
||||
#+NO_SIDENOTES: t
|
||||
#+COMMENTS: nil
|
||||
#+SLUG: wird-tracker
|
||||
|
||||
This page tracks my daily awrād and spiritual practices.
|
||||
|
||||
#+BEGIN_EXPORT html
|
||||
|
||||
<div id="wird-app">
|
||||
|
||||
<!-- Today Panel -->
|
||||
<section id="today-panel" class="wird-panel">
|
||||
<div class="wird-panel-header">
|
||||
<h2 id="today-date">—</h2>
|
||||
<div id="today-progress-wrap">
|
||||
<div id="today-progress-bar"><div id="today-progress-fill"></div></div>
|
||||
<span id="today-progress-label">0 / 5 complete</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wird-cards"></div>
|
||||
</section>
|
||||
|
||||
<!-- Shaykh Meeting Panel -->
|
||||
<section id="meeting-panel" class="wird-panel">
|
||||
<div class="wird-panel-header">
|
||||
<h2>Meeting with the Shaykh</h2>
|
||||
<button id="log-meeting-btn" class="btn-primary">+ Log Meeting</button>
|
||||
</div>
|
||||
<div id="meeting-panel-body">
|
||||
<p style="color:var(--muted);font-style:italic">Loading…</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Log Entry Modal -->
|
||||
<dialog id="log-modal">
|
||||
<form id="log-form" method="dialog">
|
||||
<h3 id="modal-title">Log Entry</h3>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="log-date">Date</label>
|
||||
<input type="date" id="log-date" required>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="log-type">Wird</label>
|
||||
<select id="log-type" required>
|
||||
<option value="durood">Durood</option>
|
||||
<option value="istighfar">Istighfar</option>
|
||||
<option value="quran">Qurʾān (juz)</option>
|
||||
<option value="muraqabah">Murāqabah (min)</option>
|
||||
<option value="wuqoof_qalbi">Wuqūf Qalbī</option>
|
||||
<option value="shaykh_meeting" style="display:none">Meeting with Shaykh</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Numeric amount — count/juz/min wirds -->
|
||||
<div class="field-group" id="value-group">
|
||||
<label for="log-value">Amount</label>
|
||||
<input type="number" id="log-value" min="0" step="0.5" placeholder="0">
|
||||
</div>
|
||||
|
||||
<!-- Rating picker — wuqoof qalbi only -->
|
||||
<div class="field-group" id="rating-group" style="display:none">
|
||||
<label>Quality of presence</label>
|
||||
<div class="modal-pips" id="modal-pips">
|
||||
<button type="button" class="modal-pip" data-val="1">
|
||||
<span class="pip-dot"></span>
|
||||
<span class="pip-lbl">distracted</span>
|
||||
</button>
|
||||
<button type="button" class="modal-pip" data-val="2">
|
||||
<span class="pip-dot"></span>
|
||||
<span class="pip-lbl">scattered</span>
|
||||
</button>
|
||||
<button type="button" class="modal-pip" data-val="3">
|
||||
<span class="pip-dot"></span>
|
||||
<span class="pip-lbl">present</span>
|
||||
</button>
|
||||
<button type="button" class="modal-pip" data-val="4">
|
||||
<span class="pip-dot"></span>
|
||||
<span class="pip-lbl">attentive</span>
|
||||
</button>
|
||||
<button type="button" class="modal-pip" data-val="5">
|
||||
<span class="pip-dot"></span>
|
||||
<span class="pip-lbl">absorbed</span>
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" id="log-rating-value">
|
||||
</div>
|
||||
|
||||
<!-- Shaykh meeting note -->
|
||||
<div class="field-group" id="shaykh-group" style="display:none">
|
||||
<p style="font-size:.82rem;color:var(--muted);margin:0">Set the date above to when the meeting took place.</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="log-notes">Notes <span class="optional">(optional)</span></label>
|
||||
<textarea id="log-notes" rows="2" placeholder="Any reflections…"></textarea>
|
||||
</div>
|
||||
|
||||
<div id="modal-actions">
|
||||
<button type="button" id="modal-cancel">Cancel</button>
|
||||
<button type="submit" id="modal-save" class="btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Trends -->
|
||||
<section id="trends-panel" class="wird-panel">
|
||||
<div class="wird-panel-header">
|
||||
<h2>Trends</h2>
|
||||
<div id="trend-controls">
|
||||
<select id="trend-type">
|
||||
<option value="durood">Durood</option>
|
||||
<option value="istighfar">Istighfar</option>
|
||||
<option value="quran">Qurʾān</option>
|
||||
<option value="muraqabah">Murāqabah</option>
|
||||
<option value="wuqoof_qalbi">Wuqūf Qalbī</option>
|
||||
</select>
|
||||
<select id="trend-range">
|
||||
<option value="14">14 days</option>
|
||||
<option value="30" selected>30 days</option>
|
||||
<option value="90">90 days</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="trend-chart"></canvas>
|
||||
<div id="trend-stats"></div>
|
||||
</section>
|
||||
|
||||
<!-- History -->
|
||||
<section id="history-panel" class="wird-panel">
|
||||
<div class="wird-panel-header">
|
||||
<h2>History</h2>
|
||||
<button id="open-log-btn" class="btn-primary">+ Log Entry</button>
|
||||
</div>
|
||||
<div id="history-table-wrap">
|
||||
<table id="history-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Wird</th>
|
||||
<th>Amount</th>
|
||||
<th>vs Target</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="history-body">
|
||||
<tr><td colspan="5" class="loading-cell">Loading…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
#+END_EXPORT
|
||||
12
index.org
12
index.org
@@ -16,19 +16,13 @@ Feel free to explore:
|
||||
|
||||
- [[file:posts/career/career-list.org][Career Page]]
|
||||
|
||||
- [[file:blogs/2026/2026-list.org][2026]]
|
||||
|
||||
- [[file:blogs/2025/2025-list.org][2025]]
|
||||
|
||||
** Blogs and Posts
|
||||
|
||||
- [[file:blogs/blogs-list.org][Blogs Page]]
|
||||
|
||||
- [[file:posts/posts-list.org][Posts Page]]
|
||||
|
||||
** Setup Page
|
||||
** Wird Page
|
||||
|
||||
- [[file:home/setup.org][Setup Page]]
|
||||
- [[file:home/wird-tracker.org][Wird Page]]
|
||||
|
||||
** Sitemap
|
||||
|
||||
@@ -42,5 +36,5 @@ Feel free to explore:
|
||||
|
||||
- [[file:home/backlog.org][Backlog Page]]
|
||||
|
||||
- [[file:recently-updated.org][Recent Updates Page]]
|
||||
- [[file:home/recently-updated.org][Recent Updates Page]]
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ Defaults to N=30."
|
||||
;; Trim
|
||||
(setq items (cl-subseq items 0 (min count (length items))))
|
||||
;; Write Org file
|
||||
(with-temp-file (expand-file-name "recently-updated.org" z-org-root)
|
||||
(with-temp-file (site-path "home/recently-updated.org")
|
||||
(insert "#+TITLE: Recently Updated\n"
|
||||
"#+OPTIONS: toc:nil num:nil\n\n"
|
||||
"* Recently Updated (top 26 files - per lima's request)\n")
|
||||
|
||||
@@ -81,6 +81,92 @@
|
||||
"\n")))
|
||||
|
||||
|
||||
|
||||
(defun z/blogs-grouped-sitemap (title list)
|
||||
"Sitemap grouped by year and month with dates and FILETAGS."
|
||||
(let ((data (make-hash-table :test 'equal)))
|
||||
|
||||
;; STEP 1: Collect entries into (year -> month -> entries)
|
||||
(dolist (entry (cdr list))
|
||||
(when (consp entry)
|
||||
(let* ((link (car entry))
|
||||
(filename (if (string-match "\\[\\[file:\\([^]]+\\)\\]" link)
|
||||
(match-string 1 link)
|
||||
link))
|
||||
(full-path (expand-file-name filename (site-path "blogs/")))
|
||||
(date (when (file-exists-p full-path)
|
||||
(org-publish-find-date full-path org-publish-project-alist))))
|
||||
|
||||
(when date
|
||||
(let* ((year (format-time-string "%Y" date))
|
||||
(month (format-time-string "%B %Y" date))
|
||||
(date-str (format-time-string "%d-%m-%Y %H:%M" date))
|
||||
(tags-str ""))
|
||||
|
||||
;; Get tags
|
||||
(when (file-exists-p full-path)
|
||||
(with-temp-buffer
|
||||
(insert-file-contents full-path)
|
||||
(org-mode)
|
||||
(let ((tags (cadr (assoc "FILETAGS"
|
||||
(org-collect-keywords '("FILETAGS"))))))
|
||||
(when tags
|
||||
(setq tags-str
|
||||
(mapconcat
|
||||
(lambda (tag)
|
||||
(format "@@html:<a href=\"/tags/%s.html\"> \
|
||||
<span class=\"post-tag\">%s</span> </a>@@" tag tag))
|
||||
(split-string tags ":" t)
|
||||
" "))))))
|
||||
|
||||
;; Insert into hash table
|
||||
(let ((year-table (or (gethash year data)
|
||||
(puthash year (make-hash-table :test 'equal) data))))
|
||||
(let ((month-list (gethash month year-table)))
|
||||
(puthash month
|
||||
(cons (list link date-str tags-str date)
|
||||
month-list)
|
||||
year-table))))))))
|
||||
|
||||
;; STEP 2: Render output
|
||||
(let ((output (concat
|
||||
"#+TITLE: " title "\n"
|
||||
"#+OPTIONS: toc:nil num:nil \n\n"
|
||||
"See the categories: @@html:<a href=\"../home/categories.html\">Categories</a>@@\n\n")))
|
||||
|
||||
;; Sort years descending
|
||||
(dolist (year (sort (hash-table-keys data) #'string>))
|
||||
(setq output (concat output "* " year "\n"))
|
||||
|
||||
(let ((year-table (gethash year data)))
|
||||
|
||||
;; Sort months by actual date (descending)
|
||||
(dolist (month
|
||||
(sort (hash-table-keys year-table)
|
||||
(lambda (a b)
|
||||
(time-less-p
|
||||
(date-to-time (concat "01 " b))
|
||||
(date-to-time (concat "01 " a))))))
|
||||
|
||||
(setq output (concat output "\n** " month "\n"))
|
||||
|
||||
;; Sort entries by date descending
|
||||
(dolist (entry
|
||||
(sort (gethash month year-table)
|
||||
(lambda (a b)
|
||||
(time-less-p (nth 3 b) (nth 3 a)))))
|
||||
|
||||
(let ((link (nth 0 entry))
|
||||
(date-str (nth 1 entry))
|
||||
(tags-str (nth 2 entry)))
|
||||
|
||||
(setq output
|
||||
(concat output
|
||||
(format "- %s @@html:<span class=\"post-date\">%s</span>@@ %s\n"
|
||||
link date-str tags-str))))))))
|
||||
|
||||
output)))
|
||||
|
||||
(defun z/books-sitemap (title list)
|
||||
"sitemap that lists books."
|
||||
(concat
|
||||
|
||||
1462
org-web-build.log
1462
org-web-build.log
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,10 @@ See the categories: @@html:<a href="../../home/categories.html">Categories</a>@@
|
||||
|
||||
See the following page for more details: @@html:<a href="./career-intro.html">Career Intro</a>@@
|
||||
|
||||
** March 2026
|
||||
- [[file: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: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>@@
|
||||
|
||||
** February 2026
|
||||
- [[file:restful-api.org][Restful API]] @@html:<span class="post-date">15-02-2026 23:00</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>@@
|
||||
|
||||
|
||||
87
posts/career/ha-dr.org
Normal file
87
posts/career/ha-dr.org
Normal file
@@ -0,0 +1,87 @@
|
||||
#+TITLE: High Availability, Disaster Recovery and Business Continuity
|
||||
#+OPTIONS: num:nil
|
||||
#+DATE: <2026-03-11 Wed 17:18>
|
||||
#+filetags: :learning:notes:
|
||||
#+WIP:
|
||||
#+COMMENTS: t
|
||||
#+SLUG: high-availability-disaster-recovery
|
||||
|
||||
* HA, DR and BC Competency Log
|
||||
Understanding High Availability (HA), Disaster Recovery (DR), and
|
||||
Business Continuity Planning (BCP) is important when designing reliable
|
||||
systems and analysing service incidents. These concepts help Microlise
|
||||
(and organisations in general) to minimise downtime, recover from
|
||||
failures, and maintain service availability.
|
||||
|
||||
*** High Availability (HA)
|
||||
High Availability focuses on preventing service disruption by using
|
||||
redundancy. Systems are designed with backup components so that if one
|
||||
fails, another can take over automatically.
|
||||
|
||||
Examples include:
|
||||
|
||||
- Redundant servers or network paths
|
||||
- Load balancing across multiple systems
|
||||
- Automatic failover modes
|
||||
|
||||
This ensures services continue operating even when individual components
|
||||
fail. We have this at Microlise where if there is a failure on one of
|
||||
the data centres, we can failover to the other one.
|
||||
|
||||
An analogy from the article compares this to a bicycle with two brakes.
|
||||
The bike can operate with only one brake, but having two provides
|
||||
redundancy in case one fails.
|
||||
|
||||
*** Disaster Recovery (DR)
|
||||
Disaster Recovery focuses on restoring systems after a major failure.
|
||||
This includes recovering infrastructure, applications, and data so
|
||||
services can resume operation.
|
||||
|
||||
Some of the common DR tools and methods include:
|
||||
|
||||
- Offsite backups
|
||||
- Replicated environments (e.g., multiple data centres with sync)
|
||||
- Recovery procedures and restoration tools
|
||||
|
||||
Unlike High Availability, which tries to prevent downtime, DR assumes a
|
||||
failure has already occurred. In the bicycle analogy, after the crash
|
||||
the rider takes a bus home and then drives to work to complete their
|
||||
important task. This represents a recovery process after a major
|
||||
disruption.
|
||||
|
||||
*** Business Continuity Planning (BCP)
|
||||
BCP ensures the business can continue operating during or after a
|
||||
disruption. This may involve alternative systems, temporary processes,
|
||||
or backup locations to keep services running.
|
||||
|
||||
In the analogy, the car at home represents BCP because it allows the
|
||||
rider to continue their journey despite the broken bicycle. A Business
|
||||
Impact Assessment (BIA) helps determine the priority of services and the
|
||||
acceptable level of downtime.
|
||||
|
||||
*** RTO and RPO
|
||||
Two key recovery metrics are:
|
||||
|
||||
- Recovery Time Objective (RTO): The maximum time allowed to restore a
|
||||
service after failure. For example, if a critical service must be
|
||||
restored within 4 hours, the disaster recovery process must ensure
|
||||
systems are operational within that timeframe.
|
||||
- Recovery Point Objective (RPO): The maximum acceptable amount of data
|
||||
loss.
|
||||
|
||||
RPO example:
|
||||
|
||||
- An RPO of 0 minutes means no data loss is acceptable.
|
||||
- An RPO of 1 hour means up to one hour of data could be lost.
|
||||
|
||||
These values help define the required level of HA and DR design.
|
||||
|
||||
*** Application in Real Scenarios
|
||||
These concepts are useful when performing incident debugging, root cause
|
||||
analysis, or explaining service issues to customer support teams. For
|
||||
example, engineers may check whether failover worked correctly, whether
|
||||
recovery met the RTO, or whether backups allowed data to be restored
|
||||
within the RPO.
|
||||
|
||||
In summary, the reason why we need to understand HA, DR, and BCP is to
|
||||
maintain service availability and ensure business continuity.
|
||||
92
posts/career/javascript.org
Normal file
92
posts/career/javascript.org
Normal file
@@ -0,0 +1,92 @@
|
||||
#+TITLE: Understands the Javascript language
|
||||
#+OPTIONS: num:nil
|
||||
#+DATE: <2026-03-11 Wed 16:52>
|
||||
#+filetags: :learning:notes:
|
||||
#+WIP:
|
||||
#+COMMENTS: t
|
||||
#+SLUG: understands-the-javascript-language
|
||||
|
||||
* JS - Competency Log
|
||||
To evidence this competency I wanted to use a comment system that I
|
||||
implemented for my website. The goal was to fetch comments from an API,
|
||||
render them dynamically and support nested replies.
|
||||
|
||||
** Working with the DOM
|
||||
A key part of front end JS development is manipulating the DOM (Document
|
||||
Object Model). I dynamically created comment elements using
|
||||
=document.createElement= rather than injecting raw HTML. This approach
|
||||
avoids security issues such as cross-site scripting (XSS).
|
||||
|
||||
As an example:
|
||||
|
||||
#+begin_src js
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "comment";
|
||||
|
||||
const author = document.createElement("strong");
|
||||
author.textContent = comment.author || "Anonymous";
|
||||
#+end_src
|
||||
|
||||
Using textContent ensures that any user submitted content is safely
|
||||
rendered as text instead of HTML.
|
||||
|
||||
** Data structures
|
||||
Comments returned from the backend are stored as a flat array, but
|
||||
replies must be displayed as a nested tree structure. To solve this, I
|
||||
implemented the buildCommentTree function.
|
||||
|
||||
#+begin_src js
|
||||
const byId = {};
|
||||
const roots = [];
|
||||
#+end_src
|
||||
|
||||
I use the JS object =byId= as a lookup table (O(1) complexity) to access
|
||||
comments by ID. This ensures that I can quickly find the parent of a
|
||||
comment. I then use the roots array to store the top-level comments,
|
||||
which are the ones without a parent ID.
|
||||
|
||||
#+begin_src js
|
||||
const parent = byId[comment.parent_id];
|
||||
if (parent) parent.children.push(comment);
|
||||
#+end_src
|
||||
|
||||
** Async programming
|
||||
Modern front-end applications frequently communicate with APIs. I used
|
||||
async/await to handle asynchronous operations when fetching and posting
|
||||
comments.
|
||||
|
||||
#+begin_src js
|
||||
async function fetchComments() {
|
||||
const res = await fetch(`/api/comments/${pageSlug}`);
|
||||
return await res.json();
|
||||
}
|
||||
#+end_src
|
||||
|
||||
I used async/await instead of promise chaining to make the code more
|
||||
readable and easier to understand. I also added some response
|
||||
validations:
|
||||
|
||||
#+begin_src js
|
||||
if (!res.ok) {
|
||||
console.error("Failed to fetch comments");
|
||||
}
|
||||
#+end_src
|
||||
|
||||
** Event Handling
|
||||
JS event handling was used to respond to user interactions, such as
|
||||
submitting a comment or replying.
|
||||
|
||||
#+begin_src js
|
||||
form.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
#+end_src
|
||||
|
||||
Calling preventDefault() prevents the browser from reloading the page
|
||||
during form submission, allowing the comment system to update
|
||||
dynamically instead.
|
||||
|
||||
Also the same for replies:
|
||||
|
||||
#+begin_src js
|
||||
replyBtn.onclick = () => showReplyForm(wrapper, Number(comment.id));
|
||||
#+end_src
|
||||
@@ -4,7 +4,9 @@
|
||||
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
||||
|
||||
* Posts:
|
||||
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">08-03-2026 17:34</span>@@
|
||||
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">19-03-2026 16:17</span>@@
|
||||
- [[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/restful-api.org][Restful API]] @@html:<span class="post-date">15-02-2026 23:00</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/database-permissions.org][Database Permissions, Roles, and Accounts]] @@html:<span class="post-date">18-01-2026 23:00</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/monitoring-and-logging.org][Monitoring and Logging]] @@html:<span class="post-date">18-01-2026 23:00</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>@@
|
||||
|
||||
70
sitemap.org
70
sitemap.org
@@ -2,16 +2,6 @@
|
||||
|
||||
- [[file:index.org][Home Page]]
|
||||
- [[file:wip.org][Work in progress]]
|
||||
- [[file:recently-updated.org][Recently Updated]]
|
||||
- home
|
||||
- [[file:home/countdown.org][Countdown]]
|
||||
- [[file:home/contact.org][Contact]]
|
||||
- [[file:home/backlog.org][Backlog]]
|
||||
- [[file:home/setup.org][Setup]]
|
||||
- [[file:home/notes.org][Notes]]
|
||||
- [[file:home/services.org][Service]]
|
||||
- [[file:home/status.org][Competency Status Board]]
|
||||
- [[file:home/categories.org][Categories]]
|
||||
- posts
|
||||
- [[file:posts/posts-intro.org][Posts Introduction]]
|
||||
- [[file:posts/posts-list.org][Posts List]]
|
||||
@@ -32,50 +22,13 @@
|
||||
- [[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/restful-api.org][Restful API]]
|
||||
- [[file:posts/career/javascript.org][Understands the Javascript language]]
|
||||
- [[file:posts/career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]]
|
||||
- [[file:posts/career/career-list.org][Career List]]
|
||||
- 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]]
|
||||
- 2025
|
||||
- [[file:blogs/2025/2025-list.org][2025 List]]
|
||||
- 11-november
|
||||
- [[file:blogs/2025/11-november/02-11-week-review.org][[02-11-2025] - Weekly Review]]
|
||||
- [[file:blogs/2025/11-november/09-11-week-review.org][[09-11-2025] - Weekly Review]]
|
||||
- [[file:blogs/2025/11-november/16-11-week-review.org][[16-11-2025] - Weekly Review]]
|
||||
- [[file:blogs/2025/11-november/23-11-week-review.org][[23-11-2025] - Weekly Review]]
|
||||
- [[file:blogs/2025/11-november/30-11-week-review.org][[30-11-2025] - Weekly Review]]
|
||||
- 08-august
|
||||
- [[file:blogs/2025/08-august/zettelkasten.org][Zettelkasten Method]]
|
||||
- [[file:blogs/2025/08-august/what-do-i-want-to-do-with-emacs.org][What Do I Want To Do With Emacs]]
|
||||
- [[file:blogs/2025/08-august/wacom-with-arch.org][Wacom With Arch]]
|
||||
- [[file:blogs/2025/08-august/spending-the-whole-day-on-this-website.org][09-08-2025: Website Changes]]
|
||||
- [[file:blogs/2025/08-august/hilberts.hotel.org][Hilbert's Hotel]]
|
||||
- [[file:blogs/2025/08-august/benefits-of-reading.org][Benefits of Reading]]
|
||||
- [[file:blogs/2025/08-august/third-time.org][Third Time]]
|
||||
- 12-december
|
||||
- [[file:blogs/2025/12-december/07-12-week-review.org][[07-12-2025] - Weekly Review]]
|
||||
- [[file:blogs/2025/12-december/14-12-week-review.org][[14-12-2025] - Weekly Review]]
|
||||
- [[file:blogs/2025/12-december/21-12-week-review.org][[21-12-2025] - Weekly Review]]
|
||||
- [[file:blogs/2025/12-december/28-12-week-review.org][[28-12-2025] - Weekly Review]]
|
||||
- 2026
|
||||
- [[file:blogs/2026/2026-list.org][2026 List]]
|
||||
- 01-january
|
||||
- [[file:blogs/2026/01-january/04-01-week-review.org][[04-01-2026] - Weekly Review]]
|
||||
- [[file:blogs/2026/01-january/11-01-week-review.org][[11-01-2026] - Weekly Review]]
|
||||
- [[file:blogs/2026/01-january/18-01-week-review.org][[18-01-2026] - Weekly Review]]
|
||||
- [[file:blogs/2026/01-january/25-01-week-review.org][[25-01-2026] - Weekly Review]]
|
||||
- 03-march
|
||||
- [[file:blogs/2026/03-march/01-03-week-review.org][[01-03-2026] - Weekly Review]]
|
||||
- 02-february
|
||||
- [[file:blogs/2026/02-february/01-02-week-review.org][[01-02-2026] - Weekly Review]]
|
||||
- [[file:blogs/2026/02-february/third-meeting.org][Third Meeting with lima :)]]
|
||||
- [[file:blogs/2026/02-february/08-02-week-review.org][[08-02-2026] - Weekly Review]]
|
||||
- [[file:blogs/2026/02-february/15-02-week-review.org][[15-02-2026] - Weekly Review]]
|
||||
- [[file:blogs/2026/02-february/22-02-week-review.org][[22-02-2026] - Weekly Review]]
|
||||
- [[file:blogs/2026/02-february/24-02-26.org][Integration tests failing (sob)]]
|
||||
- [[file:blogs/2026/02-february/26-02-26.org][Starting the Journeys Upgrade]]
|
||||
- [[file:blogs/2026/02-february/27-02-26.org][Journeys rambles again...]]
|
||||
- books
|
||||
- [[file:books/books-list.org][Books List]]
|
||||
- clean-code
|
||||
@@ -87,10 +40,23 @@
|
||||
- [[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/education.org][Tag: education]]
|
||||
- [[file:tags/insights.org][Tag: insights]]
|
||||
- [[file:tags/life.org][Tag: life]]
|
||||
- [[file:tags/website.org][Tag: website]]
|
||||
- [[file:tags/emacs.org][Tag: emacs]]
|
||||
- [[file:tags/maths.org][Tag: maths]]
|
||||
- [[file:tags/education.org][Tag: education]]
|
||||
- [[file:tags/reading.org][Tag: reading]]
|
||||
- [[file:tags/maths.org][Tag: maths]]
|
||||
- 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/categories.org][Categories]]
|
||||
- [[file:home/recently-updated.org][Recently Updated]]
|
||||
- guide
|
||||
- [[file:home/guide/setup.org][Setup]]
|
||||
- [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]]
|
||||
@@ -2,6 +2,8 @@
|
||||
#+OPTIONS: toc:nil num:nil title:nil
|
||||
|
||||
* Posts tagged learning
|
||||
- [[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]]
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
#+OPTIONS: toc:nil num:nil title:nil
|
||||
|
||||
* Posts tagged life
|
||||
- [[file:../blogs/2026/03-march/setting-a-wird-tracker-19-03.org][Setting a Wird Tracker]]
|
||||
- [[file:../blogs/2026/03-march/fixing-the-dag-18-03.org][DAG fixes]]
|
||||
- [[file:../blogs/2026/03-march/feeling-sleepy.org][Feeling extremely sleepy]]
|
||||
- [[file:../blogs/2026/03-march/oversleeping-16-03.org][Oversleeping and missing a meeting...]]
|
||||
- [[file:../blogs/2026/02-february/27-02-26.org][Journeys rambles again...]]
|
||||
- [[file:../blogs/2026/02-february/26-02-26.org][Starting the Journeys Upgrade]]
|
||||
- [[file:../blogs/2026/02-february/24-02-26.org][Integration tests failing (sob)]]
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#+OPTIONS: toc:nil num:nil title:nil
|
||||
|
||||
* Posts tagged notes
|
||||
- [[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]]
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#+OPTIONS: toc:nil num:nil title:nil
|
||||
|
||||
* Posts tagged review
|
||||
- [[file:../blogs/2026/03-march/15-03-week-review.org][[15-03-2026] - Weekly Review]]
|
||||
- [[file:../blogs/2026/03-march/08-03-week-review.org][[08-03-2026] - Weekly Review]]
|
||||
- [[file:../blogs/2026/03-march/01-03-week-review.org][[01-03-2026] - Weekly Review]]
|
||||
- [[file:../blogs/2026/02-february/22-02-week-review.org][[22-02-2026] - Weekly Review]]
|
||||
- [[file:../blogs/2026/02-february/15-02-week-review.org][[15-02-2026] - Weekly Review]]
|
||||
|
||||
Reference in New Issue
Block a user