#+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 ~ #+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 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: ~~, ~~, ~~, ~~ — 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 ~~ and ~ #+END_SRC