Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b8e9d19
01a10b01 - Add member habits with comment windows and Bitcoin invoices
TaprootFreakAI Oct 5, 2026
8683c15
01a10b01 - Use Sunday rest for habit comments
TaprootFreakAI Oct 5, 2026
a593a39
01a10b01 - Pause habit invoices on Sunday
TaprootFreakAI Oct 5, 2026
c919419
01a10b01 - Use the gift invoice contract for habit donations
TaprootFreakAI Oct 5, 2026
70d4851
Do not count a habit invoice that has no wallet, and reject a week th…
TaprootFreakAI Oct 5, 2026
19ea926
Document the habit boot path and cover the address port that does not…
TaprootFreakAI Oct 5, 2026
5789cfe
01a10b01 - Tie habit rows to their account and reuse the gift amount …
TaprootFreakAI Oct 5, 2026
9318df6
01a10b01 - Pass the SQL habit store into the process and reuse the wa…
TaprootFreakAI Oct 5, 2026
697b49b
01a10b01 - Document the habit list response and the store failure.
TaprootFreakAI Oct 6, 2026
45a6763
01a10b01 - Write each habit wording change as one statement.
TaprootFreakAI Oct 6, 2026
f273569
01a10b01 - Check habit ownership inside the wording update.
TaprootFreakAI Oct 6, 2026
641ff8b
01a10b01 - Cover an unowned habit on archive and log.
TaprootFreakAI Oct 6, 2026
784bc38
01a10b01 - Check comment deletion with roleAtLeast only.
TaprootFreakAI Oct 6, 2026
ff012b5
01a10b01 - Count habit text in Unicode code points.
TaprootFreakAI Oct 6, 2026
4862e3a
01a10b01 - Reject a bad habit comment id and bound revision text.
TaprootFreakAI Oct 6, 2026
9b85d79
01a10b01 - Prove a reused habit revision table gains its length checks.
TaprootFreakAI Oct 6, 2026
2b783d3
01a10b01 - Reject a habit invoice that is not the requested amount.
TaprootFreakAI Oct 6, 2026
ae3820d
01a10b01 - Keep the route table separate from the auth note.
TaprootFreakAI Oct 7, 2026
b795a59
01a10b01 - Refuse another member's habit log before the period check.
TaprootFreakAI Oct 7, 2026
81192fb
01a10b01 - Cover a habit log the store no longer has.
TaprootFreakAI Oct 7, 2026
67bbacb
01a10b01 - Reject another member's habit log before the period shape.
TaprootFreakAI Oct 7, 2026
6f70115
01a10b01 - Refuse a habit edit after the archived period.
TaprootFreakAI Oct 7, 2026
b1d6853
01a10b01 - Return not found before a closed period on someone else's …
TaprootFreakAI Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ Public base URLs used in examples:
| PATCH | `/messages/:id/photos` | Bearer (moderator+) | Replace the stills of a live top-level shop note; a video stays; no edit history |
| GET | `/messages/:id/edits` | Bearer (moderator+) | Staff edit history of a shop note, newest first |
| POST | `/messages/:id/invoice` | Bearer | NIP-57 zap / BOLT11 |
| GET | `/habits` | none | Public member habits. A bearer includes private notes only on the caller's own rows. |
| POST | `/habits` | Bearer | Add, edit, archive, or log a habit; comment; delete a comment; or mint `{ pr, amountSats }` for a comment. |
| GET | `/messages/:id/repayment` | none | Public credit ledger: who gave, and each repayment share |
| POST | `/messages/:id/repayment` | Bearer | Author pays the next giver share from their own wallet. A repeat for that unpaid share returns the outstanding invoice. |
| POST | `/contact` | Bearer | Send private in-app contact `{ text }` |
Expand Down Expand Up @@ -5414,6 +5416,76 @@ public notification, not `{ "ok", "tags" }`. Enqueue failure still returns
Success → **Response** `200` (one public notification with `readAt` set,
or still `null` for `moderator_proposal`).

### `GET /habits`

Public list. No bearer required. `Cache-Control: no-store`. A valid bearer
includes `notes` only on the caller's own habits; everyone else's JSON
omits `notes`. Periods run from `firstPeriod` through the latest ratable
day or week. A daily period is ratable through today in the habit's
stored IANA zone. A weekly period becomes ratable at 08:00 on the
following Monday in that zone. `logged` is false and `status` is null
when the owner has not recorded that period.

Success → **Response** `200` `{ "reviewWeek": { "start" }, "habits" }`.
Store failure → **503** `{ "error": "Habits are unavailable" }`.

