- C# 38.7%
- JavaScript 23%
- HTML 21.3%
- CSS 11.8%
- Shell 2.9%
- Other 2.2%
Every font size outside Home is on the draft's scale in px (no rem left), every button is one of three sizes (34px regular, .btn-sm 28px, .btn-lg 44px) set by a class in the markup instead of per-page copies, and What's new, Report a problem and the calendar's panels wear the inspector's dialog language with one shared CloseButton. The board's columns narrow beside a docked card, down to 240px, instead of running under it. Board settings says Delete, not Delete card. Net -390 lines of CSS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|---|---|---|
| .claude/skills | ||
| .forgejo/workflows | ||
| deploy | ||
| design | ||
| docs | ||
| src/Ponente.Kanban | ||
| tests | ||
| tools | ||
| .dockerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| docker-compose.test.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| Ponente.sln | ||
| README.md | ||
Ponente
A small self-hosted Kanban board for a private member group, built on the Atmosphere (AT Protocol) network. It holds any number of boards over one membership. Members sign in with an existing Atmosphere account, or with an account on the group's own bundled PDS (invite-code only). Everything the app stores is encrypted at rest.
Features
- Kanban boards, each at its own link. Every member can make boards, and each board is open to every member or shared with the ones its admin picks. A board you were not given simply does not appear.
- Atmosphere OAuth sign-in, gated by a DID allowlist.
- Its own federated PDS (the official
ghcr.io/bluesky-social/pds). Members without an Atmosphere account sign up with a single-use invite code, and their accounts federate with the network. - Access requests: a public
/requestform with a secret status link and an admin approval queue. - Encryption at rest: all user-supplied content is AES-256-GCM encrypted before it reaches Postgres, and OAuth tokens use ASP.NET Data Protection. PDS data lives in the PDS's own volume.
- Cards with safe markdown, drag and drop, tags, assignees, comments and due dates.
- Calendar (
/calendar): a month grid and a per-tag timeline, with.icsexport and subscription. - Live updates across tabs and members within about a second.
- Installable as an app, in English and German.
Stack
.NET 10 Blazor Server · EF Core 10 + Postgres 17 · ATProto.NET 0.8.0 (OAuth with DPoP, PAR and PKCE; PDS administration) · Markdig · SortableJS (vendored)
Documentation
CLAUDE.md is the router and docs/orientation.md is the ten-minute tour. CLAUDE.md maps each area to the page in docs/ that explains why it is built the way it is. The deployment runbook is deploy/README.md.
First run
- Put your DID in
Allowlist__Admins__0indocker-compose.yml(look it up at web.plc.directory), or inappsettings.Development.jsonfor host-side dev. - Start the stack with
podman compose up --build(ordocker compose). The app runs onhttp://localhost:8090and the PDS onhttp://localhost:8091. After code changes useup -d --build --force-recreate, because--buildalone keeps the old container.podman compose down -vstarts over. - Sign in with your Bluesky username. The Members page in the account menu adds existing Atmosphere accounts and mints invite codes for the bundled PDS.
Development
podman run -d --name ponente-db \
-e POSTGRES_DB=ponente -e POSTGRES_USER=ponente -e POSTGRES_PASSWORD=ponente \
-p 5432:5432 docker.io/library/postgres:17-alpine
cd src/Ponente.Kanban && dotnet run # http://127.0.0.1:5000
Migrations apply on startup. The PDS runs only in the compose stack (podman compose up pds).
dotnet build Ponente.sln && dotnet test Ponente.sln # unit tests
podman compose -f docker-compose.yml -f docker-compose.test.yml up -d --build --force-recreate
cd tests/e2e && npx playwright test # e2e
Configuration
| Env var | Description |
|---|---|
ConnectionStrings__KanbanDb |
Postgres connection string |
Encryption__Key |
Required. Base64 32-byte key (openssl rand -base64 32). Losing it loses the data. |
Allowlist__Admins__0 |
Bootstrap admin DID, seeded on first run |
AtProto__BaseUrl |
Public base URL; the OAuth redirect URI derives from it (a loopback IP in dev) |
AtProto__ProfileScope |
granular (default: write the profile record and upload an image), generic (transition:generic, for older account servers) or none (sign-in only). See docs/auth.md. |
Pds__Url |
Internal PDS URL (e.g. http://pds:8080). Unset hides signup and invites. |
Pds__PublicUrl / Pds__HandleDomain |
Public PDS URL and handle suffix (e.g. .ponente.app) |
Pds__AdminPassword |
Shared secret for the PDS admin endpoints |
AccessRequests__RateLimit |
Per-IP submissions per 15 minutes on /request (default 5) |
Mail__SmtpUrl |
e.g. smtps://user%40host:pass@mail.example.com:465/ (percent-encode the @). Unset sends no mail. |
Mail__FromAddress / Mail__FromName |
Sender. The address defaults to an address-shaped SMTP username, the name to Ponente. |
Mail__AdminRecipients |
Recipients of new-request notifications, comma-separated |
Feedback__Repository / Feedback__Token |
Forge repository (browser URL) and token for "Report a problem". Both are required, or the row is hidden. |
Mail is optional and best-effort: new access requests go to admins, and an approval mails the requester their invite if they left an address. What is sent and why is in docs/access-requests.md.
The PDS takes the PDS_* variables in the compose files (see deploy/.env.example). Back up the PLC rotation key, because it controls account recovery. Set PDS_EMAIL_SMTP_URL and PDS_EMAIL_FROM_ADDRESS, or password resets silently fail. PDS_HANDLE_DOMAIN lets handles sit on the apex domain (alice.ponente.app) while the PDS runs on pds.ponente.app. Handle certificates come from Caddy's on-demand TLS, gated by an ask endpoint (docs/auth.md, deploy/README.md).
Releases and deployment
git tag vX.Y.Z && git push origin vX.Y.Z
That is the whole release. CI builds and pushes git.grandiras.net/grandiras/ponente-kanban:vX.Y.Z (and :latest), and a systemd timer on the server recreates the app within a minute. Pushes to main publish :main, which the dev stack (dev.ponente.app) follows. ./deploy/deploy.sh [vX.Y.Z] deploys from a workstation over the server's podman socket; pinning a version stops auto-updates, which is how rollbacks hold. Setup, prerequisites and rollbacks are in deploy/README.md and docs/deployment.md.
GET /healthz on both services returns 200 OK.