Files
org_web/home/guide/wird-tracker-guide.org
Zaine Arch e45e05ea40
All checks were successful
Build Org Website / build (push) Successful in 45s
uhhh
2026-05-09 16:41:52 +01:00

333 lines
12 KiB
Org Mode
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#+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 27 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