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
28 changes: 28 additions & 0 deletions scripts/_open-env.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
function Open-Env([string]$EnvFile) {
if (-not (Test-Path $EnvFile)) {
Write-Error "Env file not found: $EnvFile`nRun deploy.ps1 first to generate it."
exit 1
}

$config = @{}
Get-Content $EnvFile | Where-Object { $_ -match "^\s*[^#]\w+=.+" } | ForEach-Object {
$k, $v = $_ -split "=", 2
$config[$k.Trim()] = $v.Trim()
}

$required = "APP_SERVICE_HOSTNAME", "SWA_HOSTNAME"
$missing = $required | Where-Object { -not $config[$_] }
if ($missing) {
Write-Error "Missing or empty keys in ${EnvFile}: $($missing -join ", ")"
exit 1
}

$api = "https://$($config["APP_SERVICE_HOSTNAME"])/api/v1/health"
$ui = "https://$($config["SWA_HOSTNAME"])"

Write-Host "Opening API : $api"
Write-Host "Opening UI : $ui"

Start-Process $api
Start-Process $ui
}
7 changes: 6 additions & 1 deletion scripts/deploy.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,12 @@ $EntraClientId = [string]$EntraInfo.ClientId
Write-Success "Entra app: $EntraClientId (tenant $EntraTenantId)"

# Deploy Bicep
Write-Host " Deploying Bicep template (usually 3-10 minutes, longer if Azure is slow)..."
$deployMsg = if ($Tier -eq 'free') {
"Deploying Bicep template (5-15 minutes on Free tier; Free tier is slower than Basic due to resource constraints)..."
} else {
"Deploying Bicep template (3-5 minutes on Basic tier)..."
}
Write-Host " $deployMsg"
Write-Host " Progress updates every 15s; first update may take ~30s while deployment registers."
$BicepTemplate = Join-Path $RepoRoot "infra\main.bicep"
$DeploymentName = "deploy-${Environment}-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
Expand Down
2 changes: 2 additions & 0 deletions scripts/open-prod-in-browser.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
. "$PSScriptRoot\_open-env.ps1"
Open-Env "$PSScriptRoot\.env.prod"
2 changes: 2 additions & 0 deletions scripts/open-test-in-browser.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
. "$PSScriptRoot\_open-env.ps1"
Open-Env "$PSScriptRoot\.env.test"
58 changes: 41 additions & 17 deletions src/Frontend/AHKFlowApp.UI.Blazor/Pages/Health.razor
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,17 @@
@if (_loading)
{
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Small" />
<MudText Typo="Typo.body2" Class="mt-2">Checking system health...</MudText>
@if (_retryAttempt > 0)
{
<MudAlert Severity="Severity.Warning" Class="mt-3">
<MudText Typo="Typo.subtitle2"><strong>Unable to reach the API — retrying (@_retryAttempt of @MaxRetries)</strong></MudText>
<MudText Typo="Typo.body2" Class="mt-1">The API is not responding. If it is deployed on the Free tier, the API and database may take a few minutes to cold start.</MudText>
</MudAlert>
}
else
{
<MudText Typo="Typo.body2" Class="mt-2">Checking system health...</MudText>
}
}
else if (_healthCheck != null)
{
Expand Down Expand Up @@ -105,9 +115,12 @@
@code {
[Inject] private IAhkFlowAppApiHttpClient ApiClient { get; set; } = default!;

private const int MaxRetries = 10;

private HealthResponse? _healthCheck;
private bool _loading = true;
private bool _hasError;
private int _retryAttempt;
private readonly CancellationTokenSource _cts = new();

protected override async Task OnInitializedAsync()
Expand All @@ -119,26 +132,37 @@
{
_loading = true;
_hasError = false;
_retryAttempt = 0;
StateHasChanged(); // render the spinner before awaiting

try
{
_healthCheck = await ApiClient.GetHealthAsync(_cts.Token);
if (_healthCheck is null)
_hasError = true;
}
catch (OperationCanceledException)
while (true)
{
// page was disposed
}
catch
{
_hasError = true;
}
finally
{
_loading = false;
try
{
_healthCheck = await ApiClient.GetHealthAsync(_cts.Token);
if (_healthCheck is null)
_hasError = true;
break;
}
catch (OperationCanceledException)
{
break; // page was disposed
}
catch
{
if (++_retryAttempt >= MaxRetries)
{
_hasError = true;
break;
}
StateHasChanged();
int delaySeconds = Math.Min((int)Math.Pow(2, _retryAttempt), 20);
try { await Task.Delay(TimeSpan.FromSeconds(delaySeconds), _cts.Token); }
catch (OperationCanceledException) { break; }
}
}

_loading = false;
}

private async Task RefreshHealthAsync() => await LoadHealthAsync();
Expand Down
18 changes: 14 additions & 4 deletions src/Frontend/AHKFlowApp.UI.Blazor/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@
builder.Services.AddHttpClient<IAhkFlowAppApiHttpClient, AhkFlowAppApiHttpClient>(client =>
{
client.BaseAddress = new Uri(new Uri(builder.HostEnvironment.BaseAddress), apiBaseUrl);
client.Timeout = TimeSpan.FromSeconds(30);
client.Timeout = TimeSpan.FromSeconds(35);
})
.AddStandardResilienceHandler();
.AddStandardResilienceHandler(options =>
{
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(32);
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(30);
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(60);
});

builder.Services.AddHttpClient<IHotstringsApiClient, HotstringsApiClient>(client =>
{
Expand Down Expand Up @@ -67,10 +72,15 @@
builder.Services.AddHttpClient<IAhkFlowAppApiHttpClient, AhkFlowAppApiHttpClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
client.Timeout = TimeSpan.FromSeconds(30);
client.Timeout = TimeSpan.FromSeconds(35);
})
.AddHttpMessageHandler<ApiAuthorizationMessageHandler>()
.AddStandardResilienceHandler();
.AddStandardResilienceHandler(options =>
{
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(32);
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(30);
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(60);
});

builder.Services.AddHttpClient<IHotstringsApiClient, HotstringsApiClient>(client =>
{
Expand Down
Loading