Decky/Implementation Plan.md
2026-07-19 13:10:55 -07:00

1402 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Implementation plan: Nextcloud Deck desktop client
## Architecture review (July 19, 2026)
The overall direction remains appropriate: a C# WinUI 3 client, an API-first core,
and MSIX packaging are a good fit for a small Windows-only Deck client. The
following decisions refine the original plan before implementation:
- Target **.NET 10 LTS** and a stable Windows App SDK release. Pin exact package
versions in the repository and update them deliberately.
- Keep the Nextcloud API and domain code in a UI-independent `Decky.Core`
project. This makes the compatibility spike and most tests runnable without
installing or launching the desktop UI.
- Treat board, stack, and card identifiers as `int`, matching the Deck REST API.
Open board tabs are therefore stored as `List<int>`, not `List<Guid>`.
- Keep `done` as an ISO-8601 timestamp or `null`; it is not a Boolean in the Deck
API. Marking a card complete sends the current UTC time, and reopening it sends
an explicit JSON `null`.
- Normalize card and board colors to six-digit RGB values without a leading `#`
at the API boundary; add `#` only for UI display.
- Discover the Deck API base path once during connection and persist the working
path. Prefer the documented `/index.php/apps/deck/api/v1.0/` path, then try the
rewrite-friendly `/apps/deck/api/v1.0/` path.
- Preserve the last successfully loaded board snapshot for disconnected viewing,
but never store the app password in that snapshot or in settings.
- Use ETags/`If-None-Match` once basic synchronization works; the Deck API
propagates child changes into parent ETags and this avoids re-downloading
unchanged boards every 30 seconds.
- Store credentials through Windows Credential Locker behind an interface so the
API core remains testable. Never pass credentials on command lines, including
the API spike.
### Program update strategy
Do **not** update an installed application by pulling its Git repository. A Git
pull requires developer tools, exposes the source tree to local changes, and
bypasses Windows package identity, signing, rollback, and file-integrity rules.
Use this release flow instead:
1. Forgejo Actions builds, tests, and publishes a versioned, signed
`.msixbundle` for each tagged release.
2. The same release publishes a stable `Decky.appinstaller` file whose
`MainBundle` points to that release asset.
3. Users install `Decky.appinstaller` once. Windows App Installer checks for a
newer package on launch and can also check in the background.
4. Package identity and publisher certificate remain constant; the four-part
MSIX version increases for every release.
For a private Forgejo instance, the release files must be reachable by Windows
App Installer over HTTPS without an interactive Forgejo login. If private release
assets cannot be exposed safely, publish signed artifacts to a small static HTTPS
location (which may still be deployed by Forgejo Actions). Keep updates optional
and non-blocking for this personal utility. A portable unpackaged build can be a
later secondary distribution, but it would need a separate updater.
I would build this as a \*\*C# WinUI 3 application using MVVM\*\*, distributed through an \*\*MSIX installer\*\*. WinUI 3 provides a modern native Windows interface, its list controls support drag-based reordering, and MSIX gives you a normal installed application rather than a folder of source files. An unpackaged self-contained executable could be added later, but MSIX is the cleaner primary distribution format. (\[Microsoft Learn]\[1])
\## 1. Defined scope for version 1
The first version should support:
\* Connecting to one Nextcloud server
\* Viewing every accessible Deck board
\* Opening multiple boards in tabs
\* Switching each board between horizontal and vertical layouts
\* Creating, editing, deleting, and completing cards
\* Dragging cards to reorder them within a list
\* Dragging cards between lists
\* Displaying and changing card colors
\* Creating reusable local color presets
\* Automatically applying configurable active and completed colors
\* Periodic and manual synchronization
\* Remembering window position, monitor, size, and layout choices
It should not initially include attachments, comments, labels, sharing controls, board creation, or offline editing. Those would add a disproportionate amount of complexity for a desktop task list.
\## 2. Technology stack
```text
Language: C#
UI framework: WinUI 3 / XAML
Runtime: .NET 10 LTS
Architecture: MVVM
API client: HttpClient
JSON: System.Text.Json
Local settings: JSON files in local app data
Credentials: Windows PasswordVault or Credential Manager
Distribution: MSIX installer
Testing: xUnit
```
Use `CommunityToolkit.Mvvm` for observable properties, commands, and dependency injection. It removes considerable MVVM boilerplate without changing the basic application architecture.
No local database is necessary. The source of truth remains Nextcloud, while a few small JSON files hold interface settings and color presets.
\## 3. Main interface
\### Application shell
The main window should contain:
```text
┌──────────────────────────────────────────────────────────────┐
│ Board tabs Refresh Layout Always on top Settings│
├───────────────┬──────────────────────────────────────────────┤
│ Board list │ │
│ │ Selected board │
│ Homework │ │
│ Misc. To Do │ │
│ │ │
└───────────────┴──────────────────────────────────────────────┘
```
The board sidebar should be collapsible, particularly for a narrow vertical monitor.
Selecting a board opens it in the current tab. An option in its context menu can open it in a new tab. Each tab maintains its own scroll position and layout orientation.
\### Horizontal board layout
This resembles the standard Deck interface:
```text
Saturday Sunday Monday
──────── ────── ──────
Card Card Card
Card Card Card
\&#x20; Card
```
The stacks are arranged side by side inside a horizontal `ScrollViewer`. Every stack contains a vertical card `ListView`.
\### Vertical board layout
This is intended for the vertical monitor:
```text
SATURDAY
Card
Card
SUNDAY
Card
Card
Card
MONDAY
Card
Card
```
Each stack occupies the available width and the stacks are arranged from top to bottom. The orientation should be saved separately for each board.
\## 4. Card design
Each card should display:
\* Card title
\* Card background color
\* Optional due date
\* Checkmark button
\* Context-menu button, visible on hover
\* A subtle drag handle, or allow dragging from the whole card
A compact card could look like:
```text
┌────────────────────────────────┐
│ ✓ Pharm Med Assignment 2 ⋮ │
└────────────────────────────────┘
```
Text color should be calculated from the card background so light cards use dark text and dark cards use light text.
\### Checkmark behavior
The checkmark should perform two related actions:
1\. Set the cards actual Deck `done` value.
2\. Apply the configured completed color preset.
When unchecked:
1\. Clear the `done` value.
2\. Apply the configured active color preset.
This means your typical configuration could be:
```text
Active card preset: Red
Completed card preset: Green
```
This is preferable to treating completion as only a visual color change because current Deck card updates support a `done` property as well as the card `color` property. (\[GitHub]\[2])
An optional setting can control this behavior:
```text
Checkmark action:
○ Mark done and change color
○ Change color only
```
The first option should be the default.
\### Right-click menu
```text
Mark as done / Mark as not done
Apply color preset >
──────────────────────────────
Edit card
Move to list >
──────────────────────────────
Delete card
Open in Nextcloud
```
“Apply color preset” opens a submenu containing every saved preset plus a custom color option.
\## 5. Card editor
Use a modal dialog or right-side panel containing:
```text
Title
Description
Due date
Color
\&#x20; \\\[Red] \\\[Green] \\\[Orange] \\\[Blue] \\\[Custom...]
\&#x20; \\\[Save current color as preset]
Completed checkbox
Cancel Save
```
Creating and editing should use the same component.
When a custom color is selected, show:
\* Visual color picker
\* Hex value
\* “Save as preset” checkbox
\* Preset name field
Saving a preset stores it locally. It does not create a Deck label or modify anything else on Nextcloud.
\## 6. Color preset settings
The settings page should have a dedicated section:
```text
Color presets
Name Color Actions
Needs attention #D99080 Edit Delete
Completed #7FBC9F Edit Delete
Optional #D6B36A Edit Delete
\\\[Add preset]
Active-card preset: Needs attention
Completed-card preset: Completed
```
Recommended local model:
```csharp
public sealed class ColorPreset
{
\&#x20; public Guid Id { get; init; }
\&#x20; public string Name { get; set; } = "";
\&#x20; public string HexColor { get; set; } = "";
}
```
Color presets should be global by default. Individual boards can optionally override the active and completed preset selections.
\## 7. Nextcloud API integration
Decks documented API supports loading boards and stacks, creating cards, updating cards, deleting cards, and changing card order. The current Deck controller also accepts `color` during create and update, and accepts `done` during update. (\[Nextcloud Deck]\[3])
\### API operations
| Operation | Request |
| --------------------- | --------------------------------------------------------------- |
| List boards | `GET /boards` |
| Load stacks and cards | `GET /boards/{boardId}/stacks` |
| Get full card | `GET /boards/{boardId}/stacks/{stackId}/cards/{cardId}` |
| Create card | `POST /boards/{boardId}/stacks/{stackId}/cards` |
| Update card | `PUT /boards/{boardId}/stacks/{stackId}/cards/{cardId}` |
| Delete card | `DELETE /boards/{boardId}/stacks/{stackId}/cards/{cardId}` |
| Reorder or move | `PUT /boards/{boardId}/stacks/{stackId}/cards/{cardId}/reorder` |
The client should accept only the server root from the user:
```text
https://drive.elijahkuntz.com
```
It can then test these API path forms:
```text
/apps/deck/api/v1.0
/index.php/apps/deck/api/v1.0
```
The second is the formally documented form, while installations using rewritten URLs commonly omit `index.php`. (\[Nextcloud Deck]\[3])
\### Authentication
The first-run connection screen should request:
\* Nextcloud server address
\* Username
\* App password
Use Basic authentication with the app password and include:
```http
OCS-APIRequest: true
Accept: application/json
Content-Type: application/json
```
App passwords are preferable because they can be revoked independently and do not expose the primary account password. Nextcloud supports Basic authentication using a username and password or app token. (\[Nextcloud]\[4])
The app password must be stored in Windows credential storage, never in `settings.json`.
\## 8. API client structure
```text
Services/
├── DeckApiClient.cs
├── AuthenticationService.cs
├── CredentialService.cs
├── SettingsService.cs
├── ColorPresetService.cs
├── BoardSyncService.cs
└── WindowPlacementService.cs
```
`DeckApiClient` should expose methods such as:
```csharp
Task<IReadOnlyList<BoardDto>> GetBoardsAsync();
Task<IReadOnlyList<StackDto>> GetStacksAsync(int boardId);
Task<CardDto> GetCardAsync(int boardId, int stackId, int cardId);
Task<CardDto> CreateCardAsync(CreateCardRequest request);
Task<CardDto> UpdateCardAsync(UpdateCardRequest request);
Task DeleteCardAsync(int boardId, int stackId, int cardId);
Task<CardDto> ReorderCardAsync(ReorderCardRequest request);
```
Use one long-lived `HttpClient`, not a new instance for each request.
\## 9. Important card-update behavior
Decks card update is effectively a complete update rather than a simple patch. The current controller expects required values such as `title`, `type`, and `owner`, with other fields passed alongside them. The desktop application should retain the complete server card model and construct an update request from the latest copy rather than creating a minimal payload. (\[GitHub]\[2])
This is especially important for colors. Deck 1.18.2 has a currently reported bug where updating a card without including `color` can clear the existing color. Every card update from this client should therefore resend the existing color, even when only changing the title or completion status. (\[GitHub]\[5])
The update request should include at least:
```json
{
\&#x20; "title": "Pharm Med Assignment 2",
\&#x20; "type": "plain",
\&#x20; "owner": "Elijah",
\&#x20; "description": "",
\&#x20; "order": 2,
\&#x20; "duedate": null,
\&#x20; "startdate": null,
\&#x20; "archived": false,
\&#x20; "done": null,
\&#x20; "color": "D99080"
}
```
The DTO should distinguish between:
\* A field not being sent
\* A field explicitly being sent as `null`
That distinction matters for clearing `done`, due dates, and other nullable fields.
\## 10. Drag-and-drop implementation
WinUI `ListView` supports drag reordering within a list. Moving between separate stack lists requires handling the drag source and destination manually. (\[Microsoft Learn]\[6])
\### Drag data
When dragging starts, attach:
```csharp
public sealed record CardDragData(
\&#x20; int BoardId,
\&#x20; int CardId,
\&#x20; int SourceStackId,
\&#x20; int SourceIndex);
```
\### Drop process
1\. Determine the destination stack.
2\. Determine the destination card index.
3\. Move the card locally for immediate feedback.
4\. Lock further card mutations for that board.
5\. Call the reorder endpoint with the destination stack ID and index.
6\. Verify the returned `stackId` and `order`.
7\. Keep the change on success.
8\. Roll back the local change on failure.
9\. Reload the affected stacks from Nextcloud.
10\. Release the board mutation lock.
Use a per-board `SemaphoreSlim` so rapid drag operations cannot overlap and corrupt the local ordering.
\### Cross-stack compatibility check
A reported Deck 1.16.4 API problem caused the source stack ID in the URL to override the destination stack ID in the request body, preventing cards from moving between stacks through the reorder endpoint. (\[GitHub]\[7])
Because Deck versions vary, the application should verify the response from every cross-stack move:
```text
Requested destination stack: 12
Returned card stack: 12 -> success
Requested destination stack: 12
Returned card stack: 9 -> rollback and compatibility warning
```
Do not assume an HTTP 200 response means the card actually moved.
The application can show:
```text
Your installed Deck version did not accept a cross-list move.
The card has been restored to its original position.
```
This needs to be tested against your actual server before the rest of drag-and-drop development proceeds.
\## 11. Synchronization approach
Keep synchronization intentionally simple.
\### Initial loading
1\. Load the list of boards.
2\. Restore previously open board tabs.
3\. Load only those boards.
4\. Lazy-load other boards when selected.
\### Refresh behavior
\* Automatic refresh every 30 seconds by default
\* Manual refresh button
\* Pause background refresh while a card is being edited
\* Refresh the affected board after every mutation
\* Show “Last updated” in a tooltip or status area
\### Optimistic changes
Use optimistic updates for:
\* Reordering
\* Completion toggles
\* Color changes
Use confirmed updates for:
\* Creating cards
\* Editing card details
\* Deleting cards
For deletion, show a confirmation dialog. Do not automatically retry a failed delete.
\### Offline behavior
Version 1 should be read-only when disconnected:
\* Keep the most recently loaded cards on screen
\* Display an offline banner
\* Disable create, edit, delete, and dragging
\* Retry when the user refreshes or the polling timer runs
An offline mutation queue is unnecessary for this application.
\## 12. Local settings
Suggested settings model:
```csharp
public sealed class AppSettings
{
\&#x20; public string ServerUrl { get; set; } = "";
\&#x20; public string Username { get; set; } = "";
\&#x20; public int RefreshIntervalSeconds { get; set; } = 30;
\&#x20; public bool AlwaysOnTop { get; set; }
\&#x20; public bool StartWithWindows { get; set; }
\&#x20; public bool StartMinimized { get; set; }
\&#x20; public List<int> OpenBoardTabs { get; set; } = \\\[];
\&#x20; public Dictionary<int, BoardViewSettings> Boards { get; set; } = \\\[];
\&#x20; public List<ColorPreset> ColorPresets { get; set; } = \\\[];
\&#x20; public Guid? ActiveColorPresetId { get; set; }
\&#x20; public Guid? CompletedColorPresetId { get; set; }
}
```
Per-board settings:
```csharp
public sealed class BoardViewSettings
{
\&#x20; public BoardOrientation Orientation { get; set; }
\&#x20; public Guid? ActiveColorPresetOverride { get; set; }
\&#x20; public Guid? CompletedColorPresetOverride { get; set; }
}
```
Also persist:
\* Window size
\* Window coordinates
\* Monitor identifier
\* Sidebar width and collapsed state
\* Last selected board
\* Open tabs
Validate the saved monitor position at launch so the window is not placed offscreen after a monitor is disconnected.
\## 13. Suggested project structure
```text
DeckDesktop/
├── App.xaml
├── App.xaml.cs
├── Models/
│ ├── Board.cs
│ ├── Stack.cs
│ ├── Card.cs
│ ├── ColorPreset.cs
│ └── AppSettings.cs
├── Api/
│ ├── DeckApiClient.cs
│ ├── Dtos/
│ └── Requests/
├── Services/
│ ├── BoardSyncService.cs
│ ├── CredentialService.cs
│ ├── SettingsService.cs
│ ├── ColorPresetService.cs
│ └── WindowPlacementService.cs
├── ViewModels/
│ ├── MainViewModel.cs
│ ├── BoardViewModel.cs
│ ├── StackViewModel.cs
│ ├── CardViewModel.cs
│ ├── CardEditorViewModel.cs
│ └── SettingsViewModel.cs
├── Views/
│ ├── MainWindow.xaml
│ ├── ConnectionPage.xaml
│ ├── BoardView.xaml
│ ├── CardEditorDialog.xaml
│ └── SettingsPage.xaml
├── Controls/
│ ├── DeckCardControl.xaml
│ ├── HorizontalBoardControl.xaml
│ └── VerticalBoardControl.xaml
└── Tests/
\&#x20; ├── DeckApiClientTests.cs
\&#x20; ├── CardUpdateRequestTests.cs
\&#x20; ├── DragOrderTests.cs
\&#x20; └── ColorPresetTests.cs
```
\## 14. Development phases
\### Phase 0: API compatibility spike
Before building the interface, create a console test client and a disposable Deck test board.
Verify:
\* Authentication
\* Board retrieval
\* Card color in responses
\* Creating a card with a color
\* Editing without losing color
\* Setting and clearing `done`
\* Reordering within one stack
\* Moving between stacks
\* Deleting a card
This phase is important because the published API documentation does not yet fully describe newer fields such as card color, while current source code does. Card colors were added to Deck in version 1.18.0. (\[GitHub]\[8])
\### Phase 1: Read-only application
Build:
\* Connection screen
\* Credential storage
\* Board sidebar
\* Board tabs
\* Horizontal layout
\* Vertical layout
\* Card color rendering
\* Manual refresh
\* Window placement persistence
Acceptance criteria:
\* The program starts as an installed application.
\* It reconnects without requesting credentials again.
\* It displays the same lists, cards, order, and colors as Nextcloud.
\* Layout orientation persists per board.
\### Phase 2: Card CRUD
Build:
\* New-card dialog
\* Edit-card dialog
\* Delete confirmation
\* Color picker
\* Automatic board refresh
\* Error notifications
Acceptance criteria:
\* New cards appear in both the desktop client and web Deck.
\* Edited titles, descriptions, dates, colors, and completion status persist.
\* Unrelated card properties are never lost.
\* Delete removes the correct card.
\### Phase 3: Drag and drop
Build:
\* Same-stack reordering
\* Cross-stack movement
\* Drop indicators
\* Optimistic movement
\* Response verification
\* Rollback on failure
\* Per-board mutation locking
Acceptance criteria:
\* Cards retain the correct order after reloading.
\* Cross-stack moves are verified against the server response.
\* Failed operations restore the previous UI state.
\### Phase 4: Color presets and completion workflow
Build:
\* Preset management page
\* Custom preset creation from the editor
\* Preset context menu
\* Active and completed preset selection
\* Checkmark completion button
\* Board-specific overrides
Acceptance criteria:
\* Clicking the checkmark changes the card to the completed preset.
\* Unchecking applies the active preset.
\* The actual Deck `done` state is synchronized when enabled.
\* Presets remain available after restarting.
\### Phase 5: Desktop polish and release
Build:
\* Light and dark themes
\* Always-on-top option
\* Launch at Windows startup
\* Keyboard shortcuts
\* Loading placeholders
\* Offline status banner
\* Logging
\* Application icon
\* MSIX packaging
Suggested keyboard controls:
```text
Ctrl+N New card
Ctrl+R Refresh board
Ctrl+Tab Next board tab
Ctrl+Shift+Tab Previous board tab
Ctrl+, Settings
Enter Edit selected card
Delete Delete selected card
Space Toggle completion
```
\## 15. Final acceptance checklist
The application is complete when:
\* It installs through one MSIX package.
\* Credentials are not stored as plaintext.
\* Every accessible Deck board can be opened.
\* Multiple boards can remain open in tabs.
\* Horizontal and vertical layouts work and persist independently.
\* Cards can be created, edited, deleted, reordered, and moved.
\* Colors remain intact during every type of update.
\* Color presets can be added, edited, removed, and applied.
\* The checkmark reliably changes completion state and color.
\* Network or API failures do not silently discard or misplace cards.
\* The application restores itself to the correct monitor and position.
\* A test suite covers request serialization, color preservation, completion toggling, and drag-order calculations.
This keeps the project small enough to remain a simple personal utility while leaving clear extension points for labels, due-date notifications, attachments, and comments later.
\[1]: https://learn.microsoft.com/en-us/windows/apps/get-started/winui-get-started-overview?utm\_source=chatgpt.com "Get started with WinUI - Windows apps | Microsoft Learn"
\[2]: https://github.com/nextcloud/deck/blob/main/lib/Controller/CardApiController.php?utm\_source=chatgpt.com "deck/lib/Controller/CardApiController.php at main"
\[3]: https://deck.readthedocs.io/en/latest/API/?utm\_source=chatgpt.com "REST API - Nextcloud Deck"
\[4]: https://docs.nextcloud.com/server/latest/developer\_manual/client\_apis/OCS/ocs-api-overview.html?utm\_source=chatgpt.com "OCS APIs overview — Nextcloud latest Developer Manual latest documentation"
\[5]: https://github.com/nextcloud/deck/issues/8131?utm\_source=chatgpt.com "Card update API clears the existing color when ..."
\[6]: https://learn.microsoft.com/sr-cyrl-rs/windows/apps/develop/data/drag-and-drop?view=gestures-1.0\&utm\_source=chatgpt.com "Drag and drop - Windows apps | Microsoft Learn"
\[7]: https://github.com/nextcloud/deck/issues/7933 "Deck API: CardApiController reorder() ignores request body stackId due to route parameter conflict · Issue #7933 · nextcloud/deck · GitHub"
\[8]: https://github.com/nextcloud/deck/releases "Releases · nextcloud/deck · GitHub"