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 aPolicyUpdate, then callsrefresh()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→ aLazyColumnof device cards (or “No devices enrolled yet.”).- Each
DeviceCardshows 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 toggleSwitchthat calls back intoupdatePolicy.
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():
- Desktop —
app/desktopApp/src/main/kotlin/.../main.kt: Compose Desktopapplication { Window(title = "MDMCore") { App() } }. - Web —
app/webApp/src/webMain/kotlin/.../main.kt:ComposeViewport { App() }, rendering into the browser. - iOS —
MainViewController.kt: returns aComposeUIViewController { App() }for Swift to embed.
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.