Skip to content

indiser/AppTrack

Repository files navigation

📋 AppTrack

A personal, self-hosted web app to track jobs, internships, hackathons, competitions, and grants — with deadline reminders, search, and statistics. Built with a deliberately retro terminal aesthetic.


Table of Contents


Overview

Application Tracker is a single-user Flask web application backed by a PostgreSQL database (hosted on Neon). It replaces a spreadsheet with a structured, searchable, and deadline-aware interface for managing every opportunity you're pursuing.

The UI intentionally mimics a retro terminal / BBS aesthetic — monospace fonts, bordered tables, marquee headers — which also keeps it extremely fast and dependency-free on the frontend (zero JavaScript frameworks, zero CSS frameworks).


Tech Stack

Layer Technology
Web Framework Flask 3.x (Python)
Database PostgreSQL via Neon (serverless Postgres)
DB Driver psycopg2-binary
WSGI Server Gunicorn (for production)
Config python-dotenv + .env file
Reminders smtplib (SMTP over TLS) + rich for terminal output
CI / Automation GitHub Actions (scheduled cron job)
Frontend Vanilla HTML + CSS (Jinja2 templates, zero JS deps)

Architecture

┌────────────────────────────────────────────────────┐
│                    Browser / Client                │
└────────────────────┬───────────────────────────────┘
                     │ HTTP
┌────────────────────▼───────────────────────────────┐
│              Flask App  (app.py)                   │
│  ┌──────────────┐   ┌───────────────────────────┐  │
│  │  Auth Layer  │   │    Route Handlers         │  │
│  │  (session)   │   │  /, /add, /edit, /delete  │  │
│  │              │   │  /search, /stats,         │  │
│  └──────────────┘   │  /deadlines, /login       │  │
│                     └──────────┬──────────────────┘ │
└────────────────────────────────┼───────────────────┘
                                 │
┌────────────────────────────────▼───────────────────┐
│              Data Layer  (db.py)                   │
│   psycopg2 · RealDictCursor · parameterized SQL    │
└────────────────────────────────┬───────────────────┘
                                 │ SSL / TLS
┌────────────────────────────────▼───────────────────┐
│         Neon Serverless PostgreSQL                 │
│         (connection pooling enabled)               │
└────────────────────────────────────────────────────┘

                ┌─────────────────────────┐
                │   GitHub Actions        │
                │   (cron: 3:30 UTC daily)│
                │   └── remind.py         │
                │       └── SMTP email    │
                └─────────────────────────┘

The app follows a clean three-layer separation:

  • app.py — Flask routes, session management, form parsing. No SQL here.
  • db.py — All database I/O. Uses psycopg2 with RealDictCursor so rows come back as dicts. Every function opens and closes its own connection (simple, safe for low-concurrency personal use).
  • models.py — A single Application dataclass with a computed property (days_until_deadline). No ORM overhead.

Features

Core CRUD

  • Add a new application with title, organisation, type, platform, URL, deadline, status, contact used, and notes
  • Edit any field on an existing record at any time
  • Delete a record (POST-only — never exposed as a GET to prevent CSRF via URL)
  • Search across title, org, and platform with Postgres ILIKE (case-insensitive)

