Skip to content

Downgrade Pro Data File to Base

This document describes how to convert a Pro data.json file back to the base verrsion so you can continue using the free version of TaskTrove. This procedure targets data files from version v0.12.0 and newer.

What changes during downgrade

The conversion removes Pro-only fields so the result conforms to the base packages/types data file schema.

Steps

  1. Back up your Pro data file.
  2. Copy and save the following script as convert.py:
python
#!/usr/bin/env python3
import argparse
import json
import sys
from typing import Any, Dict, List


def pick_user(user_field: Any) -> Dict[str, Any]:
    if isinstance(user_field, list):
        admins = [u for u in user_field if isinstance(u, dict) and u.get("role") == "admin"]
        if admins:
            return admins[0]
        return user_field[0] if user_field else {}
    if isinstance(user_field, dict):
        return user_field
    return {}


def clean_user(user: Dict[str, Any]) -> Dict[str, Any]:
    user = dict(user)
    user.pop("role", None)
    user.pop("preferences", None)
    return user


def clean_task(task: Dict[str, Any]) -> Dict[str, Any]:
    task = dict(task)
    task.pop("ownerId", None)
    task.pop("assignees", None)
    task.pop("reward", None)
    comments = task.get("comments")
    if isinstance(comments, list):
        cleaned_comments: List[Dict[str, Any]] = []
        for comment in comments:
            if isinstance(comment, dict):
                comment = dict(comment)
                comment.pop("reactions", None)
                cleaned_comments.append(comment)
            else:
                cleaned_comments.append(comment)
        task["comments"] = cleaned_comments
    return task


def clean_project(project: Dict[str, Any]) -> Dict[str, Any]:
    project = dict(project)
    project.pop("members", None)
    return project


def clean_settings(settings: Dict[str, Any]) -> Dict[str, Any]:
    settings = dict(settings)
    general = settings.get("general")
    if isinstance(general, dict):
        general = dict(general)
        general.pop("newTaskOwnership", None)
        settings["general"] = general
    data = settings.get("data")
    if isinstance(data, dict):
        data = dict(data)
        data.pop("calendarSync", None)
        data.pop("calendarSyncSchedule", None)
        settings["data"] = data
    settings.pop("productivity", None)
    return settings


def convert(data: Dict[str, Any]) -> Dict[str, Any]:
    tasks = data.get("tasks", [])
    projects = data.get("projects", [])
    labels = data.get("labels")
    project_groups = data.get("projectGroups")
    label_groups = data.get("labelGroups")
    settings = data.get("settings", {})
    user = pick_user(data.get("user"))

    return {
        "tasks": [clean_task(t) if isinstance(t, dict) else t for t in tasks],
        "projects": [clean_project(p) if isinstance(p, dict) else p for p in projects],
        "labels": labels,
        "projectGroups": project_groups,
        "labelGroups": label_groups,
        "settings": clean_settings(settings) if isinstance(settings, dict) else settings,
        "user": clean_user(user) if isinstance(user, dict) else user,
        "version": data.get("version"),
        "edition": "base",
    }


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=(
            "Convert a Pro TaskTrove data.json file to the base schema. "
            "Outputs to stdout unless --output is provided."
        )
    )
    parser.add_argument("input", help="Path to pro data.json")
    parser.add_argument("-o", "--output", help="Write output to file instead of stdout")
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    try:
        with open(args.input, "r", encoding="utf-8") as f:
            data = json.load(f)
    except FileNotFoundError:
        print(f"Input file not found: {args.input}", file=sys.stderr)
        return 1
    except json.JSONDecodeError as exc:
        print(f"Invalid JSON in {args.input}: {exc}", file=sys.stderr)
        return 1

    if not isinstance(data, dict):
        print("Input JSON must be an object at the top level.", file=sys.stderr)
        return 1

    converted = convert(data)

    output = json.dumps(converted, indent=2, ensure_ascii=True)
    if args.output:
        with open(args.output, "w", encoding="utf-8") as f:
            f.write(output)
            f.write("\n")
    else:
        print(output)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
  1. Run the converter (note, this will override your data.json file):
bash
convert.py path/to/data.json -o path/to/data.json
  1. Edit your docker compose file or other configuration to use the base image.
  2. Restart the docker container