Skip to content

Feature/hjemlo#507

Merged
NielsPilgaard merged 24 commits into
mainfrom
feature/hjemlo
Feb 26, 2026
Merged

Feature/hjemlo#507
NielsPilgaard merged 24 commits into
mainfrom
feature/hjemlo

Conversation

@NielsPilgaard

Copy link
Copy Markdown
Owner

No description provided.

@coderabbitai

coderabbitai Bot commented Feb 22, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@NielsPilgaard has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 21 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between a2a3e3f and 036bc26.

📒 Files selected for processing (3)
  • src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs
  • src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor
  • tasks/13-cheaper-infrastructure.md

Walkthrough

Adds a HJEM groups feature: models, blob-backed admin service with geocoding, a provider that maps/caches markers, backoffice CRUD UI, GroupSearch integration to merge additional markers, a DataForsyningen zip-code search client method, DI registration, UI/CSS/JS tweaks, and unit/integration tests.

Changes

Cohort / File(s) Summary
DataForsyningen API
src/shared/Jordnaer.Shared/UserSearch/IDataForsyningenClient.cs
Adds SearchZipCodesAsync([AliasAs("q")] string query) mapped to GET /postnumre, returning IApiResponse<IEnumerable<ZipCodeSearchResponse>>.
Hjem Groups Model
src/web/Jordnaer/Features/HjemGroups/HjemGroupEntry.cs
Adds HjemGroupEntry record and HjemGroupType enum for HJEM group metadata.
Admin Service
src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs
New HjemGroupAdminService with LoadAsync, SaveAsync, GeocodeAsync; blob read/write with container checks, camelCase JSON, cache invalidation, and GeocodeResult/error records.
Provider & Interface
src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs
Adds/updates IHjemGroupProvider and HjemGroupProvider: fusion cache usage, OneOf return type for errors, blob loading, mapping to GroupMarkerData with deterministic MD5->GUID IDs, and logging.
DI / Startup
src/web/Jordnaer/Features/HjemGroups/WebApplicationBuilderExtensions.cs, src/web/Jordnaer/Program.cs
Adds AddHjemGroupServices() and registers IHjemGroupProvider and HjemGroupAdminService as scoped; wires feature into Program.
Backoffice UI
src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor
New Blazor page for managing HJEM groups: list, add/edit dialog, delete, geocode lookup, validation, and save via HjemGroupAdminService.
GroupSearch integration
src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor, src/web/Jordnaer/Pages/GroupSearch/GroupSearch.razor
GroupSearchForm gains AdditionalMarkers parameter and merges/filters them with regular markers; GroupSearch.razor injects IHjemGroupProvider and supplies markers.
Tests — HjemGroups
tests/web/Jordnaer.Tests/HjemGroups/*
Adds extensive unit tests for HjemGroupProvider and HjemGroupAdminService: blob interactions, mapping, deterministic IDs, caching, serialization, geocoding, and error paths.
Tests — DataForsyningen
tests/web/Jordnaer.Tests/UserSearch/DataForsyningenClientTests.cs
Adds integration tests for SearchZipCodesAsync: known-city results, gibberish query, and visueltcenter coordinate ordering/validation.
UI / CSS / JS tweaks
src/web/Jordnaer/Components/NotificationItem.razor, src/web/Jordnaer/Components/NotificationItem.razor.css, src/web/Jordnaer/Pages/Shared/TopBar.razor, src/web/Jordnaer/wwwroot/css/marker-cluster.css, src/web/Jordnaer/wwwroot/js/leaflet-interop.js
Small rendering change for notification description, added CSS line-clamp, Backoffice menu item "HJEM Grupper", marker/popup color fallbacks, and popup link selection logic preferring same-domain site links when appropriate.
Docs / Tasks
tasks/13-cheaper-infrastructure.md
New migration plan to move hosting to Hetzner + Dokploy with steps, risks, rollback, and alternatives.

Sequence Diagrams

sequenceDiagram
    participant Page as GroupSearch.razor
    participant Provider as IHjemGroupProvider
    participant Blob as Azure Blob Storage
    participant Form as GroupSearchForm.razor
    participant Map as MapComponent

    Page->>Provider: GetMarkersAsync()
    Provider->>Blob: Read container "hjemlo-groups"/blob "groups.json"
    Blob-->>Provider: JSON / NotFound / Error
    Provider->>Provider: Deserialize -> List<HjemGroupEntry]
    Provider->>Provider: Map entries -> GroupMarkerData (MD5->GUID)
    Provider-->>Page: OneOf<IReadOnlyList<GroupMarkerData>, Error>
    Page->>Form: Pass AdditionalMarkers
    Form->>Form: Filter by Name, merge AdditionalMarkers + regular markers
    Form->>Map: Provide combined markers
    Map-->>Page: Render markers
Loading
sequenceDiagram
    participant UI as HjemGroupManagementPage
    participant Admin as HjemGroupAdminService
    participant Blob as Azure Blob Storage
    participant API as IDataForsyningenClient
    participant EventBus as IPublishEndpoint

    UI->>Admin: LoadAsync()
    Admin->>Blob: Get blob "groups.json"
    Blob-->>Admin: Content / NotFound / Error
    Admin-->>UI: List<HjemGroupEntry> or HjemGroupLoadError

    UI->>Admin: GeocodeAsync(locationText)
    Admin->>API: SearchZipCodesAsync(query)
    API-->>Admin: ZipCodeSearchResponse[]
    Admin->>Admin: Extract city, zip, coordinates
    Admin-->>UI: GeocodeResult or null

    UI->>Admin: SaveAsync(entries)
    Admin->>Blob: Create container if needed
    Admin->>Blob: Upload "groups.json" (camelCase, overwrite)
    Blob-->>Admin: Confirm upload
    Admin->>EventBus: Publish cache invalidation
    Admin-->>UI: Success or HjemGroupSaveError
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (1 warning, 2 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Feature/hjemlo' is vague and uses a generic feature branch naming convention without describing what the feature actually does or its main purpose. Replace with a descriptive title that explains the primary change, such as 'Add Hjem group management feature with backoffice UI and markers integration'.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess if the description relates to the changeset. Add a description explaining the purpose of the changes, key features added, and how they integrate with the existing codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/hjemlo

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (7)
tests/web/Jordnaer.Tests/UserSearch/DataForsyningenClientTests.cs (1)

79-79: Inconsistent assertion: IsSuccessStatusCode vs IsSuccessful.

The new tests use IsSuccessStatusCode (Lines 79, 99, 113) while the existing tests on Lines 26, 40, 54, 65 use IsSuccessful. IsSuccessful on IApiResponse additionally verifies deserialization succeeded, making it the stricter (and more useful) check. Consider aligning with the existing pattern for consistency.

Suggested fix
-		response.IsSuccessStatusCode.Should().BeTrue();
+		response.IsSuccessful.Should().BeTrue();

(Apply in all three new tests.)

Also applies to: 99-99, 113-113

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/web/Jordnaer.Tests/UserSearch/DataForsyningenClientTests.cs` at line
79, The tests added in UserSearch/DataForsyningenClientTests.cs assert
response.IsSuccessStatusCode but the project uses the stricter
IApiResponse.IsSuccessful pattern; update the three new assertions (the ones
currently checking IsSuccessStatusCode) to assert response.IsSuccessful instead
so deserialization success is also validated, keeping behavior consistent with
the existing tests that check IsSuccessful on IApiResponse.
src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs (1)

28-68: Cache invalidation concern if lifetime changes to Singleton.

If HjemGroupProvider is promoted to Singleton (as suggested in the DI registration review), note that Lines 39 and 47 cache an empty list when the container/blob doesn't exist. Once cached, new data uploaded to blob storage will never be picked up for the lifetime of the application.

Consider adding a TTL-based invalidation or exposing a cache-reset method for the admin service to call after saving new data.

Sketch: nullable timestamp-based TTL
+    private DateTime? _cachedAt;
+    private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(15);
+
     public async Task<IReadOnlyList<GroupMarkerData>> GetMarkersAsync(CancellationToken cancellationToken = default)
     {
-        if (_cached is not null)
+        if (_cached is not null && _cachedAt.HasValue && DateTime.UtcNow - _cachedAt.Value < CacheDuration)
             return _cached;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs` around lines 28 -
68, GetMarkersAsync currently stores an empty list into the provider-level field
_cached when the blob container or blob is missing, which will permanently hide
newly uploaded data if HjemGroupProvider becomes a singleton; change the caching
strategy in HjemGroupProvider by either (A) adding a timestamp (e.g.,
_cacheTimestamp) and TTL check in GetMarkersAsync to refresh _cached when older
than the configured TTL before returning, or (B) add a public InvalidateCache()
/ ResetCache() method that clears _cached (and _cacheTimestamp if present) so
the admin service can call it after saving new data; keep the existing blob
checks (ContainerName/BlobName) but ensure GetMarkersAsync re-fetches when the
cache is expired or explicitly invalidated.
tests/web/Jordnaer.Tests/HjemGroups/HjemGroupIntegrationTests.cs (1)

182-197: act() is invoked twice — once by NotThrow() and once explicitly.

act.Should().NotThrow() already invokes the delegate. The second act()! invokes it again. This is harmless for string deserialization but is redundant work. Consider capturing the result from the first invocation.

♻️ Proposed fix
-        var act = () => JsonSerializer.Deserialize<List<HjemGroupEntry>>(json, CamelCase);
-        act.Should().NotThrow();
-        act()!.Should().NotBeEmpty();
+        var entries = JsonSerializer.Deserialize<List<HjemGroupEntry>>(json, CamelCase);
+        entries.Should().NotBeNull();
+        entries!.Should().NotBeEmpty();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupIntegrationTests.cs` around
lines 182 - 197, In FullPipeline_BlobContainsValidJson replace the
double-invocation of the delegate 'act' by invoking JsonSerializer.Deserialize
once: call the delegate (the expression that uses JsonSerializer.Deserialize
with CamelCase) and store its result in a variable, assert that invoking the
delegate does not throw (use act.Should().NotThrow()) if you must keep the
delegate, or better, directly perform the deserialize into a local variable and
assert the result is not null/empty (use the stored result for the subsequent
Should().NotBeEmpty() check) so JsonSerializer.Deserialize is only executed a
single time.
tests/web/Jordnaer.Tests/HjemGroups/HjemGroupScraperServiceTests.cs (2)

253-277: Same act() double-invocation pattern as in integration tests.

act.Should().NotThrow() invokes the delegate, then act()! invokes it again. Consider capturing the result directly.

♻️ Proposed fix
-        var act = () => JsonSerializer.Deserialize<List<HjemGroupEntry>>(
-            uploadedJson, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
-        act.Should().NotThrow();
-        act()!.Should().NotBeEmpty();
+        var entries = JsonSerializer.Deserialize<List<HjemGroupEntry>>(
+            uploadedJson, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
+        entries.Should().NotBeNull();
+        entries!.Should().NotBeEmpty();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupScraperServiceTests.cs` around
lines 253 - 277, The test currently invokes the deserialization delegate twice
(act.Should().NotThrow() then act()!), which duplicates work and can mask
issues; replace the delegate pattern by performing the deserialization once into
a local variable using JsonSerializer.Deserialize on uploadedJson (referencing
JsonSerializer.Deserialize and uploadedJson from the test), then assert the
deserialization succeeded (result is not null) and that the result collection is
not empty; update the assertions in ScrapeAndSaveAsync_SavesValidJson_ToBlob to
use the single captured variable instead of calling act() twice.

107-118: CaptureUploadedJson() is dead code with a latent bug.

This helper is never called — all tests that capture uploaded JSON do their own inline Arg.Do setup. Additionally, if it were used, it would always return string.Empty because captured is reassigned inside the Arg.Do lambda (after the method returns), but the caller holds a reference to the original immutable empty string.

Consider removing it or, if you intend to use it, switching to a wrapper class so the reference is shared:

♻️ Option A: Remove dead code
-    // Captures the uploaded JSON from the blob upload call
-    private string CaptureUploadedJson()
-    {
-        var captured = string.Empty;
-        _blobClient
-            .UploadAsync(Arg.Do<Stream>(s =>
-            {
-                using var sr = new StreamReader(s, Encoding.UTF8);
-                captured = sr.ReadToEnd();
-            }), Arg.Any<bool>(), Arg.Any<CancellationToken>())
-            .Returns(Task.FromResult(Substitute.For<Response<BlobContentInfo>>()));
-        return captured; // note: populated after the act
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupScraperServiceTests.cs` around
lines 107 - 118, The helper CaptureUploadedJson is dead/buggy: tests never call
it and its captured variable is assigned inside the Arg.Do lambda after the
method returns (so it always returns the initial empty string). Either delete
CaptureUploadedJson entirely and consolidate UploadAsync Arg.Do setups inline,
or convert it to return/use a mutable wrapper (e.g., a small class with a string
property) and have CaptureUploadedJson register the Arg.Do on
_blobClient.UploadAsync so the wrapper instance is mutated inside the lambda and
can be inspected after the act; reference CaptureUploadedJson, _blobClient and
UploadAsync when applying the change.
tests/web/Jordnaer.Tests/HjemGroups/HjemGroupProviderTests.cs (1)

85-120: Extract repeated JsonSerializerOptions to a shared static field.

new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase } is duplicated in nearly every test. A private static field (like the integration tests do with CamelCase) would reduce noise.

♻️ Proposed fix

Add a static field at the class level:

 public class HjemGroupProviderTests
 {
+    private static readonly JsonSerializerOptions CamelCase = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+
     private readonly BlobServiceClient _blobServiceClient = Substitute.For<BlobServiceClient>();

Then replace all inline usages, e.g.:

-        SetupBlobWithContent(JsonSerializer.Serialize(entries, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
+        SetupBlobWithContent(JsonSerializer.Serialize(entries, CamelCase));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupProviderTests.cs` around lines
85 - 120, Extract the repeated JsonSerializerOptions into a single shared static
field in the HjemGroupProviderTests class and use it everywhere; specifically
add a private static readonly JsonSerializerOptions CamelCase = new
JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase } in
the HjemGroupProviderTests class and replace inline constructions like new
JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }
(used in SetupBlobWithContent(...) and other test serialization calls inside
GetMarkersAsync_ReturnsMappedMarkers_ForLokalafdeling and other tests) with the
CamelCase field.
src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs (1)

23-44: LoadAsync silently swallows all exceptions.

The coding guidelines for src/web/Jordnaer/Features/**/*.cs recommend using OneOf<TSuccess, TError> for explicit error handling. Currently, callers cannot distinguish between "no data exists" and "load failed." Given the chill review mode, this is a recommended refactor that can be addressed later.

As per coding guidelines: src/web/Jordnaer/Features/**/*.cs: Use OneOf<TSuccess, TError> for explicit error handling in service methods.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs` around lines
23 - 44, Change LoadAsync to return Task<OneOf<List<HjemGroupEntry>,
HjemGroupLoadError>> instead of silently swallowing failures: update the
signature of LoadAsync and all callers to use OneOf; treat "no data exists"
(container/blob not found) as a successful result returning an empty
List<HjemGroupEntry>, but on exceptions return the HjemGroupLoadError variant
(include the caught Exception and a descriptive message) instead of returning an
empty list; keep logging via logger.LogWarning(ex, ...) but also populate the
error object so callers can distinguish load failures from empty data; use the
existing symbols (LoadAsync, blobServiceClient, ContainerName, BlobName,
HjemGroupEntry) to find and update the implementation and call sites.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs`:
- Around line 46-56: The container is being created with PublicAccessType.Blob
which grants unnecessary public read access; update the CreateIfNotExistsAsync
call in SaveAsync (the blobServiceClient -> containerClient creation flow using
ContainerName and BlobName) to use PublicAccessType.None so the container is not
publicly readable, retaining server-side-only access for HjemGroupProvider's
JSON blob handling.

In `@src/web/Jordnaer/Features/HjemGroups/WebApplicationBuilderExtensions.cs`:
- Line 7: The provider is registered with AddScoped which creates a new
HjemGroupProvider per request so its _cached field is always null; change the DI
registration from builder.Services.AddScoped<IHjemGroupProvider,
HjemGroupProvider>() to a singleton registration
(builder.Services.AddSingleton<IHjemGroupProvider, HjemGroupProvider>), or
alternatively refactor HjemGroupProvider to use IMemoryCache/FusionCache inside
GetMarkersAsync (inject IMemoryCache or FusionCache, store/retrieve the data
with a TTL) so the cache survives across requests; keep BlobServiceClient as
Singleton and ensure GetMarkersAsync uses the chosen shared cache rather than an
instance field.

In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor`:
- Around line 236-273: CommitEdit currently saves entries without validating
coordinates, causing default (0,0) markers; add validation in CommitEdit to
ensure _form.Latitude and _form.Longitude are present and within valid ranges
(latitude between -90 and 90, longitude between -180 and 180) and reject or warn
when both are zero (or when either is null/invalid) by calling Snackbar.Add and
returning early; only construct and add/update the HjemGroupEntry and modify
_entries when coordinates are valid so markers won't be created at the Gulf of
Guinea.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupIntegrationTests.cs`:
- Around line 42-48: The tests reference a missing implementation class
HjemGroupScraperService which causes the build to fail; add the
HjemGroupScraperService class under the HjemGroups feature implementing the
behavior the tests expect (constructor signature:
HjemGroupScraperService(HttpClient, IDataForsyningenClient,
BlobServiceClient/BlobServiceClient-like _blobServiceClient,
ILogger<HjemGroupScraperService>) and the methods used by
HjemGroupIntegrationTests and HjemGroupScraperServiceTests), ensure its public
API matches how CreateScraper constructs it (accepts HttpClient and
IDataForsyningenClient and uses the provided logger and blob client), and
implement or stub the scraping and geocoding flows referenced by the tests so
the tests can compile and run.

In `@tests/web/Jordnaer.Tests/UserSearch/DataForsyningenClientTests.cs`:
- Around line 112-118: Add a null-guard assertion for the HTTP response content
before accessing it: in the test in DataForsyningenClientTests.cs that uses
response.Content!.First(), call response.Content.Should().NotBeNull() (like the
earlier test) immediately after asserting response.IsSuccessStatusCode to ensure
response.Content is non-null before using response.Content.First(),
response.Content![0], and response.Content![1].

---

Duplicate comments:
In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupScraperServiceTests.cs`:
- Line 23: The tests reference a missing type HjemGroupScraperService causing
build failures; either update the tests to use the actual service type or add
the missing implementation/reference. Locate the usages of
HjemGroupScraperService in the test classes (e.g., HjemGroupScraperService field
in HjemGroupScraperServiceTests and similar references in
HjemGroupIntegrationTests) and fix by one of: 1) change the test to depend on
the correct existing class or interface name used by the production code, 2) add
the project/reference that contains HjemGroupScraperService to the test project,
or 3) create a minimal test-double (stub) class named HjemGroupScraperService in
the test assembly to satisfy compilation; ensure namespaces and using directives
match the tests.

---

Nitpick comments:
In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs`:
- Around line 23-44: Change LoadAsync to return Task<OneOf<List<HjemGroupEntry>,
HjemGroupLoadError>> instead of silently swallowing failures: update the
signature of LoadAsync and all callers to use OneOf; treat "no data exists"
(container/blob not found) as a successful result returning an empty
List<HjemGroupEntry>, but on exceptions return the HjemGroupLoadError variant
(include the caught Exception and a descriptive message) instead of returning an
empty list; keep logging via logger.LogWarning(ex, ...) but also populate the
error object so callers can distinguish load failures from empty data; use the
existing symbols (LoadAsync, blobServiceClient, ContainerName, BlobName,
HjemGroupEntry) to find and update the implementation and call sites.

In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs`:
- Around line 28-68: GetMarkersAsync currently stores an empty list into the
provider-level field _cached when the blob container or blob is missing, which
will permanently hide newly uploaded data if HjemGroupProvider becomes a
singleton; change the caching strategy in HjemGroupProvider by either (A) adding
a timestamp (e.g., _cacheTimestamp) and TTL check in GetMarkersAsync to refresh
_cached when older than the configured TTL before returning, or (B) add a public
InvalidateCache() / ResetCache() method that clears _cached (and _cacheTimestamp
if present) so the admin service can call it after saving new data; keep the
existing blob checks (ContainerName/BlobName) but ensure GetMarkersAsync
re-fetches when the cache is expired or explicitly invalidated.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupIntegrationTests.cs`:
- Around line 182-197: In FullPipeline_BlobContainsValidJson replace the
double-invocation of the delegate 'act' by invoking JsonSerializer.Deserialize
once: call the delegate (the expression that uses JsonSerializer.Deserialize
with CamelCase) and store its result in a variable, assert that invoking the
delegate does not throw (use act.Should().NotThrow()) if you must keep the
delegate, or better, directly perform the deserialize into a local variable and
assert the result is not null/empty (use the stored result for the subsequent
Should().NotBeEmpty() check) so JsonSerializer.Deserialize is only executed a
single time.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupProviderTests.cs`:
- Around line 85-120: Extract the repeated JsonSerializerOptions into a single
shared static field in the HjemGroupProviderTests class and use it everywhere;
specifically add a private static readonly JsonSerializerOptions CamelCase = new
JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase } in
the HjemGroupProviderTests class and replace inline constructions like new
JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }
(used in SetupBlobWithContent(...) and other test serialization calls inside
GetMarkersAsync_ReturnsMappedMarkers_ForLokalafdeling and other tests) with the
CamelCase field.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupScraperServiceTests.cs`:
- Around line 253-277: The test currently invokes the deserialization delegate
twice (act.Should().NotThrow() then act()!), which duplicates work and can mask
issues; replace the delegate pattern by performing the deserialization once into
a local variable using JsonSerializer.Deserialize on uploadedJson (referencing
JsonSerializer.Deserialize and uploadedJson from the test), then assert the
deserialization succeeded (result is not null) and that the result collection is
not empty; update the assertions in ScrapeAndSaveAsync_SavesValidJson_ToBlob to
use the single captured variable instead of calling act() twice.
- Around line 107-118: The helper CaptureUploadedJson is dead/buggy: tests never
call it and its captured variable is assigned inside the Arg.Do lambda after the
method returns (so it always returns the initial empty string). Either delete
CaptureUploadedJson entirely and consolidate UploadAsync Arg.Do setups inline,
or convert it to return/use a mutable wrapper (e.g., a small class with a string
property) and have CaptureUploadedJson register the Arg.Do on
_blobClient.UploadAsync so the wrapper instance is mutated inside the lambda and
can be inspected after the act; reference CaptureUploadedJson, _blobClient and
UploadAsync when applying the change.

In `@tests/web/Jordnaer.Tests/UserSearch/DataForsyningenClientTests.cs`:
- Line 79: The tests added in UserSearch/DataForsyningenClientTests.cs assert
response.IsSuccessStatusCode but the project uses the stricter
IApiResponse.IsSuccessful pattern; update the three new assertions (the ones
currently checking IsSuccessStatusCode) to assert response.IsSuccessful instead
so deserialization success is also validated, keeping behavior consistent with
the existing tests that check IsSuccessful on IApiResponse.

Comment thread src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs Outdated
Comment thread src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor
Comment thread tests/web/Jordnaer.Tests/HjemGroups/HjemGroupIntegrationTests.cs Outdated
Comment thread tests/web/Jordnaer.Tests/UserSearch/DataForsyningenClientTests.cs
@github-project-automation github-project-automation Bot moved this from Todo to In Progress in Jordnaer Community Website Feb 22, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs (2)

155-160: StreamReader disposes the captured stream; prefer leaveOpen: true

In both Arg.Do callbacks, new StreamReader(s, Encoding.UTF8) disposes s when the using block exits. NSubstitute runs the callback synchronously before returning, so the stream the service passed in is already disposed. For MemoryStream a double-dispose is harmless, but this is fragile if the production code ever switches to a different stream type or inspects the stream after UploadAsync returns.

♻️ Proposed fix — use `leaveOpen: true` in both callbacks
- using var sr = new StreamReader(s, Encoding.UTF8);
+ using var sr = new StreamReader(s, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: -1, leaveOpen: true);

Apply the same change at line 178.

Also applies to: 177-182

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs` around
lines 155 - 160, The StreamReader in the Arg.Do<Stream> callback used to capture
uploadedJson disposes the input stream; change the StreamReader construction to
pass leaveOpen: true so the captured stream isn't closed (update the
StreamReader in the UploadAsync Arg.Do<Stream> callback that reads into
uploadedJson and the other matching callback around line 178), ensuring
UploadAsync and the service-created stream are not disposed by the test helper.

51-115: Missing test: LoadAsync with malformed/non-JSON blob content

The suite covers: container absent, blob absent, empty array [], valid entries, and RequestFailedException. It does not test the case where the blob contains syntactically invalid JSON (e.g., "not-json"). If HjemGroupAdminService.LoadAsync wraps JsonSerializer.Deserialize in a try/catch and returns empty, that path is currently untested.

♻️ Suggested additional test
[Fact]
public async Task LoadAsync_ReturnsEmpty_WhenBlobContainsInvalidJson()
{
    SetupBlobWithContent("this is not valid json {{{");

    var result = await CreateSut().LoadAsync();

    result.Should().BeEmpty();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs` around
lines 51 - 115, Add a test that verifies HjemGroupAdminService.LoadAsync returns
an empty collection when the blob contains malformed JSON: create a new Fact
similar to existing tests (e.g.,
LoadAsync_ReturnsEmpty_WhenBlobContainsInvalidJson) that uses
SetupBlobWithContent("this is not valid json {{{") then calls
CreateSut().LoadAsync() and asserts the result is empty; this exercises the
JSON-deserialization error path in LoadAsync and complements the existing tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs`:
- Around line 155-160: The StreamReader in the Arg.Do<Stream> callback used to
capture uploadedJson disposes the input stream; change the StreamReader
construction to pass leaveOpen: true so the captured stream isn't closed (update
the StreamReader in the UploadAsync Arg.Do<Stream> callback that reads into
uploadedJson and the other matching callback around line 178), ensuring
UploadAsync and the service-created stream are not disposed by the test helper.
- Around line 51-115: Add a test that verifies HjemGroupAdminService.LoadAsync
returns an empty collection when the blob contains malformed JSON: create a new
Fact similar to existing tests (e.g.,
LoadAsync_ReturnsEmpty_WhenBlobContainsInvalidJson) that uses
SetupBlobWithContent("this is not valid json {{{") then calls
CreateSut().LoadAsync() and asserts the result is empty; this exercises the
JSON-deserialization error path in LoadAsync and complements the existing tests.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor (1)

158-173: The catch block in OnInitializedAsync is dead code.

AdminService.LoadAsync() already has an internal catch (Exception) that logs and returns [], so the outer catch block (lines 164–168) can never be reached. This is misleading — it implies error-handling coverage that does not exist at this layer. The finally block is still needed to reset _isLoading.

♻️ Proposed simplification
     protected override async Task OnInitializedAsync()
     {
-        try
-        {
-            _entries = await AdminService.LoadAsync();
-        }
-        catch (Exception ex)
-        {
-            Logger.LogError(ex, "Failed to load HJEM group entries.");
-            Snackbar.Add("Kunne ikke hente HJEM grupper.", Severity.Error);
-        }
-        finally
-        {
-            _isLoading = false;
-        }
+        _entries = await AdminService.LoadAsync();
+        _isLoading = false;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor` around lines
158 - 173, The catch block in OnInitializedAsync is dead because
AdminService.LoadAsync already handles exceptions and returns an empty array, so
remove the outer try/catch (the Logger.LogError and Snackbar.Add lines) and keep
the finally to set _isLoading = false; locate the OnInitializedAsync method,
remove the catch portion that references Logger.LogError and Snackbar.Add, and
ensure _entries is still assigned from await AdminService.LoadAsync() and
_isLoading is reset in the finally block.
src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs (1)

14-21: ContainerName, BlobName, and JsonOptions are duplicated in HjemGroupProvider.

Both classes independently declare the same ContainerName = "hjemlo-groups", BlobName = "groups.json", and a CamelCase JsonOptions instance. A typo or future rename in one file would silently diverge the two readers/writers.

Consider extracting to a shared static inner class or a separate HjemGroupBlobConstants file:

♻️ Proposed extraction
+// New file: HjemGroupBlobConstants.cs
+namespace Jordnaer.Features.HjemGroups;
+
+internal static class HjemGroupBlobConstants
+{
+    internal const string ContainerName = "hjemlo-groups";
+    internal const string BlobName = "groups.json";
+
+    internal static readonly JsonSerializerOptions ReadOptions = new()
+    {
+        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+    };
+
+    internal static readonly JsonSerializerOptions WriteOptions = new()
+    {
+        WriteIndented = true,
+        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+    };
+}

Then reference HjemGroupBlobConstants.* from both HjemGroupAdminService and HjemGroupProvider instead of local constants.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs` around lines
14 - 21, ContainerName, BlobName, and JsonOptions are duplicated between
HjemGroupAdminService and HjemGroupProvider; extract these shared values into a
single static container (e.g. class HjemGroupBlobConstants with public const
string ContainerName = "hjemlo-groups", public const string BlobName =
"groups.json", and a public static readonly JsonSerializerOptions JsonOptions
configured with WriteIndented=true and CamelCase) and update both
HjemGroupAdminService and HjemGroupProvider to reference
HjemGroupBlobConstants.ContainerName, HjemGroupBlobConstants.BlobName, and
HjemGroupBlobConstants.JsonOptions to avoid divergence.
src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs (1)

26-59: In-memory cache is never invalidated — admin saves won't be reflected until restart.

_cached is populated once and never cleared. Because HjemGroupProvider is registered as a singleton, any groups added or updated via HjemGroupAdminService.SaveAsync won't appear on the map until the server process restarts. Additionally, when the container or blob is missing (lines 38, 43), _cached is deliberately not set, so every subsequent call makes two round-trips to blob storage to check existence.

Consider either adding a cache-invalidation hook (InvalidateCache()) that HjemGroupAdminService.SaveAsync can call after a successful write, or replacing the instance field with a short-TTL cache entry (e.g. IMemoryCache / FusionCache — already registered in Program.cs) so stale data ages out automatically.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs` around lines 26 -
59, HjemGroupProvider currently caches marker data in the private field _cached
inside GetMarkersAsync and never invalidates it, so updates via
HjemGroupAdminService.SaveAsync aren’t reflected; modify the provider to either
(A) expose a public InvalidateCache() method on HjemGroupProvider and call that
from HjemGroupAdminService.SaveAsync after a successful blob write, or (B)
replace the _cached field with a short-TTL cache entry using the registered
IMemoryCache/FusionCache (inject IMemoryCache/FusionCache into HjemGroupProvider
and use a cache key like "HjemGroupMarkers") so reads check the cache TTL
instead of permanently storing results; also set the cache when container/blob
is missing to avoid repeated ExistsAsync round-trips or ensure the cache stores
an empty list result to short-circuit subsequent calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs`:
- Around line 14-21: ContainerName, BlobName, and JsonOptions are duplicated
between HjemGroupAdminService and HjemGroupProvider; extract these shared values
into a single static container (e.g. class HjemGroupBlobConstants with public
const string ContainerName = "hjemlo-groups", public const string BlobName =
"groups.json", and a public static readonly JsonSerializerOptions JsonOptions
configured with WriteIndented=true and CamelCase) and update both
HjemGroupAdminService and HjemGroupProvider to reference
HjemGroupBlobConstants.ContainerName, HjemGroupBlobConstants.BlobName, and
HjemGroupBlobConstants.JsonOptions to avoid divergence.

In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs`:
- Around line 26-59: HjemGroupProvider currently caches marker data in the
private field _cached inside GetMarkersAsync and never invalidates it, so
updates via HjemGroupAdminService.SaveAsync aren’t reflected; modify the
provider to either (A) expose a public InvalidateCache() method on
HjemGroupProvider and call that from HjemGroupAdminService.SaveAsync after a
successful blob write, or (B) replace the _cached field with a short-TTL cache
entry using the registered IMemoryCache/FusionCache (inject
IMemoryCache/FusionCache into HjemGroupProvider and use a cache key like
"HjemGroupMarkers") so reads check the cache TTL instead of permanently storing
results; also set the cache when container/blob is missing to avoid repeated
ExistsAsync round-trips or ensure the cache stores an empty list result to
short-circuit subsequent calls.

In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor`:
- Around line 158-173: The catch block in OnInitializedAsync is dead because
AdminService.LoadAsync already handles exceptions and returns an empty array, so
remove the outer try/catch (the Logger.LogError and Snackbar.Add lines) and keep
the finally to set _isLoading = false; locate the OnInitializedAsync method,
remove the catch portion that references Logger.LogError and Snackbar.Add, and
ensure _entries is still assigned from await AdminService.LoadAsync() and
_isLoading is reset in the finally block.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/web/Jordnaer/Components/NotificationItem.razor (1)

26-28: Correct fix — display: block is required for CSS text-truncation to apply.

Typo.caption renders as an inline <span> in MudBlazor, so white-space: nowrap; overflow: hidden; text-overflow: ellipsis (the usual content of notification-description-truncate) silently has no effect without a block formatting context. The inline Style="display: block;" fixes that correctly.

As a minor clean-up, consider moving display: block into the notification-description-truncate CSS class so all truncation-related rules live in one place and the inline Style attribute can be removed:

♻️ Proposed refactor — consolidate into CSS class

In the component's stylesheet (or global CSS), add the property to the existing rule:

 .notification-description-truncate {
+  display: block;
   /* existing truncation rules, e.g.: */
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }

Then remove the inline style:

-<MudText Typo="Typo.caption" Color="Color.Secondary" Class="notification-description-truncate" Style="display: block;">
+<MudText Typo="Typo.caption" Color="Color.Secondary" Class="notification-description-truncate">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Components/NotificationItem.razor` around lines 26 - 28, The
MudText in NotificationItem.razor uses Typo.caption (renders as inline) so the
truncation CSS doesn't work; move the inline Style="display: block;" into the
notification-description-truncate CSS rule instead: update the
notification-description-truncate class to include display: block (alongside
white-space: nowrap; overflow: hidden; text-overflow: ellipsis) and remove the
inline Style attribute from the MudText element that outputs
`@Notification.Description` to keep truncation rules centralized and remove the
inline override.
tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs (2)

311-315: Optional: Dispose ApiResponse / HttpResponseMessage instances in the helper.

ApiResponse<T> implements IDisposable (via IApiResponse) and wraps a raw HttpResponseMessage. If the production GeocodeAsync disposes the response (e.g. using var resp = await _geocoder.SearchZipCodesAsync(...)) the HttpResponseMessage is cleaned up through that path, but the inline instance in GeocodeAsync_ReturnsNull_WhenApiCallFails (line 221) is never returned to the substitute and leaks. Wrapping these with using or making MakeGeoResponse return a helper that manages lifetime keeps the intent explicit and prevents xUnit leak-detection warnings.

♻️ Suggested approach
- private static IApiResponse<IEnumerable<ZipCodeSearchResponse>> MakeGeoResponse(params ZipCodeSearchResponse[] results) =>
-     new ApiResponse<IEnumerable<ZipCodeSearchResponse>>(
-         new HttpResponseMessage(HttpStatusCode.OK),
-         results,
-         new RefitSettings());
+ private static ApiResponse<IEnumerable<ZipCodeSearchResponse>> MakeGeoResponse(
+     params ZipCodeSearchResponse[] results) =>
+     new(new HttpResponseMessage(HttpStatusCode.OK), results, new RefitSettings());

And in GeocodeAsync_ReturnsNull_WhenApiCallFails:

- _geocoder
-     .SearchZipCodesAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
-     .Returns(new ApiResponse<IEnumerable<ZipCodeSearchResponse>>(
-         new HttpResponseMessage(HttpStatusCode.InternalServerError), null, new RefitSettings()));
+ using var failResponse = new ApiResponse<IEnumerable<ZipCodeSearchResponse>>(
+     new HttpResponseMessage(HttpStatusCode.InternalServerError), null, new RefitSettings());
+ _geocoder
+     .SearchZipCodesAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
+     .Returns(failResponse);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs` around
lines 311 - 315, The MakeGeoResponse helper currently constructs an ApiResponse
wrapping a raw HttpResponseMessage but never disposes it; update MakeGeoResponse
(used by GeocodeAsync_ReturnsNull_WhenApiCallFails) to manage disposal by either
returning an IDisposable wrapper/IApiResponse that disposes its
HttpResponseMessage or by creating/returning the ApiResponse inside a
using/factory that ensures the HttpResponseMessage is disposed when the
test/substitute is done; reference
ApiResponse<IEnumerable<ZipCodeSearchResponse>>, HttpResponseMessage and
MakeGeoResponse when changing the helper so no HttpResponseMessage leaks occur.

102-115: Optional: Verify that the logger receives an error call on blob failure.

The AI summary notes that HjemGroupAdminService.LoadAsync logs errors when an exception is raised. The test currently only asserts the return value is empty, leaving the logging path untested. A small addition would catch any future regression where the log call is silently dropped:

✅ Proposed addition
  result.Should().BeEmpty();
+
+ _logger.Received(1).Log(
+     LogLevel.Error,
+     Arg.Any<EventId>(),
+     Arg.Any<object>(),
+     Arg.Is<Exception>(e => e is RequestFailedException),
+     Arg.Any<Func<object, Exception?, string>>());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs` around
lines 102 - 115, The test currently asserts only that LoadAsync returns empty
when the blob download throws; add an assertion that the injected logger
receives an error call so the logging path is covered. After calling
CreateSut().LoadAsync() in LoadAsync_ReturnsEmpty_WhenBlobThrows, verify the
test's logger substitute (e.g. _logger or the ILogger<HjemGroupAdminService>
mock used in the test fixture) received an error-level log invocation (use
Received(1) with LogError or a ReceivedWithAnyArgs check for an error call) to
ensure HjemGroupAdminService.LoadAsync logs the exception.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/web/Jordnaer/Components/NotificationItem.razor`:
- Around line 26-28: The MudText in NotificationItem.razor uses Typo.caption
(renders as inline) so the truncation CSS doesn't work; move the inline
Style="display: block;" into the notification-description-truncate CSS rule
instead: update the notification-description-truncate class to include display:
block (alongside white-space: nowrap; overflow: hidden; text-overflow: ellipsis)
and remove the inline Style attribute from the MudText element that outputs
`@Notification.Description` to keep truncation rules centralized and remove the
inline override.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs`:
- Around line 311-315: The MakeGeoResponse helper currently constructs an
ApiResponse wrapping a raw HttpResponseMessage but never disposes it; update
MakeGeoResponse (used by GeocodeAsync_ReturnsNull_WhenApiCallFails) to manage
disposal by either returning an IDisposable wrapper/IApiResponse that disposes
its HttpResponseMessage or by creating/returning the ApiResponse inside a
using/factory that ensures the HttpResponseMessage is disposed when the
test/substitute is done; reference
ApiResponse<IEnumerable<ZipCodeSearchResponse>>, HttpResponseMessage and
MakeGeoResponse when changing the helper so no HttpResponseMessage leaks occur.
- Around line 102-115: The test currently asserts only that LoadAsync returns
empty when the blob download throws; add an assertion that the injected logger
receives an error call so the logging path is covered. After calling
CreateSut().LoadAsync() in LoadAsync_ReturnsEmpty_WhenBlobThrows, verify the
test's logger substitute (e.g. _logger or the ILogger<HjemGroupAdminService>
mock used in the test fixture) received an error-level log invocation (use
Received(1) with LogError or a ReceivedWithAnyArgs check for an error call) to
ensure HjemGroupAdminService.LoadAsync logs the exception.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor (2)

301-301: No delete confirmation, but implicit safeguard exists.

Deleting only modifies the in-memory list — changes are not persisted until "Gem alle" is clicked, so an accidental delete is recoverable by refreshing. If this is intentional, it's fine. A brief MudMessageBox confirmation could improve UX down the road.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor` at line 301,
DeleteEntry currently removes an item from the in-memory _entries list without
user confirmation; add a confirmation dialog before removal to improve UX.
Update the DeleteEntry method to show a MudMessageBox (or equivalent
confirmation component), and only call _entries.Remove(entry) if the user
confirms; keep current behavior that changes are only persisted when "Gem alle"
is clicked.

254-299: Good: coordinate validation addresses the (0,0) edge case.

The range check and zero-coordinate guard look solid. One subtle note: since HjemGroupEntry is a record, IndexOf at line 293 uses value equality. If two entries happen to have identical field values, IndexOf could match the wrong one. This is unlikely in practice but worth being aware of.

Consider using an index-based approach in OpenEditDialog instead:

♻️ Suggested alternative
-    private HjemGroupEntry? _editingEntry;
+    private int _editingIndex = -1;
 
     private void OpenEditDialog(HjemGroupEntry entry)
     {
         _editingNew = false;
-        _editingEntry = entry;
+        _editingIndex = _entries.IndexOf(entry);
         ...
     }
 
     // In CommitEdit:
-    else if (_editingEntry is not null)
+    else if (_editingIndex >= 0 && _editingIndex < _entries.Count)
     {
-        var idx = _entries.IndexOf(_editingEntry);
-        if (idx >= 0)
-            _entries[idx] = updated;
+        _entries[_editingIndex] = updated;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor` around lines
254 - 299, The CommitEdit uses _entries.IndexOf(_editingEntry) which relies on
record value-equality and can misidentify the edited item; instead add and use
an index tracked when opening the editor. In OpenEditDialog set a new private
int _editingIndex (e.g. -1 default) to the index of the entry being edited (or
-1 for new), then in CommitEdit replace the IndexOf branch with: if
(_editingNew) _entries.Add(updated) else if (_editingIndex >= 0)
_entries[_editingIndex] = updated; keep _editingEntry for convenience or clear
it, and remove the IndexOf call to avoid value-equality collisions.
tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs (1)

313-317: HttpResponseMessage in MakeGeoResponse is not disposed by callers.

The HttpResponseMessage created at line 315 is wrapped in an ApiResponse and returned, but most callers (lines 210, 236, 252, 271, 284) don't dispose it. In test code this typically doesn't cause issues, but it's inconsistent with the explicit using pattern at lines 220-221.

Consider wrapping the helper to use IDisposable or just accept the inconsistency since it's test code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs` around
lines 313 - 317, MakeGeoResponse creates an HttpResponseMessage that never gets
disposed by callers; change the helper so callers can dispose the underlying
response: create a small wrapper type (e.g., DisposableApiResponse<T>) that
implements IDisposable, holds the
ApiResponse<IEnumerable<ZipCodeSearchResponse>> and the HttpResponseMessage, and
disposes the HttpResponseMessage in Dispose; then change MakeGeoResponse to
return DisposableApiResponse<ZipCodeSearchResponse> instead of
ApiResponse<IEnumerable<ZipCodeSearchResponse>> so tests that currently call
MakeGeoResponse can dispose the wrapper (or use a using statement) to properly
release the HttpResponseMessage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/web/Jordnaer/Components/NotificationItem.razor`:
- Around line 26-28: Replace the raw div in NotificationItem.razor with the
MudBlazor component MudText to avoid hardcoding MudBlazor internals; use MudText
with the existing "notification-description-truncate" CSS via the Class
parameter and set equivalent public props (e.g., Typo="Typo.caption" and
Color="Color.Secondary") so the rendered markup and styling remain the same
while relying only on MudText's public API and keeping the content
`@Notification.Description`.

In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs`:
- Around line 26-60: The provider permanently caches markers in the private
field _cached inside GetMarkersAsync, so admin edits via
HjemGroupAdminService.SaveAsync never propagate; add a cache invalidation method
to the provider interface (e.g., InvalidateCache or ClearMarkersCache) and
implement it to set _cached = null in HjemGroupProvider, then ensure
HjemGroupAdminService.SaveAsync calls that invalidation after a successful save
(or fire an event that the admin service triggers to clear the cache) so
subsequent GetMarkersAsync reloads fresh data.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs`:
- Around line 102-116: The logger assertion in the test
LoadAsync_ReturnsEmpty_WhenBlobThrows is not verifiable because ILogger.Log is
an extension method and cannot be intercepted by NSubstitute; remove the
unverifiable assertion `_logger.ReceivedWithAnyArgs(1).Log(...)` (or
alternatively replace it by using a logging test helper like
NSubstitute.Extensions.LoggingExtensions if you want to assert logs) and keep
the existing behavioral assertion that result.Should().BeEmpty(); this change
touches the test method LoadAsync_ReturnsEmpty_WhenBlobThrows and the `_logger`
verification line.

---

Nitpick comments:
In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor`:
- Line 301: DeleteEntry currently removes an item from the in-memory _entries
list without user confirmation; add a confirmation dialog before removal to
improve UX. Update the DeleteEntry method to show a MudMessageBox (or equivalent
confirmation component), and only call _entries.Remove(entry) if the user
confirms; keep current behavior that changes are only persisted when "Gem alle"
is clicked.
- Around line 254-299: The CommitEdit uses _entries.IndexOf(_editingEntry) which
relies on record value-equality and can misidentify the edited item; instead add
and use an index tracked when opening the editor. In OpenEditDialog set a new
private int _editingIndex (e.g. -1 default) to the index of the entry being
edited (or -1 for new), then in CommitEdit replace the IndexOf branch with: if
(_editingNew) _entries.Add(updated) else if (_editingIndex >= 0)
_entries[_editingIndex] = updated; keep _editingEntry for convenience or clear
it, and remove the IndexOf call to avoid value-equality collisions.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs`:
- Around line 313-317: MakeGeoResponse creates an HttpResponseMessage that never
gets disposed by callers; change the helper so callers can dispose the
underlying response: create a small wrapper type (e.g.,
DisposableApiResponse<T>) that implements IDisposable, holds the
ApiResponse<IEnumerable<ZipCodeSearchResponse>> and the HttpResponseMessage, and
disposes the HttpResponseMessage in Dispose; then change MakeGeoResponse to
return DisposableApiResponse<ZipCodeSearchResponse> instead of
ApiResponse<IEnumerable<ZipCodeSearchResponse>> so tests that currently call
MakeGeoResponse can dispose the wrapper (or use a using statement) to properly
release the HttpResponseMessage.

Comment thread src/web/Jordnaer/Components/NotificationItem.razor
Comment thread src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs Outdated
Comment thread tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs`:
- Around line 25-45: LoadAsync currently swallows all exceptions and returns an
empty list; change its signature in HjemGroupAdminService to
Task<OneOf<List<HjemGroupEntry>, HjemGroupLoadError>> and stop returning [] on
failures so callers can distinguish "no entries" from "load error". Implement a
HjemGroupLoadError class/record that carries error details (exception
message/inner info) and return OneOf.Success when deserialization/network calls
succeed, and return OneOf.Error(HjemGroupLoadError) when blob container/blob
does not exist or any exception occurs (preserve/log exception via
logger.LogWarning but do not translate it into an empty list). Update references
to LoadAsync (e.g., HjemGroupManagementPage.razor) to handle the OneOf result
accordingly.

In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs`:
- Around line 10-37: Change IHjemGroupProvider.GetMarkersAsync and
HjemGroupProvider.GetMarkersAsync to return
OneOf<IReadOnlyList<GroupMarkerData>, Error> instead of
IReadOnlyList<GroupMarkerData>, and update the fusionCache usage so the factory
returns OneOf by calling LoadFromBlobAsync which must also be changed to return
OneOf<IReadOnlyList<GroupMarkerData>, Error> rather than swallowing exceptions;
replace the current empty-list fallback (currently "?? []" and the catch in
LoadFromBlobAsync that returns []) with propagating/constructing an Error value
on failure (and return the successful list wrapped in OneOf on success), and
update any callers of GetMarkersAsync to handle the OneOf result; refer to
IHjemGroupProvider, HjemGroupProvider, GetMarkersAsync, LoadFromBlobAsync,
CacheKey and CacheTag when implementing these changes.

In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor`:
- Around line 110-112: The label "URL (valgfri)" contradicts CommitEdit which
rejects empty _form.WebsiteUrl; update the UI to reflect the requirement by
changing the MudTextField Label from "URL (valgfri)" to a required label (e.g.,
"URL (påkrævet)" or simply "URL") for both occurrences (the MudTextField bound
to _form.WebsiteUrl at the shown block and the duplicate at lines ~256-259), and
ensure any validation messages shown next to the field match CommitEdit's
required validation so the UI and the CommitEdit method are consistent.

In `@src/web/Jordnaer/wwwroot/js/leaflet-interop.js`:
- Around line 356-369: The current logic sets isHjemUrl based only on hostname
and can allow non-HTTP schemes; update the check in the block that builds the
primary link (where parsedGroupUrl is created and isHjemUrl is set) to also
verify parsedGroupUrl.protocol is "http:" or "https:" before treating it as a
HJEM URL, and then reuse that safe isHjemUrl when computing groupUrl and
groupUrlTarget (the same change should be applied to the other occurrence around
groupUrl/groupUrlTarget assignment). Ensure you still wrap URL construction in
the try/catch around new URL(group.websiteUrl) and fall back to the encoded
local path when the protocol/hostname check fails.

---

Duplicate comments:
In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs`:
- Around line 117-121: The test is asserting logs using NSubstitute's
_logger.ReceivedWithAnyArgs(...) which is not interceptable for ILogger<T>;
remove that unverifiable assertion or replace it with a proper logging test
helper (e.g., capture logs via a TestLogger/TestSink or use
Microsoft.Extensions.Logging.Testing) to assert a warning was written; update
the HjemGroupAdminServiceTests where CreateSut().LoadAsync() is called and
remove the line "_logger.ReceivedWithAnyArgs(1).Log(...)" or swap it out for a
logging helper that inspects emitted log entries.

Comment thread src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs Outdated
Comment thread src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs
Comment thread src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor Outdated
Comment thread src/web/Jordnaer/wwwroot/js/leaflet-interop.js Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/web/Jordnaer/wwwroot/css/marker-cluster.css (1)

91-111: ⚠️ Potential issue | 🟡 Minor

Provide a fallback for initials when :has() is unsupported.

Without a fallback, initial text will be invisible (white on white) in browsers lacking :has() support. While :has() is now supported in all modern browsers (Chrome/Edge 105+, Safari 15.4+, Firefox 121+), adding a CSS-only fallback ensures robustness. Either apply the background color directly to the initial element or use a parent class.

Recommended CSS fallback
 .group-marker-icon .group-marker-initial {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background-color: var(--map-primary);
+    border-radius: 50%;
     color: var(--neutral-white);
     font-size: 14px;
     font-weight: 600;
     font-family: var(--font-body-bold), 'Helvetica Neue', Arial, sans-serif;
 }
@@
 .group-popup-avatar .group-initial {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background-color: var(--map-primary);
+    border-radius: 50%;
     color: var(--neutral-white);
     font-size: 20px;
     font-weight: 600;
 }

Also applies to: 150–170

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/wwwroot/css/marker-cluster.css` around lines 91 - 111, The
.group-marker-icon relies on the :has(.group-marker-initial) selector which
leaves initials invisible in browsers without :has() support; add a CSS-only
fallback by giving the initial element itself a visible background and text
color (e.g., set styles on .group-marker-initial: background-color:
var(--map-primary); color: var(--neutral-white); border-radius: 50%;
display:flex; align-items:center; justify-content:center) or by using a parent
helper class (e.g., .group-marker-icon--has-initial) applied where initials
exist and style .group-marker-icon--has-initial the same as the :has() rule;
apply the same fallback to the other occurrence noted (the rule around lines
150–170) and ensure contrast and sizing match the existing .group-marker-icon
styles.
🧹 Nitpick comments (5)
tests/web/Jordnaer.Tests/HjemGroups/HjemGroupProviderTests.cs (1)

96-128: Consider extracting the repeated JsonSerializerOptions to a shared static field.

The new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase } is duplicated across multiple tests (lines 109, 147, 173, 195, 207, 254, 275). A private static readonly field (similar to HjemGroupAdminServiceTests.CamelCase) would reduce repetition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupProviderTests.cs` around lines
96 - 128, Extract the repeated JsonSerializerOptions instantiation into a shared
private static readonly field (e.g. private static readonly
JsonSerializerOptions CamelCaseOptions) in HjemGroupProviderTests and replace
each occurrence of new JsonSerializerOptions { PropertyNamingPolicy =
JsonNamingPolicy.CamelCase } (used in SetupBlobWithContent calls around
GetMarkersAsync test and others) with the new CamelCaseOptions field to
eliminate duplication and centralize the camelCase configuration.
src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor (2)

318-335: SaveAsync doesn't prevent double-clicks beyond the _isSaving flag.

The _isSaving flag disables the button, but since Blazor re-renders asynchronously, there's a small window where a rapid double-click could trigger two concurrent saves. In practice this is low risk given the admin-only audience, but StateHasChanged() isn't called after setting _isSaving = true — the UI won't update until the await yields.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor` around lines
318 - 335, SaveAsync can still be started twice due to a small render race;
guard the method and force a render: at the top of SaveAsync check if _isSaving
is true and return immediately to prevent re-entry, then set _isSaving = true
and call StateHasChanged() before awaiting AdminService.SaveAsync to ensure the
UI (button disabled) updates before the async yield; keep the existing
try/catch/finally but ensure the finally sets _isSaving = false and calls
StateHasChanged() again so the UI re-enables reliably.

316-316: No delete confirmation — accidental deletions are possible.

