Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,5 @@ Don't invent a convention to fill a gap. Name what's missing, propose the smalle
- **In-app update check (added 2026-09-02, on branch `feature/update-check-notify`, not yet merged to staging — see ADR-0004):** Pulsemap's first-ever outbound network call, explicitly cleared with the PM given ADR-0003 had deliberately left this question open. Home now checks `https://api.github.com/repos/DotifyBIZ/pulsemap/releases/latest` on load (via new `IUpdateCheckService`/`GitHubUpdateCheckService`, App-layer, `IHttpClientFactory`-based) and shows a dismissible banner + release link if the tag is newer than the running build (`SemanticVersionComparer`, Core, pure/testable, compares `Assembly.GetName().Version` against a `vX.Y.Z` tag). Gated behind a new `AppSettings.CheckForUpdatesEnabled` (default on, Core, `FileAppSettingsService` at `%LocalAppData%\Pulsemap\settings.json` — Pulsemap's first app-preferences store, distinct from survey data and from `FileAppLogger`'s logs), with a visible toggle on the Settings page ("Check for updates on launch"). A real prerequisite gap found and fixed along the way: `scripts/build-installer.ps1`'s `dotnet publish` never passed `-p:Version=$Version`, so the published assembly's version was always the SDK's static default regardless of the actual release — without that fix, the whole comparison would have been meaningless. README's "won't phone home" bullet is amended accordingly (telemetry/account/server-free claim still holds; the literal zero-network-calls reading no longer does). 82 Core / 76 App tests on that branch — includes a stub-`HttpMessageHandler`-backed test of the actual HTTP/JSON path, not just the pure version-comparison logic.

**UX pass (started 2026-09-02, in progress)** — a full-codebase UX review turned up several friction points (Select tool overloaded with undiscoverable per-click-target behavior, no undo anywhere, no onboarding, inconsistent delete confirmation, silent AP-suggestion replacement, no survey-type context in Workspace, the Pivot-clipping bug). Being fixed incrementally, each as its own branch:
- **Workspace onboarding + tool legend, and a warmer Home (added 2026-09-02, on branch `feature/workspace-onboarding`, not yet merged to staging):** a "Tool Help" button (flyout, always available) now explains all four canvas tools including Select's three click-target-dependent behaviors. A skippable first-run coach-mark tour (`TeachingTip`, one per tool button, Next/Skip actions) walks a new user through them once — tracked via a new `AppSettings.HasSeenWorkspaceOnboarding` (same `IAppSettingsService` store the update-check feature added), `WorkspaceViewModel.ShouldShowOnboardingAsync`/`MarkOnboardingSeenAsync`. Separately, at the user's request, Home's static "Home" title is now a time-of-day greeting (`HomeViewModel.GreetingDisplay`, morning/afternoon/evening/late-night buckets on `DateTime.Now.Hour`) plus a small, non-interactive `WellbeingMessageDisplay` line picked at random from 4 options each visit — no reply affordance, just a quiet touch. The now-unused static "HomeTitle" resource was removed from both languages since this change made it dead.
- **Undo last delete (added 2026-09-02, on branch `feature/undo-last-delete`, not yet merged to staging):** the Delete tool removed a wall/test point/AP permanently with zero confirmation or recovery — the single highest-risk unconfirmed action in the app. Deliberately not a full undo/redo stack (that would touch nearly every mutation in `WorkspaceViewModel`) — just single-level: `TryFindNearestElement` now also reports *what* it's about to remove, `DeleteNearestElementAsync` stashes `(Floor, object Item)` before removing, and a new `UndoDeleteCommand` re-adds it. An `InfoBar` with an Undo action button appears after a delete (`CanUndoDelete`/`LastDeletedItemDisplay`), overwritten by the next delete and cleared on floor switch so it never fires somewhere other than where the deletion happened. 5 new tests covering restore-after-delete for both a wall and a test point, overwrite-on-second-delete, and floor-switch clearing.
4 changes: 4 additions & 0 deletions src/Pulsemap.App.Core/Settings/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ public sealed class AppSettings
/// API with no identifying data attached, not telemetry — but it's still an outbound network
/// call, so it stays user-visible and disableable in Settings.</summary>
public bool CheckForUpdatesEnabled { get; set; } = true;

