12 KiB
Executable File
Wird Tracker — Technical Guide
- Overview
- File Locations
- Database Schema
- REST API
- Frontend Architecture
- How To: Common Tasks
- Extensibility Notes
- Deployment Checklist
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.jshandles all rendering and API calls - Org page —
wird-tracker.orgdefines 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.
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()
);
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.
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)
);
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:
{
"wirdType": "durood",
"date": "2026-03-19",
"value": 500,
"notes": "after fajr"
}
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:
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;
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:
loadAll()— fetches/api/wird/entries, populatesallEntriesbuildTodayMap()— aggregates today's entries intotodayMap(type → total)buildMeetingLog()— filtersallEntriesto attended shaykh meetingsrenderToday()— draws the 5 wird cards and progress barrenderMeetingPanel()— draws the meeting status, next due date, cycle tablerenderHistory()— populates the history table (meetings excluded)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:
// Current: anchored to first ever meeting
const first = allMeetingDates[0];
// Alternative: rolling window from most recent
const first = addDays(allMeetingDates[allMeetingDates.length - 1], 0);
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:
durood: { label: "Durood", unit: "count", target: 700 },
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:
wird-tracker.js— add an entry toDAILY_WIRD:
tawbah: { label: "Tawbah", unit: "count", target: 100 },
wird-tracker.org— add an<option>to the modal<select>:
<option value="tawbah">Tawbah</option>
wird-tracker.org— add an<option>to the trend chart<select>:
<option value="tawbah">Tawbah</option>
wird_targetstable — insert a default target (optional but tidy):
INSERT INTO wird_targets (wird_type, target)
VALUES ('tawbah', 100);
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:
const MEETING_CYCLE_DAYS = 21; // change to e.g. 14
Edit or delete an entry
There is currently no edit/delete UI. You can do it directly in psql:
-- 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;
If you want a delete button in the UI, the backend needs a DELETE
endpoint:
@DeleteMapping("/entries/{id}")
public ResponseEntity<Void> deleteEntry(@PathVariable Long id) {
repo.deleteById(id);
return ResponseEntity.noContent().build();
}
Then in the JS, add a delete button to each history row and call:
await fetch(`${API}/entries/${id}`, { method: "DELETE" });
Inspect data directly
The wird_daily_totals view is useful for quick summaries:
-- 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;
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:
<link rel="stylesheet" href="/css/wird-tracker.css?v=2">
<script src="/js/wird-tracker.js?v=2"></script>