### `POST /habits`

Bearer session required. One strict JSON action: `add`, `edit`, `archive`,
`log`, `comment`, `deleteComment`, or `invoice`. `Cache-Control: no-store`.
The api does not pay.

`add` stores the device `Time-Zone` and requires a zone `Intl` accepts.
A missing, blank, or unknown zone is **400** `{ "error": "Invalid time zone" }`.
That check is only for `add`: the stored zone is the habit's clock, which
is a different job from Sunday rest. Name is 1–80 characters after trim.
Description and private notes are optional and at most 2000 characters.
Cadence is `daily` or `weekly` and is not changed by `edit`. Notes stay
owner-only.

`comment`, `deleteComment`, and `invoice` use the Sunday rest already
stated for public writing (`POST /messages/:id/repayment`: device
`Time-Zone` in Sunday → **403** `{ "error": "SUNDAY_REST" }`, and a
missing, blank, or invalid zone does not refuse). `add`, `edit`,
`archive`, and `log` do not rest on Sunday. The invoice Sunday check is
before the amount check.

A comment hangs on the habit. It is not a forum post and not a Nostr
note. Text is 1–2000 characters after trim. `deleteComment` requires
initiator rank; a lower role is **403** `{ "error": "Forbidden" }`.

`invoice` mints a BOLT11 invoice for the comment author's Lightning
Address through the existing gift-invoice helper. Body `amountSats` must
be an integer from 1 through 10_000_000 (the whole-sat form of
`GIFT_INVOICE_MAX_MSAT`). A non-integer, a value below 1, or a value
above that ceiling is **400**
`{ "error": "Expected a JSON body with an integer \"amountSats\"" }`.
A missing `amountSats`, or a value that is not a number, is that same
**400**.
Success is the same gift body as `POST /pay/:username/invoice`:

```json
{ "pr": "lnbc...", "amountSats": 21 }
```

The `pr` is returned only when it decodes to exactly `amountSats * 1000`
millisatoshis, the same rule as `POST /pay/:username/invoice`.

Donating to the caller's own comment is **400**
`{ "error": "Cannot donate to yourself" }`. No Lightning Address on the
author is **409** `{ "error": "The author's wallet cannot receive this Bitcoin payment" }`. The existing invoice
limiter answers **429** `{ "error": "Too many payments" }`. A failed
mint, or a BOLT11 that is missing, not a safe integer amount, or not the
requested amount, is **502**
`{ "error": "Lightning Address could not be resolved" }`, the same failure
as `POST /pay/:username/invoice`.

Missing bearer → **401** `{ "error": "Unauthorized" }`. A body that is
not one of the actions → **400** `{ "error": "Invalid body" }`. Unknown
habit or comment → **404** `{ "error": "Not found" }`. A period that is
not yet ratable → **409** `{ "error": "Period is closed" }`. Store
failure → **503** `{ "error": "Habits are unavailable" }`.

---

## Not implemented (v1, decided in CONCEPT — no HTTP paths)
Expand Down
17 changes: 16 additions & 1 deletion docs/handbook/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@

