Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# 🔒 SECURITY NOTICE: Never commit .env to version control!
# Copy this file to .env and add your actual Supabase credentials
#
# The app works in two modes:
# - WITH credentials: Full cloud sync and authentication
# The app works in three modes:
# - WITH Supabase credentials: Full cloud sync and authentication
# - WITHOUT credentials: Local storage only (no account needed)
# - WITH VITE_DATA_BACKEND=sql: Self-hosted SQL backend (see below)

# Your Supabase Project Settings
VITE_SUPABASE_URL=https://your-project-id.supabase.co
Expand Down Expand Up @@ -31,3 +32,23 @@ VITE_GEMINI_API_KEY=your_gemini_api_key_here
# Enable verbose database and auth call logging (development only)
# Set to 'true' to see detailed console logs for every database operation
# VITE_ENABLE_DB_LOGS=true

# 🗄️ Self-Hosted SQL Backend (optional, opt-in)
# Lets you run Timetraked against your own Postgres or MySQL database instead
# of Supabase or localStorage. Requires running the backend in server/
# (`pnpm run db:migrate`, `pnpm run db:seed`, `pnpm run server:start`).
# Leave VITE_DATA_BACKEND unset to keep using Supabase/localStorage as before.

# Frontend (safe to expose — read by the browser bundle):
# VITE_DATA_BACKEND=sql
# VITE_SQL_API_URL=http://localhost:4001/api

