diff --git a/MiGu.Server/Auth/PageCatalog.cs b/MiGu.Server/Auth/PageCatalog.cs
index 41eaeb6..e37191f 100644
--- a/MiGu.Server/Auth/PageCatalog.cs
+++ b/MiGu.Server/Auth/PageCatalog.cs
@@ -30,6 +30,8 @@ public static class PageCatalog
// ── 管理端 / Platform:概览 ──
new("admin-dashboard", "总览", "概览", ScopePlatform),
new("admin-map-monitor", "地图监控", "概览", ScopePlatform),
+ new("admin-tasks", "任务管理", "概览", ScopePlatform),
+ new("admin-alarms", "报警管理", "概览", ScopePlatform),
// ── 管理端 / Platform:设计与编排 ──
new("admin-maps", "地图管理", "设计与编排", ScopePlatform),
diff --git a/MiGu.Server/Controllers/FleetController.cs b/MiGu.Server/Controllers/FleetController.cs
new file mode 100644
index 0000000..cea75a9
--- /dev/null
+++ b/MiGu.Server/Controllers/FleetController.cs
@@ -0,0 +1,123 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using MiGu.Server.Fleet;
+using MiGu.Server.Persistence;
+
+namespace MiGu.Server.Controllers;
+
+/// 车队运维健康探针(延迟走 WatchDog TCP)+ CDM 任务平台侧快照读取。
+[ApiController]
+[Authorize]
+[Route("api/fleet")]
+public sealed class FleetController : ControllerBase
+{
+ private readonly FleetHealthService _health;
+ private readonly CdmTaskSyncer _cdmSyncer;
+ private readonly AlarmCollector _alarmCollector;
+ private readonly PlatformDbContext _db;
+
+ public FleetController(FleetHealthService health, CdmTaskSyncer cdmSyncer, AlarmCollector alarmCollector, PlatformDbContext db)
+ {
+ _health = health;
+ _cdmSyncer = cdmSyncer;
+ _alarmCollector = alarmCollector;
+ _db = db;
+ }
+
+ [HttpGet("health")]
+ public async Task>> Health(CancellationToken ct)
+ => Ok(await _health.GetAsync(ct));
+
+ ///
+ /// CDM 搬运任务列表(读平台快照库 cdm_tasks)。在线时先即时同步一次拿最新,
+ /// SimpleLite 关闭时回退最近快照,并通过 online/lastSyncAt 告知前端数据是否滞后。
+ ///
+ [HttpGet("tasks")]
+ public async Task> Tasks([FromQuery] int limit = 1000, CancellationToken ct = default)
+ {
+ await _cdmSyncer.SyncOnceAsync(ct);
+
+ var take = Math.Clamp(limit, 1, 5000);
+ var tasks = await _db.CdmTasks.AsNoTracking()
+ .OrderByDescending(t => t.CreateTime)
+ .Take(take)
+ .Select(t => new
+ {
+ id = t.Id,
+ taskId = t.TaskId,
+ missionId = t.MissionId,
+ missionName = t.MissionName,
+ missionTypeName = t.MissionTypeName,
+ srcSiteId = t.SrcSiteId,
+ srcLabel = t.SrcLabel,
+ dstSiteId = t.DstSiteId,
+ dstLabel = t.DstLabel,
+ status = t.Status,
+ statusCode = t.StatusCode,
+ carId = t.CarId,
+ carName = t.CarName,
+ priority = t.Priority,
+ createTime = t.CreateTime,
+ startTime = t.StartTime,
+ finishTime = t.FinishTime,
+ stuckReason = t.StuckReason,
+ overdue = t.Overdue
+ })
+ .ToListAsync(ct);
+
+ return Ok(new
+ {
+ online = _cdmSyncer.Online,
+ lastSyncAt = _cdmSyncer.LastSyncAt,
+ count = tasks.Count,
+ tasks
+ });
+ }
+
+ ///
+ /// 车辆报警列表(读平台记录 vehicle_alarms)。含活跃 + 历史;SimpleLite 离线时回退最近记录,
+ /// 通过 online/lastSyncAt 告知数据是否滞后。activeOnly=true 仅返回未恢复的报警。
+ ///
+ [HttpGet("alarms")]
+ public async Task> Alarms(
+ [FromQuery] int limit = 2000,
+ [FromQuery] bool activeOnly = false,
+ CancellationToken ct = default)
+ {
+ // 在线时先即时对帐一次,保证打开页面能拿到最新活跃报警。
+ await _alarmCollector.SyncOnceAsync(ct);
+
+ var take = Math.Clamp(limit, 1, 10000);
+ var query = _db.VehicleAlarms.AsNoTracking().AsQueryable();
+ if (activeOnly) query = query.Where(a => a.Status == "active");
+
+ var alarms = await query
+ .OrderByDescending(a => a.Status == "active")
+ .ThenByDescending(a => a.LastAt)
+ .Take(take)
+ .Select(a => new
+ {
+ id = a.Id,
+ carId = a.CarId,
+ carName = a.CarName,
+ info = a.Info,
+ level = a.Level,
+ status = a.Status,
+ firstAt = a.FirstAt,
+ lastAt = a.LastAt,
+ resolvedAt = a.ResolvedAt,
+ durationSecs = a.DurationSecs,
+ acknowledged = a.Acknowledged
+ })
+ .ToListAsync(ct);
+
+ return Ok(new
+ {
+ online = _alarmCollector.Online,
+ lastSyncAt = _alarmCollector.LastSyncAt,
+ count = alarms.Count,
+ alarms
+ });
+ }
+}
diff --git a/MiGu.Server/Controllers/OtaController.cs b/MiGu.Server/Controllers/OtaController.cs
new file mode 100644
index 0000000..f2f4185
--- /dev/null
+++ b/MiGu.Server/Controllers/OtaController.cs
@@ -0,0 +1,515 @@
+using System.Security.Claims;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using MiGu.Server.Configs;
+using MiGu.Server.Ota;
+
+namespace MiGu.Server.Controllers;
+
+[ApiController]
+[Authorize]
+[Route("api/ota")]
+public class OtaController : ControllerBase
+{
+ private readonly OtaStore _store;
+ private readonly WatchDogClient _wd;
+ private readonly OtaVehicleSource _vehicles;
+ private readonly OtaJobRunner _jobs;
+ private readonly OpsAuditStore _audits;
+ private readonly OtaOptions _opt;
+
+ public OtaController(
+ OtaStore store,
+ WatchDogClient wd,
+ OtaVehicleSource vehicles,
+ OtaJobRunner jobs,
+ OpsAuditStore audits,
+ IOptions opt)
+ {
+ _store = store;
+ _wd = wd;
+ _vehicles = vehicles;
+ _jobs = jobs;
+ _audits = audits;
+ _opt = opt.Value;
+ }
+
+ private string UserName =>
+ User.FindFirst("unique_name")?.Value
+ ?? User.Identity?.Name
+ ?? User.FindFirst(ClaimTypes.NameIdentifier)?.Value
+ ?? "unknown";
+
+ private string Scope => User.FindFirst("scope")?.Value ?? "";
+
+ private bool CanWrite()
+ {
+ if (string.Equals(Scope, "Platform", StringComparison.OrdinalIgnoreCase)) return true;
+ var ops = User.FindFirst("ops")?.Value ?? "";
+ var set = ops.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ return set.Contains("*") || set.Any(o => o.StartsWith("ops.ota", StringComparison.OrdinalIgnoreCase));
+ }
+
+ private bool DenyWrite(out ActionResult denied)
+ {
+ if (CanWrite()) { denied = null!; return false; }
+ denied = StatusCode(StatusCodes.Status403Forbidden, new { message = "需要 Platform 或 ops.ota.* 权限" });
+ return true;
+ }
+
+ private void Audit(string op, string target, string result, string? msg = null) =>
+ _audits.Append(UserName, Scope, op, target, result, msg);
+
+ [HttpGet("settings")]
+ public ActionResult GetSettings() => _store.GetSettings();
+
+ [HttpPut("settings")]
+ public ActionResult PutSettings([FromBody] OtaSettings settings)
+ {
+ if (DenyWrite(out var denied)) return denied;
+ var saved = _store.SaveSettings(settings);
+ Audit("ops.ota.settings", "settings", "ok");
+ return saved;
+ }
+
+ [HttpGet("target")]
+ public ActionResult