- **Purpose:** Public JSON of outbound gift totals: `totalSats` / `totalBtc` / `totalUsd` plus additive `totalChf` / `totalEur` / `totalPhp`, `giftCount`, `recipientCount`, date range, `spendOverTime` (giftCount+officialCount+sats+BTC+USD+fiat; `officialCount` is the distinct case-insensitive recipient handles that UTC day with kind `daily` or `welcome`, one person once, moderator excluded, gap days 0; `giftCount` remains every outbound row), `byRecipient`, `byMonth`, and `fx` (`quote` stays BTC-USD; `fx.quotes` lists USD always and CHF/EUR/PHP when at least one selected gift day has that cross). The stored payment-time USD/CHF/EUR/PHP is what is returned (not recomputed from that day's close). A gift day that lacks a fiat cross returns that currency as JSON `null` (totals go null if any selected gift lacks that cross). Optional query `recipient` filters to one Wallet of Satoshi handle (case-insensitive). When `recipient` contains `@` after the first character, the local-part before `@` is used; otherwise the whole trimmed string. Missing/blank `recipient` = unfiltered. Unknown handle = empty stats **200** with zeros and USD-only `fx.quotes` (no Coinbase / Frankfurter). Empty boots are empty **200** with zeros and USD-only `fx.quotes` (no Coinbase / Frankfurter). No invoices.
- **Errors:** 503 `{ "error": "Gift stats are unavailable" }` when the gift store throws, when BTC-USD `ensureDays` fails, or when any selected gift day still lacks BTC-USD after ensure (`gifts.stats.fx_incomplete` / `gifts.stats.failed`). Missing CHF/EUR/PHP is never 503 (`gifts.stats.fiat_failed` still 200).
- **Used by:** App statistics page (`GET /gifts/stats` same-origin proxy); optional per-recipient view via `?recipient=`; staff payout-goal widget on `/moderate`.
- **Used by:** App statistics page (`GET /gifts/stats` same-origin proxy); optional per-recipient view via `?recipient=`; staff payout-goal widget on `/moderate`; the habit tracker pay sheet when a session is present, for the same fiat suffix as a forum zap.
- **Auth:** Public.

## Endpoint: GET /healthz
Expand Down Expand Up @@ -1149,3 +1149,18 @@ Operator inspection of external Nostr identities that have earned visibility or
- **Errors:** 400 `{ error: 'invalid_code' }` when `:code` is not exactly eight hex digits after `toLowerCase()` (no trim); 404 `{ error: 'not_found' }` when neither store has a match; 409 `{ error: 'ambiguous' }` when two or more ids match (two messages, two accounts, or one of each). No ids in error bodies. No 503 path.
- **Used by:** Website short-link landing (`/l/<8 hex>`).
- **Auth:** none. Public. Soft-hidden messages are included; the public message page decides who may see them.

## Endpoint: GET /habits

- **Purpose:** Public list of member habits. No bearer required. A valid bearer includes `notes` only on the caller's own habits. `notes` is omitted for everyone else. An invalid or unknown bearer is treated as signed out and still returns the public list. Periods run from `firstPeriod` through the latest ratable day or week and stop at `lastPeriod`. `logged` is false and `status` is null when the owner has not recorded that period.
- **Inputs:** Optional `Authorization: Bearer`. No query and no body.
- **Returns / side effects:** 200 `{ reviewWeek: { start }, habits }`. `reviewWeek.start` is the Manila review Monday. No writes.
- **Errors:** 503 `{ error: 'Habits are unavailable' }` when the store throws (`habits.failed`). A bad bearer is not an error.
- **Used by:** the habit tracker page.

## Endpoint: POST /habits

- **Purpose:** One strict JSON action: `add`, `edit`, `archive`, `log`, `comment`, `deleteComment`, `invoice`. Bearer required. `add` requires `Time-Zone`. `comment`, `deleteComment`, and `invoice` follow the same Sunday rest as other public writing and as `POST /messages/:id/invoice`: 403 `{ error: 'SUNDAY_REST' }` when `Time-Zone` names an IANA zone that is Sunday. The invoice check is before the amount check. A missing, blank, or invalid zone does not refuse. Rating, add, edit, and archive are not that refusal. An edit whose current period is after `lastPeriod` is 409 `{ error: 'Period is closed' }` and does not change the stored wording. An edit of another member's habit is 404, including when that period is already closed. `invoice` returns `{ pr, amountSats }` and does not pay. Comments are not forum posts.
- **Inputs:** Bearer session. Body is one action object. `add`, `comment`, `deleteComment`, and `invoice` also send `Time-Zone`.
- **Returns / side effects:** 201 `{ ok: true, id }` for add. 201 `{ ok: true }` for comment, with no id. 200 `{ ok: true }` for edit, archive, log, and deleteComment. 200 `{ pr, amountSats }` for invoice. 400 invalid body, name, description, notes, time zone, status, period, or comment, self-donation, or `{ error: 'Expected a JSON body with an integer "amountSats"' }` when `amountSats` is missing, not a number, not a positive integer, or above 10_000_000. 401 missing bearer. 403 `{ error: 'SUNDAY_REST' }` for `comment`, `deleteComment`, and `invoice` on Sunday, or `{ error: 'Forbidden' }` for `deleteComment` below initiator rank. 404 missing habit or comment, including a comment id that is not a UUID on `deleteComment` and `invoice`. 409 `{ error: 'Period is closed' }` or `{ error: "The author's wallet cannot receive this Bitcoin payment" }`. 429 `{ error: 'Too many payments' }`. 502 `{ error: 'Lightning Address could not be resolved' }` when the mint fails or the BOLT11 does not decode to exactly `amountSats * 1000` millisatoshis. 503 `{ error: 'Habits are unavailable' }` when the store throws (`habits.failed`).
- **Used by:** the habit tracker page.
Loading
Loading