Skip to content

Commit 9ed616f

Browse files
committed
Solidify sync engine + add web UI
1 parent cd351ea commit 9ed616f

43 files changed

Lines changed: 10564 additions & 156 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.local.example

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ TASK_SYNC_PROVIDER_B=microsoft
66
TASK_SYNC_STATE_DIR=.task-sync
77
TASK_SYNC_LOG_LEVEL=info
88

9-
# Google Tasks
9+
# Google Tasks — https://console.cloud.google.com/
1010
TASK_SYNC_GOOGLE_CLIENT_ID=
1111
TASK_SYNC_GOOGLE_CLIENT_SECRET=
1212
TASK_SYNC_GOOGLE_REFRESH_TOKEN=
13-
TASK_SYNC_GOOGLE_TASKLIST_ID=@default
13+
# TASK_SYNC_GOOGLE_TASKLIST_ID=@default
1414

15-
# Microsoft To Do
15+
# Microsoft To Do — https://portal.azure.com/
1616
TASK_SYNC_MS_CLIENT_ID=
17-
TASK_SYNC_MS_TENANT_ID=common
17+
TASK_SYNC_MS_CLIENT_SECRET=
18+
TASK_SYNC_MS_TENANT_ID=consumers
1819
TASK_SYNC_MS_REFRESH_TOKEN=
1920
# TASK_SYNC_MS_LIST_ID=

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,9 @@ yarn-error.log*
1818

1919
# OS
2020
.DS_Store
21+
22+
# web
23+
web/.next/
24+
web/node_modules/
25+
web/out/
26+
web/next-env.d.ts

README.md

Lines changed: 172 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -2,162 +2,251 @@
22

33
Sync tasks between **Google Tasks** and **Microsoft To Do**.
44

5-
Providers:
5+
Works as a CLI for power users, or as a self-hosted web UI for everyone else.
66

7-
- Google Tasks (OAuth refresh-token)
8-
- Microsoft To Do via Microsoft Graph (OAuth refresh-token)
7+
## Features
98

10-
## Quickstart
9+
- **Bidirectional sync** between Google Tasks and Microsoft To Do
10+
- **Field-level conflict resolution** (last-write-wins per field)
11+
- **Cold-start matching** — deduplicates tasks on first sync by title + notes
12+
- **Delete propagation** — tombstones prevent resurrecting deleted tasks
13+
- **Dry-run mode** — preview changes before applying
14+
- **Polling mode** — auto-sync on an interval
15+
- **Web UI** — connect accounts with OAuth, sync with one click
16+
- **Self-hosted** — your data stays on your machine
1117

12-
### Requirements
18+
## Requirements
1319

1420
- Node.js **>= 22**
1521

16-
### Install
22+
## Quick Start — Web UI
1723

18-
```bash
19-
npm install
20-
```
24+
The web UI lets you connect your Google and Microsoft accounts via OAuth
25+
and sync tasks with one click. No manual token management needed.
2126

22-
### Build + run doctor
27+
### 1. Set up OAuth apps
2328

24-
```bash
25-
npm run build
26-
node dist/cli.js doctor
27-
```
29+
**Google Tasks:**
30+
31+
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
32+
2. Create a project (or select existing)
33+
3. Enable the **Google Tasks API**
34+
4. Go to **APIs & Services → Credentials → Create Credentials → OAuth client ID**
35+
5. Application type: **Web application**
36+
6. Add authorized redirect URI: `http://localhost:3000/api/auth/google/callback`
37+
7. Copy the **Client ID** and **Client Secret**
2838

29-
### Run sync once
39+
**Microsoft To Do:**
40+
41+
1. Go to [Azure Portal → App registrations](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade)
42+
2. **New registration**
43+
3. Supported account types: **Personal Microsoft accounts only** (or multi-tenant)
44+
4. Redirect URI (Web): `http://localhost:3000/api/auth/microsoft/callback`
45+
5. Go to **API permissions** → Add: `Tasks.ReadWrite`, `User.Read`, `offline_access`
46+
6. Copy the **Application (client) ID**
47+
48+
### 2. Configure
49+
50+
Create `.env.local` in the project root:
3051

