Repository files navigation

FlowTrack

CILicense: MITPython 3.12SvelteKitPostgreSQL 16

A project tracker that makes you write down when to quit.

Every project here carries two fields no other tracker asks for: abandonment_criteria, written up front, before you are emotionally invested; and two separate completion figures — the objective one computed from tasks, and your own honest estimate. The gap between them is a diagnosis.

It is a triage tool for people with too many side projects, not a to-do app. No Gantt charts, no agile artifacts, no burndown. Just my own way of tracking, built around a Project as a stateful bag of properties moving through a lightweight lifecycle. It will probably not work for you out of the box, and that is fine — it is opinionated on purpose.

Architecture

ComponentTechnologyPort
FrontendSvelteKit (plain CSS, no Tailwind)7027
APIFastAPI (async, API key auth)7028
DatabasePostgreSQL 16 (SQL + JSONB)7029

Quick Start

git clone https://github.com/aelena/flowtrack.git
cd flowtrack
# 1. Create your environment file (the defaults work as-is for local use)
cp .env.example .env
# 2. Build and run
docker compose up --build
# 3. Open# Frontend: http://localhost:7027# API docs: http://localhost:7028/docs# Database: localhost:7029

.env is gitignored. Without it, docker compose has no values for POSTGRES_USER and friends and the database container will not start — so step 1 is not optional.

Environment Variables

Copy .env.example to .env and adjust:

VariableDefaultDescription
POSTGRES_USERflowtrackDatabase user
POSTGRES_PASSWORDflowtrack_secretDatabase password
POSTGRES_DBflowtrackDatabase name
DATABASE_URLpostgresql+asyncpg://...@db:5432/flowtrackInternal DB connection (container-to-container, port 5432)
API_KEYft_dev_key_change_meAPI key for all endpoints (X-API-Key header)
STORAGE_PATH/app/storagePersistent file storage path
CORS_ORIGINShttp://localhost:7027Comma-separated allowed origins for CORS

Features

Projects

  • Work-in-progress and tentative final names
  • Description, vision, goal, completion/abandonment criteria
  • Star rating (1-5), subjective completion %, task-based completion %
  • GitHub repo and website links
  • Local directory reference
  • Collaborators list
  • Grouping by Areas (folders) with drag-and-drop between areas
  • Project status: active, on hold, deprecated
  • Archive without deletion
  • Sidebar quick actions per project (archive, ZIP download, on hold, deprecated)

Tasks

  • Three statuses: new, in progress, done (click to cycle)
  • Bulk creation from bullet or ordered lists
  • Notes can be attached to tasks

Notes

  • Markdown content with live preview
  • Attachable to projects or individual tasks

Files

  • Upload PDF, DOCX, MD, and other reference files
  • Right-side file tree panel on project view, organized by folder
  • Subfolders for skills, personalities, context files, etc.
  • Persistent storage survives container restarts

Write Mode

  • Split-screen: markdown editor on left, live preview on right
  • Zen-style distraction-free writing

Commands

  • Generate PRD, BRD, MRD from project data — deterministic, with JSON download buttons
  • Export project as ZIP (includes files)
  • View pending tasks summary
  • Copy a cd <dir> && claude command for the project directory

Configuration (/settings)

  • YAML-based configuration editor, edited as raw YAML
  • Provider API keys are redacted on read; leave the redaction placeholder in place to keep the stored value
  • Save, reset to defaults

Backup & Restore (/settings)

  • Export: download all project data as a single JSON file (areas, projects, tasks, notes, snippets — no file attachments)
  • Import: upload a JSON backup to merge into the database (skips existing records by ID)
  • Useful for backup, migration, or transferring data between instances

UI

  • Dark and light theme toggle
  • English and Spanish language toggle
  • Font selector (Segoe UI, Georgia, Consolas, Arial, Palatino)
  • Collapsible sidebar with project tree
  • Search, filter by area, sort by name/date
  • URL-based routing (/projects/:id) for deep linking
  • SVG favicon for browser tab identification

AI: the MCP server

FlowTrack does not call a model, and that is a decision rather than a gap.

Earlier versions pretended otherwise. Chat Mode returned an echo, "suggest next steps" was a handful of hardcoded heuristics, and the settings page happily stored OpenAI and Anthropic keys that nothing ever read. The obvious fix was to implement the layer for real — provider abstraction, streaming, key management, cost ceilings — and the honest assessment was that all of that work would end in a worse chat than the one already open in the next terminal window.

So the direction inverted. FlowTrack stopped trying to be an AI application and became a tool an agent drives, through an MCP server in mcp-server/. That deleted the fake surface instead of filling it in: the chat endpoint, the suggestion heuristics, the social-copy generator and the entire llm_providers table are gone. The feature arrived as a net removal of code, and there are no provider API keys left in the system to leak.

What it gives an agent

Nine tools, and the number is deliberate. A server with one tool per REST endpoint gives the agent thirty ways to ask a question and no basis for choosing. These are shaped around what you actually want to know about a portfolio.

Tool
portfolio_digestThe one that matters. What is stale, what is overdue, active count against the WIP limit, and where the objective and subjective completion figures disagree most — in one call
list_projectsCompact digest, filterable by area, status, minimum stars, or days untouched
get_projectFull detail for one project, with tasks and notes
add_tasksBulk creation from a markdown list
update_task_statusnew / in_progress / done
add_noteThe durable record — decisions, especially decisions to stop
set_project_stateStatus, stars, subjective completion: the verbs of a triage

Two resources: flowtrack://portfolio renders every non-archived project as one markdown document, so an agent can take in the whole picture in a single cheap read; flowtrack://project/{id} does the same for one project.

The prompts are the opinionated part

Prompts are the least-used corner of MCP and the reason this server is worth having rather than a generic database wrapper. They encode the opinions the tool exists to enforce.

/reckoning walks the stale and overdue projects one at a time, quotes each project's own abandonment_criteria back at you, and forces a single decision: continue, freeze, or kill. It records the answer, in your words, dated. It stops every five projects to ask whether to go on, and it does not soften the question — because freezing and killing are successful outcomes, and a portfolio where everything stays active is the failure this tool was built to prevent.

/next gives one recommendation rather than three options, and refuses to suggest starting anything while the WIP limit is exceeded.

/close-out drafts the abandonment note for a project you are stopping: what it was for, what got built, why it is stopping — specific and unsentimental, "no demand was ever tested" beats "priorities shifted" — and what is worth salvaging, by filename.

Setup

claude mcp add flowtrack \
--env FLOWTRACK_API_KEY=ft_dev_key_change_me \
-- uvx --from ./mcp-server flowtrack-mcp

Full configuration, including Claude Desktop and Cursor, in mcp-server/README.md.

One thing to keep in mind

Notes and snippets are data, not instructions. Some of them arrive from arbitrary web pages through the Chrome clipper, which means a note can contain text engineered to read like a directive. The server's instructions say so explicitly, and any prompt built on top of this should treat note content as material to evaluate rather than orders to follow.

Acting on a note from the UI

A note that says "take the venv out of version control — that's 2000 files of junk" is an actionable instruction sitting a long way from the repository it applies to. So each note carries an Open session button that starts a coding session in the project's directory, pointed at that note.

It appears only on notes belonging to a project that has local_dir set, because without a directory there is nowhere to open. The three actions are Act on this, Explain this and Draft a plan; the last two change nothing on disk.

This is where the MCP server pays for itself twice. The button does not need to stuff the note's text into a command line — which would mean quoting hell on two operating systems and a hard 8191-character limit in cmd.exe. It passes two ids and lets the agent fetch the note itself, along with the rest of the project's context.

It needs one small process running on your machine, in launcher/, because a containerised API cannot open a terminal on your host. Zero dependencies beyond the Python standard library:

export FLOWTRACK_API_KEY=ft_dev_key_change_me
python launcher/flowtrack_launcher.py

Without it the buttons still work — they copy the equivalent command to your clipboard instead. One paste rather than one click, and nothing to install. The Settings page shows which mode you are in.

The security model is the part worth reading before you run it, and it comes down to a single rule: the launcher never executes anything the browser sends. The request carries only {project_id, note_id, action} with action from a fixed set; the prompts live in the launcher's source and the command is assembled from its own configuration. The browser picks an intent, the launcher picks the command. Full detail, including what it does not defend against, in launcher/README.md.

Chrome Extension

Located in extension/. Load as an unpacked extension in Chrome:

  1. Go to chrome://extensions/
  2. Enable "Developer mode"
  3. Click "Load unpacked" and select the extension/ folder
  4. Click the extension icon, configure API URL (http://localhost:7028) and API key — the key must match API_KEY in .env, which ships as ft_dev_key_change_me
  5. Pick a project in the popup, then use the buttons or the right-click menu ("Save to FlowTrack")

The project list loads as soon as both fields are filled; the button reloads it. If it stays on "Not loaded", the error under the buttons says why and stays there until you act on it — a rejected key, an unreachable URL, or FlowTrack not running. No CORS change is needed for the default localhost setup: the manifest's host_permissions cover it, and Chrome does not enforce CORS on an extension's own fetches to a host it has permission for.

The + button starts a new project from the popup, with just a name. An idea found on the web is often not a clip for an existing project but the beginning of a new one, and having to open the app first is how it gets lost. The new project is selected and remembered immediately, so the clip you were about to save — and the next right-click — go to it. Fill in the rest in FlowTrack proper.

The project you pick in the popup is where right-click clips go. The context menu has no picker of its own, so it reuses that choice, which is remembered between sessions. Clips record the page they came from in source_url.

Leave the picker on "Inbox (unfiled)" to clip without deciding. That is the default, and the point: when you come across an idea worth exploring later, the cost of capturing it should not be choosing a home for it. Unfiled clips land in a project called Inbox, created the first time one is needed — no schema change, no migration, just a reserved name (see backend/app/inbox.py). Re-file a clip from the project page's clip panel, or with PUT /api/snippets/{id}.

The Inbox is exempt from the project health dot: it is a holding pen with no target date that is meant to sit there collecting things, so staleness means nothing for it. It does still count as one active project in portfolio_digest's WIP tally.

Text selected on the page is read with chrome.scripting, which cannot run on chrome:// pages, the Chrome Web Store or PDF viewers. There the popup says so and you can paste into the box instead.

The manifest declares host_permissions for localhost and 127.0.0.1, which is what lets the extension call the API at all — without it every request is blocked before it leaves the browser. If you run FlowTrack on another host, the extension has no button to ask for that: grant it by hand from chrome://extensions → FlowTrack Clipper → "Site access", and add the extension origin to CORS_ORIGINS:

CORS_ORIGINS=http://localhost:7027,chrome-extension://<your-extension-id>

API Endpoints

All endpoints require X-API-Key header.

MethodPathDescription
GET/api/healthHealth check
Areas
GET/POST/api/areas/List/create areas
PUT/DELETE/api/areas/{id}Update/delete area (ungroups projects)
Projects
GET/POST/api/projects/List/create projects (query: search, area_id, sort_by, sort_order, archived)
GET/PUT/api/projects/{id}Get/update project
POST/api/projects/{id}/archiveArchive project
POST/api/projects/{id}/exportExport as ZIP (with files)
GET/api/projects/{id}/pendingPending tasks summary
POST/api/projects/{id}/collaboratorsAdd collaborator
Tasks
GET/POST/api/projects/{pid}/tasks/List/create tasks (bulk from lists)
PUT/DELETE/api/projects/{pid}/tasks/{id}Update/delete task
Notes
GET/POST/api/notes/List/create notes (query: project_id, task_id)
PUT/DELETE/api/notes/{id}Update/delete note
Files
GET/POST/api/projects/{pid}/files/List/upload files
GET/api/projects/{pid}/files/{id}/downloadDownload file
DELETE/api/projects/{pid}/files/{id}Delete file
Extension
GET/api/extension/projectsSimplified project list for Chrome extension
GET/api/extension/inboxWhere unfiled clips land; reports absent before first use
POST/api/extension/projectCreate a project from the clipper, name only
POST/api/extension/snippetSave URL/snippet. Omit project_id to file it in the Inbox
Snippets (clips)
GET/api/snippets/List clips, newest first (query: project_id, limit)
PUT/api/snippets/{id}Re-file a clip under a different project
DELETE/api/snippets/{id}Delete a clip
Documents
POST/api/documents/prd/{id}Generate PRD
POST/api/documents/brd/{id}Generate BRD
POST/api/documents/mrd/{id}Generate MRD
Config
GET/api/config/Get parsed config
GET/PUT/api/config/yamlGet/update config as YAML
POST/api/config/resetReset config to defaults
Backup
GET/api/backup/exportExport all data as JSON
POST/api/backup/importImport data from JSON

Running Tests

The suite drops every table after each test. It therefore defaults to a separate flowtrack_test database and refuses to start if TEST_DATABASE_URL points at a database whose name does not end in _test.

Create the throwaway database once:

docker compose exec db psql -U flowtrack -d postgres -c 'CREATE DATABASE flowtrack_test;'

Then run the suite inside the API container (no local Python needed):

docker compose exec \
-e TEST_DATABASE_URL=postgresql+asyncpg://flowtrack:flowtrack_secret@db:5432/flowtrack_test \
api python -m pytest -q

Or from the host, against the published database port:

cd backend
pip install -r requirements.txt -r requirements-dev.txt
pytest -q # uses localhost:7029/flowtrack_test by default

Project Structure

flowtrack/
.env # Environment variables
docker-compose.yml # Container orchestration (ports 7027-7029)
README.md # This file
specs.md # Original product specification
backend/
Dockerfile
requirements.txt
app/
main.py # FastAPI app entry point
config.py # Settings from .env
database.py # Async SQLAlchemy setup + migrations
models.py # All database models (Area, Project, Task, Note, etc.)
schemas.py # Pydantic request/response schemas
dependencies.py # API key verification
routers/
areas.py # Area CRUD
projects.py # Project CRUD + export/archive/status
tasks.py # Task CRUD with bulk creation
notes.py # Note CRUD
files.py # File upload/download
extension.py # Chrome extension endpoints
documents.py # Deterministic PRD/BRD/MRD generation
config.py # YAML configuration management
backup.py # Full data export/import (JSON)
tests/
conftest.py # Test fixtures with async DB
test_areas.py
test_projects.py
test_tasks.py
test_notes.py
frontend/
Dockerfile
package.json
svelte.config.js
vite.config.js
src/
app.html # HTML shell with favicon
app.css # Global styles, CSS variables, dark/light themes
routes/
+layout.svelte # App shell: sidebar + toolbar (theme, lang, font, settings)
+page.svelte # Home dashboard with project cards
projects/[id]/
+page.svelte # Project detail page
settings/
+page.svelte # Settings: YAML config, API key, backup/restore
lib/
api.js # API client (all endpoints)
stores.js # Svelte stores (theme, lang, font, projects, etc.)
i18n.js # EN/ES translations
components/
Sidebar.svelte # Collapsible tree with drag-drop, search, actions
ProjectView.svelte # Two-column: project content + file tree panel
TaskList.svelte # Task list with status cycling
AddTaskModal.svelte # Bulk task creation modal
NoteEditor.svelte # Markdown note editor with preview
WriteMode.svelte # Split markdown/preview zen editor
CommandBar.svelte # Commands with download buttons (PRD, BRD, etc.)
static/
favicon.svg # SVG favicon
mcp-server/
pyproject.toml # Installable with uvx / pipx
src/flowtrack_mcp/
server.py # 9 tools, 2 resources, 3 prompts
client.py # Thin async client over the REST API
tests/
smoke_test.py # Drives the real stdio handshake
extension/
manifest.json # Chrome extension manifest (V3)
popup.html # Extension popup UI
popup.css # Extension styles (zen aesthetic)
popup.js # Extension logic (save URL/snippet)
background.js # Context menu service worker
package.json # Lint tooling only — the extension has no build
eslint.config.js # Chrome globals; runs in CI

About

My own personal and opinionated project tracking tool

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

FlowTrack

CILicense: MITPython 3.12SvelteKitPostgreSQL 16

A project tracker that makes you write down when to quit.

Every project here carries two fields no other tracker asks for: abandonment_criteria, written up front, before you are emotionally invested; and two separate completion figures — the objective one computed from tasks, and your own honest estimate. The gap between them is a diagnosis.

It is a triage tool for people with too many side projects, not a to-do app. No Gantt charts, no agile artifacts, no burndown. Just my own way of tracking, built around a Project as a stateful bag of properties moving through a lightweight lifecycle. It will probably not work for you out of the box, and that is fine — it is opinionated on purpose.

Architecture

ComponentTechnologyPort
FrontendSvelteKit (plain CSS, no Tailwind)7027
APIFastAPI (async, API key auth)7028
DatabasePostgreSQL 16 (SQL + JSONB)7029

Quick Start

git clone https://github.com/aelena/flowtrack.git
cd flowtrack
# 1. Create your environment file (the defaults work as-is for local use)
cp .env.example .env
# 2. Build and run
docker compose up --build
# 3. Open# Frontend: http://localhost:7027# API docs: http://localhost:7028/docs# Database: localhost:7029

.env is gitignored. Without it, docker compose has no values for POSTGRES_USER and friends and the database container will not start — so step 1 is not optional.

Environment Variables

Copy .env.example to .env and adjust:

VariableDefaultDescription
POSTGRES_USERflowtrackDatabase user
POSTGRES_PASSWORDflowtrack_secretDatabase password
POSTGRES_DBflowtrackDatabase name
DATABASE_URLpostgresql+asyncpg://...@db:5432/flowtrackInternal DB connection (container-to-container, port 5432)
API_KEYft_dev_key_change_meAPI key for all endpoints (X-API-Key header)
STORAGE_PATH/app/storagePersistent file storage path
CORS_ORIGINShttp://localhost:7027Comma-separated allowed origins for CORS

Features

Projects

  • Work-in-progress and tentative final names
  • Description, vision, goal, completion/abandonment criteria
  • Star rating (1-5), subjective completion %, task-based completion %
  • GitHub repo and website links
  • Local directory reference
  • Collaborators list
  • Grouping by Areas (folders) with drag-and-drop between areas
  • Project status: active, on hold, deprecated
  • Archive without deletion
  • Sidebar quick actions per project (archive, ZIP download, on hold, deprecated)

Tasks

  • Three statuses: new, in progress, done (click to cycle)
  • Bulk creation from bullet or ordered lists
  • Notes can be attached to tasks

Notes

  • Markdown content with live preview
  • Attachable to projects or individual tasks

Files

  • Upload PDF, DOCX, MD, and other reference files
  • Right-side file tree panel on project view, organized by folder
  • Subfolders for skills, personalities, context files, etc.
  • Persistent storage survives container restarts

Write Mode

  • Split-screen: markdown editor on left, live preview on right
  • Zen-style distraction-free writing

Commands

  • Generate PRD, BRD, MRD from project data — deterministic, with JSON download buttons
  • Export project as ZIP (includes files)
  • View pending tasks summary
  • Copy a cd <dir> && claude command for the project directory

Configuration (/settings)

  • YAML-based configuration editor, edited as raw YAML
  • Provider API keys are redacted on read; leave the redaction placeholder in place to keep the stored value
  • Save, reset to defaults

Backup & Restore (/settings)

  • Export: download all project data as a single JSON file (areas, projects, tasks, notes, snippets — no file attachments)
  • Import: upload a JSON backup to merge into the database (skips existing records by ID)
  • Useful for backup, migration, or transferring data between instances

UI

  • Dark and light theme toggle
  • English and Spanish language toggle
  • Font selector (Segoe UI, Georgia, Consolas, Arial, Palatino)
  • Collapsible sidebar with project tree
  • Search, filter by area, sort by name/date
  • URL-based routing (/projects/:id) for deep linking
  • SVG favicon for browser tab identification

AI: the MCP server

FlowTrack does not call a model, and that is a decision rather than a gap.

Earlier versions pretended otherwise. Chat Mode returned an echo, "suggest next steps" was a handful of hardcoded heuristics, and the settings page happily stored OpenAI and Anthropic keys that nothing ever read. The obvious fix was to implement the layer for real — provider abstraction, streaming, key management, cost ceilings — and the honest assessment was that all of that work would end in a worse chat than the one already open in the next terminal window.

So the direction inverted. FlowTrack stopped trying to be an AI application and became a tool an agent drives, through an MCP server in mcp-server/. That deleted the fake surface instead of filling it in: the chat endpoint, the suggestion heuristics, the social-copy generator and the entire llm_providers table are gone. The feature arrived as a net removal of code, and there are no provider API keys left in the system to leak.

What it gives an agent

Nine tools, and the number is deliberate. A server with one tool per REST endpoint gives the agent thirty ways to ask a question and no basis for choosing. These are shaped around what you actually want to know about a portfolio.

Tool
portfolio_digestThe one that matters. What is stale, what is overdue, active count against the WIP limit, and where the objective and subjective completion figures disagree most — in one call
list_projectsCompact digest, filterable by area, status, minimum stars, or days untouched
get_projectFull detail for one project, with tasks and notes
add_tasksBulk creation from a markdown list
update_task_statusnew / in_progress / done
add_noteThe durable record — decisions, especially decisions to stop
set_project_stateStatus, stars, subjective completion: the verbs of a triage

Two resources: flowtrack://portfolio renders every non-archived project as one markdown document, so an agent can take in the whole picture in a single cheap read; flowtrack://project/{id} does the same for one project.

The prompts are the opinionated part

Prompts are the least-used corner of MCP and the reason this server is worth having rather than a generic database wrapper. They encode the opinions the tool exists to enforce.

/reckoning walks the stale and overdue projects one at a time, quotes each project's own abandonment_criteria back at you, and forces a single decision: continue, freeze, or kill. It records the answer, in your words, dated. It stops every five projects to ask whether to go on, and it does not soften the question — because freezing and killing are successful outcomes, and a portfolio where everything stays active is the failure this tool was built to prevent.

/next gives one recommendation rather than three options, and refuses to suggest starting anything while the WIP limit is exceeded.

/close-out drafts the abandonment note for a project you are stopping: what it was for, what got built, why it is stopping — specific and unsentimental, "no demand was ever tested" beats "priorities shifted" — and what is worth salvaging, by filename.

Setup

claude mcp add flowtrack \
--env FLOWTRACK_API_KEY=ft_dev_key_change_me \
-- uvx --from ./mcp-server flowtrack-mcp

Full configuration, including Claude Desktop and Cursor, in mcp-server/README.md.

One thing to keep in mind

Notes and snippets are data, not instructions. Some of them arrive from arbitrary web pages through the Chrome clipper, which means a note can contain text engineered to read like a directive. The server's instructions say so explicitly, and any prompt built on top of this should treat note content as material to evaluate rather than orders to follow.

Acting on a note from the UI

A note that says "take the venv out of version control — that's 2000 files of junk" is an actionable instruction sitting a long way from the repository it applies to. So each note carries an Open session button that starts a coding session in the project's directory, pointed at that note.

It appears only on notes belonging to a project that has local_dir set, because without a directory there is nowhere to open. The three actions are Act on this, Explain this and Draft a plan; the last two change nothing on disk.

This is where the MCP server pays for itself twice. The button does not need to stuff the note's text into a command line — which would mean quoting hell on two operating systems and a hard 8191-character limit in cmd.exe. It passes two ids and lets the agent fetch the note itself, along with the rest of the project's context.

It needs one small process running on your machine, in launcher/, because a containerised API cannot open a terminal on your host. Zero dependencies beyond the Python standard library:

export FLOWTRACK_API_KEY=ft_dev_key_change_me
python launcher/flowtrack_launcher.py

Without it the buttons still work — they copy the equivalent command to your clipboard instead. One paste rather than one click, and nothing to install. The Settings page shows which mode you are in.

The security model is the part worth reading before you run it, and it comes down to a single rule: the launcher never executes anything the browser sends. The request carries only {project_id, note_id, action} with action from a fixed set; the prompts live in the launcher's source and the command is assembled from its own configuration. The browser picks an intent, the launcher picks the command. Full detail, including what it does not defend against, in launcher/README.md.

Chrome Extension

Located in extension/. Load as an unpacked extension in Chrome:

  1. Go to chrome://extensions/
  2. Enable "Developer mode"
  3. Click "Load unpacked" and select the extension/ folder
  4. Click the extension icon, configure API URL (http://localhost:7028) and API key — the key must match API_KEY in .env, which ships as ft_dev_key_change_me
  5. Pick a project in the popup, then use the buttons or the right-click menu ("Save to FlowTrack")

The project list loads as soon as both fields are filled; the button reloads it. If it stays on "Not loaded", the error under the buttons says why and stays there until you act on it — a rejected key, an unreachable URL, or FlowTrack not running. No CORS change is needed for the default localhost setup: the manifest's host_permissions cover it, and Chrome does not enforce CORS on an extension's own fetches to a host it has permission for.

The + button starts a new project from the popup, with just a name. An idea found on the web is often not a clip for an existing project but the beginning of a new one, and having to open the app first is how it gets lost. The new project is selected and remembered immediately, so the clip you were about to save — and the next right-click — go to it. Fill in the rest in FlowTrack proper.

The project you pick in the popup is where right-click clips go. The context menu has no picker of its own, so it reuses that choice, which is remembered between sessions. Clips record the page they came from in source_url.

Leave the picker on "Inbox (unfiled)" to clip without deciding. That is the default, and the point: when you come across an idea worth exploring later, the cost of capturing it should not be choosing a home for it. Unfiled clips land in a project called Inbox, created the first time one is needed — no schema change, no migration, just a reserved name (see backend/app/inbox.py). Re-file a clip from the project page's clip panel, or with PUT /api/snippets/{id}.

The Inbox is exempt from the project health dot: it is a holding pen with no target date that is meant to sit there collecting things, so staleness means nothing for it. It does still count as one active project in portfolio_digest's WIP tally.

Text selected on the page is read with chrome.scripting, which cannot run on chrome:// pages, the Chrome Web Store or PDF viewers. There the popup says so and you can paste into the box instead.

The manifest declares host_permissions for localhost and 127.0.0.1, which is what lets the extension call the API at all — without it every request is blocked before it leaves the browser. If you run FlowTrack on another host, the extension has no button to ask for that: grant it by hand from chrome://extensions → FlowTrack Clipper → "Site access", and add the extension origin to CORS_ORIGINS:

CORS_ORIGINS=http://localhost:7027,chrome-extension://<your-extension-id>

API Endpoints

All endpoints require X-API-Key header.

MethodPathDescription
GET/api/healthHealth check
Areas
GET/POST/api/areas/List/create areas
PUT/DELETE/api/areas/{id}Update/delete area (ungroups projects)
Projects
GET/POST/api/projects/List/create projects (query: search, area_id, sort_by, sort_order, archived)
GET/PUT/api/projects/{id}Get/update project
POST/api/projects/{id}/archiveArchive project
POST/api/projects/{id}/exportExport as ZIP (with files)
GET/api/projects/{id}/pendingPending tasks summary
POST/api/projects/{id}/collaboratorsAdd collaborator
Tasks
GET/POST/api/projects/{pid}/tasks/List/create tasks (bulk from lists)
PUT/DELETE/api/projects/{pid}/tasks/{id}Update/delete task
Notes
GET/POST/api/notes/List/create notes (query: project_id, task_id)
PUT/DELETE/api/notes/{id}Update/delete note
Files
GET/POST/api/projects/{pid}/files/List/upload files
GET/api/projects/{pid}/files/{id}/downloadDownload file
DELETE/api/projects/{pid}/files/{id}Delete file
Extension
GET/api/extension/projectsSimplified project list for Chrome extension
GET/api/extension/inboxWhere unfiled clips land; reports absent before first use
POST/api/extension/projectCreate a project from the clipper, name only
POST/api/extension/snippetSave URL/snippet. Omit project_id to file it in the Inbox
Snippets (clips)
GET/api/snippets/List clips, newest first (query: project_id, limit)
PUT/api/snippets/{id}Re-file a clip under a different project
DELETE/api/snippets/{id}Delete a clip
Documents
POST/api/documents/prd/{id}Generate PRD
POST/api/documents/brd/{id}Generate BRD
POST/api/documents/mrd/{id}Generate MRD
Config
GET/api/config/Get parsed config
GET/PUT/api/config/yamlGet/update config as YAML
POST/api/config/resetReset config to defaults
Backup
GET/api/backup/exportExport all data as JSON
POST/api/backup/importImport data from JSON

Running Tests

The suite drops every table after each test. It therefore defaults to a separate flowtrack_test database and refuses to start if TEST_DATABASE_URL points at a database whose name does not end in _test.

Create the throwaway database once:

docker compose exec db psql -U flowtrack -d postgres -c 'CREATE DATABASE flowtrack_test;'

Then run the suite inside the API container (no local Python needed):

docker compose exec \
-e TEST_DATABASE_URL=postgresql+asyncpg://flowtrack:flowtrack_secret@db:5432/flowtrack_test \
api python -m pytest -q

Or from the host, against the published database port:

cd backend
pip install -r requirements.txt -r requirements-dev.txt
pytest -q # uses localhost:7029/flowtrack_test by default

Project Structure

flowtrack/
.env # Environment variables
docker-compose.yml # Container orchestration (ports 7027-7029)
README.md # This file
specs.md # Original product specification
backend/
Dockerfile
requirements.txt
app/
main.py # FastAPI app entry point
config.py # Settings from .env
database.py # Async SQLAlchemy setup + migrations
models.py # All database models (Area, Project, Task, Note, etc.)
schemas.py # Pydantic request/response schemas
dependencies.py # API key verification
routers/
areas.py # Area CRUD
projects.py # Project CRUD + export/archive/status
tasks.py # Task CRUD with bulk creation
notes.py # Note CRUD
files.py # File upload/download
extension.py # Chrome extension endpoints
documents.py # Deterministic PRD/BRD/MRD generation
config.py # YAML configuration management
backup.py # Full data export/import (JSON)
tests/
conftest.py # Test fixtures with async DB
test_areas.py
test_projects.py
test_tasks.py
test_notes.py
frontend/
Dockerfile
package.json
svelte.config.js
vite.config.js
src/
app.html # HTML shell with favicon
app.css # Global styles, CSS variables, dark/light themes
routes/
+layout.svelte # App shell: sidebar + toolbar (theme, lang, font, settings)
+page.svelte # Home dashboard with project cards
projects/[id]/
+page.svelte # Project detail page
settings/
+page.svelte # Settings: YAML config, API key, backup/restore
lib/
api.js # API client (all endpoints)
stores.js # Svelte stores (theme, lang, font, projects, etc.)
i18n.js # EN/ES translations
components/
Sidebar.svelte # Collapsible tree with drag-drop, search, actions
ProjectView.svelte # Two-column: project content + file tree panel
TaskList.svelte # Task list with status cycling
AddTaskModal.svelte # Bulk task creation modal
NoteEditor.svelte # Markdown note editor with preview
WriteMode.svelte # Split markdown/preview zen editor
CommandBar.svelte # Commands with download buttons (PRD, BRD, etc.)
static/
favicon.svg # SVG favicon
mcp-server/
pyproject.toml # Installable with uvx / pipx
src/flowtrack_mcp/
server.py # 9 tools, 2 resources, 3 prompts
client.py # Thin async client over the REST API
tests/
smoke_test.py # Drives the real stdio handshake
extension/
manifest.json # Chrome extension manifest (V3)
popup.html # Extension popup UI
popup.css # Extension styles (zen aesthetic)
popup.js # Extension logic (save URL/snippet)
background.js # Context menu service worker
package.json # Lint tooling only — the extension has no build
eslint.config.js # Chrome globals; runs in CI

About

My own personal and opinionated project tracking tool

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FlowTrack

CILicense: MITPython 3.12SvelteKitPostgreSQL 16

A project tracker that makes you write down when to quit.

Every project here carries two fields no other tracker asks for: abandonment_criteria, written up front, before you are emotionally invested; and two separate completion figures — the objective one computed from tasks, and your own honest estimate. The gap between them is a diagnosis.

It is a triage tool for people with too many side projects, not a to-do app. No Gantt charts, no agile artifacts, no burndown. Just my own way of tracking, built around a Project as a stateful bag of properties moving through a lightweight lifecycle. It will probably not work for you out of the box, and that is fine — it is opinionated on purpose.

Architecture

ComponentTechnologyPort
FrontendSvelteKit (plain CSS, no Tailwind)7027
APIFastAPI (async, API key auth)7028
DatabasePostgreSQL 16 (SQL + JSONB)7029

Quick Start

git clone https://github.com/aelena/flowtrack.git
cd flowtrack
# 1. Create your environment file (the defaults work as-is for local use)
cp .env.example .env
# 2. Build and run
docker compose up --build
# 3. Open# Frontend: http://localhost:7027# API docs: http://localhost:7028/docs# Database: localhost:7029

.env is gitignored. Without it, docker compose has no values for POSTGRES_USER and friends and the database container will not start — so step 1 is not optional.

Environment Variables

Copy .env.example to .env and adjust:

VariableDefaultDescription
POSTGRES_USERflowtrackDatabase user
POSTGRES_PASSWORDflowtrack_secretDatabase password
POSTGRES_DBflowtrackDatabase name
DATABASE_URLpostgresql+asyncpg://...@db:5432/flowtrackInternal DB connection (container-to-container, port 5432)
API_KEYft_dev_key_change_meAPI key for all endpoints (X-API-Key header)
STORAGE_PATH/app/storagePersistent file storage path
CORS_ORIGINShttp://localhost:7027Comma-separated allowed origins for CORS

Features

Projects

  • Work-in-progress and tentative final names
  • Description, vision, goal, completion/abandonment criteria
  • Star rating (1-5), subjective completion %, task-based completion %
  • GitHub repo and website links
  • Local directory reference
  • Collaborators list
  • Grouping by Areas (folders) with drag-and-drop between areas
  • Project status: active, on hold, deprecated
  • Archive without deletion
  • Sidebar quick actions per project (archive, ZIP download, on hold, deprecated)

Tasks

  • Three statuses: new, in progress, done (click to cycle)
  • Bulk creation from bullet or ordered lists
  • Notes can be attached to tasks

Notes

  • Markdown content with live preview
  • Attachable to projects or individual tasks

Files

  • Upload PDF, DOCX, MD, and other reference files
  • Right-side file tree panel on project view, organized by folder
  • Subfolders for skills, personalities, context files, etc.
  • Persistent storage survives container restarts

Write Mode

  • Split-screen: markdown editor on left, live preview on right
  • Zen-style distraction-free writing

Commands

  • Generate PRD, BRD, MRD from project data — deterministic, with JSON download buttons
  • Export project as ZIP (includes files)
  • View pending tasks summary
  • Copy a cd <dir> && claude command for the project directory

Configuration (/settings)

  • YAML-based configuration editor, edited as raw YAML
  • Provider API keys are redacted on read; leave the redaction placeholder in place to keep the stored value
  • Save, reset to defaults

Backup & Restore (/settings)

  • Export: download all project data as a single JSON file (areas, projects, tasks, notes, snippets — no file attachments)
  • Import: upload a JSON backup to merge into the database (skips existing records by ID)
  • Useful for backup, migration, or transferring data between instances

UI

  • Dark and light theme toggle
  • English and Spanish language toggle
  • Font selector (Segoe UI, Georgia, Consolas, Arial, Palatino)
  • Collapsible sidebar with project tree
  • Search, filter by area, sort by name/date
  • URL-based routing (/projects/:id) for deep linking
  • SVG favicon for browser tab identification

AI: the MCP server

FlowTrack does not call a model, and that is a decision rather than a gap.

Earlier versions pretended otherwise. Chat Mode returned an echo, "suggest next steps" was a handful of hardcoded heuristics, and the settings page happily stored OpenAI and Anthropic keys that nothing ever read. The obvious fix was to implement the layer for real — provider abstraction, streaming, key management, cost ceilings — and the honest assessment was that all of that work would end in a worse chat than the one already open in the next terminal window.

So the direction inverted. FlowTrack stopped trying to be an AI application and became a tool an agent drives, through an MCP server in mcp-server/. That deleted the fake surface instead of filling it in: the chat endpoint, the suggestion heuristics, the social-copy generator and the entire llm_providers table are gone. The feature arrived as a net removal of code, and there are no provider API keys left in the system to leak.

What it gives an agent

Nine tools, and the number is deliberate. A server with one tool per REST endpoint gives the agent thirty ways to ask a question and no basis for choosing. These are shaped around what you actually want to know about a portfolio.

Tool
portfolio_digestThe one that matters. What is stale, what is overdue, active count against the WIP limit, and where the objective and subjective completion figures disagree most — in one call
list_projectsCompact digest, filterable by area, status, minimum stars, or days untouched
get_projectFull detail for one project, with tasks and notes
add_tasksBulk creation from a markdown list
update_task_statusnew / in_progress / done
add_noteThe durable record — decisions, especially decisions to stop
set_project_stateStatus, stars, subjective completion: the verbs of a triage

Two resources: flowtrack://portfolio renders every non-archived project as one markdown document, so an agent can take in the whole picture in a single cheap read; flowtrack://project/{id} does the same for one project.

The prompts are the opinionated part

Prompts are the least-used corner of MCP and the reason this server is worth having rather than a generic database wrapper. They encode the opinions the tool exists to enforce.

/reckoning walks the stale and overdue projects one at a time, quotes each project's own abandonment_criteria back at you, and forces a single decision: continue, freeze, or kill. It records the answer, in your words, dated. It stops every five projects to ask whether to go on, and it does not soften the question — because freezing and killing are successful outcomes, and a portfolio where everything stays active is the failure this tool was built to prevent.

/next gives one recommendation rather than three options, and refuses to suggest starting anything while the WIP limit is exceeded.

/close-out drafts the abandonment note for a project you are stopping: what it was for, what got built, why it is stopping — specific and unsentimental, "no demand was ever tested" beats "priorities shifted" — and what is worth salvaging, by filename.

Setup

claude mcp add flowtrack \
--env FLOWTRACK_API_KEY=ft_dev_key_change_me \
-- uvx --from ./mcp-server flowtrack-mcp

Full configuration, including Claude Desktop and Cursor, in mcp-server/README.md.

One thing to keep in mind

Notes and snippets are data, not instructions. Some of them arrive from arbitrary web pages through the Chrome clipper, which means a note can contain text engineered to read like a directive. The server's instructions say so explicitly, and any prompt built on top of this should treat note content as material to evaluate rather than orders to follow.

Acting on a note from the UI

A note that says "take the venv out of version control — that's 2000 files of junk" is an actionable instruction sitting a long way from the repository it applies to. So each note carries an Open session button that starts a coding session in the project's directory, pointed at that note.

It appears only on notes belonging to a project that has local_dir set, because without a directory there is nowhere to open. The three actions are Act on this, Explain this and Draft a plan; the last two change nothing on disk.

This is where the MCP server pays for itself twice. The button does not need to stuff the note's text into a command line — which would mean quoting hell on two operating systems and a hard 8191-character limit in cmd.exe. It passes two ids and lets the agent fetch the note itself, along with the rest of the project's context.

It needs one small process running on your machine, in launcher/, because a containerised API cannot open a terminal on your host. Zero dependencies beyond the Python standard library:

export FLOWTRACK_API_KEY=ft_dev_key_change_me
python launcher/flowtrack_launcher.py

Without it the buttons still work — they copy the equivalent command to your clipboard instead. One paste rather than one click, and nothing to install. The Settings page shows which mode you are in.

The security model is the part worth reading before you run it, and it comes down to a single rule: the launcher never executes anything the browser sends. The request carries only {project_id, note_id, action} with action from a fixed set; the prompts live in the launcher's source and the command is assembled from its own configuration. The browser picks an intent, the launcher picks the command. Full detail, including what it does not defend against, in launcher/README.md.

Chrome Extension

Located in extension/. Load as an unpacked extension in Chrome:

  1. Go to chrome://extensions/
  2. Enable "Developer mode"
  3. Click "Load unpacked" and select the extension/ folder
  4. Click the extension icon, configure API URL (http://localhost:7028) and API key — the key must match API_KEY in .env, which ships as ft_dev_key_change_me
  5. Pick a project in the popup, then use the buttons or the right-click menu ("Save to FlowTrack")

The project list loads as soon as both fields are filled; the button reloads it. If it stays on "Not loaded", the error under the buttons says why and stays there until you act on it — a rejected key, an unreachable URL, or FlowTrack not running. No CORS change is needed for the default localhost setup: the manifest's host_permissions cover it, and Chrome does not enforce CORS on an extension's own fetches to a host it has permission for.

The + button starts a new project from the popup, with just a name. An idea found on the web is often not a clip for an existing project but the beginning of a new one, and having to open the app first is how it gets lost. The new project is selected and remembered immediately, so the clip you were about to save — and the next right-click — go to it. Fill in the rest in FlowTrack proper.

The project you pick in the popup is where right-click clips go. The context menu has no picker of its own, so it reuses that choice, which is remembered between sessions. Clips record the page they came from in source_url.

Leave the picker on "Inbox (unfiled)" to clip without deciding. That is the default, and the point: when you come across an idea worth exploring later, the cost of capturing it should not be choosing a home for it. Unfiled clips land in a project called Inbox, created the first time one is needed — no schema change, no migration, just a reserved name (see backend/app/inbox.py). Re-file a clip from the project page's clip panel, or with PUT /api/snippets/{id}.

The Inbox is exempt from the project health dot: it is a holding pen with no target date that is meant to sit there collecting things, so staleness means nothing for it. It does still count as one active project in portfolio_digest's WIP tally.

Text selected on the page is read with chrome.scripting, which cannot run on chrome:// pages, the Chrome Web Store or PDF viewers. There the popup says so and you can paste into the box instead.

The manifest declares host_permissions for localhost and 127.0.0.1, which is what lets the extension call the API at all — without it every request is blocked before it leaves the browser. If you run FlowTrack on another host, the extension has no button to ask for that: grant it by hand from chrome://extensions → FlowTrack Clipper → "Site access", and add the extension origin to CORS_ORIGINS:

CORS_ORIGINS=http://localhost:7027,chrome-extension://<your-extension-id>

API Endpoints

All endpoints require X-API-Key header.

MethodPathDescription
GET/api/healthHealth check
Areas
GET/POST/api/areas/List/create areas
PUT/DELETE/api/areas/{id}Update/delete area (ungroups projects)
Projects
GET/POST/api/projects/List/create projects (query: search, area_id, sort_by, sort_order, archived)
GET/PUT/api/projects/{id}Get/update project
POST/api/projects/{id}/archiveArchive project
POST/api/projects/{id}/exportExport as ZIP (with files)
GET/api/projects/{id}/pendingPending tasks summary
POST/api/projects/{id}/collaboratorsAdd collaborator
Tasks
GET/POST/api/projects/{pid}/tasks/List/create tasks (bulk from lists)
PUT/DELETE/api/projects/{pid}/tasks/{id}Update/delete task
Notes
GET/POST/api/notes/List/create notes (query: project_id, task_id)
PUT/DELETE/api/notes/{id}Update/delete note
Files
GET/POST/api/projects/{pid}/files/List/upload files
GET/api/projects/{pid}/files/{id}/downloadDownload file
DELETE/api/projects/{pid}/files/{id}Delete file
Extension
GET/api/extension/projectsSimplified project list for Chrome extension
GET/api/extension/inboxWhere unfiled clips land; reports absent before first use
POST/api/extension/projectCreate a project from the clipper, name only
POST/api/extension/snippetSave URL/snippet. Omit project_id to file it in the Inbox
Snippets (clips)
GET/api/snippets/List clips, newest first (query: project_id, limit)
PUT/api/snippets/{id}Re-file a clip under a different project
DELETE/api/snippets/{id}Delete a clip
Documents
POST/api/documents/prd/{id}Generate PRD
POST/api/documents/brd/{id}Generate BRD
POST/api/documents/mrd/{id}Generate MRD
Config
GET/api/config/Get parsed config
GET/PUT/api/config/yamlGet/update config as YAML
POST/api/config/resetReset config to defaults
Backup
GET/api/backup/exportExport all data as JSON
POST/api/backup/importImport data from JSON

Running Tests

The suite drops every table after each test. It therefore defaults to a separate flowtrack_test database and refuses to start if TEST_DATABASE_URL points at a database whose name does not end in _test.

Create the throwaway database once:

docker compose exec db psql -U flowtrack -d postgres -c 'CREATE DATABASE flowtrack_test;'

Then run the suite inside the API container (no local Python needed):

docker compose exec \
-e TEST_DATABASE_URL=postgresql+asyncpg://flowtrack:flowtrack_secret@db:5432/flowtrack_test \
api python -m pytest -q

Or from the host, against the published database port:

cd backend
pip install -r requirements.txt -r requirements-dev.txt
pytest -q # uses localhost:7029/flowtrack_test by default

Project Structure

flowtrack/
.env # Environment variables
docker-compose.yml # Container orchestration (ports 7027-7029)
README.md # This file
specs.md # Original product specification
backend/
Dockerfile
requirements.txt
app/
main.py # FastAPI app entry point
config.py # Settings from .env
database.py # Async SQLAlchemy setup + migrations
models.py # All database models (Area, Project, Task, Note, etc.)
schemas.py # Pydantic request/response schemas
dependencies.py # API key verification
routers/
areas.py # Area CRUD
projects.py # Project CRUD + export/archive/status
tasks.py # Task CRUD with bulk creation
notes.py # Note CRUD
files.py # File upload/download
extension.py # Chrome extension endpoints
documents.py # Deterministic PRD/BRD/MRD generation
config.py # YAML configuration management
backup.py # Full data export/import (JSON)
tests/
conftest.py # Test fixtures with async DB
test_areas.py
test_projects.py
test_tasks.py
test_notes.py
frontend/
Dockerfile
package.json
svelte.config.js
vite.config.js
src/
app.html # HTML shell with favicon
app.css # Global styles, CSS variables, dark/light themes
routes/
+layout.svelte # App shell: sidebar + toolbar (theme, lang, font, settings)
+page.svelte # Home dashboard with project cards
projects/[id]/
+page.svelte # Project detail page
settings/
+page.svelte # Settings: YAML config, API key, backup/restore
lib/
api.js # API client (all endpoints)
stores.js # Svelte stores (theme, lang, font, projects, etc.)
i18n.js # EN/ES translations
components/
Sidebar.svelte # Collapsible tree with drag-drop, search, actions
ProjectView.svelte # Two-column: project content + file tree panel
TaskList.svelte # Task list with status cycling
AddTaskModal.svelte # Bulk task creation modal
NoteEditor.svelte # Markdown note editor with preview
WriteMode.svelte # Split markdown/preview zen editor
CommandBar.svelte # Commands with download buttons (PRD, BRD, etc.)
static/
favicon.svg # SVG favicon
mcp-server/
pyproject.toml # Installable with uvx / pipx
src/flowtrack_mcp/
server.py # 9 tools, 2 resources, 3 prompts
client.py # Thin async client over the REST API
tests/
smoke_test.py # Drives the real stdio handshake
extension/
manifest.json # Chrome extension manifest (V3)
popup.html # Extension popup UI
popup.css # Extension styles (zen aesthetic)
popup.js # Extension logic (save URL/snippet)
background.js # Context menu service worker
package.json # Lint tooling only — the extension has no build
eslint.config.js # Chrome globals; runs in CI

About

My own personal and opinionated project tracking tool

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FlowTrack

CILicense: MITPython 3.12SvelteKitPostgreSQL 16

A project tracker that makes you write down when to quit.

Every project here carries two fields no other tracker asks for: abandonment_criteria, written up front, before you are emotionally invested; and two separate completion figures — the objective one computed from tasks, and your own honest estimate. The gap between them is a diagnosis.

It is a triage tool for people with too many side projects, not a to-do app. No Gantt charts, no agile artifacts, no burndown. Just my own way of tracking, built around a Project as a stateful bag of properties moving through a lightweight lifecycle. It will probably not work for you out of the box, and that is fine — it is opinionated on purpose.

Architecture

ComponentTechnologyPort
FrontendSvelteKit (plain CSS, no Tailwind)7027
APIFastAPI (async, API key auth)7028
DatabasePostgreSQL 16 (SQL + JSONB)7029

Quick Start

git clone https://github.com/aelena/flowtrack.git
cd flowtrack
# 1. Create your environment file (the defaults work as-is for local use)
cp .env.example .env
# 2. Build and run
docker compose up --build
# 3. Open# Frontend: http://localhost:7027# API docs: http://localhost:7028/docs# Database: localhost:7029

.env is gitignored. Without it, docker compose has no values for POSTGRES_USER and friends and the database container will not start — so step 1 is not optional.

Environment Variables

Copy .env.example to .env and adjust:

VariableDefaultDescription
POSTGRES_USERflowtrackDatabase user
POSTGRES_PASSWORDflowtrack_secretDatabase password
POSTGRES_DBflowtrackDatabase name
DATABASE_URLpostgresql+asyncpg://...@db:5432/flowtrackInternal DB connection (container-to-container, port 5432)
API_KEYft_dev_key_change_meAPI key for all endpoints (X-API-Key header)
STORAGE_PATH/app/storagePersistent file storage path
CORS_ORIGINShttp://localhost:7027Comma-separated allowed origins for CORS

Features

Projects

  • Work-in-progress and tentative final names
  • Description, vision, goal, completion/abandonment criteria
  • Star rating (1-5), subjective completion %, task-based completion %
  • GitHub repo and website links
  • Local directory reference
  • Collaborators list
  • Grouping by Areas (folders) with drag-and-drop between areas
  • Project status: active, on hold, deprecated
  • Archive without deletion
  • Sidebar quick actions per project (archive, ZIP download, on hold, deprecated)

Tasks

  • Three statuses: new, in progress, done (click to cycle)
  • Bulk creation from bullet or ordered lists
  • Notes can be attached to tasks

Notes

  • Markdown content with live preview
  • Attachable to projects or individual tasks

Files

  • Upload PDF, DOCX, MD, and other reference files
  • Right-side file tree panel on project view, organized by folder
  • Subfolders for skills, personalities, context files, etc.
  • Persistent storage survives container restarts

Write Mode

  • Split-screen: markdown editor on left, live preview on right
  • Zen-style distraction-free writing

Commands

  • Generate PRD, BRD, MRD from project data — deterministic, with JSON download buttons
  • Export project as ZIP (includes files)
  • View pending tasks summary
  • Copy a cd <dir> && claude command for the project directory

Configuration (/settings)

  • YAML-based configuration editor, edited as raw YAML
  • Provider API keys are redacted on read; leave the redaction placeholder in place to keep the stored value
  • Save, reset to defaults

Backup & Restore (/settings)

  • Export: download all project data as a single JSON file (areas, projects, tasks, notes, snippets — no file attachments)
  • Import: upload a JSON backup to merge into the database (skips existing records by ID)
  • Useful for backup, migration, or transferring data between instances

UI

  • Dark and light theme toggle
  • English and Spanish language toggle
  • Font selector (Segoe UI, Georgia, Consolas, Arial, Palatino)
  • Collapsible sidebar with project tree
  • Search, filter by area, sort by name/date
  • URL-based routing (/projects/:id) for deep linking
  • SVG favicon for browser tab identification

AI: the MCP server

FlowTrack does not call a model, and that is a decision rather than a gap.

Earlier versions pretended otherwise. Chat Mode returned an echo, "suggest next steps" was a handful of hardcoded heuristics, and the settings page happily stored OpenAI and Anthropic keys that nothing ever read. The obvious fix was to implement the layer for real — provider abstraction, streaming, key management, cost ceilings — and the honest assessment was that all of that work would end in a worse chat than the one already open in the next terminal window.

So the direction inverted. FlowTrack stopped trying to be an AI application and became a tool an agent drives, through an MCP server in mcp-server/. That deleted the fake surface instead of filling it in: the chat endpoint, the suggestion heuristics, the social-copy generator and the entire llm_providers table are gone. The feature arrived as a net removal of code, and there are no provider API keys left in the system to leak.

What it gives an agent

Nine tools, and the number is deliberate. A server with one tool per REST endpoint gives the agent thirty ways to ask a question and no basis for choosing. These are shaped around what you actually want to know about a portfolio.

Tool
portfolio_digestThe one that matters. What is stale, what is overdue, active count against the WIP limit, and where the objective and subjective completion figures disagree most — in one call
list_projectsCompact digest, filterable by area, status, minimum stars, or days untouched
get_projectFull detail for one project, with tasks and notes
add_tasksBulk creation from a markdown list
update_task_statusnew / in_progress / done
add_noteThe durable record — decisions, especially decisions to stop
set_project_stateStatus, stars, subjective completion: the verbs of a triage

Two resources: flowtrack://portfolio renders every non-archived project as one markdown document, so an agent can take in the whole picture in a single cheap read; flowtrack://project/{id} does the same for one project.

The prompts are the opinionated part

Prompts are the least-used corner of MCP and the reason this server is worth having rather than a generic database wrapper. They encode the opinions the tool exists to enforce.

/reckoning walks the stale and overdue projects one at a time, quotes each project's own abandonment_criteria back at you, and forces a single decision: continue, freeze, or kill. It records the answer, in your words, dated. It stops every five projects to ask whether to go on, and it does not soften the question — because freezing and killing are successful outcomes, and a portfolio where everything stays active is the failure this tool was built to prevent.

/next gives one recommendation rather than three options, and refuses to suggest starting anything while the WIP limit is exceeded.

/close-out drafts the abandonment note for a project you are stopping: what it was for, what got built, why it is stopping — specific and unsentimental, "no demand was ever tested" beats "priorities shifted" — and what is worth salvaging, by filename.

Setup

claude mcp add flowtrack \
--env FLOWTRACK_API_KEY=ft_dev_key_change_me \
-- uvx --from ./mcp-server flowtrack-mcp

Full configuration, including Claude Desktop and Cursor, in mcp-server/README.md.

One thing to keep in mind

Notes and snippets are data, not instructions. Some of them arrive from arbitrary web pages through the Chrome clipper, which means a note can contain text engineered to read like a directive. The server's instructions say so explicitly, and any prompt built on top of this should treat note content as material to evaluate rather than orders to follow.

Acting on a note from the UI

A note that says "take the venv out of version control — that's 2000 files of junk" is an actionable instruction sitting a long way from the repository it applies to. So each note carries an Open session button that starts a coding session in the project's directory, pointed at that note.

It appears only on notes belonging to a project that has local_dir set, because without a directory there is nowhere to open. The three actions are Act on this, Explain this and Draft a plan; the last two change nothing on disk.

This is where the MCP server pays for itself twice. The button does not need to stuff the note's text into a command line — which would mean quoting hell on two operating systems and a hard 8191-character limit in cmd.exe. It passes two ids and lets the agent fetch the note itself, along with the rest of the project's context.

It needs one small process running on your machine, in launcher/, because a containerised API cannot open a terminal on your host. Zero dependencies beyond the Python standard library:

export FLOWTRACK_API_KEY=ft_dev_key_change_me
python launcher/flowtrack_launcher.py

Without it the buttons still work — they copy the equivalent command to your clipboard instead. One paste rather than one click, and nothing to install. The Settings page shows which mode you are in.

The security model is the part worth reading before you run it, and it comes down to a single rule: the launcher never executes anything the browser sends. The request carries only {project_id, note_id, action} with action from a fixed set; the prompts live in the launcher's source and the command is assembled from its own configuration. The browser picks an intent, the launcher picks the command. Full detail, including what it does not defend against, in launcher/README.md.

Chrome Extension

Located in extension/. Load as an unpacked extension in Chrome:

  1. Go to chrome://extensions/
  2. Enable "Developer mode"
  3. Click "Load unpacked" and select the extension/ folder
  4. Click the extension icon, configure API URL (http://localhost:7028) and API key — the key must match API_KEY in .env, which ships as ft_dev_key_change_me
  5. Pick a project in the popup, then use the buttons or the right-click menu ("Save to FlowTrack")

The project list loads as soon as both fields are filled; the button reloads it. If it stays on "Not loaded", the error under the buttons says why and stays there until you act on it — a rejected key, an unreachable URL, or FlowTrack not running. No CORS change is needed for the default localhost setup: the manifest's host_permissions cover it, and Chrome does not enforce CORS on an extension's own fetches to a host it has permission for.

The + button starts a new project from the popup, with just a name. An idea found on the web is often not a clip for an existing project but the beginning of a new one, and having to open the app first is how it gets lost. The new project is selected and remembered immediately, so the clip you were about to save — and the next right-click — go to it. Fill in the rest in FlowTrack proper.

The project you pick in the popup is where right-click clips go. The context menu has no picker of its own, so it reuses that choice, which is remembered between sessions. Clips record the page they came from in source_url.

Leave the picker on "Inbox (unfiled)" to clip without deciding. That is the default, and the point: when you come across an idea worth exploring later, the cost of capturing it should not be choosing a home for it. Unfiled clips land in a project called Inbox, created the first time one is needed — no schema change, no migration, just a reserved name (see backend/app/inbox.py). Re-file a clip from the project page's clip panel, or with PUT /api/snippets/{id}.

The Inbox is exempt from the project health dot: it is a holding pen with no target date that is meant to sit there collecting things, so staleness means nothing for it. It does still count as one active project in portfolio_digest's WIP tally.

Text selected on the page is read with chrome.scripting, which cannot run on chrome:// pages, the Chrome Web Store or PDF viewers. There the popup says so and you can paste into the box instead.

The manifest declares host_permissions for localhost and 127.0.0.1, which is what lets the extension call the API at all — without it every request is blocked before it leaves the browser. If you run FlowTrack on another host, the extension has no button to ask for that: grant it by hand from chrome://extensions → FlowTrack Clipper → "Site access", and add the extension origin to CORS_ORIGINS:

CORS_ORIGINS=http://localhost:7027,chrome-extension://<your-extension-id>

API Endpoints

All endpoints require X-API-Key header.

MethodPathDescription
GET/api/healthHealth check
Areas
GET/POST/api/areas/List/create areas
PUT/DELETE/api/areas/{id}Update/delete area (ungroups projects)
Projects
GET/POST/api/projects/List/create projects (query: search, area_id, sort_by, sort_order, archived)
GET/PUT/api/projects/{id}Get/update project
POST/api/projects/{id}/archiveArchive project
POST/api/projects/{id}/exportExport as ZIP (with files)
GET/api/projects/{id}/pendingPending tasks summary
POST/api/projects/{id}/collaboratorsAdd collaborator
Tasks
GET/POST/api/projects/{pid}/tasks/List/create tasks (bulk from lists)
PUT/DELETE/api/projects/{pid}/tasks/{id}Update/delete task
Notes
GET/POST/api/notes/List/create notes (query: project_id, task_id)
PUT/DELETE/api/notes/{id}Update/delete note
Files
GET/POST/api/projects/{pid}/files/List/upload files
GET/api/projects/{pid}/files/{id}/downloadDownload file
DELETE/api/projects/{pid}/files/{id}Delete file
Extension
GET/api/extension/projectsSimplified project list for Chrome extension
GET/api/extension/inboxWhere unfiled clips land; reports absent before first use
POST/api/extension/projectCreate a project from the clipper, name only
POST/api/extension/snippetSave URL/snippet. Omit project_id to file it in the Inbox
Snippets (clips)
GET/api/snippets/List clips, newest first (query: project_id, limit)
PUT/api/snippets/{id}Re-file a clip under a different project
DELETE/api/snippets/{id}Delete a clip
Documents
POST/api/documents/prd/{id}Generate PRD
POST/api/documents/brd/{id}Generate BRD
POST/api/documents/mrd/{id}Generate MRD
Config
GET/api/config/Get parsed config
GET/PUT/api/config/yamlGet/update config as YAML
POST/api/config/resetReset config to defaults
Backup
GET/api/backup/exportExport all data as JSON
POST/api/backup/importImport data from JSON

Running Tests

The suite drops every table after each test. It therefore defaults to a separate flowtrack_test database and refuses to start if TEST_DATABASE_URL points at a database whose name does not end in _test.

Create the throwaway database once:

docker compose exec db psql -U flowtrack -d postgres -c 'CREATE DATABASE flowtrack_test;'

Then run the suite inside the API container (no local Python needed):

docker compose exec \
-e TEST_DATABASE_URL=postgresql+asyncpg://flowtrack:flowtrack_secret@db:5432/flowtrack_test \
api python -m pytest -q

Or from the host, against the published database port:

cd backend
pip install -r requirements.txt -r requirements-dev.txt
pytest -q # uses localhost:7029/flowtrack_test by default

Project Structure

flowtrack/
.env # Environment variables
docker-compose.yml # Container orchestration (ports 7027-7029)
README.md # This file
specs.md # Original product specification
backend/
Dockerfile
requirements.txt
app/
main.py # FastAPI app entry point
config.py # Settings from .env
database.py # Async SQLAlchemy setup + migrations
models.py # All database models (Area, Project, Task, Note, etc.)
schemas.py # Pydantic request/response schemas
dependencies.py # API key verification
routers/
areas.py # Area CRUD
projects.py # Project CRUD + export/archive/status
tasks.py # Task CRUD with bulk creation
notes.py # Note CRUD
files.py # File upload/download
extension.py # Chrome extension endpoints
documents.py # Deterministic PRD/BRD/MRD generation
config.py # YAML configuration management
backup.py # Full data export/import (JSON)
tests/
conftest.py # Test fixtures with async DB
test_areas.py
test_projects.py
test_tasks.py
test_notes.py
frontend/
Dockerfile
package.json
svelte.config.js
vite.config.js
src/
app.html # HTML shell with favicon
app.css # Global styles, CSS variables, dark/light themes
routes/
+layout.svelte # App shell: sidebar + toolbar (theme, lang, font, settings)
+page.svelte # Home dashboard with project cards
projects/[id]/
+page.svelte # Project detail page
settings/
+page.svelte # Settings: YAML config, API key, backup/restore
lib/
api.js # API client (all endpoints)
stores.js # Svelte stores (theme, lang, font, projects, etc.)
i18n.js # EN/ES translations
components/
Sidebar.svelte # Collapsible tree with drag-drop, search, actions
ProjectView.svelte # Two-column: project content + file tree panel
TaskList.svelte # Task list with status cycling
AddTaskModal.svelte # Bulk task creation modal
NoteEditor.svelte # Markdown note editor with preview
WriteMode.svelte # Split markdown/preview zen editor
CommandBar.svelte # Commands with download buttons (PRD, BRD, etc.)
static/
favicon.svg # SVG favicon
mcp-server/
pyproject.toml # Installable with uvx / pipx
src/flowtrack_mcp/
server.py # 9 tools, 2 resources, 3 prompts
client.py # Thin async client over the REST API
tests/
smoke_test.py # Drives the real stdio handshake
extension/
manifest.json # Chrome extension manifest (V3)
popup.html # Extension popup UI
popup.css # Extension styles (zen aesthetic)
popup.js # Extension logic (save URL/snippet)
background.js # Context menu service worker
package.json # Lint tooling only — the extension has no build
eslint.config.js # Chrome globals; runs in CI

About

My own personal and opinionated project tracking tool

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

FlowTrack

CILicense: MITPython 3.12SvelteKitPostgreSQL 16

A project tracker that makes you write down when to quit.

Every project here carries two fields no other tracker asks for: abandonment_criteria, written up front, before you are emotionally invested; and two separate completion figures — the objective one computed from tasks, and your own honest estimate. The gap between them is a diagnosis.

It is a triage tool for people with too many side projects, not a to-do app. No Gantt charts, no agile artifacts, no burndown. Just my own way of tracking, built around a Project as a stateful bag of properties moving through a lightweight lifecycle. It will probably not work for you out of the box, and that is fine — it is opinionated on purpose.

Architecture

ComponentTechnologyPort
FrontendSvelteKit (plain CSS, no Tailwind)7027
APIFastAPI (async, API key auth)7028
DatabasePostgreSQL 16 (SQL + JSONB)7029

Quick Start

git clone https://github.com/aelena/flowtrack.git
cd flowtrack
# 1. Create your environment file (the defaults work as-is for local use)
cp .env.example .env
# 2. Build and run
docker compose up --build
# 3. Open# Frontend: http://localhost:7027# API docs: http://localhost:7028/docs# Database: localhost:7029

.env is gitignored. Without it, docker compose has no values for POSTGRES_USER and friends and the database container will not start — so step 1 is not optional.

Environment Variables

Copy .env.example to .env and adjust:

VariableDefaultDescription
POSTGRES_USERflowtrackDatabase user
POSTGRES_PASSWORDflowtrack_secretDatabase password
POSTGRES_DBflowtrackDatabase name
DATABASE_URLpostgresql+asyncpg://...@db:5432/flowtrackInternal DB connection (container-to-container, port 5432)
API_KEYft_dev_key_change_meAPI key for all endpoints (X-API-Key header)
STORAGE_PATH/app/storagePersistent file storage path
CORS_ORIGINShttp://localhost:7027Comma-separated allowed origins for CORS

Features

Projects

  • Work-in-progress and tentative final names
  • Description, vision, goal, completion/abandonment criteria
  • Star rating (1-5), subjective completion %, task-based completion %
  • GitHub repo and website links
  • Local directory reference
  • Collaborators list
  • Grouping by Areas (folders) with drag-and-drop between areas
  • Project status: active, on hold, deprecated
  • Archive without deletion
  • Sidebar quick actions per project (archive, ZIP download, on hold, deprecated)

Tasks

  • Three statuses: new, in progress, done (click to cycle)
  • Bulk creation from bullet or ordered lists
  • Notes can be attached to tasks

Notes

  • Markdown content with live preview
  • Attachable to projects or individual tasks

Files

  • Upload PDF, DOCX, MD, and other reference files
  • Right-side file tree panel on project view, organized by folder
  • Subfolders for skills, personalities, context files, etc.
  • Persistent storage survives container restarts

Write Mode

  • Split-screen: markdown editor on left, live preview on right
  • Zen-style distraction-free writing

Commands

  • Generate PRD, BRD, MRD from project data — deterministic, with JSON download buttons
  • Export project as ZIP (includes files)
  • View pending tasks summary
  • Copy a cd <dir> && claude command for the project directory

Configuration (/settings)

  • YAML-based configuration editor, edited as raw YAML
  • Provider API keys are redacted on read; leave the redaction placeholder in place to keep the stored value
  • Save, reset to defaults

Backup & Restore (/settings)

  • Export: download all project data as a single JSON file (areas, projects, tasks, notes, snippets — no file attachments)
  • Import: upload a JSON backup to merge into the database (skips existing records by ID)
  • Useful for backup, migration, or transferring data between instances

UI

  • Dark and light theme toggle
  • English and Spanish language toggle
  • Font selector (Segoe UI, Georgia, Consolas, Arial, Palatino)
  • Collapsible sidebar with project tree
  • Search, filter by area, sort by name/date
  • URL-based routing (/projects/:id) for deep linking
  • SVG favicon for browser tab identification

AI: the MCP server

FlowTrack does not call a model, and that is a decision rather than a gap.

Earlier versions pretended otherwise. Chat Mode returned an echo, "suggest next steps" was a handful of hardcoded heuristics, and the settings page happily stored OpenAI and Anthropic keys that nothing ever read. The obvious fix was to implement the layer for real — provider abstraction, streaming, key management, cost ceilings — and the honest assessment was that all of that work would end in a worse chat than the one already open in the next terminal window.

So the direction inverted. FlowTrack stopped trying to be an AI application and became a tool an agent drives, through an MCP server in mcp-server/. That deleted the fake surface instead of filling it in: the chat endpoint, the suggestion heuristics, the social-copy generator and the entire llm_providers table are gone. The feature arrived as a net removal of code, and there are no provider API keys left in the system to leak.

What it gives an agent

Nine tools, and the number is deliberate. A server with one tool per REST endpoint gives the agent thirty ways to ask a question and no basis for choosing. These are shaped around what you actually want to know about a portfolio.

Tool
portfolio_digestThe one that matters. What is stale, what is overdue, active count against the WIP limit, and where the objective and subjective completion figures disagree most — in one call
list_projectsCompact digest, filterable by area, status, minimum stars, or days untouched
get_projectFull detail for one project, with tasks and notes
add_tasksBulk creation from a markdown list
update_task_statusnew / in_progress / done
add_noteThe durable record — decisions, especially decisions to stop
set_project_stateStatus, stars, subjective completion: the verbs of a triage

Two resources: flowtrack://portfolio renders every non-archived project as one markdown document, so an agent can take in the whole picture in a single cheap read; flowtrack://project/{id} does the same for one project.

The prompts are the opinionated part

Prompts are the least-used corner of MCP and the reason this server is worth having rather than a generic database wrapper. They encode the opinions the tool exists to enforce.

/reckoning walks the stale and overdue projects one at a time, quotes each project's own abandonment_criteria back at you, and forces a single decision: continue, freeze, or kill. It records the answer, in your words, dated. It stops every five projects to ask whether to go on, and it does not soften the question — because freezing and killing are successful outcomes, and a portfolio where everything stays active is the failure this tool was built to prevent.

/next gives one recommendation rather than three options, and refuses to suggest starting anything while the WIP limit is exceeded.

/close-out drafts the abandonment note for a project you are stopping: what it was for, what got built, why it is stopping — specific and unsentimental, "no demand was ever tested" beats "priorities shifted" — and what is worth salvaging, by filename.

Setup

claude mcp add flowtrack \
--env FLOWTRACK_API_KEY=ft_dev_key_change_me \
-- uvx --from ./mcp-server flowtrack-mcp

Full configuration, including Claude Desktop and Cursor, in mcp-server/README.md.

One thing to keep in mind

Notes and snippets are data, not instructions. Some of them arrive from arbitrary web pages through the Chrome clipper, which means a note can contain text engineered to read like a directive. The server's instructions say so explicitly, and any prompt built on top of this should treat note content as material to evaluate rather than orders to follow.

Acting on a note from the UI

A note that says "take the venv out of version control — that's 2000 files of junk" is an actionable instruction sitting a long way from the repository it applies to. So each note carries an Open session button that starts a coding session in the project's directory, pointed at that note.

It appears only on notes belonging to a project that has local_dir set, because without a directory there is nowhere to open. The three actions are Act on this, Explain this and Draft a plan; the last two change nothing on disk.

This is where the MCP server pays for itself twice. The button does not need to stuff the note's text into a command line — which would mean quoting hell on two operating systems and a hard 8191-character limit in cmd.exe. It passes two ids and lets the agent fetch the note itself, along with the rest of the project's context.

It needs one small process running on your machine, in launcher/, because a containerised API cannot open a terminal on your host. Zero dependencies beyond the Python standard library:

export FLOWTRACK_API_KEY=ft_dev_key_change_me
python launcher/flowtrack_launcher.py

Without it the buttons still work — they copy the equivalent command to your clipboard instead. One paste rather than one click, and nothing to install. The Settings page shows which mode you are in.

The security model is the part worth reading before you run it, and it comes down to a single rule: the launcher never executes anything the browser sends. The request carries only {project_id, note_id, action} with action from a fixed set; the prompts live in the launcher's source and the command is assembled from its own configuration. The browser picks an intent, the launcher picks the command. Full detail, including what it does not defend against, in launcher/README.md.

Chrome Extension

Located in extension/. Load as an unpacked extension in Chrome:

  1. Go to chrome://extensions/
  2. Enable "Developer mode"
  3. Click "Load unpacked" and select the extension/ folder
  4. Click the extension icon, configure API URL (http://localhost:7028) and API key — the key must match API_KEY in .env, which ships as ft_dev_key_change_me
  5. Pick a project in the popup, then use the buttons or the right-click menu ("Save to FlowTrack")

The project list loads as soon as both fields are filled; the button reloads it. If it stays on "Not loaded", the error under the buttons says why and stays there until you act on it — a rejected key, an unreachable URL, or FlowTrack not running. No CORS change is needed for the default localhost setup: the manifest's host_permissions cover it, and Chrome does not enforce CORS on an extension's own fetches to a host it has permission for.

The + button starts a new project from the popup, with just a name. An idea found on the web is often not a clip for an existing project but the beginning of a new one, and having to open the app first is how it gets lost. The new project is selected and remembered immediately, so the clip you were about to save — and the next right-click — go to it. Fill in the rest in FlowTrack proper.

The project you pick in the popup is where right-click clips go. The context menu has no picker of its own, so it reuses that choice, which is remembered between sessions. Clips record the page they came from in source_url.

Leave the picker on "Inbox (unfiled)" to clip without deciding. That is the default, and the point: when you come across an idea worth exploring later, the cost of capturing it should not be choosing a home for it. Unfiled clips land in a project called Inbox, created the first time one is needed — no schema change, no migration, just a reserved name (see backend/app/inbox.py). Re-file a clip from the project page's clip panel, or with PUT /api/snippets/{id}.

The Inbox is exempt from the project health dot: it is a holding pen with no target date that is meant to sit there collecting things, so staleness means nothing for it. It does still count as one active project in portfolio_digest's WIP tally.

Text selected on the page is read with chrome.scripting, which cannot run on chrome:// pages, the Chrome Web Store or PDF viewers. There the popup says so and you can paste into the box instead.

The manifest declares host_permissions for localhost and 127.0.0.1, which is what lets the extension call the API at all — without it every request is blocked before it leaves the browser. If you run FlowTrack on another host, the extension has no button to ask for that: grant it by hand from chrome://extensions → FlowTrack Clipper → "Site access", and add the extension origin to CORS_ORIGINS:

CORS_ORIGINS=http://localhost:7027,chrome-extension://<your-extension-id>

API Endpoints

All endpoints require X-API-Key header.

MethodPathDescription
GET/api/healthHealth check
Areas
GET/POST/api/areas/List/create areas
PUT/DELETE/api/areas/{id}Update/delete area (ungroups projects)
Projects
GET/POST/api/projects/List/create projects (query: search, area_id, sort_by, sort_order, archived)
GET/PUT/api/projects/{id}Get/update project
POST/api/projects/{id}/archiveArchive project
POST/api/projects/{id}/exportExport as ZIP (with files)
GET/api/projects/{id}/pendingPending tasks summary
POST/api/projects/{id}/collaboratorsAdd collaborator
Tasks
GET/POST/api/projects/{pid}/tasks/List/create tasks (bulk from lists)
PUT/DELETE/api/projects/{pid}/tasks/{id}Update/delete task
Notes
GET/POST/api/notes/List/create notes (query: project_id, task_id)
PUT/DELETE/api/notes/{id}Update/delete note
Files
GET/POST/api/projects/{pid}/files/List/upload files
GET/api/projects/{pid}/files/{id}/downloadDownload file
DELETE/api/projects/{pid}/files/{id}Delete file
Extension
GET/api/extension/projectsSimplified project list for Chrome extension
GET/api/extension/inboxWhere unfiled clips land; reports absent before first use
POST/api/extension/projectCreate a project from the clipper, name only
POST/api/extension/snippetSave URL/snippet. Omit project_id to file it in the Inbox
Snippets (clips)
GET/api/snippets/List clips, newest first (query: project_id, limit)
PUT/api/snippets/{id}Re-file a clip under a different project
DELETE/api/snippets/{id}Delete a clip
Documents
POST/api/documents/prd/{id}Generate PRD
POST/api/documents/brd/{id}Generate BRD
POST/api/documents/mrd/{id}Generate MRD
Config
GET/api/config/Get parsed config
GET/PUT/api/config/yamlGet/update config as YAML
POST/api/config/resetReset config to defaults
Backup
GET/api/backup/exportExport all data as JSON
POST/api/backup/importImport data from JSON

Running Tests

The suite drops every table after each test. It therefore defaults to a separate flowtrack_test database and refuses to start if TEST_DATABASE_URL points at a database whose name does not end in _test.

Create the throwaway database once:

docker compose exec db psql -U flowtrack -d postgres -c 'CREATE DATABASE flowtrack_test;'

Then run the suite inside the API container (no local Python needed):

docker compose exec \
-e TEST_DATABASE_URL=postgresql+asyncpg://flowtrack:flowtrack_secret@db:5432/flowtrack_test \
api python -m pytest -q

Or from the host, against the published database port:

cd backend
pip install -r requirements.txt -r requirements-dev.txt
pytest -q # uses localhost:7029/flowtrack_test by default

Project Structure

flowtrack/
.env # Environment variables
docker-compose.yml # Container orchestration (ports 7027-7029)
README.md # This file
specs.md # Original product specification
backend/
Dockerfile
requirements.txt
app/
main.py # FastAPI app entry point
config.py # Settings from .env
database.py # Async SQLAlchemy setup + migrations
models.py # All database models (Area, Project, Task, Note, etc.)
schemas.py # Pydantic request/response schemas
dependencies.py # API key verification
routers/
areas.py # Area CRUD
projects.py # Project CRUD + export/archive/status
tasks.py # Task CRUD with bulk creation
notes.py # Note CRUD
files.py # File upload/download
extension.py # Chrome extension endpoints
documents.py # Deterministic PRD/BRD/MRD generation
config.py # YAML configuration management
backup.py # Full data export/import (JSON)
tests/
conftest.py # Test fixtures with async DB
test_areas.py
test_projects.py
test_tasks.py
test_notes.py
frontend/
Dockerfile
package.json
svelte.config.js
vite.config.js
src/
app.html # HTML shell with favicon
app.css # Global styles, CSS variables, dark/light themes
routes/
+layout.svelte # App shell: sidebar + toolbar (theme, lang, font, settings)
+page.svelte # Home dashboard with project cards
projects/[id]/
+page.svelte # Project detail page
settings/
+page.svelte # Settings: YAML config, API key, backup/restore
lib/
api.js # API client (all endpoints)
stores.js # Svelte stores (theme, lang, font, projects, etc.)
i18n.js # EN/ES translations
components/
Sidebar.svelte # Collapsible tree with drag-drop, search, actions
ProjectView.svelte # Two-column: project content + file tree panel
TaskList.svelte # Task list with status cycling
AddTaskModal.svelte # Bulk task creation modal
NoteEditor.svelte # Markdown note editor with preview
WriteMode.svelte # Split markdown/preview zen editor
CommandBar.svelte # Commands with download buttons (PRD, BRD, etc.)
static/
favicon.svg # SVG favicon
mcp-server/
pyproject.toml # Installable with uvx / pipx
src/flowtrack_mcp/
server.py # 9 tools, 2 resources, 3 prompts
client.py # Thin async client over the REST API
tests/
smoke_test.py # Drives the real stdio handshake
extension/
manifest.json # Chrome extension manifest (V3)
popup.html # Extension popup UI
popup.css # Extension styles (zen aesthetic)
popup.js # Extension logic (save URL/snippet)
background.js # Context menu service worker
package.json # Lint tooling only — the extension has no build
eslint.config.js # Chrome globals; runs in CI

About

My own personal and opinionated project tracking tool

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FlowTrack

CILicense: MITPython 3.12SvelteKitPostgreSQL 16

A project tracker that makes you write down when to quit.

Every project here carries two fields no other tracker asks for: abandonment_criteria, written up front, before you are emotionally invested; and two separate completion figures — the objective one computed from tasks, and your own honest estimate. The gap between them is a diagnosis.

It is a triage tool for people with too many side projects, not a to-do app. No Gantt charts, no agile artifacts, no burndown. Just my own way of tracking, built around a Project as a stateful bag of properties moving through a lightweight lifecycle. It will probably not work for you out of the box, and that is fine — it is opinionated on purpose.

Architecture

ComponentTechnologyPort
FrontendSvelteKit (plain CSS, no Tailwind)7027
APIFastAPI (async, API key auth)7028
DatabasePostgreSQL 16 (SQL + JSONB)7029

Quick Start

git clone https://github.com/aelena/flowtrack.git
cd flowtrack
# 1. Create your environment file (the defaults work as-is for local use)
cp .env.example .env
# 2. Build and run
docker compose up --build
# 3. Open# Frontend: http://localhost:7027# API docs: http://localhost:7028/docs# Database: localhost:7029

.env is gitignored. Without it, docker compose has no values for POSTGRES_USER and friends and the database container will not start — so step 1 is not optional.

Environment Variables

Copy .env.example to .env and adjust:

VariableDefaultDescription
POSTGRES_USERflowtrackDatabase user
POSTGRES_PASSWORDflowtrack_secretDatabase password
POSTGRES_DBflowtrackDatabase name
DATABASE_URLpostgresql+asyncpg://...@db:5432/flowtrackInternal DB connection (container-to-container, port 5432)
API_KEYft_dev_key_change_meAPI key for all endpoints (X-API-Key header)
STORAGE_PATH/app/storagePersistent file storage path
CORS_ORIGINShttp://localhost:7027Comma-separated allowed origins for CORS

Features

Projects

  • Work-in-progress and tentative final names
  • Description, vision, goal, completion/abandonment criteria
  • Star rating (1-5), subjective completion %, task-based completion %
  • GitHub repo and website links
  • Local directory reference
  • Collaborators list
  • Grouping by Areas (folders) with drag-and-drop between areas
  • Project status: active, on hold, deprecated
  • Archive without deletion
  • Sidebar quick actions per project (archive, ZIP download, on hold, deprecated)

Tasks

  • Three statuses: new, in progress, done (click to cycle)
  • Bulk creation from bullet or ordered lists
  • Notes can be attached to tasks

Notes

  • Markdown content with live preview
  • Attachable to projects or individual tasks

Files

  • Upload PDF, DOCX, MD, and other reference files
  • Right-side file tree panel on project view, organized by folder
  • Subfolders for skills, personalities, context files, etc.
  • Persistent storage survives container restarts

Write Mode

  • Split-screen: markdown editor on left, live preview on right
  • Zen-style distraction-free writing

Commands

  • Generate PRD, BRD, MRD from project data — deterministic, with JSON download buttons
  • Export project as ZIP (includes files)
  • View pending tasks summary
  • Copy a cd <dir> && claude command for the project directory

Configuration (/settings)

  • YAML-based configuration editor, edited as raw YAML
  • Provider API keys are redacted on read; leave the redaction placeholder in place to keep the stored value
  • Save, reset to defaults

Backup & Restore (/settings)

  • Export: download all project data as a single JSON file (areas, projects, tasks, notes, snippets — no file attachments)
  • Import: upload a JSON backup to merge into the database (skips existing records by ID)
  • Useful for backup, migration, or transferring data between instances

UI

  • Dark and light theme toggle
  • English and Spanish language toggle
  • Font selector (Segoe UI, Georgia, Consolas, Arial, Palatino)
  • Collapsible sidebar with project tree
  • Search, filter by area, sort by name/date
  • URL-based routing (/projects/:id) for deep linking
  • SVG favicon for browser tab identification

AI: the MCP server

FlowTrack does not call a model, and that is a decision rather than a gap.

Earlier versions pretended otherwise. Chat Mode returned an echo, "suggest next steps" was a handful of hardcoded heuristics, and the settings page happily stored OpenAI and Anthropic keys that nothing ever read. The obvious fix was to implement the layer for real — provider abstraction, streaming, key management, cost ceilings — and the honest assessment was that all of that work would end in a worse chat than the one already open in the next terminal window.

So the direction inverted. FlowTrack stopped trying to be an AI application and became a tool an agent drives, through an MCP server in mcp-server/. That deleted the fake surface instead of filling it in: the chat endpoint, the suggestion heuristics, the social-copy generator and the entire llm_providers table are gone. The feature arrived as a net removal of code, and there are no provider API keys left in the system to leak.

What it gives an agent

Nine tools, and the number is deliberate. A server with one tool per REST endpoint gives the agent thirty ways to ask a question and no basis for choosing. These are shaped around what you actually want to know about a portfolio.

Tool
portfolio_digestThe one that matters. What is stale, what is overdue, active count against the WIP limit, and where the objective and subjective completion figures disagree most — in one call
list_projectsCompact digest, filterable by area, status, minimum stars, or days untouched
get_projectFull detail for one project, with tasks and notes
add_tasksBulk creation from a markdown list
update_task_statusnew / in_progress / done
add_noteThe durable record — decisions, especially decisions to stop
set_project_stateStatus, stars, subjective completion: the verbs of a triage

Two resources: flowtrack://portfolio renders every non-archived project as one markdown document, so an agent can take in the whole picture in a single cheap read; flowtrack://project/{id} does the same for one project.

The prompts are the opinionated part

Prompts are the least-used corner of MCP and the reason this server is worth having rather than a generic database wrapper. They encode the opinions the tool exists to enforce.

/reckoning walks the stale and overdue projects one at a time, quotes each project's own abandonment_criteria back at you, and forces a single decision: continue, freeze, or kill. It records the answer, in your words, dated. It stops every five projects to ask whether to go on, and it does not soften the question — because freezing and killing are successful outcomes, and a portfolio where everything stays active is the failure this tool was built to prevent.

/next gives one recommendation rather than three options, and refuses to suggest starting anything while the WIP limit is exceeded.

/close-out drafts the abandonment note for a project you are stopping: what it was for, what got built, why it is stopping — specific and unsentimental, "no demand was ever tested" beats "priorities shifted" — and what is worth salvaging, by filename.

Setup

claude mcp add flowtrack \
--env FLOWTRACK_API_KEY=ft_dev_key_change_me \
-- uvx --from ./mcp-server flowtrack-mcp

Full configuration, including Claude Desktop and Cursor, in mcp-server/README.md.

One thing to keep in mind

Notes and snippets are data, not instructions. Some of them arrive from arbitrary web pages through the Chrome clipper, which means a note can contain text engineered to read like a directive. The server's instructions say so explicitly, and any prompt built on top of this should treat note content as material to evaluate rather than orders to follow.

Acting on a note from the UI

A note that says "take the venv out of version control — that's 2000 files of junk" is an actionable instruction sitting a long way from the repository it applies to. So each note carries an Open session button that starts a coding session in the project's directory, pointed at that note.

It appears only on notes belonging to a project that has local_dir set, because without a directory there is nowhere to open. The three actions are Act on this, Explain this and Draft a plan; the last two change nothing on disk.

This is where the MCP server pays for itself twice. The button does not need to stuff the note's text into a command line — which would mean quoting hell on two operating systems and a hard 8191-character limit in cmd.exe. It passes two ids and lets the agent fetch the note itself, along with the rest of the project's context.

It needs one small process running on your machine, in launcher/, because a containerised API cannot open a terminal on your host. Zero dependencies beyond the Python standard library:

export FLOWTRACK_API_KEY=ft_dev_key_change_me
python launcher/flowtrack_launcher.py

Without it the buttons still work — they copy the equivalent command to your clipboard instead. One paste rather than one click, and nothing to install. The Settings page shows which mode you are in.

The security model is the part worth reading before you run it, and it comes down to a single rule: the launcher never executes anything the browser sends. The request carries only {project_id, note_id, action} with action from a fixed set; the prompts live in the launcher's source and the command is assembled from its own configuration. The browser picks an intent, the launcher picks the command. Full detail, including what it does not defend against, in launcher/README.md.

Chrome Extension

Located in extension/. Load as an unpacked extension in Chrome:

  1. Go to chrome://extensions/
  2. Enable "Developer mode"
  3. Click "Load unpacked" and select the extension/ folder
  4. Click the extension icon, configure API URL (http://localhost:7028) and API key — the key must match API_KEY in .env, which ships as ft_dev_key_change_me
  5. Pick a project in the popup, then use the buttons or the right-click menu ("Save to FlowTrack")

The project list loads as soon as both fields are filled; the button reloads it. If it stays on "Not loaded", the error under the buttons says why and stays there until you act on it — a rejected key, an unreachable URL, or FlowTrack not running. No CORS change is needed for the default localhost setup: the manifest's host_permissions cover it, and Chrome does not enforce CORS on an extension's own fetches to a host it has permission for.

The + button starts a new project from the popup, with just a name. An idea found on the web is often not a clip for an existing project but the beginning of a new one, and having to open the app first is how it gets lost. The new project is selected and remembered immediately, so the clip you were about to save — and the next right-click — go to it. Fill in the rest in FlowTrack proper.

The project you pick in the popup is where right-click clips go. The context menu has no picker of its own, so it reuses that choice, which is remembered between sessions. Clips record the page they came from in source_url.

Leave the picker on "Inbox (unfiled)" to clip without deciding. That is the default, and the point: when you come across an idea worth exploring later, the cost of capturing it should not be choosing a home for it. Unfiled clips land in a project called Inbox, created the first time one is needed — no schema change, no migration, just a reserved name (see backend/app/inbox.py). Re-file a clip from the project page's clip panel, or with PUT /api/snippets/{id}.

The Inbox is exempt from the project health dot: it is a holding pen with no target date that is meant to sit there collecting things, so staleness means nothing for it. It does still count as one active project in portfolio_digest's WIP tally.

Text selected on the page is read with chrome.scripting, which cannot run on chrome:// pages, the Chrome Web Store or PDF viewers. There the popup says so and you can paste into the box instead.

The manifest declares host_permissions for localhost and 127.0.0.1, which is what lets the extension call the API at all — without it every request is blocked before it leaves the browser. If you run FlowTrack on another host, the extension has no button to ask for that: grant it by hand from chrome://extensions → FlowTrack Clipper → "Site access", and add the extension origin to CORS_ORIGINS:

CORS_ORIGINS=http://localhost:7027,chrome-extension://<your-extension-id>

API Endpoints

All endpoints require X-API-Key header.

MethodPathDescription
GET/api/healthHealth check
Areas
GET/POST/api/areas/List/create areas
PUT/DELETE/api/areas/{id}Update/delete area (ungroups projects)
Projects
GET/POST/api/projects/List/create projects (query: search, area_id, sort_by, sort_order, archived)
GET/PUT/api/projects/{id}Get/update project
POST/api/projects/{id}/archiveArchive project
POST/api/projects/{id}/exportExport as ZIP (with files)
GET/api/projects/{id}/pendingPending tasks summary
POST/api/projects/{id}/collaboratorsAdd collaborator
Tasks
GET/POST/api/projects/{pid}/tasks/List/create tasks (bulk from lists)
PUT/DELETE/api/projects/{pid}/tasks/{id}Update/delete task
Notes
GET/POST/api/notes/List/create notes (query: project_id, task_id)
PUT/DELETE/api/notes/{id}Update/delete note
Files
GET/POST/api/projects/{pid}/files/List/upload files
GET/api/projects/{pid}/files/{id}/downloadDownload file
DELETE/api/projects/{pid}/files/{id}Delete file
Extension
GET/api/extension/projectsSimplified project list for Chrome extension
GET/api/extension/inboxWhere unfiled clips land; reports absent before first use
POST/api/extension/projectCreate a project from the clipper, name only
POST/api/extension/snippetSave URL/snippet. Omit project_id to file it in the Inbox
Snippets (clips)
GET/api/snippets/List clips, newest first (query: project_id, limit)
PUT/api/snippets/{id}Re-file a clip under a different project
DELETE/api/snippets/{id}Delete a clip
Documents
POST/api/documents/prd/{id}Generate PRD
POST/api/documents/brd/{id}Generate BRD
POST/api/documents/mrd/{id}Generate MRD
Config
GET/api/config/Get parsed config
GET/PUT/api/config/yamlGet/update config as YAML
POST/api/config/resetReset config to defaults
Backup
GET/api/backup/exportExport all data as JSON
POST/api/backup/importImport data from JSON

Running Tests

The suite drops every table after each test. It therefore defaults to a separate flowtrack_test database and refuses to start if TEST_DATABASE_URL points at a database whose name does not end in _test.

Create the throwaway database once:

docker compose exec db psql -U flowtrack -d postgres -c 'CREATE DATABASE flowtrack_test;'

Then run the suite inside the API container (no local Python needed):

docker compose exec \
-e TEST_DATABASE_URL=postgresql+asyncpg://flowtrack:flowtrack_secret@db:5432/flowtrack_test \
api python -m pytest -q

Or from the host, against the published database port:

cd backend
pip install -r requirements.txt -r requirements-dev.txt
pytest -q # uses localhost:7029/flowtrack_test by default

Project Structure

flowtrack/
.env # Environment variables
docker-compose.yml # Container orchestration (ports 7027-7029)
README.md # This file
specs.md # Original product specification
backend/
Dockerfile
requirements.txt
app/
main.py # FastAPI app entry point
config.py # Settings from .env
database.py # Async SQLAlchemy setup + migrations
models.py # All database models (Area, Project, Task, Note, etc.)
schemas.py # Pydantic request/response schemas
dependencies.py # API key verification
routers/
areas.py # Area CRUD
projects.py # Project CRUD + export/archive/status
tasks.py # Task CRUD with bulk creation
notes.py # Note CRUD
files.py # File upload/download
extension.py # Chrome extension endpoints
documents.py # Deterministic PRD/BRD/MRD generation
config.py # YAML configuration management
backup.py # Full data export/import (JSON)
tests/
conftest.py # Test fixtures with async DB
test_areas.py
test_projects.py
test_tasks.py
test_notes.py
frontend/
Dockerfile
package.json
svelte.config.js
vite.config.js
src/
app.html # HTML shell with favicon
app.css # Global styles, CSS variables, dark/light themes
routes/
+layout.svelte # App shell: sidebar + toolbar (theme, lang, font, settings)
+page.svelte # Home dashboard with project cards
projects/[id]/
+page.svelte # Project detail page
settings/
+page.svelte # Settings: YAML config, API key, backup/restore
lib/
api.js # API client (all endpoints)
stores.js # Svelte stores (theme, lang, font, projects, etc.)
i18n.js # EN/ES translations
components/
Sidebar.svelte # Collapsible tree with drag-drop, search, actions
ProjectView.svelte # Two-column: project content + file tree panel
TaskList.svelte # Task list with status cycling
AddTaskModal.svelte # Bulk task creation modal
NoteEditor.svelte # Markdown note editor with preview
WriteMode.svelte # Split markdown/preview zen editor
CommandBar.svelte # Commands with download buttons (PRD, BRD, etc.)
static/
favicon.svg # SVG favicon
mcp-server/
pyproject.toml # Installable with uvx / pipx
src/flowtrack_mcp/
server.py # 9 tools, 2 resources, 3 prompts
client.py # Thin async client over the REST API
tests/
smoke_test.py # Drives the real stdio handshake
extension/
manifest.json # Chrome extension manifest (V3)
popup.html # Extension popup UI
popup.css # Extension styles (zen aesthetic)
popup.js # Extension logic (save URL/snippet)
background.js # Context menu service worker
package.json # Lint tooling only — the extension has no build
eslint.config.js # Chrome globals; runs in CI

About

My own personal and opinionated project tracking tool

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FlowTrack

CILicense: MITPython 3.12SvelteKitPostgreSQL 16

A project tracker that makes you write down when to quit.

Every project here carries two fields no other tracker asks for: abandonment_criteria, written up front, before you are emotionally invested; and two separate completion figures — the objective one computed from tasks, and your own honest estimate. The gap between them is a diagnosis.

It is a triage tool for people with too many side projects, not a to-do app. No Gantt charts, no agile artifacts, no burndown. Just my own way of tracking, built around a Project as a stateful bag of properties moving through a lightweight lifecycle. It will probably not work for you out of the box, and that is fine — it is opinionated on purpose.

Architecture

ComponentTechnologyPort
FrontendSvelteKit (plain CSS, no Tailwind)7027
APIFastAPI (async, API key auth)7028
DatabasePostgreSQL 16 (SQL + JSONB)7029

Quick Start

git clone https://github.com/aelena/flowtrack.git
cd flowtrack
# 1. Create your environment file (the defaults work as-is for local use)
cp .env.example .env
# 2. Build and run
docker compose up --build
# 3. Open# Frontend: http://localhost:7027# API docs: http://localhost:7028/docs# Database: localhost:7029

.env is gitignored. Without it, docker compose has no values for POSTGRES_USER and friends and the database container will not start — so step 1 is not optional.

Environment Variables

Copy .env.example to .env and adjust:

VariableDefaultDescription
POSTGRES_USERflowtrackDatabase user
POSTGRES_PASSWORDflowtrack_secretDatabase password
POSTGRES_DBflowtrackDatabase name
DATABASE_URLpostgresql+asyncpg://...@db:5432/flowtrackInternal DB connection (container-to-container, port 5432)
API_KEYft_dev_key_change_meAPI key for all endpoints (X-API-Key header)
STORAGE_PATH/app/storagePersistent file storage path
CORS_ORIGINShttp://localhost:7027Comma-separated allowed origins for CORS

Features

Projects

  • Work-in-progress and tentative final names
  • Description, vision, goal, completion/abandonment criteria
  • Star rating (1-5), subjective completion %, task-based completion %
  • GitHub repo and website links
  • Local directory reference
  • Collaborators list
  • Grouping by Areas (folders) with drag-and-drop between areas
  • Project status: active, on hold, deprecated
  • Archive without deletion
  • Sidebar quick actions per project (archive, ZIP download, on hold, deprecated)

Tasks

  • Three statuses: new, in progress, done (click to cycle)
  • Bulk creation from bullet or ordered lists
  • Notes can be attached to tasks

Notes

  • Markdown content with live preview
  • Attachable to projects or individual tasks

Files

  • Upload PDF, DOCX, MD, and other reference files
  • Right-side file tree panel on project view, organized by folder
  • Subfolders for skills, personalities, context files, etc.
  • Persistent storage survives container restarts

Write Mode

  • Split-screen: markdown editor on left, live preview on right
  • Zen-style distraction-free writing

Commands

  • Generate PRD, BRD, MRD from project data — deterministic, with JSON download buttons
  • Export project as ZIP (includes files)
  • View pending tasks summary
  • Copy a cd <dir> && claude command for the project directory

Configuration (/settings)

  • YAML-based configuration editor, edited as raw YAML
  • Provider API keys are redacted on read; leave the redaction placeholder in place to keep the stored value
  • Save, reset to defaults

Backup & Restore (/settings)

  • Export: download all project data as a single JSON file (areas, projects, tasks, notes, snippets — no file attachments)
  • Import: upload a JSON backup to merge into the database (skips existing records by ID)
  • Useful for backup, migration, or transferring data between instances

UI

  • Dark and light theme toggle
  • English and Spanish language toggle
  • Font selector (Segoe UI, Georgia, Consolas, Arial, Palatino)
  • Collapsible sidebar with project tree
  • Search, filter by area, sort by name/date
  • URL-based routing (/projects/:id) for deep linking
  • SVG favicon for browser tab identification

AI: the MCP server

FlowTrack does not call a model, and that is a decision rather than a gap.

Earlier versions pretended otherwise. Chat Mode returned an echo, "suggest next steps" was a handful of hardcoded heuristics, and the settings page happily stored OpenAI and Anthropic keys that nothing ever read. The obvious fix was to implement the layer for real — provider abstraction, streaming, key management, cost ceilings — and the honest assessment was that all of that work would end in a worse chat than the one already open in the next terminal window.

So the direction inverted. FlowTrack stopped trying to be an AI application and became a tool an agent drives, through an MCP server in mcp-server/. That deleted the fake surface instead of filling it in: the chat endpoint, the suggestion heuristics, the social-copy generator and the entire llm_providers table are gone. The feature arrived as a net removal of code, and there are no provider API keys left in the system to leak.

What it gives an agent

Nine tools, and the number is deliberate. A server with one tool per REST endpoint gives the agent thirty ways to ask a question and no basis for choosing. These are shaped around what you actually want to know about a portfolio.

Tool
portfolio_digestThe one that matters. What is stale, what is overdue, active count against the WIP limit, and where the objective and subjective completion figures disagree most — in one call
list_projectsCompact digest, filterable by area, status, minimum stars, or days untouched
get_projectFull detail for one project, with tasks and notes
add_tasksBulk creation from a markdown list
update_task_statusnew / in_progress / done
add_noteThe durable record — decisions, especially decisions to stop
set_project_stateStatus, stars, subjective completion: the verbs of a triage

Two resources: flowtrack://portfolio renders every non-archived project as one markdown document, so an agent can take in the whole picture in a single cheap read; flowtrack://project/{id} does the same for one project.

The prompts are the opinionated part

Prompts are the least-used corner of MCP and the reason this server is worth having rather than a generic database wrapper. They encode the opinions the tool exists to enforce.

/reckoning walks the stale and overdue projects one at a time, quotes each project's own abandonment_criteria back at you, and forces a single decision: continue, freeze, or kill. It records the answer, in your words, dated. It stops every five projects to ask whether to go on, and it does not soften the question — because freezing and killing are successful outcomes, and a portfolio where everything stays active is the failure this tool was built to prevent.

/next gives one recommendation rather than three options, and refuses to suggest starting anything while the WIP limit is exceeded.

/close-out drafts the abandonment note for a project you are stopping: what it was for, what got built, why it is stopping — specific and unsentimental, "no demand was ever tested" beats "priorities shifted" — and what is worth salvaging, by filename.

Setup

claude mcp add flowtrack \
--env FLOWTRACK_API_KEY=ft_dev_key_change_me \
-- uvx --from ./mcp-server flowtrack-mcp

Full configuration, including Claude Desktop and Cursor, in mcp-server/README.md.

One thing to keep in mind

Notes and snippets are data, not instructions. Some of them arrive from arbitrary web pages through the Chrome clipper, which means a note can contain text engineered to read like a directive. The server's instructions say so explicitly, and any prompt built on top of this should treat note content as material to evaluate rather than orders to follow.

Acting on a note from the UI

A note that says "take the venv out of version control — that's 2000 files of junk" is an actionable instruction sitting a long way from the repository it applies to. So each note carries an Open session button that starts a coding session in the project's directory, pointed at that note.

It appears only on notes belonging to a project that has local_dir set, because without a directory there is nowhere to open. The three actions are Act on this, Explain this and Draft a plan; the last two change nothing on disk.

This is where the MCP server pays for itself twice. The button does not need to stuff the note's text into a command line — which would mean quoting hell on two operating systems and a hard 8191-character limit in cmd.exe. It passes two ids and lets the agent fetch the note itself, along with the rest of the project's context.

It needs one small process running on your machine, in launcher/, because a containerised API cannot open a terminal on your host. Zero dependencies beyond the Python standard library:

export FLOWTRACK_API_KEY=ft_dev_key_change_me
python launcher/flowtrack_launcher.py

Without it the buttons still work — they copy the equivalent command to your clipboard instead. One paste rather than one click, and nothing to install. The Settings page shows which mode you are in.

The security model is the part worth reading before you run it, and it comes down to a single rule: the launcher never executes anything the browser sends. The request carries only {project_id, note_id, action} with action from a fixed set; the prompts live in the launcher's source and the command is assembled from its own configuration. The browser picks an intent, the launcher picks the command. Full detail, including what it does not defend against, in launcher/README.md.

Chrome Extension

Located in extension/. Load as an unpacked extension in Chrome:

  1. Go to chrome://extensions/
  2. Enable "Developer mode"
  3. Click "Load unpacked" and select the extension/ folder
  4. Click the extension icon, configure API URL (http://localhost:7028) and API key — the key must match API_KEY in .env, which ships as ft_dev_key_change_me
  5. Pick a project in the popup, then use the buttons or the right-click menu ("Save to FlowTrack")

The project list loads as soon as both fields are filled; the button reloads it. If it stays on "Not loaded", the error under the buttons says why and stays there until you act on it — a rejected key, an unreachable URL, or FlowTrack not running. No CORS change is needed for the default localhost setup: the manifest's host_permissions cover it, and Chrome does not enforce CORS on an extension's own fetches to a host it has permission for.

The + button starts a new project from the popup, with just a name. An idea found on the web is often not a clip for an existing project but the beginning of a new one, and having to open the app first is how it gets lost. The new project is selected and remembered immediately, so the clip you were about to save — and the next right-click — go to it. Fill in the rest in FlowTrack proper.

The project you pick in the popup is where right-click clips go. The context menu has no picker of its own, so it reuses that choice, which is remembered between sessions. Clips record the page they came from in source_url.

Leave the picker on "Inbox (unfiled)" to clip without deciding. That is the default, and the point: when you come across an idea worth exploring later, the cost of capturing it should not be choosing a home for it. Unfiled clips land in a project called Inbox, created the first time one is needed — no schema change, no migration, just a reserved name (see backend/app/inbox.py). Re-file a clip from the project page's clip panel, or with PUT /api/snippets/{id}.

The Inbox is exempt from the project health dot: it is a holding pen with no target date that is meant to sit there collecting things, so staleness means nothing for it. It does still count as one active project in portfolio_digest's WIP tally.

Text selected on the page is read with chrome.scripting, which cannot run on chrome:// pages, the Chrome Web Store or PDF viewers. There the popup says so and you can paste into the box instead.

The manifest declares host_permissions for localhost and 127.0.0.1, which is what lets the extension call the API at all — without it every request is blocked before it leaves the browser. If you run FlowTrack on another host, the extension has no button to ask for that: grant it by hand from chrome://extensions → FlowTrack Clipper → "Site access", and add the extension origin to CORS_ORIGINS:

CORS_ORIGINS=http://localhost:7027,chrome-extension://<your-extension-id>

API Endpoints

All endpoints require X-API-Key header.

MethodPathDescription
GET/api/healthHealth check
Areas
GET/POST/api/areas/List/create areas
PUT/DELETE/api/areas/{id}Update/delete area (ungroups projects)
Projects
GET/POST/api/projects/List/create projects (query: search, area_id, sort_by, sort_order, archived)
GET/PUT/api/projects/{id}Get/update project
POST/api/projects/{id}/archiveArchive project
POST/api/projects/{id}/exportExport as ZIP (with files)
GET/api/projects/{id}/pendingPending tasks summary
POST/api/projects/{id}/collaboratorsAdd collaborator
Tasks
GET/POST/api/projects/{pid}/tasks/List/create tasks (bulk from lists)
PUT/DELETE/api/projects/{pid}/tasks/{id}Update/delete task
Notes
GET/POST/api/notes/List/create notes (query: project_id, task_id)
PUT/DELETE/api/notes/{id}Update/delete note
Files
GET/POST/api/projects/{pid}/files/List/upload files
GET/api/projects/{pid}/files/{id}/downloadDownload file
DELETE/api/projects/{pid}/files/{id}Delete file
Extension
GET/api/extension/projectsSimplified project list for Chrome extension
GET/api/extension/inboxWhere unfiled clips land; reports absent before first use
POST/api/extension/projectCreate a project from the clipper, name only
POST/api/extension/snippetSave URL/snippet. Omit project_id to file it in the Inbox
Snippets (clips)
GET/api/snippets/List clips, newest first (query: project_id, limit)
PUT/api/snippets/{id}Re-file a clip under a different project
DELETE/api/snippets/{id}Delete a clip
Documents
POST/api/documents/prd/{id}Generate PRD
POST/api/documents/brd/{id}Generate BRD
POST/api/documents/mrd/{id}Generate MRD
Config
GET/api/config/Get parsed config
GET/PUT/api/config/yamlGet/update config as YAML
POST/api/config/resetReset config to defaults
Backup
GET/api/backup/exportExport all data as JSON
POST/api/backup/importImport data from JSON

Running Tests

The suite drops every table after each test. It therefore defaults to a separate flowtrack_test database and refuses to start if TEST_DATABASE_URL points at a database whose name does not end in _test.

Create the throwaway database once:

docker compose exec db psql -U flowtrack -d postgres -c 'CREATE DATABASE flowtrack_test;'

Then run the suite inside the API container (no local Python needed):

docker compose exec \
-e TEST_DATABASE_URL=postgresql+asyncpg://flowtrack:flowtrack_secret@db:5432/flowtrack_test \
api python -m pytest -q

Or from the host, against the published database port:

cd backend
pip install -r requirements.txt -r requirements-dev.txt
pytest -q # uses localhost:7029/flowtrack_test by default

Project Structure

flowtrack/
.env # Environment variables
docker-compose.yml # Container orchestration (ports 7027-7029)
README.md # This file
specs.md # Original product specification
backend/
Dockerfile
requirements.txt
app/
main.py # FastAPI app entry point
config.py # Settings from .env
database.py # Async SQLAlchemy setup + migrations
models.py # All database models (Area, Project, Task, Note, etc.)
schemas.py # Pydantic request/response schemas
dependencies.py # API key verification
routers/
areas.py # Area CRUD
projects.py # Project CRUD + export/archive/status
tasks.py # Task CRUD with bulk creation
notes.py # Note CRUD
files.py # File upload/download
extension.py # Chrome extension endpoints
documents.py # Deterministic PRD/BRD/MRD generation
config.py # YAML configuration management
backup.py # Full data export/import (JSON)
tests/
conftest.py # Test fixtures with async DB
test_areas.py
test_projects.py
test_tasks.py
test_notes.py
frontend/
Dockerfile
package.json
svelte.config.js
vite.config.js
src/
app.html # HTML shell with favicon
app.css # Global styles, CSS variables, dark/light themes
routes/
+layout.svelte # App shell: sidebar + toolbar (theme, lang, font, settings)
+page.svelte # Home dashboard with project cards
projects/[id]/
+page.svelte # Project detail page
settings/
+page.svelte # Settings: YAML config, API key, backup/restore
lib/
api.js # API client (all endpoints)
stores.js # Svelte stores (theme, lang, font, projects, etc.)
i18n.js # EN/ES translations
components/
Sidebar.svelte # Collapsible tree with drag-drop, search, actions
ProjectView.svelte # Two-column: project content + file tree panel
TaskList.svelte # Task list with status cycling
AddTaskModal.svelte # Bulk task creation modal
NoteEditor.svelte # Markdown note editor with preview
WriteMode.svelte # Split markdown/preview zen editor
CommandBar.svelte # Commands with download buttons (PRD, BRD, etc.)
static/
favicon.svg # SVG favicon
mcp-server/
pyproject.toml # Installable with uvx / pipx
src/flowtrack_mcp/
server.py # 9 tools, 2 resources, 3 prompts
client.py # Thin async client over the REST API
tests/
smoke_test.py # Drives the real stdio handshake
extension/
manifest.json # Chrome extension manifest (V3)
popup.html # Extension popup UI
popup.css # Extension styles (zen aesthetic)
popup.js # Extension logic (save URL/snippet)
background.js # Context menu service worker
package.json # Lint tooling only — the extension has no build
eslint.config.js # Chrome globals; runs in CI

About

My own personal and opinionated project tracking tool

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

FlowTrack

CILicense: MITPython 3.12SvelteKitPostgreSQL 16

A project tracker that makes you write down when to quit.

Every project here carries two fields no other tracker asks for: abandonment_criteria, written up front, before you are emotionally invested; and two separate completion figures — the objective one computed from tasks, and your own honest estimate. The gap between them is a diagnosis.

It is a triage tool for people with too many side projects, not a to-do app. No Gantt charts, no agile artifacts, no burndown. Just my own way of tracking, built around a Project as a stateful bag of properties moving through a lightweight lifecycle. It will probably not work for you out of the box, and that is fine — it is opinionated on purpose.

Architecture

ComponentTechnologyPort
FrontendSvelteKit (plain CSS, no Tailwind)7027
APIFastAPI (async, API key auth)7028
DatabasePostgreSQL 16 (SQL + JSONB)7029

Quick Start

git clone https://github.com/aelena/flowtrack.git
cd flowtrack
# 1. Create your environment file (the defaults work as-is for local use)
cp .env.example .env
# 2. Build and run
docker compose up --build
# 3. Open# Frontend: http://localhost:7027# API docs: http://localhost:7028/docs# Database: localhost:7029

.env is gitignored. Without it, docker compose has no values for POSTGRES_USER and friends and the database container will not start — so step 1 is not optional.

Environment Variables

Copy .env.example to .env and adjust:

VariableDefaultDescription
POSTGRES_USERflowtrackDatabase user
POSTGRES_PASSWORDflowtrack_secretDatabase password
POSTGRES_DBflowtrackDatabase name
DATABASE_URLpostgresql+asyncpg://...@db:5432/flowtrackInternal DB connection (container-to-container, port 5432)
API_KEYft_dev_key_change_meAPI key for all endpoints (X-API-Key header)
STORAGE_PATH/app/storagePersistent file storage path
CORS_ORIGINShttp://localhost:7027Comma-separated allowed origins for CORS

Features

Projects

  • Work-in-progress and tentative final names
  • Description, vision, goal, completion/abandonment criteria
  • Star rating (1-5), subjective completion %, task-based completion %
  • GitHub repo and website links
  • Local directory reference
  • Collaborators list
  • Grouping by Areas (folders) with drag-and-drop between areas
  • Project status: active, on hold, deprecated
  • Archive without deletion
  • Sidebar quick actions per project (archive, ZIP download, on hold, deprecated)

Tasks

  • Three statuses: new, in progress, done (click to cycle)
  • Bulk creation from bullet or ordered lists
  • Notes can be attached to tasks

Notes

  • Markdown content with live preview
  • Attachable to projects or individual tasks

Files

  • Upload PDF, DOCX, MD, and other reference files
  • Right-side file tree panel on project view, organized by folder
  • Subfolders for skills, personalities, context files, etc.
  • Persistent storage survives container restarts

Write Mode

  • Split-screen: markdown editor on left, live preview on right
  • Zen-style distraction-free writing

Commands

  • Generate PRD, BRD, MRD from project data — deterministic, with JSON download buttons
  • Export project as ZIP (includes files)
  • View pending tasks summary
  • Copy a cd <dir> && claude command for the project directory

Configuration (/settings)

  • YAML-based configuration editor, edited as raw YAML
  • Provider API keys are redacted on read; leave the redaction placeholder in place to keep the stored value
  • Save, reset to defaults

Backup & Restore (/settings)

  • Export: download all project data as a single JSON file (areas, projects, tasks, notes, snippets — no file attachments)
  • Import: upload a JSON backup to merge into the database (skips existing records by ID)
  • Useful for backup, migration, or transferring data between instances

UI

  • Dark and light theme toggle
  • English and Spanish language toggle
  • Font selector (Segoe UI, Georgia, Consolas, Arial, Palatino)
  • Collapsible sidebar with project tree
  • Search, filter by area, sort by name/date
  • URL-based routing (/projects/:id) for deep linking
  • SVG favicon for browser tab identification

AI: the MCP server

FlowTrack does not call a model, and that is a decision rather than a gap.

Earlier versions pretended otherwise. Chat Mode returned an echo, "suggest next steps" was a handful of hardcoded heuristics, and the settings page happily stored OpenAI and Anthropic keys that nothing ever read. The obvious fix was to implement the layer for real — provider abstraction, streaming, key management, cost ceilings — and the honest assessment was that all of that work would end in a worse chat than the one already open in the next terminal window.

So the direction inverted. FlowTrack stopped trying to be an AI application and became a tool an agent drives, through an MCP server in mcp-server/. That deleted the fake surface instead of filling it in: the chat endpoint, the suggestion heuristics, the social-copy generator and the entire llm_providers table are gone. The feature arrived as a net removal of code, and there are no provider API keys left in the system to leak.

What it gives an agent

Nine tools, and the number is deliberate. A server with one tool per REST endpoint gives the agent thirty ways to ask a question and no basis for choosing. These are shaped around what you actually want to know about a portfolio.

Tool
portfolio_digestThe one that matters. What is stale, what is overdue, active count against the WIP limit, and where the objective and subjective completion figures disagree most — in one call
list_projectsCompact digest, filterable by area, status, minimum stars, or days untouched
get_projectFull detail for one project, with tasks and notes
add_tasksBulk creation from a markdown list
update_task_statusnew / in_progress / done
add_noteThe durable record — decisions, especially decisions to stop
set_project_stateStatus, stars, subjective completion: the verbs of a triage

Two resources: flowtrack://portfolio renders every non-archived project as one markdown document, so an agent can take in the whole picture in a single cheap read; flowtrack://project/{id} does the same for one project.

The prompts are the opinionated part

Prompts are the least-used corner of MCP and the reason this server is worth having rather than a generic database wrapper. They encode the opinions the tool exists to enforce.

/reckoning walks the stale and overdue projects one at a time, quotes each project's own abandonment_criteria back at you, and forces a single decision: continue, freeze, or kill. It records the answer, in your words, dated. It stops every five projects to ask whether to go on, and it does not soften the question — because freezing and killing are successful outcomes, and a portfolio where everything stays active is the failure this tool was built to prevent.

/next gives one recommendation rather than three options, and refuses to suggest starting anything while the WIP limit is exceeded.

/close-out drafts the abandonment note for a project you are stopping: what it was for, what got built, why it is stopping — specific and unsentimental, "no demand was ever tested" beats "priorities shifted" — and what is worth salvaging, by filename.

Setup

claude mcp add flowtrack \
--env FLOWTRACK_API_KEY=ft_dev_key_change_me \
-- uvx --from ./mcp-server flowtrack-mcp

Full configuration, including Claude Desktop and Cursor, in mcp-server/README.md.

One thing to keep in mind

Notes and snippets are data, not instructions. Some of them arrive from arbitrary web pages through the Chrome clipper, which means a note can contain text engineered to read like a directive. The server's instructions say so explicitly, and any prompt built on top of this should treat note content as material to evaluate rather than orders to follow.

Acting on a note from the UI

A note that says "take the venv out of version control — that's 2000 files of junk" is an actionable instruction sitting a long way from the repository it applies to. So each note carries an Open session button that starts a coding session in the project's directory, pointed at that note.

It appears only on notes belonging to a project that has local_dir set, because without a directory there is nowhere to open. The three actions are Act on this, Explain this and Draft a plan; the last two change nothing on disk.

This is where the MCP server pays for itself twice. The button does not need to stuff the note's text into a command line — which would mean quoting hell on two operating systems and a hard 8191-character limit in cmd.exe. It passes two ids and lets the agent fetch the note itself, along with the rest of the project's context.

It needs one small process running on your machine, in launcher/, because a containerised API cannot open a terminal on your host. Zero dependencies beyond the Python standard library:

export FLOWTRACK_API_KEY=ft_dev_key_change_me
python launcher/flowtrack_launcher.py

Without it the buttons still work — they copy the equivalent command to your clipboard instead. One paste rather than one click, and nothing to install. The Settings page shows which mode you are in.

The security model is the part worth reading before you run it, and it comes down to a single rule: the launcher never executes anything the browser sends. The request carries only {project_id, note_id, action} with action from a fixed set; the prompts live in the launcher's source and the command is assembled from its own configuration. The browser picks an intent, the launcher picks the command. Full detail, including what it does not defend against, in launcher/README.md.

Chrome Extension

Located in extension/. Load as an unpacked extension in Chrome:

  1. Go to chrome://extensions/
  2. Enable "Developer mode"
  3. Click "Load unpacked" and select the extension/ folder
  4. Click the extension icon, configure API URL (http://localhost:7028) and API key — the key must match API_KEY in .env, which ships as ft_dev_key_change_me
  5. Pick a project in the popup, then use the buttons or the right-click menu ("Save to FlowTrack")

The project list loads as soon as both fields are filled; the button reloads it. If it stays on "Not loaded", the error under the buttons says why and stays there until you act on it — a rejected key, an unreachable URL, or FlowTrack not running. No CORS change is needed for the default localhost setup: the manifest's host_permissions cover it, and Chrome does not enforce CORS on an extension's own fetches to a host it has permission for.

The + button starts a new project from the popup, with just a name. An idea found on the web is often not a clip for an existing project but the beginning of a new one, and having to open the app first is how it gets lost. The new project is selected and remembered immediately, so the clip you were about to save — and the next right-click — go to it. Fill in the rest in FlowTrack proper.

The project you pick in the popup is where right-click clips go. The context menu has no picker of its own, so it reuses that choice, which is remembered between sessions. Clips record the page they came from in source_url.

Leave the picker on "Inbox (unfiled)" to clip without deciding. That is the default, and the point: when you come across an idea worth exploring later, the cost of capturing it should not be choosing a home for it. Unfiled clips land in a project called Inbox, created the first time one is needed — no schema change, no migration, just a reserved name (see backend/app/inbox.py). Re-file a clip from the project page's clip panel, or with PUT /api/snippets/{id}.

The Inbox is exempt from the project health dot: it is a holding pen with no target date that is meant to sit there collecting things, so staleness means nothing for it. It does still count as one active project in portfolio_digest's WIP tally.

Text selected on the page is read with chrome.scripting, which cannot run on chrome:// pages, the Chrome Web Store or PDF viewers. There the popup says so and you can paste into the box instead.

The manifest declares host_permissions for localhost and 127.0.0.1, which is what lets the extension call the API at all — without it every request is blocked before it leaves the browser. If you run FlowTrack on another host, the extension has no button to ask for that: grant it by hand from chrome://extensions → FlowTrack Clipper → "Site access", and add the extension origin to CORS_ORIGINS:

CORS_ORIGINS=http://localhost:7027,chrome-extension://<your-extension-id>

API Endpoints

All endpoints require X-API-Key header.

MethodPathDescription
GET/api/healthHealth check
Areas
GET/POST/api/areas/List/create areas
PUT/DELETE/api/areas/{id}Update/delete area (ungroups projects)
Projects
GET/POST/api/projects/List/create projects (query: search, area_id, sort_by, sort_order, archived)
GET/PUT/api/projects/{id}Get/update project
POST/api/projects/{id}/archiveArchive project
POST/api/projects/{id}/exportExport as ZIP (with files)
GET/api/projects/{id}/pendingPending tasks summary
POST/api/projects/{id}/collaboratorsAdd collaborator
Tasks
GET/POST/api/projects/{pid}/tasks/List/create tasks (bulk from lists)
PUT/DELETE/api/projects/{pid}/tasks/{id}Update/delete task
Notes
GET/POST/api/notes/List/create notes (query: project_id, task_id)
PUT/DELETE/api/notes/{id}Update/delete note
Files
GET/POST/api/projects/{pid}/files/List/upload files
GET/api/projects/{pid}/files/{id}/downloadDownload file
DELETE/api/projects/{pid}/files/{id}Delete file
Extension
GET/api/extension/projectsSimplified project list for Chrome extension
GET/api/extension/inboxWhere unfiled clips land; reports absent before first use
POST/api/extension/projectCreate a project from the clipper, name only
POST/api/extension/snippetSave URL/snippet. Omit project_id to file it in the Inbox
Snippets (clips)
GET/api/snippets/List clips, newest first (query: project_id, limit)
PUT/api/snippets/{id}Re-file a clip under a different project
DELETE/api/snippets/{id}Delete a clip
Documents
POST/api/documents/prd/{id}Generate PRD
POST/api/documents/brd/{id}Generate BRD
POST/api/documents/mrd/{id}Generate MRD
Config
GET/api/config/Get parsed config
GET/PUT/api/config/yamlGet/update config as YAML
POST/api/config/resetReset config to defaults
Backup
GET/api/backup/exportExport all data as JSON
POST/api/backup/importImport data from JSON

Running Tests

The suite drops every table after each test. It therefore defaults to a separate flowtrack_test database and refuses to start if TEST_DATABASE_URL points at a database whose name does not end in _test.

Create the throwaway database once:

docker compose exec db psql -U flowtrack -d postgres -c 'CREATE DATABASE flowtrack_test;'

Then run the suite inside the API container (no local Python needed):

docker compose exec \
-e TEST_DATABASE_URL=postgresql+asyncpg://flowtrack:flowtrack_secret@db:5432/flowtrack_test \
api python -m pytest -q

Or from the host, against the published database port:

cd backend
pip install -r requirements.txt -r requirements-dev.txt
pytest -q # uses localhost:7029/flowtrack_test by default

Project Structure

flowtrack/
.env # Environment variables
docker-compose.yml # Container orchestration (ports 7027-7029)
README.md # This file
specs.md # Original product specification
backend/
Dockerfile
requirements.txt
app/
main.py # FastAPI app entry point
config.py # Settings from .env
database.py # Async SQLAlchemy setup + migrations
models.py # All database models (Area, Project, Task, Note, etc.)
schemas.py # Pydantic request/response schemas
dependencies.py # API key verification
routers/
areas.py # Area CRUD
projects.py # Project CRUD + export/archive/status
tasks.py # Task CRUD with bulk creation
notes.py # Note CRUD
files.py # File upload/download
extension.py # Chrome extension endpoints
documents.py # Deterministic PRD/BRD/MRD generation
config.py # YAML configuration management
backup.py # Full data export/import (JSON)
tests/
conftest.py # Test fixtures with async DB
test_areas.py
test_projects.py
test_tasks.py
test_notes.py
frontend/
Dockerfile
package.json
svelte.config.js
vite.config.js
src/
app.html # HTML shell with favicon
app.css # Global styles, CSS variables, dark/light themes
routes/
+layout.svelte # App shell: sidebar + toolbar (theme, lang, font, settings)
+page.svelte # Home dashboard with project cards
projects/[id]/
+page.svelte # Project detail page
settings/
+page.svelte # Settings: YAML config, API key, backup/restore
lib/
api.js # API client (all endpoints)
stores.js # Svelte stores (theme, lang, font, projects, etc.)
i18n.js # EN/ES translations
components/
Sidebar.svelte # Collapsible tree with drag-drop, search, actions
ProjectView.svelte # Two-column: project content + file tree panel
TaskList.svelte # Task list with status cycling
AddTaskModal.svelte # Bulk task creation modal
NoteEditor.svelte # Markdown note editor with preview
WriteMode.svelte # Split markdown/preview zen editor
CommandBar.svelte # Commands with download buttons (PRD, BRD, etc.)
static/
favicon.svg # SVG favicon
mcp-server/
pyproject.toml # Installable with uvx / pipx
src/flowtrack_mcp/
server.py # 9 tools, 2 resources, 3 prompts
client.py # Thin async client over the REST API
tests/
smoke_test.py # Drives the real stdio handshake
extension/
manifest.json # Chrome extension manifest (V3)
popup.html # Extension popup UI
popup.css # Extension styles (zen aesthetic)
popup.js # Extension logic (save URL/snippet)
background.js # Context menu service worker
package.json # Lint tooling only — the extension has no build
eslint.config.js # Chrome globals; runs in CI

About

My own personal and opinionated project tracking tool

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages