timesincechorereminder

Time Since Chore Reminder

A local-first tracker for things that make more sense as time since the last occurrence than as fixed calendar events.

Live on the Apple App Store.

iOS App Store · Web App · Kiera McCabe on the App Store

Project History

Started: December 2024

Time Since began as a practical tracker for chores and recurring life events that are easier to manage by elapsed time than fixed calendar dates. The current Expo app is a later cross-platform implementation of that workflow.

What Time Since does

Time Since can track chores, maintenance, routines, habits, timed sessions, and long-running life dates without requiring an account or cloud service.

Quick start

1. Add a tracker

Create a tracker and choose its type:

Type Best for Main behavior
Occurrence Chores, maintenance, routines, one-off events Log each occurrence and measure time between logs
Session Fasting, workouts, study, focused work Start, pause, resume, and end a timed session
Counter Anniversaries, move dates, deadlines, lease dates Count since a start date or toward a target date

Give the tracker a name and category. Occurrence trackers also inherit the current default reminder interval when they are created; changing the app-wide default later does not rewrite existing tracker reminders.

2. Log occurrence trackers

For something like Litterbox or Change bed:

3. Use session trackers

Session trackers are for activities with a duration rather than a single timestamp.

4. Use counters

Counters are date-based rather than completion-based.

5. Organize and use swipe actions

Trackers are grouped by category. In Settings → Swipe Actions, each horizontal direction can be configured as:

This lets the main list stay fast for repetitive logging while still keeping detailed entry available when needed.

6. Configure reminders

Native mobile reminders apply to occurrence trackers.

The browser version is useful for the UI, local storage, PWA use, and data portability, but native notification behavior should be tested on iOS/Android.

Import, export, and moving your history

CSV is the app’s visible interchange format. There is no account or automatic cloud sync: moving data between browsers/devices is intentionally manual.

Export from the Web App

  1. Open the data/import panel.
  2. Choose Export CSV.
  3. The browser downloads time-since-export.csv.
  4. Store that file somewhere you control, such as Files, iCloud Drive, Google Drive, or another backup location.

Export from the iOS app

  1. Open Settings → Data.
  2. Tap Export CSV.
  3. Time Since opens a read-only text view containing the CSV export.
  4. Copy the text and save/share it using a destination you control.

The current native interface exposes the export as copyable text rather than automatically writing a file.

Import CSV history

  1. Open Settings → Data → Import CSV History.
  2. Paste the CSV contents into the import field.
  3. Choose an import mode.
  4. Tap Restore / Import CSV.

Before using Overwrite or Replace all, export your current data first.

Import modes

Mode What it does
New only Keeps existing tracker settings/history, adds trackers that do not exist yet, and adds nonduplicate imported history to both new and matching trackers.
Overwrite For matching trackers, replaces their tracker fields and existing occurrence/session history with the imported version. Unrelated local trackers remain untouched.
Replace all Replaces the current local tracker/history dataset with the imported CSV. App-level settings are rebuilt from defaults because CSV does not contain those settings.

Trackers match on the exact combination of name + category + type.

Duplicate history is skipped using:

Accepted CSV format 1: simple history

For straightforward one-row-per-event history, use this header:

Category,Event,Occurrence,Note
Household,Litterbox,2026-08-18T09:15:00,Cleaned both boxes
Household,Change bed,2026-08-12 21:30:00,
Maintenance,Air filter,2026-08-01T12:00:00,Hallway unit

Required fields:

Note is optional.

The simple format does not carry explicit session/counter structures, reminder settings, or counter targets. Because there is no type column, Time Since infers an obvious tracker semantic from the event name when possible; otherwise it imports the tracker as an occurrence tracker.

Accepted CSV format 2: full Time Since export

A Time Since export starts with:

kind,category,name,type,date,note,reminder_days,duration_format,counter_start,counter_target

The full format can represent:

Dates may be ISO-style timestamps or legacy local timestamps such as 2026-08-18 09:15:00.

CSV limitations

CSV is intended for portable tracker/history data, not a byte-for-byte application backup.

Privacy and storage

Time Since is local-first.

Because there is no automatic cross-device sync, exporting a CSV periodically is the safest way to keep a portable copy of your history.

Calendar integration

Google Calendar integration is not implemented. There are no Google OAuth, calendar API, or expo-calendar code paths in the current app.

Current limitations

Development

Canonical local repo

Use the checked-out repository as the source of truth for local development. Separate working copies should not be treated as canonical unless they have been explicitly promoted.

Run locally

npm install
npm run web

For mobile testing:

npm run ios
npm run android

Build the web version

npm install
npm run build:web

The static web build is written to dist/ and can be deployed to Vercel or Netlify.

Vercel

Framework Preset: Other
Install Command: npm install
Build Command: npm run build:web
Output Directory: dist

The repo includes vercel.json with the production build/output settings.

Tweak swipe sensitivity

Swipe feel is separate from swipe actions:

A row commits on either of two independent paths, so there are two sets of knobs rather than one threshold:

committed = travel >= commitDistance                          // slow, deliberate drag
         || (travel >= flickMinDistance && speed >= flickVelocity)   // fast flick

Native (iOS / Android)

All native gesture feel lives in one object, SWIPE_TUNING in src/swipe-tuning.ts, so the recognition logic in components/TrackerRow.tsx never has to be edited to change how the gesture feels. Distances are React Native points; flickVelocity is PanResponder’s vx, roughly screen widths per second.

Knob Default Effect
activationDistance 7 Horizontal travel before a row claims the gesture. Lower = easier pickup.
directionRatio 1.35 How far horizontal movement must dominate vertical. Raise it if scrolling gets mistaken for swiping.
commitDistance 64 Travel at which a slow, deliberate swipe runs the action.
flickMinDistance 18 Minimum travel before a fast flick is allowed to count at all.
flickVelocity 0.55 Speed at which a short flick commits without reaching commitDistance.
maxTranslation 112 How far the row can visually slide while being dragged.
springTension 135 Snap-back speed. Higher = faster.
springFriction 14 Snap-back bounce. Higher = less bounce.

Starting points for the usual complaints:

Symptom Change
Swipes fire accidentally while scrolling the list Raise directionRatio (1.35 → 1.8), then activationDistance (7 → 12)
Swiping feels like hard work Lower commitDistance (64 → 48) and flickVelocity (0.55 → 0.40)
Taps get misread as flicks Raise flickMinDistance (18 → 26)
Row snaps back too abruptly Lower springTension (135 → 100), raise springFriction (14 → 18)

Change one knob at a time and retest on a device — the simulator’s trackpad flicks are much faster than a thumb, so flickVelocity in particular does not feel the same in both places.

A Debug build reloads over Metro, so saving the file is enough to see the change. Then:

npm test        # tests/swipe-config.test.js compiles src/ and asserts gesture behavior
npm run typecheck

Web

TrackerRow’s PanResponder returns false whenever Platform.OS === 'web', so the browser runs its own pointer-event recognizer. The two are separate implementations that are deliberately kept at matching values, so a tuning change belongs in both places or they drift apart. Web constants sit at the top of the IIFE in web-demo/mobile-gesture-import-fixes.js:

Constant Default Native counterpart
ACTIVATION_PX 7 activationDistance
DIRECTION_RATIO 1.35 directionRatio
COMMIT_PX 64 commitDistance
FLICK_MIN_PX 18 flickMinDistance
FLICK_VELOCITY_PX_PER_MS 0.55 flickVelocity
MAX_TRANSLATE_PX 112 maxTranslation
SCROLL_PX 18 vertical travel that concedes the gesture to the scroller
SCROLL_RATIO 1.5 vertical must dominate by this much to concede
TAP_SLOP_PX 7 movement still counted as a tap
TAP_MAX_MS 700 longest press still counted as a tap
EDGE_START_PX 28 edge-swipe back: how close to the edge the gesture must start
EDGE_COMMIT_PX 64 edge-swipe back: travel required to navigate

The units are not identical even though the numbers are: web measures CSS pixels and px/ms, React Native measures points and PanResponder vx. They are matched because they represent the same physical interaction, not because the scales are the same — so port a change by intent, not by copying a number blindly.

Keep TAP_SLOP_PX equal to ACTIVATION_PX. If slop is the smaller of the two, there is a dead band of movement that registers as neither a tap nor a swipe.

Web has the same two commit paths as native — absX >= COMMIT_PX, or a flick clearing both FLICK_MIN_PX and FLICK_VELOCITY_PX_PER_MS — so the symptom table above applies to the browser too.

npm run build:web   # regenerates dist/ from web-demo/; editing dist/ directly is overwritten
npm test            # tests/web-swipe-gestures.test.js drives pointer events under jsdom

Distribution