mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-11 14:27:45 +01:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
107 lines
4.2 KiB
Markdown
107 lines
4.2 KiB
Markdown
# freedays
|
|
|
|
One shared pool of pre-declared days on which **every** gate app stands down.
|
|
|
|
Mark a day. Nothing locks, nothing rings, nothing asks you for a photo, and
|
|
nothing mentions it afterwards.
|
|
|
|
## For a gate app
|
|
|
|
The entire integration is one call:
|
|
|
|
```python
|
|
import freedays
|
|
|
|
if freedays.is_free_day():
|
|
return # no lock, no dialog, no alarm -- and say nothing
|
|
```
|
|
|
|
`is_free_day()` reads one local JSON file. It never touches the network, so
|
|
it cannot block a systemd-fired gate, and it returns `False` for anything it
|
|
cannot parse — an unreadable pool means *the normal rules apply*, never that
|
|
everything is switched off.
|
|
|
|
## For a person
|
|
|
|
```console
|
|
$ freedays status
|
|
2026: 33 of 35 free days left
|
|
booked ahead: 2026-12-24
|
|
|
|
$ freedays mark 2026-12-24 --reason "travelling"
|
|
$ freedays mark today
|
|
$ freedays release 2026-12-24
|
|
$ freedays check tomorrow # exit 0 if free, 1 if not -- for scripts
|
|
$ freedays sync # pull and push now
|
|
```
|
|
|
|
Or on the phone: the calendar icon in **Wake Alarm Sync** is the same pool
|
|
(`~/src/wake-alarm/DOCS-free-days.md`). A day marked there stands the phone's
|
|
own alarm down immediately; a day marked here reaches the phone at its next
|
|
app launch.
|
|
|
|
## The rules
|
|
|
|
| | |
|
|
|---|---|
|
|
| **Budget** | 35 per calendar year, resetting Jan 1. Unused days do not carry over. |
|
|
| **One pool** | A free day is free for *every* app at once. There is nothing per-app to mark, and nothing per-app to forget. |
|
|
| **Declared, not backdated** | Today or later. A past day cannot be marked, so a gate you already failed cannot be erased afterwards. |
|
|
| **Consecutive is fine** | Take a fortnight in a row if you want. Nothing here rate-limits you. |
|
|
| **No reason required** | `--reason` is stored if given and never asked for. |
|
|
| **Releasing refunds only the future** | Give back a day that has not arrived and it returns to the pool. Give back today, and it does not: those hours were already unguarded. |
|
|
| **Silent** | No notification, no low-budget warning, no prompt when a gate fires. You find out by asking. |
|
|
|
|
## Weekly workdays
|
|
|
|
`freedays.WORKDAYS` (`{1, 2, 3}`, Tue/Wed/Thu in `date.weekday()` numbering),
|
|
`NON_WORKDAYS` and `is_workday(day=None)` are the one definition of the weekly
|
|
workdays every gate app reads — screen-locker's relaxed days, leetcode-guard's
|
|
cheap days, wake-alarm's ramp hold and workday stick. Import it; never write
|
|
the set out again. Dart/Kotlin code that cannot import it keeps a literal copy
|
|
guarded by a drift test against this package.
|
|
|
|
## Free days are not sick days
|
|
|
|
screen-locker's sick days stay exactly where they are, with their rolling
|
|
windows, their 120-character justifications and their escalating countdown.
|
|
They are *designed to be hard to take*.
|
|
|
|
These are the opposite, and merging the two would have to ruin one of them:
|
|
rolling windows forbid consecutive days, which is the entire point of a
|
|
holiday. So the policies stay separate. Only the storage is shared.
|
|
|
|
## How it syncs
|
|
|
|
Firebase RTDB, one directory per device under `freedays-sync/devices/`, the
|
|
same shape every other app in this fleet uses. No two devices write the same
|
|
node, so nothing can conflict; convergence is the CRDT layer's job.
|
|
|
|
Marking is a last-writer-wins flip of a `state` field rather than a create
|
|
and a delete, because crdt-sync tombstones are monotonic — deleting the
|
|
record would burn that date permanently, and a released day has to be
|
|
markable again.
|
|
|
|
`consumed` is a deliberately sticky flag: once a day has arrived while free
|
|
it stays spent, even if two devices disagree about what "today" is across a
|
|
midnight boundary.
|
|
|
|
## Timezone
|
|
|
|
Local, everywhere, decided in `_day.today()` and nowhere else.
|
|
|
|
This package exists partly because the apps disagreed about it: screen-locker
|
|
computed the day in UTC in its lock chain while the tool that wrote its skip
|
|
file used local time, so a skip added near midnight could name a date the
|
|
chain never matched. A free day is a human calendar day — if it is Tuesday
|
|
where you are standing, it is Tuesday.
|
|
|
|
## Development
|
|
|
|
```console
|
|
$ pip install ../crdt-sync
|
|
$ pip install -r requirements.txt && pip install --no-deps -e .
|
|
$ python -m pytest # 100% branch coverage is enforced
|
|
$ pre-commit run --config .pre-commit-config.yaml --all-files
|
|
```
|