- year, season, label
- start_date, end_date
- is_active
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.
Planner
A drag-and-drop timeline for scheduling work ahead of time.
Recorder
Timers, manual entries, and breaks — the dashboard where time gets logged.
Analytics
Charts, heatmaps, and streaks that rescale from a week to your whole history.
Chat & projects
Ask questions about your own data, and watch projects evolve across sessions.
Data & database
CSV import, category families, and the exact SQLite schema underneath.
Reference
Settings, the Claude API key, backups, and honest limitations.
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.
manual_entriesthe minutes
Plus the planner, hanging off the same categories
Every table, the columns that matter
Twelve core tables. id is the primary key everywhere; → marks a foreign key.
- name, display_name
- color, position
- is_system
- name, display_name, color
- group_id → groups
- name, display_name
- session_id → sessions
- family_id → families
- session_id, category_id
- date, start_time, end_time
- duration_minutes, is_paused
- plan_item_id → plan_items
- session_id, category_id
- date, duration_minutes
- description, start_time
- plan_item_id → plan_items
- title, notes, status
- start_date, end_date
- start_time, end_time, importance
- category_id → categories
- session_id → sessions
- date, week_number
- total_minutes
- daily_record_id, category_id
- minutes
- source (import/timer/manual)
- date (unique)
- label
- family_id → families
- match_type (exact / prefix)
- pattern
- 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.
Every other change, same discipline
Add a manual entry
Insert, then the same upsert-and-recompute as a timer stop. Identical downstream.
Edit an entry
Change the date, category, or duration and the aggregate rebalances: subtract from the old bucket, add to the new.
Delete an entry
Subtract from the aggregate first; the observation is dropped if it hits zero. Then the row goes.
Mark a break
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
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
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.
observationsis 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_idis a nullableSET NULLforeign 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.