Files
2026-02-23 14:02:44 +01:00

184 lines
7.5 KiB
Plaintext

@model List<UserSyncState>
@{
ViewData["Title"] = "Sync Status";
var nextSync = (DateTimeOffset?)ViewBag.NextSyncUtc;
var lastCycle = (DateTimeOffset?)ViewBag.LastCycleStartedUtc;
var now = (DateTimeOffset)ViewBag.Now;
}
<div class="card">
<h1>📊 Sync Status</h1>
@if (TempData["SyncNowMessage"] is string msg)
{
<div style="background: #e8f5e9; border: 1px solid #a5d6a7; border-radius: 6px; padding: 12px 16px; margin-bottom: 16px; color: #2e7d32;">
@msg
</div>
}
@* ── Global sync schedule ── *@
<div style="background: #f0f4ff; border-radius: 8px; padding: 16px; margin-bottom: 24px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h3 style="margin-top: 0; margin-bottom: 0;">⏱ Sync Schedule</h3>
<div style="display: flex; gap: 8px;">
<a asp-action="TestExcel" class="btn" style="font-size: 0.85rem; padding: 0.4rem 1rem; background: #ff9800; color: white; text-decoration: none; border-radius: 4px;">
🧪 Test Excel
</a>
<a asp-action="SyncNow" class="btn btn-primary" style="font-size: 0.85rem; padding: 0.4rem 1rem; text-decoration: none; color: white;">
⚡ Sync Now
</a>
</div>
</div>
<table style="width: auto;">
<tr>
<td><strong>Server time (UTC):</strong></td>
<td>@now.ToString("yyyy-MM-dd HH:mm:ss") UTC</td>
</tr>
<tr>
<td><strong>Last sync cycle started:</strong></td>
<td>
@if (lastCycle.HasValue)
{
var ago = now - lastCycle.Value;
<span>@lastCycle.Value.ToString("yyyy-MM-dd HH:mm:ss") UTC <em>(@FormatTimeSpan(ago) ago)</em></span>
}
else
{
<span>⏳ Not yet run (worker starting up…)</span>
}
</td>
</tr>
<tr>
<td><strong>Next sync cycle at:</strong></td>
<td>
@if (nextSync.HasValue)
{
var remaining = nextSync.Value - now;
if (remaining.TotalSeconds <= 0)
{
<span>🔄 Running now…</span>
}
else
{
<span>@nextSync.Value.ToString("yyyy-MM-dd HH:mm:ss") UTC <em>(in @FormatTimeSpan(remaining))</em></span>
}
}
else
{
<span>⏳ Worker starting up…</span>
}
</td>
</tr>
</table>
</div>
@* ── Per-user status table ── *@
<p>All enrolled users and their current synchronization state.</p>
@if (Model.Count == 0)
{
<p><em>No users enrolled yet.</em></p>
}
else
{
<table>
<thead>
<tr>
<th>Status</th>
<th>Email</th>
<th>User ID</th>
<th>Delta Link</th>
<th>Last Sync</th>
<th>Last Result</th>
</tr>
</thead>
<tbody>
@foreach (var user in Model)
{
var hasDelta = !string.IsNullOrEmpty(user.LastDeltaLink);
var hasError = !string.IsNullOrEmpty(user.LastSyncError);
var statusClass = hasError ? "status-error" : hasDelta ? "status-synced" : "status-pending";
var statusLabel = hasError ? "Error" : hasDelta ? "Synced" : "Pending";
<tr>
<td>
<span class="status-dot @statusClass"></span>
@statusLabel
</td>
<td>@user.UserEmail</td>
<td><code>@user.UserId[..Math.Min(8, user.UserId.Length)]…</code></td>
<td>
@if (hasDelta)
{
<span title="@user.LastDeltaLink">✅ Stored (@(user.LastDeltaLink!.Length) chars)</span>
}
else
{
<span>⏳ Initial sync pending</span>
}
</td>
<td>
@if (user.LastSyncUtc.HasValue)
{
var syncAgo = now - user.LastSyncUtc.Value;
<span title="@user.LastSyncUtc.Value.ToString("o")">
@user.LastSyncUtc.Value.ToString("HH:mm:ss")
<br /><em>(@FormatTimeSpan(syncAgo) ago)</em>
</span>
}
else
{
<span>—</span>
}
</td>
<td>
@if (hasError)
{
var isTokenError = user.LastSyncError!.Contains("MsalUiRequired") ||
user.LastSyncError!.Contains("IDW10502") ||
user.LastSyncError!.Contains("re-authenticate");
if (isTokenError)
{
<span style="color: #d32f2f;">
🔑 Token expired — <a asp-action="Reauthorize" style="color: #d32f2f; font-weight: bold;">re-authenticate here</a>
</span>
}
else
{
<span style="color: #d32f2f;" title="@user.LastSyncError">
❌ @(user.LastSyncError!.Length > 80 ? user.LastSyncError[..80] + "…" : user.LastSyncError)
</span>
}
}
else if (!string.IsNullOrEmpty(user.LastSyncResult))
{
<span style="color: #2e7d32;">✅ @user.LastSyncResult</span>
}
else
{
<span>—</span>
}
</td>
</tr>
}
</tbody>
</table>
}
<p style="margin-top: 16px; font-size: 0.85em; color: #888;">
📝 Detailed sync logs are also written to <strong>CalendarSync.xlsx</strong> in each user's OneDrive root.
</p>
</div>
@functions {
static string FormatTimeSpan(TimeSpan ts)
{
if (ts.TotalSeconds < 60)
return $"{(int)ts.TotalSeconds}s";
if (ts.TotalMinutes < 60)
return $"{(int)ts.TotalMinutes}m {ts.Seconds}s";
if (ts.TotalHours < 24)
return $"{(int)ts.TotalHours}h {ts.Minutes}m";
return $"{(int)ts.TotalDays}d {ts.Hours}h";
}
}