Skip to content
Suraj Yadav edited this page Dec 24, 2025 · 4 revisions

Time Management for Ubuntu Touch - Developer Wiki

Table of Contents

  1. Project Overview
  2. Architecture
  3. Project Structure
  4. Core Components
  5. Data Flow
  6. Database Schema
  7. Synchronization System
  8. Background Daemon
  9. QML-Python Bridge
  10. Notification System
  11. Building and Running
  12. Debugging Guide
  13. Files to Remove (Development/Testing Only)
  14. Common Issues & Solutions

Project Overview

Time Management (UBTMS) is a time tracking and project management application for Ubuntu Touch devices. It integrates with Odoo ERP for syncing projects, tasks, timesheets, and activities.

Key Features

  • Multi-account Odoo synchronization
  • Offline-first architecture with local SQLite database
  • Background sync daemon with push notifications
  • Timesheet tracking with timer functionality
  • Eisenhower matrix (quadrant-based) task prioritization
  • Project and task management
  • Activity scheduling and tracking
  • Draft auto-save system for crash recovery

Technology Stack

Layer Technology
UI Framework QML (Qt Quick) with Lomiri Components
Business Logic JavaScript (QML models) + Python (backend)
Database SQLite (via Qt LocalStorage)
Odoo Integration Python XML-RPC
Notifications C++ QML Plugin + DBus
Build System Clickable + CMake

Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                           USER INTERFACE                             │
│                    QML Pages (Dashboard, Tasks, etc.)                │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                         MODELS LAYER                                 │
│                    JavaScript Modules (models/*.js)                  │
│     project.js │ task.js │ timesheet.js │ activity.js │ accounts.js │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                    ┌───────────────┴───────────────┐
                    ▼                               ▼
┌───────────────────────────────┐   ┌───────────────────────────────┐
│      LOCAL DATABASE           │   │     BACKEND BRIDGE            │
│   SQLite (LocalStorage)       │   │  (pyotherside → Python)       │
│   dbinit.js │ database.js     │   │     BackendBridge.qml         │
└───────────────────────────────┘   └───────────────────────────────┘
                                                    │
                                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                         PYTHON BACKEND                               │
│              backend.py │ sync_from_odoo.py │ sync_to_odoo.py        │
│              odoo_client.py │ common.py │ config.py                  │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                    ┌───────────────┴───────────────┐
                    ▼                               ▼
┌───────────────────────────────┐   ┌───────────────────────────────┐
│       ODOO SERVER             │   │    BACKGROUND DAEMON          │
│      (XML-RPC API)            │   │    daemon.py (DBus/GLib)      │
└───────────────────────────────┘   └───────────────────────────────┘
                                                    │
                                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    NOTIFICATION SYSTEM                               │
│         C++ NotificationHelper │ Lomiri Postal │ freedesktop        │
└─────────────────────────────────────────────────────────────────────┘

Project Structure

timemanagement/
├── assets/                    # App icons and images
├── models/                    # JavaScript business logic modules
│   ├── Main.js               # Dashboard statistics and utilities
│   ├── database.js           # Database helper functions
│   ├── dbinit.js             # Database schema initialization
│   ├── project.js            # Project CRUD operations
│   ├── task.js               # Task CRUD with multi-assignee support
│   ├── timesheet.js          # Timesheet management
│   ├── activity.js           # Activity/scheduling operations
│   ├── accounts.js           # Account management
│   ├── timer_service.js      # Global timer singleton
│   ├── notifications.js      # In-app notification handling
│   ├── draft_manager.js      # Form auto-save/recovery
│   ├── utils.js              # Common utilities
│   ├── constants.js          # App-wide constants
│   └── global.js             # Global state management
│
├── qml/                       # QML UI files
│   ├── TSApp.qml             # Main application entry point
│   ├── Menu.qml              # Navigation menu
│   ├── Dashboard.qml         # Main dashboard
│   ├── Projects.qml          # Projects list
│   ├── Project_Page.qml      # Project detail/edit
│   ├── Tasks.qml             # Tasks list
│   ├── Task_Page.qml         # Task detail/edit
│   ├── Timesheet.qml         # Timesheets list
│   ├── Timesheet_Page.qml    # Timesheet detail/edit
│   ├── Activities.qml        # Activities list
│   ├── Activity_Page.qml     # Activity detail/edit
│   ├── Account_Page.qml      # Account configuration
│   ├── Settings_Page.qml     # App settings
│   ├── Charts*.qml           # Various chart views
│   └── components/           # Reusable QML components
│       ├── BackendBridge.qml # Python integration bridge
│       ├── GlobalTimerWidget.qml # Floating timer widget
│       ├── NotificationBell.qml  # Notification UI
│       ├── AccountSelector.qml   # Account switcher
│       ├── FormDraftHandler.qml  # Draft auto-save handler
│       ├── TaskList.qml          # Task list component
│       ├── ProjectList.qml       # Project list component
│       └── ... (50+ components)
│
├── src/                       # Python backend
│   ├── backend.py            # Main backend API for QML
│   ├── odoo_client.py        # Odoo XML-RPC client
│   ├── sync_from_odoo.py     # Sync: Odoo → Local DB
│   ├── sync_to_odoo.py       # Sync: Local DB → Odoo
│   ├── daemon.py             # Background sync daemon
│   ├── daemon_bootstrap.py   # Daemon systemd setup
│   ├── bus.py                # QML event bus (pyotherside)
│   ├── config.py             # Configuration management
│   ├── common.py             # Shared utilities
│   ├── logger.py             # Logging setup
│   └── field_config.json     # Odoo ↔ SQLite field mapping
│
├── qml-notify-module/         # C++ notification plugin
│   ├── NotificationHelper.h
│   ├── NotificationHelper.cpp
│   ├── plugin.cpp
│   └── CMakeLists.txt
│
├── po/                        # Translations
├── manifest.json.in          # Click package manifest
├── clickable.yaml            # Build configuration
├── CMakeLists.txt            # CMake build file
└── start-daemon.sh           # Daemon startup script

Core Components

1. QML UI Layer (qml/)

The user interface is built with QML using Lomiri Components (Ubuntu Touch's UI toolkit).

Key Files:

  • TSApp.qml: Main application entry point. Contains:

    • AdaptivePageLayout for responsive multi-column layout
    • Deep link handling via UriHandler
    • Global timer widget integration
    • Account switching logic
  • Menu.qml: Navigation sidebar with links to all main sections

  • Dashboard.qml: Home screen with:

    • Quadrant summary (Eisenhower matrix)
    • Project pie charts
    • Notification bell
    • Quick actions

2. JavaScript Models Layer (models/)

Business logic is implemented in JavaScript modules that QML imports.

Key Patterns:

// Import pattern
.import QtQuick.LocalStorage 2.7 as Sql
.import "database.js" as DBCommon

// Database access pattern
function getProjectDetails(project_id) {
    var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE);
    var result = {};
    db.transaction(function(tx) {
        var rs = tx.executeSql('SELECT * FROM project_project_app WHERE id = ?', [project_id]);
        if (rs.rows.length > 0) {
            result = DBCommon.rowToObject(rs.rows.item(0));
        }
    });
    return result;
}

Database Schema Initialization (dbinit.js):

  • Called on app startup
  • Creates all required tables
  • Handles schema migrations (adds missing columns)
  • Creates default "Local Account" for offline use

Draft Manager (draft_manager.js):

  • Auto-saves form state every few seconds
  • Detects unsaved changes
  • Provides crash recovery for in-progress edits

3. Python Backend (src/)

The Python backend handles Odoo integration via pyotherside.

Key Modules:

Module Purpose
backend.py Main API exposed to QML; sync, login, file operations
odoo_client.py XML-RPC client for Odoo with timeout handling
sync_from_odoo.py Downloads data from Odoo to local SQLite
sync_to_odoo.py Uploads local changes to Odoo
common.py Thread-safe SQL execution, utilities
config.py Account retrieval from local DB
bus.py Event system for Python→QML communication
logger.py Logging with file and memory handlers

Field Mapping (field_config.json): Maps Odoo field names to local SQLite column names:

{
    "project.project": {
        "name": "name",
        "date_start": "planned_start_date",
        "allocated_hours": "allocated_hours",
        "id": "odoo_record_id"
    }
}

4. Background Daemon (src/daemon.py)

A persistent background service that:

  • Runs even when the app is closed
  • Syncs with Odoo periodically (every 60 seconds)
  • Sends push notifications for new tasks/activities
  • Survives device sleep using wakelocks

Key Features:

  • DBus integration for system notifications
  • Wakelock acquisition via com.lomiri.Repowerd
  • OOM killer protection (lowered oom_score_adj)
  • Sleep/wake handling via logind signals
  • PID file and heartbeat for health monitoring

5. Notification Plugin (qml-notify-module/)

A C++ QML plugin that provides:

  • showNotificationMessage(title, message) - Display system notification
  • updateCount(count) - Update app badge count
  • startDaemon() - Launch background daemon
  • isDaemonHealthy() - Check daemon status

Data Flow

1. User Creates a Task (Offline)

┌────────────────┐     ┌─────────────────┐     ┌──────────────────┐
│   Task_Page.qml │────▶│   task.js       │────▶│  SQLite Database │
│   (User Input)  │     │ saveOrUpdateTask│     │  status='created'│
└────────────────┘     └─────────────────┘     └──────────────────┘
  1. User fills form in Task_Page.qml
  2. Form data passed to task.js::saveOrUpdateTask()
  3. Record inserted with status = 'created' (pending sync)

2. Sync to Odoo

┌─────────────────┐     ┌──────────────────┐     ┌───────────────┐
│  BackendBridge  │────▶│  backend.py      │────▶│  Odoo Server  │
│  call("sync")   │     │  sync_to_odoo.py │     │  (XML-RPC)    │
└─────────────────┘     └──────────────────┘     └───────────────┘
        │                        │
        ▼                        ▼
┌─────────────────┐     ┌──────────────────┐
│  QML receives   │◀────│  bus.send()      │
│  progress events│     │  sync_progress   │
└─────────────────┘     └──────────────────┘
  1. QML calls backend_bridge.call("backend.sync_background", [db_path, account_id])
  2. Python reads records with status IN ('created', 'updated')
  3. For each record:
    • If odoo_record_id IS NULL: Create in Odoo
    • Else: Update in Odoo
  4. On success: Clear local status field
  5. Progress events sent to QML via bus.send()

3. Sync from Odoo

┌───────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  Odoo Server  │────▶│ sync_from_odoo.py│────▶│ SQLite Database │
│  search_read  │     │  insert_record() │     │  INSERT/REPLACE │
└───────────────┘     └──────────────────┘     └─────────────────┘
  1. Fetch records modified since last sync (write_date > last_sync)
  2. Map Odoo fields to SQLite columns using field_config.json
  3. Insert or replace records in local database
  4. Preserve local status if record has pending changes

Database Schema

All tables are created in models/dbinit.js. Key tables:

Users Table

CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    link TEXT NOT NULL,           -- Odoo server URL
    database TEXT NOT NULL,       -- Odoo database name
    username TEXT NOT NULL,
    api_key TEXT,
    is_default INTEGER DEFAULT 0
);

Projects Table

CREATE TABLE project_project_app (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    account_id INTEGER,           -- Links to users.id
    parent_id INTEGER,            -- For subprojects
    planned_start_date DATE,
    planned_end_date DATE,
    allocated_hours FLOAT,
    favorites INTEGER,
    description TEXT,
    status TEXT DEFAULT "",       -- 'created', 'updated', 'deleted'
    odoo_record_id INTEGER,       -- Odoo's ID
    UNIQUE (odoo_record_id, account_id)
);

Tasks Table

CREATE TABLE project_task_app (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    account_id INTEGER,
    project_id INTEGER,           -- odoo_record_id of project
    parent_id INTEGER,            -- For subtasks
    user_id TEXT,                 -- Comma-separated assignee IDs
    priority TEXT,
    deadline DATE,
    state INTEGER,                -- Stage ID
    status TEXT DEFAULT "",
    odoo_record_id INTEGER,
    UNIQUE (odoo_record_id, account_id)
);

Timesheets Table

CREATE TABLE account_analytic_line_app (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT,
    account_id INTEGER,
    project_id INTEGER,
    task_id INTEGER,
    user_id INTEGER,
    unit_amount FLOAT,            -- Hours (decimal)
    record_date DATE,
    quadrant_id INTEGER,          -- Eisenhower quadrant (1-4)
    timer_type TEXT,              -- 'manual' or 'timer'
    status TEXT DEFAULT "",
    odoo_record_id INTEGER
);

Activities Table

CREATE TABLE mail_activity_app (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    summary TEXT,
    account_id INTEGER,
    activity_type_id INTEGER,
    due_date DATE,
    user_id INTEGER,
    notes TEXT,
    link_id INTEGER,              -- res_id (linked record)
    resModel TEXT,                -- 'project.task', 'project.project', etc.
    state TEXT,                   -- 'today', 'planned', 'overdue', 'done'
    status TEXT DEFAULT "",
    odoo_record_id INTEGER
);

Sync Report Table

CREATE TABLE sync_report (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    status TEXT,                  -- 'In Progress', 'Successful', 'Failed'
    account_id INTEGER,
    timestamp TEXT,
    message TEXT
);

Form Drafts Table (Auto-save)

CREATE TABLE form_drafts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    draft_type TEXT,              -- 'task', 'timesheet', 'project', 'activity'
    record_id INTEGER,
    account_id INTEGER,
    form_data TEXT,               -- JSON
    original_data TEXT,           -- JSON
    page_identifier TEXT,
    is_new_record INTEGER,
    created_at TEXT,
    updated_at TEXT
);

Synchronization System

Sync Flow

                    ┌─────────────────┐
                    │  Trigger Sync   │
                    │  (Manual/Auto)  │
                    └────────┬────────┘
                             │
            ┌────────────────┴────────────────┐
            ▼                                 ▼
┌──────────────────────┐          ┌──────────────────────┐
│   sync_from_odoo.py  │          │   sync_to_odoo.py    │
│ ─────────────────────│          │ ─────────────────────│
│ 1. Fetch from Odoo   │          │ 1. Get local changes │
│ 2. Map fields        │          │ 2. Map to Odoo fields│
│ 3. Insert/Replace    │          │ 3. Create/Update     │
│ 4. Preserve pending  │          │ 4. Clear status      │
└──────────────────────┘          └──────────────────────┘

Record Status Values

Status Meaning
NULL or '' Synced with Odoo
'created' New record, needs upload to Odoo
'updated' Modified locally, needs sync to Odoo
'deleted' Marked for deletion

Conflict Resolution

  1. Local changes take priority during sync from Odoo if status IN ('created', 'updated')
  2. Favorites and other user preferences are preserved
  3. Last-modified timestamp comparison for updates

Background Daemon

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                     daemon.py (NotificationDaemon)               │
├─────────────────────────────────────────────────────────────────┤
│  ┌─────────────┐   ┌─────────────┐   ┌──────────────────────┐  │
│  │   GLib      │   │    DBus     │   │    Wakelock          │  │
│  │   MainLoop  │   │  Session    │   │  com.lomiri.Repowerd │  │
│  └─────────────┘   └─────────────┘   └──────────────────────┘  │
├─────────────────────────────────────────────────────────────────┤
│  Main Loop:                                                      │
│  1. Every 60 seconds: sync_all_accounts()                       │
│  2. Check for new tasks/activities                              │
│  3. Send push notifications                                      │
│  4. Update heartbeat file                                        │
└─────────────────────────────────────────────────────────────────┘

Daemon Lifecycle

  1. Startup:

    • Write PID to ~/.daemon.pid
    • Initialize DBus connection
    • Request wakelock from Repowerd
    • Lower OOM killer priority
    • Register for sleep/wake signals
  2. Running:

    • GLib main loop with 60-second timer
    • For each account: sync and check for updates
    • Update heartbeat file (~/.daemon_heartbeat)
    • Send notifications for new items
  3. Shutdown:

    • Release wakelock
    • Remove PID file
    • Clean exit

Health Monitoring

// NotificationHelper.cpp
bool NotificationHelper::isDaemonHealthy() {
    // Check heartbeat file age (< 5 minutes)
    // Check PID file and if process is running
}

QML-Python Bridge

BackendBridge Component

// Usage in QML
BackendBridge {
    id: backend_bridge
    module: "backend"  // Python module to import
    
    onMessageReceived: function(data) {
        // Handle events from Python
        if (data.event === "sync_progress") {
            progressBar.value = data.payload;
        }
    }
}

// Calling Python functions
backend_bridge.call("backend.sync_background", [db_path, account_id], function(result) {
    console.log("Sync started:", result);
});

Event System (bus.py)

# Python side
from bus import send

def sync_background(db_path, account_id):
    send("sync_progress", 0)
    # ... sync logic ...
    send("sync_progress", 50)
    # ... more sync ...
    send("sync_completed", True)
# bus.py
try:
    import pyotherside
    _HAS_PYOTHERSIDE = True
except ImportError:
    _HAS_PYOTHERSIDE = False

def send(event_name, payload):
    if _HAS_PYOTHERSIDE:
        pyotherside.send({'event': event_name, 'payload': payload})

Notification System

Components

  1. C++ Plugin (qml-notify-module/):

    • Native DBus access
    • Postal API for Ubuntu Touch notifications
    • Badge count updates
  2. Daemon Notifications (daemon.py):

    • Triggered on new tasks/activities
    • Deep link URIs for navigation
  3. In-App Notifications (notifications.js):

    • SQLite-backed notification storage
    • Notification bell UI component

Notification Flow

┌─────────────────┐
│  Daemon detects │
│  new task       │
└────────┬────────┘
         │
         ▼
┌─────────────────┐     ┌─────────────────────┐
│ Add to local DB │────▶│  notification table │
│ (notifications) │     │  read_status = 0    │
└────────┬────────┘     └─────────────────────┘
         │
         ▼
┌─────────────────┐
│ Send system     │
│ notification    │
│ via DBus/Postal │
└─────────────────┘
         │
         ▼
┌─────────────────┐
│ User taps       │
│ notification    │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ Deep link opens │
│ app to record   │
│ ubtms://navigate│
│ ?type=Task&id=X │
└─────────────────┘

Building and Running

Prerequisites

# Install Clickable
pip3 install --user clickable-ut
export PATH=$PATH:~/.local/bin

Desktop Development

# Run on desktop (uses QML emulation)
clickable desktop

# View logs
tail -f ~/daemon.log

Device Deployment

# Build and install on connected device
clickable install

# SSH to device and check logs
adb shell
cat /home/phablet/.local/share/ubtms/daemon.log

Build Configuration

# clickable.yaml
clickable_minimum_required: 8.0.0
builder: cmake
kill: qmlscene
skip_review: true

Debugging Guide

Log Files

Log Location Purpose
Daemon log ~/.local/share/ubtms/daemon.log Background sync, notifications
App console QML console.log() output UI debugging

Enabling Debug Logging

# logger.py
def setup_logger(name="odoo_sync", log_file=None, level=logging.DEBUG):
    # Change to DEBUG for verbose output

Common Debugging Commands

# Check if daemon is running
cat ~/.daemon.pid && ps aux | grep daemon

# Check heartbeat freshness
stat ~/.daemon_heartbeat

# View recent sync reports
sqlite3 ~/.local/share/ubtms/Databases/*.sqlite \
  "SELECT * FROM sync_report ORDER BY timestamp DESC LIMIT 5;"

# Check pending sync items
sqlite3 ~/.local/share/ubtms/Databases/*.sqlite \
  "SELECT id, name, status FROM project_task_app WHERE status != '';"

# Force restart daemon
kill $(cat ~/.daemon.pid); python3 src/daemon.py

QML Debugging

// Add to any QML file for debugging
Component.onCompleted: {
    console.log("Page loaded, accountId:", accountId);
    console.log("Data:", JSON.stringify(modelData, null, 2));
}

Python Debugging

# Add to sync functions
from logger import setup_logger
log = setup_logger()
log.debug(f"[DEBUG] Processing record: {record}")

Database Inspection

# Open database with sqlite3
sqlite3 ~/.clickable/home/.local/share/ubtms/Databases/*.sqlite

# List tables
.tables

# Check schema
.schema project_task_app

# Query data
SELECT * FROM project_task_app LIMIT 10;

Files to Remove

The following files are development/testing utilities that are not needed for production:

Development Tools (Can Remove)

File Purpose Safe to Remove
src/autotester.py Automated sync testing ✅ Yes
src/add_demo_connection.py Adds demo Odoo account ✅ Yes
src/tool_field_sync.py Fetches Odoo field metadata ✅ Yes
src/tool_get_remote_record.py Debug tool for Odoo records ✅ Yes
src/cli.py CLI interface (unused) ✅ Yes
src/test_data/ Test database files ✅ Yes
src/odoo_config/ Cached Odoo field metadata ✅ Yes
src/all_odoo_fields.json Duplicate of above ✅ Yes
po/nl.po~ Backup translation file ✅ Yes

Command to Remove

cd /home/suraj/timemanagement

# Remove development/testing files
rm -f src/autotester.py
rm -f src/add_demo_connection.py
rm -f src/tool_field_sync.py
rm -f src/tool_get_remote_record.py
rm -f src/cli.py
rm -rf src/test_data/
rm -rf src/odoo_config/
rm -f src/all_odoo_fields.json
rm -f po/nl.po~

Common Issues & Solutions

Issue: Sync Fails with Authentication Error

Symptom: "Authentication failed for server" message

Solution:

  1. Verify account credentials in Settings
  2. Check if API key is correct
  3. Ensure Odoo server is accessible
  4. Check SSL certificate validity

Issue: Daemon Not Starting

Symptom: No notifications, no background sync

Solution:

# Check dependencies
sudo apt install python3-dbus python3-gi gir1.2-glib-2.0

# Check for marker file
cat ~/.ubtms_needs_setup

# Manually start daemon
python3 /path/to/src/daemon.py

Issue: Database Locked Errors

Symptom: "database is locked" in logs

Solution:

  • The safe_sql_execute() function handles retries automatically
  • If persistent, check for zombie processes: pkill -f "sqlite.*ubtms"

Issue: Missing Notifications

Symptom: Sync works but no push notifications

Solution:

  1. Check DBus is available: echo $DBUS_SESSION_BUS_ADDRESS
  2. Verify Postal service: dbus-send --print-reply --dest=com.lomiri.Postal /com/lomiri/Postal org.freedesktop.DBus.Introspectable.Introspect
  3. Check daemon logs for notification errors

Issue: Records Not Syncing

Symptom: Local changes not appearing in Odoo

Solution:

  1. Check record has status = 'created' or 'updated'
  2. Verify account is not "Local Account" (id=0)
  3. Check sync_report table for errors
  4. Manually trigger sync from Settings

Version History

Version Changes
1.2.2 Current version - Multi-assignee support, draft auto-save

License

MIT License - Copyright (c) 2025 CIT-Services


Last updated: December 2025