Repository navigation
Bellhop
Bellhop is the Android companion app for Front Desk. Pair a phone once and it becomes a pocket view of your fleet: live member health, request traffic, the fleet event log, and, for operator devices, the controls to drain a member, activate it again, or push a config sync. Notifications tell you when something changes and open the app when you tap them. Bellhop talks only to Front Desk, never to Model Hotel members directly, so it holds no provider credentials and no member admin tokens. It authenticates with its own device token that either side can revoke at any time, and like the rest of Model Hotel it moves only routing and metering metadata, never prompt or response content.
Three surfaces cover day-to-day monitoring. The dashboard is the linked home screen: it names the Front Desk you paired with, carries a strip of provider quota badges, shows a fleet health banner, and lists every member as a card with its health dot, address, latency, Traefik status, version, a request sparkline, and its latest event. Tapping a card opens the member detail screen with a full request-traffic graph, the member's metadata, operator controls, and its recent events. The top-bar log icon opens the fleet-wide events screen. A fourth surface never needs opening at all: the home-screen widget keeps the fleet, its badges and the latest event on your launcher. Tap any screenshot on this page to view it full size.
Linking always starts on the Front Desk side, so an operator stays in control of which phones can see the fleet and what they can do.
Open Settings, then Paired devices, pick the new device's role, and generate a pairing code. The role sets the permission ceiling that Front Desk enforces on that device's token: a Monitor device is read-only, while an Operator device can additionally drain and activate members, trigger a config sync, toggle auto-sync, switch individual alerts on or off, run a fleet version check, and reset a failover group's circuit breakers fleet-wide. The code renders as a QR image alongside a copyable pairing string; both carry the same payload (the Front Desk URL, a one-time code, and a display name). Codes are single-use and expire three minutes after they are generated, and they live only in Front Desk's memory, so a restart voids any that are still outstanding. The panel dismisses a code on its own once a device pairs with it.
Every paired device stays listed in the panel with its role and last-seen state, and an operator can revoke any of them with one tap, which invalidates that device's token immediately.
On the phone, either tap Scan QR code and point the camera at the code (Bellhop asks for camera access the first time, and points you back at the paste field if the camera cannot be opened), or copy the pairing string and paste it into the field. Bellhop parses it, shows which Front Desk you are about to link so you can confirm it is the one you meant, and lets you rename the device before pairing. Tap Pair and Bellhop exchanges the one-time code for a device token. Front Desk returns that token exactly once; Bellhop stores it encrypted at rest with the Android Keystore (AES-GCM) and never displays it again.
A device can never do more than its role allows, because Front Desk enforces the ceiling on the token itself rather than trusting the app. A Monitor device sees everything (health, traffic, events, and alerts) but cannot change anything. An Operator device gets the same read access plus the operator controls, and those writes are additionally gated behind a biometric or device-PIN prompt on the phone, so a borrowed or unlocked device still cannot drain a member without the owner present. One prompt authorizes a short burst of operator taps (about a minute) before the next action asks again, and the window resets whenever the app is killed.
The dashboard is the linked home screen shown at the top of this page. A banner summarizes fleet health at a glance ("All members up", or a count when something is down), and a strip of provider quota badges sits above it. Each member card carries a colored health dot, the member name, the member address, a compact status line (reachability, latency, Traefik state, and running version), a request-traffic sparkline drawn only when the card is on screen to save battery, and the member's most recent event with a relative timestamp.
The primary member's card names its role with a Primary badge and, beside it, an Auto-sync on or Auto-sync off badge saying whether the fleet's members track the primary's config. On a monitor device that badge is a readout; on an operator device it is a button that opens the auto-sync switch in a bottom sheet, so a setting worth changing once in a blue moon does not sit on the dashboard as though it needed watching. Pull to refresh forces an immediate poll. If Front Desk stops accepting the device's token, usually because an operator revoked it, a banner across the dashboard, member, events and alerts screens says the device can no longer authenticate and tells you to unlink and pair again with a fresh code.
The badge strip reports what is left of each provider's quota, straight from the same readings the Model Hotel dashboard shows: a short provider code and one headline number per provider, in that provider's own brand color, packed as tightly as the screen allows. A provider whose quota is spent draws its number in the warning color instead, so an exhausted provider stands out of the strip without opening anything. Tap a badge for a detail sheet with the full picture: a meter bar per metered reading (with per-model grouping where a provider reports it that way), and the flat facts underneath, such as membership level, parallel-request limit, or reset time. A reading with no ceiling to measure against gets a plain row rather than a bar, since a bar nobody can fill is decoration.
Badge style and which providers appear is yours to set, per surface, in Settings β Quota badges: reorder the providers, hide the ones you do not care about, and choose whether a badge counts what you have used or what is remaining. The dashboard and the widget keep separate lists, so a phone can carry eight badges while the widget carries the two you actually glance at.
Tapping a card opens the member. The top of the screen is a request-traffic graph over the window you chose in Settings (requests and errors are drawn as separate series with their own legend, and the time axis spans the selected range). Below it sit the member's address, running version, and when it was added to the fleet. The Operator controls section (present only on operator devices) offers Drain to bleed traffic off a member, Activate to bring a drained member back, and Sync fleet config to push the primary's config out; each action asks for the biometric confirmation described under Roles and reports back whether Front Desk accepted it. A Recent events list closes the screen, with its own date-range chips so you can narrow it to the last hour or open it up to all time.
The events screen is the fleet-wide log. A header counts the events in view, and a row of range chips (1h, 24h, 7d, 30d, All) plus a calendar picker scope the list. Each entry shows a human title and one-line summary, a severity (Error, Warning, Info, or Success) shown as a colored edge, and the raw event type with its source and member (for example health.down from frontdesk-poller on a given member). The log covers member health transitions, version read failures and recoveries, config syncs (manual and automatic), and device pairing and revocation, so it doubles as an audit trail of everything Front Desk noticed.
The widget answers "is the fleet fine?" without opening anything. It carries the Front Desk's name, a member count badge (2/2 above), a row per member with its health dot and state, the quota badge strip, and the latest fleet event with its time and date. Rows are drawn for up to five members; a larger fleet collapses to up, down and drained counts instead. A footer stamps how old the reading is ("as of 09:01") beside a refresh button, because a widget that cannot say when it last spoke to Front Desk is worse than no widget, and it flags Sync stale when the fleet's config is at risk of drifting from the primary's, or Monitoring off when background checks are disabled. Before a phone is paired the widget simply reads "Open Bellhop to pair".
It never polls on its own. The widget draws whatever the app or the background check last wrote, which is what keeps it free: no extra battery, no extra requests. Tapping refresh forces a read right then, and background monitoring keeps it current on its own schedule. Tapping the widget anywhere else opens the app; tapping a quota badge opens that provider's detail view where there is one to open, and otherwise raises a toast with the provider's full name, since a badge on a widget has no room to spell out "openrouter-personal".
Resize it and the contents follow: member rows stack to the height you give it, and the badge strip repacks to the width, fitting each badge to its own label and marking anything that did not fit with a +N so a trimmed strip never pretends to be the whole picture. Three settings under Settings β Home-screen widget decide what it carries and how it sits: the per-member Traffic graphs overlay, the Quota badges strip, and Badge alignment (left, center, or right). The two switches are worth knowing the cost of, since each adds a read to every background check, and turning one off removes that read rather than merely hiding the result.
The Alerts screen shows what Front Desk raises alerts for and, on operator devices, lets you change it. Events are grouped (Health, Config Sync, and so on), each with a severity badge and a switch; flipping a switch enables or mutes that alert on Front Desk right away, so the phone acts as a remote control for the fleet's alerting policy rather than just a viewer of it. A Notification delivery panel at the top reports whether an outbound channel (such as an Apprise target) is configured on Front Desk. Monitor devices see the same screen read-only.
Settings gathers the device-side preferences.
- Linked Front Desk: which Front Desk you paired with, the name and role you linked as, and the date. Long-press it to copy.
- Hold to copy: whether long-pressing an event or member cell copies it to the clipboard.
- Home-screen widget: Traffic graphs overlays each member row with its last hour of requests, Quota badges carries the badge strip, and Badge alignment puts that strip left, center, or right. The two switches each cost the background check an extra read while they are on, which is why each says so and why each can be turned off on its own.
- Time format: the clock every time in Bellhop is drawn on (follow the device, or force 24-hour or 12-hour).
- Traffic graph range: how far back the request charts reach (1h, 3h, 6h, 12h, or 24h).
- App lock: requires a fingerprint or device PIN to open Bellhop, with a Lock after inactivity window of Now, 1 min, 5 min, 15 min, 30 min, or 1 hr. On a device with no fingerprint or screen lock set up the switch is unavailable, and the gate opens rather than shutting you out of your own fleet view.
- Background monitoring: checks the fleet every fifteen minutes and notifies you when a member goes down or recovers, and on the Front Desk alerts switched on under Alerts, even while the app is closed.
- Real-time push: wakes Bellhop the instant Front Desk pushes an alert, over UnifiedPush and ntfy, with no Google dependency and no polling delay. Opt-in.
- Battery: reports whether Android is letting Bellhop run in the background at all, and offers to fix it when it is not. Some phones (OnePlus, Xiaomi, and others) need Bellhop allowed in their own per-app battery and auto-start settings as well.
- Quota badges: opens the badge picker described above.
- Alerts: opens the alert policy.
- Language: the system default, English, or one of ten hand-translated locales.
- Unlink: the last item on the screen, covered under Unlinking.
Bellhop keeps you informed with two independent layers so a phone does not have to stay open. Background monitoring is a scheduled worker that polls Front Desk every fifteen minutes and raises a per-severity notification when a member's health changes, which works everywhere without any push infrastructure. Real-time push adds low-latency delivery on top: when Front Desk raises an alert it pushes a wake to the phone over UnifiedPush and ntfy, Bellhop runs an immediate check, and you get the notification within seconds rather than at the next poll. Both layers also deliver every Front Desk alert you have switched on under Alerts (a member drained, a sync held, a stale backup, and so on) as Front Desk's own message, so the toggles there decide what reaches the phone. Push is fully optional and self-hosted-friendly, so you can run Bellhop with no Google services at all and still get timely alerts.
Real-time push uses UnifiedPush with ntfy as the transport, so there is nothing to type into Bellhop and no Google dependency. You install a distributor app, flip one switch, and hand the topic Bellhop generates to Front Desk.
- Install a UnifiedPush distributor. Install the ntfy Android app from F-Droid or Play. It holds the persistent connection and receives pushes; Bellhop has no push transport of its own. If you self-host ntfy, point the ntfy app at your server in its own settings. Without a distributor installed, Bellhop's push section stays idle and tells you to add one.
- Turn on Real-time push. In Bellhop Settings β Real-time push, flip the switch. Bellhop registers with the distributor and, a moment later, shows a Push topic for Front Desk with a copy button. (If Android notifications are off for Bellhop, grant them so pushes can arrive.)
-
Point Front Desk at that topic. In Front Desk β Settings β Alerts, press Set up alerts (or Add destination if alerts are already configured), pick the Bellhop tile, and paste the whole Push topic for Front Desk string into the one box. Front Desk splits it into server and topic, composes the Apprise URL, and the next step sends a test through it. Nothing is saved until Finish. This needs an
apprise-apicontainer in the Front Desk stack; Alerting has the one-line compose addition and the full pipeline.
Once wired, any Front Desk alert wakes Bellhop within seconds. Bellhop trusts nothing in the push itself: it treats it as a wake, runs a fresh fleet poll that also reads back Front Desk's event log, and notifies from that record, so the notification says the same sentence you would read in the Front Desk event log rather than a stale or reshaped payload. The alerts that notify are the ones switched on under Alerts; Bellhop's own notifications for a member going down or recovering and for auto-sync drift stay on regardless, and it never posts both its own row and Front Desk's for the same outage, so one outage is one notification. The first check after you enable a layer only records where the event log stands, so a backlog of old alerts does not arrive as though it were new. A Send test from Front Desk shows a "push test received" notification on the phone, which is how you confirm the whole pipeline works. If you only want plain ntfy notifications without Bellhop's fleet view, skip steps 1 and 2, pick the Phone (ntfy app) tile instead, let Front Desk generate the topic, and subscribe to it directly in the ntfy app.
Bellhop inherits Model Hotel's privacy posture and adds device-level protection. It never contacts Model Hotel members directly, so it holds no provider API keys and no member admin tokens; the only secret on the phone is its own device token, stored encrypted with the Android Keystore and never shown after pairing. Operator actions are gated behind a biometric or device-PIN prompt (one prompt covering about a minute of taps), and the whole app can be locked the same way. Front Desk enforces the device's role on the server side, so a Monitor token cannot be tricked into an operator action, and any device can be revoked instantly from either the phone (Unlink) or the Front Desk panel. As with the gateway, Bellhop moves only routing and metering metadata, never the content of requests or responses.
Unlink from Settings, at the bottom. Bellhop confirms first, then clears its local token and asks Front Desk to revoke it, so the device stops monitoring and can no longer act on the fleet. You can pair the same phone again anytime with a fresh code. If Front Desk cannot be reached, Bellhop says so and leaves the device linked with nothing revoked; you can retry, or unlink anyway and then revoke the device from the Front Desk panel, which Bellhop tells you to do so a live token is never left behind. An operator can also revoke the device from the Front Desk Paired devices panel without touching the phone, which is the path to use for a lost or stolen device.
The quickest way onto a phone is the signed APK on GitHub Releases. The bellhop-latest tag always points at the newest build, each release is also tagged bellhop-vX.Y.Z, and the release layout is Obtainium-compatible, so you can point Obtainium at the repository once and let it pick up every new version. The repository README carries the same download badge and link.
To build it yourself, Bellhop lives in android/: a Kotlin and Jetpack Compose app targeting Android 8.0 (API 26) and up, built with Gradle on JDK 21. Run make android-build from the repository root (it pins JDK 21 and the Android SDK path, and both are overridable) or ./gradlew assembleDebug from android/. A local ./gradlew assembleRelease produces an unsigned APK: release signing happens only in CI, which decodes the keystore from a repository secret. See the android/ README for the full build steps.
Last synced from hugalafutro/model-hotel@6affc90 on 2026-10-06 16:16 UTC. Edit these pages under wiki/ (and images under docs/screenshots/) in the main repo, not here.
- π¨ Home
- βοΈ Configuration
- π Development
- π Virtual Keys
- π₯ Multi-User
- π API Reference
- π Request Logging
- π Model Discovery
- π Failover and Hotel Routing
- π Alerting
- π Observability
- π§ High Availability
- π± Bellhop
- π§± CrowdSec













