Skip to content

Begin modeling pictures in MiniLcm#2300

Merged
rmunn merged 47 commits into
developfrom
feat/pictures
Jun 30, 2026
Merged

Begin modeling pictures in MiniLcm#2300
rmunn merged 47 commits into
developfrom
feat/pictures

Conversation

@rmunn

@rmunn rmunn commented May 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #2232.
Fixes #2233.

UI will be in a different PR.

Pictures now sync between FwData and Crdt, and unit tests verify that the sync is correct. Until the UI PR is ready, this one can be manually tested by checking the "Entry as JSON" data: the Pictures list is a child of each Sense.

All unit tests and integration tests are passing.

NOTE: Should probably incorporate the changes from #2367 into the PictureSync code as well, i.e. have the sync helpers fire off SubmitUpdate instead of Update (and so on) for pictures as well.

@rmunn rmunn self-assigned this May 27, 2026
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 44b0e5ea-7fdd-49d1-8471-11f1a2e6ed93

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds picture modeling and CRUD support for Sense.Pictures across MiniLcm, CRDT storage, sync helpers, backend adapters, frontend types, and test fixtures.

Changes

Picture support on Senses

Layer / File(s) Summary
Picture model and API contracts
backend/FwLite/MiniLcm/Models/*, backend/FwLite/MiniLcm/IMiniLcm*.cs, backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs, frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/*
Picture becomes a MiniLcm model, Sense gains Pictures, and the read/write interfaces plus generated viewer types expose picture data.
Picture change types and ordering
backend/FwLite/LcmCrdt/Changes/*.cs, backend/FwLite/LcmCrdt/OrderPicker.cs, backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs, backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.*.verified.txt, backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt
CreateSenseChange carries picture data, new create/update/reorder/remove changes operate on Sense.Pictures, and change fixtures cover those payloads.
CRDT storage and migration wiring
backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs, backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs, backend/FwLite/LcmCrdt/LcmCrdtKernel.cs, backend/FwLite/LcmCrdt/Migrations/*PicturesColumnToSenses*, backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs, backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt, backend/FwLite/LcmCrdt.Tests/Data/*
Sense.Pictures is persisted to jsonb, picture CRDT APIs generate picture changes, and migration/snapshot fixtures add the new column and serialized picture data.
Picture sync and normalization
backend/FwLite/MiniLcm/SyncHelpers/*, backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs, backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs, backend/FwLite/FwLiteProjectSync.Tests/*, backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/*
PictureSync diffs picture lists and patches, SenseSync accumulates picture sync results, normalization/dry-run wrappers forward picture writes, and sync tests adjust order handling.
FwData and LF Classic adapters
backend/FwLite/FwDataMiniLcmBridge/Api/..., backend/LfClassicData/Entities/Entry.cs, backend/LfClassicData/LfClassicMiniLcmApi.cs
FwDataMiniLcmApi maps pictures in both directions and adds picture CRUD, UpdatePictureProxy exposes picture updates, and LF Classic entities/API convert Sense.Pictures and fetch pictures by id.
Frontend demo sense data
frontend/viewer/src/lib/utils.ts, frontend/viewer/src/lib/sandbox/Sandbox.svelte, frontend/viewer/src/project/demo/demo-entry-data.ts
Viewer defaults, sandbox senses, and demo entry fixtures now initialize pictures: [].

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related PRs

Suggested reviewers

  • myieye

Poem

A bunny found a picture trail,
with captions hopping by the tail.
I thumped through sync and jsonb skies,
and tucked new Pictures into Sensey pies.
Hooray! 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.30% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding picture modeling support in MiniLcm.
Linked Issues check ✅ Passed The changes implement picture modeling and FieldWorks-to-FW-Lite sync, covering the goals in #2232 and #2233.
Out of Scope Changes check ✅ Passed The added frontend types, migrations, sync logic, and tests all support picture modeling and sync, with no clear unrelated changes.
Description check ✅ Passed The description matches the changeset: it explains picture sync/modeling work across FwData and CRDT and mentions related tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pictures

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the 💻 FW Lite issues related to the fw lite application, not miniLcm or crdt related label May 27, 2026
@rmunn rmunn linked an issue May 27, 2026 that may be closed by this pull request
@github-actions

github-actions Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

UI unit Tests

  1 files  ±0   62 suites  ±0   30s ⏱️ +2s
186 tests ±0  186 ✅ ±0  0 💤 ±0  0 ❌ ±0 
258 runs  ±0  258 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 0891da0. ± Comparison against base commit 2d6b20e.

♻️ This comment has been updated with latest results.

@argos-ci

argos-ci Bot commented May 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ✅ No changes detected - Jun 30, 2026, 4:36 AM

@github-actions github-actions Bot added the 📦 Lexbox issues related to any server side code, fw-headless included label May 28, 2026
@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

C# Unit Tests

165 tests   165 ✅  21s ⏱️
 23 suites    0 💤
  1 files      0 ❌

Results for commit 0891da0.

♻️ This comment has been updated with latest results.

@rmunn

rmunn commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit - Please examine how MediaUri is handled in audio files, then suggest how to handle the MediaUri property in the Picture class.

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Search for MediaUri in the codebase ==="
rg -n "MediaUri" --type cs -C 3

echo ""
echo "=== Search for audio-related model files ==="
fd -e cs -i "audio" --exec echo {}

echo ""
echo "=== Search for Picture model files ==="
fd -e cs -i "picture" --exec echo {}

echo ""
echo "=== Search for ExampleSentence or audio model definitions ==="
rg -n "class.*Audio\|AudioFile\|Sound" --type cs -C 5

Length of output: 45536


🏁 Script executed:

#!/bin/bash
echo "=== Find MiniLcm model files ==="
fd -e cs . --search-path "MiniLcm" | head -60

echo ""
echo "=== Search for all MediaUri occurrences ==="
rg -rn "MediaUri" -C 5

Length of output: 57201


🏁 Script executed:

#!/bin/bash
echo "=== Search for PictureFileRA (LCM picture file property) ==="
rg -n "PictureFileRA\|InternalPath\|PictureFolder\|Pictures folder\|TypeToLinkedFolder" --type cs -C 3

echo ""
echo "=== Look at UpdatePictureProxy in full ==="
cat backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdatePictureProxy.cs

echo ""
echo "=== Look at CreatePictureChange in full ==="
cat backend/FwLite/LcmCrdt/Changes/CreatePictureChange.cs

echo ""
echo "=== Look at PictureSync in full ==="
cat backend/FwLite/MiniLcm/SyncHelpers/PictureSync.cs

echo ""
echo "=== Search for TypeToLinkedFolder and AudioVisualFolder constant ==="
rg -n "TypeToLinkedFolder\|AudioVisualFolder\|PictureFolder\|Pictures\b" --type cs -C 2

Length of output: 5543


🏁 Script executed:

#!/bin/bash
echo "=== Find AudioVisualFolder and TypeToLinkedFolder in FwDataMiniLcmApi.cs ==="
grep -n "AudioVisual\|TypeToLinked\|PictureFile\|InternalPath\|Pictures\|LinkedFiles\|CmFile\|cmFile" \
  backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs | head -60

echo ""
echo "=== View lines around the folder/type mapping function ==="
grep -n "TypeToLinkedFolder\|AudioVisualFolder\|const string\|private.*Folder" \
  backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

echo ""
echo "=== Grep for picture file path references in FW LCM ==="
rg -n "PictureFile\|CmFile\|InternalPath\|\.PictureFilePath\|picture\..*[Ff]ile\|\.ImageFile" \
  backend/FwLite/FwDataMiniLcmBridge/ -C 2

echo ""
echo "=== Check what LCM picture interface looks like ==="
rg -rn "PictureFileRA\|ICmPicture\|CmPicture" --type cs -C 3 | head -80

Length of output: 3346


🏁 Script executed:

#!/bin/bash
echo "=== LcmToPicture method context ==="
grep -n "LcmToPicture\|private.*Picture\|FromLcmPicture\|ToMiniLcmPicture\|ICmPicture" \
  backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

echo ""
echo "=== View lines 795-830 of FwDataMiniLcmApi.cs (picture mapping) ==="
sed -n '795,835p' backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

echo ""
echo "=== View lines 1940-1960 (TypeToLinkedFolder) ==="
sed -n '1940,1960p' backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

echo ""
echo "=== Search for ICmPicture file-path related properties in LCM NuGet/dlls ==="
rg -rn "\.PictureFile\|picture\.\|ICmPicture\b" --type cs backend/FwLite/ -C 2 | head -60

echo ""
echo "=== Check the SIL LCModel ICmPicture or CmPicture in dlls/packages ==="
find . -name "*.cs" | xargs grep -l "ICmPicture" 2>/dev/null | head -10

echo ""
echo "=== View LfClassicMiniLcmApi picture mapping (line ~350) ==="
grep -n "picture\.\|Picture\|MediaUri\|CmFile\|InternalPath" backend/LfClassicData/LfClassicMiniLcmApi.cs | head -30

Length of output: 4911


🏁 Script executed:

#!/bin/bash
echo "=== Search for ICmFile usage anywhere ==="
rg -rn "ICmFile\|PictureFileRA\|InternalPath\|\.PicturePath\b" --type cs -C 3 | head -60

echo ""
echo "=== Look for how FW code sets picture file path - CreatePicture context ==="
sed -n '1810,1850p' backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

echo ""
echo "=== Search for Pictures folder const or PictureFolder ==="
rg -rn "const.*[Pp]icture\|PictureFolder\b" --type cs -C 2

echo ""
echo "=== Look for nuget packages that contain LCModel ==="
find . -name "*.csproj" | xargs grep -l "LCModel\|liblcm\|SIL.LCModel" 2>/dev/null | head -5

echo ""
echo "=== Check any existing usages that access picture.PictureFile ==="
rg -rn "picture\." --type cs backend/FwLite/FwDataMiniLcmBridge/ -C 1 | head -40

Length of output: 5205



@rmunn Here's a comprehensive analysis of the audio MediaUri pattern and how to apply it to pictures.


How MediaUri works for audio files

The data model difference

In FieldWorks LCM:

  • Audio: Stored as a relative filename (e.g. "recording.wav") inside a MultiString on a writing system tagged with IsAudio. Path is under LinkedFilesRootDir/AudioVisual/.
  • Picture: Stored via ICmPicture.PictureFileRA, which is an ICmFile object whose InternalPath is relative to LinkedFilesRootDir. Files land in LinkedFilesRootDir/Pictures/ (already mapped in TypeToLinkedFolder for image/*).

The audio translation pipeline

Reading (FW → MiniLcm)ToMediaUri() in FwDataMiniLcmApi.cs:

// relative filename from MultiString → full path → MediaUri
var fullFilePath = Path.Join(Cache.LangProject.LinkedFilesRootDir, AudioVisualFolder, tsString);
return mediaAdapter.MediaUriFromPath(fullFilePath, Cache).ToString();

Writing (MiniLcm → FW)FromMediaUri() in FwDataMiniLcmApi.cs:

// MediaUri string → full path → relative path back to AudioVisual subfolder
var mediaUri = new MediaUri(mediaUriString);
var path = mediaAdapter.PathFromMediaUri(mediaUri, Cache);
return Path.GetRelativePath(Path.Join(Cache.LangProject.LinkedFilesRootDir, AudioVisualFolder), path);

Guard on writeUpdateDictionaryProxy.ShouldSet():

if (!wsId.IsAudio) return true;
return value != MediaUri.NotFoundString; // skip writing back NotFound URIs

Suggested implementation for Picture.MediaUri

The pattern is the same; the only difference is the FW storage mechanism (PictureFileRA/ICmFile instead of a MultiString value).

1. Reading — FromLcmPicture in FwDataMiniLcmApi.cs

private Picture FromLcmPicture(Guid senseGuid, ICmPicture picture)
{
    var mediaUri = MediaUri.NotFound;
    var internalPath = picture.PictureFileRA?.InternalPath;
    if (!string.IsNullOrEmpty(internalPath))
    {
        var fullPath = Path.Join(Cache.LangProject.LinkedFilesRootDir, internalPath);
        mediaUri = mediaAdapter.MediaUriFromPath(fullPath, Cache);
    }
    return new Picture
    {
        Id = picture.Guid,
        Caption = FromLcmMultiString(picture.Caption),
        // SenseId = senseGuid,
        MediaUri = mediaUri,
    };
}

2. Writing — CreatePicture in FwDataMiniLcmApi.cs

internal void CreatePicture(ILexSense lexSense, Picture picture, BetweenPosition? between = null)
{
    if (picture.Id == default) picture.Id = Guid.NewGuid();
    var lcmPicture = LcmPictureFactory.Create(picture.Id);
    UpdateLcmMultiString(lcmPicture.Caption, picture.Caption);
    SetLcmPictureFile(lcmPicture, picture.MediaUri);
    InsertPicture(lexSense, lcmPicture, between);
}

private void SetLcmPictureFile(ICmPicture lcmPicture, MediaUri mediaUri)
{
    if (mediaUri == MediaUri.NotFound) return;
    var fullPath = mediaAdapter.PathFromMediaUri(mediaUri, Cache);
    if (fullPath is null) return;
    // Reuse an existing ICmFile with the same path, or create a new one
    var relativePath = Path.GetRelativePath(Cache.LangProject.LinkedFilesRootDir, fullPath);
    var fileRepo = Cache.ServiceLocator.GetInstance<IRepository<ICmFile>>();
    var cmFile = fileRepo.AllInstances().FirstOrDefault(f => f.InternalPath == relativePath);
    if (cmFile is null)
    {
        cmFile = Cache.ServiceLocator.GetInstance<ICmFileFactory>().Create();
        Cache.LangProject.MediaFilesOA.FilesOC.Add(cmFile);
        cmFile.InternalPath = relativePath;
    }
    lcmPicture.PictureFileRA = cmFile;
}

Note: SetLcmPictureFile can be made internal so UpdatePictureProxy can call it directly.

3. Update proxy — UpdatePictureProxy.cs

public override MediaUri MediaUri
{
    get
    {
        var internalPath = picture.PictureFileRA?.InternalPath;
        if (string.IsNullOrEmpty(internalPath)) return MediaUri.NotFound;
        var fullPath = Path.Join(lexboxLcmApi.Cache.LangProject.LinkedFilesRootDir, internalPath);
        return lexboxLcmApi.mediaAdapter.MediaUriFromPath(fullPath, lexboxLcmApi.Cache);
    }
    set => lexboxLcmApi.SetLcmPictureFile(picture, value);
}

4. Sync diff — PictureSync.DiffToUpdate

public static UpdateObjectInput<Picture>? DiffToUpdate(Picture beforePicture, Picture afterPicture)
{
    JsonPatchDocument<Picture> patchDocument = new();
    patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff<Picture>(
        nameof(Picture.Caption),
        beforePicture.Caption,
        afterPicture.Caption));

    if (beforePicture.MediaUri != afterPicture.MediaUri)
        patchDocument.Replace(p => p.MediaUri, afterPicture.MediaUri);

    if (patchDocument.Operations.Count == 0) return null;
    return new UpdateObjectInput<Picture>(patchDocument);
}

5. CRDT change — CreatePictureChange.cs

public MediaUri MediaUri { get; set; }

public CreatePictureChange(Picture picture, Guid senseId)
    : base(picture.Id == Guid.Empty ? Guid.NewGuid() : picture.Id)
{
    picture.Id = EntityId;
    Order = picture.Order;
    Caption = picture.Caption;
    MediaUri = picture.MediaUri;
}

public override async ValueTask<Picture> NewEntity(Commit commit, IChangeContext context)
{
    return new Picture
    {
        Id = EntityId,
        Order = Order,
        Caption = Caption ?? new(),
        MediaUri = MediaUri,
        DeletedAt = null,
    };
}

6. LF Classic — LfClassicMiniLcmApi.ToPicture

LF Classic's Entities.Picture likely has an ImageFilePath (or similar) field storing a filename. The pattern would be the same: build the full path using the project's linked files root, then call through mediaAdapter:

private static MiniLcm.Models.Picture ToPicture(Guid senseId, Entities.Picture picture)
{
    return new MiniLcm.Models.Picture
    {
        Id = picture.Guid,
        Caption = ToRichMultiString(picture.Caption),
        MediaUri = !string.IsNullOrEmpty(picture.ImageFilePath)
            ? new MediaUri(picture.ImageFilePath)   // or via adapter if one exists for LF Classic
            : MediaUri.NotFound,
    };
}

LF Classic may need its own adapter (or a simpler direct-path approach), since it doesn't go through IMediaAdapter/LCM like the FW bridge does. You'll want to confirm what field on Entities.Picture holds the file reference.


Summary of the pattern

Step Audio Picture
FW storage MultiString[audioWs] = relative filename ICmPicture.PictureFileRA.InternalPath = relative path
Subfolder LinkedFilesRootDir/AudioVisual/ LinkedFilesRootDir/Pictures/
Read (FW→MiniLcm) mediaAdapter.MediaUriFromPath(fullPath) same
Write (MiniLcm→FW) mediaAdapter.PathFromMediaUri() → update string same, but find/create ICmFile, set PictureFileRA
NotFound guard UpdateDictionaryProxy.ShouldSet skips write SetLcmPictureFile returns early if MediaUri.NotFound

@rmunn

rmunn commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Current code is partly working, but I haven't yet told Entity Framework how to map a Picture object into the DB. Which results in this exception when I run a sync:

System.InvalidOperationException: The property 'Picture.MediaUri' could not be mapped because it is of type 'MediaUri', which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidatePropertyMapping(IConventionTypeBase structuralType, IConventionModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidatePropertyMapping(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Sqlite.Infrastructure.Internal.SqliteModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelRuntimeInitializer.Initialize(IModel model, Boolean designTime, IDiagnosticsLogger`1 validationLogger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, ModelCreationDependencies modelCreationDependencies, Boolean designTime)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, ModelCreationDependencies modelCreationDependencies, Boolean designTime)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel(Boolean designTime)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
   at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__8_4(IServiceProvider p)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkRelationalServicesBuilder.<>c.<TryAddCoreServices>b__3_3(IServiceProvider p)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.EntityFrameworkCore.Infrastructure.Internal.InfrastructureExtensions.GetService(IInfrastructure`1 accessor, Type serviceType)
   at Microsoft.EntityFrameworkCore.Infrastructure.Internal.InfrastructureExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.get_Dependencies()
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.Microsoft.EntityFrameworkCore.Storage.IDatabaseFacadeDependenciesAccessor.get_Dependencies()
   at Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.GetFacadeDependencies(DatabaseFacade databaseFacade)
   at Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.CloseConnectionAsync(DatabaseFacade databaseFacade)
   at LcmCrdt.CrdtProjectsService.CreateProject(CreateProjectRequest request) in /src/backend/FwLite/LcmCrdt/CrdtProjectsService.cs:line 181
   at LcmCrdt.CrdtProjectsService.CreateProject(CreateProjectRequest request) in /src/backend/FwLite/LcmCrdt/CrdtProjectsService.cs:line 194
   at LcmCrdt.CrdtProjectsService.CreateProject(CreateProjectRequest request) in /src/backend/FwLite/LcmCrdt/CrdtProjectsService.cs:line 194
   at FwHeadless.Services.SyncWorker.SetupCrdtProject(String crdtFile, IProjectLookupService projectLookupService, Guid projectId, CrdtProjectsService projectsService, String projectFolder, Guid fwProjectId, String lexboxUrl) in /src/backend/FwHeadless/Services/SyncHostedService.cs:line 371
   at FwHeadless.Services.SyncWorker.ExecuteSync(CancellationToken stoppingToken, Boolean onlyHarmony) in /src/backend/FwHeadless/Services/SyncHostedService.cs:line 188
   at FwHeadless.Services.SyncWorker.ExecuteSync(CancellationToken stoppingToken, Boolean onlyHarmony) in /src/backend/FwHeadless/Services/SyncHostedService.cs:line 276
   at FwHeadless.Services.SyncHostedService.ExecuteAsync(CancellationToken stoppingToken) in /src/backend/FwHeadless/Services/SyncHostedService.cs:line 33

@rmunn

rmunn commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit - Look at the exception in #2300 (comment) and look at the Entity Framework Core documentation. Summarize how to map the MediaUri property, and provide links to the documentation explaining how to map properties in EF Core.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Find MediaUri type definition ==="
rg -n "class MediaUri\|struct MediaUri\|record MediaUri" --type cs -C 5

echo ""
echo "=== Find existing EF Core value converters in LcmCrdt ==="
rg -n "ValueConverter\|HasConversion\|OnModelCreating\|EntityTypeBuilder\|modelBuilder" --type cs backend/FwLite/LcmCrdt/ -C 3 | head -100

echo ""
echo "=== Find DbContext in LcmCrdt ==="
fd -e cs . backend/FwLite/LcmCrdt/ | xargs grep -l "DbContext\|OnModelCreating" 2>/dev/null

echo ""
echo "=== Check if MediaUri implements any interface or has implicit conversion ==="
rg -n "implicit\|explicit\|operator\|ToString\|Parse\|TryParse" --type cs -l | xargs grep -l "MediaUri" 2>/dev/null

Length of output: 3199


🌐 Web query:

Entity Framework Core value converters custom type mapping documentation HasConversion

Length of output: 20413


🏁 Script executed:

#!/bin/bash
echo "=== MediaUri definition - search all file types ==="
rg -rn "MediaUri" --type cs | grep -i "class\|record\|struct" | head -20

echo ""
echo "=== Find the file that defines MediaUri ==="
rg -rn "MediaUri" --type cs -l

echo ""
echo "=== Read LcmCrdtDbContext.cs ==="
cat backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs

echo ""
echo "=== Read MiniLcm MediaUri source file ==="
fd -e cs "MediaUri" | head -5

Length of output: 6757


🏁 Script executed:

#!/bin/bash
echo "=== Read MediaUri.cs ==="
cat backend/FwLite/MiniLcm/Media/MediaUri.cs

Length of output: 1864



@rmunn — Great news: LcmCrdtDbContext already has the exact pattern you need. The WritingSystemIdConverter does WritingSystemId ↔ string in the same way MediaUri needs to be handled, since MediaUri serializes to/from a plain URI string via ToString() / new MediaUri(str).


The fix

Add a converter class and register it in ConfigureConventions inside LcmCrdtDbContext:

// In LcmCrdtDbContext.ConfigureConventions:
protected override void ConfigureConventions(ModelConfigurationBuilder builder)
{
    builder.Properties<MultiString>()
        .HaveColumnType("jsonb")
        .HaveConversion<MultiStringDbConverter>();
    builder.Properties<RichString?>()
        .HaveColumnType("jsonb")
        .HaveConversion<RichStringDbConverter>();
    builder.Properties<RichMultiString>()
        .HaveColumnType("jsonb")
        .HaveConversion<RichMultiStringDbConverter>();
    builder.Properties<WritingSystemId>()
        .HaveConversion<WritingSystemIdConverter>();

    // Add this:
    builder.Properties<MediaUri>()
        .HaveConversion<MediaUriConverter>();
}

// Add this converter class alongside the others:
private class MediaUriConverter() : ValueConverter<MediaUri, string>(
    uri => uri.ToString(),
    str => new MediaUri(str));

HaveConversion in ConfigureConventions is the pre-convention (global) approach — it applies the converter to every MediaUri property across all entity types automatically, so you don't need to touch OnModelCreating for each entity.


Relevant EF Core documentation

Topic Link
Value Conversions (main doc — HasConversion, ValueConverter<TModel,TProvider>, pre-convention config) https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions
Pre-convention model configuration (ConfigureConventions / HaveConversion) https://learn.microsoft.com/en-us/ef/core/modeling/bulk-configuration#pre-convention-configuration
PropertyBuilder.HasConversion API reference https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.builders.propertybuilder.hasconversion
ValueConverter<TModel,TProvider> API reference https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.storage.valueconversion.valueconverter-2

After adding the converter, you'll also need to create a new EF migration (dotnet ef migrations add AddMediaUriConverter or similar) so the schema change is tracked.

@rmunn

rmunn commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit - When I run task fw-lite-web, I am unable to login to the local Lexbox server inside the FW Lite Web application. Clicking on the "log in to see projects" button in the "Lexbox Local Server" category at http://localhost:5137/ tries to go to http://localhost:5137/api/auth/login/localhost:3050, which produces an error page. The error message is Microsoft.Identity.Client.MsalServiceException: Issuer validation failed for authority: https://localhost:3050/ . Issuer from OIDC endpoint does not match any expected pattern: http://localhost:3050/.

Please analyze the port-forwarding setup for the task fw-lite-web process, and determine why it's using HTTP on port 3050 to forward to the local lexbox server, instead of HTTPS on port 3050. Suggest a solution.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Find Taskfile for fw-lite-web ==="
fd -i "Taskfile" | head -20

echo ""
echo "=== Search for fw-lite-web in Taskfiles ==="
rg -n "fw-lite-web\|fw_lite_web" --type yaml -C 5

echo ""
echo "=== Search for port 3050 references ==="
rg -rn "3050" -C 3 | head -60

echo ""
echo "=== Search for port forwarding or devtunnel setup ==="
rg -rn "devtunnel\|port.forward\|portforward\|forward.*3050\|3050.*forward" -C 5 | head -60

Length of output: 28033


🏁 Script executed:

#!/bin/bash
echo "=== Search for OIDC/auth configuration with localhost ==="
rg -rn "localhost:3050\|localhost.*3050\|3050.*localhost" -C 3 | head -80

echo ""
echo "=== Search for OidcAuthority or Authority config ==="
rg -rn "Authority\|OidcAuthority\|OpenIdConnect\|oidc" --type cs -C 3 | head -80

echo ""
echo "=== Search for auth login endpoint ==="
rg -rn "api/auth/login\|AuthController\|login.*controller" --type cs -C 5 | head -60

Length of output: 637


🏁 Script executed:

#!/bin/bash
echo "=== Root Taskfile.yml - search for fw-lite-web or related ==="
grep -n "fw-lite-web\|fw_lite_web\|fw-lite\|fwlite\|5137\|3050\|https-proxy\|devtunnel\|forward" Taskfile.yml | head -60

echo ""
echo "=== backend/FwLite/Taskfile.yml - full content ==="
cat backend/FwLite/Taskfile.yml

echo ""
echo "=== frontend/https-proxy/vite.config.ts - full content ==="
cat frontend/https-proxy/vite.config.ts

echo ""
echo "=== backend/Taskfile.yml - search for fw-lite or 3050 or 5137 ==="
grep -n "fw-lite\|3050\|5137\|https-proxy\|devtunnel\|forward" backend/Taskfile.yml | head -40

Length of output: 10948


🏁 Script executed:

#!/bin/bash
echo "=== Search for 5137 in all files ==="
rg -rn "5137" -C 2 | head -60

echo ""
echo "=== Search for lexbox server URL config in appsettings or launchSettings ==="
fd -e json "appsettings\|launchSettings" backend/FwLite/ | head -20
fd -e json "appsettings\|launchSettings" backend/FwLite/ | xargs grep -l "localhost\|lexbox\|3050\|Authority\|oidc\|auth" 2>/dev/null | head -10

echo ""
echo "=== Search for auth login in FwLite web project ==="
rg -rn "login\|auth\|oidc\|Authority" backend/FwLite/FwLiteWeb/ -C 3 | head -80

echo ""
echo "=== Find FwLiteWeb project structure ==="
fd . backend/FwLite/FwLiteWeb/ | head -40

Length of output: 14226


🏁 Script executed:

#!/bin/bash
echo "=== AuthRoutes.cs ==="
cat backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs

echo ""
echo "=== Program.cs ==="
cat backend/FwLite/FwLiteWeb/Program.cs

echo ""
echo "=== appsettings.json files in FwLiteWeb ==="
fd -e json "appsettings" backend/FwLite/FwLiteWeb/ | xargs -I{} sh -c 'echo "--- {} ---"; cat {}'

echo ""
echo "=== frontend Taskfile.yml - fw-lite-infr task ==="
grep -n "fw-lite-infr\|https-proxy\|3050\|lexbox" frontend/Taskfile.yml -A 10 | head -60

echo ""
echo "=== FwLiteWebConfig.cs ==="
cat backend/FwLite/FwLiteWeb/FwLiteWebConfig.cs

echo ""
echo "=== ServerRedirectUrlProvider.cs ==="
cat backend/FwLite/FwLiteWeb/Services/ServerRedirectUrlProvider.cs

Length of output: 9618


🏁 Script executed:

#!/bin/bash
echo "=== Find AuthService and AuthConfig ==="
fd -e cs "AuthService\|AuthConfig\|OAuthService" backend/FwLite/ | head -10

echo ""
echo "=== AuthConfig.cs ==="
fd -e cs "AuthConfig" backend/FwLite/ | xargs cat

echo ""
echo "=== AuthService.cs ==="
fd -e cs "AuthService" backend/FwLite/ | xargs cat

echo ""
echo "=== OAuthService.cs ==="
fd -e cs "OAuthService" backend/FwLite/ | xargs cat

echo ""
echo "=== UrlContext.cs ==="
cat backend/FwLite/FwLiteWeb/UrlContext.cs

echo ""
echo "=== Search for ValidateAuthority or ValidIssuers or MSAL config ==="
rg -rn "ValidateAuthority\|ValidIssuers\|PublicClientApplicationBuilder\|WithAuthority\|OidcWellKnown\|openid-configuration" --type cs -C 5 | head -80

echo ""
echo "=== Root Taskfile lines 113-120 (fw-lite-web task definition) ==="
sed -n '100,130p' Taskfile.yml

Length of output: 22755


🏁 Script executed:

#!/bin/bash
echo "=== Find OAuthClientFactory and OAuthClient ==="
fd -e cs "OAuthClient" backend/FwLite/ | head -10

echo ""
echo "=== OAuthClientFactory.cs ==="
fd -e cs "OAuthClientFactory" backend/FwLite/ | xargs cat

echo ""
echo "=== OAuthClient.cs ==="
fd -e cs -x echo {} backend/FwLite/ | xargs grep -l "OAuthClient" | grep -v "Factory\|Service\|Test" | head -5 | xargs cat

echo ""
echo "=== Search for PublicClientApplication or WithAuthority in FwLiteShared ==="
rg -rn "PublicClientApplication\|WithAuthority\|BuildApplication\|IPublicClientApplication" --type cs backend/FwLite/ -C 5 | head -80

echo ""
echo "=== Search for ValidateIssuer or authority in appsettings for the local lexbox ==="
fd -e json "appsettings" backend/ | xargs grep -l "localhost\|3050\|3000\|authority\|Authority" 2>/dev/null | head -5

echo ""
echo "=== Check if local lexbox server config has ASPNETCORE_FORWARDEDHEADERS ==="
fd -i "docker-compose" . | xargs grep -l "lexbox\|3000\|3050" 2>/dev/null | head -5
fd "docker-compose" . | head -10

Length of output: 60547



@rmunn Here's the complete analysis.


Root Cause

The data flow is:

Browser → https://localhost:3050 (vite HTTPS proxy)
                  ↓ (forwards to)
          http://localhost:3000 (local lexbox server, plain HTTP)

The chain of events that causes the mismatch:

  1. OAuthClient.cs builds the MSAL application with .WithOidcAuthority("https://localhost:3050/") — because LexboxServer.Authority is the HTTPS proxy URL.
  2. MSAL fetches the OIDC discovery document from https://localhost:3050/.well-known/openid-configuration.
  3. The vite proxy (with changeOrigin: false) forwards this to http://localhost:3000/.well-known/openid-configuration, passing the browser's Host: localhost:3050 header through unchanged.
  4. The local lexbox server (ASP.NET Core on HTTP) builds its issuer URL as: own scheme (http://) + Host header (localhost:3050) → "issuer": "http://localhost:3050/".
  5. The x-forwarded-proto: https header IS sent by the vite proxy, but the lexbox server's ForwardedHeaders middleware is not configured to honor it in development.
  6. protocolRewrite: 'https' in the vite config only rewrites Location headers in redirects — it does not rewrite JSON response bodies. The OIDC discovery JSON is untouched.
  7. MSAL validates authority https://localhost:3050/ against issuer http://localhost:3050/mismatch → exception.

Fix Options

Option A — Fix in the vite proxy (self-contained, no server changes needed)

Intercept the OIDC discovery response and rewrite http://https:// in the JSON body. Add this to the configure callback in frontend/https-proxy/vite.config.ts:

import { gunzipSync } from 'zlib';

// Inside the lexboxServer ProxyOptions configure callback, after existing code:
proxy.on('proxyRes', (proxyRes, req, res) => {
    if (!req.url?.includes('openid-configuration')) return;

    const chunks: Buffer[] = [];
    const originalWrite = res.write.bind(res);
    const originalEnd = res.end.bind(res);

    // Intercept the streamed response
    proxyRes.on('data', (chunk: Buffer) => chunks.push(chunk));
    proxyRes.on('end', () => {
        const body = Buffer.concat(chunks).toString('utf-8');
        const rewritten = body.replaceAll(
            /"issuer"\s*:\s*"http:\/\//g,
            '"issuer":"https://'
        );
        const newBody = Buffer.from(rewritten, 'utf-8');
        res.setHeader('Content-Length', newBody.length);
        originalWrite(newBody);
        originalEnd();
    });
});

Note: Vite uses http-proxy under the hood; you may need to set selfHandleResponse: true on the proxy options to take full control of the response stream, or use the simpler approach of hooking into proxyRes before headers are flushed.

A simpler alternative using vite's built-in transformer hook (if supported by your vite version):

// In vite.config.ts server.proxy options:
'/': {
    ...lexboxServer,
    // Add a response rewriter for OIDC discovery only
    rewrite: (path) => path,
}

Option B — Fix in the local lexbox server (correct architectural fix)

Enable ForwardedHeaders middleware in the lexbox API so it honors X-Forwarded-Proto: https. The easiest way in development is to set an environment variable in the dev docker-compose or k8s manifest:

environment:
  ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true"

This makes ASP.NET Core automatically enable forwarded headers, so the lexbox server generates https://localhost:3050/ as the OIDC issuer when it receives X-Forwarded-Proto: https.

Or in backend/LexBoxApi/appsettings.Development.json:

{
  "ForwardedHeaders": {
    "ForwardedHeaders": "XForwardedFor, XForwardedProto"
  }
}

Option C — Fix in OAuthClient.cs (pragmatic dev workaround)

For localhost authorities specifically, skip MSAL's authority validation:

var builder = PublicClientApplicationBuilder.Create(options.Value.ClientId)
    .WithExperimentalFeatures()
    .WithLogging(loggerAdapter, hostEnvironment?.IsDevelopment() ?? false)
    .WithHttpClientFactory(new HttpClientFactoryAdapter(httpMessageHandlerFactory))
    .WithParentActivityOrWindow(() => options.Value.ParentActivityOrWindow)
    .WithOidcAuthority(lexboxServer.Authority.ToString());

// For localhost dev servers, the HTTPS proxy wraps HTTP, so the issuer will be http:// 
// even though the authority is https://. Skip validation in this case.
if (lexboxServer.Authority.IsLoopback)
    builder.WithValidateAuthority(false);

Recommendation

Option B is the most architecturally correct fix — the lexbox server should honor X-Forwarded-Proto, especially since the vite proxy already sends it. Setting ASPNETCORE_FORWARDEDHEADERS_ENABLED=true in the dev environment is a one-line change with no code modifications.

Option A is useful if you can't or don't want to touch the lexbox server dev config, since the vite proxy was designed specifically to handle this HTTPS wrapping concern.

Option C is a quick local workaround but slightly weakens MSAL's validation for all localhost servers, not just this specific scenario.

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

C# FwHeadless Unit Tests

48 tests   48 ✅  15s ⏱️
 5 suites   0 💤
 1 files     0 ❌

Results for commit 0891da0.

♻️ This comment has been updated with latest results.

@rmunn

rmunn commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit - Syncing pictures using commit 3a729a7 produced the following exception:

System.NotSupportedException: type JsonPatchChange`1 does not support NewEntity, because it inherits from EditChange, this means it must be called with a from an existing entity, not a newly generated one

at SIL.Harmony.Changes.EditChange`1.NewEntity(Commit commit, IChangeContext context) in backend/harmony/src/SIL.Harmony/Changes/EditChange.cs:line 8
   at SIL.Harmony.Changes.Change`1.SIL.Harmony.Changes.IChange.NewEntity(Commit commit, IChangeContext context) in backend/harmony/src/SIL.Harmony/Changes/Change.cs:line 51
   at SIL.Harmony.SnapshotWorker.ApplyCommitChanges(SortedSet`1 commits) in backend/harmony/src/SIL.Harmony/SnapshotWorker.cs:line 78
   at SIL.Harmony.SnapshotWorker.UpdateSnapshots(SortedSet`1 commits) in backend/harmony/src/SIL.Harmony/SnapshotWorker.cs:line 54
   at SIL.Harmony.DataModel.UpdateSnapshots(CrdtRepository repo, SortedSet`1 commitsToApply) in backend/harmony/src/SIL.Harmony/DataModel.cs:line 217
   at SIL.Harmony.DataModel.SIL.Harmony.ISyncable.AddRangeFromSync(IEnumerable`1 commits) in backend/harmony/src/SIL.Harmony/DataModel.cs:line 159
   at SIL.Harmony.DataModel.SIL.Harmony.ISyncable.AddRangeFromSync(IEnumerable`1 commits) in backend/harmony/src/SIL.Harmony/DataModel.cs:line 161
   at SIL.Harmony.DataModel.SIL.Harmony.ISyncable.AddRangeFromSync(IEnumerable`1 commits) in backend/harmony/src/SIL.Harmony/DataModel.cs:line 161
   at SIL.Harmony.SyncHelper.SyncWith(ISyncable localModel, ISyncable remoteModel, JsonSerializerOptions serializerOptions) in backend/harmony/src/SIL.Harmony/SyncHelper.cs:line 42
   at SIL.Harmony.DataModel.SyncWith(ISyncable remoteModel) in backend/harmony/src/SIL.Harmony/DataModel.cs:line 377
   at FwLiteShared.Sync.SyncService.ExecuteSync(Boolean skipNotifications) in backend/FwLite/FwLiteShared/Sync/SyncService.cs:line 89
   at FwLiteShared.Projects.CombinedProjectsService.<>c.<<DownloadProject>b__16_1>d.MoveNext() in backend/FwLite/FwLiteShared/Projects/CombinedProjectsService.cs:line 202
--- End of stack trace from previous location ---
   at LcmCrdt.CrdtProjectsService.CreateProject(CreateProjectRequest request) in backend/FwLite/LcmCrdt/CrdtProjectsService.cs:line 175
   at LcmCrdt.CrdtProjectsService.CreateProject(CreateProjectRequest request) in backend/FwLite/LcmCrdt/CrdtProjectsService.cs:line 191
   at LcmCrdt.CrdtProjectsService.CreateProject(CreateProjectRequest request) in backend/FwLite/LcmCrdt/CrdtProjectsService.cs:line 194
   at LcmCrdt.CrdtProjectsService.CreateProject(CreateProjectRequest request) in backend/FwLite/LcmCrdt/CrdtProjectsService.cs:line 194
   at FwLiteShared.Projects.CombinedProjectsService.<>c__DisplayClass16_0.<<DownloadProject>b__0>d.MoveNext() in backend/FwLite/FwLiteShared/Projects/CombinedProjectsService.cs:line 196
--- End of stack trace from previous location ---
   at FwLiteShared.Projects.CombinedProjectsService.DownloadProject(ProjectModel project) in backend/FwLite/FwLiteShared/Projects/CombinedProjectsService.cs:line 196

If possible, analyse why NewEntity is being called on a JsonPatchChange<Picture> instead of a CreatePictureChange being used. Summarise the reason and suggest a solution.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== PictureSync.cs ==="
fd -e cs "PictureSync" | xargs cat

echo ""
echo "=== CreatePictureChange.cs ==="
fd -e cs "CreatePictureChange" | xargs cat

echo ""
echo "=== Search for where pictures are synced from FW to CRDT ==="
rg -n "CreatePicture\|AddPicture\|picture.*sync\|sync.*picture" --type cs -i backend/FwLite/ -C 3 | head -80

echo ""
echo "=== How other entity syncs look - e.g. EntrySync ==="
fd -e cs "EntrySync" backend/FwLite/ | xargs cat 2>/dev/null | head -100

echo ""
echo "=== How other entity syncs look - e.g. SenseSync ==="
fd -e cs "SenseSync" backend/FwLite/ | xargs cat 2>/dev/null | head -100

Length of output: 10820


🏁 Script executed:

#!/bin/bash
echo "=== Find CrdtMiniLcmApi picture-related methods ==="
fd -e cs "CrdtMiniLcmApi" backend/FwLite/ | xargs grep -n "Picture\|picture" | head -60

echo ""
echo "=== Find CreatePicture and UpdatePicture implementations in LcmCrdt ==="
rg -n "CreatePicture\|UpdatePicture\|DeletePicture\|MovePicture" --type cs backend/FwLite/LcmCrdt/ -C 5

echo ""
echo "=== Find how JsonPatchChange is created/used ==="
rg -n "JsonPatchChange\b" --type cs backend/FwLite/ -C 3

echo ""
echo "=== CrdtFwdataProjectSyncService - picture related sync code ==="
rg -n "Picture\|picture" --type cs backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs -C 3

echo ""
echo "=== Check UpdatePicture in LcmCrdt and what change type it creates ==="
rg -n "async.*UpdatePicture\|Task.*UpdatePicture" --type cs backend/FwLite/ -C 10

Length of output: 14639



@rmunn — Here's the analysis.

Root cause

CrdtMiniLcmApi has two UpdatePicture overloads:

// Overload 1 — takes an UpdateObjectInput, creates JsonPatchChange<Picture>
public async Task<Picture> UpdatePicture(Guid entryId, Guid senseId, Guid pictureId, UpdateObjectInput<Picture> update)
{
    var patchChange = new JsonPatchChange<Picture>(pictureId, jsonPatch);  // ← EditChange, no NewEntity support
    await AddChange(patchChange);
    ...
}

// Overload 2 — takes before/after Pictures, calls PictureSync.Sync (single-picture version)
public async Task<Picture> UpdatePicture(Guid entryId, Guid senseId, Picture before, Picture after, ...)
{
    await PictureSync.Sync(entryId, senseId, before, after, api ?? this);  // ← ONLY calls UpdatePicture, NEVER CreatePicture
    ...
}

The single-picture PictureSync.Sync overload only ever calls api.UpdatePicture(...). It never calls api.CreatePicture. So if this overload is invoked for a picture that doesn't yet exist in the CRDT, it commits a JsonPatchChange<Picture> for an entity that has no CreatePictureChange — and when Harmony's SnapshotWorker tries to apply it, it calls NewEntity on the JsonPatchChange, which throws.

The sync code on the FW headless server is taking a path that calls the "before/after Picture" UpdatePicture overload (or uses PictureSync.Sync single-picture overload) for pictures that are new to the CRDT — treating them as updates rather than creates.


Where to look

The FW headless sync must have new code (in this PR) in either SenseSync.cs or CrdtFwdataProjectSyncService.cs that processes pictures. The bug will be that for senses containing pictures, instead of going through the collection-based PictureSync.Sync:

// CORRECT — uses DiffOrderable, calls CreatePicture for new pictures
await PictureSync.Sync(entryId, senseId, beforeSense.Pictures, afterSense.Pictures, api);

…it's going through something like:

// WRONG — single-picture overload, always emits JsonPatchChange, never CreatePicture
foreach (var (before, after) in matchedPictures)
    await api.UpdatePicture(entryId, senseId, before, after);
// or
await PictureSync.Sync(entryId, senseId, beforePicture, afterPicture, api);

Suggested fix

Option A — In SenseSync.Sync, add the collection-based picture sync (the same pattern already used for ExampleSentenceSync):

// In SenseSync.Sync, alongside the ExampleSentenceSync call:
changes += await PictureSync.Sync(entryId,
    beforeSense.Id,
    beforeSense.Pictures,
    afterSense.Pictures,
    api);

Option B — If the FW sync has a custom loop in CrdtFwdataProjectSyncService, replace any per-picture UpdatePicture call with a call to the collection overload of PictureSync.Sync, so DiffOrderable can correctly route to CreatePicture (Add) vs UpdatePicture (Replace).

The key invariant to enforce: api.CreatePicture must be called for any picture that doesn't yet exist in the CRDT, and api.UpdatePicture must only be called for pictures that already have a CreatePictureChange in the commit log.


🧠 Learnings used
Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs : In FwLiteProjectSync/CrdtFwdataProjectSyncService.cs, regenerate ProjectSnapshot from CRDT after sync (see issue `#1912`)

Learnt from: rmunn
Repo: sillsdev/languageforge-lexbox PR: 2192
File: backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs:125-127
Timestamp: 2026-03-11T04:58:21.676Z
Learning: In sillsdev/languageforge-lexbox backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs SyncInternal, the two-pass sync pattern is intentional and lossless: pass 1 (e.g., line 126 for MorphTypeData) updates the live CRDT from FW data using the project snapshot as "before"; pass 2 (e.g., line 127) reads the *already-updated* live CRDT and pushes it to FW. This means a null value in the CRDT snapshot cannot destructively wipe an FW value — it will be reconciled to the FW value in pass 1 before pass 2 ever runs. A null reaching FW in pass 2 only happens when the user intentionally deleted it in CRDT, which is correct bidirectional sync behavior.

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/FwLiteProjectSync/*.cs : Explicitly call fwdataApi.Save() after applying changes to FwData in sync operations

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwHeadless/AGENTS.md:0-0
Timestamp: 2025-12-05T10:28:05.034Z
Learning: Compare CRDT and FwData states manually when debugging sync failures; if ProjectSnapshot is incorrect, sync results will be incorrect

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwHeadless/AGENTS.md:0-0
Timestamp: 2025-12-05T10:28:05.034Z
Learning: The most dangerous sync logic is located in `backend/FwLite/FwLiteProjectSync/` with the core algorithm in `CrdtFwdataProjectSyncService.cs`; refer to `backend/FwLite/AGENTS.md` for detailed sync documentation

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/FwLiteProjectSync.Tests/**/*Tests.cs : Add sync round-trip tests in FwLiteProjectSync.Tests/ for new sync features to verify bidirectional consistency

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs : Use two-phase sync for complex forms in FwLiteProjectSync: SyncWithoutComplexFormsAndComponents first, then SyncComplexFormsAndComponents

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwHeadless/AGENTS.md:0-0
Timestamp: 2025-12-05T10:28:05.034Z
Learning: Applies to backend/FwHeadless/Services/CrdtSyncService.cs : Always check `syncResults.IsSynced`, `syncResults.MissingFromLocal`, and `syncResults.MissingFromRemote` after calling `dataModel.SyncWith()` in CrdtSyncService; if sync fails silently, users lose data

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/LcmCrdt/Changes/*.cs : Profile CRDT change classes with realistic data (1000+ entries) before committing if adding complex operations

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/LcmCrdt/Changes/*.cs : In Harmony Change classes, check if referenced entities are deleted (DeletedAt is not null) before processing

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/LcmCrdt/Changes/*.cs : For new simple field changes in CRDT, use JsonPatchChange<T> instead of creating dedicated change classes

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/MiniLcm.Tests/**/*Tests.cs : Add test cases in MiniLcm.Tests/ for base model behavior when adding new model features

rmunn added 3 commits June 11, 2026 13:58
Compiles, but some logic (most notably MediaUri handling) is incomplete
Next up will be writing tests to make sure we can properly two-way sync
a picture from FW Classic, i.e. that the MediaUri handling is correct.
@rmunn

rmunn commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on top of current develop; hopefully this will let me test syncing now.

@rmunn

rmunn commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Huh. Look commit 2ce7ec8 and, specifically, at the DB migration. Why did it add the HomographNumber property? I might have had a local DB that's not up-to-date. I might have to delete that migration and create a new one.

@rmunn

rmunn commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Force-pushed to get rid of commit 2ce7ec8 so the migration won't add a duplicate HomographNumber column.

rmunn added 3 commits June 11, 2026 15:53
MiniLcmApiWriteNormalizationWrapper is new on develop branch since I
started this PR, so I didn't have the Picture CRUD API calls for it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs (1)

12-21: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't half-wire the new Pictures payload.

CreateSenseChange now captures sense.Pictures, but NewEntity() still hardcodes Pictures = new(). That makes the new field unreachable on replay and can silently drop picture data unless some other path recreates it. Either materialize the pictures here or remove the field/assignment from this change type.

Based on the separate picture-change flow in backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs, this payload should be either fully applied or not serialized here at all.

Also applies to: 38-53

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs` around lines 12 - 21,
CreateSenseChange is only partially wiring the new Pictures payload: the
constructor captures sense.Pictures, but NewEntity still initializes Pictures to
an empty collection, so replay can drop picture data. Update
CreateSenseChange/NewEntity to either fully persist and restore Pictures from
the change payload (using the existing Pictures field) or remove the Pictures
assignment from this change type entirely so it is not serialized here.
♻️ Duplicate comments (1)
backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs (1)

1828-1833: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

GetPicture dereferences lcmPicture before the null check.

ValidateOwnership accesses lcmPicture.Owner, but TryGetObject leaves lcmPicture null for unknown IDs. The null check on Line 1832 runs too late, so an unknown id throws a NullReferenceException instead of returning null. This was previously raised and appears unaddressed.

Suggested fix
 public Task<Picture?> GetPicture(Guid entryId, Guid senseId, Guid id)
 {
     PictureRepository.TryGetObject(id, out var lcmPicture);
-    ValidateOwnership(lcmPicture, entryId, senseId);
-    return Task.FromResult(lcmPicture is null ? null : FromLcmPicture(senseId, lcmPicture));
+    if (lcmPicture is null) return Task.FromResult<Picture?>(null);
+    ValidateOwnership(lcmPicture, entryId, senseId);
+    return Task.FromResult<Picture?>(FromLcmPicture(senseId, lcmPicture));
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs` around lines 1828
- 1833, GetPicture is calling ValidateOwnership before checking whether
PictureRepository.TryGetObject returned a picture, which can dereference a null
lcmPicture. Update GetPicture in FwDataMiniLcmApi to return null immediately
when TryGetObject fails, and only call ValidateOwnership and FromLcmPicture
after confirming lcmPicture is non-null; use the GetPicture, TryGetObject, and
ValidateOwnership symbols to place the fix.
🧹 Nitpick comments (3)
backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs (1)

323-327: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Misleading parameter name in MovePicture.

The picture id parameter is named exampleId (copy-paste from MoveExampleSentence), which also surfaces in the dry-run description text. Rename to pictureId for clarity.

Proposed fix
-    public Task MovePicture(Guid entryId, Guid senseId, Guid exampleId, BetweenPosition between)
+    public Task MovePicture(Guid entryId, Guid senseId, Guid pictureId, BetweenPosition between)
     {
-        DryRunRecords.Add(new DryRunRecord(nameof(MovePicture), $"Move picture {exampleId} between {between.Previous} and {between.Next}"));
+        DryRunRecords.Add(new DryRunRecord(nameof(MovePicture), $"Move picture {pictureId} between {between.Previous} and {between.Next}"));
         return Task.CompletedTask;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs` around lines 323 - 327,
The MovePicture method is using a misleading parameter name copied from
MoveExampleSentence, so update the signature and any related dry-run message to
use pictureId instead of exampleId. Make the change in DryRunMiniLcmApi’s
MovePicture implementation and ensure the DryRunRecord description clearly
refers to the picture identifier for consistency and readability.
backend/FwLite/LcmCrdt/OrderPicker.cs (1)

41-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider extracting the shared ordering switch to remove duplication.

The (previous, next) resolution plus the order-selection switch is copied verbatim from the IQueryable overload (Lines 21-37). Since the divergence is only how items/previous/next are materialized, the shared tail could be a private helper taking the resolved list and neighbors, keeping both overloads in sync if the ordering heuristic changes later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/LcmCrdt/OrderPicker.cs` around lines 41 - 63, The ordering
heuristic in OrderPicker.PickOrder is duplicated between the List<T> overload
and the IQueryable overload, so update the shared neighbor-to-order logic to
live in one private helper. Keep the existing PickOrder methods responsible only
for resolving previous/next from items and then delegate the common switch-based
order selection to the helper. This will keep both overloads in sync if the
ordering rules change later and avoid copy-paste drift.
backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs (1)

929-932: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused repo in CreatePicture.

The repo opened at line 929 is never used — the readback goes through GetPicture, which opens its own repo. This is dead code that misleads readers into thinking the surrounding logic shares a DbContext.

♻️ Proposed cleanup
 public async Task<Picture> CreatePicture(Guid entryId,
     Guid senseId,
     Picture picture,
     BetweenPosition? between = null)
 {
-    await using var repo = await repoFactory.CreateRepoAsync();
     var change = new CreateSensePictureChange(picture, senseId, between);
     await AddChange(change);
     return await GetPicture(entryId, senseId, change.PictureId) ?? throw NotFoundException.ForType<Picture>(change.PictureId);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs` around lines 929 - 932, Remove the
unused repo acquisition in CreatePicture: the await using var repo = await
repoFactory.CreateRepoAsync() in CrdtMiniLcmApi.CreatePicture is dead code
because AddChange and GetPicture do not use it, so delete that local and keep
the method flow unchanged with CreateSensePictureChange, AddChange, and
GetPicture.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs`:
- Around line 564-600: NormalizePicturePatch is mutating the caller’s
UpdateObjectInput by removing MediaUri operations from update.Patch.Operations.
Change the removal/reinsertion flow to work on a copied operations list or a
cloned patch inside NormalizePicturePatch, and pass that working copy into
NormalizePatch so the original update remains unchanged. Keep the existing
MediaUri-preserving logic, but ensure all RemoveAt/Insert operations are applied
only to the local copy, not the input object.

---

Outside diff comments:
In `@backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs`:
- Around line 12-21: CreateSenseChange is only partially wiring the new Pictures
payload: the constructor captures sense.Pictures, but NewEntity still
initializes Pictures to an empty collection, so replay can drop picture data.
Update CreateSenseChange/NewEntity to either fully persist and restore Pictures
from the change payload (using the existing Pictures field) or remove the
Pictures assignment from this change type entirely so it is not serialized here.

---

Duplicate comments:
In `@backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs`:
- Around line 1828-1833: GetPicture is calling ValidateOwnership before checking
whether PictureRepository.TryGetObject returned a picture, which can dereference
a null lcmPicture. Update GetPicture in FwDataMiniLcmApi to return null
immediately when TryGetObject fails, and only call ValidateOwnership and
FromLcmPicture after confirming lcmPicture is non-null; use the GetPicture,
TryGetObject, and ValidateOwnership symbols to place the fix.

---

Nitpick comments:
In `@backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs`:
- Around line 323-327: The MovePicture method is using a misleading parameter
name copied from MoveExampleSentence, so update the signature and any related
dry-run message to use pictureId instead of exampleId. Make the change in
DryRunMiniLcmApi’s MovePicture implementation and ensure the DryRunRecord
description clearly refers to the picture identifier for consistency and
readability.

In `@backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs`:
- Around line 929-932: Remove the unused repo acquisition in CreatePicture: the
await using var repo = await repoFactory.CreateRepoAsync() in
CrdtMiniLcmApi.CreatePicture is dead code because AddChange and GetPicture do
not use it, so delete that local and keep the method flow unchanged with
CreateSensePictureChange, AddChange, and GetPicture.

In `@backend/FwLite/LcmCrdt/OrderPicker.cs`:
- Around line 41-63: The ordering heuristic in OrderPicker.PickOrder is
duplicated between the List<T> overload and the IQueryable overload, so update
the shared neighbor-to-order logic to live in one private helper. Keep the
existing PickOrder methods responsible only for resolving previous/next from
items and then delegate the common switch-based order selection to the helper.
This will keep both overloads in sync if the ordering rules change later and
avoid copy-paste drift.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6bb4c0fe-ae82-4313-9773-10df85b0e890

📥 Commits

Reviewing files that changed from the base of the PR and between ff6ac34 and fdbc1e0.

📒 Files selected for processing (53)
  • backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
  • backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdatePictureProxy.cs
  • backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs
  • backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs
  • backend/FwLite/FwLiteProjectSync.Tests/Snapshots/sena-3_snapshot.2026-04-09.verified.txt
  • backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs
  • backend/FwLite/FwLiteProjectSync.Tests/UpdateDiffTests.cs
  • backend/FwLite/FwLiteProjectSync.Tests/sena-3-live_snapshot.verified.txt
  • backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs
  • backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs
  • backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.latest.verified.txt
  • backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.legacy.verified.txt
  • backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs
  • backend/FwLite/LcmCrdt.Tests/Data/MigrationTests_FromScriptedDb.v1.ChangeEntities.verified.txt
  • backend/FwLite/LcmCrdt.Tests/Data/MigrationTests_FromScriptedDb.v1.ProjectSnapshot.verified.txt
  • backend/FwLite/LcmCrdt.Tests/Data/MigrationTests_FromScriptedDb.v1.Snapshots.verified.txt
  • backend/FwLite/LcmCrdt.Tests/Data/MigrationTests_FromScriptedDb.v2.ChangeEntities.verified.txt
  • backend/FwLite/LcmCrdt.Tests/Data/MigrationTests_FromScriptedDb.v2.ProjectSnapshot.verified.txt
  • backend/FwLite/LcmCrdt.Tests/Data/MigrationTests_FromScriptedDb.v2.Snapshots.verified.txt
  • backend/FwLite/LcmCrdt.Tests/Data/SnapshotDeserializationRegressionData.latest.verified.txt
  • backend/FwLite/LcmCrdt.Tests/Data/SnapshotDeserializationRegressionData.legacy.verified.txt
  • backend/FwLite/LcmCrdt.Tests/Data/VerifyRegeneratedSnapshotsAfterMigrationFromScriptedDb.v1.verified.json
  • backend/FwLite/LcmCrdt.Tests/Data/VerifyRegeneratedSnapshotsAfterMigrationFromScriptedDb.v2.verified.json
  • backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt
  • backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt
  • backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs
  • backend/FwLite/LcmCrdt/Changes/CreateSensePictureChange.cs
  • backend/FwLite/LcmCrdt/Changes/RemoveSensePictureChange.cs
  • backend/FwLite/LcmCrdt/Changes/ReorderSensePictureChange.cs
  • backend/FwLite/LcmCrdt/Changes/UpdateSensePictureChange.cs
  • backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
  • backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs
  • backend/FwLite/LcmCrdt/LcmCrdtKernel.cs
  • backend/FwLite/LcmCrdt/Migrations/20260619022528_AddPicturesColumnToSenses.Designer.cs
  • backend/FwLite/LcmCrdt/Migrations/20260619022528_AddPicturesColumnToSenses.cs
  • backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs
  • backend/FwLite/LcmCrdt/OrderPicker.cs
  • backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/AutoFakerDefault.cs
  • backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/EntryFakerHelper.cs
  • backend/FwLite/MiniLcm/IMiniLcmReadApi.cs
  • backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs
  • backend/FwLite/MiniLcm/Models/Picture.cs
  • backend/FwLite/MiniLcm/Models/Sense.cs
  • backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs
  • backend/FwLite/MiniLcm/SyncHelpers/PictureSync.cs
  • backend/FwLite/MiniLcm/SyncHelpers/SenseSync.cs
  • backend/LfClassicData/Entities/Entry.cs
  • backend/LfClassicData/LfClassicMiniLcmApi.cs
  • frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IPicture.ts
  • frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/ISense.ts
  • frontend/viewer/src/lib/sandbox/Sandbox.svelte
  • frontend/viewer/src/lib/utils.ts
  • frontend/viewer/src/project/demo/demo-entry-data.ts

Comment thread backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs Outdated
Comment thread backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs Outdated
Comment thread backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs
Comment thread backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs
Comment thread backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs
Comment thread backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs Outdated
Comment thread backend/FwLite/LcmCrdt/Changes/CreateSensePictureChange.cs Outdated
Comment thread backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs Outdated
Comment thread backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs Outdated
Comment thread backend/FwLite/MiniLcm/Models/Picture.cs Outdated
Comment thread backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs Outdated
Comment thread backend/FwLite/LcmCrdt/LcmCrdtKernel.cs Outdated
Comment thread backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs Outdated
Comment thread backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs
Comment thread backend/FwLite/MiniLcm.Tests/AutoFakerHelpers/AutoFakerDefault.cs Outdated
rmunn added 4 commits June 29, 2026 14:24
If LinkedFiles directory was missing, the LocalMediaAdapter code was
throwing an exception. Instead, we should just say "no files found" and
not call Directory.EnumerateFiles() if the root path doesn't exist.
@rmunn rmunn requested a review from hahn-kev June 29, 2026 10:00
@rmunn

rmunn commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

@hahn-kev - I believe all review comments are addressed now. I've left the conversations open so you can more easily track which ones I've addressed. Please feel free to resolve any that you don't need to discuss further.

@hahn-kev hahn-kev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great work! I'm really happy with the PR now, just get the tests passing and it should be good to go.

@rmunn rmunn merged commit 88f6ccc into develop Jun 30, 2026
32 checks passed
@rmunn rmunn deleted the feat/pictures branch June 30, 2026 05:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FW Lite issues related to the fw lite application, not miniLcm or crdt related 📦 Lexbox issues related to any server side code, fw-headless included

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Model images in FW-Lite Sync images from FieldWorks to FW-Lite

2 participants