# Backend only (used by server/, never VITE_-prefixed, never sent to the browser):
# DB_CLIENT=pg # "pg" (PostgreSQL) or "mysql2" (MySQL)
# DB_HOST=localhost
# DB_PORT=5432 # 3306 for MySQL
# DB_USER=timetraked
# DB_PASSWORD=your_db_password_here
# DB_NAME=timetraked
# DB_SSL=false # set to true if your database requires SSL
# SQL_SERVER_PORT=4001 # port the server/ API listens on
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,11 @@ export const MyComponent = () => {
| File | Purpose |
| --------------------------------------------- | ---------------------------------------------------------------------------------- |
| `src/contexts/TimeTrackingContext.tsx` | Main application state and logic (1200+ lines) |
| `src/services/dataService.ts` | Data persistence factory — returns `LocalStorageService` or `SupabaseService` |
| `src/services/dataService.ts` | Data persistence factory — returns `LocalStorageService`, `SupabaseService`, or `SqlApiService` |
| `src/services/supabaseService.ts` | Supabase data persistence implementation (1100+ lines) |
| `src/services/localStorageService/` | localStorage data persistence implementation (split into per-entity modules) |
| `src/services/sqlApiService.ts` | `DataService` implementation that talks to the self-hosted `server/` REST API over `fetch` |
| `server/` | Optional self-hosted SQL backend (Express + Knex, Postgres/MySQL) — see `docs/SQL_BACKEND.md` |
| `src/contexts/AuthContext.tsx` | Authentication state management |
| `src/lib/supabase.ts` | Supabase client configuration and caching |
| `src/config/categories.ts` | Default category definitions |
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Self-hosted SQL backend (opt-in, additive) — lets local deployments use their own PostgreSQL or MySQL database instead of Supabase or `localStorage`. Browsers can't open a raw SQL connection, so this adds a small Express + Knex REST API (`server/`) that the frontend talks to over HTTP when `VITE_DATA_BACKEND=sql` is set. Supabase and `localStorage` modes are unchanged when this var is unset.
— `server/` (new: `db.ts`, `schema.ts`, `app.ts`, `index.ts`, `migrate.ts`, `seed.ts`, `repositories/*.ts`, `types.ts`), `src/services/sqlApiService.ts` (new), `src/services/dataService.ts` (factory branch), `package.json` (`db:migrate`, `db:seed`, `server:dev`, `server:start` scripts; `express`, `knex`, `pg`, `mysql2`, `cors`, `dotenv`, `tsx` deps), `.env.example`, `docs/SQL_BACKEND.md` (new)

- `AppSidebar` — new collapsible sidebar navigation component replacing the top `SiteNavigationMenu`. Uses shadcn/ui `Sidebar` primitives; groups nav items into Planning/Manage/Reports sections; shows live session timer in the footer alongside sync status, auth, and export actions.
— `src/components/AppSidebar.tsx` (new), `src/App.tsx` (wrapped in `SidebarProvider`, renders `AppSidebar`)

Expand Down
37 changes: 34 additions & 3 deletions README-EXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ Task descriptions support **GitHub Flavored Markdown (GFM)**:
| ---------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Guest (default)** | No account required. All data in `localStorage`. Full functionality, single-device only. |
| **Authenticated (optional)** | Sign in via Supabase. Data synced to PostgreSQL. Multi-device access with automatic `localStorage` migration. |
| **Self-hosted SQL (optional)** | Opt-in via `VITE_DATA_BACKEND=sql`. Talks to the small REST API in `server/` backed by your own Postgres or MySQL database. See [docs/SQL_BACKEND.md](docs/SQL_BACKEND.md). |

### How Data Storage Works

Expand Down Expand Up @@ -218,6 +219,34 @@ cp .env.example .env

> ⚠️ Never commit your `.env` file to version control.

### Setting Up a Self-Hosted SQL Backend

For local deployments that don't want Supabase, Timetraked can run against your own PostgreSQL or MySQL database via a small bundled REST API (`server/`). This mode is fully opt-in — leaving `VITE_DATA_BACKEND` unset keeps the existing Supabase/`localStorage` behavior unchanged.

**1. Configure the backend** in `.env` (see `.env.example` for the full list): `DB_CLIENT` (`pg` or `mysql2`), `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME`.

**2. Apply the schema and seed default data:**

```bash
pnpm run db:migrate
pnpm run db:seed
```

**3. Start the backend API:**

```bash
pnpm run server:dev
```

**4. Point the frontend at it** in `.env`:

```bash
VITE_DATA_BACKEND=sql
VITE_SQL_API_URL=http://localhost:4001/api
```

See [docs/SQL_BACKEND.md](docs/SQL_BACKEND.md) for full setup details.

### Authentication Flow

**Sign In:** Click "Sign In" → enter credentials → data migrates from `localStorage`.
Expand Down Expand Up @@ -265,7 +294,7 @@ App.tsx

#### 2. Service Layer Pattern

Data persistence is abstracted through a factory that returns either `LocalStorageService` or `SupabaseService` depending on auth state:
Data persistence is abstracted through a factory that returns `LocalStorageService`, `SupabaseService`, or `SqlApiService` depending on auth state and the `VITE_DATA_BACKEND` env var:

```typescript
interface DataService {
Expand Down Expand Up @@ -390,8 +419,9 @@ src/
│ └── TaskList.tsx # Active task list and NewTaskForm
├── services/
│ ├── localStorageService/ # localStorage implementation (per-entity modules)
│ ├── dataService.ts # Factory — returns LocalStorage or Supabase impl
│ └── supabaseService.ts # Supabase implementation (1100+ lines)
│ ├── dataService.ts # Factory — returns LocalStorage, Supabase, or SqlApi impl
│ ├── supabaseService.ts # Supabase implementation (1100+ lines)
│ └── sqlApiService.ts # REST client for the self-hosted server/ backend
├── utils/
│ ├── calculationUtils.ts # Revenue and hours calculations
│ ├── checklistUtils.ts # GFM checklist extraction
Expand Down Expand Up @@ -566,6 +596,7 @@ const MyPage = lazy(() => import("./pages/MyPage"));
| [docs/AUTH_DATA_PERSISTENCE_FIX.md](docs/AUTH_DATA_PERSISTENCE_FIX.md) | Persistence implementation details |
| [docs/SCHEMA_COMPATIBILITY.md](docs/SCHEMA_COMPATIBILITY.md) | Database schema history |
| [docs/MIGRATION.md](docs/MIGRATION.md) | Supabase data migration guide |
| [docs/SQL_BACKEND.md](docs/SQL_BACKEND.md) | Self-hosted SQL (Postgres/MySQL) backend setup |
| [docs/SECURITY.md](docs/SECURITY.md) | Security configuration and practices |
| [docs/CSV_TEMPLATES_README.md](docs/CSV_TEMPLATES_README.md) | CSV import/export format |
| [docs/FEATURES.md](docs/FEATURES.md) | Feature requests and improvement notes |
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ A Progressive Web App (PWA) for time tracking built with React, TypeScript, and
- **CSV Import** — bring in existing time data from other tools
- **Weekly Report** — AI-generated work summaries (standup, client, or retrospective tone)
- **No Account Required** — full functionality with local storage; optional cloud sync via Supabase
- **Self-Hosted SQL Backend** — optionally run against your own PostgreSQL or MySQL database instead of Supabase or local storage (see [docs/SQL_BACKEND.md](docs/SQL_BACKEND.md))
- **PWA** — installable on desktop/mobile

---
Expand Down Expand Up @@ -68,6 +69,12 @@ pnpm screenshots # Capture screenshots (headless)
pnpm test-csv-import
pnpm test-full-import
pnpm test-error-handling

# Self-Hosted SQL Backend (optional)
pnpm db:migrate # apply schema to your Postgres/MySQL database
pnpm db:seed # seed default categories/projects
pnpm server:dev # run the backend API in watch mode
pnpm server:start # run the backend API
```

---
Expand Down Expand Up @@ -126,6 +133,7 @@ See [CHANGELOG.md](CHANGELOG.md) for the full history of changes.

**Recent highlights:**

- **Self-hosted SQL backend** — opt-in PostgreSQL/MySQL support via a small REST API (`server/`), alongside the existing Supabase and local storage modes
- **Backdated entry creation** — "Add Past Entry" button on Archive page opens a multi-step dialog to log tasks for any past date
- **Kanban planning board** — drag-and-drop task planning view (`KanbanBoard`, `KanbanColumn`, `PlannedTaskCard`)
- Persistent report summaries saved to localStorage; markdown preview/export in the report output panel
Expand Down
92 changes: 92 additions & 0 deletions docs/SQL_BACKEND.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Self-Hosted SQL Backend

Timetraked can run against your own PostgreSQL or MySQL database instead of Supabase or `localStorage`. This is an **opt-in third storage mode** — Supabase and `localStorage` continue to work exactly as before if you don't configure this.

Browsers cannot open a raw MySQL/Postgres connection directly, so this mode adds a small self-hosted REST API (`server/`) that the frontend talks to over HTTP. The API is a thin wrapper around [Knex](https://knexjs.org), so the same code works against either database engine.

## When to use this

- You want to self-host Timetraked with your own database instead of relying on Supabase.
- You're fine running one extra Node process (the `server/` API) alongside the frontend.

If you just want a quick start with no account, the default `localStorage` mode requires no setup at all. If you want multi-device cloud sync, Supabase is the simplest path. This mode is for self-hosters who specifically want their own SQL database.

## Architecture

```text
Browser (SqlApiService) --HTTP--> server/app.ts (Express) --Knex--> Postgres or MySQL
```

- `server/db.ts` — Knex connection, configured by `DB_*` env vars.
- `server/schema.ts` — idempotent schema creation (`ensureSchema`), cross-dialect (Postgres + MySQL).
- `server/repositories/*.ts` — one module per entity (current day, archived days, projects, clients, categories, todos, planned tasks), mirroring `SupabaseService`'s persistence semantics.
- `server/app.ts` / `server/index.ts` — Express app and entrypoint, served under `/api/*`.
- `server/migrate.ts` / `server/seed.ts` — standalone scripts for schema setup and default data.
- `src/services/sqlApiService.ts` — frontend `DataService` implementation that calls the API via `fetch`.

This backend is **single-tenant**: there's no `user_id` or row-level security, since it's meant for one self-hosted deployment with one shared dataset — the same data model as guest/`localStorage` mode, just persisted centrally.

## Setup

### 1. Provision a database

Create an empty PostgreSQL or MySQL database and a user with access to it.

### 2. Configure environment variables

Copy `.env.example` to `.env` and fill in the backend-only variables (none of these are `VITE_`-prefixed, so they're never bundled into the browser build):

```bash
DB_CLIENT=pg # "pg" for PostgreSQL, "mysql2" for MySQL
DB_HOST=localhost
DB_PORT=5432 # 3306 for MySQL
DB_USER=timetraked
DB_PASSWORD=your_db_password_here
DB_NAME=timetraked
DB_SSL=false # set true if your database requires SSL
SQL_SERVER_PORT=4001 # port the server/ API listens on
```

### 3. Apply the schema

```bash
pnpm run db:migrate
```

Safe to re-run — it only creates tables that don't already exist.

### 4. Seed default data (optional)

```bash
pnpm run db:seed
```

Populates the default categories and projects (and their derived clients) the same way a fresh `localStorage` install would. Safe to re-run — it skips seeding if rows already exist.

### 5. Start the backend API

```bash
pnpm run server:dev # watch mode, for local development
pnpm run server:start # plain start, for production
```

### 6. Point the frontend at it

Add to `.env`:

```bash
VITE_DATA_BACKEND=sql
VITE_SQL_API_URL=http://localhost:4001/api
```

Restart the Vite dev server (or rebuild) so the new env vars are picked up.

## Migrating existing data

`SqlApiService` implements the same `migrateFromLocalStorage()` / `migrateToLocalStorage()` methods as `SupabaseService`: switching `VITE_DATA_BACKEND` to `sql` on a browser that already has guest `localStorage` data will pull that data into the SQL backend (only filling in entities the backend doesn't already have).

## Troubleshooting

- **Frontend can't reach the backend**: confirm `VITE_SQL_API_URL` matches where `server/index.ts` is actually listening, and that CORS isn't being blocked (the server enables CORS for all origins by default).
- **`db:migrate` fails to connect**: double-check `DB_HOST`/`DB_PORT`/`DB_USER`/`DB_PASSWORD`/`DB_NAME` and that the database accepts connections from where the script runs.
- **MySQL errors about key length**: this shouldn't happen with the bundled schema (all indexed columns are `VARCHAR`, not `TEXT`), but if you've hand-edited `server/schema.ts`, remember MySQL requires an explicit length on indexed `TEXT` columns.
15 changes: 14 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
"electron:build:main": "vite build --config vite.electron.config.ts",
"electron:dev": "pnpm run electron:build:main && concurrently \"pnpm run dev\" \"wait-on http://localhost:8080 && ELECTRON_DEV=true electron dist-electron/main.cjs\"",
"electron:preview": "pnpm run build && pnpm run electron:build:main && electron dist-electron/main.cjs",
"electron:build": "pnpm run build && pnpm run electron:build:main && electron-builder"
"electron:build": "pnpm run build && pnpm run electron:build:main && electron-builder",
"db:migrate": "tsx server/migrate.ts",
"db:seed": "tsx server/seed.ts",
"server:dev": "tsx watch server/index.ts",
"server:start": "tsx server/index.ts"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
Expand Down Expand Up @@ -60,9 +64,15 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"embla-carousel-react": "^8.6.0",
"express": "^5.2.1",
"knex": "^3.2.10",
"lucide-react": "^1.17.0",
"mysql2": "^3.22.5",
"next-themes": "^0.3.0",
"pg": "^8.21.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
Expand Down Expand Up @@ -91,6 +101,8 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^14.3.1",
"@testing-library/user-event": "^14.6.1",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/node": "^22.19.17",
"@types/react": "^18.3.28",
"@types/react-dom": "^18.3.7",
Expand All @@ -108,6 +120,7 @@
"shadcn": "^4.9.0",
"tailwindcss": "^4.3.0",
"ts-node": "^10.9.2",
"tsx": "^4.22.4",
"typescript": "^5.9.3",
"typescript-eslint": "^8.38.0",
"vite": "^5.4.21",
Expand Down
Loading
Loading