DeleteEntry immediately removes the entry from the in-memory list without a confirmation prompt. The "Gem alle" save pattern provides a safety net (unsaved changes are lost on navigation), but a simple MudDialog confirmation or a "Fortryd" (undo) Snackbar would be a nice UX improvement for an admin tool.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor` at line 316,
DeleteEntry currently removes the HjemGroupEntry from the in-memory _entries
list immediately, risking accidental deletions; update DeleteEntry (or replace
calls to it) to open a MudDialog confirmation (or show a Snackbar with undo)
before removing the item, and only call _entries.Remove(entry) after the dialog
returns a positive confirmation (or if undo is not used, queue removal until the
save action); reference the DeleteEntry method and the _entries collection when
adding the confirmation flow so the UI blocks removal until the user confirms.
src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs (1)

53-67: SaveAsync doesn't use OneOf for error signaling.

Exceptions from blob upload or message publish propagate directly to the caller. The Razor page wraps calls in try/catch, but per the coding guideline for src/web/Jordnaer/Features/**/*.cs, service methods should use OneOf<TSuccess, TError> for explicit error handling. This is a lower priority since the caller already handles exceptions, but worth noting for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs` around lines
53 - 67, SaveAsync currently throws exceptions from blob upload and publish
instead of returning a OneOf result; change its signature from Task to
Task<OneOf<Unit, TError>> (pick or create an appropriate error type, e.g.,
HjemGroupSaveError) and wrap the blobServiceClient/container/blobClient/upload
and publishEndpoint.Publish( new InvalidateCacheTags { Tags =
[HjemGroupProvider.CacheTag] } ) calls in try/catch blocks, returning
OneOf.Success (Unit.Value) on success and OneOf.Error with the mapped error on
failure; update callers to handle the OneOf result and keep HjemGroupEntry,
publishEndpoint, InvalidateCacheTags and HjemGroupProvider.CacheTag references
to locate the code.
tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs (1)

332-336: MakeGeoResponse creates unmanaged HttpResponseMessage instances.

Each call allocates a HttpResponseMessage that isn't disposed. While this is test code and the impact is negligible, the two explicit MakeGeoResponse callers in GeocodeAsync_ReturnsNull_WhenApiCallFails (lines 239-240) correctly use using. For consistency, consider making this helper return a disposable or noting that the caller should dispose.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs` around
lines 332 - 336, MakeGeoResponse currently allocates an HttpResponseMessage that
is never disposed; change it to return a disposable wrapper so callers can use
using and ensure the message is disposed. Implement a small class (e.g.,
DisposableApiResponse<T> : IDisposable) that holds the
ApiResponse<IEnumerable<ZipCodeSearchResponse>> (constructed with the created
HttpResponseMessage) and disposes the underlying HttpResponseMessage in Dispose;
update MakeGeoResponse to return
DisposableApiResponse<IEnumerable<ZipCodeSearchResponse>> and adjust callers
(e.g., in GeocodeAsync_ReturnsNull_WhenApiCallFails) to use using on the
wrapper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs`:
- Around line 32-39: GetMarkersAsync currently caches the OneOf result returned
by LoadFromBlobAsync, so transient Error variants get stored and served; change
the factory passed to fusionCache.GetOrSetAsync (the lambda for key CacheKey
with tags CacheTag) so it does not return Error values to FusionCache: call
LoadFromBlobAsync, inspect the OneOf result and if it's an Error throw an
exception (or otherwise propagate a failure) instead of returning the Error,
ensuring only successful IReadOnlyList<GroupMarkerData> is cached; alternatively
implement a short-lived cache path for errors or use a predicate/overload that
prevents caching Error variants—apply this change around GetMarkersAsync, the
fusionCache.GetOrSetAsync call, and LoadFromBlobAsync.

In `@tasks/13-cheaper-infrastructure.md`:
- Around line 49-62: The document incorrectly labels Hetzner's CX22 as ARM and
misstates CX32 pricing; update the text to either (A) treat CX22 as an x86 plan:
remove all "Ampere ARM" references, delete ARM-specific guidance (the
`--platform linux/arm64` build flag note, ARM Docker image comment, and the ARM
compatibility risk row) and confirm the existing Dockerfile works for x86, or
(B) switch to a CAX plan if ARM is intended: replace CX22 with the appropriate
CAX model (e.g., CAX11/CAX21), update specs/pricing and ARM build guidance
accordingly; also correct the CX32 price to €6.80/month wherever CX32 is
mentioned.
- Line 168: Update the prose to use the correct trademark capitalization
"GitHub" instead of "Github" in the sentence that references the website_cd.yml
workflow and the Dokploy webhook replacement; locate the string "Github" in the
file (it appears near the sentence "In the current
[website_cd.yml](.github/workflows/website_cd.yml), replace the Azure deployment
steps with a Dokploy webhook call") and change it to "GitHub".
- Around line 184-188: The "Deploy via Dokploy webhook" CI step uses curl
without --fail so HTTP 4xx/5xx responses won't fail the job; update the curl
invocation in that step to include --fail (and optionally --show-error and
--silent if you want quieter output) so curl returns a non-zero exit code on
HTTP errors—i.e., change the curl command used in the "Deploy via Dokploy
webhook" step to curl --fail [--show-error --silent] -X POST "${{
secrets.DOKPLOY_WEBHOOK_URL }}" -H "Authorization: Bearer ${{
secrets.DOKPLOY_API_KEY }}" to ensure the CI step fails on rejected/failed
webhook calls.
- Line 5: Replace the duplicated phrase "storage storage" with the correct term
"azure blob storage" in the document: find the line containing "storage storage
(blob storage, close to free)" and change it to "azure blob storage (close to
free)" so the parenthetical isn't redundant and the service name is accurate.
- Around line 148-151: The current "Install Dokploy" step pipes the remote
installer into sh (the curl | sh command), which is a supply-chain risk; change
the runbook to first download the installer to a local file (e.g., save the
output of the curl command to dokploy-install.sh), verify its integrity (provide
steps to check a published SHA256 checksum or GPG signature), inspect the file
before running, and only then execute it (e.g., sh dokploy-install.sh); update
the "Install Dokploy" step to reference the saved installer file and include the
checksum/verification step and an explicit execution step instead of piping into
sh.
- Line 215: The migration plan is missing enabling Docker Swarm mode in Dokploy
so the "Rolling" mitigation will actually work; under Step 2 add a sub-step to
enable Swarm mode in Dokploy's Advanced Tab → Cluster Settings → Swarm Settings
and configure a health check that points at /alive plus an update_config for
rolling updates (so the Rolling strategy will use the /alive probe and perform
update_config-based draining rather than stopping the old container
immediately); mention that this is required for HostOptions.ShutdownTimeout,
boot.js retry behavior, and Blazor.reconnect() to function as described.

---

Outside diff comments:
In `@src/web/Jordnaer/wwwroot/css/marker-cluster.css`:
- Around line 91-111: The .group-marker-icon relies on the
:has(.group-marker-initial) selector which leaves initials invisible in browsers
without :has() support; add a CSS-only fallback by giving the initial element
itself a visible background and text color (e.g., set styles on
.group-marker-initial: background-color: var(--map-primary); color:
var(--neutral-white); border-radius: 50%; display:flex; align-items:center;
justify-content:center) or by using a parent helper class (e.g.,
.group-marker-icon--has-initial) applied where initials exist and style
.group-marker-icon--has-initial the same as the :has() rule; apply the same
fallback to the other occurrence noted (the rule around lines 150–170) and
ensure contrast and sizing match the existing .group-marker-icon styles.

---

Nitpick comments:
In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs`:
- Around line 53-67: SaveAsync currently throws exceptions from blob upload and
publish instead of returning a OneOf result; change its signature from Task to
Task<OneOf<Unit, TError>> (pick or create an appropriate error type, e.g.,
HjemGroupSaveError) and wrap the blobServiceClient/container/blobClient/upload
and publishEndpoint.Publish( new InvalidateCacheTags { Tags =
[HjemGroupProvider.CacheTag] } ) calls in try/catch blocks, returning
OneOf.Success (Unit.Value) on success and OneOf.Error with the mapped error on
failure; update callers to handle the OneOf result and keep HjemGroupEntry,
publishEndpoint, InvalidateCacheTags and HjemGroupProvider.CacheTag references
to locate the code.

In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor`:
- Around line 318-335: SaveAsync can still be started twice due to a small
render race; guard the method and force a render: at the top of SaveAsync check
if _isSaving is true and return immediately to prevent re-entry, then set
_isSaving = true and call StateHasChanged() before awaiting
AdminService.SaveAsync to ensure the UI (button disabled) updates before the
async yield; keep the existing try/catch/finally but ensure the finally sets
_isSaving = false and calls StateHasChanged() again so the UI re-enables
reliably.
- Line 316: DeleteEntry currently removes the HjemGroupEntry from the in-memory
_entries list immediately, risking accidental deletions; update DeleteEntry (or
replace calls to it) to open a MudDialog confirmation (or show a Snackbar with
undo) before removing the item, and only call _entries.Remove(entry) after the
dialog returns a positive confirmation (or if undo is not used, queue removal
until the save action); reference the DeleteEntry method and the _entries
collection when adding the confirmation flow so the UI blocks removal until the
user confirms.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs`:
- Around line 332-336: MakeGeoResponse currently allocates an
HttpResponseMessage that is never disposed; change it to return a disposable
wrapper so callers can use using and ensure the message is disposed. Implement a
small class (e.g., DisposableApiResponse<T> : IDisposable) that holds the
ApiResponse<IEnumerable<ZipCodeSearchResponse>> (constructed with the created
HttpResponseMessage) and disposes the underlying HttpResponseMessage in Dispose;
update MakeGeoResponse to return
DisposableApiResponse<IEnumerable<ZipCodeSearchResponse>> and adjust callers
(e.g., in GeocodeAsync_ReturnsNull_WhenApiCallFails) to use using on the
wrapper.

In `@tests/web/Jordnaer.Tests/HjemGroups/HjemGroupProviderTests.cs`:
- Around line 96-128: Extract the repeated JsonSerializerOptions instantiation
into a shared private static readonly field (e.g. private static readonly
JsonSerializerOptions CamelCaseOptions) in HjemGroupProviderTests and replace
each occurrence of new JsonSerializerOptions { PropertyNamingPolicy =
JsonNamingPolicy.CamelCase } (used in SetupBlobWithContent calls around
GetMarkersAsync test and others) with the new CamelCaseOptions field to
eliminate duplication and centralize the camelCase configuration.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee72b73 and 590342d.

📒 Files selected for processing (11)
  • src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs
  • src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs
  • src/web/Jordnaer/Features/Partners/PartnerService.cs
  • src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor
  • src/web/Jordnaer/Pages/GroupSearch/GroupSearch.razor
  • src/web/Jordnaer/Program.cs
  • src/web/Jordnaer/wwwroot/css/marker-cluster.css
  • src/web/Jordnaer/wwwroot/js/leaflet-interop.js
  • tasks/13-cheaper-infrastructure.md
  • tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs
  • tests/web/Jordnaer.Tests/HjemGroups/HjemGroupProviderTests.cs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/web/Jordnaer/Program.cs
  • src/web/Jordnaer/Pages/GroupSearch/GroupSearch.razor

