diff --git a/README.md b/README.md index 4fb0697..54dac88 100644 --- a/README.md +++ b/README.md @@ -118,3 +118,12 @@ app/src/main/java/net/rodakot/ngxhttpmonitoringclient/ - Widget layouts: `app/src/main/res/layout/widget_fleet.xml`, `widget_server.xml`, `widget_metric.xml`, `widget_graph.xml`, `widget_incidents.xml` These assets are vector drawables, so they scale cleanly across screen densities. + +## Google Play Release + +Release configuration, signing instructions, Play Console metadata, and privacy/data-safety drafts live in: + +- `docs/PLAY_STORE_RELEASE.md` +- `docs/PLAY_STORE_DATA_SAFETY.md` +- `docs/PRIVACY_POLICY.md` +- `fastlane/metadata/android/en-US/` diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 52cd3c5..2e26abf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,8 +1,32 @@ +import java.util.Properties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.compose) } +val localProperties = Properties().apply { + val file = rootProject.file("local.properties") + if (file.isFile) { + file.inputStream().use { load(it) } + } +} + +fun releaseSecret(name: String): String? { + return providers.environmentVariable(name).orNull ?: localProperties.getProperty(name) +} + +val releaseStoreFile = releaseSecret("NGX_RELEASE_STORE_FILE") +val releaseStorePassword = releaseSecret("NGX_RELEASE_STORE_PASSWORD") +val releaseKeyAlias = releaseSecret("NGX_RELEASE_KEY_ALIAS") +val releaseKeyPassword = releaseSecret("NGX_RELEASE_KEY_PASSWORD") +val hasReleaseSigning = listOf( + releaseStoreFile, + releaseStorePassword, + releaseKeyAlias, + releaseKeyPassword, +).all { !it.isNullOrBlank() } + android { namespace = "net.rodakot.ngxhttpmonitoringclient" compileSdk { @@ -15,15 +39,30 @@ android { applicationId = "net.rodakot.ngxhttpmonitoringclient" minSdk = 24 targetSdk = 36 - versionCode = 1 - versionName = "1.0" + versionCode = (providers.gradleProperty("VERSION_CODE").orNull ?: "1").toInt() + versionName = providers.gradleProperty("VERSION_NAME").orNull ?: "1.0.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + signingConfigs { + if (hasReleaseSigning) { + create("release") { + storeFile = file(releaseStoreFile!!) + storePassword = releaseStorePassword + keyAlias = releaseKeyAlias + keyPassword = releaseKeyPassword + } + } + } + buildTypes { release { - isMinifyEnabled = false + isMinifyEnabled = true + isShrinkResources = true + if (hasReleaseSigning) { + signingConfig = signingConfigs.getByName("release") + } proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 578a630..b5d15f5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,7 +8,8 @@ ) { @Composable private fun SettingsCockpit(server: ServerProfile, controller: MonitorController) { - DataPanel("Alert Rules") { - Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - RuleRow("CPU limit", "${server.alertOverrides.cpuPercent ?: DefaultCpuThreshold}%") - RuleRow("Memory limit", "${server.alertOverrides.memoryPercent ?: DefaultMemoryThreshold}%") - RuleRow("Disk limit", "${server.alertOverrides.diskPercent ?: DefaultDiskThreshold}%") - RuleRow("p95 latency", "${(server.alertOverrides.latencyP95Millis ?: DefaultLatencyP95ThresholdMs).toInt()} ms") - RuleRow("5xx limit", "${server.alertOverrides.errors5xx ?: DefaultErrors5xxThreshold}") - RuleRow("Fallback IPs", server.fallbackIpAddresses.ifEmpty { listOf("None") }.joinToString(", ")) - Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { - Button(onClick = controller::showEditSelectedServer, shape = RoundedCornerShape(8.dp)) { Text("Edit") } - OutlinedButton(onClick = controller::deleteSelectedServer, shape = RoundedCornerShape(8.dp)) { Text("Delete") } + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + DataPanel("Alert Rules") { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + RuleRow("CPU limit", "${server.alertOverrides.cpuPercent ?: DefaultCpuThreshold}%") + RuleRow("Memory limit", "${server.alertOverrides.memoryPercent ?: DefaultMemoryThreshold}%") + RuleRow("Disk limit", "${server.alertOverrides.diskPercent ?: DefaultDiskThreshold}%") + RuleRow("p95 latency", "${(server.alertOverrides.latencyP95Millis ?: DefaultLatencyP95ThresholdMs).toInt()} ms") + RuleRow("5xx limit", "${server.alertOverrides.errors5xx ?: DefaultErrors5xxThreshold}") + RuleRow("Fallback IPs", server.fallbackIpAddresses.ifEmpty { listOf("None") }.joinToString(", ")) + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Button(onClick = controller::showEditSelectedServer, shape = RoundedCornerShape(8.dp)) { Text("Edit") } + OutlinedButton(onClick = controller::deleteSelectedServer, shape = RoundedCornerShape(8.dp)) { Text("Delete") } + } + } + } + DataPanel("Privacy") { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + RuleRow("Storage", "Server profiles, credentials, samples, alerts, and widget choices stay on this device.") + RuleRow("Credentials", "Monitor tokens and Basic Auth passwords are encrypted with Android Keystore.") + RuleRow("Network", "The app connects only to monitor endpoints configured by the user.") + RuleRow("Backups", "Android cloud backup and device transfer are disabled for app data.") } } } diff --git a/app/src/main/java/net/rodakot/ngxhttpmonitoringclient/widget/WidgetConfigureActivities.kt b/app/src/main/java/net/rodakot/ngxhttpmonitoringclient/widget/WidgetConfigureActivities.kt index 97b3eb6..a3a5eb7 100644 --- a/app/src/main/java/net/rodakot/ngxhttpmonitoringclient/widget/WidgetConfigureActivities.kt +++ b/app/src/main/java/net/rodakot/ngxhttpmonitoringclient/widget/WidgetConfigureActivities.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.graphics.Color import android.os.Bundle import android.view.Gravity +import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.Button @@ -66,14 +67,14 @@ abstract class BaseWidgetConfigureActivity : Activity() { orientation = RadioGroup.VERTICAL servers.forEachIndexed { index, server -> addView(RadioButton(this@BaseWidgetConfigureActivity).apply { - id = index + 1 + id = View.generateViewId() text = server.name setTextColor(Color.rgb(234, 240, 247)) textSize = 16f setPadding(0, 10, 0, 10) }) } - check(1) + check(getChildAt(0).id) } val scroll = ScrollView(this).apply { addView(serverGroup) @@ -98,7 +99,8 @@ abstract class BaseWidgetConfigureActivity : Activity() { } root.addView(actionButton("Add ${mode.buttonLabel}") { - val serverIndex = (serverGroup.checkedRadioButtonId - 1).coerceAtLeast(0) + val checkedView = serverGroup.findViewById(serverGroup.checkedRadioButtonId) + val serverIndex = serverGroup.indexOfChild(checkedView).coerceAtLeast(0) val server = servers[serverIndex] when (mode) { WidgetConfigureMode.Server -> { diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 19cfc46..79c52a0 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -5,7 +5,6 @@ true false false - false @color/command_black sans @color/signal_green diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml index 4df9255..31f9edf 100644 --- a/app/src/main/res/xml/backup_rules.xml +++ b/app/src/main/res/xml/backup_rules.xml @@ -1,13 +1,12 @@ - + - - \ No newline at end of file + + + + diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml index 9ee9997..136e725 100644 --- a/app/src/main/res/xml/data_extraction_rules.xml +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -1,19 +1,25 @@ - + - + + + - - \ No newline at end of file + diff --git a/docs/PLAY_STORE_DATA_SAFETY.md b/docs/PLAY_STORE_DATA_SAFETY.md new file mode 100644 index 0000000..1e0f0ae --- /dev/null +++ b/docs/PLAY_STORE_DATA_SAFETY.md @@ -0,0 +1,37 @@ +# Play Console Data Safety Notes + +Use this as a starting point for the Play Console Data safety form. Review it before submission because the final answers are your legal responsibility. + +## App Data Flow + +NGX Monitor is a client for monitor endpoints configured by the user. The developer does not operate a backend for this app. + +Stored locally on the device: + +- Server names, base URLs, tags, fallback LAN IPs, and alert thresholds +- Monitor API tokens and optional Basic Auth credentials, encrypted with Android Keystore +- Metric samples, alert history, route diagnostics, and widget preferences + +Transmitted by the app: + +- HTTP(S) requests to user-configured monitor endpoints +- Authentication headers only for the specific endpoint configured by the user +- Notification content generated locally on the device + +Not used by the app: + +- Advertising ID +- Location APIs +- Contacts, photos, camera, microphone, calendar, or SMS +- Developer-operated analytics or crash reporting SDKs + +## Suggested Data Safety Answers + +- Data collected by developer: No +- Data shared with third parties by developer: No +- Data is encrypted in transit: Yes, when users configure HTTPS monitor endpoints +- Users can request data deletion: Data is local; users can delete server profiles in-app or clear app data +- App type: utility/productivity +- Account creation: Not required + +Important note for the form: the app allows user-configured HTTP endpoints for private LAN monitoring. If you publish with HTTP support enabled, disclose that encryption in transit depends on the endpoint configured by the user. diff --git a/docs/PLAY_STORE_RELEASE.md b/docs/PLAY_STORE_RELEASE.md new file mode 100644 index 0000000..10daf2b --- /dev/null +++ b/docs/PLAY_STORE_RELEASE.md @@ -0,0 +1,92 @@ +# Google Play Release Checklist + +This project is configured to build Google Play compatible Android App Bundles. + +## Current Project Readiness + +- Package name: `net.rodakot.ngxhttpmonitoringclient` +- Minimum SDK: 24 +- Target SDK: 36 +- Version: `VERSION_NAME` / `VERSION_CODE` in `gradle.properties` +- Release build: R8 minification enabled, resource shrinking enabled +- App data backup: disabled for app data, databases, shared preferences, and files +- Release signing: read from environment variables or `local.properties` + +## Create An Upload Key + +Create an upload keystore outside the repository: + +```powershell +keytool -genkeypair ` + -v ` + -keystore "C:\secure\keys\ngx-monitor-upload.jks" ` + -storetype JKS ` + -keyalg RSA ` + -keysize 4096 ` + -validity 10000 ` + -alias ngx-monitor-upload +``` + +Copy `local.properties.example` to `local.properties` and set: + +```properties +NGX_RELEASE_STORE_FILE=C:\Users\meghdad\keystores\ngx-monitor-upload.jks +NGX_RELEASE_STORE_PASSWORD= +NGX_RELEASE_KEY_ALIAS=ngx-monitor-upload +NGX_RELEASE_KEY_PASSWORD= +``` + +Do not commit `local.properties` or the keystore. + +## Build Release Artifacts + +Unsigned release sanity build: + +```powershell +$env:GRADLE_USER_HOME='C:\Users\meghdad\AndroidStudioProjects\NGXhttpMonitoringClient\.gradle-user' +.\gradlew.bat :app:bundleRelease --console=plain +``` + +Signed upload bundle, after signing values are configured: + +```powershell +$env:GRADLE_USER_HOME='C:\Users\meghdad\AndroidStudioProjects\NGXhttpMonitoringClient\.gradle-user' +.\gradlew.bat :app:bundleRelease --console=plain +``` + +Output: + +```text +app/build/outputs/bundle/release/app-release.aab +``` + +## Before Uploading To Play Console + +1. Create the app in Play Console. +2. Enable Play App Signing. +3. Upload `app-release.aab`. +4. Fill the store listing using `fastlane/metadata/android/en-US`. +5. Upload phone screenshots and a 1024x500 feature graphic. +6. Host `docs/PRIVACY_POLICY.md` as a public web page and paste that URL into Play Console. +7. Complete Data Safety using `docs/PLAY_STORE_DATA_SAFETY.md`. +8. Complete Content Rating. Suggested category: utility/productivity/server monitoring. +9. Complete Target Audience. This app is for server administrators, not children. +10. Run an internal test release before production. + +## Release Version Bump + +For every Play upload, increment `VERSION_CODE` in `gradle.properties`. + +Example: + +```properties +VERSION_CODE=2 +VERSION_NAME=1.0.1 +``` + +## Official References + +- Google Play target API requirements: https://support.google.com/googleplay/android-developer/answer/11926878 +- Android app signing: https://developer.android.com/studio/publish/app-signing +- Build and upload an Android App Bundle: https://developer.android.com/guide/app-bundle +- Google Play Data safety: https://support.google.com/googleplay/android-developer/answer/10787469 diff --git a/docs/PRIVACY_POLICY.md b/docs/PRIVACY_POLICY.md new file mode 100644 index 0000000..806748d --- /dev/null +++ b/docs/PRIVACY_POLICY.md @@ -0,0 +1,37 @@ +# NGX Monitor Privacy Policy + +Effective date: 2026-05-11 + +NGX Monitor is a mobile client for monitoring Nginx servers that you configure in the app. + +## Data Stored On Your Device + +The app stores server profiles, monitor endpoint URLs, optional fallback LAN IP addresses, tags, alert thresholds, metric history, route diagnostics, alerts, and widget preferences on your device. + +If you save monitor tokens or Basic Auth credentials, the app encrypts them with Android Keystore before storing them locally. + +Android cloud backup and device transfer are disabled for app data. + +## Data Sent By The App + +The app sends monitor API requests only to endpoints that you configure. If you configure authentication, the app sends the configured token or Basic Auth credentials to that monitor endpoint. + +The app does not send your monitor data, credentials, server list, or usage data to the app developer. + +## Network Security + +HTTPS is recommended for monitor endpoints. The app also supports HTTP endpoints for private LAN monitoring when the user explicitly configures them. + +## Third Parties + +The app does not include advertising SDKs, analytics SDKs, or developer-operated crash reporting SDKs. + +Google Play may process app install, billing, review, crash, and device information according to Google's own policies. + +## Data Deletion + +You can delete server profiles inside the app. You can also remove all app data from Android system settings by clearing the app's storage or uninstalling the app. + +## Contact + +Replace this section with the publisher support email before submitting the app to Google Play. diff --git a/fastlane/metadata/android/en-US/changelogs/1.txt b/fastlane/metadata/android/en-US/changelogs/1.txt new file mode 100644 index 0000000..b321ce4 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/1.txt @@ -0,0 +1,7 @@ +Initial release. + +- Fleet monitor for Nginx monitor endpoints +- Server detail cockpit with system, request, disk, alert, history, and route diagnostics +- VPN and DNS resilience with fallback LAN IPs +- Home-screen widgets for fleet status, server bars, metric sparklines, telemetry graphs, and incidents +- Local encrypted credential storage diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt new file mode 100644 index 0000000..4f97b91 --- /dev/null +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -0,0 +1,15 @@ +NGX Monitor is a focused Android client for Nginx monitor endpoints. + +Track CPU, memory, storage, request rate, latency, connection state, HTTP errors, and route health from your phone. The app is designed for server administrators who monitor multiple Nginx hosts and need a fast, readable mobile command deck. + +Key features: +- Monitor 20+ servers from one fleet view +- View live CPU, memory, storage, request, latency, and connection signals +- Follow Nginx worker, request, upstream, disk, history, alert, and route diagnostics +- Configure monitor API tokens and optional Basic Auth credentials +- Store credentials locally with Android Keystore encryption +- Use fallback LAN IPs for VPN, DNS, and local network routing problems +- Get Android notifications for degraded or unreachable servers +- Add home-screen widgets for fleet status, server bars, metric sparklines, telemetry graphs, and incident watch + +NGX Monitor connects only to the monitor endpoints you configure. HTTPS endpoints are recommended, and HTTP endpoints are supported for private LAN monitoring. diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt new file mode 100644 index 0000000..d548454 --- /dev/null +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -0,0 +1 @@ +Monitor Nginx servers, alerts, routes, and home-screen widgets. diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt new file mode 100644 index 0000000..fafd01a --- /dev/null +++ b/fastlane/metadata/android/en-US/title.txt @@ -0,0 +1 @@ +NGX Monitor diff --git a/gradle.properties b/gradle.properties index 34c5e9e..b037341 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,4 +12,6 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects # org.gradle.parallel=true # Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official \ No newline at end of file +kotlin.code.style=official +VERSION_CODE=1 +VERSION_NAME=1.0.0 diff --git a/local.properties.example b/local.properties.example new file mode 100644 index 0000000..0873389 --- /dev/null +++ b/local.properties.example @@ -0,0 +1,9 @@ +## Copy these values into local.properties or export them as environment variables +## before building the signed Play upload bundle. +## +## Do not commit local.properties or the keystore file. + +NGX_RELEASE_STORE_FILE=C:\\path\\to\\ngx-monitor-upload.jks +NGX_RELEASE_STORE_PASSWORD=replace-with-keystore-password +NGX_RELEASE_KEY_ALIAS=ngx-monitor-upload +NGX_RELEASE_KEY_PASSWORD=replace-with-key-password