Implemented the Play Store readiness pass.

This commit is contained in:
2026-05-11 19:06:10 +03:30
parent 33fa8744e1
commit ad05e9da35
17 changed files with 312 additions and 46 deletions

View File

@@ -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/`

View File

@@ -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"

View File

@@ -8,7 +8,8 @@
<application
android:name=".NgxMonitorApplication"
android:allowBackup="true"
android:allowBackup="false"
android:appCategory="productivity"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"

View File

@@ -587,17 +587,27 @@ private fun HistoryCockpit(history: List<MetricSummary>) {
@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.")
}
}
}

View File

@@ -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<RadioButton>(serverGroup.checkedRadioButtonId)
val serverIndex = serverGroup.indexOfChild(checkedView).coerceAtLeast(0)
val server = servers[serverIndex]
when (mode) {
WidgetConfigureMode.Server -> {

View File

@@ -5,7 +5,6 @@
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
<item name="android:windowBackground">@color/command_black</item>
<item name="android:fontFamily">sans</item>
<item name="android:colorAccent">@color/signal_green</item>

View File

@@ -1,13 +1,12 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
<exclude
domain="database"
path="." />
<exclude
domain="sharedpref"
path="." />
<exclude
domain="file"
path="." />
</full-backup-content>

View File

@@ -1,19 +1,25 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
<exclude
domain="database"
path="." />
<exclude
domain="sharedpref"
path="." />
<exclude
domain="file"
path="." />
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
<exclude
domain="database"
path="." />
<exclude
domain="sharedpref"
path="." />
<exclude
domain="file"
path="." />
</device-transfer>
-->
</data-extraction-rules>
</data-extraction-rules>

View File

@@ -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.

View File

@@ -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=<redacted>
NGX_RELEASE_KEY_ALIAS=ngx-monitor-upload
NGX_RELEASE_KEY_PASSWORD=<redacted>
```
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

37
docs/PRIVACY_POLICY.md Normal file
View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -0,0 +1 @@
Monitor Nginx servers, alerts, routes, and home-screen widgets.

View File

@@ -0,0 +1 @@
NGX Monitor

View File

@@ -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
kotlin.code.style=official
VERSION_CODE=1
VERSION_NAME=1.0.0

9
local.properties.example Normal file
View File

@@ -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