/// <summary>Whether the Workspace first-run coach-mark tour has been shown or skipped —
/// suppresses it on every later visit regardless of which survey is open.</summary>
public bool HasSeenWorkspaceOnboarding { get; set; }
}
16 changes: 16 additions & 0 deletions src/Pulsemap.App/Services/LocalizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ public string GetString(string key)
["WorkspaceUndoDeleteAccessPointMessage"] = "Access point deleted.",

["HomeUpdateAvailableFormat"] = "Pulsemap {0} is available.",
["HomeGreetingMorning"] = "Good morning",
["HomeGreetingAfternoon"] = "Good afternoon",
["HomeGreetingEvening"] = "Good evening",
["HomeGreetingNight"] = "Hello",
["HomeWellbeingMessage1"] = "Hope you're doing well today.",
["HomeWellbeingMessage2"] = "Take a moment for yourself if you need one.",
["HomeWellbeingMessage3"] = "However today's going, we hope it gets a little easier.",
["HomeWellbeingMessage4"] = "Glad you're here. Take care of yourself out there.",
},
["pl-PL"] = new Dictionary<string, string>
{
Expand Down Expand Up @@ -198,6 +206,14 @@ public string GetString(string key)
["WorkspaceUndoDeleteAccessPointMessage"] = "Usunięto punkt dostępowy.",

["HomeUpdateAvailableFormat"] = "Dostępny jest Pulsemap {0}.",
["HomeGreetingMorning"] = "Dzień dobry",
["HomeGreetingAfternoon"] = "Dzień dobry",
["HomeGreetingEvening"] = "Dobry wieczór",
["HomeGreetingNight"] = "Witaj",
["HomeWellbeingMessage1"] = "Mamy nadzieję, że wszystko u Ciebie dobrze.",
["HomeWellbeingMessage2"] = "Jeśli potrzebujesz chwili dla siebie, zrób sobie przerwę.",
["HomeWellbeingMessage3"] = "Niezależnie od tego, jak mija dzisiejszy dzień, mamy nadzieję, że będzie choć trochę lżej.",
["HomeWellbeingMessage4"] = "Miło, że tu jesteś. Zadbaj o siebie.",
},
};
}
81 changes: 78 additions & 3 deletions src/Pulsemap.App/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@
</data>

