REST API sync server for Noctalia Todo plugin — SQLite-backed, ZeroTier-friendly
  • QML 54.2%
  • TypeScript 38.6%
  • JavaScript 7%
  • Dockerfile 0.2%
Find a file
Maikel Frias Mosquea 554434f760 docs: remove hardcoded DB credentials from README and AGENTS
Replace hardcoded mariadb://todosync:todosync@... URLs with
placeholders and --env-file .env in Docker commands. Add
env.example as a template for local configuration.
2026-07-08 01:13:40 +02:00
src feat: add active field to todo schema, types, and API 2026-07-04 17:47:58 +02:00
tests test: add active field to test fixtures, import getActiveTodos 2026-07-04 18:02:48 +02:00
todo refactor: convert FocusOverlay from PanelWindow to FloatingWindow 2026-07-06 22:44:27 +02:00
todosync-app feat: add active/focus indicator and toggle to mobile app 2026-07-04 21:06:43 +02:00
.dockerignore fix: include src/ in Docker build context 2026-07-04 07:10:13 +02:00
.gitignore chore: rename TODO.mc to TODO.md, ignore it 2026-07-04 21:09:16 +02:00
AGENTS.md docs: remove hardcoded DB credentials from README and AGENTS 2026-07-08 01:13:40 +02:00
Dockerfile fix: Dockerfile copies dist from builder stage, not stale host dist 2026-07-04 19:02:07 +02:00
env.example docs: remove hardcoded DB credentials from README and AGENTS 2026-07-08 01:13:40 +02:00
package-lock.json feat: switch from PostgreSQL to MariaDB 2026-07-04 05:35:55 +02:00
package.json feat: switch from PostgreSQL to MariaDB 2026-07-04 05:35:55 +02:00
README.md docs: remove hardcoded DB credentials from README and AGENTS 2026-07-08 01:13:40 +02:00
registry.json chore: bump version 1.15.0 -> 1.15.1 2026-07-04 18:16:26 +02:00
tsconfig.json feat: TDD — REST API with full test suite 2026-07-04 04:38:04 +02:00
vitest.config.ts feat: switch from PostgreSQL to MariaDB 2026-07-04 05:35:55 +02:00

todosync

Multi-device todo list with real-time sync. A REST API server + Noctalia Shell plugin that keeps todos synchronised across machines via a central MariaDB database.

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│  Desktop     │     │  REST API    │     │  Laptop     │
│  (Noctalia)  │◄───►│  + MariaDB   │◄───►│  (Noctalia) │
│  Plugin      │     │  (hetzner1)  │     │  Plugin     │
└─────────────┘     └──────────────┘     └─────────────┘

Quick Start

1. Run the server

# Build & run with Podman
npm run build
podman build -t todosync .
podman run -d \
  --name todosync \
  -p 3456:3456 \
  -e DATABASE_URL="mariadb://user:pass@host:3306/todosync" \
  todosync

# Or directly (dev)
npm run dev

2. Install the plugin

# Symlink the plugin into Noctalia
ln -sfn ~/code/todosync/todo ~/.config/noctalia/plugins/todo

Register in ~/.config/noctalia/plugins.json:

{
  "todo": {
    "enabled": true,
    "sourceUrl": "https://github.com/maikelthedev/todosync"
  }
}

Restart Noctalia (from fish shell):

kill (pgrep -f quickshell | head -1)
sleep 3
WAYLAND_DISPLAY=wayland-1 nohup noctalia-shell &>/dev/null &

Open Todo Settings → set http://your-server:3456 → Save. The plugin connects immediately — no second restart needed.

3. Done

Todos sync automatically across all machines connected to the same server.


Project Structure

todosync/
├── src/                  TypeScript REST API server
│   ├── app.ts            Express app factory
│   ├── db.ts             Schema & database helpers
│   ├── server.ts         Entry point
│   └── types.ts          Shared types
├── todo/                 Noctalia Shell plugin
│   ├── manifest.json     Plugin definition
│   ├── Main.qml          Core logic, sync engine, IPC handlers
│   ├── Panel.qml         Overlay panel with status indicator
│   ├── BarWidget.qml     Compact bar widget
│   ├── DesktopWidget.qml Full desktop widget
│   ├── Settings.qml      Settings UI (server URL, priorities, pages)
│   ├── logic.mjs         Pure JS business logic (also tested by Vitest)
│   └── i18n/             Translations
├── tests/                Test suite (Vitest + supertest)
│   ├── todos.test.ts     REST API: todos
│   ├── pages.test.ts     REST API: pages
│   ├── export.test.ts    REST API: export & health
│   ├── logic.test.ts     Pure business logic (34 tests)
│   └── setup.ts          Shared MariaDB pool for tests
├── registry.json         Plugin registry for Noctalia auto-discovery
├── Dockerfile            Container build
└── package.json

How Sync Works

Architecture

The sync uses a central server pattern. All plugin instances connect to the same REST API, which stores data in MariaDB.

Plugin → Server (Push)

  1. User adds, edits, or deletes a todo
  2. Main.qml calls saveTodos() or savePages()
  3. A 500ms debounce timer batches rapid changes
  4. After 500ms of inactivity, POST /api/todos/batch and POST /api/pages/batch are called
  5. The batch endpoints replace ALL data atomically inside a transaction

Server → Plugin (Pull)

  1. A 5-second poll timer (GET /api/export) checks for remote changes
  2. If the server state differs from local state, local state is replaced
  3. State comparison uses JSON.stringify on both sides — quick and avoids flicker

Offline Handling

  • When the server is unreachable, the plugin uses its last cached settings.json on disk
  • A 30-second retry timer keeps trying to reach the server
  • A status bar at the bottom of the panel shows connection state:
    • Hidden — connected, healthy
    • Blue "Syncing…" — push or pull in progress
    • Red "Server offline — retrying…" — server unreachable

On Startup

  1. Component.onCompleted reads serverUrl from saved settings
  2. If set, calls GET /api/export to load full state from server
  3. On success: display server data, start polling
  4. On failure: fall back to local cache, start retry timer

Settings Save = Instant Connect

When the user sets a server URL in settings and saves, Settings.qml calls mainInstance.connectToServer() directly — no shell restart needed.


REST API

Todos

Method Path Description
GET /api/todos List all todos
POST /api/todos Create a todo
PUT /api/todos/:id Update a todo (text, completed, priority, details, pageId)
DELETE /api/todos/:id Delete a todo
POST /api/todos/batch Replace all todos atomically
POST /api/todos/clear-completed Remove completed todos
DELETE /api/todos Remove all todos

Pages

Method Path Description
GET /api/pages List all pages
POST /api/pages Create a page
PUT /api/pages/:id Rename a page
DELETE /api/pages/:id Delete a page (moves todos to default)
POST /api/pages/batch Replace all pages atomically

Other

Method Path Description
GET /api/export Full export (todos + pages)
GET /api/health Database health check

Todo fields

{
  "id": 1783137530132121,
  "text": "buy milk",
  "completed": false,
  "createdAt": "2026-07-04T03:58:50.132Z",
  "lastModified": "2026-07-04T03:58:50.132Z",
  "pageId": 0,
  "priority": "medium",
  "details": ""
}

Page fields

{
  "id": 0,
  "name": "General"
}

Page ID 0 is the default "General" page — it always exists and cannot be deleted.


Plugin Features

  • Bar widget — shows active todo count in the system bar; click to open panel
  • Panel — full todo management with pages, priorities, drag-to-reorder, search
  • Desktop widget — floating desktop todo list with expand/collapse and per-page views
  • Control center widget — quick-access button
  • Pages — group todos into custom pages (Work, Personal, Shopping, …)
  • Priority system — high / medium / low, each with configurable colours
  • Export — markdown or JSON to file
  • Sync status bar — shows connection state at the bottom of the panel
  • Auto-reconnection — retries every 30s if server goes offline
  • Focus overlay — top-right HUD showing your currently active tasks with gentle pulse animation. Designed for people with ADHD who juggle multiple things at once — mark any task as "in focus" and it stays visible over everything, even fullscreen apps. Customise or toggle via IPC.

IPC Commands

Control todos programmatically from scripts or the command line via Quickshell IPC:

# Read operations
noctalia-shell ipc call plugin:todo getTodos         # all todos as JSON
noctalia-shell ipc call plugin:todo getTodo <id>     # single todo as JSON
noctalia-shell ipc call plugin:todo getPages         # all pages as JSON
noctalia-shell ipc call plugin:todo getCount         # {total, active, completed}

# Create
noctalia-shell ipc call plugin:todo addTodoDefault "Buy groceries"
noctalia-shell ipc call plugin:todo addTodo "Write report" "high" <pageId>
noctalia-shell ipc call plugin:todo addPage "Work"

# Update
noctalia-shell ipc call plugin:todo setTodoText <id> "New text"
noctalia-shell ipc call plugin:todo setTodoPriority <id> "high"
noctalia-shell ipc call plugin:todo setTodoCompleted <id> true
noctalia-shell ipc call plugin:todo toggleTodo <id>
noctalia-shell ipc call plugin:todo renamePage <pageId> "New Name"

# Delete
noctalia-shell ipc call plugin:todo removeTodo <id>
noctalia-shell ipc call plugin:todo removePage <pageId>
noctalia-shell ipc call plugin:todo clearCompleted
noctalia-shell ipc call plugin:todo clearAll

# Panel
noctalia-shell ipc call plugin:todo togglePanel

Note: Todo and page IDs are 13+ digit timestamp numbers. They fit in QML var/double but not in QML int (32-bit). See todo/logic.mjs for full ID constraint documentation.

Configuration

Accessible via Settings → Todo:

  • Show Completed — toggle visibility of completed items
  • Show Background — desktop widget background opacity toggle
  • Custom Priority Colors — enable/disable custom colours per priority level
  • Priority Colors — individual colours for high, medium, and low
  • Export Format — markdown or JSON
  • Export Path — directory for exported files

Testing

70 tests total — all with real database access:

npm test                    # run all tests (Vitest)
npm run test:watch          # watch mode
npx vitest run tests/logic  # logic tests only (34 tests, ~400ms)

Test structure

File Tests What it covers
tests/logic.test.ts 34 Pure business logic (no DB, no API)
tests/todos.test.ts 20 REST API for todos
tests/pages.test.ts 13 REST API for pages
tests/export.test.ts 3 Export & health endpoints

The business logic tests run in ~400ms and don't need a database — they test the pure JavaScript functions in todo/logic.mjs.


Deployment

npm run build
podman build -t todosync .
podman run -d \
  --name todosync \
  --restart unless-stopped \
  -p 3456:3456 \
  -e DATABASE_URL="mariadb://user:pass@host:3306/todosync" \
  todosync

Option 2: PM2

npm run build
pm2 start dist/server.js --name todosync
pm2 save
pm2 startup

Option 3: NixOS systemd service

Add a systemd.services.todosync entry to your NixOS config with EnvironmentFile pointing to a .env with DATABASE_URL.


Environment Variables

Variable Required Description
DATABASE_URL Production database connection URL (set in .env)
TODO_SYNC_TEST_DATABASE_URL (for tests) Test database connection URL (set in .env)
TODO_SYNC_PORT (default: 3456) Server port
TODO_SYNC_HOST (default: 0.0.0.0) Server bind address

Security: Copy env.example to .env and fill in your real credentials. Never commit .env to version control.


Development

# Install dependencies
npm install

# Run server with hot reload
npm run dev

# Run tests
npm test

# Build TypeScript
npm run build

The plugin can be developed with hot reload — enable debug mode by clicking the Noctalia logo 8 times in Settings > About, then enable dev mode for the plugin. QML changes apply on save without restarting the shell.


Repositories