3152
```bash
32-
node dist/cli.js sync
53+
TASK_SYNC_PROVIDER_A=google
54+
TASK_SYNC_PROVIDER_B=microsoft
55+
56+
TASK_SYNC_GOOGLE_CLIENT_ID=your-google-client-id
57+
TASK_SYNC_GOOGLE_CLIENT_SECRET=your-google-client-secret
58+
59+
TASK_SYNC_MS_CLIENT_ID=your-microsoft-client-id
60+
TASK_SYNC_MS_TENANT_ID=consumers
3361
```
3462

35-
### Polling mode
63+
### 3. Install and run
3664

3765
```bash
38-
# every 5 minutes
39-
node dist/cli.js sync --poll 5
40-
41-
# or env
42-
export TASK_SYNC_POLL_INTERVAL_MINUTES=5
43-
node dist/cli.js sync
66+
npm install
67+
npm run web:install
68+
npm run web:dev
4469
```
4570

46-
### Dry-run
71+
Open [http://localhost:3000](http://localhost:3000). Click **Connect** for each
72+
provider, approve the OAuth consent, then hit **Sync Now**.
4773

48-
Dry-run still uses your configured providers, but **does not write** any changes.
74+
### Production
4975

5076
```bash
51-
node dist/cli.js sync --dry-run
77+
npm run web:build
78+
npm run web:start
5279
```
5380

54-
## Configuration (.env)
81+
## Quick Start — CLI
5582

56-
Create a `.env.local` (recommended) or `.env`:
83+
For headless environments, scripts, or cron jobs.
5784

58-
### Provider selection
85+
### 1. Install
5986

6087
```bash
61-
TASK_SYNC_PROVIDER_A=google
62-
TASK_SYNC_PROVIDER_B=microsoft
88+
npm install
89+
npm run build
6390
```
6491

65-
### State
92+
### 2. Get refresh tokens
93+
94+
You need refresh tokens for each provider. Helper scripts are included:
6695

6796
```bash
68-
TASK_SYNC_STATE_DIR=.task-sync
69-
TASK_SYNC_LOG_LEVEL=info
97+
# Google
98+
export TASK_SYNC_GOOGLE_CLIENT_ID=...
99+
export TASK_SYNC_GOOGLE_CLIENT_SECRET=...
100+
npm run oauth:google
101+
102+
# Microsoft
103+
export TASK_SYNC_MS_CLIENT_ID=...
104+
export TASK_SYNC_MS_TENANT_ID=consumers
105+
npm run oauth:microsoft
70106
```
71107

72-
### Google Tasks
108+
Each script opens a browser for consent, then prints the refresh token.
109+
110+
### 3. Configure
111+
112+
Add all tokens to `.env.local`:
73113

74114
```bash
115+
TASK_SYNC_PROVIDER_A=google
116+
TASK_SYNC_PROVIDER_B=microsoft
117+
75118
TASK_SYNC_GOOGLE_CLIENT_ID=...
76119
TASK_SYNC_GOOGLE_CLIENT_SECRET=...
77120
TASK_SYNC_GOOGLE_REFRESH_TOKEN=...
78-
TASK_SYNC_GOOGLE_TASKLIST_ID=@default # optional
79-
```
80121

81-
### Microsoft To Do (Graph)
82-
83-
```bash
84122
TASK_SYNC_MS_CLIENT_ID=...
85-
TASK_SYNC_MS_TENANT_ID=common # or your tenant id
123+
TASK_SYNC_MS_TENANT_ID=consumers
86124
TASK_SYNC_MS_REFRESH_TOKEN=...
87-
TASK_SYNC_MS_LIST_ID=... # optional (defaults to first list)
88125
```
89126

90-
Run:
127+
### 4. Run
91128

92129
```bash
93-
task-sync doctor
130+
# Check config
131+
node dist/cli.js doctor
132+
133+
# Sync once
134+
node dist/cli.js sync
135+
136+
# Dry-run (preview changes)
137+
node dist/cli.js sync --dry-run
138+
139+
# Auto-sync every 5 minutes
140+
node dist/cli.js sync --poll 5
141+
142+
# JSON output (for scripts)
143+
node dist/cli.js sync --format json
94144
```
95145

96-
to see what's missing.
146+
## Configuration
97147

98-
## OAuth helper scripts (refresh tokens)
148+
All configuration is via environment variables. Create a `.env.local` file in the
149+
project root.
99150

100-
These scripts spin up a local HTTP callback server, print an auth URL, and on success print the refresh token.
151+
### Required
101152

102-
### Google refresh token
153+
| Variable | Description |
154+
|---|---|
155+
| `TASK_SYNC_PROVIDER_A` | First provider (`google` or `microsoft`) |
156+
| `TASK_SYNC_PROVIDER_B` | Second provider (`google` or `microsoft`) |
157+
| `TASK_SYNC_GOOGLE_CLIENT_ID` | Google OAuth client ID |
158+
| `TASK_SYNC_GOOGLE_CLIENT_SECRET` | Google OAuth client secret |
159+
| `TASK_SYNC_MS_CLIENT_ID` | Microsoft app (client) ID |
103160

104-
1) Create OAuth credentials in Google Cloud Console:
105-
- APIs & Services → Credentials
106-
- Create Credentials → OAuth client ID
107-
- Application type: **Desktop app** (recommended)
108-
- Enable the **Google Tasks API** on the project
161+
### Optional
109162

110-
2) Set env vars and run:
163+
| Variable | Default | Description |
164+
|---|---|---|
165+
| `TASK_SYNC_MS_TENANT_ID` | `consumers` | Azure tenant ID |
166+
| `TASK_SYNC_GOOGLE_REFRESH_TOKEN` || CLI only: Google refresh token |
167+
| `TASK_SYNC_MS_REFRESH_TOKEN` || CLI only: Microsoft refresh token |
168+
| `TASK_SYNC_GOOGLE_TASKLIST_ID` | `@default` | Google task list to sync |
169+
| `TASK_SYNC_MS_LIST_ID` | First list | Microsoft To Do list to sync |
170+
| `TASK_SYNC_STATE_DIR` | `.task-sync` | Directory for sync state |
171+
| `TASK_SYNC_LOG_LEVEL` | `info` | Log level: `silent\|error\|warn\|info\|debug` |
172+
| `TASK_SYNC_POLL_INTERVAL_MINUTES` || Auto-sync interval (CLI only) |
173+
| `TASK_SYNC_MODE` | `bidirectional` | Sync mode: `bidirectional\|a-to-b-only\|mirror` |
174+
| `TASK_SYNC_TOMBSTONE_TTL_DAYS` | `30` | How long to keep delete tombstones |
111175

112-
```bash
113-
export TASK_SYNC_GOOGLE_CLIENT_ID=...
114-
export TASK_SYNC_GOOGLE_CLIENT_SECRET=...
115-
npm run oauth:google
116-
```
176+
## How It Works
117177

118-
### Microsoft refresh token
178+
### Sync State
119179

120-
1) Create an app registration in Azure:
121-
- Azure Portal → App registrations → New registration
122-
- Add a **redirect URI** (platform: *Mobile and desktop applications*):
123-
- `http://localhost:53683/callback`
124-
- API permissions (Delegated):
125-
- `offline_access`
126-
- `User.Read`
127-
- `Tasks.ReadWrite`
180+
`task-sync` stores local state in `.task-sync/state.json`:
128181

129-
2) Run:
182+
- **`lastSyncAt`** — watermark timestamp for incremental sync
183+
- **`mappings`** — links canonical task IDs to provider-specific IDs
184+
- **`tombstones`** — prevents resurrecting deleted tasks
130185

131-
```bash
132-
export TASK_SYNC_MS_CLIENT_ID=...
133-
export TASK_SYNC_MS_TENANT_ID=common
134-
npm run oauth:microsoft
135-
```
186+
Delete `.task-sync/` to reset all sync state.
136187

137-
## How state works
188+
### Web UI Token Storage
138189

139-
`task-sync` writes local state under:
190+
The web UI stores OAuth refresh tokens in `.task-sync/tokens.json`. These
191+
tokens never leave your machine. The web server uses them to authenticate
192+
with Google and Microsoft APIs on your behalf during sync.
140193

141-
- `.task-sync/state.json`
194+
### Sync Algorithm
142195

143-
This includes:
196+
1. **Fetch** tasks from all providers (incremental via `lastSyncAt`)
197+
2. **Cold-start match** — on first run, match tasks by title+notes to avoid duplicates
198+
3. **Tombstone check** — skip tasks that were intentionally deleted
199+
4. **Field-level diff** — compare each field (title, notes, status, due date) against the last canonical snapshot
200+
5. **Conflict resolution** — if multiple providers changed the same field, last-write-wins
201+
6. **Fan out** — apply the resolved canonical state to all providers
144202

145-
- `lastSyncAt` watermark (ISO timestamp)
146-
- `mappings`: links a canonical ID to provider IDs
147-
- `tombstones`: prevents resurrecting deleted tasks
203+
## Project Structure
148204

149-
Delete `.task-sync/` to reset sync state.
205+
```
206+
task-sync/
207+
├── src/ # Core engine + CLI
208+
│ ├── cli.ts # CLI entry point
209+
│ ├── sync/engine.ts # Sync algorithm
210+
│ ├── providers/ # Google, Microsoft, Mock providers
211+
│ ├── store/ # State persistence (JSON)
212+
│ ├── model.ts # Task data model
213+
│ └── ...
214+
├── web/ # Next.js web UI
215+
│ ├── app/ # Pages + API routes
216+
│ │ ├── page.tsx # Dashboard
217+
│ │ └── api/ # OAuth + sync endpoints
218+
│ ├── components/ # React components (shadcn/ui)
219+
│ └── lib/ # Env loading, token storage
220+
├── test/ # Vitest tests
221+
├── scripts/ # OAuth helper scripts
222+
└── .env.local # Your credentials (git-ignored)
223+
```
150224

151225
## Development
152226

153227
```bash
228+
# Run CLI in dev mode
154229
npm run dev -- doctor
155230
npm run dev -- sync --dry-run
231+
232+
# Run web in dev mode (builds core first)
233+
npm run web:dev
234+
235+
# Tests
156236
npm test
237+
238+
# Lint & typecheck
157239
npm run lint
158240
npm run typecheck
159241
```
160242

243+
## Security Notes
244+
245+
- **Self-hosted only** — this project is designed to run on your own machine or server.
246+
- **No telemetry** — no data is sent anywhere except Google and Microsoft APIs.
247+
- **Tokens on disk** — refresh tokens are stored in `.task-sync/tokens.json`. Treat this file like a password.
248+
- **No auth on the web UI** — if you expose the web UI to the internet, put it behind a reverse proxy with authentication (e.g., Caddy, nginx + basic auth, Cloudflare Tunnel).
249+
161250
## License
162251

163-
MIT (see LICENSE)
252+
MIT (see [LICENSE](LICENSE))

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default tseslint.config(
55
js.configs.recommended,
66
...tseslint.configs.recommended,
77
{
8-
ignores: ['dist/**', 'node_modules/**'],
8+
ignores: ['dist/**', 'node_modules/**', 'web/**'],
99
},
1010
{
1111
rules: {

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
"lint": "eslint .",
1212
"typecheck": "tsc -p tsconfig.json --noEmit",
1313
"oauth:google": "tsx scripts/google_oauth.ts",
14-
"oauth:microsoft": "tsx scripts/microsoft_oauth.ts"
14+
"oauth:microsoft": "tsx scripts/microsoft_oauth.ts",
15+
"web:dev": "npm run build && npm run --prefix web dev",
16+
"web:build": "npm run build && npm run --prefix web build",
17+
"web:start": "npm run --prefix web start",
18+
"web:install": "npm install --prefix web"
1519
},
1620
"repository": {
1721
"type": "git",

src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ program
9090
clientId: env.TASK_SYNC_MS_CLIENT_ID!,
9191
tenantId: env.TASK_SYNC_MS_TENANT_ID!,
9292
refreshToken: env.TASK_SYNC_MS_REFRESH_TOKEN!,
93+
// CLI uses public client flow (Mobile/Desktop platform) — no secret needed.
94+
// The web UI uses confidential client flow (Web platform) with its own secret.
9395
listId: env.TASK_SYNC_MS_LIST_ID,
9496
});
9597
};

0 commit comments

Comments
 (0)