Dev.to WebDev 🛠 Dev 👁 0 📖 5 min read

Our prep plan has no done button, because a checkbox would be a second opinion about what you practised

CogniPrep added an applications tracker last week: you name the employers you are applying to, attach the dates from the invitation emails, and each one gets a practice plan for the stage you are at. The plan is the inte

CogniPrep added an applications tracker last week: you
name the employers you are applying to, attach the dates from the invitation
emails, and each one gets a practice plan for the stage you are at. The plan is
the interesting part, because of what it does not have.

It has no done column, no "mark as complete" checkbox and no row in the
database. It is a function:

 * The plan is not stored. It is a pure function of the path, the stage, the
 * dates and the activity, recomputed on every read, which is why changing a
 * deadline or finishing a game moves it with no write.

124 lines, zero database access, and the only way a task becomes done is that
the thing it points at happened.

Done is a membership test

A task can be one of three kinds, and each one asks the activity set a
different question:

export function isPracticeDone(item: PracticeItem, completed: CompletedPractice): boolean {
  switch (item.kind) {
    case 'game':
      return completed.gameIds.has(item.gameId);
    case 'interview':
      return completed.interviews > 0;
    case 'exercise':
      return completed.exerciseKinds.has(item.exerciseKind);
  }
}

CompletedPractice is two sets and a count, and the window it covers is the
caller's decision rather than the plan's. The caller picks "since this
application was added", which is the rule with the fewest surprises in it: the
plan is about preparing for this employer from the day you told us about them.

It costs something. Practice you did last month does not tick a box for an
application you added today, and somebody will eventually report that as a bug.
The alternative is a checkbox, and a checkbox is a second record of what you
practised that can disagree with the first one, forever, with no way to tell
which is right. A derived plan cannot drift from your history because it has no
state of its own to drift with.

One activity read for every application

The naive shape here is a query per application. Instead the loader takes the
oldest application's creation date, reads activity once from there, and slices
it per application in memory:

const earliest = applications.reduce<Date | null>(
  (min, app) => (min === null || app.created_at < min ? app.created_at : min),
  null
);
if (!earliest) return [];
const activity = await getPracticeActivity(userId, earliest);
return applications.map((app) =>
  toApplicationView(app, completedPracticeSince(activity, app.created_at), now)
);

That read is three grouped queries in one Promise.all, one per activity type,
each returning the latest completion per id rather than the rows. Forty
applications with a dozen dates each is still three queries.

The module is import 'server-only' and its doc comment says why it exists at
all:

 * The applications with their plans, as the Applications page, the calendar
 * feed and the Google sync all need them.
 *
 * One activity read for every application, from the oldest one's creation;
 * each plan then counts only what happened since its own application. Shared so
 * the plan in a calendar is always the plan on the page.

Three surfaces, one builder. The subscribed calendar feed and the pushed Google
copy are not re-deriving anything; they render the same PrepPlan object the
page renders.

The schedule is nine lines and front-loaded

With a deadline, the unfinished tasks are laid out day by day up to it. One a
day while the days allow it, doubling up only when there are more tasks than
days:

tasks.forEach((task, index) => {
  const dayIndex =
    tasks.length <= availableDays ? index : Math.floor((index * availableDays) / tasks.length);
  const bucket = days.get(dayIndex) ?? [];
  bucket.push(task);
  days.set(dayIndex, bucket);
});

Five tasks, run against the real function at four different deadlines:

due in 5 days  -> 11-01:1  11-02:1  11-03:1  11-04:1  11-05:1
due in 2 days  -> 11-01:3  11-02:2
due today      -> 11-01:5
no deadline    -> (no schedule)

Two things fall out of that. The deadline day itself is not a slot: with five
days left the work finishes on day five of six, so the days nearest the
deadline stay free for whatever went badly. And with no deadline there is no
schedule at all, only the task list, because a day-by-day plan with no end date
is an invented urgency.

Finish two of the five and the schedule recomputes to three days, with no write
anywhere:

with 2 of 5 already played -> 2 done, 11-01:1  11-02:1  11-03:1

The caps, and one piece of belt-and-braces

The constants that bound the whole feature are five lines in a file with no
imports, so the client components can read them too:

export const MAX_ACTIVE_APPLICATIONS = 40;
export const MAX_EVENTS_PER_APPLICATION = 12;
export const EVENT_TITLE_MAX_LENGTH = 80;
export const APPLICATION_RETENTION_YEARS = 2;

Forty is far above what a heavy graduate season needs, which is the point: it
is there to stop the tracker becoming a bulk store, not to make anyone choose.

The stage and outcome values are text columns with CHECK constraints behind
them, so the database cannot hold an unknown stage. The view builder narrows
them anyway:

const stage = isApplicationStage(application.stage) ? application.stage : 'researching';

That is deliberate. The constraint is a guarantee about the current schema, and
a dropped-and-recreated constraint during a migration is exactly the moment the
page should still render. The cost of being wrong about that is one wrong stage
badge; the cost of trusting it is a 500 on someone's tracker.

Tests for the whole directory: 6 files, 59 cases, 483ms, with now injected
everywhere so a plan that counts days is deterministic.

See it

The tracker lives behind the login, so the honest public surface is the
documentation of it. Section 12 of
cogniprep.app/privacy describes what the page
stores and what it derives, and the retention line is the two-year constant
above, written in English:

curl -s https://cogniprep.app/privacy | sed 's/<[^>]*>/ /g' \
  | grep -o "Tracked applications and their dates[^.]*"

An account is free at cogniprep.app/signup, and
adding one employer with one date is enough to see the day-by-day schedule
above with real tasks in it. If you want the shape of the plan without signing
up, cogniprep.app/assessment-centre-practice
and cogniprep.app/interview are the two
kinds of practice item it can schedule, with the third being the provider games
on cogniprep.app/games.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.