Repository navigation
Description
Status: undecided. This is a product decision of the maintainer; nothing here is scheduled.
What and why
Decide whether NOVA should alert the user outside the app (a notification when the server reports something important), and if so by which route. This would reverse the 0.1.42 decision of no background polling.
User demand: "I would love being able to get push notifications for major issues." (r/unRAID, 2025-03-01, https://www.reddit.com/r/unRAID/comments/1j0ston/ , post with 173 points). "Push notifications, widgets, reliable background refresh are a few reasons for app vs mobile web." (2025-08-05, https://www.reddit.com/r/unRAID/comments/1mi1v85/). Counter-voice: "I'm put off by the idea of my stuff running through Discords servers." (6 points) and a note that Unraid already pushes through many services under Settings - Notifications (70 points), https://www.reddit.com/r/unRAID/comments/1wkolt8/ (2026-09-19).
Other apps (their own descriptions):
- U-Manager: a server-side agent posts to a Cloudflare Worker run by its developer, which pushes through Firebase Cloud Messaging (https://ca.unraid.net/apps/u-manager-push-notifications-0eryqfx1olt49d).
- Unraid Deck: push through APNs via a Cloudflare Worker (https://ca.unraid.net/apps/unraid-deck-0pm0cs30j7fmhz).
- Quartermaster: optional relay or the user's own ntfy (https://apps.apple.com/us/app/quartermaster-homelab-stack/id6779994284).
What NOVA has today
No alerts outside the app. Background polling was removed in 0.1.42 (HANDOFF.md). There is no WorkManager or NotificationCompat use in the code. Inside the app, the notification inbox works (mutations.graphql notification actions).
API side
Route (a) uses no NOVA API calls: Unraid's built-in notification agents send to a service the user runs. Route (b) would poll GetNotifications (queries.graphql:96) from a background job.
Options
- (a) UnifiedPush through the user's own ntfy server, fed by Unraid's notification agents. NOVA only receives the message and opens the right screen. No NOVA relay, no Google services. Feasibility is unchecked (UnifiedPush distributor on the phone, ntfy topic setup, deep link into the right screen).
- (b) An opt-in WorkManager check. Simple, but Android limits periodic work to at least 15 minutes, it needs the app to hold a key in the background, and it reverses the 0.1.42 decision.
- (c) No alerts, and document how to use Unraid's own agents with the user's preferred notification app.
Recommendation of the market overview
Yes: UnifiedPush/ntfy first (a), after a short feasibility spike. This is the overview's opinion, not a decision.
Constraints from NOVA's principles
Official API only; no server plugin; no relay run by the project; no Google services (so no FCM); least-privilege keys.
Open questions
- Is any alert feature wanted at all?
- If (a): is a documented recipe enough, or should NOVA parse the ntfy message and deep-link?
- If (b): what is the battery and privacy cost the maintainer accepts?
Acceptance criteria (after a decision)
- The maintainer records the decision (option, or "no") in an ADR.
- If (a) or (b): a spike result confirms feasibility before a feature issue is opened.
Size
L (if built); S for the spike.
Activity
nofuturekid commentedon Nov 13, 2026
Decided by the maintainer (2026-11-13): yes, NOVA gets alerts outside the app, as an opt-in. Background checks run only while notifications are switched on; with the switch off (the default) the app does no background work at all, as today. The design (events, check interval, permission flow, privacy policy, routes without Google services and without a relay) follows in this issue before any code.
nofuturekid commentedon Nov 13, 2026
Design (2026-11-13)
Decisions of 2026-11-13 (the maintainer, or the coordinator on the design's own recommendations by his standing rule):
Section 10 of the design ("Product questions") is therefore answered; its recommendations stand.
Planned pull request split (section 7; order: PR 0 any time, PR 1 after #336 is on main, PR 2 on PR 1, PR 3 on PR 2, PR 4 together with PR 3):
The full design follows in two comments (part 1 of 2: sections 0 to 6; part 2 of 2: sections 7 to 10 and sources).
Alerts outside the app: implementation design (issue #321)
Status: design for review, written 2026-11-13. Nothing here is built.
Fixed by the maintainer (issue #321, comment 2026-11-13): opt-in, background work only while notifications are switched on, default off.
Fixed by the project's principles: official Unraid API only, no server plugin, no relay or push service run by the project, no Google Play services, least-privilege (read-only) key, no telemetry.
Every factual statement below carries its source. "Decision" marks an engineering choice made here, with its reason.
0. Corrections to the starting assumptions
POST_NOTIFICATIONStoapp/. 0.1.42 fixed a bug: three foreground screens kept polling while the app was in the background. The rule that alerts must amend is ADR-0017 ("backgrounded app = zero polls, period").git log --all -S POST_NOTIFICATIONS -- app/→ empty;CHANGELOG.md:193-195"No more background polling — Settings → Server plugins, Network interfaces and the container Logs tab kept refreshing ... while the app was in the background";docs/adr/0017-domain-split-queries-with-lifecycle-polling.md"All gating logic ANDs withappVisible— so backgrounded app = zero polls, period."unraid/webguiemhttp/plugins/dynamix/agents/ntfy.sh.xml:33-39(plaincurl -d "$MESSAGE")feat/needs-attention-card, headc79e7f1), open.timeout 120 gh pr list -R hellwand/nova --state open→{"number":336,"headRefOid":"c79e7f16..."}1. Summary of the design
PeriodicWorkRequest, unique namenova-alert-check) runs every 30 minutes by default (15 min, 30 min, 1 h or 3 h selectable). It asks every saved server two existing, already shipped read-only queries (GetNotifications,GetArray) and posts Android notifications for:attentionItems(), posted when the condition appears, not while it lasts).2. Mechanism
2.1 Periodic check with WorkManager (build now)
Platform facts (verified):
androidx/androidxwork/work-runtime/api/current.txt:379-380:MIN_PERIODIC_FLEX_MILLIS = 300000L,MIN_PERIODIC_INTERVAL_MILLIS = 900000LRECEIVE_BOOT_COMPLETED,WAKE_LOCK,ACCESS_NETWORK_STATE,FOREGROUND_SERVICEand aBOOT_COMPLETEDRescheduleReceiverthat is disabled by default and enabled only while unfinished work exists. No own boot receiver is needed.work/work-runtime/src/main/AndroidManifest.xml:20-23(permissions),:54-63(android:enabled="false",BOOT_COMPLETED);UnfinishedWorkListener.kt:73-74setReceiverEnabled(appContext, RescheduleReceiver::class.java, hasUnfinishedWork)ForceStopRunnable.java:70-71"Alarms and Jobs get cancelled when an application is force-stopped."RTC_WAKEUPalarm at every app start (force-stop detection).ForceStopRunnable.java:87TEN_YEARS,:423-425alarmManager.setExact(RTC_WAKEUP, triggerAt, pendingIntent)Decisions:
PeriodicWorkRequest(enqueueUniquePeriodicWork("nova-alert-check", UPDATE, …)), aCoroutineWorker.UPDATEapplies a changed interval or network option without restarting the period.default.cfg:53system="*/1 * * * *"); the phone only needs to notice a result. 30 min keeps 48 checks/day; a wide flex lets Android batch the job with other apps' jobs.NetworkType.CONNECTED;UNMETEREDwhen "Only on Wi-Fi" is on. NorequiresBatteryNotLow, norequiresCharging.Result.success(); neverretry().notificationAddedsubscription, ADR-0026) would need a permanent foreground notification.RescheduleReceiverreschedules after boot (it receivesBOOT_COMPLETED, which arrives after the first unlock).AlertScheduler.reconcile()runs on every app start (fromMainViewModelinit) and re-enqueues withUPDATEwhen the switch is on, or cancels when it is off.androidx.work.WorkManagerInitializerfromInitializationProviderin the app manifest (tools:node="remove"on thatmeta-data), letNovaAppimplementConfiguration.Provider.androidx.work.workdb, no 10-year alarm, no force-stop bookkeeping. That is what "the app does no background work, as today" means in practice. The only cost is ~10 lines.CoroutineWorkerthat reads its dependencies through a Hilt@EntryPoint(EntryPointAccessors.fromApplication). Noandroidx.hilt:hilt-work.WorkerFactory; the house test pattern (internal constructor with lambdas,UpdateTracker.kt:50-56) still applies to the logic behind it.FOREGROUND_SERVICE<uses-permission android:name="android.permission.FOREGROUND_SERVICE" tools:node="remove" />, guarded by a manifest test likeNoInstallPermissionTest.WAKE_LOCK,RECEIVE_BOOT_COMPLETEDandACCESS_NETWORK_STATEstay (WorkManager needs them).REQUEST_IGNORE_BATTERY_OPTIMIZATIONS. Show the state (PowerManager.isIgnoringBatteryOptimizations, and the standby bucket viaUsageStatsManager.getAppStandbyBucket()on API 28+, neither needs a permission) and a button that opensSettings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS.storeflavour goes to F-Droid and later Google Play (ADR-0040); Play forbids the direct request unless the core function is affected (developer.android.com doze-standby page, as reported by the research lane; not re-fetched). The settings intent needs no permission and works in both stores.What the user must be told (in the settings section, plain English):
https://dontkillmyapp.comin the browser (user action only; NOVA itself makes no request).2.2 UnifiedPush or Unraid's own agents (do not build)
What Unraid offers (verified):
timeout 120 gh api 'repos/unraid/webgui/contents/emhttp/plugins/dynamix/agents' --jq '.[].name')EVENT,SUBJECT,DESCRIPTION,IMPORTANCE,CONTENT,LINK,TIMESTAMPas environment variables (scripts/notify:278).Priority,Icon(an image hosted onraw.githubusercontent.com),Authorization: Bearer $NTFY_TOKENandTitle, plus the body. It sends noClickheader and ignores$LINK(agents/ntfy.sh.xml:33-39).1= browser only (default.cfg:54-57entity="1" normal="1" warning="1" alert="1";scripts/notify:266($entity & 1)= browser,:277& 2= e-mail,:278& 4= agents).UnifiedPush: the spec requires RFC 8291-encrypted messages from an application server. Unraid has no such sender, and the project excludes a relay or plugin that could add one. A "wake-up only" path (Unraid's ntfy agent posting plain text to an ntfy UnifiedPush topic, NOVA then running a check) is outside the spec and untested; the agent always sends a Bearer header, and how the ntfy server treats it on an anonymous UnifiedPush topic is unverified.
Decision: no UnifiedPush code and no own issue for it. It would cost a distributor dependency and a second code path for an off-spec use, while a user who already runs ntfy gets instant alerts today with configuration only (Unraid agent → own ntfy server → ntfy app, which uses no Firebase for self-hosted servers per docs.ntfy.sh, as reported by the research lane). NOVA's documentation gets a short recipe in the documentation PR (section 6). Revisit only if Unraid gains a Web Push sender or a generic webhook.
Optional outward-facing step (needs the maintainer's explicit go): a small upstream PR to
unraid/webguiadding-H "Click: $LINK"to the ntfy agent, so a tap opens the server's web UI. Not needed for NOVA.3. Events
3.1 What Unraid already notifies about (verified)
Unraid's monitor (
emhttp/plugins/dynamix/scripts/monitor, runs every minute) creates notifications, each with a "returned to normal" counterpart:monitor:236monitor:271, 288monitor:315monitor:346-349if (_var($var,'fsState')!='Stopped')monitor:415monitor:379, 386monitor:447-i \"warning\"monitor:461-463$level = ($status == 0 && $var['sbSyncErrs'] == 0) ? "normal" : "warning"monitor:476monitor:503, 507monitor:538, 543Also: the API never fills
ParityCheck.errors(schema.graphqls, comment aboveenum ParityCheckStatus: "running,pausedanderrorsbelow are declared but the Unraid API never fills them"). So "parity check finished with errors" is only knowable from Unraid's notification.3.2 Events in version one (decision)
GetNotifications→notifications.warningsAndAlerts { id … importance … }attentionItemskeyarray-stopped)GetArray→array.state == STOPPEDvia PR #336attentionItemskeyarray-degraded; statesDISABLE_DISK,INVALID_EXPANSION,PARITY_NOT_BIGGEST,TOO_MANY_MISSING_DISKS)GetArray→array.state, mapped atGraphQlMapper.kt:527-528Why only these:
GetArraywould post every problem twice. (PR feat: a needs-attention summary on the Overview #336's card still shows them in the app.)monitor:346), and nothing notifies "array not started" (for example after a power cut with an encrypted array waiting for its passphrase).enum ContainerState { RUNNING PAUSED EXITED }); "was running, now stopped" is noisy because backup plugins, updates and the user stop containers on purpose, and PR feat: a needs-attention summary on the Overview #336 already rejected auto-start as a signal (Attention.kt:48-50). Unraid's own Docker image notifications still arrive through E1/E2.SettingsStore.kt:30,UnraidRepository.kt:1107-1110), and false alarms teach users to ignore NOVA. The settings line shows it instead. Revisit when a server has a remote address and both addresses failed for a long time, if users ask.Known noise the user accepts with the default: a scheduled parity check posts one WARNING when it starts (
monitor:447). The settings text names this; "Alerts only" removes it but also drops parity-errors and SMART warnings (product question 2).3.3 Detection and duplicate avoidance
Per server, a persisted
ServerAlertState:Rules (all in one pure function, section 3.5):
warningsAndAlertsthat is not inseenNotificationIdsand whose importance passes the level filter. Thenseen = (seen + all current ids), oldest dropped beyond 300. An id is never posted twice, even if it leaves the unread list and returns. Ids are the notification file names<event>-<timestamp>.notify(notifications.service.ts:925-926getIdFromPath = basename(path);scripts/notify:208,252$ticket = $timestamp), so a new occurrence of the same problem has a new id and is posted again, which is intended.attentionItemsand not inopenConditions; setopenConditionsto the current set. While it holds nothing is repeated; once it clears, the next occurrence is posted again.GetNotificationsfailed, keep the notification half of the state unchanged; ifGetArrayfailed, keep the condition half unchanged. A failed or unreachable check never changes state, so items created during an outage are posted when the server is reachable again, and nothing is re-posted.Caveats (verified, documented in the README):
scripts/notify:265-266) and the API's archive list shows it (notifications.service.ts:705-716). v1 reads unread only. Users who turned Browser off get no E1/E2.warningsAndAlertsis deduplicated by content and limited to 50 (notifications.service.ts:741-749). It returns the newest id for repeated identical content; beyond 50 distinct unread warnings and alerts, older ones are not seen. Acceptable for an alert feature./tmp/notifications(RAM) unless the user stores them on the boot drive (default.cfg:52); a reboot of the server clears unread items. Ids stay unique, so no re-post results.3.4 Several servers, addresses, failures
trustFor(mode, trustSelfSignedLocal, url, pin)(Tls.kt:147-160) withmode = Localfor the local URL andRemotefor the remote one, so pinned self-signed trust never applies to the remote address. Cost away from home: one failed connect (connect timeout 8 s,ApolloClientFactory.kt:56) before the remote address.Ok,Unreachable(all addresses failed),CertificateProblem(pinned certificate changed or untrusted: the check fails closed, never prompts and never pins;Tls.kt:125-126, every production caller passesacceptFirstUse = false,Tls.kt:87-91),KeyMissing/KeyUndecryptable(ApiKeyResult,ApiKeyStore.kt:42-46; never resets keys from the background, ADR-0035),Rejected(GraphQL or HTTP error, for example a revoked key).3.5 Shared evaluation for the Overview card (#319) and a widget (#322)
PR #336 adds the pure
attentionItems(AttentionSnapshot(array, notifications, thresholds)): List<AttentionItem>with stable condition keys (Attention.kt:15-19, 56). Alerts wrap it; they do not fork it:Inside, the condition half calls
attentionItems(AttentionSnapshot(array, notifications = null, thresholds = null))and keepsSTATE_ALERT_KEYS, so the wording and rules are the card's own. The notification half works per id, because PR #336's notification items are count-based ("notifications-alert",Attention.kt:131) and cannot say which notification is new.For the widget (#322): the one-shot
ServerProbe(section 4.4) returns the sameArrayInfoandNotificationsthe card uses. A widget later callsattentionItems()on that result and stores its own cache with a timestamp; v1 writes no cache nobody reads. Note: an active widget exempts NOVA from the restricted bucket (appstandby page above), which helps alerts too.4. Android details
4.1 Permission flow
android.permission.POST_NOTIFICATIONSin the main manifest. minSdk is 26 (app/build.gradle.kts:17), so notification channels always exist.ActivityResultContracts.RequestPermission; (3) granted → save the setting, create channels, enqueue the job; denied or dismissed → the switch stays off and the section shows "Android blocks notifications for NOVA" with "Open notification settings" (Settings.ACTION_APP_NOTIFICATION_SETTINGS). After two denials the system no longer shows its dialog; the settings button is the only way.NotificationManagerCompat.areNotificationsEnabled(); if the user had blocked NOVA's notifications in system settings, the same "blocked" line and button appear.areNotificationsEnabled()first. If false it makes no network request, records "Blocked in Android settings", and cancels the job.reconcile()re-enqueues on the next app start once notifications are allowed again.ACCESS_LOCAL_NETWORKfor LAN connections; without it "TCP Connections will typically result in a timeout error" (developer.android.com/privacy-and-security/local-network-permission, fetched 2026-11-13). NOVA targets 36 (app/build.gradle.kts:18), so nothing changes now; the ADR records that the target bump must add the permission and treat a blocked LAN as "cannot check", not as "server down". Requesting it before targeting 37 is explicitly discouraged by that page.4.2 Channels, content, tap targets, lock screen
alerts"Server alerts",IMPORTANCE_HIGH(sound, heads-up) — E1, E4, and test notifications.warnings"Server warnings",IMPORTANCE_DEFAULT— E2, E3.Channel importance cannot be changed after creation; the user owns it from then on.
subject(for example "Warning [Tower] - disk1 is hot (52 C)"), text =description, sub-text = NOVA's server name,setWhenfrom the notification's ISO timestamp (notifications.service.ts:953). E3/E4: title and detail fromattentionItems("Array is not started", "Start it on the Array tab.").setAutoCancel(true). No action buttons: "Mark as read" would need a write key.setGroup("server:<id>")plus a group summary per server when two or more are posted.serverId + "|" + key, tag = server id, so all of a server's notifications can be cancelled by tag (NotificationManager.getActiveNotifications(), API 23+).MainActivitywith extrasnova.serverIdandnova.target(ArrayorNotifications, i.e. PR feat: a needs-attention summary on the Overview #336'sAttentionTarget),PendingIntent.FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT, request code = notification id,FLAG_ACTIVITY_SINGLE_TOP | FLAG_ACTIVITY_CLEAR_TOP.MainActivityhandlesonCreateandonNewIntentand hands the request to a small singletonPendingNavigationthatMainViewModelconsumes: switch the active server when it differs (a deliberate, visible change of the selection), select the Array tab or open the notifications sheet. The sheet's open state moves fromrememberinMainScreen.kt:140into the view model for this. TodayMainActivity.kt:14-22handles no extras andAppNavGraph.kt:20-27has no deep links.VISIBILITY_PRIVATEwith a public version "Server alert" / "Unlock to see details", without server name or content. The user can override per channel.4.3 API key and certificate in the background (verified in code)
AndroidKeystoreKmsClient.generateNewAeadKey(ApiKeyStore.kt:83), which in Tink 1.24.0 uses onlysetKeySize(256).setBlockModes(GCM).setEncryptionPaddings(NONE)(tink-javav1.24.0AndroidKeystore.java:51-58): no user-authentication and no unlocked-device requirement. A worker can decrypt while the screen is locked.directBootAwareinAndroidManifest.xml; WorkManager's components aredirectBootAware="false", work manifest:39, :44, :52, :58), and all data is in credential-encrypted storage. Nothing runs after a reboot until the first unlock; after that the schedule resumes. The design promises nothing before the first unlock.SettingsStore.serverSnapshot(servers + pins, one consistent read,SettingsStore.kt:76-78),ApiKeyStore.getResult(id)per server,trustFor(...),ApolloClientFactory.build(url, key, trust), response classification with the existing public pureclassifyResponse(ResponseClassifier.kt:36). It must not useUnraidRepository's streams:domainStreamraises the in-app certificate prompt (UnraidRepository.kt:837-847).UpdateTrackerstarts only when injected (UpdateTracker.kt:109), and the worker's entry point does not inject it.setUserAuthenticationRequired) or to an unlocked device, background checks could no longer decrypt the key. Record this as a constraint on Optional biometric app lock #323.4.4 The one-shot seam
New
@Singleton class ServerProbe internal constructor(...)indata/alerts/, with lambdas for "read key", "build client", "run query" (house pattern ofUpdateTracker), and an@Injectconstructor for production:It sends the two existing operations
GetNotifications(queries.graphql:96-108, mapperGraphQlMapper.kt:228) andGetArray(queries.graphql:68-77, mappertoArrayInfo) as two requests over one connection. No new GraphQL document, so ADR-0017's all-or-nothing risk stays per query, and the request-limit test on branchtest/graphql-request-limitskeeps covering them (that branch's HANDOFF note: GetArray cost 59 of 1,000; "All 15 shipped queries answer HTTP 200 with data" on Unraid 7.4.0; not merged, medium confidence).warningsAndAlertsexists in API 4.30.0, NOVA's minimum (unraid/apiv4.30.0api/generated-schema.graphql:2095).4.5 Read-only key
Upstream, every notification read field and
arrayrequireREAD_ANYonNOTIFICATIONS/ARRAY(notifications.resolver.ts:28-69,array.resolver.ts:15-18), and the built-in VIEWER role hasREAD_ANYon every resource exceptAPI_KEY(auth/casbin/policy.ts:3-7), atunraid/apimain8e587fe2f8(2026-11-04). So a VIEWER key, or a custom key withnotifications:readandarray:read, is enough. To be confirmed on the device with the maintainer's read-only key (acceptance A1); feeds #316.4.6 Data and battery cost (estimate, not measured)
Per server and check: two HTTPS POSTs over one connection. Request bodies under 1 KB each with headers. Responses:
GetNotificationsgrows with unread warnings and alerts (about 0.3 KB each, at most 50);GetArrayabout 0.3 KB per disk. A full TLS handshake each time (a 15-minute gap exceeds OkHttp's default idle keep-alive; library default, not set in the repo). Order of magnitude: 3–15 KB per server per check, so about 0.15–0.7 MB per server per day at 30 minutes, four times that at 15 minutes. Battery cost is dominated by waking the radio; JobScheduler batches the job with other apps' jobs. Acceptance A9 measures both on the test phone; the estimate is not to be published until measured.4.7 What is cancelled when
cancelUniqueWork("nova-alert-check"), cancel any running test work, cancel all posted NOVA notifications, clearServerAlertState(so turning on again starts with a fresh baseline). Channels stay (deleting them shows "categories deleted" in Android settings).ServerRepository.delete,ServerRepository.kt:67-75)reconcile()).reconcile()re-enqueues when allowed again.resetKeyStorage)5. Settings UI
New section "Notifications" in
SettingsScreen.ktbetween "Layout" (:224) and "Updates" (:258), using the existingTogglehelper (:497-505). Settings follow theincludePrereleasespattern (SettingsStore.kt:37, 198-204). Defaults: off, 30 minutes, "Alerts and warnings", Wi-Fi only off.Texts (plain English):
test = true, never touches the seen state). Shows "Test: ", or "Test: NOVA reached . No unread alerts or warnings.", or "Test: NOVA could not reach ." This exercises key decryption in the worker, the network path, the channel and the tap target with a read-only key.6. Documentation
ADR-0047 "Opt-in background alert checks" (amends ADR-0017's "backgrounded app = zero polls", only while the switch is on; the screen streams keep their lifecycle gating):
FOREGROUND_SERVICEstripped).ACCESS_LOCAL_NETWORKwhen targeting 37; Optional biometric app lock #323 must not bind the key to user authentication.notificationAddedsubscription (permanent notification, foreground-service limits), server plugin or relay (excluded by principles), no alerts plus docs only.setJobSchedulerPersistenceEnabled(androidx-main commitf8d4af12be, 2026-11-09, not released) becomes stable.docs/adr/README.md.Privacy policy (
docs/privacy/POLICY.md): new effective date (:3); under "Data the app handles" (:11-14) add "Alert state: the ids of server notifications NOVA has already shown, the last check time and result, per server, on the device, excluded from backup and device transfer" (the wholedatastoredirectory is already excluded,backup_rules.xml:18,data_extraction_rules.xml:17,23); under "Data the app sends" (:18-20) add "Only while you turn on Alerts from your servers: NOVA asks each of your servers for its unread alerts and warnings and its array state about every 15 minutes to 3 hours (your choice), in the background. These requests go only to your own servers. Nothing else is sent, and nothing is sent at all while the switch is off." Change "No background tracking of any kind" (:38) to "No background tracking of any kind. The only background work is the optional server check described above." Permissions list (:44-49): addPOST_NOTIFICATIONS(Android 13+, asked only when you turn alerts on), and the three WorkManager permissionsWAKE_LOCK,RECEIVE_BOOT_COMPLETED(re-plan the checks after a restart, only active while alerts are on) and noteACCESS_NETWORK_STATE(already listed).README: privacy bullet (
README.md:116) gets the background-check sentence; a new short section "Alerts" with: what is checked, the read-only key, the Browser-delivery caveat, the late-alert and battery caveat, the Huawei/Xiaomi/Samsung hint, and the "Instant alerts with Unraid's own agents" recipe (Settings → Notifications in Unraid → ntfy.sh agent with your own server and topic → tick "Agents" for warnings and alerts → subscribe in the ntfy app; the agent adds an icon URL hosted on GitHub). Same facts onsite/index.html(privacy block around:405-413, features:198-199) andfastlane/metadata/android/en-US/full_description.txt:20.CHANGELOG (
## [Unreleased]→ Added):"Alerts outside the app (opt-in) — Settings → Notifications → Alerts from your servers checks your servers in the background and tells you about new Unraid alerts and warnings and an array that is not started or is degraded. It is off by default; while it is off, NOVA does nothing in the background. The checks go only to your own servers and a read-only API key is enough. Android decides when a check runs, so an alert can come late (#321)."
nofuturekid commentedon Nov 13, 2026
Design (2026-11-13), part 2 of 2
7. Pull requests, file ownership and tests
Order: PR 0 can land any time (docs). PR 1 waits for #336 on main. PR 2 stacks on PR 1, PR 3 on PR 2, PR 4 lands with PR 3, before the version bump. PRs 1–3 change the APK:
./scripts/local-ci.shgreen before push (R-BUILD), and none lands while a beta waits for acceptance (R-FREEZE).docs/adr/0047-opt-in-background-alert-checks.md,docs/adr/README.mddata/alerts/AlertRules.kt,data/alerts/AlertState.kt,data/alerts/AlertStateStore.kt,data/alerts/CheckAddresses.kt,data/local/SettingsStore.kt(keysalerts_enabled,alerts_interval_min,alerts_level,alerts_unmetered_only),data/repository/SettingsRepository.kt, tests underapp/src/test/.../data/alerts/evaluateAlerts,checkAddresses, the DataStorealert_state(JSON per server). Nothing wired.data/alerts/{AlertCheckWorker,AlertScheduler,ServerProbe,AlertNotifier,AlertsEntryPoint,PendingNavigation}.kt,NovaApp.kt,MainActivity.kt,AndroidManifest.xml,data/repository/ServerRepository.kt(delete hook),ui/screens/main/MainViewModel.kt+MainScreen.kt(navigation request, sheet state,reconcile()at start),gradle/libs.versions.toml,app/build.gradle.kts(androidx.work:work-runtimelatest stable,androidx.work:work-testingfor tests)ui/screens/settings/SettingsScreen.kt(it also holdsSettingsViewModel), newui/screens/settings/AlertsSection.kt, screenshot testsdocs/privacy/POLICY.md,README.md,site/index.html,fastlane/metadata/android/en-US/full_description.txt, ADR statusTests (each states the reason it exists):
PR 1 (JVM, plus Robolectric for the store):
first_check_posts_nothing_and_records_baseline— turning alerts on must not flood the user with old unread items.new_id_is_posted_once_and_never_again_even_after_leaving_unread— the maintainer's "never re-alert the same notification".info_is_never_postedandalerts_only_drops_warnings_but_marks_them_seen— switching levels must not post old warnings.array_stopped_posted_on_transition_not_while_it_lasts_and_rearmed_after_clearing.disk_temperature_and_disk_error_items_are_not_posted— Unraid already notifies these; posting both doubles every alert. This test fails if someone widensSTATE_ALERT_KEYSwithout deciding it.failed_half_keeps_its_state(notifications failed / array failed) — an outage must neither drop nor repeat alerts.more_than_five_new_fold_into_summary,seen_ids_are_capped_at_300_oldest_first.addresses_local_first_then_remote_and_pin_only_on_local_https— pinned trust must never apply to the remote address (reusestrustFor).store_survives_restart_and_drops_removed_server(Robolectric DataStore).PR 2 (Robolectric with
work-testing):switch_off_enqueues_nothing_and_does_not_initialise_workmanager— the maintainer's rule; a manifest test also asserts thatandroidx.work.WorkManagerInitializeris removed andFOREGROUND_SERVICEcarriestools:node="remove", and thatPOST_NOTIFICATIONSis declared.turning_off_cancels_work_and_posted_notifications,removing_last_server_cancels_work.interval_change_updates_the_existing_work(one unique work, new period).certificate_change_reports_problem_and_never_pins_or_prompts— background trust must fail closed (ADR-0041).undecryptable_key_reports_and_never_resets— ADR-0035.blocked_notifications_mean_no_network_request— no background traffic without a visible purpose.worker_always_returns_success— no backoff wake-ups.tap_intent_carries_server_and_target_and_is_immutable.PR 3: view-model tests
denied_permission_leaves_switch_off,test_action_does_not_touch_seen_state; Roborazzi screenshots of off, on, blocked and battery-optimised states.8. Device acceptance (an Android 9 phone; read-only key; a real server)
Nothing on the server is changed. Package
io.github.nofuturekid.nova.adb shell dumpsys jobscheduler | grep -A4 io.github.nofuturekid.nova;adb shell dumpsys alarm | grep io.github.nofuturekid.nova;adb shell dumpsys package io.github.nofuturekid.nova | grep -i -A3 disabledComponentsadb shell dumpsys jobscheduleradb shell cmd jobscheduler run -f io.github.nofuturekid.nova <jobId>(job id from A2; command availability on EMUI 9 to be confirmed)dumpsys jobscheduleradb shell am set-standby-bucket io.github.nofuturekid.nova rare, thendumpsys deviceidle force-idle; wait;am get-standby-bucket;dumpsys jobschedulerhistoryadb shell dumpsys deviceidle unforce.adb shell dumpsys batterystats --reset; one day with the switch on (30 min);adb shell dumpsys batterystats io.github.nofuturekid.novaandadb shell dumpsys netstats detail(NOVA's uid)Cannot be provoked read-only on production, and how they are covered instead:
monitor:447), so the next scheduled check is a free end-to-end test.evaluateAlertswithattentionItems; optionally on a separate test server where stopping the array is harmless (maintainer's call).pm revoke,pm clear-permission-flags … user-set user-fixed).9. Risks
reconcile()at app startSTATE_ALERT_KEYSallow-list with a test that fails when widenedACCESS_LOCAL_NETWORKWAKE_LOCKandRECEIVE_BOOT_COMPLETEDto every installFOREGROUND_SERVICEstripped10. Product questions (only these need the maintainer)
feat: decide which server events raise an alert and remember what was…