SeedData.cs
21.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
using Microsoft.EntityFrameworkCore;
using RobotProductionSystem.Api.Domain;
using RobotProductionSystem.Api.Services;
namespace RobotProductionSystem.Api.Infrastructure;
public static class SeedData
{
public static async Task InitializeAsync(AppDbContext db, CancellationToken cancellationToken = default)
{
await SeedUsersAsync(db, cancellationToken);
await SeedSettingsAsync(db, cancellationToken);
await SeedDeviceTypesAsync(db, cancellationToken);
await SeedWorkOrdersAsync(db, cancellationToken);
await SeedSnAsync(db, cancellationToken);
await SeedOperationsAsync(db, cancellationToken);
await SeedDashboardAsync(db, cancellationToken);
await SeedShellAsync(db, cancellationToken);
}
private static DateTimeOffset Now() => DateTimeOffset.UtcNow;
private static string EventId(string prefix) => $"{prefix}-{Guid.NewGuid():N}";
private static async Task SeedUsersAsync(AppDbContext db, CancellationToken ct)
{
if (await db.Users.AnyAsync(ct))
{
return;
}
db.Users.AddRange(
new User
{
Id = 1001,
Username = "admin",
PasswordHash = PasswordHasher.Hash("123456"),
Name = "系统管理员",
Email = "admin@robot.local",
Station = "总控台",
AvatarSrc = "https://i.pravatar.cc/128?u=robot-admin",
AvatarAlt = "系统管理员",
Roles = [new UserRole { Role = "admin" }, new UserRole { Role = "qa_manager" }]
},
new User
{
Id = 1002,
Username = "operator",
PasswordHash = PasswordHasher.Hash("123456"),
Name = "装配工位操作员",
Email = "operator@robot.local",
Station = "装配工位 A01",
AvatarSrc = "https://i.pravatar.cc/128?u=robot-operator",
AvatarAlt = "装配工位操作员",
Roles = [new UserRole { Role = "operator" }]
});
await db.SaveChangesAsync(ct);
}
private static async Task SeedSettingsAsync(AppDbContext db, CancellationToken ct)
{
if (!await db.SettingsProfiles.AnyAsync(ct))
{
db.SettingsProfiles.Add(new SettingsProfile { Id = 1, Name = "Benjamin Canac", Email = "ben@nuxtlabs.com", Username = "benjamincanac" });
}
if (!await db.SettingsNotifications.AnyAsync(ct))
{
db.SettingsNotifications.Add(new SettingsNotification { Id = 1, Email = true, Desktop = false, ProductUpdates = true, WeeklyDigest = false, ImportantUpdates = true });
}
if (!await db.SecurityAccounts.AnyAsync(ct))
{
db.SecurityAccounts.Add(new SecurityAccount { Id = 1, PasswordHash = PasswordHasher.Hash("12345678"), AccountDeleted = false });
}
if (!await db.Members.AnyAsync(ct))
{
db.Members.AddRange(
Member(1, "Anthony Fu", "antfu@robot.local", "antfu", "member", "Assembly line member", "https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/antfu"),
Member(2, "Baptiste Leproux", "larbish@robot.local", "larbish", "member", "Quality check support", "https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/larbish"),
Member(3, "Benjamin Canac", "ben@nuxtlabs.com", "benjamincanac", "admin", "System admin", "https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/benjamincanac"),
Member(4, "Celine Dumerc", "celinedumerc@robot.local", "celinedumerc", "customer", "Customer representative", "https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/celinedumerc"),
Member(5, "Daniel Roe", "danielroe@robot.local", "danielroe", "member", "Production operator", "https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/danielroe"),
Member(6, "Hugo Richard", "hugorcd@robot.local", "hugorcd", "admin", "Operation supervisor", "https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/hugorcd"));
}
await db.SaveChangesAsync(ct);
}
private static Member Member(int id, string name, string email, string username, string role, string bio, string avatar) =>
new() { Id = id, Name = name, Email = email, Username = username, Role = role, Bio = bio, AvatarSrc = avatar, AvatarAlt = name };
private static async Task SeedDeviceTypesAsync(AppDbContext db, CancellationToken ct)
{
if (await db.DeviceTypes.AnyAsync(ct))
{
return;
}
var now = Now();
db.DeviceTypes.AddRange(
new DeviceType { Id = 1001, Name = "协作机械臂控制柜", Model = "RPS-CB-500", Category = "controller", LengthMm = 680, WidthMm = 520, HeightMm = 1480, WeightKg = 92, HasBattery = true, BatterySpec = "24V 15Ah 锂电", Description = "用于协作机械臂整机控制与供电管理。", UpdatedAt = now },
new DeviceType { Id = 1002, Name = "末端视觉模组", Model = "RPS-VM-220", Category = "vision", LengthMm = 210, WidthMm = 135, HeightMm = 118, WeightKg = 2.8m, HasBattery = false, Description = "安装于末端执行器位置,用于识别与定位。", UpdatedAt = now },
new DeviceType { Id = 1003, Name = "底盘驱动单元", Model = "RPS-CH-920", Category = "chassis", LengthMm = 1020, WidthMm = 740, HeightMm = 360, WeightKg = 118, HasBattery = true, BatterySpec = "48V 40Ah 磷酸铁锂", Description = "AGV 底盘驱动与转向一体化单元。", UpdatedAt = now },
new DeviceType { Id = 1004, Name = "工业网关", Model = "RPS-GW-110", Category = "communication", LengthMm = 280, WidthMm = 180, HeightMm = 86, WeightKg = 3.2m, HasBattery = false, Description = "用于产线网络与设备数据转发。", UpdatedAt = now });
await db.SaveChangesAsync(ct);
}
private static async Task SeedWorkOrdersAsync(AppDbContext db, CancellationToken ct)
{
if (await db.WorkOrders.AnyAsync(ct))
{
return;
}
db.WorkOrders.AddRange(
WorkOrder(4001, "WO-202605-001", "RPS-CB-500", "BATCH-202605-A", "装配一线", "danielroe", "Daniel Roe", "2026-05-20", "draft", 0, 50, "系统管理员"),
WorkOrder(4002, "WO-202605-002", "RPS-VM-220", "BATCH-202605-B", "装配二线", "antfu", "Anthony Fu", "2026-05-21", "pending_dispatch", 10, 80, "系统管理员"),
WorkOrder(4003, "WO-202605-003", "RPS-CH-920", "BATCH-202605-C", "测试一线", "larbish", "Baptiste Leproux", "2026-05-22", "running", 28, 100, "系统管理员"),
WorkOrder(4004, "WO-202605-004", "RPS-GW-110", "BATCH-202605-D", "质检线", "hugorcd", "Hugo Richard", "2026-05-18", "pending_qc", 40, 40, "质量经理"),
WorkOrder(4005, "WO-202605-005", "RPS-CB-500", "BATCH-202605-E", "装配三线", "benjamincanac", "Benjamin Canac", "2026-05-16", "closed", 60, 60, "系统管理员"));
await db.SaveChangesAsync(ct);
}
private static WorkOrder WorkOrder(int id, string orderNo, string deviceCode, string batchNo, string line, string ownerUsername, string ownerName, string plannedDate, string status, int completedSn, int totalSn, string by)
{
var createdAt = Now();
var initial = new WorkOrderEvent { EventId = EventId("wo-evt"), Action = "create", FromStatus = null, ToStatus = "draft", Operator = by, At = createdAt, Remark = "初始化工单" };
var events = new List<WorkOrderEvent> { initial };
var lastAction = "create";
var lastAt = createdAt;
if (status != "draft")
{
lastAction = "dispatch";
lastAt = createdAt.AddMilliseconds(1);
events.Insert(0, new WorkOrderEvent { EventId = EventId("wo-evt"), Action = "dispatch", FromStatus = "draft", ToStatus = status, Operator = by, At = lastAt, Remark = "初始状态同步" });
}
return new WorkOrder { Id = id, OrderNo = orderNo, DeviceCode = deviceCode, BatchNo = batchNo, Line = line, OwnerUsername = ownerUsername, OwnerName = ownerName, PlannedDate = DateOnly.Parse(plannedDate), Status = status, CompletedSn = completedSn, TotalSn = totalSn, CreatedBy = by, CreatedAt = createdAt, UpdatedBy = by, UpdatedAt = lastAt, LastAction = lastAction, LastActionAt = lastAt, LastActionBy = by, Events = events };
}
private static async Task SeedSnAsync(AppDbContext db, CancellationToken ct)
{
if (await db.SnItems.AnyAsync(ct))
{
return;
}
db.SnItems.AddRange(
Sn(1001, "SN-RBTX5-0001", "WO-202605-001", "in_process", ProcessStep.Assembly, "none", "系统管理员"),
Sn(1002, "SN-RBTX5-0002", "WO-202605-001", "frozen", ProcessStep.Testing, "open", "质量经理", freeze: "测试电流波动超阈值"),
Sn(1003, "SN-RBTM2-0001", "WO-202605-003", "completed", ProcessStep.FinalInspection, "closed", "系统管理员"),
Sn(1004, "SN-RBTM2-0002", "WO-202605-003", "pending", ProcessStep.Assembly, "none", "系统管理员"),
Sn(1005, "SN-RBTQA-0001", "WO-202605-004", "scrapped", ProcessStep.Rework, "rework", "质量经理", scrap: "主控板损坏无法修复"),
Sn(1006, "SN-RBTX5-0003", "WO-202605-002", "in_process", ProcessStep.Testing, "none", "系统管理员"),
Sn(1007, "SN-RBTX5-0004", "WO-202605-002", "frozen", ProcessStep.Testing, "rework", "质量经理", freeze: "重复扫码冲突待复核"),
Sn(1008, "SN-RBTX5-0005", "WO-202605-002", "completed", ProcessStep.Warehousing, "none", "系统管理员"),
Sn(1009, "SN-RBTQX-0001", "WO-202605-005", "scrapped", ProcessStep.FinalInspection, "closed", "系统管理员", scrap: "外壳结构缺陷"),
Sn(1010, "SN-RBTQX-0002", "WO-202605-005", "pending", ProcessStep.Assembly, "none", "系统管理员"),
Sn(1011, "SN-RBTX5-0006", "WO-202605-001", "in_process", ProcessStep.Debug, "none", "系统管理员"),
Sn(1012, "SN-RBTM2-0003", "WO-202605-003", "frozen", ProcessStep.Rework, "open", "质量经理", freeze: "异常单 EX-202605-009 未关闭"),
Sn(1013, "SN-RBTM2-0004", "WO-202605-003", "completed", ProcessStep.Warehousing, "none", "系统管理员"),
Sn(1014, "SN-RBTQA-0002", "WO-202605-004", "pending", ProcessStep.Assembly, "none", "系统管理员"),
Sn(1015, "SN-RBTQA-0003", "WO-202605-004", "in_process", ProcessStep.Testing, "open", "质量经理"),
Sn(1016, "SN-RBTQA-0004", "WO-202605-004", "completed", ProcessStep.FinalInspection, "none", "系统管理员"),
Sn(1017, "SN-RBTX5-0007", "WO-202605-001", "frozen", ProcessStep.Debug, "rework", "质量经理", freeze: "电机抖动异常待返修"),
Sn(1018, "SN-RBTX5-0008", "WO-202605-001", "pending", ProcessStep.Assembly, "none", "系统管理员"),
Sn(1019, "SN-RBTQX-0003", "WO-202605-005", "in_process", ProcessStep.Testing, "none", "系统管理员"),
Sn(1020, "SN-RBTQX-0004", "WO-202605-005", "completed", ProcessStep.Warehousing, "closed", "系统管理员"));
await db.SaveChangesAsync(ct);
}
private static SnItem Sn(int id, string sn, string wo, string status, ProcessStep step, string exception, string by, string? freeze = null, string? scrap = null)
{
var at = Now();
var item = new SnItem
{
Id = id,
Sn = sn,
WorkOrderNo = wo,
Status = status,
ExceptionStatus = exception,
FreezeReason = freeze,
ScrapReason = scrap,
CreatedBy = by,
CreatedAt = at,
UpdatedBy = by,
UpdatedAt = at,
LastAction = "import",
LastActionAt = at,
LastActionBy = by
};
if (!item.TryInitializeCurrentStep(step, out var error))
{
throw new InvalidOperationException($"初始化 SN 工序失败: {error}");
}
var importEvent = new SnEvent
{
EventId = EventId("sn-evt"),
Action = "import",
FromStatus = null,
ToStatus = status,
Operator = by,
At = at,
Reason = "初始化导入",
ExceptionStatus = exception
};
importEvent.CaptureCurrentStep(item.CurrentStep);
item.Events = [importEvent];
return item;
}
private static async Task SeedOperationsAsync(AppDbContext db, CancellationToken ct)
{
if (await db.OperationTasks.AnyAsync(ct))
{
return;
}
db.OperationTasks.AddRange(
Op(9001, "WO-202605-001", "SN-RBTX5-0001", ProcessStep.Assembly, "A01", "ASM-JIG-01", "装配工位操作员", "pending", "pending"),
Op(9002, "WO-202605-001", "SN-RBTX5-0002", ProcessStep.Assembly, "A02", "ASM-JIG-02", "装配工位操作员", "in_progress", "pending", "2026-05-18T07:15:00.000Z"),
Op(9003, "WO-202605-002", "SN-RBTX5-0003", ProcessStep.Testing, "T01", "TEST-BENCH-07", "质量经理", "pending_test", "pending", "2026-05-18T06:45:00.000Z"),
Op(9004, "WO-202605-003", "SN-RBTM2-0001", ProcessStep.FinalInspection, "Q01", "QC-STATION-03", "质量经理", "completed", "pass", "2026-05-18T05:00:00.000Z", "2026-05-18T05:30:00.000Z"),
Op(9005, "WO-202605-004", "SN-RBTQA-0001", ProcessStep.Testing, "T02", "TEST-BENCH-11", "质量经理", "failed", "fail", "2026-05-18T04:10:00.000Z", "2026-05-18T04:20:00.000Z"),
Op(9006, "WO-202605-004", "SN-RBTQA-0002", ProcessStep.Rework, "R01", "REPAIR-DESK-02", "装配工位操作员", "rework", "pending", "2026-05-18T03:00:00.000Z"),
Op(9007, "WO-202605-005", "SN-RBTQX-0003", ProcessStep.Assembly, "A03", "ASM-JIG-05", "装配工位操作员", "skipped", "pending"),
Op(9008, "WO-202605-003", "SN-RBTM2-0003", ProcessStep.Testing, "T01", "TEST-BENCH-07", "质量经理", "pending_test", "pending", "2026-05-18T02:10:00.000Z"));
await db.SaveChangesAsync(ct);
}
private static OperationTask Op(int id, string wo, string sn, ProcessStep step, string station, string device, string op, string status, string result, string? started = null, string? ended = null)
{
var at = Now();
var next = Rules.OperationNextAction[status];
return new OperationTask { Id = id, WorkOrderNo = wo, Sn = sn, StepName = step, Workstation = station, Device = device, Operator = op, Status = status, Result = result, StartedAt = started is null ? null : DateTimeOffset.Parse(started), EndedAt = ended is null ? null : DateTimeOffset.Parse(ended), NextAction = next, AuditEvents = [new OperationTaskEvent { EventId = EventId("op-evt"), Action = "start", FromStatus = null, ToStatus = status, Operator = op, At = at, Remark = "任务初始化", NextAction = next }] };
}
private static async Task SeedDashboardAsync(AppDbContext db, CancellationToken ct)
{
if (await db.DashboardWorkOrderSnapshots.AnyAsync(ct))
{
return;
}
(string orderNo, string line, string status, string date, int completed, int total, int inProcess, int frozen, int firstPassed, int firstTotal, int retestPassed, int retestTotal, int exceptions)[] rows =
[
("WO-202605-001", "L1-装配线", "running", "2026-05-17", 82, 120, 31, 7, 70, 82, 8, 12, 9),
("WO-202605-002", "L2-测试线", "pending_qc", "2026-05-17", 56, 56, 0, 0, 49, 56, 4, 6, 3),
("WO-202605-003", "L3-总装线", "in_exception", "2026-05-16", 35, 90, 43, 12, 28, 35, 3, 5, 14),
("WO-202605-004", "L1-装配线", "completed", "2026-05-16", 130, 130, 0, 0, 120, 130, 7, 9, 2),
("WO-202605-005", "L2-测试线", "pending_dispatch", "2026-05-15", 0, 75, 75, 0, 0, 0, 0, 0, 0),
("WO-202605-006", "L3-总装线", "running", "2026-05-15", 46, 88, 39, 3, 40, 46, 3, 4, 5),
("WO-202605-007", "L1-装配线", "pending_qc", "2026-05-14", 64, 64, 0, 0, 57, 64, 5, 7, 4),
("WO-202605-008", "L2-测试线", "completed", "2026-05-13", 108, 108, 0, 0, 100, 108, 6, 7, 2),
("WO-202605-009", "L3-总装线", "running", "2026-05-12", 52, 100, 42, 6, 45, 52, 4, 6, 7),
("WO-202605-010", "L1-装配线", "in_exception", "2026-05-11", 28, 72, 33, 11, 21, 28, 2, 4, 12),
("WO-202605-011", "L2-测试线", "running", "2026-05-10", 61, 92, 26, 5, 54, 61, 4, 6, 6),
("WO-202605-012", "L3-总装线", "pending_dispatch", "2026-05-09", 0, 80, 80, 0, 0, 0, 0, 0, 0)
];
var id = 1;
db.DashboardWorkOrderSnapshots.AddRange(rows.Select(x => new DashboardWorkOrderSnapshot { Id = id++, OrderNo = x.orderNo, Line = x.line, Status = x.status, PlannedDate = DateOnly.Parse(x.date), CompletedSn = x.completed, TotalSn = x.total, InProcessSn = x.inProcess, FrozenSn = x.frozen, FirstTestPassed = x.firstPassed, FirstTestTotal = x.firstTotal, RetestPassed = x.retestPassed, RetestTotal = x.retestTotal, ExceptionCount = x.exceptions }));
await db.SaveChangesAsync(ct);
}
private static async Task SeedShellAsync(AppDbContext db, CancellationToken ct)
{
await UpsertShellItem(db, "customers", """[{"id":1,"name":"Alex Smith","email":"alex.smith@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=1"},"status":"subscribed","location":"New York, USA"},{"id":2,"name":"Jordan Brown","email":"jordan.brown@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=2"},"status":"unsubscribed","location":"London, UK"},{"id":3,"name":"Taylor Green","email":"taylor.green@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=3"},"status":"bounced","location":"Paris, France"},{"id":4,"name":"Morgan White","email":"morgan.white@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=4"},"status":"subscribed","location":"Berlin, Germany"},{"id":5,"name":"Casey Gray","email":"casey.gray@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=5"},"status":"subscribed","location":"Tokyo, Japan"},{"id":6,"name":"Jamie Johnson","email":"jamie.johnson@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=6"},"status":"subscribed","location":"Sydney, Australia"},{"id":7,"name":"Riley Davis","email":"riley.davis@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=7"},"status":"subscribed","location":"New York, USA"},{"id":8,"name":"Kelly Wilson","email":"kelly.wilson@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=8"},"status":"subscribed","location":"London, UK"},{"id":9,"name":"Drew Moore","email":"drew.moore@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=9"},"status":"bounced","location":"Paris, France"},{"id":10,"name":"Jordan Taylor","email":"jordan.taylor@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=10"},"status":"subscribed","location":"Berlin, Germany"},{"id":11,"name":"Morgan Anderson","email":"morgan.anderson@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=11"},"status":"subscribed","location":"Tokyo, Japan"},{"id":12,"name":"Casey Thomas","email":"casey.thomas@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=12"},"status":"unsubscribed","location":"Sydney, Australia"},{"id":13,"name":"Jamie Jackson","email":"jamie.jackson@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=13"},"status":"unsubscribed","location":"New York, USA"},{"id":14,"name":"Riley White","email":"riley.white@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=14"},"status":"unsubscribed","location":"London, UK"},{"id":15,"name":"Kelly Harris","email":"kelly.harris@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=15"},"status":"subscribed","location":"Paris, France"},{"id":16,"name":"Drew Martin","email":"drew.martin@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=16"},"status":"subscribed","location":"Berlin, Germany"},{"id":17,"name":"Alex Thompson","email":"alex.thompson@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=17"},"status":"unsubscribed","location":"Tokyo, Japan"},{"id":18,"name":"Jordan Garcia","email":"jordan.garcia@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=18"},"status":"subscribed","location":"Sydney, Australia"},{"id":19,"name":"Taylor Rodriguez","email":"taylor.rodriguez@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=19"},"status":"bounced","location":"New York, USA"},{"id":20,"name":"Morgan Lopez","email":"morgan.lopez@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=20"},"status":"subscribed","location":"London, UK"}]""", ct);
await UpsertShellItem(db, "mails", """[{"id":1,"from":{"name":"Alex Smith","email":"alex.smith@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=1"}},"subject":"Meeting Schedule: Q1 Marketing Strategy Review","body":"Production planning meeting reminder.","date":"2026-05-18T00:00:00.000Z"},{"id":2,"unread":true,"from":{"name":"Jordan Brown","email":"jordan.brown@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=2"}},"subject":"Project Phoenix - Sprint 3 Update","body":"Quick update on sprint deliverables.","date":"2026-05-17T23:53:00.000Z"}]""", ct);
await UpsertShellItem(db, "notifications", """[{"id":1,"unread":true,"sender":{"name":"Jordan Brown","email":"jordan.brown@example.com","avatar":{"src":"https://i.pravatar.cc/128?u=2"}},"body":"sent you a message","date":"2026-05-17T23:53:00.000Z"},{"id":2,"sender":{"name":"Lindsay Walton"},"body":"subscribed to your email list","date":"2026-05-17T23:00:00.000Z"}]""", ct);
await db.SaveChangesAsync(ct);
}
private static async Task UpsertShellItem(AppDbContext db, string kind, string payloadJson, CancellationToken ct)
{
var item = await db.ShellJsonItems.SingleOrDefaultAsync(x => x.Kind == kind, ct);
if (item is null)
{
db.ShellJsonItems.Add(new ShellJsonItem { Kind = kind, PayloadJson = payloadJson });
return;
}
item.PayloadJson = payloadJson;
}
}