diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..990f7bd
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,54 @@
+# 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, 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
+ `%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.
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/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 0fc5a14..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",
@@ -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..e2bd819 100644
--- a/app/src/App.svelte
+++ b/app/src/App.svelte
@@ -14,7 +14,6 @@
ChevronRight,
Columns3,
LayoutList,
- LoaderCircle,
Minus,
Pencil,
Plus,
@@ -53,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.';
@@ -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 @@
}
-