Comment thread src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs
Comment thread tasks/13-cheaper-infrastructure.md Outdated
Comment thread tasks/13-cheaper-infrastructure.md Outdated
Comment thread tasks/13-cheaper-infrastructure.md
Comment thread tasks/13-cheaper-infrastructure.md
Comment thread tasks/13-cheaper-infrastructure.md Outdated
Comment thread tasks/13-cheaper-infrastructure.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs (1)

22-46: Verify there is a cache invalidation path after admin updates.

If no invalidation exists, markers can remain stale until cache expiry/app restart. Please confirm that admin save flows clear this cache (e.g., via fusionCache.RemoveByTag(CacheTag)), or add an explicit invalidation method.

#!/bin/bash
# Search for HJEM group cache invalidation usage (tags/keys).
rg -n -C2 'hjem-groups|CacheTag|RemoveByTag|InvalidateCache|Clear.*Cache' 
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs` around lines 22 -
46, GetMarkersAsync is caching markers under CacheKey with tag CacheTag but
there is no evidence here of a cache invalidation path; add or confirm a call to
fusionCache.RemoveByTag(CacheTag) (or fusionCache.Remove(CacheKey)) from the
admin save/update flow that modifies Hjem groups (e.g., in the admin
controller/service method that handles group saves), or expose an explicit
InvalidateHjemGroupCache() method that calls fusionCache.RemoveByTag(CacheTag)
and invoke it after successful persistence so markers are refreshed immediately
after admin updates.
🧹 Nitpick comments (1)
tasks/13-cheaper-infrastructure.md (1)

198-204: application.deploy is asynchronous — curl --fail only validates job acceptance, not deployment success

Deployments are asynchronous; many CRUD operations return immediately, but deployment progress must be polled until completion. The POST /api/application.deploy call returns HTTP 200 as soon as the job is queued, so --fail only catches cases where Dokploy itself is unreachable or rejects the request — a deployment that builds, starts, and then fails health checks will still produce a green CI step.

Consider one of:

  1. Poll the deployment status using GET /api/deployment.allByType?id=...&type=application after triggering until it transitions to a terminal state.
  2. Use dokploy/dokploy-action@v1 — the official GitHub Action — which may abstract the polling. The Dokploy docs show uses: dokploy/dokploy-action@v1 as the recommended way to trigger deployments from GitHub Actions workflows.
  3. Rely on the health check + rollback already configured in the Swarm settings (line 232) as the safety net, and accept the async nature — document this explicitly so the team knows CI green ≠ deploy green.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tasks/13-cheaper-infrastructure.md` around lines 198 - 204, The current curl
POST to "application.deploy" (the block that uses curl with x-api-key: ${{
secrets.DOKPLOY_API_KEY }} and -d '{"applicationId":"${{
secrets.DOKPLOY_APPLICATION_ID }}"}') only validates job acceptance because
application.deploy is asynchronous; update the workflow to either (A) poll the
deployment status after the POST using the GET
/api/deployment.allByType?id=...&type=application endpoint until the deployment
reaches a terminal state and fail the step on terminal failure, or (B) replace
the curl step with the official dokploy/dokploy-action@v1 to let the action
handle polling, or (C) explicitly document and accept the async behavior and
rely on the existing Swarm health check/rollback, making the CI step's semantics
clear to the team; pick one approach and implement it for the application.deploy
invocation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs`:
- Around line 53-104: Change SaveAsync and GeocodeAsync to return OneOf-based
results instead of void/null/exceptions: for SaveAsync (method SaveAsync) catch
blobServiceClient/container/blob UploadAsync and publishEndpoint.Publish errors
and return an appropriate OneOf (e.g., OneOf<Success, HjemGroupLoadError> or
OneOf<Success, Error<string>>) instead of letting exceptions bubble; update
callers accordingly. For GeocodeAsync (method GeocodeAsync) return
OneOf<GeocodeResult, NotFound> (or OneOf<GeocodeResult,
HjemGroupLoadError/NotFoundType>) when the Dataforsyningen response is
unsuccessful or missing fields instead of returning null; use the existing
dataForsyningenClient.SearchZipCodesAsync response checks and map parse failures
(int.TryParse) into the appropriate OneOf outcome. Ensure new result types are
used consistently where LoadAsync already uses OneOf and preserve existing
symbols like GeocodeResult, HjemGroupProvider.CacheTag, and HjemGroupEntry.

In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs`:
- Around line 32-52: The catch-all in GetMarkersAsync swallows cancellations;
update the exception handling in GetMarkersAsync so OperationCanceledException
(and any cancellation originating from the provided CancellationToken or
innerToken used in fusionCache.GetOrSetAsync) is not converted into Error —
specifically, either check cancellationToken.IsCancellationRequested and rethrow
new OperationCanceledException(cancellationToken) or catch
OperationCanceledException and rethrow it before returning Error for other
exceptions; ensure this applies to both the outer method and any
OperationCanceledException thrown from the GetOrSetAsync callback (refer to
GetMarkersAsync, fusionCache.GetOrSetAsync call, and the innerToken parameter).

In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor`:
- Around line 41-80: The MudLink in HjemGroupManagementPage.razor that renders
context.WebsiteUrl should include rel="noopener noreferrer" via the
UserAttributes dictionary to prevent reverse-tabnabbing; update the MudLink (the
instance with Href="@context.WebsiteUrl.ToString()") to supply UserAttributes =
new Dictionary<string, object>{{ "rel", "noopener noreferrer" }} (or reuse a
shared attribute helper) so the rendered anchor receives rel="noopener
noreferrer".

In `@tasks/13-cheaper-infrastructure.md`:
- Line 108: Update the remaining stray "CX22" mentions to "CAX11": replace the
table row text "Hetzner CX22 ARM (app only)" with "Hetzner CAX11 ARM (app
only)", change the instruction "Create Hetzner CX22 ARM server" to "Create
Hetzner CAX11 ARM server", and update the note "CX22 has 4 GB ..." to "CAX11 has
4 GB ...", ensuring all three referenced strings are consistently renamed
(search for "CX22" in the file and substitute "CAX11").

---

Duplicate comments:
In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs`:
- Around line 22-46: GetMarkersAsync is caching markers under CacheKey with tag
CacheTag but there is no evidence here of a cache invalidation path; add or
confirm a call to fusionCache.RemoveByTag(CacheTag) (or
fusionCache.Remove(CacheKey)) from the admin save/update flow that modifies Hjem
groups (e.g., in the admin controller/service method that handles group saves),
or expose an explicit InvalidateHjemGroupCache() method that calls
fusionCache.RemoveByTag(CacheTag) and invoke it after successful persistence so
markers are refreshed immediately after admin updates.

---

Nitpick comments:
In `@tasks/13-cheaper-infrastructure.md`:
- Around line 198-204: The current curl POST to "application.deploy" (the block
that uses curl with x-api-key: ${{ secrets.DOKPLOY_API_KEY }} and -d
'{"applicationId":"${{ secrets.DOKPLOY_APPLICATION_ID }}"}') only validates job
acceptance because application.deploy is asynchronous; update the workflow to
either (A) poll the deployment status after the POST using the GET
/api/deployment.allByType?id=...&type=application endpoint until the deployment
reaches a terminal state and fail the step on terminal failure, or (B) replace
the curl step with the official dokploy/dokploy-action@v1 to let the action
handle polling, or (C) explicitly document and accept the async behavior and
rely on the existing Swarm health check/rollback, making the CI step's semantics
clear to the team; pick one approach and implement it for the application.deploy
invocation.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee72b73 and 0dc7aa0.

📒 Files selected for processing (13)
  • src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs
  • src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs
  • src/web/Jordnaer/Features/HjemGroups/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Features/Partners/PartnerService.cs
  • src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor
  • src/web/Jordnaer/Pages/GroupSearch/GroupSearch.razor
  • src/web/Jordnaer/Program.cs
  • src/web/Jordnaer/wwwroot/css/marker-cluster.css
  • src/web/Jordnaer/wwwroot/js/leaflet-interop.js
  • tasks/13-cheaper-infrastructure.md
  • tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs
  • tests/web/Jordnaer.Tests/HjemGroups/HjemGroupProviderTests.cs
  • tests/web/Jordnaer.Tests/UserSearch/DataForsyningenClientTests.cs
✅ Files skipped from review due to trivial changes (1)
  • src/web/Jordnaer/Features/Partners/PartnerService.cs
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/web/Jordnaer.Tests/UserSearch/DataForsyningenClientTests.cs
  • tests/web/Jordnaer.Tests/HjemGroups/HjemGroupAdminServiceTests.cs

Comment thread src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs Outdated
Comment thread src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs
Comment thread src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor
Comment thread tasks/13-cheaper-infrastructure.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor (1)

36-94: LGTM on the table structure overall, but the MudLink is still missing rel="noopener noreferrer".

The Target="_blank" link at line 79 should include rel="noopener noreferrer" to prevent reverse-tabnabbing. Pass it via UserAttributes:

