Note: This application is 100% vibe coded. It's exclusively for me to learn and experiment with GitHub Copilot CLI.
WatchTracker is a full-stack web application for cataloging and managing your personal watch collection. Track details like brand, model, movement type, band type, purchase info, and images — with optional AI-powered watch analysis via the Anthropic API. Every watch is scoped to your authenticated account using JWT-based authentication.
It also includes a wish list for tracking watches you want to buy, a stats dashboard with a GitHub-style wear heatmap, and wear logging to track each time you wear a watch.
On first launch, a setup wizard walks you through creating an admin account and configuring application settings.
| Layer | Tech | Path |
|---|---|---|
| Backend | .NET 10 Web API, EF Core, SQLite | src/api/ |
| Frontend | React 19, TypeScript, Vite (PWA) | src/web/ |
The React SPA communicates with the .NET API exclusively via REST (/api/*). In production the API serves the SPA as static files from a single container, so no separate web server is needed.
The frontend is a Progressive Web App (PWA) and can be installed on iOS (Safari → Share → Add to Home Screen), Android, and desktop browsers for a native app-like experience with offline caching. When running as a standalone PWA on iOS, the navigation bar collapses into a hamburger menu to avoid the notch and status bar area.
- .NET 10 SDK
- Node.js (v18+)
- Task — optional, for task runner commands
- Docker & Docker Compose — optional, for containerized deployment
Clone the repository and start the development servers:
git clone <repo-url> && cd watch-tracker-app
task run # starts both API and frontend in parallelThe API runs on http://localhost:5062 and the Vite dev server on http://localhost:5173. You can also start them individually:
task run-api # API only
task run-web # frontend onlyWhen the app launches for the first time, the setup wizard at /setup will guide you through creating an admin account and optionally configuring the Anthropic API key for AI watch analysis.
| Command | Description |
|---|---|
task init |
Generate a .env file with a random JWT key |
task run |
Run API and frontend in parallel |
task run-api |
Run the .NET API server |
task run-web |
Run the Vite dev server |
task build |
Build both API and frontend |
task build-api |
Build the .NET API project |
task build-web |
Build the React frontend |
task docker-build |
Build the Docker container image |
task docker-run |
Run the Docker container locally |
task db |
Create and apply EF Core migrations |
A GitHub Actions workflow (.github/workflows/docker-publish.yml) builds and pushes the Docker image to Docker Hub:
- Triggers — Push to
mainbranch and manual dispatch - Tags — Full commit SHA, short commit SHA, and
latest(on main) - Caching — Uses GitHub Actions cache for Docker layer caching
| Secret | Description |
|---|---|
DOCKERHUB_USERNAME |
Your Docker Hub username |
DOCKERHUB_TOKEN |
A Docker Hub access token |
The application ships as a single Docker container that serves both the API and the React SPA. The pre-built image is hosted on GitHub Container Registry.
Generate a .env file with a random JWT signing key:
task initThis creates a .env file with a cryptographically random JWT_KEY. Edit the file to optionally add your Anthropic API key:
JWT_KEY=<auto-generated>
ANTHROPIC_API_KEY= # optional — enables AI watch analysis
ALLOWED_ORIGINS=* # optional — restrict CORS origins (semicolon-separated, or * for any)
LOG_LEVEL=Information # optional — initial log level (Trace, Debug, Information, Warning, Error, Critical, None)
docker compose uptask docker-build # build the image from source
task docker-run # run the container (reads .env automatically)docker run -p 8080:8080 -d \
-e Jwt__Key=YourSecretKeyHere \
-e Jwt__Issuer=WatchTracker \
-e Jwt__Audience=WatchTracker \
-e AllowedOrigins=* \
-v watch-tracker-data:/app/data \
-v watch-tracker-uploads:/app/uploads \
ghcr.io/briandenicola/watch-tracker-app:latestThe application will be available on http://localhost:8080. On first launch, the setup wizard will guide you through creating an admin account.
The .NET backend handles X-Forwarded-For and X-Forwarded-Proto headers automatically. The standalone web container (src/web/Dockerfile) uses a Node.js server that proxies /api/ and /uploads/ to the backend — no nginx required.
Set AllowedOrigins to your public domain(s) or * if the proxy is trusted:
# Single origin
-e AllowedOrigins=https://watches.example.com
# Multiple origins (semicolon-separated)
-e AllowedOrigins="https://watches.example.com;https://api.example.com"The web container accepts an API_URL environment variable (default http://localhost:8080) to locate the backend:
services:
web:
build: src/web
ports:
- "3000:3000"
environment:
- API_URL=http://api:8080
- PORT=3000
api:
build: src/api
# ...The main collection page supports two view modes:
- Gallery View — Card grid showing each watch's cover image with brand and model. Supports infinite scroll (loads 12 at a time), filtering by brand and band type, and sorting by date added, brand, or last worn date (ascending/descending).
- Table View — Sortable table with columns for Brand, Model, Size, Type, Date Added, Last Worn, and Worn Count. All columns are sortable. On mobile screens (≤600px), the table collapses to Brand, Model, and Date Added for readability.
Toggle between views using the ▦/☰ buttons in the toolbar. The Brand field on the Add/Edit Watch forms includes an autocomplete dropdown populated from existing brands in your collection.
Track watches you'd like to buy without cluttering your main collection:
- Add to Wish List — Simplified form collecting brand, model, estimated price, product page URL, and an image URL. The image is downloaded from the URL and stored locally.
- Wish List Gallery — Toggle the "Wish List" button in the toolbar to view your wish list. Cards show the watch image (click to edit) and the brand/model as a link to the product page.
- Edit Wish List Item — Update details, replace the image, delete the item, or mark it as Purchased which redirects to the Add Watch page with the brand and model pre-filled.
Wish list items are stored in the same database table as watches but are hidden from the main collection by default.
Each time you click Wore Today on a watch detail page, an individual wear event is logged (in addition to the aggregate counters). The Stats page (accessible from the navigation bar) shows:
- Summary cards — Total wears and unique watches worn
- Wear Heatmap — A GitHub-style 365-day heatmap showing wear frequency per day, with 4 intensity levels and dark/light theme support
- Wear Timeline — Chronological list of the 30 most recent wear events, each linking to the watch detail page
Each watch can store:
- Core fields — Brand, model, movement type, case size, band type, purchase date/price
- Extended properties — Crystal type, case shape, crown type, calendar type, country of origin, water resistance, lug width, dial color, bezel type, power reserve, serial/reference number
- Link — A URL with customizable display text (defaults to "Product Page"), shown as a chip on the detail page
- Notes — Markdown-supported notes field, displayed in a scrollable container within the Additional Details accordion
- Images — Multiple image uploads per watch with the ability to choose a cover image for the gallery view
Upload photos of a watch and click 🤖 Analyze with AI to get an AI-powered analysis via the Anthropic API. The analysis is returned in a modal popup rendered as Markdown. If accepted, the analysis is appended to the watch's notes.
When editing a watch with multiple images, a Gallery Image picker lets you choose which image appears as the cover in the gallery view.
All authenticated users can access Settings (click your avatar in the navigation bar) to configure:
- My Account — Change your display username and profile photo. Your email address is shown but not editable.
- Change Password — Update your account password.
- Theme — Switch between light and dark mode. Defaults to the OS preference and persists in the browser.
- Default View — Choose between gallery and table as the default collection view.
- Time Zone — Select your time zone for date/time display. Defaults to your browser's time zone.
Settings are displayed in a modal popup rather than a separate page.
Admins can manage application-wide settings under Admin → Settings, organized into grouped sections:
- My Account — Change your display username. Your email address is shown but not editable.
- AI Configuration — View the masked Anthropic API key (first 10 characters only) and reset it with a new value. Customize the AI analysis prompt.
- Security — Max failed login attempts and lockout duration.
- Logging — Runtime log level (Trace through None). Changes take effect immediately without a restart.
| Variable | Default | Description |
|---|---|---|
JWT_KEY |
(required) | JWT signing key (openssl rand -base64 48) |
ALLOWED_ORIGINS |
* |
CORS allowed origins (* or semicolon-separated URLs) |
LOG_LEVEL |
Information |
Initial log level (Trace, Debug, Information, Warning, Error, Critical, None) |
ASPNETCORE_FORWARDEDHEADERS_ENABLED |
true |
Enable forwarded headers for reverse proxy support |
watch-tracker-app/
├── .github/
│ └── workflows/
│ ├── codeql-analysis.yml # CodeQL security scanning
│ └── docker-publish.yml # Build & push to Docker Hub
├── src/
│ ├── api/ # .NET 10 Web API
│ │ ├── Controllers/ # API endpoints
│ │ ├── DTOs/ # Data transfer objects
│ │ ├── Models/ # EF Core entities
│ │ ├── Services/ # Business logic
│ │ ├── Data/ # DbContext & configuration
│ │ ├── Migrations/ # EF Core migrations
│ │ └── Program.cs # App entry point
│ └── web/ # React SPA
│ ├── src/
│ │ ├── api/ # API client & resource functions
│ │ ├── components/ # Reusable components (WatchCard, WatchForm, etc.)
│ │ ├── context/ # React context (auth, preferences)
│ │ ├── pages/ # Page components
│ │ ├── types/ # TypeScript type definitions
│ │ └── utils/ # Utility functions (gravatar, etc.)
│ ├── public/ # PWA icons & static assets
│ ├── index.html
│ └── vite.config.ts
├── Dockerfile # Multi-stage build (web + API)
├── Taskfile.yml # Task runner configuration
├── docker-compose.yaml # Container orchestration
└── README.md
Future feature ideas for the app:
- Maintenance Tracker — Log service history, battery replacements, and strap changes. Set reminders for the next scheduled service.
- Watch Comparison — Side-by-side spec comparison of any two watches in your collection.
- Collection Statistics — Dashboard with total collection value, most-worn watch, brand breakdown charts, average price, and wearing streaks.
- Collection Timeline — Visual timeline of when each watch was acquired, showing how the collection grew over time.
- Export / Import — Export your collection as CSV or JSON. Import watches from a spreadsheet.
This project is licensed under the MIT License.