184 lines
7.5 KiB
Plaintext
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";
|
|
}
|
|
}
|