🔒 Proposed fix
-                <MudLink Href="@context.WebsiteUrl.ToString()" Target="_blank">
+                <MudLink Href="@context.WebsiteUrl.ToString()" Target="_blank"
+                         UserAttributes="@(new Dictionary<string, object> { { "rel", "noopener noreferrer" } })">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor` around lines
36 - 94, The MudLink with Target="_blank" in the RowTemplate needs rel="noopener
noreferrer" to prevent reverse-tabnabbing; update the MudLink (component name
MudLink) to pass UserAttributes including rel="noopener noreferrer" (e.g. add
UserAttributes supplying the rel attribute) so external links opened in a new
tab include the security attribute.
src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs (1)

84-110: GeocodeAsync still returns null instead of OneOf for error/not-found cases.

Per coding guidelines (src/web/Jordnaer/Features/**/*.cs), service methods should use OneOf<TSuccess, TError> for explicit error handling. LoadAsync and SaveAsync already follow this pattern, but GeocodeAsync returns a nullable, making callers handle null checks instead of pattern-matching on a discriminated union.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs` around lines
84 - 110, Change GeocodeAsync to return OneOf<GeocodeResult, TError> (matching
the same error variant type used by LoadAsync/SaveAsync) instead of
Task<GeocodeResult?>: update the method signature, replace each null return
(failure to call dataForsyningenClient, missing content, invalid Visueltcenter,
parse failure) with returning the appropriate OneOf error variant (e.g.,
OneOf.FromT1(new GeocodeNotFound() or the repository's standard error type), and
return OneOf.FromT0(...) for the successful new GeocodeResult; also update all
callers to pattern-match the OneOf result. Ensure you reference GeocodeAsync and
GeocodeResult when making these changes so behavior matches other service
methods.
🧹 Nitpick comments (2)
src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs (1)

18-19: ContainerName and BlobName are duplicated in HjemGroupProvider.

Both this service and HjemGroupProvider define identical ContainerName = "hjemlo-groups" and BlobName = "groups.json" constants. Consider extracting them to a shared location (e.g., a static class or HjemGroupEntry) to keep them in sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs` around lines
18 - 19, The ContainerName and BlobName constants are duplicated between
HjemGroupAdminService and HjemGroupProvider; extract these into a single shared
constant (e.g., add public const strings ContainerName and BlobName to a shared
type such as HjemGroupEntry or a new static class like HjemGroupStorage) and
replace the private const usages in HjemGroupAdminService and HjemGroupProvider
to reference the shared symbols (HjemGroupEntry.ContainerName /
HjemGroupEntry.BlobName or HjemGroupStorage.ContainerName / BlobName) so both
classes use the single source of truth.
src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor (1)

253-315: Record value equality may cause IndexOf to match the wrong entry if duplicates exist.

HjemGroupEntry is a record, so IndexOf uses value equality rather than reference equality. If two entries have identical property values, editing one could replace the other. This is an edge case (unlikely with distinct geocoded data) but worth being aware of.

A safer approach would be to track the index directly when opening the dialog:

♻️ Sketch (optional refactor)
-    private HjemGroupEntry? _editingEntry;
+    private int _editingIndex = -1;

     private void OpenEditDialog(HjemGroupEntry entry)
     {
         _editingNew = false;
-        _editingEntry = entry;
+        _editingIndex = _entries.IndexOf(entry);
         // ...
     }

     private void CommitEdit()
     {
         // ...
-        else if (_editingEntry is not null)
+        else if (_editingIndex >= 0 && _editingIndex < _entries.Count)
         {
-            var idx = _entries.IndexOf(_editingEntry);
-            if (idx >= 0)
-                _entries[idx] = updated;
+            _entries[_editingIndex] = updated;
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor` around lines
253 - 315, CommitEdit uses _entries.IndexOf(_editingEntry) which relies on
HjemGroupEntry record value equality and can replace the wrong list item if
duplicates exist; change the dialog-edit flow to record the index when opening
the editor (e.g., add a private int? _editingIndex set in the method that
assigns _editingEntry) and then in CommitEdit use that stored _editingIndex to
update _entries[_editingIndex.Value] (fall back to adding when _editingNew is
true and clear _editingIndex when closing); keep using _editingEntry only for
displaying current values but use _editingIndex for the authoritative list
position to avoid IndexOf/value-equality issues.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor`:
- Around line 236-240: The Snackbar message currently always interpolates
result.ZipCode which shows empty parentheses when ZipCode is null; update the
Snackbar.Add call (the line invoking Snackbar.Add in
HjemGroupManagementPage.razor) to conditionally include the zip code from the
GeocodeResult (or omit the parentheses entirely) — e.g., build the message using
result.City plus (only if result.ZipCode is not null or empty) the `
({result.ZipCode})` suffix — leaving the assignments to
_form.City/_form.ZipCode/_form.Latitude/_form.Longitude unchanged.

---

Duplicate comments:
In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs`:
- Around line 84-110: Change GeocodeAsync to return OneOf<GeocodeResult, TError>
(matching the same error variant type used by LoadAsync/SaveAsync) instead of
Task<GeocodeResult?>: update the method signature, replace each null return
(failure to call dataForsyningenClient, missing content, invalid Visueltcenter,
parse failure) with returning the appropriate OneOf error variant (e.g.,
OneOf.FromT1(new GeocodeNotFound() or the repository's standard error type), and
return OneOf.FromT0(...) for the successful new GeocodeResult; also update all
callers to pattern-match the OneOf result. Ensure you reference GeocodeAsync and
GeocodeResult when making these changes so behavior matches other service
methods.

In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor`:
- Around line 36-94: The MudLink with Target="_blank" in the RowTemplate needs
rel="noopener noreferrer" to prevent reverse-tabnabbing; update the MudLink
(component name MudLink) to pass UserAttributes including rel="noopener
noreferrer" (e.g. add UserAttributes supplying the rel attribute) so external
links opened in a new tab include the security attribute.

---

Nitpick comments:
In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs`:
- Around line 18-19: The ContainerName and BlobName constants are duplicated
between HjemGroupAdminService and HjemGroupProvider; extract these into a single
shared constant (e.g., add public const strings ContainerName and BlobName to a
shared type such as HjemGroupEntry or a new static class like HjemGroupStorage)
and replace the private const usages in HjemGroupAdminService and
HjemGroupProvider to reference the shared symbols (HjemGroupEntry.ContainerName
/ HjemGroupEntry.BlobName or HjemGroupStorage.ContainerName / BlobName) so both
classes use the single source of truth.

In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor`:
- Around line 253-315: CommitEdit uses _entries.IndexOf(_editingEntry) which
relies on HjemGroupEntry record value equality and can replace the wrong list
item if duplicates exist; change the dialog-edit flow to record the index when
opening the editor (e.g., add a private int? _editingIndex set in the method
that assigns _editingEntry) and then in CommitEdit use that stored _editingIndex
to update _entries[_editingIndex.Value] (fall back to adding when _editingNew is
true and clear _editingIndex when closing); keep using _editingEntry only for
displaying current values but use _editingIndex for the authoritative list
position to avoid IndexOf/value-equality issues.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0dc7aa0 and 6229e8a.

📒 Files selected for processing (3)
  • src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs
  • src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor
  • tests/web/Jordnaer.Tests/HjemGroups/HjemGroupProviderTests.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/web/Jordnaer.Tests/HjemGroups/HjemGroupProviderTests.cs

Comment thread src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor (1)

236-241: ⚠️ Potential issue | 🟡 Minor

Snackbar still renders empty parentheses when ZipCode is null.

Line 241 interpolates result.ZipCode unconditionally: when null the user sees "Fandt Randers ()". Conditionally include the zip suffix.

✏️ Proposed fix
-            Snackbar.Add($"Fandt {result.City} ({result.ZipCode})", Severity.Success);
+            var zipInfo = result.ZipCode.HasValue ? $" ({result.ZipCode})" : "";
+            Snackbar.Add($"Fandt {result.City}{zipInfo}", Severity.Success);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor` around lines
236 - 241, The Snackbar message unconditionally interpolates result.ZipCode
which yields empty parentheses when ZipCode is null; update the Snackbar.Add
call (same area where _form.City/_form.ZipCode are set) to conditionally append
the " (ZipCode)" suffix only when result.ZipCode is non-null/non-empty (e.g.,
use a conditional or ternary around result.ZipCode or string.IsNullOrWhiteSpace)
so the message shows "Fandt City" when no zip and "Fandt City (ZipCode)" when
present; keep using the existing Snackbar.Add call and Severity.Success
severity.
🧹 Nitpick comments (1)
tasks/13-cheaper-infrastructure.md (1)

150-154: Make checksum verification executable, not just descriptive.

Good improvement, but the current step still relies on manual comparison. Add an explicit compare command so the runbook is deterministic.

Proposed runbook tweak
 # Download the installer first, verify it, then execute
 curl -sSL https://dokploy.com/install.sh -o dokploy-install.sh
-# Verify SHA256 checksum matches the value published on https://dokploy.com/docs/get-started/installation
-sha256sum dokploy-install.sh
+# Fetch expected SHA256 from Dokploy docs/release page, then compare explicitly
+EXPECTED_SHA256="<paste-published-sha256-here>"
+echo "${EXPECTED_SHA256}  dokploy-install.sh" | sha256sum -c -
 # Inspect the script before running
 less dokploy-install.sh
 sh dokploy-install.sh
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tasks/13-cheaper-infrastructure.md` around lines 150 - 154, Replace the
manual sha256sum step with an explicit verification that compares the computed
checksum of dokploy-install.sh to the published expected hash and fails the run
if it does not match; specifically, after downloading with curl and before
executing or inspecting, run a checksum-compare command that uses the expected
SHA256 string for dokploy-install.sh and exits non-zero on mismatch so the
installer is never executed when verification fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs`:
- Around line 59-95: The catch-all in LoadFromBlobAsync currently swallows
OperationCanceledException and returns Error; change the single catch(Exception
ex) into two catches: first catch (OperationCanceledException) { throw; } to
preserve cancellation semantics, then catch (Exception ex) { logger.LogError(ex,
"Failed to load HJEM group markers from blob storage."); return new Error(); }
so cancellations from blobServiceClient/blobClient calls propagate while other
exceptions are logged and wrapped as Error.

In `@tasks/13-cheaper-infrastructure.md`:
- Line 20: Replace the ambiguous claim "zero code changes required" with the
more precise phrase "zero application code changes required" wherever it appears
in this document (e.g., the summary sentence "Yes, it is viable. Estimated
monthly cost: ~$10-11 (saving ~$14/month, ~56% reduction) with zero code changes
required." and the other occurrences that reference CI/workflow edits), so
readers understand CI/workflow or infra configuration edits may still be
required even though application source code does not need changes.

---

Duplicate comments:
In `@src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor`:
- Around line 236-241: The Snackbar message unconditionally interpolates
result.ZipCode which yields empty parentheses when ZipCode is null; update the
Snackbar.Add call (same area where _form.City/_form.ZipCode are set) to
conditionally append the " (ZipCode)" suffix only when result.ZipCode is
non-null/non-empty (e.g., use a conditional or ternary around result.ZipCode or
string.IsNullOrWhiteSpace) so the message shows "Fandt City" when no zip and
"Fandt City (ZipCode)" when present; keep using the existing Snackbar.Add call
and Severity.Success severity.

---

Nitpick comments:
In `@tasks/13-cheaper-infrastructure.md`:
- Around line 150-154: Replace the manual sha256sum step with an explicit
verification that compares the computed checksum of dokploy-install.sh to the
published expected hash and fails the run if it does not match; specifically,
after downloading with curl and before executing or inspecting, run a
checksum-compare command that uses the expected SHA256 string for
dokploy-install.sh and exits non-zero on mismatch so the installer is never
executed when verification fails.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6229e8a and a2a3e3f.

📒 Files selected for processing (3)
  • src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs
  • src/web/Jordnaer/Pages/Backoffice/HjemGroupManagementPage.razor
  • tasks/13-cheaper-infrastructure.md

Comment thread src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs
Comment thread tasks/13-cheaper-infrastructure.md Outdated
@NielsPilgaard NielsPilgaard merged commit eb570a9 into main Feb 26, 2026
5 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Jordnaer Community Website Feb 26, 2026
@NielsPilgaard NielsPilgaard deleted the feature/hjemlo branch February 26, 2026 20:29
@coderabbitai coderabbitai Bot mentioned this pull request Feb 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant