From 1bea5547a65971a3c497d2da288010483acb64a9 Mon Sep 17 00:00:00 2001 From: Elijah Date: Sun, 19 Jul 2026 19:12:28 -0700 Subject: [PATCH 1/9] chore(release): publish Decky 0.3.0 updater metadata --- releases/latest.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/releases/latest.json b/releases/latest.json index dd38344..0ab61e5 100644 --- a/releases/latest.json +++ b/releases/latest.json @@ -1,11 +1,11 @@ -{ - "version": "0.2.0", - "notes": "First realistic-testing release of Decky with Nextcloud Deck editing, live drag-and-drop, and signed in-app updates.", - "pub_date": "2026-07-19T22:25:48.1776589+00:00", - "platforms": { - "windows-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTSHZBY0NPTW9uRERoeFMyZWZLU2pRWmFudFh1ZWJKZVhJOUFRd2pMSVZGdmFYdUxwQUt5ZFFGcnQ5ek5FNGRZMmluMWIzdUJVZEIvTU15OGw1QXEySmh1ZWFJL2ZNUWdjPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg0NDk5ODgyCWZpbGU6RGVja3lfMC4yLjBfeDY0LXNldHVwLmV4ZQpKK1F2dFg0QWdoZ3dZcHpoTm9jTmIvaDVjeHJuQ1dhWTh3L0NJUEpQcnBmTmRWTk1Va3prSkVBdHo3V2RIMVlvMkxYMDdsZTY5M095Zjg0NjMrTHRBdz09Cg==", - "url": "https://git.elijahkuntz.com/Elijah/Decky/releases/download/v0.2.0/Decky_0.2.0_x64-setup.exe" - } - } -} +{ + "version": "0.3.0", + "notes": "Board and stack creation, editing, reordering, and deletion; polished management controls; Forgejo update workflow.", + "pub_date": "2026-07-20T02:00:14.7906154+00:00", + "platforms": { + "windows-x86_64": { + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTSHZBY0NPTW9uRERoeS84YzV1UnFyMVFkTWFqSEpqUm5rclQ2NDlZb09Ra0ZwZ0treFdVZ3cvOTFGWXY3aHhmUFlwNFNFc3QxQTQ2T0l6UXRPdkFCbW01K2VXaXZHNVFJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg0NTEyODEzCWZpbGU6RGVja3lfMC4zLjBfeDY0LXNldHVwLmV4ZQpBTGlRUjhmdnkyNm1pZDFFTlJhdWNEQzdTZGRSS1crOSt3Mm5XaHpsR3ZtMGNZZmpLYmtYSHJrZFpUSnVxTHRHTEtDaHFlUUxxTkJ3SXRvRnZReWRCUT09Cg==", + "url": "https://git.elijahkuntz.com/Elijah/Decky/releases/download/v0.3.0/Decky_0.3.0_x64-setup.exe" + } + } +} \ No newline at end of file From e9d76cc4a2e0bed119129fede944bfc9fe0570de Mon Sep 17 00:00:00 2001 From: Elijah Date: Sun, 19 Jul 2026 19:18:15 -0700 Subject: [PATCH 2/9] Prompt for Forgejo token during release publishing --- app/UPDATES.md | 10 ++++++++++ scripts/Publish-DeckyRelease.cmd | 24 ++++++++++++++++++++++++ scripts/Publish-ForgejoRelease.ps1 | 8 +++++++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 scripts/Publish-DeckyRelease.cmd diff --git a/app/UPDATES.md b/app/UPDATES.md index 64b5c11..4646a49 100644 --- a/app/UPDATES.md +++ b/app/UPDATES.md @@ -76,6 +76,16 @@ Remove-Item Env:FORGEJO_PAT git pull --ff-only ``` +For an interactive, double-clickable option, run: + +```text +scripts\Publish-DeckyRelease.cmd +``` + +It bypasses PowerShell's script policy only for the publisher process, prompts +for the version and a hidden personal access token, and leaves the window open +so any error can be read. The token is not saved. + The publisher creates the `v0.3.0` Forgejo release/tag at `main`, uploads the installer, signature, and metadata, then commits the tracked `releases/latest.json` through the Forgejo API. The final pull synchronizes that diff --git a/scripts/Publish-DeckyRelease.cmd b/scripts/Publish-DeckyRelease.cmd new file mode 100644 index 0000000..6502488 --- /dev/null +++ b/scripts/Publish-DeckyRelease.cmd @@ -0,0 +1,24 @@ +@echo off +setlocal +title Publish Decky Release +cd /d "%~dp0.." + +echo Decky Forgejo release publisher +echo. +echo This creates the Forgejo release, uploads the signed installer, +echo and updates releases/latest.json for Decky's in-app updater. +echo. + +powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0Publish-ForgejoRelease.ps1" +set "DECKY_RESULT=%ERRORLEVEL%" + +echo. +if not "%DECKY_RESULT%"=="0" ( + echo Publishing failed. The error is shown above. + echo No successful release should be assumed until the script says it was published. +) else ( + echo Publishing completed successfully. +) +echo. +pause +exit /b %DECKY_RESULT% diff --git a/scripts/Publish-ForgejoRelease.ps1 b/scripts/Publish-ForgejoRelease.ps1 index 6c4570e..0d29d04 100644 --- a/scripts/Publish-ForgejoRelease.ps1 +++ b/scripts/Publish-ForgejoRelease.ps1 @@ -12,7 +12,13 @@ param( ) $ErrorActionPreference = 'Stop' -if (-not $Token) { throw 'FORGEJO_TOKEN or FORGEJO_PAT is required. Set it only for this PowerShell process.' } +if (-not $Token) { + Write-Host 'A Forgejo personal access token is required to create the release.' -ForegroundColor Cyan + Write-Host 'The token is used only by this process and is not saved by Decky.' -ForegroundColor DarkGray + $secureToken = Read-Host 'Forgejo personal access token' -AsSecureString + $Token = [Net.NetworkCredential]::new('', $secureToken).Password +} +if (-not $Token) { throw 'No Forgejo personal access token was entered.' } $repositoryRoot = Split-Path -Parent $PSScriptRoot $releaseDirectory = Join-Path $repositoryRoot "artifacts\release\v$Version" From ee2ef039820cfa9c5a8aeac84cef7768091b73dc Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 20 Jul 2026 09:00:24 -0700 Subject: [PATCH 3/9] Add automatic background refresh --- README.md | 1 + app/package-lock.json | 4 +- app/package.json | 2 +- app/src-tauri/Cargo.lock | 2 +- app/src-tauri/Cargo.toml | 2 +- app/src-tauri/tauri.conf.json | 4 +- app/src/App.svelte | 113 ++++++++++++++++++++++++++----- app/src/lib/auto-refresh.test.ts | 37 ++++++++++ app/src/lib/auto-refresh.ts | 23 +++++++ 9 files changed, 163 insertions(+), 25 deletions(-) create mode 100644 app/src/lib/auto-refresh.test.ts create mode 100644 app/src/lib/auto-refresh.ts diff --git a/README.md b/README.md index afab0fb..ac97669 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Implemented functionality includes: - active-board filtering and saved board selection; - board creation, renaming, recoloring, and deletion; - stack creation, renaming, reordering, and deletion; +- idle-aware background synchronization of boards, stacks, and cards; - horizontal and vertical layouts saved independently per board; - card creation, editing, deletion, due date, description, and color controls; - a directly visible completion checkmark that synchronizes Deck's `done` value diff --git a/app/package-lock.json b/app/package-lock.json index 9edc841..84f6305 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "decky", - "version": "0.3.0", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "decky", - "version": "0.3.0", + "version": "0.3.1", "dependencies": { "@lucide/svelte": "^0.545.0", "@tauri-apps/api": "^2.0.0", diff --git a/app/package.json b/app/package.json index 91c90a6..d3c9127 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,7 @@ { "name": "decky", "private": true, - "version": "0.3.0", + "version": "0.3.1", "type": "module", "scripts": { "dev": "vite --configLoader runner", diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index d556937..aa4f7f0 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -590,7 +590,7 @@ dependencies = [ [[package]] name = "decky" -version = "0.3.0" +version = "0.3.1" dependencies = [ "keyring", "serde", diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index 3d172b1..7a52e66 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "decky" -version = "0.3.0" +version = "0.3.1" description = "A focused cross-platform client for Nextcloud Deck" authors = ["Elijah Kuntz"] edition = "2021" diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index 3a07518..0fc5a14 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Decky", - "version": "0.3.0", + "version": "0.3.1", "identifier": "com.elijahkuntz.decky", "build": { "beforeDevCommand": "npm run dev", @@ -34,7 +34,7 @@ "category": "Productivity", "shortDescription": "Nextcloud Deck on your desktop", "longDescription": "A focused cross-platform desktop client for viewing and editing Nextcloud Deck boards.", - "copyright": "Copyright © 2026 Elijah Kuntz", + "copyright": "Copyright © 2026 Elijah Kuntz", "windows": { "webviewInstallMode": { "type": "downloadBootstrapper" diff --git a/app/src/App.svelte b/app/src/App.svelte index 906a5d6..7fde8a8 100644 --- a/app/src/App.svelte +++ b/app/src/App.svelte @@ -26,6 +26,7 @@ X } from '@lucide/svelte'; import { cssColor, normalizeColor, textColor } from './lib/color'; + import { shouldAutoRefresh } from './lib/auto-refresh'; import { loadPassword, removePassword, savePassword } from './lib/credentials'; import { cardUpdateFrom, DeckApiClient } from './lib/deck-api'; import { loadSettings, saveSettings } from './lib/settings'; @@ -52,7 +53,7 @@ let dragOriginStacks: Stack[] | null = null; let dragGhost: { kind: 'card' | 'stack'; title: string; color?: string | null; x: number; y: number; width: number; offsetX: number; offsetY: number } | null = null; let reduceMotion = false; - let currentVersion = '0.3.0'; + let currentVersion = '0.3.1'; let pendingUpdate: Update | null = null; let updateState: 'idle' | 'checking' | 'available' | 'current' | 'downloading' | 'installing' | 'error' = 'idle'; let updateMessage = 'Updates are checked securely against releases from git.elijahkuntz.com.'; @@ -63,6 +64,9 @@ | { kind: 'stack'; mode: 'create' | 'edit'; id?: number; title: string; order: number } | null = null; let pendingDeletion: { kind: 'board' | 'stack'; id: number; title: string; cardCount: number } | null = null; + let lastInteractionAt = Date.now(); + let lastAutoRefreshAt = 0; + let autoRefreshInFlight = false; const appWindow = '__TAURI_INTERNALS__' in window ? getCurrentWindow() : null; const [sendCard, receiveCard] = crossfade({ duration: (distance) => reduceMotion ? 0 : Math.min(190, Math.max(120, Math.sqrt(distance * 220))), @@ -109,7 +113,7 @@ { id: 106, stackId: 13, title: 'Patho Concept Map', type: 'plain', owner: 'admin', order: 2, color: 'E69A88', done: null } ]} ]; - status = 'Homework · 6 cards · Preview data'; + status = 'Homework · 6 cards · Preview data'; return; } if (!serverUrl || !username) return; @@ -122,6 +126,17 @@ } }); + onMount(() => { + const noteInteraction = () => { lastInteractionAt = Date.now(); }; + const events: (keyof WindowEventMap)[] = ['pointerdown', 'keydown', 'wheel']; + for (const event of events) window.addEventListener(event, noteInteraction, { passive: true }); + const timer = window.setInterval(() => void runIdleRefresh(), 1_000); + return () => { + window.clearInterval(timer); + for (const event of events) window.removeEventListener(event, noteInteraction); + }; + }); + function friendlyError(error: unknown): string { if (error instanceof Error) return error.message; return String(error); @@ -134,7 +149,7 @@ return; } updateState = 'checking'; - updateMessage = 'Checking Forgejo for a newer signed release…'; + updateMessage = 'Checking Forgejo for a newer signed release…'; updateProgress = 0; pendingUpdate = null; try { @@ -155,7 +170,7 @@ async function installUpdate() { if (!pendingUpdate) return; updateState = 'downloading'; - updateMessage = `Downloading Decky ${pendingUpdate.version}…`; + updateMessage = `Downloading Decky ${pendingUpdate.version}…`; let downloaded = 0; let total = 0; try { @@ -169,7 +184,7 @@ } else if (event.event === 'Finished') { updateState = 'installing'; updateProgress = 100; - updateMessage = 'Download verified. Installing and restarting Decky…'; + updateMessage = 'Download verified. Installing and restarting Decky…'; } }); await relaunch(); @@ -181,7 +196,7 @@ async function connect(silent = false) { busy = true; - status = 'Connecting to Nextcloud…'; + status = 'Connecting to Nextcloud…'; try { client = await DeckApiClient.connect({ serverUrl, username, appPassword }); boards = (await client.getBoards()).filter((board) => !board.archived && !board.deletedAt); @@ -213,13 +228,13 @@ async function refreshBoard() { if (!client || !selectedBoard) return; busy = true; - status = `Loading ${selectedBoard.title}…`; + status = `Loading ${selectedBoard.title}…`; try { stacks = (await client.getStacks(selectedBoard.id)) .filter((stack) => !stack.deletedAt) .sort((a, b) => a.order - b.order); const count = stacks.reduce((total, stack) => total + (stack.cards?.filter((card) => !card.archived).length ?? 0), 0); - status = `${selectedBoard.title} · ${count} card${count === 1 ? '' : 's'} · Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`; + status = `${selectedBoard.title} · ${count} card${count === 1 ? '' : 's'} · Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`; } catch (error) { status = friendlyError(error); } finally { @@ -227,6 +242,68 @@ } } + async function refreshFromNextcloud(background = false) { + if (!client) return; + const activeClient = client; + const startingBoardId = selectedBoard?.id; + const interactionAtStart = lastInteractionAt; + if (!background) { + busy = true; + status = selectedBoard ? `Loading ${selectedBoard.title}…` : 'Refreshing boards…'; + } + try { + const refreshedBoards = (await activeClient.getBoards()).filter((board) => !board.archived && !board.deletedAt); + if (client !== activeClient || (background && (selectedBoard?.id !== startingBoardId || lastInteractionAt !== interactionAtStart || document.visibilityState !== 'visible'))) return; + boards = refreshedBoards; + const board = refreshedBoards.find((item) => item.id === startingBoardId) ?? refreshedBoards[0]; + if (!board) { + selectedBoard = null; + stacks = []; + status = 'Connected, but no active Deck boards were found.'; + return; + } + + selectedBoard = board; + if (settings.selectedBoardId !== board.id) { + settings = { ...settings, selectedBoardId: board.id }; + saveSettings(settings); + } + const refreshedStacks = (await activeClient.getStacks(board.id)) + .filter((stack) => !stack.deletedAt) + .sort((a, b) => a.order - b.order); + if (client !== activeClient || selectedBoard?.id !== board.id || (background && (lastInteractionAt !== interactionAtStart || document.visibilityState !== 'visible'))) return; + stacks = refreshedStacks; + const count = stacks.reduce((total, stack) => total + (stack.cards?.filter((card) => !card.archived).length ?? 0), 0); + status = `${board.title} · ${count} card${count === 1 ? '' : 's'} · Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`; + } catch (error) { + status = `${background ? 'Background refresh failed' : 'Refresh failed'}: ${friendlyError(error)}`; + } finally { + if (!background) busy = false; + } + } + + async function runIdleRefresh() { + const now = Date.now(); + if (autoRefreshInFlight || !shouldAutoRefresh({ + now, + lastInteractionAt, + lastRefreshAt: lastAutoRefreshAt, + connected: Boolean(client), + busy, + editing: Boolean(editor || structureEditor || pendingDeletion || settingsOpen), + dragging: Boolean(dragKind), + visible: document.visibilityState === 'visible' + })) return; + + autoRefreshInFlight = true; + lastAutoRefreshAt = now; + try { + await refreshFromNextcloud(true); + } finally { + autoRefreshInFlight = false; + } + } + function canEdit(board: Board | null): boolean { return Boolean(board) && (board?.permissions?.PERMISSION_EDIT ?? true); } @@ -426,7 +503,7 @@ } async function deleteCard(card: Card) { - if (!client || !selectedBoard || !confirm(`Delete “${card.title}”?`)) return; + if (!client || !selectedBoard || !confirm(`Delete “${card.title}”?`)) return; busy = true; try { await client.deleteCard(selectedBoard.id, card.stackId, card.id); @@ -599,7 +676,7 @@ const targetIndex = stacks.findIndex((stack) => stack.id === stackId); stacks = await client.reorderStack(board.id, stackId, targetIndex); const count = stacks.reduce((total, stack) => total + (stack.cards?.filter((item) => !item.archived).length ?? 0), 0); - status = `${board.title} · ${count} card${count === 1 ? '' : 's'} · Stack order saved`; + status = `${board.title} · ${count} card${count === 1 ? '' : 's'} · Stack order saved`; } } catch (error) { stacks = origin; @@ -634,7 +711,7 @@ } -{selectedBoard ? `${selectedBoard.title} · Decky` : 'Decky'} +{selectedBoard ? `${selectedBoard.title} · Decky` : 'Decky'}
- + @@ -790,7 +867,7 @@ diff --git a/app/src/lib/auto-refresh.test.ts b/app/src/lib/auto-refresh.test.ts new file mode 100644 index 0000000..e0f8c30 --- /dev/null +++ b/app/src/lib/auto-refresh.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { AUTO_REFRESH_IDLE_MS, AUTO_REFRESH_REPEAT_MS, shouldAutoRefresh } from './auto-refresh'; + +const idleState = { + now: AUTO_REFRESH_REPEAT_MS, + lastInteractionAt: 0, + lastRefreshAt: 0, + connected: true, + busy: false, + editing: false, + dragging: false, + visible: true +}; + +describe('automatic refresh scheduling', () => { + it('refreshes after the user has been idle long enough', () => { + expect(shouldAutoRefresh(idleState)).toBe(true); + }); + + it('waits for ten seconds of inactivity', () => { + expect(shouldAutoRefresh({ ...idleState, lastInteractionAt: idleState.now - AUTO_REFRESH_IDLE_MS + 1 })).toBe(false); + }); + + it('does not repeatedly refresh more often than the background interval', () => { + expect(shouldAutoRefresh({ ...idleState, lastRefreshAt: idleState.now - AUTO_REFRESH_REPEAT_MS + 1 })).toBe(false); + }); + + it.each([ + ['disconnected', { connected: false }], + ['busy', { busy: true }], + ['editing', { editing: true }], + ['dragging', { dragging: true }], + ['hidden', { visible: false }] + ])('pauses while %s', (_name, change) => { + expect(shouldAutoRefresh({ ...idleState, ...change })).toBe(false); + }); +}); diff --git a/app/src/lib/auto-refresh.ts b/app/src/lib/auto-refresh.ts new file mode 100644 index 0000000..039dfa3 --- /dev/null +++ b/app/src/lib/auto-refresh.ts @@ -0,0 +1,23 @@ +export const AUTO_REFRESH_IDLE_MS = 10_000; +export const AUTO_REFRESH_REPEAT_MS = 30_000; + +export interface AutoRefreshState { + now: number; + lastInteractionAt: number; + lastRefreshAt: number; + connected: boolean; + busy: boolean; + editing: boolean; + dragging: boolean; + visible: boolean; +} + +export function shouldAutoRefresh(state: AutoRefreshState): boolean { + return state.connected + && state.visible + && !state.busy + && !state.editing + && !state.dragging + && state.now - state.lastInteractionAt >= AUTO_REFRESH_IDLE_MS + && state.now - state.lastRefreshAt >= AUTO_REFRESH_REPEAT_MS; +} From 42ed79095647760b3d18760ec6dbc235e7cd15f1 Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 20 Jul 2026 09:01:07 -0700 Subject: [PATCH 4/9] chore(release): publish Decky 0.3.1 updater metadata --- releases/latest.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/releases/latest.json b/releases/latest.json index 0ab61e5..ce23712 100644 --- a/releases/latest.json +++ b/releases/latest.json @@ -1,11 +1,11 @@ { - "version": "0.3.0", - "notes": "Board and stack creation, editing, reordering, and deletion; polished management controls; Forgejo update workflow.", - "pub_date": "2026-07-20T02:00:14.7906154+00:00", + "version": "0.3.1", + "notes": "Automatic idle-aware background refresh for boards, stacks, and cards.", + "pub_date": "2026-07-20T15:55:49.5091194+00:00", "platforms": { "windows-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTSHZBY0NPTW9uRERoeS84YzV1UnFyMVFkTWFqSEpqUm5rclQ2NDlZb09Ra0ZwZ0treFdVZ3cvOTFGWXY3aHhmUFlwNFNFc3QxQTQ2T0l6UXRPdkFCbW01K2VXaXZHNVFJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg0NTEyODEzCWZpbGU6RGVja3lfMC4zLjBfeDY0LXNldHVwLmV4ZQpBTGlRUjhmdnkyNm1pZDFFTlJhdWNEQzdTZGRSS1crOSt3Mm5XaHpsR3ZtMGNZZmpLYmtYSHJrZFpUSnVxTHRHTEtDaHFlUUxxTkJ3SXRvRnZReWRCUT09Cg==", - "url": "https://git.elijahkuntz.com/Elijah/Decky/releases/download/v0.3.0/Decky_0.3.0_x64-setup.exe" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTSHZBY0NPTW9uREdMTkg2RWVNeU53anpNRy9SYy9LS3RnS3I5a0FSVzJrRVZ5Y1NjdHlId2VaRi9YcnNyZnF3YjRIT3VuRVVNOGt2Zk8wWmZObGpYWmFJc2xpRENVOEFnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg0NTYyOTQ4CWZpbGU6RGVja3lfMC4zLjFfeDY0LXNldHVwLmV4ZQpDeEphcVV0T2hkc09kYUJxak14UnU1N1h6L3dJTWhDV2Rqa3Nhc21sTWxuS0VYWW5scEhYakx5MDJ5bVdmbmRkTnphWjdib3BHTCtYVm5yMGZldHdCQT09Cg==", + "url": "https://git.elijahkuntz.com/Elijah/Decky/releases/download/v0.3.1/Decky_0.3.1_x64-setup.exe" } } } \ No newline at end of file From c5287352fd1b70a5137e04233dbc6eedd0ee8394 Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 20 Jul 2026 09:08:20 -0700 Subject: [PATCH 5/9] Simplify refresh status layout --- app/src-tauri/tauri.conf.json | 2 +- app/src/App.svelte | 49 ++++++++++++++++------------------- app/src/app.css | 6 ++--- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index 0fc5a14..1fc5dc1 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -34,7 +34,7 @@ "category": "Productivity", "shortDescription": "Nextcloud Deck on your desktop", "longDescription": "A focused cross-platform desktop client for viewing and editing Nextcloud Deck boards.", - "copyright": "Copyright © 2026 Elijah Kuntz", + "copyright": "Copyright (c) 2026 Elijah Kuntz", "windows": { "webviewInstallMode": { "type": "downloadBootstrapper" diff --git a/app/src/App.svelte b/app/src/App.svelte index 7fde8a8..4b03d42 100644 --- a/app/src/App.svelte +++ b/app/src/App.svelte @@ -14,7 +14,6 @@ ChevronRight, Columns3, LayoutList, - LoaderCircle, Minus, Pencil, Plus, @@ -113,7 +112,7 @@ { id: 106, stackId: 13, title: 'Patho Concept Map', type: 'plain', owner: 'admin', order: 2, color: 'E69A88', done: null } ]} ]; - status = 'Homework · 6 cards · Preview data'; + status = 'Preview data'; return; } if (!serverUrl || !username) return; @@ -149,7 +148,7 @@ return; } updateState = 'checking'; - updateMessage = 'Checking Forgejo for a newer signed release…'; + updateMessage = 'Checking Forgejo for a newer signed release...'; updateProgress = 0; pendingUpdate = null; try { @@ -170,7 +169,7 @@ async function installUpdate() { if (!pendingUpdate) return; updateState = 'downloading'; - updateMessage = `Downloading Decky ${pendingUpdate.version}…`; + updateMessage = `Downloading Decky ${pendingUpdate.version}...`; let downloaded = 0; let total = 0; try { @@ -184,7 +183,7 @@ } else if (event.event === 'Finished') { updateState = 'installing'; updateProgress = 100; - updateMessage = 'Download verified. Installing and restarting Decky…'; + updateMessage = 'Download verified. Installing and restarting Decky...'; } }); await relaunch(); @@ -196,7 +195,7 @@ async function connect(silent = false) { busy = true; - status = 'Connecting to Nextcloud…'; + status = 'Connecting...'; try { client = await DeckApiClient.connect({ serverUrl, username, appPassword }); boards = (await client.getBoards()).filter((board) => !board.archived && !board.deletedAt); @@ -228,13 +227,12 @@ async function refreshBoard() { if (!client || !selectedBoard) return; busy = true; - status = `Loading ${selectedBoard.title}…`; + status = 'Refreshing...'; try { stacks = (await client.getStacks(selectedBoard.id)) .filter((stack) => !stack.deletedAt) .sort((a, b) => a.order - b.order); - const count = stacks.reduce((total, stack) => total + (stack.cards?.filter((card) => !card.archived).length ?? 0), 0); - status = `${selectedBoard.title} · ${count} card${count === 1 ? '' : 's'} · Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`; + status = `Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`; } catch (error) { status = friendlyError(error); } finally { @@ -249,7 +247,7 @@ const interactionAtStart = lastInteractionAt; if (!background) { busy = true; - status = selectedBoard ? `Loading ${selectedBoard.title}…` : 'Refreshing boards…'; + status = 'Refreshing...'; } try { const refreshedBoards = (await activeClient.getBoards()).filter((board) => !board.archived && !board.deletedAt); @@ -273,8 +271,7 @@ .sort((a, b) => a.order - b.order); if (client !== activeClient || selectedBoard?.id !== board.id || (background && (lastInteractionAt !== interactionAtStart || document.visibilityState !== 'visible'))) return; stacks = refreshedStacks; - const count = stacks.reduce((total, stack) => total + (stack.cards?.filter((card) => !card.archived).length ?? 0), 0); - status = `${board.title} · ${count} card${count === 1 ? '' : 's'} · Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`; + status = `Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`; } catch (error) { status = `${background ? 'Background refresh failed' : 'Refresh failed'}: ${friendlyError(error)}`; } finally { @@ -503,7 +500,7 @@ } async function deleteCard(card: Card) { - if (!client || !selectedBoard || !confirm(`Delete “${card.title}”?`)) return; + if (!client || !selectedBoard || !confirm(`Delete "${card.title}"?`)) return; busy = true; try { await client.deleteCard(selectedBoard.id, card.stackId, card.id); @@ -675,8 +672,7 @@ } else if (stackId !== null) { const targetIndex = stacks.findIndex((stack) => stack.id === stackId); stacks = await client.reorderStack(board.id, stackId, targetIndex); - const count = stacks.reduce((total, stack) => total + (stack.cards?.filter((item) => !item.archived).length ?? 0), 0); - status = `${board.title} · ${count} card${count === 1 ? '' : 's'} · Stack order saved`; + status = `Updated ${new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`; } } catch (error) { stacks = origin; @@ -711,7 +707,7 @@ } -{selectedBoard ? `${selectedBoard.title} · Decky` : 'Decky'} +{selectedBoard ? `${selectedBoard.title} - Decky` : 'Decky'}
- +
+ + {status} +
@@ -841,10 +840,6 @@ {/if} -
- {#if busy}{:else}{/if} - {status} -
{#if dragGhost} @@ -867,7 +862,7 @@ diff --git a/app/src/app.css b/app/src/app.css index 01db576..10b9a57 100644 --- a/app/src/app.css +++ b/app/src/app.css @@ -29,7 +29,7 @@ button { color: inherit; } .app-shell { height: 100%; display: grid; - grid-template: 34px minmax(0, 1fr) 28px / 250px minmax(0, 1fr); + grid-template: 34px minmax(0, 1fr) / 250px minmax(0, 1fr); background: var(--bg); } @@ -115,6 +115,8 @@ textarea { resize: vertical; min-height: 90px; } .sidebar-layout button { justify-content: center; gap: 6px; padding: 0 6px; background: #292929; } .sidebar-layout button.active { color: #fff; background: #3a3a3a; box-shadow: inset 0 -2px var(--accent); } .sidebar-tool span, .sidebar-layout span { font-size: 12px; } +.refresh-row { min-width: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 4px; } +.sync-status { overflow: hidden; padding-right: 7px; color: var(--muted); font-size: 10px; text-align: right; text-overflow: ellipsis; white-space: nowrap; } .workspace { grid-column: 2; grid-row: 2; min-width: 0; min-height: 0; display: flex; flex-direction: column; background: #232323; } .workspace-tools { min-height: 46px; flex: 0 0 auto; display: grid; grid-template-columns: minmax(120px, 1fr) minmax(180px, 280px) minmax(80px, 1fr); align-items: center; gap: 16px; padding: 6px 18px; color: var(--muted); font-size: 12px; border-bottom: 1px solid var(--line); } @@ -173,8 +175,6 @@ textarea { resize: vertical; min-height: 90px; } .empty-illustration span { position: absolute; width: 66px; height: 44px; z-index: -1; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); transform: translate(-28px, 13px) rotate(-12deg); } .empty-illustration span:last-child { transform: translate(28px, 13px) rotate(12deg); } -.statusbar { grid-column: 1 / -1; grid-row: 3; display: flex; align-items: center; gap: 8px; min-width: 0; padding: 0 12px; border-top: 1px solid var(--line); color: var(--muted); background: #161616; font-size: 11px; } -.statusbar span:last-child { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .spinning { animation: spin .9s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } From 0055fb196bd20c263795ee0415bd889b023fa6a7 Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 20 Jul 2026 09:15:52 -0700 Subject: [PATCH 6/9] Release Decky 0.3.2 --- app/package-lock.json | 4 ++-- app/package.json | 2 +- app/src-tauri/Cargo.lock | 2 +- app/src-tauri/Cargo.toml | 2 +- app/src-tauri/tauri.conf.json | 2 +- app/src/App.svelte | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index 84f6305..305ef39 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "decky", - "version": "0.3.1", + "version": "0.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "decky", - "version": "0.3.1", + "version": "0.3.2", "dependencies": { "@lucide/svelte": "^0.545.0", "@tauri-apps/api": "^2.0.0", diff --git a/app/package.json b/app/package.json index d3c9127..014efc3 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,7 @@ { "name": "decky", "private": true, - "version": "0.3.1", + "version": "0.3.2", "type": "module", "scripts": { "dev": "vite --configLoader runner", diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index aa4f7f0..ccac9cd 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -590,7 +590,7 @@ dependencies = [ [[package]] name = "decky" -version = "0.3.1" +version = "0.3.2" dependencies = [ "keyring", "serde", diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index 7a52e66..2f2f041 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "decky" -version = "0.3.1" +version = "0.3.2" description = "A focused cross-platform client for Nextcloud Deck" authors = ["Elijah Kuntz"] edition = "2021" diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index 1fc5dc1..9e23b13 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Decky", - "version": "0.3.1", + "version": "0.3.2", "identifier": "com.elijahkuntz.decky", "build": { "beforeDevCommand": "npm run dev", diff --git a/app/src/App.svelte b/app/src/App.svelte index 4b03d42..e2bd819 100644 --- a/app/src/App.svelte +++ b/app/src/App.svelte @@ -52,7 +52,7 @@ let dragOriginStacks: Stack[] | null = null; let dragGhost: { kind: 'card' | 'stack'; title: string; color?: string | null; x: number; y: number; width: number; offsetX: number; offsetY: number } | null = null; let reduceMotion = false; - let currentVersion = '0.3.1'; + let currentVersion = '0.3.2'; let pendingUpdate: Update | null = null; let updateState: 'idle' | 'checking' | 'available' | 'current' | 'downloading' | 'installing' | 'error' = 'idle'; let updateMessage = 'Updates are checked securely against releases from git.elijahkuntz.com.'; From 134a0ea05b7cf3a6ef26fdcb5fdbf1b246674de6 Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 20 Jul 2026 09:16:08 -0700 Subject: [PATCH 7/9] chore(release): publish Decky 0.3.2 updater metadata --- releases/latest.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/releases/latest.json b/releases/latest.json index ce23712..a6d0ac5 100644 --- a/releases/latest.json +++ b/releases/latest.json @@ -1,11 +1,11 @@ { - "version": "0.3.1", - "notes": "Automatic idle-aware background refresh for boards, stacks, and cards.", - "pub_date": "2026-07-20T15:55:49.5091194+00:00", + "version": "0.3.2", + "notes": "Removed the bottom status bar, moved the updated time beside Refresh, and cleaned malformed text encoding.", + "pub_date": "2026-07-20T16:13:33.7170490+00:00", "platforms": { "windows-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTSHZBY0NPTW9uREdMTkg2RWVNeU53anpNRy9SYy9LS3RnS3I5a0FSVzJrRVZ5Y1NjdHlId2VaRi9YcnNyZnF3YjRIT3VuRVVNOGt2Zk8wWmZObGpYWmFJc2xpRENVOEFnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg0NTYyOTQ4CWZpbGU6RGVja3lfMC4zLjFfeDY0LXNldHVwLmV4ZQpDeEphcVV0T2hkc09kYUJxak14UnU1N1h6L3dJTWhDV2Rqa3Nhc21sTWxuS0VYWW5scEhYakx5MDJ5bVdmbmRkTnphWjdib3BHTCtYVm5yMGZldHdCQT09Cg==", - "url": "https://git.elijahkuntz.com/Elijah/Decky/releases/download/v0.3.1/Decky_0.3.1_x64-setup.exe" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTSHZBY0NPTW9uREMyYUlZNUJnYzhqRHRlTE1qeDZCYlZtVXRGRDZtZER5V2p1V3ZBdXprVndkZ3FqNURzQitoUG1ZWFdFQytxRFFzQ1BMNnJnVE10ZlhCYm5UQk9NTFFFPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzg0NTY0MDEyCWZpbGU6RGVja3lfMC4zLjJfeDY0LXNldHVwLmV4ZQpLOGZ4TXp6V0xYalp6czNDbitleVhBSVlrZHNLT0ZQSmhiTjFGeXlNTUVEYy8vSS9JdlZwQUdiNWdOL0NvcXVFcTVJWDhXWXZwQnhlUnh2Z0xER3dEdz09Cg==", + "url": "https://git.elijahkuntz.com/Elijah/Decky/releases/download/v0.3.2/Decky_0.3.2_x64-setup.exe" } } } \ No newline at end of file From a0b46497c13c9393d8ed305cac156ddd8c8ccb37 Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 20 Jul 2026 09:32:20 -0700 Subject: [PATCH 8/9] Add Decky agent guidance --- AGENTS.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9edf6bf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,53 @@ +# Decky agent notes + +## Project + +- The active app is `app/`: Tauri 2 + Svelte 5 + TypeScript. The older .NET + projects under `src/` and `tests/` are reference code, not the product. +- Decky is a compact, Nextcloud-inspired desktop client for Nextcloud Deck. + Preserve its dark native title bar, horizontal/vertical views, drag-and-drop, + compact cards, collapsible sidebar, and footer-free layout. +- Background sync begins after 10 seconds idle and repeats every 30 seconds. It + must pause during edits, drags, loading, modals, and while the window is hidden. +- Never store or commit Nextcloud credentials. Use an app password and the OS + credential store. Live-instance tests require the user to enter credentials. + +## Nextcloud API compatibility + +- Client code and tests are in `app/src/lib/deck-api.ts` and + `app/src/lib/deck-api.test.ts`. +- Support both documented `index.php/apps/deck/api/v1.0/` and rewrite-friendly + `apps/deck/api/v1.0/` paths. +- Preserve complete card fields on updates, including color and object/string + owner handling. Keep the tested cross-stack card-move fallback. +- Stack reorder uses Deck's OCS reorder endpoint and verifies persisted order. +- Respect board permissions before exposing edit/manage/delete controls. + +## Validation + +From `app/`, run `npm ci`, `npm run check`, `npm test`, and `npm run build`. +Build Windows installers on this Windows machine; the Unraid Forgejo Docker +runner only validates the web client via `.forgejo/workflows/validate.yml`. + +## Versions and releases + +- Installed apps update from signed Forgejo Release assets, never from Git. +- A released version/tag is immutable. Always increment the semantic version; + never republish or replace assets for an existing version. +- Use `scripts/Set-Version.ps1` to synchronize npm, Cargo, Tauri, and the browser + fallback. Commit and push source to `main` before publishing the release. +- Build/sign with `scripts/Prepare-Release.ps1`. Generated files live under + `artifacts/release/vX.Y.Z/` and stay out of Git. +- Publish by double-clicking `scripts/Publish-DeckyRelease.cmd` or by invoking + `scripts/Publish-ForgejoRelease.ps1`. The publisher creates the tag/release, + uploads the installer/signature, and commits `releases/latest.json` to `main`. +- Publishing creates a new remote metadata commit. Fetch/rebase or pull before + the next push; a non-fast-forward rejection usually means this commit is + missing locally. LF-to-CRLF messages on Windows are warnings, not failures. +- Forgejo: `https://git.elijahkuntz.com/Elijah/Decky`. A publishing PAT needs + `write:repository`; never commit or print it. +- The permanent Tauri updater key is outside the repository at + `%USERPROFILE%\.tauri\decky.key` with its password beside it. Never expose, + replace, or regenerate this key after releases have shipped. + +See `app/UPDATES.md` for the complete release procedure. From e7b232652166b28a5b9861231bdade18bfdcb6c2 Mon Sep 17 00:00:00 2001 From: Elijah Date: Mon, 20 Jul 2026 09:35:51 -0700 Subject: [PATCH 9/9] Synchronize local branch after publishing --- AGENTS.md | 9 +++--- app/UPDATES.md | 12 ++++---- scripts/Publish-ForgejoRelease.ps1 | 45 ++++++++++++++++++++++++++---- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9edf6bf..990f7bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,10 +40,11 @@ runner only validates the web client via `.forgejo/workflows/validate.yml`. `artifacts/release/vX.Y.Z/` and stay out of Git. - Publish by double-clicking `scripts/Publish-DeckyRelease.cmd` or by invoking `scripts/Publish-ForgejoRelease.ps1`. The publisher creates the tag/release, - uploads the installer/signature, and commits `releases/latest.json` to `main`. -- Publishing creates a new remote metadata commit. Fetch/rebase or pull before - the next push; a non-fast-forward rejection usually means this commit is - missing locally. LF-to-CRLF messages on Windows are warnings, not failures. + uploads the installer/signature, commits `releases/latest.json` to `main`, and + automatically fast-forwards the local checkout to that metadata commit. +- The publisher requires a clean `main` that exactly matches `origin/main`, so + commit and push source first. LF-to-CRLF messages on Windows are warnings, + not failures. - Forgejo: `https://git.elijahkuntz.com/Elijah/Decky`. A publishing PAT needs `write:repository`; never commit or print it. - The permanent Tauri updater key is outside the repository at diff --git a/app/UPDATES.md b/app/UPDATES.md index 4646a49..39a8ad3 100644 --- a/app/UPDATES.md +++ b/app/UPDATES.md @@ -73,7 +73,6 @@ current PowerShell process and run: $env:FORGEJO_PAT = 'paste-token-for-this-session' powershell -ExecutionPolicy Bypass -File .\scripts\Publish-ForgejoRelease.ps1 -Version 0.3.0 -Notes "Board and stack management" Remove-Item Env:FORGEJO_PAT -git pull --ff-only ``` For an interactive, double-clickable option, run: @@ -86,11 +85,12 @@ It bypasses PowerShell's script policy only for the publisher process, prompts for the version and a hidden personal access token, and leaves the window open so any error can be read. The token is not saved. -The publisher creates the `v0.3.0` Forgejo release/tag at `main`, uploads the -installer, signature, and metadata, then commits the tracked -`releases/latest.json` through the Forgejo API. The final pull synchronizes that -small metadata commit locally. Existing Decky installations then discover the -release through Settings. +The publisher first verifies that the working tree is clean and local `main` +exactly matches `origin/main`. It then creates the `v0.3.0` Forgejo release/tag, +uploads the installer, signature, and metadata, and commits the tracked +`releases/latest.json` through the Forgejo API. Finally, it automatically +fast-forwards local `main` to that metadata commit. Existing Decky installations +then discover the release through Settings. ## Forgejo Actions diff --git a/scripts/Publish-ForgejoRelease.ps1 b/scripts/Publish-ForgejoRelease.ps1 index 0d29d04..554c9f6 100644 --- a/scripts/Publish-ForgejoRelease.ps1 +++ b/scripts/Publish-ForgejoRelease.ps1 @@ -12,6 +12,35 @@ param( ) $ErrorActionPreference = 'Stop' +$repositoryRoot = Split-Path -Parent $PSScriptRoot +$releaseDirectory = Join-Path $repositoryRoot "artifacts\release\v$Version" +$latestJsonPath = Join-Path $releaseDirectory 'latest.json' +if (-not (Test-Path -LiteralPath $latestJsonPath)) { throw "Run Prepare-Release.ps1 first: $latestJsonPath is missing." } + +function Invoke-GitCommand { + param([Parameter(Mandatory)][string[]]$ArgumentList) + $output = @(& git -C $repositoryRoot @ArgumentList 2>&1) + if ($LASTEXITCODE) { + throw "git $($ArgumentList -join ' ') failed: $($output -join [Environment]::NewLine)" + } + return $output +} + +$branch = ((Invoke-GitCommand @('rev-parse', '--abbrev-ref', 'HEAD')) | Select-Object -Last 1).Trim() +if ($branch -ne 'main') { throw "Releases must be published from main; the current branch is '$branch'." } + +$workingChanges = @(Invoke-GitCommand @('status', '--porcelain')) +if ($workingChanges.Count) { + throw "Commit or discard all source changes before publishing:`n$($workingChanges -join [Environment]::NewLine)" +} + +Invoke-GitCommand @('fetch', 'origin', 'main') | Out-Null +$localHead = ((Invoke-GitCommand @('rev-parse', 'HEAD')) | Select-Object -Last 1).Trim() +$remoteHead = ((Invoke-GitCommand @('rev-parse', 'origin/main')) | Select-Object -Last 1).Trim() +if ($localHead -ne $remoteHead) { + throw 'Local main does not exactly match origin/main. Pull/rebase or push the source before publishing.' +} + if (-not $Token) { Write-Host 'A Forgejo personal access token is required to create the release.' -ForegroundColor Cyan Write-Host 'The token is used only by this process and is not saved by Decky.' -ForegroundColor DarkGray @@ -20,11 +49,6 @@ if (-not $Token) { } if (-not $Token) { throw 'No Forgejo personal access token was entered.' } -$repositoryRoot = Split-Path -Parent $PSScriptRoot -$releaseDirectory = Join-Path $repositoryRoot "artifacts\release\v$Version" -$latestJsonPath = Join-Path $releaseDirectory 'latest.json' -if (-not (Test-Path -LiteralPath $latestJsonPath)) { throw "Run Prepare-Release.ps1 first: $latestJsonPath is missing." } - $headers = @{ Authorization = "token $Token"; Accept = 'application/json' } $apiRoot = "$($ForgejoBaseUrl.TrimEnd('/'))/api/v1/repos/$Repository" $tag = "v$Version" @@ -82,3 +106,14 @@ $contentBody = @{ if ($sha) { $contentBody.sha = $sha } Invoke-RestMethod -Method Put -Uri "$apiRoot/contents/releases/latest.json" -Headers $headers -ContentType 'application/json' -Body ($contentBody | ConvertTo-Json) | Out-Null Write-Host "Published Decky $Version and updated releases/latest.json." + +Invoke-GitCommand @('fetch', 'origin', 'main') | Out-Null +$publishedHead = ((Invoke-GitCommand @('rev-parse', 'origin/main')) | Select-Object -Last 1).Trim() +if ($publishedHead -ne $localHead) { + $mergeBase = ((Invoke-GitCommand @('merge-base', $localHead, $publishedHead)) | Select-Object -Last 1).Trim() + if ($mergeBase -ne $localHead) { + throw 'The release was published, but origin/main also changed independently. Fetch and reconcile it manually.' + } + Invoke-GitCommand @('merge', '--ff-only', 'origin/main') | Out-Null +} +Write-Host 'Local main is synchronized with the Forgejo release metadata commit.'