<!-- Home page -->
<data name="HomeTitle.Text" xml:space="preserve">
<value>Home</value>
</data>
<data name="HomeSubtitle.Text" xml:space="preserve">
<value>Your surveys, all in one place.</value>
</data>
Expand Down Expand Up @@ -278,6 +275,84 @@
<data name="WorkspaceDeleteToolButton.Content" xml:space="preserve">
<value>Delete</value>
</data>
<data name="WorkspaceToolLegendButton.Content" xml:space="preserve">
<value>Tool Help</value>
</data>
<data name="WorkspaceToolLegendTitle.Text" xml:space="preserve">
<value>What each tool does</value>
</data>
<data name="WorkspaceToolLegendSelectHeader.Text" xml:space="preserve">
<value>Select</value>
</data>
<data name="WorkspaceToolLegendSelectBody.Text" xml:space="preserve">
<value>Click a wall to add it to a multi-select for batch material/thickness editing. Click a test point to rescan and replace its reading. On an outdoor area, drag its edge to resize or drag inside it to move the whole area.</value>
</data>
<data name="WorkspaceToolLegendAddTestPointHeader.Text" xml:space="preserve">
<value>Add Test Point</value>
</data>
<data name="WorkspaceToolLegendAddTestPointBody.Text" xml:space="preserve">
<value>Click anywhere on the canvas to place a manual test point without doing a full guided walk.</value>
</data>
<data name="WorkspaceToolLegendDrawWallHeader.Text" xml:space="preserve">
<value>Draw Wall</value>
</data>
<data name="WorkspaceToolLegendDrawWallBody.Text" xml:space="preserve">
<value>Click one endpoint, then click the other to draw a wall between them.</value>
</data>
<data name="WorkspaceToolLegendDeleteHeader.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="WorkspaceToolLegendDeleteBody.Text" xml:space="preserve">
<value>Click near a wall, test point, or access point to remove it. An Undo option appears right after, until your next delete.</value>
</data>
<data name="WorkspaceOnboardingStep1.Title" xml:space="preserve">
<value>Welcome to the Workspace</value>
</data>
<data name="WorkspaceOnboardingStep1.Subtitle" xml:space="preserve">
<value>Select does different things depending on what you click: a wall, a test point, or an outdoor area's edge. The Tool Help button always explains this if you forget.</value>
</data>
<data name="WorkspaceOnboardingStep1.ActionButtonContent" xml:space="preserve">
<value>Next</value>
</data>
<data name="WorkspaceOnboardingStep1.CloseButtonContent" xml:space="preserve">
<value>Skip tour</value>
</data>
<data name="WorkspaceOnboardingStep2.Title" xml:space="preserve">
<value>Add Test Point</value>
</data>
<data name="WorkspaceOnboardingStep2.Subtitle" xml:space="preserve">
<value>Click anywhere to drop a manual test point — or use the Adapter tab's guided walk to be led through suggested points instead.</value>
</data>
<data name="WorkspaceOnboardingStep2.ActionButtonContent" xml:space="preserve">
<value>Next</value>
</data>
<data name="WorkspaceOnboardingStep2.CloseButtonContent" xml:space="preserve">
<value>Skip tour</value>
</data>
<data name="WorkspaceOnboardingStep3.Title" xml:space="preserve">
<value>Draw Wall</value>
</data>
<data name="WorkspaceOnboardingStep3.Subtitle" xml:space="preserve">
<value>Click one end, then the other, to draw a wall. Give it a material afterward with Select for a more accurate coverage prediction.</value>
</data>
<data name="WorkspaceOnboardingStep3.ActionButtonContent" xml:space="preserve">
<value>Next</value>
</data>
<data name="WorkspaceOnboardingStep3.CloseButtonContent" xml:space="preserve">
<value>Skip tour</value>
</data>
<data name="WorkspaceOnboardingStep4.Title" xml:space="preserve">
<value>Delete</value>
</data>
<data name="WorkspaceOnboardingStep4.Subtitle" xml:space="preserve">
<value>Click near anything to remove it — an Undo option appears right after, so a misclick isn't permanent.</value>
</data>
<data name="WorkspaceOnboardingStep4.ActionButtonContent" xml:space="preserve">
<value>Got it</value>
</data>
<data name="WorkspaceOnboardingStep4.CloseButtonContent" xml:space="preserve">
<value>Skip tour</value>
</data>
<data name="WorkspaceBackButton.Content" xml:space="preserve">
<value>Back</value>
</data>
Expand Down
81 changes: 78 additions & 3 deletions src/Pulsemap.App/Strings/pl-PL/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@
</data>

