refactor: v1 deprecation — migrate legacy tables to V4#119
Open
refactor: v1 deprecation — migrate legacy tables to V4#119
Conversation
Remove 12 coach marks that just restated obvious UI labels (sidebar nav, page headings, metric labels). Rewrite 8 onboarding marks to give actionable guidance instead of repeating card descriptions — e.g. "Devices → Your CGM, pump, and meter" becomes "Add your current device → Add the CGM, pump, or meter you use right now and mark it as current." Consolidate duplicate marks on dashboard (widgets, chart) from paired arrays down to single marks with better descriptions.
- URL now includes dash-formatted code (ABC-DEFG) - ValidateSessionAsync uses AsNoTracking for per-request efficiency - Added test: creator (not just data owner) can revoke guest links
Tasks 7-9: GuestSessionHandler (priority 52) authenticates via encrypted cookie backed by IDataProtector with 30s validation cache. GuestLinkController provides CRUD + activation endpoints. MemberScopeMiddleware grants scopes directly from the guest grant, bypassing membership lookup since guests have no SubjectId. Also moves baseUrl from GuestLinkService constructor to a method parameter on CreateGuestLinkAsync so the service can be registered normally via DI, with the controller computing the URL from the request.
Brings in unified API key system.
…st banner - Add /guest/[[code]] entry page for guest link activation - Detect nocturne-guest-session cookie in authHandle and validate via API session endpoint, creating a guest-specific user object - Add isGuestSession and guestExpiresAt to App.Locals and layout data - Add GuestBanner component with countdown timer shown in guest mode - Hide Settings, Members, Tools, Food, Meals, Dev Tools, tenant switcher, and notifications in AppSidebar for guest sessions - Forward guest session cookie through the API proxy handler - Add /guest to PUBLIC_PREFIXES so the entry page bypasses auth gates
…try shape Projects SensorGlucose, MeterGlucose, and Calibration models back into the legacy Entry shape for V1/V3 API compatibility. Handles LegacyId fallback, timestamp formatting, and type-specific field mapping.
…h V4-only reads Implements IEntryStore by querying ISensorGlucoseRepository, IMeterGlucoseRepository, and ICalibrationRepository exclusively, projecting results into legacy Entry shape via EntryProjection. Handles type routing, merge-sort across all three types, demo mode filtering, time range parsing from MongoDB-style find queries, DateString filtering, pagination, and duplicate detection.
…String priority - ResolveDemoFilter() now returns (source, excludeDemo) tuple: when demo mode is off, post-filter ephemeral records instead of passing null (which let demo data leak through) - Single-type queries (sgv/mbg/cal) push limit/offset directly to the database instead of over-fetching count+skip and paginating in memory - Add comment documenting that DateString takes priority over Find-based time range, matching old DualPathEntryStore behavior - Add tests for demo exclusion in both QueryAsync and GetCurrentAsync
…gacy table EntryService.CreateEntriesAsync now validates entry types (sgv/mbg/cal) and writes directly to V4 tables via IEntryDecomposer.DecomposeAsync instead of IEntryRepository.CreateEntriesAsync. UpdateEntryAsync uses the decomposer's idempotent LegacyId-based upsert. DeleteEntryAsync deletes from V4 tables via DeleteByLegacyIdAsync. SignalREntryEventSink sets DecomposeToV4 = false since decomposition now happens before side effects fire. IEntryRepository is retained only for the bulk delete path (DeleteEntriesAsync).
…vice layer Replace DualPathEntryStore with EntryReadService in DI registration so all entry reads go through V4 tables exclusively. Remove IEntryRepository from V3 EntriesController and route all operations through IEntryService, which delegates to the new EntryReadService for reads and IEntryDecomposer for writes. V1 controller already used IEntryService exclusively (no-op).
Delete DualPathEntryStore (replaced by EntryReadService), and remove MergeAndDeduplicate, SelectMostRecent, and ShouldProject from EntryDomainLogic — these were only needed for the dual-path merge between legacy entries and V4 projections. Clean up stale seealso references and comments in EntryReadService and V4ToLegacyProjectionService.
…ntries Golden file tests were seeding EntryEntity (legacy entries table) but EntryReadService now reads from V4 tables (sensor_glucose, meter_glucose). Updated all entries golden tests to seed SensorGlucoseEntity and MeterGlucoseEntity directly, added SeedSensorGlucose/SeedMeterGlucose helpers to GoldenFileTestBase, and accepted updated snapshot files.
…tore (Phase 2) Swap PredictionService, AlexaService, CacheWarmingService, DDataService, and DebugController from IEntryRepository to IEntryStore.QueryAsync / GetCurrentAsync. Update corresponding unit test mocks.
…repos (Phase 3) Replace IEntryRepository usage in DataSourceService, ConnectorHealthService, ServicesController, and DemoServiceHealthMonitor with V4 repository methods and new IDataSourceService aggregation methods. - Add GetLatestTimestampAsync, GetOldestTimestampAsync, DeleteBySourceAsync to ISensorGlucoseRepository, IMeterGlucoseRepository, ICalibrationRepository - Add GetDataSourceStatsAsync, GetLatestGlucoseTimestampBySourceAsync, GetOldestGlucoseTimestampBySourceAsync, DeleteGlucoseDataBySourceAsync to IDataSourceService - Move GetEntryStatsBySourceAsync logic from EntryRepository to DataSourceService (querying V4 tables instead of legacy entries) - DataSourceService.GetActiveDataSourcesAsync now queries sensor_glucose instead of legacy entries table - DataSourceService delete methods use V4 repo DeleteBySourceAsync - ConnectorHealthService depends on IDataSourceService instead of IEntryRepository - ServicesController uses IDataSourceService for glucose timestamps - DemoServiceHealthMonitor uses IDataSourceService.DeleteDemoDataAsync
…s (Phase 4) Replace all IEntryRepository usage in the demo service with ISensorGlucoseRepository: - DemoEntryService: creates SensorGlucose records directly via BulkCreateAsync instead of legacy Entry objects through IEntryRepository - DemoDataHostedService: uses ISensorGlucoseRepository.DeleteBySourceAsync for clearing demo data instead of IEntryRepository.DeleteEntriesByDataSourceAsync - Program.cs /stats endpoint: uses CountBySourceAsync instead of CountEntriesAsync with JSON find query - Program.cs /clear endpoint: uses DeleteBySourceAsync instead of DeleteEntriesByDataSourceAsync Also adds CountBySourceAsync to ISensorGlucoseRepository interface and implementation, and registers ISensorGlucoseRepository + IAuditContext in the demo service DI container.
Contributor
Preview Container ImagesPublished for commit
This comment is updated on each push to this PR. |
Resolve conflict in TenantPermissions.cs — accept audit permissions (AuditRead, AuditManage) from main.
…ntryService (Phase 5a)
…letion (Phase 6b)
…oser for bulk delete (Phase 6c)
…ase 6g) - Delete RecordType.Entry branch in GetOrCreateCanonicalIdAsync (dead code) - Delete "entry" case in RecordExistsAsync - Delete GetUnifiedEntryAsync and MergeEntries (zero callers) - Remove GetUnifiedEntryAsync from IDeduplicationService interface - Remove entryCount from DeduplicateAllAsync total; add meterGlucoseCount - Refactor DeduplicateEntriesAsync to scan SensorGlucose + MeterGlucose V4 tables instead of legacy Entries table, using appropriate RecordType strings (sensorglucose/meterglucose) for linked records
…yMapper, V4BackfillService (Phase 6h) Delete dead code now that all consumers have been migrated to V4 tables: - IEntryRepository, EntryRepository, EntryEntity, EntryMapper - V4BackfillService and BackfillController - EntryRepositoryTests, entry-specific soft-delete tests - All EntryEntity model configuration from NocturneDbContext - DefaultEntryConverters from QueryParser - IEntryRepository DI registrations from ServiceCollectionExtensions - Legacy entry migration methods from MigrationJobService Fix all compilation errors in test files by converting EntryEntity seeding to SensorGlucoseEntity and removing EntryRepository references from integration/performance benchmarks.
|
|
||
| if (hasFind && !hasTimeBounds) | ||
| { | ||
| _logger.LogWarning("BulkDelete refused: find query has no parseable time range, would delete all records. find={Find}", find); |
|
|
||
| var total = (long)sgDeleted + mgDeleted + calDeleted; | ||
| _logger.LogInformation("BulkDelete: removed {Total} v4 records (sg={Sg}, mg={Mg}, cal={Cal}) for find={Find}", | ||
| total, sgDeleted, mgDeleted, calDeleted, find); |
…triesAsync LINQ translation
| if (bolusCalcsTotal > 0) { typeBreakdown["BolusCalculations"] = bolusCalcsTotal; typeBreakdown24h["BolusCalculations"] = bolusCalcs24h; } | ||
| if (notesTotal > 0) { typeBreakdown["Notes"] = notesTotal; typeBreakdown24h["Notes"] = notes24h; } | ||
| if (deviceEventsTotal > 0) { typeBreakdown["DeviceEvents"] = deviceEventsTotal; typeBreakdown24h["DeviceEvents"] = deviceEvents24h; } | ||
| if ((stateSpanStats?.TotalStateSpans ?? 0) > 0) { typeBreakdown["StateSpans"] = stateSpanStats!.TotalStateSpans; typeBreakdown24h["StateSpans"] = stateSpanStats.StateSpansLast24Hours; } |
| if (deviceEventsTotal > 0) { typeBreakdown["DeviceEvents"] = deviceEventsTotal; typeBreakdown24h["DeviceEvents"] = deviceEvents24h; } | ||
| if ((stateSpanStats?.TotalStateSpans ?? 0) > 0) { typeBreakdown["StateSpans"] = stateSpanStats!.TotalStateSpans; typeBreakdown24h["StateSpans"] = stateSpanStats.StateSpansLast24Hours; } | ||
| if (deviceStatusTotal > 0) { typeBreakdown["DeviceStatus"] = deviceStatusTotal; typeBreakdown24h["DeviceStatus"] = deviceStatus24h; } | ||
| if ((treatmentStats?.TotalTreatments ?? 0) > 0) { typeBreakdown["Treatments"] = treatmentStats!.TotalTreatments; typeBreakdown24h["Treatments"] = treatmentStats.TreatmentsLast24Hours; } |
| if ((treatmentStats?.TotalTreatments ?? 0) > 0) { typeBreakdown["Treatments"] = treatmentStats!.TotalTreatments; typeBreakdown24h["Treatments"] = treatmentStats.TreatmentsLast24Hours; } | ||
|
|
||
| var lastTreatmentTime = treatmentStats?.LastTreatmentMills.HasValue == true | ||
| ? DateTimeOffset.FromUnixTimeMilliseconds(treatmentStats.LastTreatmentMills.Value).UtcDateTime |
| ? DateTimeOffset.FromUnixTimeMilliseconds(treatmentStats.LastTreatmentMills.Value).UtcDateTime | ||
| : (DateTime?)null; | ||
| var firstTreatmentTime = treatmentStats?.FirstTreatmentMills.HasValue == true | ||
| ? DateTimeOffset.FromUnixTimeMilliseconds(treatmentStats.FirstTreatmentMills.Value).UtcDateTime |
| { | ||
| var results = await _sgRepo.GetAsync(from, to, device, source: null, limit: 100, offset: 0, descending: true, nativeOnly: false, ct: ct); | ||
| var match = sgv.HasValue | ||
| ? results.FirstOrDefault(r => Math.Abs(r.Mgdl - sgv.Value) < 0.01) |
| { | ||
| var results = await _mgRepo.GetAsync(from, to, device, source: null, limit: 100, offset: 0, descending: true, ct: ct); | ||
| var match = mbg.HasValue | ||
| ? results.FirstOrDefault(r => Math.Abs(r.Mgdl - mbg.Value) < 0.01) |
| var query = _context.Calibrations.AsQueryable(); | ||
|
|
||
| if (from.HasValue) | ||
| query = query.Where(e => e.Timestamp >= from.Value); |
| if (from.HasValue) | ||
| query = query.Where(e => e.Timestamp >= from.Value); | ||
| if (to.HasValue) | ||
| query = query.Where(e => e.Timestamp < to.Value); |
| var query = _context.MeterGlucose.AsQueryable(); | ||
|
|
||
| if (from.HasValue) | ||
| query = query.Where(e => e.Timestamp >= from.Value); |
| if (from.HasValue) | ||
| query = query.Where(e => e.Timestamp >= from.Value); | ||
| if (to.HasValue) | ||
| query = query.Where(e => e.Timestamp < to.Value); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Incrementally deprecates legacy V1 infrastructure by migrating reads and writes from legacy tables to V4 tables.
Entries table (Phases 1-4 complete)
V1/V3 entry CRUD and most internal consumers now read/write exclusively from V4 tables (
sensor_glucose,meter_glucose,calibrations). The legacyentriestable remains but is only used by 4 consumers that depend on the MongoDB-style find query parser.EntryProjectionstatic mappers (V4 to Entry shape)EntryReadServicereplacingDualPathEntryStorefor V4-only readsDualPathEntryStoreremovedIEntryStoreISensorGlucoseRepositoryRemaining work
See
docs/plans/2026-04-24-drop-entries-table-phases.mdfor the full roadmap.Test plan