feat(sync): add opt-in remote deletion for gcal and gtasks

Problem: push/sync permanently deleted remote Google Calendar events and
Google Tasks entries whenever a local task was marked deleted, done, or
de-due'd. There was no opt-out, so a misfire could silently cause
irreversible data loss on the remote side.

Solution: add a `remote_delete` boolean to the config (default `false`).
A unified flag at `sync.remote_delete` sets the base; per-backend
overrides at `sync.gcal.remote_delete` / `sync.gtasks.remote_delete`
take precedence when non-nil. When disabled, `_extra` remote IDs are
cleared silently (unlinking) so stale IDs don't accumulate.
This commit is contained in:
Barrett Ruth 2026-03-06 13:06:06 -05:00
parent 55e83644b3
commit cc9b121521
3 changed files with 78 additions and 26 deletions

View file

@ -129,6 +129,31 @@ local function delete_event(access_token, calendar_id, event_id)
return err
end
---@return boolean
local function allow_remote_delete()
local cfg = config.get()
local sync = cfg.sync or {}
local per = (sync.gcal or {}) --[[@as pending.GcalConfig]]
if per.remote_delete ~= nil then
return per.remote_delete == true
end
return sync.remote_delete == true
end
---@param task pending.Task
---@param extra table
---@param now_ts string
local function unlink_remote(task, extra, now_ts)
extra['_gcal_event_id'] = nil
extra['_gcal_calendar_id'] = nil
if next(extra) == nil then
task._extra = nil
else
task._extra = extra
end
task.modified = now_ts
end
---@param callback fun(access_token: string): nil
local function with_token(callback)
oauth.async(function()
@ -159,6 +184,7 @@ function M.push()
end
local s = require('pending').store()
local now_ts = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
local created, updated, deleted = 0, 0, 0
for _, task in ipairs(s:tasks()) do
@ -175,19 +201,20 @@ function M.push()
)
if should_delete then
local del_err =
delete_event(access_token, cal_id --[[@as string]], event_id --[[@as string]])
if del_err then
log.warn('gcal delete failed: ' .. del_err)
else
extra['_gcal_event_id'] = nil
extra['_gcal_calendar_id'] = nil
if next(extra) == nil then
task._extra = nil
if allow_remote_delete() then
local del_err =
delete_event(access_token, cal_id --[[@as string]], event_id --[[@as string]])
if del_err then
log.warn('gcal delete failed: ' .. del_err)
else
task._extra = extra
unlink_remote(task, extra, now_ts)
deleted = deleted + 1
end
task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
else
log.debug(
'gcal: remote delete skipped (remote_delete disabled) — unlinked task #' .. task.id
)
unlink_remote(task, extra, now_ts)
deleted = deleted + 1
end
elseif task.status == 'pending' and task.due then
@ -213,7 +240,7 @@ function M.push()
end
task._extra['_gcal_event_id'] = new_id
task._extra['_gcal_calendar_id'] = lid
task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
task.modified = now_ts
created = created + 1
end
end