feat(offline): make every dashboard write durable offline (P2–P5)
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m0s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 38s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 36s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 3m11s
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m0s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 38s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m5s
CI/CD / CI · Admin Web (tsc) (push) Successful in 36s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 3m11s
Builds on the outbox engine to take the whole dashboard offline in one place
instead of wiring 114 mutation sites individually.
Frontend (single chokepoint = the API client):
- offline-write: any write auto-queues to the outbox on offline/network failure
and returns an optimistic value; the online path is unchanged apart from an
Idempotency-Key header (so even online retries de-dup). entityType is derived
from the URL; POSTs get a remappable local id.
- client.doWrite unifies POST/PUT/PATCH/DELETE through this path. WriteOptions
gains `offline: "queue" | "reject" | "manual"`.
- Guardrails: auth / billing / payments / SMS / exports are online-only and throw
OFFLINE_UNAVAILABLE offline rather than queueing (no queued double-charges or
surprise SMS blasts). use-api-error resolves the friendly localized message
(fa/en/ar).
- submit-order opts out ("manual") to keep its richer local-Order mock; shared
helpers de-duplicated into offline-write.
- Request persistent storage on mount so unsynced writes survive eviction.
Backend:
- IdempotencyCleanupJob: daily purge of idempotency records older than 7 days
(the table now gets a row per keyed write). Registered in Hangfire. No migration.
86 API tests pass; dashboard tsc + build clean.
This commit is contained in:
@@ -245,6 +245,11 @@ public static class ServiceCollectionExtensions
|
||||
"branch-permanent-delete",
|
||||
job => job.ExecuteAsync(),
|
||||
Cron.Hourly);
|
||||
|
||||
RecurringJob.AddOrUpdate<IdempotencyCleanupJob>(
|
||||
"idempotency-cleanup",
|
||||
job => job.ExecuteAsync(),
|
||||
Cron.Daily(4));
|
||||
}
|
||||
|
||||
return app;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Meezi.Infrastructure.Data;
|
||||
|
||||
namespace Meezi.API.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// Purges old idempotency records. Keys only need to outlive realistic offline
|
||||
/// gaps and client retries, so a short retention keeps the table small.
|
||||
/// </summary>
|
||||
public class IdempotencyCleanupJob
|
||||
{
|
||||
private static readonly TimeSpan Retention = TimeSpan.FromDays(7);
|
||||
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<IdempotencyCleanupJob> _logger;
|
||||
|
||||
public IdempotencyCleanupJob(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<IdempotencyCleanupJob> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync()
|
||||
{
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var cutoff = DateTime.UtcNow - Retention;
|
||||
var removed = await db.IdempotencyRecords
|
||||
.Where(r => r.CreatedAt < cutoff)
|
||||
.ExecuteDeleteAsync();
|
||||
if (removed > 0)
|
||||
_logger.LogInformation("Purged {Count} idempotency records older than {Days}d", removed, Retention.TotalDays);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user