Deadline Awareness

  • Deadlines are hidden for job and internship types (they typically don't have hard deadlines)
  • The deadline field is dynamically shown/hidden in the form based on the selected type (vanilla JS)
  • The index table colour-codes deadlines:
    • 🔴 Past — shown in red
    • 🟠 ≤ 3 days — urgent styling
    • 🟡 ≤ 7 days — soon styling
    • Remaining days shown inline as (Xd)
  • /deadlines route shows only applications with upcoming deadlines, sorted ascending

Status Lifecycle

Status transitions: pending → applied → interview → offer / rejected

When an application's status is changed to applied for the first time, date_applied is automatically set to today. Subsequent edits preserve the original date_applied.

Statistics Dashboard

The /stats page aggregates:

  • Total records in the database
  • Active count (status is pending or interview)
  • Breakdown by application type
  • Breakdown by status

Daily Email Reminders

remind.py queries the database directly and sends a categorised email:

  • 🔴 URGENT — deadlines ≤ 3 days
  • 🟡 COMING UP — 4–7 days
  • 📋 THIS MONTH — 8–30 days

Jobs and internships are intentionally excluded from reminders (no hard deadlines). The terminal output uses rich for colour-coded formatting. The email uses plain text with the same structure.

Authentication

Single-password authentication using Flask sessions. All routes except /login are protected by a @login_required decorator.


Data Model

@dataclass
class Application:
    id:            Optional[int]    # SERIAL PRIMARY KEY
    title:         str              # Role / opportunity name
    org:           str              # Company / organisation
    type:          str              # job | internship | hackathon | competition | grant
    platform:      str              # Where you found / applied (LinkedIn, Devpost, etc.)
    url:           str              # Direct link to the listing
    deadline:      Optional[date]   # Submission deadline (nullable)
    status:        str              # pending | applied | interview | offer | rejected
    date_applied:  Optional[date]   # Auto-set when status → applied
    notes:         str              # Free-form notes
    contact_used:  str              # Email / phone used to apply

    @property
    def days_until_deadline(self) -> Optional[int]: ...

PostgreSQL schema (auto-created by db.init_db() on first run):

CREATE TABLE IF NOT EXISTS applications (
    id            SERIAL PRIMARY KEY,
    title         TEXT NOT NULL,
    org           TEXT NOT NULL,
    type          TEXT NOT NULL,
    platform      TEXT NOT NULL,
    url           TEXT NOT NULL,
    deadline      DATE,
    status        TEXT NOT NULL,
    date_applied  DATE,
    notes         TEXT NOT NULL,
    contact_used  TEXT NOT NULL DEFAULT ''
);

The contact_used column is added via ALTER TABLE ... ADD COLUMN IF NOT EXISTS — safe to run on an existing database without data loss.


Project Structure

Tracker/
├── app.py                  # Flask application, routes, auth
├── db.py                   # Database layer (psycopg2)
├── models.py               # Application dataclass
├── remind.py               # Standalone reminder script
├── requirements.txt        # Python dependencies
├── .env                    # Secrets (never commit this)
├── .gitignore
│
├── templates/
│   ├── base.html           # Shared layout (nav, flash messages, footer)
│   ├── index.html          # Main table view (all apps / deadlines / search results)
│   ├── add.html            # Add new application form
│   ├── edit.html           # Edit existing application form
│   ├── stats.html          # Statistics dashboard
│   └── login.html          # Password login page
│
├── static/
│   ├── style.css           # Retro terminal stylesheet
│   ├── favicon.ico
│   └── ...                 # PWA icons + webmanifest
│
└── .github/
    └── workflows/
        └── daily_reminder.yml   # GitHub Actions cron job

Setup & Installation

Prerequisites

  • Python 3.10+
  • A PostgreSQL database (a free Neon project works perfectly)
  • A Gmail account with an App Password (for reminders)

1. Clone and create a virtual environment

git clone https://github.com/indiser/AppTrack.git
cd AppTrack

python -m venv venv

# Windows
venv\Scripts\activate

# macOS / Linux
source venv/bin/activate

2. Install dependencies

pip install -r requirements.txt

3. Configure environment variables

Copy the example below into a .env file at the project root and fill in your values:

DATABASE_URL="postgresql://user:password@host/dbname?sslmode=require"
SECRET_KEY="a-long-random-hex-string"
APP_PASSWORD="your-login-password"

# Optional — only needed for remind.py
REMINDER_EMAIL_FROM="you@gmail.com"
REMINDER_EMAIL_PASSWORD="your-app-password"
REMINDER_EMAIL_TO="destination@email.com"

4. Run the app

# Development
python app.py

# Production (Gunicorn)
gunicorn app:app

The database table is created automatically on startup via db.init_db(). No migration tool required.


Environment Variables

Variable Required Description
DATABASE_URL ✅ Yes Full PostgreSQL connection string
SECRET_KEY ✅ Yes Flask session signing key (use a long random string)
APP_PASSWORD ✅ Yes Single password to log in to the web UI
REMINDER_EMAIL_FROM ⚪ Optional Gmail address to send reminders from
REMINDER_EMAIL_PASSWORD ⚪ Optional Gmail App Password (not your account password)
REMINDER_EMAIL_TO ⚪ Optional Recipient email address for reminders

⚠️ Never commit .env to version control. It is listed in .gitignore.


Running the App

Development server

python app.py
# Flask dev server runs on http://127.0.0.1:5000

Production with Gunicorn

gunicorn app:app --workers 2 --bind 0.0.0.0:8000

For deployment on platforms like Railway, Render, or Fly.io, set all environment variables as secrets in the platform dashboard and use the Gunicorn command as the start command.


Daily Reminder System

remind.py is a standalone script — it does not import from app.py or use Flask. It connects to the database directly, queries upcoming deadlines, and sends an email summary.

Run it manually:

python remind.py

Schedule it locally:

# Linux / macOS (crontab -e)
30 3 * * * cd /path/to/Tracker && python remind.py

# Windows — use Task Scheduler to run `python remind.py` daily at 9:00 AM

Or let GitHub Actions run it automatically (see below).


GitHub Actions Automation

The workflow at .github/workflows/daily_reminder.yml runs remind.py every day at 3:30 AM UTC (9:00 AM IST) using a scheduled cron trigger. It can also be triggered manually from the GitHub Actions UI.

Required GitHub Secrets (set in your repo → Settings → Secrets → Actions):

Secret Value
DATABASE_URL Your Neon PostgreSQL connection string
REMINDER_EMAIL_FROM Gmail address
REMINDER_EMAIL_PASSWORD Gmail App Password
REMINDER_EMAIL_TO Recipient address

The workflow checks out the repo, sets up Python 3.11, installs psycopg2-binary and rich, and runs the script.


API Routes Reference

All routes require authentication (redirect to /login if not logged in).

Method Route Description
GET / All applications, sorted by deadline (NULLs last)
GET /deadlines Upcoming deadlines only (today or future), ascending
GET/POST /add Add a new application
GET/POST /edit/<id> Edit an existing application
POST /delete/<id> Delete an application (POST-only, confirms in UI)
GET /search?q=<query> Search title, org, platform via ILIKE
GET /stats Aggregated statistics
GET/POST /login Password login
GET /logout Clear session and redirect to login

Security Design

  • Single-password auth — session cookie is set HttpOnly and Secure. The password is stored only in the environment, never in code or the database.
  • Parameterized queries throughoutdb.py uses %s placeholders in every psycopg2 call. No string interpolation in SQL. Zero SQL injection surface.
  • POST-only deletes — the /delete/<id> route only accepts POST. Attempting a GET to that URL does nothing, preventing accidental or malicious deletion via a crafted link.
  • CSRF on destructive actions — the delete form includes a JS confirm() dialog as a UI-level guard.
  • No secrets in sourceDATABASE_URL, SECRET_KEY, APP_PASSWORD, and email credentials are all loaded from .env / environment at runtime and are excluded from version control.

Future Improvements

Short-term

  • Tags / labels — free-form tagging system to group applications by cohort, season, or custom category
  • Bulk actions — select multiple records and update status or delete in one operation
  • Notes history — append-only log per application instead of a single editable notes field
  • CSV export — download all applications as a spreadsheet for external analysis

Medium-term

  • Reminder configuration via UI — let the user set reminder thresholds (e.g., 1 day, 3 days, 7 days) from a settings page instead of hardcoding them in remind.py
  • Status timeline — record timestamps for every status change to build an activity history
  • Duplicate detection — warn when adding an application with the same title + org as an existing record
  • Calendar view — a monthly calendar that visualises deadline distribution at a glance

Long-term

  • Multi-user support — user accounts with isolated data per user; requires reworking the auth layer and adding a user_id foreign key to the applications table
  • REST API — a JSON API layer so the data can be consumed by a mobile client or integrated with other tools
  • Browser extension — one-click "Add to Tracker" from a job board or hackathon listing page
  • Analytics — response rate, average time-to-decision, success rate by platform, type, or source — surfaced on the stats page
  • Notification channels — Telegram bot, Slack webhook, or Discord message as alternatives to email reminders

Built With

This project is intentionally minimal — no magic frameworks, no frontend build pipeline. Every dependency earns its place.

Tool Role Why
Python Python 3.10+ Runtime Readable, batteries-included, perfect for small web apps
Flask Flask Web framework Lightweight, no ORM baggage, full control over routing
PostgreSQL PostgreSQL Database Relational, reliable, first-class date/text support
Neon Neon Hosted Postgres Serverless, free tier, auto-suspend, no infra to manage
Gunicorn Gunicorn WSGI server Production-grade process manager for Flask
GitHub Actions GitHub Actions CI / automation Free cron scheduling without a separate server
psycopg2-binary DB driver Mature, battle-tested PostgreSQL adapter for Python
python-dotenv Config Keeps secrets out of source code
rich Terminal output Colour-coded reminder output with clickable hyperlinks
smtplib Email Standard-library SMTP — no third-party email SDK needed

Acknowledgements

  • Neon — for making serverless Postgres genuinely usable on a free tier. The auto-suspend feature means this app costs nothing to host when idle.
  • Flask — for staying out of the way. The micro-framework philosophy is the right call for a project like this.
  • Textualize / Rich — the rich library makes remind.py's terminal output genuinely enjoyable to read, and the hyperlink support in modern terminals is a nice touch.
  • psycopg2 — the oldest and most reliable PostgreSQL adapter in the Python ecosystem. Still the right choice.
  • The retro BBS / terminal aesthetic was inspired by the era of Netscape Navigator and ASCII-art bulletin boards — a deliberate design choice to keep the UI fast, accessible, and free of frontend complexity.

License

This project is licensed under the MIT License.

MIT License

Copyright (c) 2024 Rana Banerjee

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Contact

Built and maintained by Indiser.

Feel free to open an issue or fork the repo if you want to adapt it for your own use. Pull requests are welcome.

About

Self-hosted tracker for jobs, internships, hackathons, and grants. Flask + Neon Postgres + daily email reminders.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors