#+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
