Skip to content

Repository files navigation

Woven Time Tracking

Internal time tracking tool for the Woven Research & Insights team. Tracks time logged against client projects with workspace isolation, Microsoft SSO, and reporting.

Features

  • Projects — Auto-generated project codes (workspace-configurable prefix, e.g. WRI-001, WRI-002, …), project types with color tags, budgeted hours with usage progress bar, file attachments, and team member allocation (with a designated SPOC per project); assigned members are emailed when added to a project. The list view is kept compact (Code, Project Name, Type, Request Date, Client, Users Assigned, Status, TAT) and sorts by Request Date (newest first) by default; Requestor and Credits are omitted from the list and shown only in the detail panel, which surfaces every project field (Request Date, Requestor, Report Initiated, Report Delivered, TAT, Users Assigned, Credits, budget usage, members, timesheets, and documents). A "Users Assigned" column/field shows project members as soon as they're assigned, regardless of whether they've logged time yet, and is visible to all roles, not just admins. Admins can export the full project list to CSV.
  • Timesheets — Log time against your assigned projects only; entries are tied to the logged-in user automatically and can only be viewed, edited, or deleted by their owner (or a workspace admin — only admins can see everyone's entries, via an "All Entries" toggle). The daily 24h logging cap is scoped to the active workspace, so hours logged in one workspace never block entries in another for users who belong to multiple workspaces. The Log Time project picker only lists active projects (an in-progress edit keeps its own project selectable even if that project has since been put on hold/completed/cancelled). Log Time defaults to the project's Report Initiated date and is constrained to fall within the project's Report Initiated / Report Delivered window (enforced both client- and server-side)
  • Report status updates — Project members (not just admins) can update a project's status inline from the Projects table; all other project fields remain admin-only
  • Clients & Contacts — Multi-project clients with contacts (name, email, phone required); contacts can be set as project requestors
  • Reports — Time by user, project, client, or period (Daily / Weekly / Monthly, with business-day TAT per project), with budget vs. logged hours visualization, CSV export, and a report delivery calendar view showing delivered/upcoming report dates; "Users Assigned" reflects project membership, not just users who've logged time. The By Project view has an All / Active / Completed status filter
  • Daily backup email — Every day at 8:00 AM IST, the server emails a workbook (Projects + Timesheet Entries sheets) to a fixed recipient; sent via Microsoft Graph sendMail (trying each configured SUPER_ADMIN_EMAIL sender in turn) when Microsoft SSO is configured, otherwise via SMTP. A super admin can also trigger it on demand from POST /api/admin/send-backup-now
  • Workspaces — Each team gets an isolated workspace; users can belong to multiple workspaces; any authenticated user can create a new workspace (becoming its admin); new sign-ups can alternatively join an existing workspace that already has a member sharing their email domain, joining as a regular member. Each page has its own URL route
  • Settings (Admin) — Workspace admins manage project types, workspace users/roles, and clients & contacts; a super admin (configured via SUPER_ADMIN_EMAIL) can additionally manage all workspaces org-wide
  • Microsoft SSO — Azure AD OAuth2 login restricted to the configured tenant domain; dev-login fallback when credentials are not configured

Tech Stack

Layer Technology
Frontend React 18 + Vite
Backend Express 4 + Node.js built-in node:sqlite
Database SQLite (via Node 22 native module)
Auth Azure AD OAuth2 + custom SQLite sessions
File uploads multer (stored in uploads/)
Containerization Docker + Docker Compose (autoheal for health-based restarts); PM2 supported as a legacy alternative
Reverse proxy Shared nginx-proxy container on the host (--network host, config outside this repo — routes several unrelated domains besides this app)

Node 22+ required — the node:sqlite built-in is only available from Node 22 onwards.

Local Development

Prerequisites

  • Node.js v22+
  • npm

Setup

# Install dependencies
npm install

# Copy and fill in environment variables
cp .env.example .env

Edit .env with your values (see Environment Variables).

Run

# Start both backend and Vite dev server concurrently
npm run dev

Backend runs on http://localhost:3000, Vite dev server on http://localhost:5173 (proxies /api and /auth to the backend automatically).

When AZURE_CLIENT_ID is not set, a dev-login form is shown instead of Microsoft SSO — no Azure credentials needed for local development.

Production Build & Deployment

# Build the frontend
npm run build

# Start the server (serves built frontend + API)
npm start

The server serves the compiled dist/ and all /api/* routes from a single Express process.

Docker (production)

The app ships with a multi-stage Dockerfile and a docker-compose.yml that runs the app plus an autoheal sidecar, so the service comes back on its own whether it crashes outright or just goes unresponsive:

  • restart: unless-stopped relaunches the container whenever the process exits.
  • The image's HEALTHCHECK hits /api/health; autoheal watches container health and force-restarts anything Docker marks unhealthy (a hang that never exits on its own).
  • The SQLite DB and uploads/ live in named volumes (db-data, uploads-data) so they survive rebuilds/restarts.
# On the server, with .env present (see below)
docker compose up -d --build
docker compose logs -f app        # tail logs
docker compose ps                 # check health status

The container listens on 3000 internally and is published on host port 3009. Production traffic is routed by a separate, shared nginx-proxy container (--network host, config at /opt/alternative-ens-deployment/nginx.production.conf on the host — not part of this repo, and not Caddy despite earlier docs here saying so) whose time_tracking upstream points at 127.0.0.1:3009 (see nginx-proxy below). Before repointing ports: to a different host port, check that nginx config first — 3002 is already claimed by an unrelated site (salesforce_lens_web / lens.nativeworld.com) and 3007 was the legacy PM2 port; colliding with either has previously taken production down (see #103, and the "Disable deploy.yml auto-trigger" commit for a second incident where stopping what looked like a stale container on a shared port took the real production container down for ~2 hours).

Production deployment (current): the GitHub Actions workflow (.github/workflows/deploy.yml) is manual-trigger only (workflow_dispatch) — it is not wired to run on every push to main, despite what earlier revisions of this doc said. The push: branches: [main] trigger was disabled after the incident above and has been left disabled deliberately; re-enable it only once this deploy path has run stably for a while. To deploy, run the "Deploy to Production" workflow manually (Actions tab → Deploy to Production → Run workflow, on main). Once triggered, it rsyncs the repo to /var/www/time-tracking/ on the server, runs docker compose up -d --build --remove-orphans, and polls docker inspect's health status until the container reports healthy (failing the workflow and dumping the last 100 log lines if it doesn't within ~2 minutes).

Migrating an existing PM2 deployment to Docker (already done in production, kept here for reference): stop and remove the PM2 process first — pm2 delete woven-time-tracking — then run docker compose up -d --build from /var/www/time-tracking, and repoint the time_tracking upstream in the shared nginx-proxy config from PM2's port to this container's port (see nginx-proxy below). The SQLite DB isn't shared automatically; to carry over existing data, copy the old timetracking.db and uploads/ into the new named volumes before first start (e.g. docker run --rm -v woven-time-tracking_db-data:/data -v /var/www/time-tracking:/old alpine cp /old/timetracking.db /data/timetracking.db, similarly for uploads-data).

Deploying to the server (PM2, legacy fallback)

Only needed if Docker is unavailable on the host and you must fall back to the pre-Docker deployment path — production no longer runs this by default.

# Sync server files
rsync -avz --exclude node_modules --exclude dist --exclude '*.db' --exclude .env --exclude uploads \
  ./ user@your-server:/var/www/time-tracking/

# On the server
cd /var/www/time-tracking
npm install --omit=dev
npm run build
pm2 restart woven-time-tracking

See PM2 & nginx-proxy for full server setup.

Environment Variables

Copy .env.example to .env and fill in:

Variable Description
AZURE_TENANT_ID Azure AD tenant ID or domain (e.g. woventalent.in) — also used to reject SSO logins from email domains outside this tenant
AZURE_CLIENT_ID Azure AD application (client) ID
AZURE_CLIENT_SECRET Azure AD client secret value
AUTH_REDIRECT_URI OAuth2 callback URL (e.g. https://your-production-domain.example.com/auth/callback)
SUPER_ADMIN_EMAIL Email (or comma-separated emails) granted the super_admin global role, which can manage all workspaces
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM SMTP credentials for project-assignment emails (used as a fallback when Microsoft SSO isn't configured); assignment emails are silently skipped if unset
APP_URL Public app URL, used as the link target in assignment emails (set this to your production domain)
PORT Port for the Express server (default 3000)
NODE_ENV Set to production to enable secure (HTTPS-only) session cookies
DB_PATH Path to the SQLite database file (default <app dir>/timetracking.db) — set by docker-compose.yml to point at the db-data volume
UPLOAD_DIR Directory for uploaded project files (default <app dir>/uploads) — set by docker-compose.yml to point at the uploads-data volume

If AZURE_CLIENT_ID is not set, the app falls back to a dev-login form.

GET /api/health returns { status: 'ok' } and requires no auth — used by the Docker HEALTHCHECK and any external uptime monitor.

PM2 & nginx-proxy

PM2 (ecosystem.config.cjs) — legacy fallback

Production runs on Docker (see Docker (production) above); PM2 is kept only as a manual fallback if Docker is ever unavailable on the host. The PM2 config reads .env at startup and passes all variables to the Node process:

pm2 delete woven-time-tracking   # needed after env changes
pm2 start ecosystem.config.cjs
pm2 save

The app must be started with --experimental-sqlite:

node_args: '--experimental-sqlite'

nginx-proxy

Production is not fronted by Caddy, despite what earlier revisions of this README said — that was an earlier session's incorrect assumption, later corrected after reading the real config on the host. Traffic for every domain on this host, including time.woventalent.in, is routed by a single shared nginx-proxy container running with --network host, configured at /opt/alternative-ens-deployment/nginx.production.conf (outside this repo, independently managed). The relevant upstream block:

upstream time_tracking {
    server 127.0.0.1:3009;   # this app's Docker container (see docker-compose.yml)
}

If falling back to the legacy PM2 path, that upstream line is the one to repoint — back to 127.0.0.1:3007 (PM2's port). Never point it at a port another upstream on this same shared proxy already uses (e.g. 3002 serves an unrelated site, salesforce_lens_web / lens.nativeworld.com) — and never assume a container holding a port is "stale" without checking what it actually serves first; doing so once took the real production container down for ~2 hours (see the "Disable deploy.yml auto-trigger" commit).

After editing the config, validate before reloading:

docker exec nginx-proxy nginx -t && docker exec nginx-proxy nginx -s reload

A full docker restart nginx-proxy (rather than a graceful reload) briefly interrupts every other site on the shared proxy, not just this one — prefer nginx -s reload unless a restart is specifically required (e.g. to reattach a detached bind mount).

Microsoft SSO Setup (Azure AD)

  1. Go to Azure Portal → App registrations → New registration
  2. Set Redirect URI to https://your-domain/auth/callback
  3. Under Certificates & secrets, create a new client secret
  4. Copy the Tenant ID, Application (client) ID, and Secret value into .env
  5. Under API permissions, ensure User.Read (Microsoft Graph) is granted

Project Structure

├── server.js            # Express backend — all API routes, auth, SQLite schema
├── src/
│   ├── contexts/
│   │   └── AuthContext.jsx   # Auth state (user, workspace, MSAuth)
│   ├── components/
│   │   ├── Sidebar.jsx
│   │   └── Modal.jsx
│   ├── pages/
│   │   ├── Login.jsx
│   │   ├── WorkspaceSelect.jsx
│   │   ├── Projects.jsx
│   │   ├── Timesheets.jsx
│   │   ├── Reports.jsx
│   │   ├── Calendar.jsx      # Report delivery calendar view
│   │   └── Admin.jsx         # Settings page (project types, users, clients, and — for super admins — all workspaces)
│   └── main.jsx
├── index.html
├── vite.config.js
├── Dockerfile                # Multi-stage build (frontend build + production runtime)
├── docker-compose.yml        # App + autoheal, persistent DB/uploads volumes — current production deployment
├── ecosystem.config.cjs      # PM2 config (legacy fallback, not used in production)
├── .github/workflows/deploy.yml  # Deploys to Docker on the production host — manual trigger only (workflow_dispatch), see Production Build & Deployment
├── .env.example
└── package.json

License

Internal tool — not for public distribution.

About

Time tracking app for Woven teams — React + Express + SQLite, deployed at time.woventalent.in

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages