Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Agency Task Tracker

A focused task tracker for small digital agencies. One screen, low-friction capture, inline status updates. Built with Django + HTMX + Alpine.js + PostgreSQL, run with Docker.

The problem this solves

Layla runs a 3-person digital agency for ~20 clients. Requests arrive in Slack and email all day. Things fall through the cracks; clients re-ask about already-handled work; the team context-switches because there's no single place to look. This app is that single place.

Quick start

cp .env.example .env
docker compose up --build

In a second terminal:

docker compose run --rm web python manage.py migrate
docker compose run --rm web python manage.py seed_demo

Open http://localhost:8000 and sign in:

Username Password Role
layla demo1234 Owner
sam demo1234 Member
jamie demo1234 Member

Permission model

is_superuser=True is treated as the agency owner. The model is:

Action Owner Member (own task) Member (others' task)
View yes yes yes
Create yes (any assignee) yes (forced to self)
Edit yes yes (no assignee change) 403
Status change / drag yes yes 403
Delete yes yes 403
Reassign yes no (field hidden) no (field hidden)

Defense in depth: action buttons are hidden in the template, drag-and-drop is filtered server-side via SortableJS filter, and every mutation view checks _can_modify(user, task) before touching the model.

At three users, is_superuser is a fine proxy for the owner role; a real multi-agency product would warrant a dedicated Role enum or custom user model.

Owners (Layla) get a Manage link in the header → http://localhost:8000/manage/ — an in-app page to add and edit team members and clients without leaving the product UI. The Django admin (/admin/) is still there as a fallback.

Stack

  • Django 5 — backend + server-rendered templates
  • HTMX — dynamic interactions without an SPA
  • Alpine.js — ephemeral client state (modal, delete-confirm step, toast queue)
  • SortableJS — drag-and-drop between status columns
  • Tailwind CSS (CDN) — styling, no build step
  • PostgreSQL 16 — persistence
  • Docker Compose — local dev orchestration

Architecture

┌─────────────────────────────────────────────────────────┐
│  Browser                                                │
│  ┌──────────────────┐   ┌──────────────────────────┐    │
│  │ Alpine           │   │ HTMX                     │    │
│  │ - modal open?    │   │ - filters → re-render    │    │
│  │ - delete confirm │   │ - status arrows / drag   │    │
│  │ - toast queue    │   │ - form post → swap+toast │    │
│  └──────────────────┘   └──────────────────────────┘    │
└──────────────────────────────┬──────────────────────────┘
                               │ HTTP (HTML responses only)
                               ▼
┌─────────────────────────────────────────────────────────┐
│  Django                                                 │
│  - views render full pages OR HTML partials             │
│  - HX-Trigger headers fire toast / close-modal events   │
│  - HX-Retarget redirects swap on form success           │
│  - request.htmx + HX-Current-URL keep filters sticky    │
└──────────────────────────────┬──────────────────────────┘
                               │
                               ▼
                       ┌──────────────┐
                       │  PostgreSQL  │
                       └──────────────┘

State contract: Postgres owns persistent truth. Django renders partials. Alpine owns ephemeral UI (modal open/closed, delete-confirm step, toast queue). No JSON endpoints; everything is HTML.

Project layout

src/
├── config/                # settings, urls, wsgi
├── core/                  # base template, login template
├── clients/               # Client model + admin
└── tasks/                 # Task model, views, forms, board templates,
    │                      # seed_demo, smoke tests
    ├── models.py          # Task, Status/Priority/Source choices, save() override
    ├── views.py           # board + 4 mutation views; HTMX partial responses
    ├── forms.py           # TaskForm with Tailwind widget classes
    ├── templates/tasks/
    │   ├── board.html         # full-page kanban + filter strip + search
    │   ├── _columns.html      # 4-column container (HTMX swap target)
    │   ├── _task_card.html    # one card; arrows + drag handle + edit/delete
    │   ├── _task_form.html    # modal body for create + edit
    │   └── _empty.html        # column empty state
    └── management/commands/seed_demo.py   # idempotent demo data

Tests

docker compose run --rm web python manage.py test

Five tests in src/tasks/tests.py:

  • One end-to-end smoke flow: render board → create task → advance status via arrow → drop directly to Done via the drag pattern. Asserts response status, HX-Trigger JSON, HX-Retarget header, and resulting model state.
  • Four focused tests on Task.save()'s completed_at lifecycle (the most subtle correctness property in this codebase): set on transition into Done, cleared on transition out, untouched when re-saving an already-Done task, set on creation when starting Done.

Key decisions

See the PR description for the full rundown. Short version:

  • Manual capture only — no Slack/email ingestion in MVP. Huge surface area; manual capture validates the workflow first.
  • One board, not per-client/per-assignee views — Layla's pain is "no single place." Filters narrow it; nothing replaces it.
  • source field on every task (Slack/Email/Meeting/Other) — preserves where requests came from.
  • Done column = last 7 days only — addresses "clients follow up on already-handled" without becoming a graveyard.
  • Task.save() sets completed_at only on transition into Done; clears on transition out — uses a DB-fetched old_status for simplicity at this scale.
  • Server-rendered HTML over SPA — right tool for 3 users on the prescribed stack.

Scope deliberately deferred

  • Slack/email ingestion
  • Notifications, daily digests, real-time push
  • Comments thread, attachments, time tracking, recurring tasks
  • Custom user model, multi-tenant
  • CI configuration, Tailwind build pipeline, themed admin

Each is a "yes, eventually" — none is on the critical path to "stop dropping work."

Reset the demo data

docker compose exec db psql -U agency_tasks -d agency_tasks -c "TRUNCATE tasks_task RESTART IDENTITY;"
docker compose run --rm web python manage.py seed_demo

Or full wipe (recreates the volume):

docker compose down -v
docker compose up --build
docker compose run --rm web python manage.py migrate
docker compose run --rm web python manage.py seed_demo

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages