Build Rise Calendar

KINDAback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A personal calendar and task planner whose difficulty is not the grid but the sync underneath it: an incremental sync that invalidates, push channels that expire quietly, blocks that must never move a real meeting, and a plan that has to re-fit itself every time the day slips.

estimated effort multi-daythe files for this build are in the project pack

RuntimeA desktop application with a local database and one calendar accountSyncIncremental sync tokens with a full resync whenever the server invalidates oneOwnershipBlocks the tool creates carry a stored identifier and a marker; every other event is read-onlyPlanningA packer that fills free windows in working hours and keeps earlier placements stableTimeInstants stored, the zone applied at the edges, recurrence expanded with exceptionsFreshnessPush channels renewed before expiry with polling as the backstop, and the sync age on screen

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the desktop application, the sync worker and the tests.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. free

    Why Recurrence with exceptions, declined invitations and all-day events are the cases that break sync and planning, and an empty test calendar contains none of them.

    Get it Your own calendar, with at least one recurring series that has a cancelled instance and one declined invitation.

    Verify The cancelled instance is free and the rest of the series is busy after sync

  3. API keyfree

    Why The tool syncs as you, through a client you created, and the unverified cap of a hundred users is why it stays personal.

    Get it Create a project and an OAuth client in the provider's console, add the narrowest calendar scope, and add yourself as a test user. open ↗

    Verify The consent screen grants the scope and an incremental sync completes

  4. have readyfree with a tunnel, pennies on a host

    Why Push channels deliver to a URL the provider can reach, and without one the tool is polling only, which is acceptable but should be a decision.

    Get it A tunnel to your machine or a tiny relay on a host you control.

    Verify The sync message arrives when a channel is created

  5. decidefree

    Why The planner packs into working hours and treats declined and all-day events by rules that are asserted in tests, so they belong decided before phase one.

    Get it Write down working hours per day in your zone, whether an all-day event blocks the day, and whether a declined invitation is free time.

    Verify All three are in the configuration and named in the README

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- calendars(id, provider, account_ref, scopes_granted, sync_token, last_full_sync_at, last_incremental_at, channel_id, channel_expires_at)
- events(id, calendar_id, external_id, starts_at, ends_at, all_day, status, recurrence_json, is_ours, updated_at)
- tasks(id, title, estimate_minutes, remaining_minutes, deadline_at, priority, project_id, state)
- blocks(id, task_id, external_event_id, starts_at, ends_at, placed_at, pinned, at_risk) · the join between a task and an event the tool created
- working_hours(day_of_week, start_local, end_local, timezone)
- plan_runs(id, at, reason, placed, moved, unplaced, at_risk)
- A block links a task to a calendar event identified by stored id, so the planner can move its own blocks freely and never touches a meeting it did not make.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/planner.dbEvents, tasks, blocks and plan runs.
CALENDAR_CLIENT_IDsecretrequireda client id from your own projectYour own OAuth client. Unverified, and capped at a hundred users.
CALENDAR_CLIENT_SECRETsecretrequireda client secret from the same projectNever in the repository. The desktop app holds it in the platform keychain.
PUSH_CALLBACK_URLoptionalhttps://relay.example/calendar-pushWhere notification channels deliver. Absent means polling only, stated on screen.
POLL_SECONDSrequired300The backstop. Runs whether or not a channel is live.
SYNC_STALE_MINUTESrequired30Past this the sync age becomes a warning on screen.
WORKING_HOURS_PATHrequired./config/working-hours.jsonPer day, in your zone. The planner never places outside them.
TOUCH_FOREIGN_EVENTSrequiredneverPresent as a constant. There is no other value.

The build, in order

  1. Reading one calendar correctly

    The local copy matches the server, including after the server invalidates the sync token.

    1. Clear the store and start again; the token is gone and will not come back.

      Files src/sync.tstests/resync-410.test.ts

    2. The series minus its cancellations, not the series.

      Files src/recurrence.tssrc/recurrence.test.ts

    3. Read events and write your own is enough for everything here.

      Files src/oauth.ts

    done when · tick each as it passes
    watch out
    • Treating the 410 as an error and retrying the same token, or ignoring it and carrying on, leaves a calendar that drifts from the server without anything visibly breaking, until a meeting cancelled last week still blocks a slot. Clear and resync, and keep your own blocks through it by their stored ids.
  2. Tasks as blocks in free time

    Tasks are packed into free working time and stay where they were put unless they must move.

    1. Working hours minus every event, recurring ones expanded first.

      Files src/planner.tssrc/planner.test.ts

    2. Move what conflicts and nothing else, and say what moved.

      Files src/planner.ts

    3. A deadline the plan cannot meet is a fact the person needs today.

      Files src/risk.ts

    done when · tick each as it passes
    watch out
    • Re-planning from scratch on every change reshuffles the whole day each time a meeting lands, and the person stops trusting the plan within a week. Keep existing placements stable, move only what conflicts, and show what moved and why.
  3. Writing blocks without touching anyone else's events

    Blocks appear in the calendar as events the tool owns, and no foreign event is ever changed.

    1. A rename must not orphan a block or endanger a meeting.

      Files src/calendar-write.ts

    2. Tested by attempting it, not by intending it.

      Files tests/foreign-events.test.ts

    3. Moved by hand means pinned, not overwritten.

      Files src/reconcile.ts

    done when · tick each as it passes
    watch out
    • Identifying your own events by title means a renamed block goes invisible and gets recreated as a duplicate, and a coincidence of naming lets the planner move a real meeting. Store the created event id, and treat everything without one as untouchable.
  4. Staying current when the day slips

    The calendar is never quietly stale, and an overrun rolls the rest of the day forward.

    1. A lapsed channel looks exactly like a quiet day.

      Files src/push.tstests/channel-expiry.test.ts

    2. The notification says something changed. Go and look.

      Files src/push.ts

    3. Remaining minutes, stable placement, affected blocks only.

      Files src/rollover.ts

    done when · tick each as it passes
    watch out
    • Assuming a notification carries the changed data, or that a channel lives forever, produces a client that goes stale the day the channel expires while looking entirely normal. Renew before expiry, sync on every notification, poll regardless, and put the sync age on screen.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more scheduling.