Catalog ›Personal life ›Wellbeing
Period Tracker — private cycle calendar
A minimal diary for your menstrual cycle: log the start date and its length, and the app works out on its own when the next one is due. Unlike several well-known apps in this space that have faced real privacy controversies, here your data stays **only on your device** — no account, no server, no syncing back to any company: your cycle is some of the most personal information there is, and it never leaves your phone.
What it records
- Cycle length in days, e.g. 28
How it works
This is a Reactive app: a plain text document that your browser turns into a working app — forms, live data, tables, statistics and even charts, with nothing to install or configure.
- Use it right away. Hit “Use this app”: it opens right in your browser, ready to go, with data saved only on your device.
- Your data stays yours. Everything is stored locally on your device, kept separate for each app: no account, no server, nothing to set up.
- Make it your own. The text below is the entire app: copy it, tweak fields, views and words, and your changes become interactive instantly.
- Share it as a file. An app is a simple file: save it, export it or share it with a link; and with encrypted collaboration several people edit the same data in real time.
The app’s source
# 🩸 Period Tracker
A minimal diary for your menstrual cycle: log the start date and its length,
and the app works out on its own when the next one is due. Unlike several
well-known apps in this space that have faced real privacy controversies,
here your data stays **only on your device** — no account, no server, no
syncing back to any company: your cycle is some of the most personal
information there is, and it never leaves your phone.
::::form{path="cycles" id="fci"}
::input{form="fci" field="start" type="date" required="true" legend="Cycle start"}
::input{form="fci" field="length" type="number" placeholder="Cycle length in days, e.g. 28" required="true"}
::add-form{form="fci" path="cycles" label="Log cycle"}
::::
## History
:::sort{path="cycles" field="start" order="desc" as="table" headers="Start,Length (days)" deletable="true" editform="fci"}
{start} | {length}
:::
:::python{data="cycles"}
```
from datetime import date, timedelta
rows = data["cycles"]
if rows:
ordered = sorted(rows, key=lambda r: r.get("start", ""))
last = ordered[-1]
length = int(last.get("length") or 28)
start = date.fromisoformat(last["start"])
next_date = start + timedelta(days=length)
print(f"Next period expected: {next_date.strftime('%b %d, %Y')}")
else:
print("Log at least one cycle to see the prediction.")
```
:::
Every time you add a new cycle the prediction recalculates on its own from
the latest entry — just keep the history above up to date.