From b25f4ff2447e05d5048f6c5c9bd0ae79db92badc Mon Sep 17 00:00:00 2001 From: James Maes Date: Fri, 16 Oct 2026 09:08:20 -0500 Subject: [PATCH] fix(app): Backups Setup and Needs attention derive from one list The home said "1 item needs attention" in Setup while Needs attention said "Nothing needs attention" (unresolved storage aliases). Every Setup item that needs setup is now also listed under Needs attention, from the same item list. Co-Authored-By: Claude Opus 5.5 --- .../qbits/backups/app/BackupsHomeView.java | 30 ++++++++------ .../qbits/backups/app/BackupsHomeTest.java | 41 ++++++++++++++++++- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/kingsrook/qbits/backups/app/BackupsHomeView.java b/src/main/java/com/kingsrook/qbits/backups/app/BackupsHomeView.java index cddfac4..796c814 100644 --- a/src/main/java/com/kingsrook/qbits/backups/app/BackupsHomeView.java +++ b/src/main/java/com/kingsrook/qbits/backups/app/BackupsHomeView.java @@ -69,10 +69,11 @@ public final class BackupsHomeView public static String render(BackupsHomeData data, String pathPrefix) { StringBuilder html = new StringBuilder("
"); + List setupItems = BackupsSetupStatus.load(data.plans()); planStatus(html, data, pathPrefix); - setup(html, data, pathPrefix); + setup(html, setupItems, pathPrefix); html.append("
"); - needsAttention(html, data, pathPrefix); + needsAttention(html, data, setupItems, pathPrefix); latestTestRestore(html, data, pathPrefix); recentActivity(html, data, pathPrefix); trend(html, data); @@ -144,9 +145,8 @@ public final class BackupsHomeView ** The Setup card: what a working backup needs, whether each part is in place, ** and the next step, linked to its table. *******************************************************************************/ - private static void setup(StringBuilder html, BackupsHomeData data, String prefix) + private static void setup(StringBuilder html, List items, String prefix) { - List items = BackupsSetupStatus.load(data.plans()); open(html, "Setup"); long todo = items.stream().filter(i -> i.state() == BackupsSetupStatus.State.TODO).count(); html.append("

") @@ -185,23 +185,27 @@ public final class BackupsHomeView /******************************************************************************* - ** Needs attention. + ** Needs attention: every Setup item that needs setup (the same list the Setup + ** card counts), plus plans in a problem state and recent failed runs, so the two + ** cards never disagree. *******************************************************************************/ - private static void needsAttention(StringBuilder html, BackupsHomeData data, String prefix) + private static void needsAttention(StringBuilder html, BackupsHomeData data, List setupItems, String prefix) { open(html, "Needs attention"); - List plans = data.plansNeedingAttention(); - if(data.plans().isEmpty()) + List setupTodo = setupItems.stream().filter(i -> i.state() == BackupsSetupStatus.State.TODO).toList(); + List plans = data.plansNeedingAttention(); + if(setupTodo.isEmpty() && plans.isEmpty() && data.failedRuns().isEmpty()) { - empty(html, "Nothing to check yet: no plans."); - } - else if(plans.isEmpty() && data.failedRuns().isEmpty()) - { - empty(html, "Nothing needs attention."); + empty(html, data.plans().isEmpty() ? "Nothing to check yet: no plans." : "Nothing needs attention."); } else { html.append("

    "); + for(BackupsSetupStatus.Item item : setupTodo) + { + html.append("
  • Setup: ").append(esc(item.label())).append(": ") + .append(esc(item.detail())).append("
  • "); + } for(QRecord plan : plans) { html.append("
  • ").append(recordLink(prefix, BackupsTables.PLAN, plan.getValue("id"), plan.getValueString("name"))) diff --git a/src/test/java/com/kingsrook/qbits/backups/app/BackupsHomeTest.java b/src/test/java/com/kingsrook/qbits/backups/app/BackupsHomeTest.java index 23a9d56..550cff3 100644 --- a/src/test/java/com/kingsrook/qbits/backups/app/BackupsHomeTest.java +++ b/src/test/java/com/kingsrook/qbits/backups/app/BackupsHomeTest.java @@ -88,7 +88,7 @@ class BackupsHomeTest void testEmptyStates() { String html = BackupsHomeView.render(new BackupsHomeData(List.of(), List.of(), List.of(), List.of(), List.of(), Instant.now()), "/app"); - assertThat(html).contains("No backup plans yet").contains("Nothing to check yet").contains("No test restore yet") + assertThat(html).contains("No backup plans yet").contains("Setup: BackupsTestUtils.BACKUPS); + new BackupCatalog().insert(BackupsTables.PLAN, new LinkedHashMap<>(Map.of("name", "Offsite", "planKey", "offsite", "isActive", true, "scopeType", "INSTANCE", + "storageBucketAlias", "backupsAws", "pruneBucketAlias", "backupsAwsPrune"))); + + ////////////////////////////////////////////////////////////////////////////// + // the aliases resolved when the plan was saved; now the bucket is gone // + ////////////////////////////////////////////////////////////////////////////// + BackupsRuntime.getConfig().withStorageResolver(alias -> "backups".equals(alias) ? BackupsTestUtils.BACKUPS : null); + + BackupsHomeData data = BackupsHomeData.load(); + List setupTodo = BackupsSetupStatus.load(data.plans()).stream().filter(i -> i.state() == BackupsSetupStatus.State.TODO).toList(); + assertThat(setupTodo).extracting(BackupsSetupStatus.Item::label).contains("Storage"); + + String html = BackupsHomeView.render(data, "/app"); + String attention = html.substring(html.indexOf(">Needs attention<")); + attention = attention.substring(0, attention.indexOf("All runs")); + assertThat(attention).doesNotContain("Nothing needs attention").contains("backupsAws").contains("backupsAwsPrune"); + + /////////////////////////////////////////////////////////////////////////////// + // every Setup TODO is listed under Needs attention (counts agree: 2 and 2) // + /////////////////////////////////////////////////////////////////////////////// + assertThat(html).contains(setupTodo.size() + " items need attention."); + for(BackupsSetupStatus.Item item : setupTodo) + { + assertThat(attention).contains("Setup: ", -1).length - 1).isEqualTo(setupTodo.size() + data.plansNeedingAttention().size() + data.failedRuns().size()); + } + + + /******************************************************************************* ** The links follow the configured table path prefix. *******************************************************************************/ -- 2.54.0