Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Threadline — a habit ledger

A full-stack habit tracker built with Flask, SQLAlchemy, and vanilla JS. This is an enhanced rebuild of the original Habit Tracker project: same core idea, with a real design system, streaks, reports, and a friendlier data model.

What's new in this version

  • Accounts. Threadline now supports multiple people on one deployment — each person registers their own login, and habits, completions, and notes are private to their account. Enforced server-side on every API route, not just hidden in the UI.
  • Full front end. Four working pages (Dashboard, Calendar, Reports, Settings) with a custom "habit ledger" visual design — no template UI kit, no Bootstrap defaults.
  • Streak math, done properly. Current streak, best streak, and 30-day completion rate are computed server-side per habit, and there's a combined "every due habit, every day" overall streak on the dashboard.
  • Frequency-aware scheduling. Habits can be Daily, Weekdays, Weekends, or Custom (pick specific weekdays) — the calendar and streaks respect this, so a "Mon/Wed/Fri" habit isn't penalized for Tuesdays.
  • Monthly ledger view. A spreadsheet-style grid (habits × days) to tap through a whole month at once, styled like the README always promised.
  • Reports page. Weekly and monthly views — category breakdown, per-habit streak/rate bars, a trend vs. last week, and a month heatmap.
  • Journal. A dedicated page for daily notes; once a day ends, that entry is sealed and can't be edited — same rule as habit completions.
  • Days lock at midnight. Completions and journal entries can only be edited for today; once a day is over, it's a permanent record.
  • Archive instead of just delete. Retire a habit without losing its history; permanent delete is still available and asks for confirmation.
  • Daily reminders. Optional, local to your device — a nudge (in-app and as a browser notification) if you still have open habits at a time you set.
  • Installable on mobile. A Progressive Web App — add it to your home screen from Chrome or Safari for an app-like, full-screen experience.
  • Input validation + JSON error responses on every write endpoint.
  • Dark / light mode, remembered across visits.
  • Safe upgrades. If you already have a database.db from an earlier, single-user version of this app, startup adds the new columns and folds any pre-existing habits/notes into one fallback account automatically — nothing is deleted. See "Upgrading from the single-user version" below.

Setup

  1. Python 3.8+
  2. Install dependencies:
    pip install -r requirements.txt
  3. Run it:
    python app.py
  4. Open http://127.0.0.1:5000 and create an account — the first screen you'll see is the login page, with a link to register.

Project layout

app.py            Routes: auth, pages, JSON API
models.py         User / Habit / Completion / Note models, scheduling logic
stats.py          Streaks, completion rate, weekly/monthly report calculations
database.py       SQLAlchemy instance
templates/        Jinja pages (auth, dashboard, calendar, reports, settings, journal)
static/css/       Design system (tokens, layout, components)
static/js/        Page logic + shared helpers (api, toasts, modal, theme, reminders)
static/manifest.json, static/sw.js, static/icons/   PWA install support

API overview

Every /api/* route requires a logged-in session and only ever touches that account's own data (verified server-side, not just filtered in the UI).

Method Path Purpose
GET/POST /api/habits list / create habits
PUT /api/habits/<id> update a habit
PATCH /api/habits/<id>/archive archive / restore
DELETE /api/habits/<id> permanently delete
GET /api/completions completions in a date range
POST /api/completions/toggle mark/unmark a habit done — today only
GET/POST /api/notes read/write a day's journal entry — today only to write
GET /api/dashboard today's list + streaks + progress + insight
GET /api/habits/<id>/stats streaks + 30-day rate for one habit
GET /api/reports/weekly this week's numbers + trend vs. last week
GET /api/reports/overview category breakdown + heatmap for a month

Tech stack

  • Backend: Flask, Flask-SQLAlchemy, Flask-Login, SQLite (or Postgres — see Deploying below)
  • Frontend: Jinja templates, hand-written CSS design system, vanilla JS (no build step, no framework)
  • Fonts: Fraunces (display), Inter (body), JetBrains Mono (data)

Deploying to Railway

  1. Push this folder to a GitHub repo.
  2. On railway.app, New Project → Deploy from GitHub repo.
  3. Railway auto-detects the Procfile and installs requirements.txt — no build config needed.
  4. Add Postgres: in the project, New → Database → PostgreSQL. Railway creates a DATABASE_URL variable automatically; reference it from the web service under Variables → Add Reference. (SQLite works too if you'd rather skip this, but Railway's filesystem isn't guaranteed to persist across deploys — Postgres is the safer default.)
  5. If you added Postgres, uncomment psycopg2-binary in requirements.txt and redeploy.
  6. Set a SECRET_KEY variable to a long random string (e.g. python -c "import secrets; print(secrets.token_hex(32))"). Without this, the app generates a random one on each boot, which logs everyone out every time it restarts or redeploys.
  7. Railway gives you a *.up.railway.app HTTPS domain automatically — that's what makes the PWA install prompt available on phones.
  8. Open the domain, register the first account, and start tracking.

The same steps work on Render or Fly.io with minor UI differences — the Procfile and DATABASE_URL/SECRET_KEY env vars are what matter, not the specific host.

Upgrading from the single-user version

If you're moving an existing database.db from before accounts existed: copy it into instance/database.db next to app.py and start the app once. It adds the missing columns and creates a locked legacy@local account that owns all your old habits and notes — nothing is lost, but that account's password is randomly generated and not shared anywhere, so you can't log into it as-is. If you need to actually use that old data going forward, the simplest path is to open the database directly (sqlite3 instance/database.db) and update user.email / user.password_hash for that row to credentials you choose (use werkzeug.security.generate_password_hash for the hash) — or just start fresh with a new account, since this migration exists mainly so the upgrade doesn't silently delete anything.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages