- Kotlin 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Build and Publish APK / build (push) Successful in 3m47s
The module UI schema was cached until the app process died, so a server-side change — a module updated in place, or a thunk-registered schema reshaping itself — kept rendering as yesterday's page for as long as the app stayed alive. The cache now expires after a minute, which keeps navigation inside a module free while a redeploy shows up promptly; a refetch that fails falls back to the stale copy, because an old page beats an error where a page used to be. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
| .forgejo/workflows | ||
| app | ||
| gradle | ||
| .gitattributes | ||
| .gitignore | ||
| build.gradle.kts | ||
| CLAUDE.md | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| README.md | ||
| settings.gradle.kts | ||
OpsDeck Mobile
Native Android client for OpsDeck — Kotlin + Jetpack Compose (Material 3), no cross-platform layer, fully module-agnostic.
How it renders modules
The app contains zero module-specific code. Every module — built-in or
third-party — registers a portable UI schema on the server
(ctx.ui.register(...), see core/docs/modules.md), and the app renders it
natively:
-
GET /api/core/modules— discover enabled modules (hasUiflag) -
GET /api/core/modules/<name>/ui— fetch the module's page schema -
Blocks (
fields,table,list,chart,usage,tree,logText,actions,form) are rendered with native Compose components; data binds by dot-path into the module's REST responses under/api/mod/<name>/… -
Charts combine
/api/core/metrics/queryhistory with live-channel ticks; pages re-fetch on the schema'srefresh.topics(pushed over the live channel;intervalSeccovers channel downtime);logTextblocks with afollowtopic stream appends from a live-channel provider (e.g. docker log tails);actionsPOST with optional confirmation and show the work's progress. A repeatedkeyREPLACES that entity's row instead of appending, which is what keeps a multi-GB image pull from filling the card with thousands of near-identical frames; events without one are plain text.An action's progress arrives one of two ways, and the RESPONSE decides which — no schema field selects it:
{ runId }— the route started a host-owned task run and the work outlives the request. The app followscore:task:<runId>, a retained topic: the snapshot is the whole run so far, deltas follow it, and a reconnect replays the gap. The run ends when its status is terminal, so backgrounding the app and returning resumes rather than loses it, and following a run that already finished works the same as one just started. Fields a viewer may not see arrive empty, named inwithheld; the progress card names them rather than reading as a silent run.- a schema
progressTopic(deprecated) — at-most-once{msg}frames whose end is inferred from a line starting "done" or "error". Kept for servers and modules that have not migrated.
-
Detail pages (
stack/:project) are navigated from listlinks with param interpolation; unknown block types from newer servers degrade to a hint instead of failing the page
A new module dropped onto the server appears in the app with zero app changes.
Server contract
- Auth: OIDC browser login only (opaque
opsdeck_sessioncookie, no token endpoint). The app opens/auth/login?app_challenge=<S256>in a Custom Tab (system browser, so passkeys work); the server redirects toopsdeck://auth?code=…and the app trades code + PKCE verifier for the session id atPOST /auth/app-exchange, then attaches it natively. 401 anywhere returns to login.OPSDECK_AUTH=disabledskips login. - Every non-GET carries
x-opsdeck-csrf: 1. - One shared WebSocket to the live push channel at
/api/core/live: per-topic subscriptions with acks and a heartbeat, held only while a screen collects the topic — the server produces data only for what is actually visible. Delivery is at-most-once: after a reconnect (exponential backoff) every subscription re-fetches its REST baseline. Requires a server with the live channel (core ≥ the version that ships/api/core/live); there is no SSE fallback. - Alert push (optional, admins): content-free FCM data message wakes the
app, which fetches
/api/core/notificationsand builds local system notifications — alert content never passes through Google. No Firebase config is compiled in: the app fetches it from/api/core/push/configafter admin login and initializes Firebase at runtime; servers without push configured fall back to live-channel/poll updates while the app is open. Setup (server-side only):core/docs/push-notifications.md. - System info (optional, admins):
GET /api/core/systembacks the unlisted System screen — versions, runtime facts, and which modules loaded versus why they did not. It is reached by tapping the server URL in Settings five times, and then takes over the navbar's module slot for as long as it is open — the same way the web shell shows it in the sidebar only while active. Servers without the endpoint answer 404 and the screen says so. Web equivalent:/system.
Build
./gradlew assembleDebug # -> app/build/outputs/apk/debug/app-debug.apk
./gradlew installDebug
Requires JDK 17+ and Android SDK platform 36 (local.properties, not
committed).
Development against a local server
cd ../core
OPSDECK_AUTH=disabled OPSDECK_FAKE_DATA=1 OPSDECK_MODULES=hardware,zfs,docker,demo deno task start
App server URL: http://10.0.2.2:8080 (emulator) or http://<lan-ip>:8080
(device). Cleartext HTTP is allowed in the manifest for the self-hosted/LAN
case.
Architecture
data/
model/ core DTOs (User, ModuleInfo, Notification, metrics) +
Ui.kt — schema models (polymorphic blocks, unknown-safe)
JsonPath.kt dot-path resolution into arbitrary module JSON
net/ ApiClient (cookie/CSRF interceptor, generic moduleGet/
modulePost/moduleUi), Retrofit core API, LiveClient
(live push channel WebSocket: topic subs, acks, resync)
SettingsRepository DataStore: server URL, session cookie, push config,
chart range, update registry, notification markers
ui/
AppViewModel root state machine: NeedsServer -> NeedsLogin -> Ready
nav/ RootNav + MainScaffold (static tabs: Home, last-used
module, Alerts for admins, Settings)
push/ FCM wake-up handling, local alert notifications (per-level
channels, grouped, dismiss action syncing back to the server)
schema/ SchemaViewModel (page match, data fetch, live refresh,
log follow, charts, actions) + SchemaScreen (all block
renderers)
common/ LineChart (Canvas), formatters incl. schema formats, cards
home/ notifications/ settings/ setup/ login/ core screens