Console — the operator UI

The console is a Compose Multiplatform app that lists every device’s compliance status and lets an operator edit the desired policy. The same UI code runs on desktop (JVM) and the web (JS or Wasm); iOS is wired to compile but parked as a viewer.

Source: app/shared/src/commonMain/kotlin/com/arganaemre/mdmcore/ plus the thin per-platform entry points under app/desktopApp and app/webApp.

It is a live UI, not a placeholder — it talks to the running server.

Talking to the server: ConsoleClient

ConsoleClient.kt is a Ktor client pointed at http://localhost:8081 (the console runs on the host, alongside the server). It uses the shared MdmJson for content negotiation, and uses two endpoints:

Call Endpoint Returns
fetchDevices() GET /devices List<DeviceStatus>
updatePolicy(id, PolicyUpdate) PUT /devices/{id}/policy the new DevicePolicy

The HTTP engine is chosen per target (the client core is shared, the engine is not): CIO on the JVM/desktop, the JS fetch engine on web (JS and Wasm). See the source sets in app/shared/build.gradle.kts.

State: ConsoleViewModel

ConsoleViewModel.kt is a multiplatform ViewModel exposing a StateFlow<ConsoleUiState>:

sealed interface ConsoleUiState {
    data object Loading : ConsoleUiState
    data class Loaded(val devices: List<DeviceStatus>) : ConsoleUiState
    data class Error(val message: String) : ConsoleUiState
}
  • It calls refresh() on construction to load the roster.
  • updatePolicy(deviceId, cameraDisabled) sends a PolicyUpdate, then calls refresh() again so the list repaints with the bumped version — which is when you’ll see the device flip to STALE until the agent catches up (see the loop).
  • Failures surface as ConsoleUiState.Error.

What it renders: App

App.kt builds a Material 3 UI:

  • A “MDM Console” header with a Refresh button.
  • Loading → a spinner; Error → the message; Loaded → a LazyColumn of device cards (or “No devices enrolled yet.”).
  • Each DeviceCard shows the device id, the compliance state as a color-coded label (UNKNOWN gray, STALE orange, NON_COMPLIANT red, COMPLIANT green), the desired policy (camera=… (v…)), the last reported state (camera + applied version, or “No report yet”), and a camera toggle Switch that calls back into updatePolicy.

So the operator’s whole workflow — see who’s drifted, flip a policy, watch it propagate — is on one screen.

The expect/actual platform pattern

The console is also the canonical example of Kotlin’s expect/actual. commonMain declares the contract and each target supplies its own implementation (Platform.kt):

interface Platform { val name: String }
expect fun getPlatform(): Platform
Target File name resolves to
Android Platform.android.kt "Android ${Build.VERSION.SDK_INT}"
JVM/desktop Platform.jvm.kt "Java ${java.version}"
JS Platform.js.kt the browser parsed from the user-agent
Wasm Platform.wasmJs.kt "Web with Kotlin/Wasm"
iOS Platform.ios.kt UIDevice system name + version

You must provide an actual for every target source set or the build fails. More on the pattern: architecture.md.

Entry points

Each platform has a tiny launcher that just hosts the shared App():

Running it

# Desktop (JVM)
./gradlew :app:desktopApp:run
./gradlew :app:desktopApp:hotRun --auto      # with Compose hot reload

# Web — Wasm (faster, modern browsers)
./gradlew :app:webApp:wasmJsBrowserDevelopmentRun
# Web — JS (broader browser support)
./gradlew :app:webApp:jsBrowserDevelopmentRun

Each of these is a long-lived Gradle task that holds the project lock — start the server first (or from a separate checkout). See development.md.

Compose resources

Shared resources live in app/shared/src/commonMain/composeResources/ and are reached through the generated Res class (package mdmcore.app.shared.generated.resources). The console currently leans on Material 3 built-ins; the only bundled resource is the template compose-multiplatform.xml drawable. Generated code under app/shared/build/ is not source — never edit it.