Files
joakim f5f7bc3ad7 feat: Complete key:value format implementation and fix tag sync
Implement complete key:value format parsing for change log entries and fix
critical tag synchronization issue from server to client.

Key Changes:

1. Shared Key:Value Parser (NEW: internal/engine/parser.go)
   - Created ParseKeyValueFormat() for both edit and sync operations
   - Supports flexible whitespace: 'key:value' and 'key: value'
   - Handles comment skipping for edit files
   - Consolidates parsing logic (DRY principle)

2. Database Triggers - Tags Support (internal/engine/database.go)
   - Added tags to track_task_create trigger
   - Added tags to track_task_update trigger
   - Tags sorted alphabetically via SQL ORDER BY
   - Format: 'tags: alpha,bravo,charlie'

3. Task Creation - Tag Update Fix (internal/engine/task.go)
   - CreateTaskWithModifier() now triggers update after adding tags
   - Ensures tags appear in change log (UPDATE entry)
   - Fixes missing tags in initial CREATE entries

4. Edit Command - Use Shared Parser (cmd/edit.go)
   - Replaced custom parseEditedFile() with shared ParseKeyValueFormat()
   - Added tag sorting in parseTags()
   - Removed ~30 lines, improved maintainability

5. Sync Client - Complete Implementation (internal/sync/client.go)
   - NEW: applyChangeDataToTask() - parses all fields from change log
   - NEW: Helper functions for status, priority, tag parsing
   - FIXED: parseChanges() - sort by timestamp+ID before grouping
   - Added parent/child task ordering (prevents FK violations)
   - Enhanced tag sync in merge loop with task reload
   - Specific validation error messages per field

Critical Bug Fix:
- When CREATE and UPDATE have same timestamp, old code kept CREATE (no tags)
- New code sorts by ID as tiebreaker, ensuring UPDATE (with tags) is used
- Verified: Server->client tag sync now works correctly

Validation:
- Description must not be empty (both edit and sync)
- Recurrence validated (not negative, max 100 years)
- All timestamps parsed correctly (Unix epoch)
- Tags sorted alphabetically in all contexts

Testing:
- Fresh pull from server:  All tags present
- API-created tasks:  Tags sync correctly
- Local->server->client round-trip:  No data loss
- Same-second CREATE+UPDATE:  Correct entry processed
- Parent/child tasks:  Correct ordering

Files Changed:
- NEW: internal/engine/parser.go (+44 lines)
- Modified: internal/engine/database.go (+10 lines)
- Modified: internal/engine/task.go (+8 lines)
- Modified: cmd/edit.go (-25 lines net)
- Modified: internal/sync/client.go (+280 lines)
- Modified: srv/README.md (+1 line)

Total: +318 lines added, -25 removed, net +293 lines

This completes Phase 6: Full bidirectional sync with complete tag support.
2026-01-05 18:56:17 +01:00
..

Opal-Task Server

Opal-task server provides a REST API for syncing tasks across multiple devices. This enables household task sharing and access from anywhere.

Features

  • REST API - Chi-based HTTP API for task management
  • API Key Authentication - Secure authentication using bcrypt-hashed keys
  • Bidirectional Sync - Push and pull changes with conflict resolution
  • Offline Support - Queue changes when server is unreachable
  • Change Tracking - Automatic change log with configurable retention
  • Single Database - Shared household task database (v1)

Quick Start

1. Build the Binary

cd opal-task
go build -o opal main.go

2. Generate API Key

On your server (with direct database access):

./opal server keygen --name "My Device" --db /var/lib/opal/opal.db

Output:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
API Key Generated Successfully
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Name: My Device
Key:  oak_abc123...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

⚠️  IMPORTANT: Save this key securely!
   It will not be displayed again.

3. Start the Server

./opal server start --addr :8080 --db /var/lib/opal/opal.db

4. Configure Client

On your local machine or phone:

opal sync init --url https://opal.yourdomain.com --key oak_abc123...

Or use interactive mode:

opal sync init
# Enter server URL: https://opal.yourdomain.com
# Enter API key: oak_abc123...

5. Sync Your Tasks

# Full bidirectional sync
opal sync now

# Push local changes only
opal sync up

# Pull server changes only
opal sync down

# Initial merge (for existing local database)
opal sync merge --prefer-local

Server Deployment

1. Install Binary

# Build
go build -o opal main.go

# Copy to system location
sudo cp opal /usr/local/bin/

# Create data directory
sudo mkdir -p /var/lib/opal
sudo chown $USER:$USER /var/lib/opal

2. Generate First API Key

opal server keygen --name "Admin" --db /var/lib/opal/opal.db
# Save the generated key!

3. Create SystemD Service

Create /etc/systemd/system/opal.service:

[Unit]
Description=Opal Task API Server
After=network.target

[Service]
Type=simple
User=your-user
WorkingDirectory=/var/lib/opal
ExecStart=/usr/local/bin/opal server start --addr :8080 --db /var/lib/opal/opal.db
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

4. Enable and Start Service

sudo systemctl daemon-reload
sudo systemctl enable opal
sudo systemctl start opal
sudo systemctl status opal

Option 2: Direct Execution

# Run in background
nohup ./opal server start --addr :8080 --db /var/lib/opal/opal.db > opal.log 2>&1 &

Reverse Proxy Setup

Add to your Caddyfile:

opal.yourdomain.com {
    reverse_proxy localhost:8080
}

Reload Caddy:

sudo systemctl reload caddy

Nginx

server {
    listen 443 ssl http2;
    server_name opal.yourdomain.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

API Endpoints

Authentication

All endpoints (except /health) require authentication via API key:

Authorization: Bearer oak_abc123...

Health Check

GET /health

Returns server status.

Tasks

GET    /tasks                  - List tasks (with filters)
GET    /tasks/{uuid}           - Get specific task
POST   /tasks                  - Create task
PUT    /tasks/{uuid}           - Update task
DELETE /tasks/{uuid}           - Delete task
POST   /tasks/{uuid}/complete  - Mark task complete
POST   /tasks/{uuid}/start     - Start task
POST   /tasks/{uuid}/stop      - Stop task

Query Parameters for GET /tasks:

  • status - pending, completed, deleted
  • project - Project name
  • priority - L, M, H, D
  • tag - Tag name (can specify multiple)

Tags

GET    /tasks/{uuid}/tags      - Get task tags
POST   /tasks/{uuid}/tags      - Add tag
DELETE /tasks/{uuid}/tags/{tag} - Remove tag
GET    /tags                   - List all tags

Projects

GET /projects - List all projects

Sync

POST /sync/changes - Get changes since timestamp
POST /sync/push    - Push local changes

API Keys

GET    /auth/keys       - List API keys
DELETE /auth/keys/{id}  - Revoke API key

Client Configuration

Sync settings are stored in ~/.config/jade/opal.yml:

# Task settings
default_filter: status:pending
default_sort: due,priority
color_output: true
week_start_day: monday
default_due_time: ""

# Sync settings
sync_enabled: true
sync_url: https://opal.yourdomain.com
sync_api_key: oak_abc123...
sync_client_id: 550e8400-e29b-41d4-a716-446655440000
sync_strategy: last-write-wins
sync_queue_offline: true

Sync Strategies

  • last-write-wins (default) - Most recently modified version wins
  • server-wins - Server version always wins
  • client-wins - Client version always wins

Change Log Retention

By default, the change log retains entries for 30 days. This can be configured in the database:

UPDATE sync_config SET value = '60' WHERE key = 'change_log_retention_days';

Or programmatically:

engine.SetChangeLogRetentionDays(60)

Cleanup runs manually or can be scheduled:

# In your cron or systemd timer
./opal server cleanup --db /var/lib/opal/opal.db

Troubleshooting

Server won't start

  • Check if port 8080 is already in use: sudo lsof -i :8080
  • Verify database path exists and is writable
  • Check logs: journalctl -u opal -f

Client can't connect

  • Test server health: curl https://opal.yourdomain.com/health
  • Verify API key is correct
  • Check firewall allows traffic on port 8080
  • Ensure reverse proxy is properly configured

Sync conflicts

  • View conflict log: opal sync log
  • Conflicts are automatically resolved based on strategy
  • By default, last-write-wins is used

Offline changes not syncing

  • Check queue status: opal sync status
  • Verify sync_queue_offline: true in config
  • Clear queue if needed: Delete ~/.config/jade/sync_queue.json

Database Schema

Core Tables

  • tasks - Main task storage
  • tags - Task tags (many-to-many)
  • working_set - Display ID mapping (client-local)

Sync Tables

  • users - User accounts (currently single shared user)
  • api_keys - Authentication keys
  • sync_state - Per-client sync state
  • change_log - Change tracking (key:value format)
  • sync_config - Server configuration

Security Considerations

  • API keys are hashed with bcrypt before storage
  • Never log full API keys
  • Use HTTPS in production (via reverse proxy)
  • Implement rate limiting for production use
  • Backup database regularly
  • Rotate API keys periodically

Future Enhancements

  • OAuth2 with Authentik for web/mobile clients
  • Per-user task separation with sharing
  • Real-time sync via WebSockets
  • Web/mobile frontend (SvelteKit PWA)
  • Access control (share specific tags/projects)
  • Audit logging
  • Database backup automation

Support

For issues or questions:

  • Check logs: journalctl -u opal -f
  • View sync status: opal sync status
  • View conflicts: opal sync log
  • Test connectivity: curl https://opal.yourdomain.com/health

License

Same as opal-task main project.