Skip to content

Deck and note services, OpenAPI with Swagger UI, and a working Alpine Docker image - #3

Merged
evgTSV merged 5 commits into
mainfrom
deck-note-services
Oct 26, 2026
Merged

evgTSV merged 5 commits into
mainfrom
deck-note-services

Conversation

@evgTSV

@evgTSV evgTSV commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

Moves the deck and note logic out of AnkiSyncWorker into services and adds the missing commands. On top of that, it describes the API with OpenAPI and Swagger UI, and fixes the Docker image, which couldn't run.

Services and endpoints

  • Services (Services.fs): IDeckService, INoteService, INotetypeService. They return CommandResult<'a> with NotFound / Rejected / InfraFail, which the handlers map to 404 / 422 / 500.
  • Worker: no longer knows about commands or the backend. It runs queued jobs one at a time (AnkiSyncMsg.submit), and successful changes schedule the debounced sync. A full sync cancels a pending one, a failed job triggers none, and exceptions reach the HTTP caller. Reads don't go through the worker and never trigger a sync.
  • New endpoints: GET /decks, PUT/DELETE /decks/{id}, GET /notes?query=…, GET/PUT/DELETE /notes/{id}, GET /note-types. Documented in docs/server.md.

OpenAPI and Swagger UI

  • Every endpoint is described through Oxpecker.OpenApi (ApiDocs.fs): operation id, summary, request body, path and query parameters, and every success and error response.
  • The spec is served at /openapi/v1.json. Swagger UI is at /swagger, from Swashbuckle.AspNetCore.SwaggerUI, which only serves the UI for that spec.
  • Schema transformers and the HTTP JSON options make the spec match what the API actually accepts: integers aren't strings, fieldless unions are string enums, non-option record fields aren't nullable, and 204 has no body.

Docker

  • The runtime stage used dotnet/runtime, which has no ASP.NET Core. The build also never restored Paket, generated the rslib client or built libankit_rslib.so, so the image couldn't be built or run.
  • The image now builds on sdk:10.0-alpine through the same build-script targets CI runs, and checks that the native library was published. It runs on aspnet:10.0-alpine as the app user with a writable /app/data. The image is 223 MB.
  • Two Alpine-specific fixes: gcompat runs Grpc.Tools' glibc protoc, and musl targets build without crt-static, which a cdylib needs (.cargo/config.toml, musl only). Host build output stays out of the Docker context.
  • Rider's Docker fast mode can't work with this project: it runs the Windows build in the container, which has no Linux native library. Use regular mode or ./build Run.

Build and CI

  • CI is pinned to .NET SDK 10.0.402. 10.0.x resolved to 10.0.403, which wasn't downloadable (HTTP 409). Unpin once it is.
  • Before-Push now formats before it verifies, so formatting issues get fixed instead of failing the run.
  • CLAUDE.md adds guidance for AI agents: build, CI, conventions, architecture and known quirks.

Breaking API changes

  • POST /decks and POST /notes answer 201 {"id": …} instead of 204.
  • A missing deck or note in the URL answers 404. rslib rejections stay 422, now with rslib's reason as the message.

Fixes found along the way

  • Notes for a missing or filtered deck are rejected. rslib would silently put their cards in the Default deck (cardgen.rs).
  • Not-found errors name the missing deck, note or note type instead of rslib's desktop text ("…use the Check Database action").
  • Union values are read case-insensitively. docs/server.md documents "direction": "upload", but only "UPLOAD" bound before.
  • Oxpecker's query binding is case-sensitive, so SearchNotesDto uses a lowercase query field.
  • Oxpecker's query binder throws on an empty query string, so GET /notes returned 400. bindOrDeclineQuery now validates an empty model, which gives the usual 422.

Known issue, not fixed here

  • If the sync username isn't configured, POST /sync/full returns 500, because protobuf rejects the null username. It should fail with a clear error instead.

Tests

  • DeckServiceTests, NoteServiceTests and NotetypeServiceTests run on a temp collection, without Docker.
  • AnkiSyncWorkerTests uses a counting fake ISyncService. It depends on timing (200 ms debounce) and passed 8 repeated runs locally.
  • ./build Before-Push passes locally: 9 native and 33 server tests, including the Docker sync tests.
  • Manual checks: I called every endpoint on a running server with a scratch collection. I also checked the generated spec and Swagger UI, and ran the Alpine image with ./build Run: the native library loads against musl, the API works end to end, and non-ASCII text round-trips.

🤖 Generated with Claude Code

The worker now only runs queued jobs one at a time and syncs the changes
they made; deck and note logic lives in IDeckService, INoteService and
INotetypeService, which report NotFound, Rejected or InfraFail.

New endpoints: list, rename and delete decks; get, search, update and
delete notes; list note types with their fields. Creating a deck or note
answers 201 with its id, a missing deck or note in the URL answers 404.

Notes for a missing or filtered deck are rejected instead of landing in
the default deck, as rslib would do silently. Not-found errors name the
missing object instead of rslib's desktop text. GET /notes without a
query answers 422 instead of failing in Oxpecker's query binder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@evgTSV evgTSV self-assigned this Oct 25, 2026
evgTSV and others added 4 commits October 26, 2026 00:42
10.0.x resolves to 10.0.403 (runtime 10.0.14), which setup-dotnet finds
but can't download: dotnetorigin answers 409, and Microsoft's 10.0
releases.json still lists 10.0.402 as the latest release. Go back to
10.0.x once 10.0.403 is published.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The image couldn't run: the runtime stage used dotnet/runtime, which
lacks ASP.NET Core, and the build never restored Paket, generated the
rslib client or built libankit_rslib.so, so publishing failed.

The image now builds on sdk:10.0-alpine through the same build script
targets CI runs and checks that the native library was published; it
runs on aspnet:10.0-alpine as the app user with a writable /app/data.
gcompat runs Grpc.Tools' glibc protoc, and musl targets build without
crt-static, which a cdylib needs. Host build output stays out of the
Docker context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every endpoint is described through Oxpecker.OpenApi (ApiDocs.fs): an
operation id and summary, request bodies, path and query parameters,
and each success and error response. The spec is served at
/openapi/v1.json and browsable with Swagger UI at /swagger.

Schema transformers and HTTP JSON options make the spec match what the
API accepts: integers aren't strings, fieldless unions are string
enums, non-option record fields aren't nullable, and 204 has no body.

Union values are now read case-insensitively. docs/server.md documents
"direction": "upload", but only "UPLOAD" bound before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@evgTSV evgTSV changed the title Extract deck, note and notetype services from the sync worker Deck and note services, OpenAPI with Swagger UI, and a working Alpine Docker image Oct 25, 2026
@evgTSV
evgTSV merged commit cbe875b into main Oct 26, 2026
4 checks passed
@evgTSV
evgTSV deleted the deck-note-services branch October 26, 2026 09:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant