Add system (curated) vs. user-uploaded flashcards

System decks/notes are owned by a reserved sentinel user id, reusing
all existing per-user store/index plumbing unchanged. Regular users
see them under a separate "Official Decks" section, can study them
with fully independent per-user FSRS progress (lazily materialized on
first visit), but can never edit or archive them - enforced both in
the client (no Edit/Archive controls) and server-side (ownership guard
on every mutating endpoint). Includes a small idempotent seed command
(`dotnet run -- seed-flashcards`) for testing, since there's no in-app
authoring UI yet.
This commit is contained in:
hz
2026-07-22 15:52:19 -05:00
parent 3a01e9156c
commit 7f76ca0fdc
13 changed files with 444 additions and 31 deletions
@@ -81,6 +81,44 @@ public sealed class FlashcardScheduleStore
}
}
// Sibling of SyncVariantsAsync: materializes this note's variant schedule rows under a
// DIFFERENT user's ownership (the studying user, not the note's own owner). Used only for
// system notes, so a regular user studying shared content gets independent, per-user FSRS
// state. Deliberately duplicated rather than sharing a parameterized private helper with
// SyncVariantsAsync, which is called eagerly on every regular note save and must stay
// untouched/low-risk.
public async Task SyncVariantsForStudyingUserAsync(string studyingUserId, FlashcardNote note)
{
var desiredVariants = note.Type == FlashcardTypes.Cloze
? FlashcardClozeParser.DistinctClozeNumbers(note.Prompt)
: [0];
if (desiredVariants.Count == 0) return;
var existing = await GetByNoteIdAsync(studyingUserId, note.Id!);
var existingVariants = existing.Select(v => v.Variant).ToHashSet();
var now = DateTime.UtcNow;
foreach (var variant in desiredVariants.Where(v => !existingVariants.Contains(v)))
{
var schedule = new FlashcardSchedule
{
UserId = studyingUserId,
NoteId = note.Id!,
Variant = variant,
State = FlashcardScheduleStates.New,
DueUtc = now,
CreatedUtc = now
};
try
{
await _schedules.InsertOneAsync(schedule);
}
catch (MongoWriteException exception) when (exception.WriteError?.Category == ServerErrorCategory.DuplicateKey)
{
// Concurrent save already created this variant.
}
}
}
public async Task<FlashcardSchedule?> GetAsync(string userId, string cardId) =>
await _schedules.Find(v => v.UserId == userId && v.Id == cardId).FirstOrDefaultAsync();