Back to overview

Documentation

Everything log(ger) does, in one page: how planning and tracking fit together, what the dashboard is actually showing you, how the chat stays grounded and safe, and — down in Data & database — exactly how it’s all stored on your machine.

What log(ger) is

log(ger) is a free, local productivity app built around one loop: plan what you mean to work on, track it as you actually do it, and analyze the result until it tells you something. All three share the same data, and all of it lives in a single SQLite file on your machine — there is no server, no account, and no sync.

Running it

log(ger) builds from source into a real .app — there’s nothing to sign up for. See Run it locally on the overview page for the exact commands.

Planning

The Planner is a horizontal, scrollable timeline — today marked with a guide line, days as columns, plan items as colored bars. It’s where you decide what you’re going to work on before you sit down to do it.

Creating and shaping a plan

Drag across empty days to block out a new item, or use the + New button to add one on today without dragging. Once an item exists, drag its body to move it to different days, or drag either edge to lengthen or shorten it — useful for a task that’s going to take a few days, not one sitting. A plan item can carry a specific time of day, but only while it’s still a single day; stretch it across multiple days and the time-of-day is dropped, since it stops meaning anything.

Every plan item belongs to a category (the same categories you log time against) and can carry a priority — low, medium, or high — shown as a small flag on the bar so the things that matter don’t get lost in the schedule.

Starting and closing a plan

Open a plan item and hit Start timer and a real timer starts, linked back to that plan — it shows up on the Recorder dashboard exactly like any other timer. You can also log time against a plan after the fact, with the category pre-filled from the plan itself.

💡

Closing a session does not automatically mark the plan complete. A multi-day task is expected to collect several sessions before it’s actually done, so completion is a separate, explicit checkbox (“Mark plan complete”) you tick only when you mean it.

Filtering the timeline

Filter the visible plan items by category, priority, or completion state — useful once the timeline fills up with more than a week or two of plans.

Recording time

The Recorder (log(ger)’s dashboard) is where time actually gets logged, whether or not it started from a plan.

Timers

Pick a category and hit play — the clock runs, and you can pause, resume, or stop it whenever. Run several timers in parallel if you’re genuinely switching between things. Every stopped timer rounds to the nearest honest minute.

Manual entries

Forgot to start a timer? A manual entry backfills the time after the fact — category, duration, and an optional description. You can also give it a specific start time to place it correctly on the day’s timeline instead of leaving it inferred from when you happened to log it.

Breaks

Mark a day, or a range of days, as a break. A break logs no time — it’s a neutral marker — but it bridges your streak, so a planned rest never resets a run you’ve been building. The date picker highlights break days so you can see them at a glance when picking a date.

Late nights

Logging something at 1am that really belongs to “yesterday”? Between midnight and 5am, log(ger) asks which day a new entry should count toward, so a late night doesn’t silently file itself under the wrong date.

Analyzing

Everything you log rolls up into the Analytics page, built around one idea: the same charts should work whether you’re looking at last week or your entire history.

One control, every scale

A single filter switches every chart on the page between overall, year, month, and week — a stacked area chart of time by category, a category breakdown, a weekly heatmap, and a session-by-session comparison grouped by family, all re-fit to whatever range you pick.

Streaks

A streak counts consecutive days with logged time, walking backward from today. Break days don’t count toward it, but they don’t reset it either — the streak simply steps over them.

The daily timeline

The Recorder dashboard also shows today (or any past day you scrub back to) as a Gantt-style row of colored blocks, one per entry, sized by how long it actually ran.

Chat & projects

Two ways to step back from the raw log: ask about it directly, or watch a single line of work evolve across sessions.

Chat

The chat is powered by the Claude API and reads your own sessions to answer — it doesn’t know anything you haven’t logged. Every query it wants to run against your data is shown to you first and requires a one-tap approval before it executes; nothing runs silently in the background. You’ll need your own Claude API key (see Reference), and you can pick which model to use.

Projects

A family — a project or thread of work that spans multiple sessions, like “Salk Research” across three quarters — gets its own timeline here: total time, active days, and an AI-generated summary per session. Research families can link a GitHub repository, and their summaries pull in real recent commits for extra context.

Data & database

No server, no sync — log(ger) is one SQLite file on your disk. This section is the honest, complete picture: how categories are organized, how a CSV import lands, and exactly how the schema is shaped underneath every screen above.

~/Library/Application Support/Logger/logger.db in the packaged app · ./logger.db in dev

Importing legacy data

Drop a CSV pair (a timesheet + a notes file) onto the import page and it fans out across the database in a single transaction — sessions, categories, daily totals, and text notes all land together, messy headers and all. New categories snap to the right family automatically, matched by exact name or prefix rule, so you don’t sort a year of history by hand.

The category hierarchy

Flat tags collapse over a year of use, so log(ger) uses three levels instead: a group (the kind of work — Research, Courses, Personal), a family (the thread that runs through it — “Salk”, “MPI”), and a category (one session’s slice of that family, like salk_spring26). Plan items and logged entries both point at a category; families are what let the Projects view stitch those categories back together across sessions.

One file, four levels

The core is a single chain: a group holds families, a family holds per-session categories, a category holds the entries you log. The planner hangs off the same categories.

category_groupsResearch · Courses · Personal 1→N category_familiesSalk · MPI · COGS… 1→N categoriesone per session 1→N timer_entries
manual_entriesthe minutes

Plus the planner, hanging off the same categories

plan_items.category_id → categories timer_entries.plan_item_id → plan_items manual_entries.plan_item_id → plan_items

Every table, the columns that matter

Twelve core tables. id is the primary key everywhere; marks a foreign key.

sessions
  • year, season, label
  • start_date, end_date
  • is_active
category_groups
  • name, display_name
  • color, position
  • is_system
category_families
  • name, display_name, color
  • group_id → groups
categories
  • name, display_name
  • session_id → sessions
  • family_id → families
timer_entries
  • session_id, category_id
  • date, start_time, end_time
  • duration_minutes, is_paused
  • plan_item_id → plan_items
manual_entries
  • session_id, category_id
  • date, duration_minutes
  • description, start_time
  • plan_item_id → plan_items
plan_items
  • title, notes, status
  • start_date, end_date
  • start_time, end_time, importance
  • category_id → categories
daily_records
  • session_id → sessions
  • date, week_number
  • total_minutes
observations
  • daily_record_id, category_id
  • minutes
  • source (import/timer/manual)
break_days
  • date (unique)
  • label
family_match_rules
  • family_id → families
  • match_type (exact / prefix)
  • pattern
text_entries
  • session_id → sessions
  • date, location
  • notes, study_materials

Watch a write land

Stop a 45-minute timer and three tables change in one transaction — the entry is recorded, the per-day-per-category aggregate is upserted, and the day’s total recomputes. Charts read the aggregate, never the raw rows.

timer_entries INSERT
May 25Salkearlier
May 25Salk45m
observations UPSERT
May 25 · Salk120m
daily_records RECOMPUTE
May 25 total200m

Every other change, same discipline

Add a manual entry

manual_entries +1observations ↑daily_records ↻

Insert, then the same upsert-and-recompute as a timer stop. Identical downstream.

Edit an entry

observations −oldobservations +new

Change the date, category, or duration and the aggregate rebalances: subtract from the old bucket, add to the new.

Delete an entry

observations ↓row removed

Subtract from the aggregate first; the observation is dropped if it hits zero. Then the row goes.

Mark a break

break_days +1no minutes

A date (or range) is inserted with a label. It adds no time; the streak query simply steps over it.

Start a plan, close it later

timer_entries.plan_item_idplan_items.status

Starting a timer from the planner just stamps the new entry with the plan’s id — the plan stays planned until you explicitly check it off.

Import a CSV pair

sessionscategoriesdaily_recordsobservationstext_entries

One file pair fans across the tables in a single transaction; new categories auto-link to families by rule.

Reading it back, fast

Pre-aggregated truth

observations holds minutes per day per category. Dashboards and charts query it directly.

Two SQL views

v_daily_totals (date × category) and v_family_totals (family per session) pre-join the hierarchy for analytics.

🔥

Streaks, break-aware

The streak walks daily_records back from today; a day in break_days bridges the gap instead of resetting it.

Why it holds up

  • Aggregations are stored, not derived. observations is written transactionally with every entry — charts never scan raw timer or manual rows.
  • Every mutation is transactional. Insert/upsert/recompute commit together; a failure rolls the whole thing back, so totals can’t drift from entries.
  • Migrations are idempotent. Every start runs init_db, which probes the schema before it changes anything. Safe to restart; safe to load an older database.
  • The file is the source of truth. Download current archives a snapshot; Choose .db file loads one back. Each swap keeps one rolling logger.db.bak.
  • Dates live on the entry. A timer started at 1am attributed to “yesterday” writes date = yesterday; the aggregate rolls up under yesterday too.
  • Plans and logged time are linked, not merged. plan_item_id is a nullable SET NULL foreign key — delete a plan and every session you logged against it keeps its minutes, it just stops pointing anywhere.

Every table and column lives in models.py.

Reference

Settings, portability, and the honest limits of what’s here today.

Settings

  • Theme — light, dark, or follow the system.
  • Timezone — used to bucket timers into the right calendar day.
  • Claude API key — required for chat and AI-generated project summaries; stored locally, never sent anywhere but Anthropic’s API.
  • Model — pick which Claude model answers chat queries.

Backing up

Download current saves the live database as a file; Choose .db file loads one back in. Dev mode and the packaged Mac app keep independent database files, so moving data between them is a manual download/load, on purpose — nothing crosses silently.

Known limitations

  • Local only, for now — one machine, no mobile app, no sync between devices.
  • Chat and AI summaries need your own Claude API key; nothing else in the app does.
  • One session (academic term or work cycle) is active at a time.