Compare commits

...

3 Commits

Author SHA1 Message Date
Курнат Андрей 371f94b669 Update Android production verification to 1.3.11 2026-06-10 07:09:06 +03:00
Курнат Андрей 73bb8a62bd Bump Android release to 1.3.11 2026-06-10 07:07:21 +03:00
Курнат Андрей 1a76143c2e Add Android backup status summary 2026-06-10 07:05:27 +03:00
11 changed files with 161 additions and 15 deletions
+2 -2
View File
@@ -9,9 +9,9 @@
## Policy
- Stable Argus releases for `qsfera-mobile` must keep the same package name and production signing certificate after the production-signed `1.3.10` / `38` release.
- Stable Argus releases for `qsfera-mobile` must keep the same package name and production signing certificate after the production-signed `1.3.11` / `39` release.
- The in-app updater must not bypass certificate mismatch. `AppUpdateManager` validates package name, monotonically newer `versionCode`, Argus `androidVersionCode`, and signer digest before starting Android package installation.
- A client installed from the debug-signed `1.3.5` / `33` Argus APK cannot be moved to the production-signed `1.3.10` / `38` APK by the in-app updater because the signer digest check intentionally fails.
- A client installed from the debug-signed `1.3.5` / `33` Argus APK cannot be moved to the production-signed `1.3.11` / `39` APK by the in-app updater because the signer digest check intentionally fails.
- For debug-signed `1.3.5` installs, the supported migration path is: back up unsynced local files, uninstall the debug-signed build, then install the current stable QSfera APK from Argus.
- On certificate mismatch, the Android app must show an explicit reinstall-required dialog instead of a generic update error.
+2 -2
View File
@@ -135,8 +135,8 @@ android {
testInstrumentationRunner "eu.qsfera.android.utils.OCTestAndroidJUnitRunner"
versionCode = 38
versionName = "1.3.10"
versionCode = 39
versionName = "1.3.11"
buildConfigField "String", gitRemote, "\"" + getGitOriginRemote() + "\""
buildConfigField "String", commitSHA1, "\"" + getLatestGitHash() + "\""
@@ -0,0 +1,74 @@
/**
* qsfera Android client application
*
* Copyright (C) 2026 QSfera.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*/
package eu.qsfera.android.presentation.settings.automaticuploads
import android.content.Context
import eu.qsfera.android.R
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration
internal object AutomaticUploadStatusFormatter {
fun format(
context: Context,
configuration: FolderBackUpConfiguration?,
sourcePaths: List<String>,
lastSyncSummary: String,
): String {
if (configuration == null) {
return context.getString(R.string.prefs_camera_upload_status_off)
}
return listOf(
context.getString(R.string.prefs_camera_upload_status_on),
formatSourceCount(context, sourcePaths.size),
formatConditions(context, configuration),
context.getString(R.string.prefs_camera_upload_status_last_scan, lastSyncSummary),
).joinToString(separator = "\n")
}
private fun formatSourceCount(context: Context, sourceCount: Int): String =
if (sourceCount == 0) {
context.getString(R.string.prefs_camera_upload_source_paths_empty)
} else {
context.resources.getQuantityString(
R.plurals.prefs_camera_upload_status_source_count,
sourceCount,
sourceCount
)
}
private fun formatConditions(context: Context, configuration: FolderBackUpConfiguration): String {
val conditions = mutableListOf<String>()
if (configuration.wifiOnly) {
conditions += context.getString(R.string.prefs_camera_upload_status_wifi_only)
} else {
conditions += context.getString(
R.string.prefs_camera_upload_status_mobile_data,
formatMobileDataLimit(context, configuration.mobileDataLimitBytes)
)
if (configuration.allowRoaming) {
conditions += context.getString(R.string.prefs_camera_upload_status_roaming_allowed)
}
}
if (configuration.chargingOnly) {
conditions += context.getString(R.string.prefs_camera_upload_status_charging_only)
}
return conditions.joinToString(separator = "")
}
private fun formatMobileDataLimit(context: Context, mobileDataLimitBytes: Long): String {
val values = context.resources.getStringArray(R.array.prefs_camera_upload_mobile_data_limit_values)
val entries = context.resources.getStringArray(R.array.prefs_camera_upload_mobile_data_limit_entries)
val index = values.indexOf(mobileDataLimitBytes.toString())
return entries.getOrNull(index)
?: context.getString(R.string.prefs_camera_upload_status_custom_mobile_data_limit, mobileDataLimitBytes)
}
}
@@ -67,6 +67,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
private val picturesViewModel by viewModel<SettingsPictureUploadsViewModel>()
private val manageAccountsViewModel by viewModel<ManageAccountsViewModel>()
private var prefPictureUploadsStatus: Preference? = null
private var prefEnablePictureUploads: SwitchPreferenceCompat? = null
private var prefPictureUploadsPath: Preference? = null
private var prefPictureUploadsOnWifi: CheckBoxPreference? = null
@@ -101,6 +102,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings_picture_uploads, rootKey)
prefPictureUploadsStatus = findPreference(PREF_PICTURE_UPLOADS_STATUS)
prefEnablePictureUploads = findPreference(PREF__CAMERA_PICTURE_UPLOADS_ENABLED)
prefPictureUploadsPath = findPreference(PREF__CAMERA_PICTURE_UPLOADS_PATH)
prefPictureUploadsOnWifi = findPreference(PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY)
@@ -162,6 +164,12 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
prefPictureUploadsAccount?.value = it.accountName
prefPictureUploadsPath?.summary = picturesViewModel.getUploadPathString()
val sourcePaths = picturesViewModel.getPictureUploadsSourcePaths()
prefPictureUploadsStatus?.summary = AutomaticUploadStatusFormatter.format(
context = requireContext(),
configuration = it,
sourcePaths = sourcePaths,
lastSyncSummary = formatLastSyncSummary(it.lastSyncTimestamp),
)
prefPictureUploadsSourcePath?.summary = getSourcePathsSummary(sourcePaths)
prefPictureUploadsClearSourcePaths?.isEnabled = sourcePaths.isNotEmpty()
prefPictureUploadsOnWifi?.isChecked = it.wifiOnly
@@ -314,6 +322,12 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
}
private fun resetFields() {
prefPictureUploadsStatus?.summary = AutomaticUploadStatusFormatter.format(
context = requireContext(),
configuration = null,
sourcePaths = emptyList(),
lastSyncSummary = getString(R.string.prefs_camera_upload_last_sync_never),
)
prefPictureUploadsAccount?.value = null
prefPictureUploadsPath?.summary = null
prefPictureUploadsSourcePath?.summary = getString(R.string.prefs_camera_upload_source_paths_empty)
@@ -343,6 +357,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
}
companion object {
private const val PREF_PICTURE_UPLOADS_STATUS = "picture_uploads_status"
private const val PREF_PICTURE_UPLOADS_CLEAR_SOURCE_PATHS = "picture_uploads_clear_source_paths"
}
}
@@ -67,6 +67,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
private val videosViewModel by viewModel<SettingsVideoUploadsViewModel>()
private val manageAccountsViewModel by viewModel<ManageAccountsViewModel>()
private var prefVideoUploadsStatus: Preference? = null
private var prefEnableVideoUploads: SwitchPreferenceCompat? = null
private var prefVideoUploadsPath: Preference? = null
private var prefVideoUploadsOnWifi: CheckBoxPreference? = null
@@ -101,6 +102,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings_video_uploads, rootKey)
prefVideoUploadsStatus = findPreference(PREF_VIDEO_UPLOADS_STATUS)
prefEnableVideoUploads = findPreference(PREF__CAMERA_VIDEO_UPLOADS_ENABLED)
prefVideoUploadsPath = findPreference(PREF__CAMERA_VIDEO_UPLOADS_PATH)
prefVideoUploadsOnWifi = findPreference(PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY)
@@ -160,6 +162,12 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
prefVideoUploadsAccount?.value = it.accountName
prefVideoUploadsPath?.summary = videosViewModel.getUploadPathString()
val sourcePaths = videosViewModel.getVideoUploadsSourcePaths()
prefVideoUploadsStatus?.summary = AutomaticUploadStatusFormatter.format(
context = requireContext(),
configuration = it,
sourcePaths = sourcePaths,
lastSyncSummary = formatLastSyncSummary(it.lastSyncTimestamp),
)
prefVideoUploadsSourcePath?.summary = getSourcePathsSummary(sourcePaths)
prefVideoUploadsClearSourcePaths?.isEnabled = sourcePaths.isNotEmpty()
prefVideoUploadsOnWifi?.isChecked = it.wifiOnly
@@ -312,6 +320,12 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
}
private fun resetFields() {
prefVideoUploadsStatus?.summary = AutomaticUploadStatusFormatter.format(
context = requireContext(),
configuration = null,
sourcePaths = emptyList(),
lastSyncSummary = getString(R.string.prefs_camera_upload_last_sync_never),
)
prefVideoUploadsAccount?.value = null
prefVideoUploadsPath?.summary = null
prefVideoUploadsSourcePath?.summary = getString(R.string.prefs_camera_upload_source_paths_empty)
@@ -341,6 +355,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
}
companion object {
private const val PREF_VIDEO_UPLOADS_STATUS = "video_uploads_status"
private const val PREF_VIDEO_UPLOADS_CLEAR_SOURCE_PATHS = "video_uploads_clear_source_paths"
}
}
@@ -563,6 +563,21 @@
<string name="prefs_camera_upload_source_paths_empty">Папки телефона не выбраны</string>
<string name="confirmation_clear_camera_upload_sources_title">Убрать выбранные папки</string>
<string name="confirmation_clear_camera_upload_sources_message">Копирование перестанет проверять папки телефона, пока вы снова не добавите папку.</string>
<string name="prefs_camera_upload_status_title">Состояние копирования</string>
<string name="prefs_camera_upload_status_off">Выключено</string>
<string name="prefs_camera_upload_status_on">Включено</string>
<plurals name="prefs_camera_upload_status_source_count">
<item quantity="one">%d папка телефона</item>
<item quantity="few">%d папки телефона</item>
<item quantity="many">%d папок телефона</item>
<item quantity="other">%d папки телефона</item>
</plurals>
<string name="prefs_camera_upload_status_wifi_only">Только Wi-Fi</string>
<string name="prefs_camera_upload_status_mobile_data">Мобильные данные: %1$s</string>
<string name="prefs_camera_upload_status_custom_mobile_data_limit">%1$d байт в день</string>
<string name="prefs_camera_upload_status_roaming_allowed">роуминг разрешён</string>
<string name="prefs_camera_upload_status_charging_only">только во время зарядки</string>
<string name="prefs_camera_upload_status_last_scan">Последняя проверка: %1$s</string>
<string name="prefs_camera_upload_allow_roaming">Разрешить копирование в роуминге</string>
<string name="prefs_camera_upload_allow_roaming_summary">Применяется только если копирование только по Wi-Fi выключено</string>
<string name="prefs_camera_upload_behaviour_dialog_title">Исходный файл после копирования</string>
@@ -602,6 +602,19 @@
<string name="prefs_camera_upload_source_paths_empty">No phone folders selected</string>
<string name="confirmation_clear_camera_upload_sources_title">Remove selected folders</string>
<string name="confirmation_clear_camera_upload_sources_message">Backup will stop scanning phone folders until you add a folder again.</string>
<string name="prefs_camera_upload_status_title">Backup status</string>
<string name="prefs_camera_upload_status_off">Off</string>
<string name="prefs_camera_upload_status_on">On</string>
<plurals name="prefs_camera_upload_status_source_count">
<item quantity="one">%d phone folder</item>
<item quantity="other">%d phone folders</item>
</plurals>
<string name="prefs_camera_upload_status_wifi_only">Wi-Fi only</string>
<string name="prefs_camera_upload_status_mobile_data">Mobile data: %1$s</string>
<string name="prefs_camera_upload_status_custom_mobile_data_limit">%1$d bytes per day</string>
<string name="prefs_camera_upload_status_roaming_allowed">roaming allowed</string>
<string name="prefs_camera_upload_status_charging_only">charging only</string>
<string name="prefs_camera_upload_status_last_scan">Last scan: %1$s</string>
<string name="prefs_camera_upload_allow_roaming">Allow backup while roaming</string>
<string name="prefs_camera_upload_allow_roaming_summary">Only applies when Wi-Fi-only backup is off</string>
<string name="prefs_camera_upload_mobile_data_limit_title">Mobile data daily limit</string>
@@ -25,6 +25,12 @@
app:key="enable_picture_uploads"
app:summary="@string/prefs_camera_picture_upload_summary"
app:title="@string/prefs_camera_picture_upload" />
<Preference
app:iconSpaceReserved="false"
app:key="picture_uploads_status"
app:selectable="false"
app:summary="@string/prefs_camera_upload_status_off"
app:title="@string/prefs_camera_upload_status_title" />
<ListPreference
app:dialogTitle="@string/prefs_picture_upload_account"
app:iconSpaceReserved="false"
@@ -23,6 +23,12 @@
app:key="enable_video_uploads"
app:summary="@string/prefs_camera_video_upload_summary"
app:title="@string/prefs_camera_video_upload" />
<Preference
app:iconSpaceReserved="false"
app:key="video_uploads_status"
app:selectable="false"
app:summary="@string/prefs_camera_upload_status_off"
app:title="@string/prefs_camera_upload_status_title" />
<ListPreference
app:dialogTitle="@string/prefs_video_upload_account"
app:iconSpaceReserved="false"
+10 -8
View File
@@ -22,10 +22,11 @@ Date: 2026-06-08.
## Completed In This Pass
- Android release builds now fail fast when release signing variables are missing, so unsigned release artifacts cannot be published by accident.
- Android version was raised to `1.3.10` with `versionCode` `38`.
- Android version was raised to `1.3.11` with `versionCode` `39`.
- Android now has QSfera-specific Argus publishing scripts: local upload-keystore generation, signed APK staging, APK metadata verification, debug-certificate rejection, and Argus upload.
- Android `1.3.9`/`37` was published to Argus as `qsfera-mobile` on 2026-06-10. Argus upload returned release id `53ecc24e-106b-43a5-a39e-ffacd72a43f8`, package size `12226572`, and SHA-256 `cbd2c25328e395ea4da4e0f582feac39dec18d2d47b3e105d344904d8a24f017`.
- Android `1.3.10`/`38` was published to Argus as `qsfera-mobile` on 2026-06-10. Argus upload returned release id `8465ba6d-a008-45f5-9c70-e6cd2c94317a`, package size `12228464`, and SHA-256 `ce33a73ce122de6c9c797e7cb552e5493b628c60ddc98fda6a9e230e3c1ed7fe`.
- Android `1.3.11`/`39` was published to Argus as `qsfera-mobile` on 2026-06-10. Argus upload returned release id `3dbac5ad-b19c-46e6-b24f-8f22e1c466dd`, package size `12234884`, and SHA-256 `c9eae41bbaae421e1a69721e891d8431ce599684e0f655b71693a422aa83720a`.
- Android auto-upload source paths are normalized and new/first source folders start from timestamp `0`, so existing media is eligible for initial backup.
- Android auto-upload validates persisted read permission and `DocumentFile.canRead()` before scanning SAF folders.
- Android immediate auto-upload scheduling no longer blocks the caller while waiting for WorkManager state.
@@ -33,6 +34,7 @@ Date: 2026-06-08.
- Android auto-upload now exposes a no-roaming control for photo and video backup, stores it in Room schema version `50`, and maps mobile backup without roaming to WorkManager `NetworkType.NOT_ROAMING`.
- Android auto-upload now exposes a photo/video daily mobile-data limit, stores it in Room schema version `51`, and stops queuing additional uploads on metered automatic-upload runs after the configured daily byte limit is reached while keeping skipped files eligible for the next scan period.
- Android video backup settings now label the Wi-Fi-only control as "Never use mobile data for videos" and explain that turning it off enables metered-network limits and roaming controls.
- Android photo/video backup settings now show a compact overall backup status: enabled state, selected phone-folder count, network/roaming/charging conditions, and last backup scan.
- Server `qsfera_full` compose files now require explicit admin, Keycloak, and S3/MinIO secrets instead of demo defaults.
- Server `qsfera_full/env.production.example` provides a tracked production-safe starting point with TLS validation enabled and demo users disabled.
- Server `qsfera_full/validate-production-env.sh` checks production `.env` invariants before compose validation: TLS validation enabled, demo users disabled, pinned production image, required domains, required secrets, and no public-production MinIO.
@@ -45,7 +47,7 @@ Date: 2026-06-08.
- Windows tray "Show" now brings the settings window to the foreground after restoring it to an available screen. The same `Application::showSettings()` path is used by tray menu actions and `qsfera.exe --show`, and Win32 window enumeration on 2026-06-10 reported `Visible=True`, `Foreground=True`, title `КуСфера`.
- Windows Desktop now has repeatable local packaging and feed-publication scripts. `scripts/package-windows-desktop.ps1` produced a smoke-tested `4.0.0` x64 package with Qt/MSVC runtime files; `scripts/publish-windows-desktop.ps1` generated a valid local `stable.xml` feed for that MSI and refuses remote publication unless the MSI is Authenticode-signed.
- Added `scripts/verify-production-state.ps1` as a repeatable live production endpoint gate for the public server status, Android Argus manifest/download, and Desktop update feed.
- `scripts/verify-production-state.ps1 -VerifyAndroidDownload` passed on 2026-06-10: server `edition=stable`/`productversion=7.0.0`, Android `1.3.10`/`38`, downloaded APK size `12228464`, APK SHA-256 `ce33a73ce122de6c9c797e7cb552e5493b628c60ddc98fda6a9e230e3c1ed7fe`, and valid Desktop no-update feed.
- `scripts/verify-production-state.ps1 -VerifyAndroidDownload` passed on 2026-06-10: server `edition=stable`/`productversion=7.0.0`, Android `1.3.11`/`39`, downloaded APK size `12234884`, APK SHA-256 `c9eae41bbaae421e1a69721e891d8431ce599684e0f655b71693a422aa83720a`, and valid Desktop no-update feed.
- Added `Server/scripts/verify-rpi-production.sh`, declared a loopback-only Pi metrics port in `Server/docker-compose.rpi5.yml`, and expanded the Raspberry Pi Docker runbook with local/public status checks, container/image checks, Prometheus metrics verification, backup/restore drill steps, and Docker data-root/retention planning.
- Added systemd monitoring units for the Pi: `Server/systemd/qsfera-rpi-production-check.service`, `Server/systemd/qsfera-rpi-production-check.timer`, `Server/systemd/qsfera-rpi-production-check-alert@.service`, `Server/systemd/rpi-production-check.env.example`, plus installer `Server/scripts/install-rpi-production-monitoring.sh` and alert helper `Server/scripts/alert-rpi-production-check-failure.sh`.
- Deployed the Pi metrics/checker update on 2026-06-09. Before upload, `/mnt/data/qsfera/cloud-server/src/docker-compose.rpi5.yml` was backed up to `/mnt/data/qsfera/backups/docker-compose.rpi5.yml-20260609-181614.bak`; `docker compose -f docker-compose.rpi5.yml up -d` recreated and started `qsfera-cloud-server`; `docker compose ps` reported loopback ports `127.0.0.1:9200->9200/tcp` and `127.0.0.1:9205->9205/tcp`.
@@ -66,16 +68,16 @@ Date: 2026-06-08.
## Android Argus Publication Check
- Current public Argus manifest for `https://argus.kusoft.xyz/api/apps/qsfera-mobile/manifest?platform=android&channel=stable` reports release id `8465ba6d-a008-45f5-9c70-e6cd2c94317a`, version `1.3.10`, channel `stable`, platform `android`, package kind `apk`, `androidVersionCode` `38`, package size `12228464`, and SHA-256 `ce33a73ce122de6c9c797e7cb552e5493b628c60ddc98fda6a9e230e3c1ed7fe`.
- Independent download verification of `https://argus.kusoft.xyz/api/apps/qsfera-mobile/download/latest?platform=android&channel=stable` produced a `12228464` byte APK with the same SHA-256 `ce33a73ce122de6c9c797e7cb552e5493b628c60ddc98fda6a9e230e3c1ed7fe`.
- Current public Argus manifest for `https://argus.kusoft.xyz/api/apps/qsfera-mobile/manifest?platform=android&channel=stable` reports release id `3dbac5ad-b19c-46e6-b24f-8f22e1c466dd`, version `1.3.11`, channel `stable`, platform `android`, package kind `apk`, `androidVersionCode` `39`, package size `12234884`, and SHA-256 `c9eae41bbaae421e1a69721e891d8431ce599684e0f655b71693a422aa83720a`.
- Independent download verification of `https://argus.kusoft.xyz/api/apps/qsfera-mobile/download/latest?platform=android&channel=stable` produced a `12234884` byte APK with the same SHA-256 `c9eae41bbaae421e1a69721e891d8431ce599684e0f655b71693a422aa83720a`.
- The previously published `1.3.5`/`33` APK matched Argus SHA-256 `b7d5bdcb49527c00983a15849252fa42189dee4287754803c46cf2f32e2130f3`, but `apksigner` reported `CN=Android Debug` with certificate SHA-256 `b86d745cecb01ca24bda03092fe94ca7801678513b97edd1175bcabfdea4e941`.
- The new `1.3.10`/`38` APK is signed by `CN=QSfera Argus Upload, O=QSfera, C=RU` with certificate SHA-256 `ea5c304c78ee333f6586f54c933d986c1f865da9c4e3595f8fe0df274c1ab877`.
- `AppUpdateManager` validates downloaded APK package name, greater `versionCode`, Argus `androidVersionCode`, and signer digest against the installed app signature. Because of that local code path, devices that installed debug-signed `1.3.5` should be treated as needing reinstall or an explicit migration plan before they can move to the production-signed `1.3.10`.
- The new `1.3.11`/`39` APK is signed by `CN=QSfera Argus Upload, O=QSfera, C=RU` with certificate SHA-256 `ea5c304c78ee333f6586f54c933d986c1f865da9c4e3595f8fe0df274c1ab877`.
- `AppUpdateManager` validates downloaded APK package name, greater `versionCode`, Argus `androidVersionCode`, and signer digest against the installed app signature. Because of that local code path, devices that installed debug-signed `1.3.5` should be treated as needing reinstall or an explicit migration plan before they can move to the production-signed `1.3.11`.
- The rollout policy for debug-signed `1.3.5` devices is documented in `Android/docs/argus-rollout-policy.md`: the app does not bypass signer mismatch; users must back up unsynced local files, uninstall the debug-signed build, then install the current stable APK from Argus. The Android app now shows a reinstall-required dialog for this certificate-mismatch path.
## Remaining P0
- Back up the gitignored Android release signing files created under `Android/signing/` and `Android/argus-signing.local.properties` in a secure external secret store. Without this key material, future in-place Android updates from the production-signed `1.3.10` cannot be signed with the same certificate.
- Back up the gitignored Android release signing files created under `Android/signing/` and `Android/argus-signing.local.properties` in a secure external secret store. Without this key material, future in-place Android updates from the production-signed `1.3.11` cannot be signed with the same certificate.
- Provide CI/Argus signing secrets derived from the same release key: `QSFERA_RELEASE_KEYSTORE`, `QSFERA_RELEASE_KEYSTORE_PASSWORD`, `QSFERA_RELEASE_KEY_ALIAS`, and `QSFERA_RELEASE_KEY_PASSWORD`.
- Fill production domains, ACME email, admin passwords, Keycloak passwords, and storage secrets from a secret manager. Do not commit real values.
- Run `./validate-production-env.sh .env` and then `docker compose config` for `Server/devtools/deployments/qsfera_full` with a filled production `.env`; Docker CLI is not available in this local environment, so compose rendering is not locally verified.
@@ -84,7 +86,7 @@ Date: 2026-06-08.
## Remaining P1
- Add Google Photos-style backup status UX: overall status, per-item status, queued/paused/permanent-failure states, and low-storage messaging.
- Extend Android backup status UX beyond the new overall settings summary with per-item status, queued/paused/permanent-failure states, and low-storage messaging.
- Store auto-upload sync cursors per source folder rather than one timestamp for all folders.
- Configure `QSFERA_ALERT_WEBHOOK_URL` in `/etc/qsfera/rpi-production-check.env` if off-host alert delivery is required; the installed failure service currently logs through `systemd-cat` and journal without an external endpoint.
- Verify sharing controls against Google Drive and Yandex Disk expectations: link expiration, password, disable downloads, organization-only access, and role inheritance.
+3 -3
View File
@@ -6,12 +6,12 @@ param(
[string]$AndroidManifestUrl = "https://argus.kusoft.xyz/api/apps/qsfera-mobile/manifest?platform=android&channel=stable",
[string]$ExpectedAndroidSlug = "qsfera-mobile",
[string]$ExpectedAndroidPackageName = "eu.qsfera.android",
[string]$ExpectedAndroidVersion = "1.3.10",
[int]$ExpectedAndroidVersionCode = 38,
[string]$ExpectedAndroidVersion = "1.3.11",
[int]$ExpectedAndroidVersionCode = 39,
[string]$ExpectedAndroidChannel = "stable",
[string]$ExpectedAndroidPlatform = "android",
[string]$ExpectedAndroidPackageKind = "apk",
[string]$ExpectedAndroidSha256 = "ce33a73ce122de6c9c797e7cb552e5493b628c60ddc98fda6a9e230e3c1ed7fe",
[string]$ExpectedAndroidSha256 = "c9eae41bbaae421e1a69721e891d8431ce599684e0f655b71693a422aa83720a",
[string]$AndroidDownloadUrl = "https://argus.kusoft.xyz/api/apps/qsfera-mobile/download/latest?platform=android&channel=stable",
[switch]$VerifyAndroidDownload,