From e30c008f7e1f8fd1b043f35cea44d89ce4a72e3e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 18:51:16 +0000 Subject: [PATCH] Honour the export and delete promises for everything the app now stores The disclaimer promises "export a copy of all your data" and deletion via Delete All Data, and the backup dialog promises phone-transfer fidelity. An audit against the current schema and preferences found gaps on all three surfaces (export, import, delete); this closes them. Export (full backup v4 to v5): - Groups exported (name, colorRole, defaultInputType, in display order) plus a per-category groupName, matched by name on import since row ids change across a restore. Without this, restores lost all grouping and turned inherit-coloured categories neutral. - Per-log loggedAt exported so time-tracked history keeps its times. - Colour profiles now carry lightBackgroundArgb/darkBackgroundArgb. - Settings now carry customLight/DarkBackgroundArgb, customThemeName, periodTrackingEnabled, periodGapToleranceDays (validated 0..3 on import), dailyCheckEnabled, and flowLevelRestoreDone. Import: - New importGroups (replace mode clears existing groups first, never their member categories) runs before category config; categories are filed via the exported groupName; backups without the key leave existing filing untouched. - Log import passes the category's allowMultiple and the exported loggedAt to saveLog. Previously a day with several entries collapsed to its last entry and every timestamp was dropped on restore. Delete: - Reset category settings also deletes groups (user-entered group names can be sensitive), unfiling members first; new additive DAO queries clearAllGroupAssignments and deleteAllGroups. - Delete All Data also clears the saved pregnancy date and the cached export files, and the dialog copy now says so. - Export cache no longer accumulates: date-stamped files meant the old "overwritten every export" comment was wrong, so each export clears the directory first and clearExportCache is shared with delete. Copy: full-backup description mentions groups; fixed pre-existing en/em dashes in the custom range separator and the Merge/Replace dialog text. LESSONS.md gains the transferable lesson; subsystem map 02 updated. Claude-Session: https://claude.ai/code/session_01PZJLynVBkgLtehJFXffnfg Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PZJLynVBkgLtehJFXffnfg --- LESSONS.md | 3 + .../goflo/data/database/dao/GroupDao.kt | 4 + .../data/database/dao/TrackingCategoryDao.kt | 4 + .../mapgie/goflo/data/export/DataExporter.kt | 22 +++- .../data/repository/TrackingRepository.kt | 11 +- .../screens/settings/ExportOptionsDialog.kt | 4 +- .../ui/screens/settings/SettingsScreen.kt | 14 ++- .../ui/screens/settings/SettingsViewModel.kt | 118 ++++++++++++++++-- .../export-delete-completeness.json | 12 ++ .../subsystem-maps/02-category-data-model.md | 2 +- 10 files changed, 167 insertions(+), 27 deletions(-) create mode 100644 changelog/unreleased/export-delete-completeness.json diff --git a/LESSONS.md b/LESSONS.md index c56a175..815790d 100644 --- a/LESSONS.md +++ b/LESSONS.md @@ -35,6 +35,9 @@ When a SmallFloatingActionButton contains an Icon with `contentDescription = nul **`ModalBottomSheetProperties` requires all parameters explicitly in Material3 1.2.x** The constructor has no default values in this version — passing only `shouldDismissOnBackPress` fails to compile. Always supply all three: `securePolicy = SecureFlagPolicy.Inherit, isFocusable = true, shouldDismissOnBackPress = false`. `SecureFlagPolicy` also needs an explicit import from `androidx.compose.ui.window`. +**Every schema or preference addition must sweep the full-data surfaces: export, import, and delete-all — a reminder comment there is not enforcement** +An app that promises "export all your data" and "delete all your data" has hidden contracts in its backup writer, backup importer, and delete-everything path. New tables and health-bearing preferences silently break all three: the data exists, round-trips nowhere, and survives deletion. A NOTE comment in the delete method asking future authors to keep it in sync did not survive contact with the next table (groups shipped without touching any of the three). The working defence is a checklist step on every schema/preference PR: grep for the export builder, the importer, and the delete-all implementation, and either update each or record why the new data is exempt (pure view state, id-based selections that cannot survive a restore). Restores also need the writer and reader to agree on more than field names: flags like allow-multiple change the SAVE semantics, so an importer that calls the ordinary save path with defaults quietly collapses data that the export faithfully contains. + **A classification defined by negation ("anything but X") silently misclassifies new variants** `TrackingCategory.isNumeric` was `categoryType != "default"`, which was correct while every non-default type happened to store numbers. Adding the label-valued "yes_no" and "time" types would have silently routed "Yes"/"HH:mm" strings into numeric chart math (`toFloatOrNull()` returning null everywhere) with no compile error, because a negated predicate auto-includes every future variant. When a derived property gates behaviour, define membership positively (enumerate the types that ARE numeric); then a new variant defaults to the safe side and the property's KDoc records why. Grep for `!=` against discriminator fields whenever adding a variant to a string-keyed or enum type. diff --git a/app/src/main/java/com/mapgie/goflo/data/database/dao/GroupDao.kt b/app/src/main/java/com/mapgie/goflo/data/database/dao/GroupDao.kt index 51897fe..c3ea756 100644 --- a/app/src/main/java/com/mapgie/goflo/data/database/dao/GroupDao.kt +++ b/app/src/main/java/com/mapgie/goflo/data/database/dao/GroupDao.kt @@ -35,4 +35,8 @@ interface GroupDao { @Delete suspend fun deleteGroup(group: Group) + + /** Removes every group. Callers must unfile member categories first. */ + @Query("DELETE FROM `groups`") + suspend fun deleteAllGroups() } diff --git a/app/src/main/java/com/mapgie/goflo/data/database/dao/TrackingCategoryDao.kt b/app/src/main/java/com/mapgie/goflo/data/database/dao/TrackingCategoryDao.kt index 44c1015..98b9ede 100644 --- a/app/src/main/java/com/mapgie/goflo/data/database/dao/TrackingCategoryDao.kt +++ b/app/src/main/java/com/mapgie/goflo/data/database/dao/TrackingCategoryDao.kt @@ -69,6 +69,10 @@ interface TrackingCategoryDao { @Query("UPDATE tracking_categories SET groupId = NULL WHERE groupId = :groupId") suspend fun clearGroupAssignments(groupId: Long) + /** Unfiles every category. Called before all groups are deleted in a reset. */ + @Query("UPDATE tracking_categories SET groupId = NULL") + suspend fun clearAllGroupAssignments() + // ── Values ──────────────────────────────────────────────────────────── @Query("SELECT * FROM tracking_values WHERE categoryId = :categoryId ORDER BY displayOrder ASC, id ASC") diff --git a/app/src/main/java/com/mapgie/goflo/data/export/DataExporter.kt b/app/src/main/java/com/mapgie/goflo/data/export/DataExporter.kt index 80de6c8..c2b8f6e 100644 --- a/app/src/main/java/com/mapgie/goflo/data/export/DataExporter.kt +++ b/app/src/main/java/com/mapgie/goflo/data/export/DataExporter.kt @@ -11,13 +11,25 @@ import java.time.LocalDate * ACTION_SEND intent pointing at it via FileProvider, ready to pass to * Context.startActivity(Intent.createChooser(...)). * - * The cache file lives in context.cacheDir/exports/ and is overwritten on - * every export — no accumulation of old files. + * The cache file lives in context.cacheDir/exports/. Because the file name + * embeds the date, every export first clears the directory so full health + * exports never accumulate in the cache; "Delete All Data" clears it too + * via [clearExportCache]. */ object DataExporter { + /** Removes every previously written export file from the cache. */ + fun clearExportCache(context: Context) { + File(context.cacheDir, "exports").deleteRecursively() + } + + private fun freshExportDir(context: Context): File { + clearExportCache(context) + return File(context.cacheDir, "exports").also { it.mkdirs() } + } + fun buildShareIntent(context: Context, json: String): Intent { - val dir = File(context.cacheDir, "exports").also { it.mkdirs() } + val dir = freshExportDir(context) val file = File(dir, "goflo_export_${LocalDate.now()}.json") file.writeText(json, Charsets.UTF_8) @@ -41,7 +53,7 @@ object DataExporter { * for sharing a plain-text doctor visit summary. */ fun buildTextShareIntent(context: Context, text: String): Intent { - val dir = File(context.cacheDir, "exports").also { it.mkdirs() } + val dir = freshExportDir(context) val file = File(dir, "goflo_cycle_summary_${LocalDate.now()}.txt") file.writeText(text, Charsets.UTF_8) @@ -65,7 +77,7 @@ object DataExporter { * pointing at it via FileProvider, ready for Context.startActivity(). */ fun buildCsvShareIntent(context: Context, csv: String): Intent { - val dir = File(context.cacheDir, "exports").also { it.mkdirs() } + val dir = freshExportDir(context) val file = File(dir, "goflo_export_${LocalDate.now()}.csv") file.writeText(csv, Charsets.UTF_8) diff --git a/app/src/main/java/com/mapgie/goflo/data/repository/TrackingRepository.kt b/app/src/main/java/com/mapgie/goflo/data/repository/TrackingRepository.kt index 3f68fa1..b6bce57 100644 --- a/app/src/main/java/com/mapgie/goflo/data/repository/TrackingRepository.kt +++ b/app/src/main/java/com/mapgie/goflo/data/repository/TrackingRepository.kt @@ -518,11 +518,16 @@ class TrackingRepository( } /** - * Deletes all user-created categories and restores built-in categories to visible. - * Tracking logs for deleted categories are cascade-deleted by the FK constraint. - * Period data (periods, symptoms) is untouched. + * Deletes all user-created categories and groups, and restores built-in + * categories to visible. Tracking logs for deleted categories are + * cascade-deleted by the FK constraint. Period data (periods, symptoms) + * is untouched. Groups are category configuration, so a configuration + * reset removes them too (their user-entered names can be sensitive); + * built-in categories are unfiled rather than deleted. */ suspend fun resetCategoryConfiguration() { + categoryDao.clearAllGroupAssignments() + groupDao?.deleteAllGroups() categoryDao.deleteAllCustomCategories() categoryDao.unarchiveAllSystemCategories() } diff --git a/app/src/main/java/com/mapgie/goflo/ui/screens/settings/ExportOptionsDialog.kt b/app/src/main/java/com/mapgie/goflo/ui/screens/settings/ExportOptionsDialog.kt index d6b7d55..4b8f780 100644 --- a/app/src/main/java/com/mapgie/goflo/ui/screens/settings/ExportOptionsDialog.kt +++ b/app/src/main/java/com/mapgie/goflo/ui/screens/settings/ExportOptionsDialog.kt @@ -128,7 +128,7 @@ fun ExportOptionsDialog( ) { Text(customStart?.format(dateDisplayFmt) ?: "From") } - Text("–") + Text("to") OutlinedButton( onClick = { showEndPicker = true }, modifier = Modifier.weight(1f) @@ -200,7 +200,7 @@ fun ExportOptionsDialog( Text(f.name, style = MaterialTheme.typography.bodyMedium) Text( when (f) { - ExportFormat.JSON -> "Full backup — can be re-imported" + ExportFormat.JSON -> "Full backup, can be re-imported" ExportFormat.CSV -> "Spreadsheet-friendly flat table" }, style = MaterialTheme.typography.bodySmall, diff --git a/app/src/main/java/com/mapgie/goflo/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/mapgie/goflo/ui/screens/settings/SettingsScreen.kt index 335e198..b5087ae 100644 --- a/app/src/main/java/com/mapgie/goflo/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/mapgie/goflo/ui/screens/settings/SettingsScreen.kt @@ -486,14 +486,14 @@ fun SettingsScreen( Text("How should the imported data be handled?", style = MaterialTheme.typography.bodyMedium) Spacer(Modifier.height(4.dp)) Text( - "Merge — adds new periods; skips any whose start date already exists. " + + "Merge: adds new periods; skips any whose start date already exists. " + "Safe if you have already logged some entries on this device.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(Modifier.height(2.dp)) Text( - "Replace — deletes everything on this device first, then imports. " + + "Replace: deletes everything on this device first, then imports. " + "Use when moving all data from your old phone.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant @@ -554,7 +554,8 @@ fun SettingsScreen( text = { Text( "This will permanently remove all period logs, symptoms, tracking logs, " + - "and notes. Your category configuration is kept. This cannot be undone." + "notes, any saved pregnancy date, and cached export files. Your category " + + "and group configuration is kept. This cannot be undone." ) }, confirmButton = { @@ -575,8 +576,9 @@ fun SettingsScreen( title = { Text("Reset category settings?") }, text = { Text( - "This will delete all your custom categories and restore any hidden built-in " + - "categories. Your period logs and tracking history are kept. This cannot be undone." + "This will delete all your custom categories and groups, and restore any hidden " + + "built-in categories. Your period logs and tracking history are kept. " + + "This cannot be undone." ) }, confirmButton = { @@ -1635,7 +1637,7 @@ private fun ExportDataSubScreen( Column { Text("Full backup", style = MaterialTheme.typography.bodyMedium) Text( - "Data plus category names, values, colours, settings, and dashboard pins. Use for transferring to a new phone.", + "Data plus category names, values, colours, groups, settings, and dashboard pins. Use for transferring to a new phone.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/app/src/main/java/com/mapgie/goflo/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/mapgie/goflo/ui/screens/settings/SettingsViewModel.kt index aed5e01..f0bab64 100644 --- a/app/src/main/java/com/mapgie/goflo/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/mapgie/goflo/ui/screens/settings/SettingsViewModel.kt @@ -481,7 +481,7 @@ class SettingsViewModel( val metaTo = endDate ?: LocalDate.now() val root = JSONObject() - root.put("version", if (config.fullBackup) 4 else 2) + root.put("version", if (config.fullBackup) 5 else 2) root.put("exportedAt", LocalDate.now().toString()) if (config.fullBackup) root.put("fullBackup", true) val rangeObj = JSONObject() @@ -489,8 +489,24 @@ class SettingsViewModel( rangeObj.put("to", metaTo.toString()) root.put("dateRange", rangeObj) - // ── Full backup: category configuration, settings, and alarms ──────── + // ── Full backup: groups, category configuration, settings, alarms ──── if (config.fullBackup) { + // Groups first: categories reference them by name on import, since + // row ids change across a restore. + val allGroups = trackingRepository.getAllGroupsOnce() + if (allGroups.isNotEmpty()) { + val groupsArray = JSONArray() + allGroups.forEach { group -> + val groupObj = JSONObject() + groupObj.put("name", group.name) + groupObj.put("colorRole", group.colorRole) + groupObj.put("defaultInputType", group.defaultInputType) + groupsArray.put(groupObj) + } + root.put("groups", groupsArray) + } + val groupNameById = allGroups.associateBy({ it.id }, { it.name }) + val allCats = trackingRepository.getAllCategoriesOnce() val catsArray = JSONArray() allCats.forEach { cat -> @@ -513,6 +529,7 @@ class SettingsViewModel( catObj.put("showInLogPeriod", cat.showInLogPeriod) catObj.put("trackAgainstTime", cat.trackAgainstTime) catObj.put("modeKey", cat.modeKey) + catObj.put("groupName", cat.groupId?.let { groupNameById[it] } ?: "") val valArr = JSONArray() trackingRepository.getValuesForCategoryOnce(cat.id) .sortedBy { it.displayOrder } @@ -554,6 +571,13 @@ class SettingsViewModel( put("pregnancyDateStr", prefs.pregnancyDateStr) put("pregnancyStartType", prefs.pregnancyStartType) put("temperatureUnitCelsius", prefs.temperatureUnitCelsius) + put("customLightBackgroundArgb", prefs.customLightBackgroundArgb) + put("customDarkBackgroundArgb", prefs.customDarkBackgroundArgb) + put("customThemeName", prefs.customThemeName) + put("periodTrackingEnabled", prefs.periodTrackingEnabled) + put("periodGapToleranceDays", prefs.periodGapToleranceDays) + put("dailyCheckEnabled", prefs.dailyCheckEnabled) + put("flowLevelRestoreDone", prefs.flowLevelRestoreDone) put("preperiodEnabled", prefs.reminder.preperiodEnabled) put("preperiodDaysBefore", prefs.reminder.preperiodDaysBefore) put("ovulationEnabled", prefs.reminder.ovulationEnabled) @@ -599,6 +623,8 @@ class SettingsViewModel( profileObj.put("primaryArgb", profile.primaryArgb) profileObj.put("secondaryArgb", profile.secondaryArgb) profileObj.put("tertiaryArgb", profile.tertiaryArgb) + profileObj.put("lightBackgroundArgb", profile.lightBackgroundArgb) + profileObj.put("darkBackgroundArgb", profile.darkBackgroundArgb) profilesArray.put(profileObj) } if (profilesArray.length() > 0) root.put("colorProfiles", profilesArray) @@ -659,6 +685,7 @@ class SettingsViewModel( entry.values.forEach { valArray.put(it) } logObj.put("values", valArray) logObj.put("notes", entry.log.notes) + if (entry.log.loggedAt.isNotBlank()) logObj.put("loggedAt", entry.log.loggedAt) logsArray.put(logObj) } catObj.put("logs", logsArray) @@ -772,7 +799,12 @@ class SettingsViewModel( runCatching { if (!json.trimStart().startsWith('[')) { val root = JSONObject(json) - // Full backup: restore category configuration first so log import can match names. + // Full backup: restore groups first (categories reference them by + // name), then category configuration so log import can match names. + val groupsArray = root.optJSONArray("groups") + if (groupsArray != null || replace) { + importGroups(groupsArray, replace) + } val categoriesArray = root.optJSONArray("categories") if (categoriesArray != null) { importCategoryConfig(categoriesArray, replace) @@ -819,6 +851,19 @@ class SettingsViewModel( settingsObj.optString("pregnancyStartType", "EDD"), ) store.setTemperatureUnitCelsius(settingsObj.optBoolean("temperatureUnitCelsius", true)) + store.setCustomBackgroundArgbs( + settingsObj.optInt("customLightBackgroundArgb", 0), + settingsObj.optInt("customDarkBackgroundArgb", 0), + ) + store.setCustomThemeName(settingsObj.optString("customThemeName", "")) + store.setPeriodTrackingEnabled(settingsObj.optBoolean("periodTrackingEnabled", true)) + settingsObj.optInt("periodGapToleranceDays", -1) + .takeIf { it in 0..3 } + ?.let { store.setPeriodGapToleranceDays(it) } + store.setDailyCheckEnabled(settingsObj.optBoolean("dailyCheckEnabled", true)) + if (settingsObj.optBoolean("flowLevelRestoreDone", false)) { + store.setFlowLevelRestoreDone(true) + } store.setPreperiodEnabled(settingsObj.optBoolean("preperiodEnabled", false)) store.setPreperiodDaysBefore(settingsObj.optInt("preperiodDaysBefore", 2)) store.setOvulationEnabled(settingsObj.optBoolean("ovulationEnabled", false)) @@ -848,6 +893,8 @@ class SettingsViewModel( primaryArgb = obj.optInt("primaryArgb", 0), secondaryArgb = obj.optInt("secondaryArgb", 0), tertiaryArgb = obj.optInt("tertiaryArgb", 0), + lightBackgroundArgb = obj.optInt("lightBackgroundArgb", 0), + darkBackgroundArgb = obj.optInt("darkBackgroundArgb", 0), )) } } @@ -859,6 +906,32 @@ class SettingsViewModel( } } + /** + * Restores category groups from a full backup. Groups are matched by name + * because row ids change across a restore; in replace mode all existing + * groups are removed first (never their member categories, which are + * unfiled and re-filed by the category import that follows). + */ + private suspend fun importGroups(groupsArray: JSONArray?, replace: Boolean) { + if (replace) { + trackingRepository.getAllGroupsOnce().forEach { trackingRepository.deleteGroup(it.id) } + } + if (groupsArray == null) return + for (i in 0 until groupsArray.length()) { + val obj = groupsArray.getJSONObject(i) + val name = obj.optString("name").takeIf { it.isNotBlank() } ?: continue + val colorRole = obj.optString("colorRole", "primary") + val inputType = obj.optString("defaultInputType", "default") + val existing = trackingRepository.getAllGroupsOnce().firstOrNull { it.name == name } + if (existing == null) { + trackingRepository.addGroup(name, colorRole, inputType) + } else { + trackingRepository.updateGroupRole(existing.id, colorRole) + trackingRepository.updateGroupDefaultInputType(existing.id, inputType) + } + } + } + private suspend fun importTrackingLogs(trackingArray: JSONArray, replace: Boolean) { for (i in 0 until trackingArray.length()) { val catObj = trackingArray.getJSONObject(i) @@ -872,7 +945,8 @@ class SettingsViewModel( if (isArchived) trackingRepository.archiveCategory(newId) category = trackingRepository.getCategoryByIdOnce(newId) } - val categoryId = category?.id ?: continue + val cat = category ?: continue + val categoryId = cat.id val logsArray = catObj.optJSONArray("logs") ?: continue for (j in 0 until logsArray.length()) { @@ -888,12 +962,20 @@ class SettingsViewModel( for (k in 0 until valuesArray.length()) add(valuesArray.getString(k)) } } - trackingRepository.saveLog(date, categoryId, values, logObj.optString("notes", "")) + // Honour allow-multiple and per-tap timestamps: without them, a + // day with several entries collapses to one on restore and + // time-tracked history loses its times. + trackingRepository.saveLog( + date, categoryId, values, logObj.optString("notes", ""), + allowMultiple = cat.allowMultiple, + loggedAt = logObj.optString("loggedAt", ""), + ) } } } private suspend fun importCategoryConfig(categoriesArray: JSONArray, replace: Boolean) { + val groupIdByName = trackingRepository.getAllGroupsOnce().associateBy({ it.name }, { it.id }) for (i in 0 until categoriesArray.length()) { val catObj = categoriesArray.getJSONObject(i) val catName = catObj.optString("name").takeIf { it.isNotBlank() } ?: continue @@ -949,6 +1031,17 @@ class SettingsViewModel( trackingRepository.unarchiveCategory(categoryId) } + // Restore group membership (backups from before groups existed have + // no "groupName" key; leave those categories' filing untouched). + if (catObj.has("groupName")) { + val groupId = groupIdByName[catObj.optString("groupName", "")] + if (groupId != null) { + trackingRepository.assignCategoryToGroup(categoryId, groupId) + } else { + trackingRepository.unassignCategory(categoryId) + } + } + // Restore values (labels/options) for this category. val valuesArray = catObj.optJSONArray("values") ?: continue val existingValues = trackingRepository.getValuesForCategoryOnce(categoryId) @@ -963,17 +1056,22 @@ class SettingsViewModel( } /** - * Permanently deletes all stored data (periods, symptoms, and tracking logs), - * then reschedules reminders (which will cancel predictive alarms now that - * data is gone). Categories and their value definitions are preserved. + * Permanently deletes all stored health data (periods, symptoms, tracking + * logs, the saved pregnancy date, and any cached export files), then + * reschedules reminders (which will cancel predictive alarms now that data + * is gone). Categories, groups, and value definitions are configuration and + * are preserved; "Reset category settings" removes those. * - * NOTE: whenever new data tables are added, this method must be updated - * to include them — and the same applies to importData and exportWithOptions. + * NOTE: whenever new data tables or health-bearing preferences are added, + * this method must be updated to include them — and the same applies to + * importData and exportWithOptions. */ fun deleteAllData(onComplete: () -> Unit) { viewModelScope.launch { repository.deleteAllData() trackingRepository.deleteAllLogs() + store.setPregnancyDate("", "EDD") + DataExporter.clearExportCache(context) reschedule() onComplete() } diff --git a/changelog/unreleased/export-delete-completeness.json b/changelog/unreleased/export-delete-completeness.json new file mode 100644 index 0000000..03f94e3 --- /dev/null +++ b/changelog/unreleased/export-delete-completeness.json @@ -0,0 +1,12 @@ +{ + "bump": "minor", + "added": [ + "Full backups now include category groups and group membership, so restoring on a new phone keeps your grouping and group-inherited colours" + ], + "fixed": [ + "Restoring a backup no longer loses per-entry times or collapses a day with several entries into one", + "Full backups now carry custom theme backgrounds, saved palette backgrounds, the period gap tolerance, period tracking on/off, and other previously missed settings", + "Delete All Data now also removes any saved pregnancy date and cached export files, and Reset category settings now also removes groups", + "Exported files no longer accumulate in the app cache: each new export clears the previous one" + ] +} diff --git a/docs/design/logging-redesign/subsystem-maps/02-category-data-model.md b/docs/design/logging-redesign/subsystem-maps/02-category-data-model.md index 906fbba..2b40154 100644 --- a/docs/design/logging-redesign/subsystem-maps/02-category-data-model.md +++ b/docs/design/logging-redesign/subsystem-maps/02-category-data-model.md @@ -92,7 +92,7 @@ Values: `getValuesForCategory`/`…Once`, `insertValue` (IGNORE), `updateValue`, Groups *(v24)*: `assignCategoryToGroup(categoryId, groupId?)`, `clearGroupAssignments(groupId)`. ### GroupDao *(v24)* -`getAllGroups(): Flow` / `getAllGroupsOnce()` (ordered `displayOrder, name`), `getGroupById`, `insertGroup` (REPLACE→Long), `updateGroup`, `deleteGroup`. Table name is always backticked in raw SQL (`groups` is keyword-adjacent). +`getAllGroups(): Flow` / `getAllGroupsOnce()` (ordered `displayOrder, name`), `getGroupById`, `insertGroup` (REPLACE→Long), `updateGroup`, `deleteGroup`, `deleteAllGroups` (config reset; callers unfile members first via `TrackingCategoryDao.clearAllGroupAssignments`). Table name is always backticked in raw SQL (`groups` is keyword-adjacent). Groups round-trip through the v5 full backup by name (`groups` array + per-category `groupName`), and `resetCategoryConfiguration` removes them. ### TrackingLogDao Logs: `getLogsForDate`/`…Once`; `getAllLogDates`; `getLogById`/`…Once`; `getLogForDateAndCategory` (LIMIT 1); `getLogsForDateAndCategory` (multiple, ordered by loggedAt); `insertLog` (REPLACE→Long), `updateLog`, `deleteLog`. Log values: `getLogValuesForLog`/`…Once`, `insertLogValue`, `deleteLogValuesForLog`. Stats/export: `getLogsForCategoryInRange`, `getValueCountsForCategory` (→ `ValueCount`), `getAllLogsInRange`, `getLogsForCategoriesInRange`, `getAllLogsForCategories`, `getLogValuesForLogs`, `getEarliest/LatestLogDate`, delete ranges/date/all.