<!-- Home page -->
<data name="HomeTitle.Text" xml:space="preserve">
<value>Strona główna</value>
</data>
<data name="HomeSubtitle.Text" xml:space="preserve">
<value>Wszystkie Twoje badania w jednym miejscu.</value>
</data>
Expand Down Expand Up @@ -278,6 +275,84 @@
<data name="WorkspaceDeleteToolButton.Content" xml:space="preserve">
<value>Usuń</value>
</data>
<data name="WorkspaceToolLegendButton.Content" xml:space="preserve">
<value>Pomoc dot. narzędzi</value>
</data>
<data name="WorkspaceToolLegendTitle.Text" xml:space="preserve">
<value>Co robi każde narzędzie</value>
</data>
<data name="WorkspaceToolLegendSelectHeader.Text" xml:space="preserve">
<value>Zaznacz</value>
</data>
<data name="WorkspaceToolLegendSelectBody.Text" xml:space="preserve">
<value>Kliknij ścianę, aby dodać ją do zaznaczenia wielokrotnego w celu zbiorczej edycji materiału/grubości. Kliknij punkt pomiarowy, aby ponownie zeskanować i zastąpić jego odczyt. W obszarze zewnętrznym przeciągnij krawędź, aby zmienić rozmiar, lub przeciągnij wnętrze, aby przesunąć cały obszar.</value>
</data>
<data name="WorkspaceToolLegendAddTestPointHeader.Text" xml:space="preserve">
<value>Dodaj punkt pomiarowy</value>
</data>
<data name="WorkspaceToolLegendAddTestPointBody.Text" xml:space="preserve">
<value>Kliknij dowolne miejsce na płótnie, aby ręcznie umieścić punkt pomiarowy bez pełnego pomiaru terenowego.</value>
</data>
<data name="WorkspaceToolLegendDrawWallHeader.Text" xml:space="preserve">
<value>Rysuj ścianę</value>
</data>
<data name="WorkspaceToolLegendDrawWallBody.Text" xml:space="preserve">
<value>Kliknij jeden koniec, a następnie drugi, aby narysować między nimi ścianę.</value>
</data>
<data name="WorkspaceToolLegendDeleteHeader.Text" xml:space="preserve">
<value>Usuń</value>
</data>
<data name="WorkspaceToolLegendDeleteBody.Text" xml:space="preserve">
<value>Kliknij w pobliżu ściany, punktu pomiarowego lub punktu dostępowego, aby go usunąć. Zaraz potem pojawi się opcja cofnięcia, ważna do kolejnego usunięcia.</value>
</data>
<data name="WorkspaceOnboardingStep1.Title" xml:space="preserve">
<value>Witaj w Obszarze roboczym</value>
</data>
<data name="WorkspaceOnboardingStep1.Subtitle" xml:space="preserve">
<value>Narzędzie Zaznacz robi różne rzeczy w zależności od tego, co klikniesz: ścianę, punkt pomiarowy lub krawędź obszaru zewnętrznego. Przycisk Pomoc dot. narzędzi zawsze to wyjaśni, gdybyś zapomniał.</value>
</data>
<data name="WorkspaceOnboardingStep1.ActionButtonContent" xml:space="preserve">
<value>Dalej</value>
</data>
<data name="WorkspaceOnboardingStep1.CloseButtonContent" xml:space="preserve">
<value>Pomiń</value>
</data>
<data name="WorkspaceOnboardingStep2.Title" xml:space="preserve">
<value>Dodaj punkt pomiarowy</value>
</data>
<data name="WorkspaceOnboardingStep2.Subtitle" xml:space="preserve">
<value>Kliknij dowolne miejsce, aby umieścić ręczny punkt pomiarowy — albo skorzystaj z pomiaru terenowego w zakładce Adapter, aby zostać poprowadzonym przez sugerowane punkty.</value>
</data>
<data name="WorkspaceOnboardingStep2.ActionButtonContent" xml:space="preserve">
<value>Dalej</value>
</data>
<data name="WorkspaceOnboardingStep2.CloseButtonContent" xml:space="preserve">
<value>Pomiń</value>
</data>
<data name="WorkspaceOnboardingStep3.Title" xml:space="preserve">
<value>Rysuj ścianę</value>
</data>
<data name="WorkspaceOnboardingStep3.Subtitle" xml:space="preserve">
<value>Kliknij jeden koniec, a potem drugi, aby narysować ścianę. Nadaj jej materiał później za pomocą Zaznacz, aby uzyskać dokładniejszą prognozę pokrycia.</value>
</data>
<data name="WorkspaceOnboardingStep3.ActionButtonContent" xml:space="preserve">
<value>Dalej</value>
</data>
<data name="WorkspaceOnboardingStep3.CloseButtonContent" xml:space="preserve">
<value>Pomiń</value>
</data>
<data name="WorkspaceOnboardingStep4.Title" xml:space="preserve">
<value>Usuń</value>
</data>
<data name="WorkspaceOnboardingStep4.Subtitle" xml:space="preserve">
<value>Kliknij w pobliżu dowolnego elementu, aby go usunąć — zaraz potem pojawi się opcja cofnięcia, więc pomyłka nie jest trwała.</value>
</data>
<data name="WorkspaceOnboardingStep4.ActionButtonContent" xml:space="preserve">
<value>Rozumiem</value>
</data>
<data name="WorkspaceOnboardingStep4.CloseButtonContent" xml:space="preserve">
<value>Pomiń</value>
</data>
<data name="WorkspaceBackButton.Content" xml:space="preserve">
<value>Wstecz</value>
</data>
Expand Down
21 changes: 21 additions & 0 deletions src/Pulsemap.App/ViewModels/HomeViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,31 @@ public partial class HomeViewModel(
// Home is a dashboard, not the library — the Surveys nav tab shows the full, unlimited list.
private const int RecentSurveysLimit = 3;

// A quiet, non-interactive touch — no reply expected or possible, just a small sign the app
// isn't purely transactional. Picked once per Home visit (HomeViewModel is transient, so a
// fresh navigation gets a fresh pick) rather than rotated on a timer or tracked across visits.
private static readonly string[] WellbeingMessageKeys =
[
"HomeWellbeingMessage1",
"HomeWellbeingMessage2",
"HomeWellbeingMessage3",
"HomeWellbeingMessage4",
];

private string? _releaseUrl;

public ObservableCollection<SurveySummary> Surveys { get; } = [];

public string GreetingDisplay => DateTime.Now.Hour switch
{
>= 5 and < 12 => localizationService.GetString("HomeGreetingMorning"),
>= 12 and < 17 => localizationService.GetString("HomeGreetingAfternoon"),
>= 17 and < 22 => localizationService.GetString("HomeGreetingEvening"),
_ => localizationService.GetString("HomeGreetingNight"),
};

public string WellbeingMessageDisplay { get; } = localizationService.GetString(WellbeingMessageKeys[Random.Shared.Next(WellbeingMessageKeys.Length)]);

[ObservableProperty]
public partial bool IsLoading { get; set; }

Expand Down
19 changes: 18 additions & 1 deletion src/Pulsemap.App/ViewModels/WorkspaceViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Pulsemap.App.Core.Persistence;
using Pulsemap.App.Core.Placement;
using Pulsemap.App.Core.Propagation;
using Pulsemap.App.Core.Settings;
using Pulsemap.App.Services;
using Windows.System;

Expand Down Expand Up @@ -42,6 +43,7 @@ public partial class WorkspaceViewModel : ObservableObject
private readonly IReportExporter _reportExporter;
private readonly ISurveyExportFilePickerService _exportFilePickerService;
private readonly IKrigingInterpolator _krigingInterpolator;
private readonly IAppSettingsService _appSettingsService;

private string? _filePath;

Expand All @@ -55,7 +57,8 @@ public WorkspaceViewModel(
ISurveyDataExporter surveyDataExporter,
IReportExporter reportExporter,
ISurveyExportFilePickerService exportFilePickerService,
IKrigingInterpolator krigingInterpolator)
IKrigingInterpolator krigingInterpolator,
IAppSettingsService appSettingsService)
{
_surveyFileService = surveyFileService;
_propagationModel = propagationModel;
Expand All @@ -67,6 +70,7 @@ public WorkspaceViewModel(
_reportExporter = reportExporter;
_exportFilePickerService = exportFilePickerService;
_krigingInterpolator = krigingInterpolator;
_appSettingsService = appSettingsService;
}

public event EventHandler? FloorChanged;
Expand Down Expand Up @@ -226,6 +230,19 @@ public string AccessPointSummaryDisplay
public string WallSelectionCountDisplay =>
string.Format(CultureInfo.CurrentCulture, _localizationService.GetString("WorkspaceWallSelectionCountFormat"), SelectedWallCount);

public async Task<bool> ShouldShowOnboardingAsync()
{
var settings = await _appSettingsService.LoadAsync();
return !settings.HasSeenWorkspaceOnboarding;
}

public async Task MarkOnboardingSeenAsync()
{
var settings = await _appSettingsService.LoadAsync();
settings.HasSeenWorkspaceOnboarding = true;
await _appSettingsService.SaveAsync(settings);
}

public async Task LoadAsync(string filePath, CancellationToken cancellationToken = default)
{
_filePath = filePath;
Expand Down
3 changes: 2 additions & 1 deletion src/Pulsemap.App/Views/HomePage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@
</Grid.ColumnDefinitions>

<StackPanel Grid.Column="0">
<TextBlock x:Uid="HomeTitle" FontSize="{StaticResource FontSize2xl}" FontWeight="SemiBold" Foreground="{StaticResource Gray900Brush}" />
<TextBlock Text="{x:Bind ViewModel.GreetingDisplay}" FontSize="{StaticResource FontSize2xl}" FontWeight="SemiBold" Foreground="{StaticResource Gray900Brush}" />
<TextBlock x:Uid="HomeSubtitle" FontSize="{StaticResource FontSizeSm}" Foreground="{StaticResource Gray500Brush}" Margin="0,4,0,0" />
<TextBlock Text="{x:Bind ViewModel.WellbeingMessageDisplay}" FontSize="{StaticResource FontSizeXs}" FontStyle="Italic" Foreground="{StaticResource Gray400Brush}" Margin="0,6,0,0" />
</StackPanel>

<Button Grid.Column="1" VerticalAlignment="Top" Style="{StaticResource PrimaryButtonStyle}" Click="NewSurvey_Click">
Expand Down
Loading
Loading