diff --git a/RMuseum/Controllers/GanjoorController.cs b/RMuseum/Controllers/GanjoorController.cs index 6e8dbb16..9b104cc9 100644 --- a/RMuseum/Controllers/GanjoorController.cs +++ b/RMuseum/Controllers/GanjoorController.cs @@ -4546,6 +4546,27 @@ namespace RMuseum.Controllers return BadRequest(res.ExceptionString); } + /// + /// recovery job: regenerates TajikHtmlText for every Tajik poem from its TajikVerses, + /// overwriting whatever is currently stored - use after RegenerateTajikCatAndPoetHtmlText + /// (or anything else) may have corrupted poem pages. Check the Admin area's + /// LongRunningJobs page for progress. + /// + /// + [HttpPut] + [Route("tajik/restore-poem-html")] + [Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + RMuseumSecurableItem.ImportOperationShortName)] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public IActionResult RestoreTajikPoemHtmlTextAsync() + { + RServiceResult res = + _ganjoorService.RestoreTajikPoemHtmlTextAsync(); + if (res.Result) + return Ok(); + return BadRequest(res.ExceptionString); + } + /// /// tajik poets /// diff --git a/RMuseum/Services/IGanjoorService.cs b/RMuseum/Services/IGanjoorService.cs index cb02fe54..8e71d1ff 100644 --- a/RMuseum/Services/IGanjoorService.cs +++ b/RMuseum/Services/IGanjoorService.cs @@ -1591,6 +1591,15 @@ namespace RMuseum.Services /// RServiceResult RegenerateTajikCatAndPoetHtmlTextAsync(); + /// + /// recovery job: regenerates TajikHtmlText for every Tajik poem from its TajikVerses, + /// overwriting whatever is currently stored. Safe to run on every poem regardless of + /// whether it was actually affected by anything - it recomputes each poem's correct + /// HTML from its real verse data every time. + /// + /// + RServiceResult RestoreTajikPoemHtmlTextAsync(); + /// diff --git a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Tajik.cs b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Tajik.cs index a9ac2f38..7b4e449c 100644 --- a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Tajik.cs +++ b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Tajik.cs @@ -255,11 +255,100 @@ namespace RMuseum.Services.Implementation foreach (var tajikCat in tajikCats) { await jobProgressServiceEF.UpdateJob(job.Id, done, $"cat: {tajikCat.Id}"); - var tajikPage = await context.TajikPages.Where(p => p.Id == tajikCat.Id).SingleOrDefaultAsync(); - if (tajikPage != null) + // tajikCat.Id is the CATEGORY's own id (GanjoorCategories.Id), which is a + // completely different id space from GanjoorPages.Id (catPageId is generated + // independently at import time - see GanjoorService-SQLiteImport.cs). Must + // resolve the actual CatPage's GanjoorPages.Id first, exactly like the poet + // loop above already does - using tajikCat.Id directly here previously + // caused TajikPages rows to be looked up (and overwritten) by the wrong id, + // corrupting whichever unrelated page (often a poem) happened to have that id. + var catPage = await context.GanjoorPages.AsNoTracking().Where(p => p.GanjoorPageType == GanjoorPageType.CatPage && p.CatId == tajikCat.Id).SingleOrDefaultAsync(); + if (catPage != null) { - tajikPage.TajikHtmlText = await PrepareTajikCatHtmlTextAsync(context, tajikCat); - await context.SaveChangesAsync(); + var tajikPage = await context.TajikPages.Where(p => p.Id == catPage.Id).SingleOrDefaultAsync(); + if (tajikPage != null) + { + tajikPage.TajikHtmlText = await PrepareTajikCatHtmlTextAsync(context, tajikCat); + await context.SaveChangesAsync(); + } + } + done++; + } + + await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true); + } + catch (Exception exp) + { + await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString()); + } + } + } + ); + } + catch (Exception exp) + { + return new RServiceResult(false, exp.ToString()); + } + return new RServiceResult(true); + } + + /// + /// recovery job: regenerates TajikHtmlText for every Tajik poem from its TajikVerses, + /// overwriting whatever is currently stored. GanjoorTajikVerse rows carry no VersePosition + /// of their own, so it's recovered by joining back to the original GanjoorVerses row with + /// the same Id (the SQLite import sets GanjoorTajikVerse.Id = the original verse's Id). + /// Safe to run on every poem regardless of whether it was actually affected by the CatPage + /// id bug that RegenerateTajikCatAndPoetHtmlTextAsync had (see its cat-loop comment) - this + /// simply recomputes each poem's correct HTML from its real verse data every time, so poems + /// that were never touched come out unchanged. + /// + public RServiceResult RestoreTajikPoemHtmlTextAsync() + { + try + { + _backgroundTaskQueue.QueueBackgroundWorkItem + ( + async token => + { + using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions())) + { + LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context); + var job = (await jobProgressServiceEF.NewJob("RestoreTajikPoemHtmlText", "Query data")).Result; + + try + { + var tajikPoems = await context.TajikPoems.AsNoTracking().ToListAsync(); + int done = 0; + foreach (var tajikPoem in tajikPoems) + { + await jobProgressServiceEF.UpdateJob(job.Id, done, $"poem: {tajikPoem.Id}"); + + var tajikVerses = await context.TajikVerses.AsNoTracking().Where(v => v.PoemId == tajikPoem.Id).ToListAsync(); + if (tajikVerses.Count > 0) + { + var originalVerses = await context.GanjoorVerses.AsNoTracking().Where(v => v.PoemId == tajikPoem.Id).ToListAsync(); + var verses = tajikVerses + .Select(tv => + { + var original = originalVerses.SingleOrDefault(v => v.Id == tv.Id); + return new GanjoorVerse() + { + VOrder = tv.VOrder, + VersePosition = original != null ? original.VersePosition : VersePosition.Paragraph, + Text = tv.TajikText, + }; + }) + .OrderBy(v => v.VOrder) + .ToList(); + + // TajikPages.Id == the poem's own Id for poem pages (see + // GanjoorService-TajikSQLiteImport.cs) - no GanjoorPages lookup needed here + var tajikPage = await context.TajikPages.Where(p => p.Id == tajikPoem.Id).SingleOrDefaultAsync(); + if (tajikPage != null) + { + tajikPage.TajikHtmlText = PrepareHtmlText(verses); + await context.SaveChangesAsync(); + } } done++; } diff --git a/TajikGanjoor/wwwroot/css/tajik-ganjoor.css b/TajikGanjoor/wwwroot/css/tajik-ganjoor.css index b1aefc66..7ecaf1d9 100644 --- a/TajikGanjoor/wwwroot/css/tajik-ganjoor.css +++ b/TajikGanjoor/wwwroot/css/tajik-ganjoor.css @@ -458,11 +458,12 @@ html, body { .tg-cat-page p:has(> a:only-child) { font-family: 'PT Sans', sans-serif; font-size: 1rem; - text-align: center; + text-align: start; margin: 0; padding: 10px 14px; border-bottom: 1px solid var(--tg-rule); max-width: 620px; + line-height: 1.6; } .tg-poet-page p:has(> a:only-child):first-of-type, @@ -475,6 +476,16 @@ html, body { .tg-cat-page p:has(> a:only-child) a { color: var(--tg-accent-deep); text-decoration: none; + font-weight: 700; +} + +/* the excerpt trailing the link (": ..." from PrepareTajikCatHtmlTextAsync / + PrepareTajikPoetHtmlTextAsync) is plain text in the same

, so it can only + be dimmed via the paragraph's own color - the link's explicit color above + still wins for the title itself */ +.tg-poet-page p:has(> a:only-child), +.tg-cat-page p:has(> a:only-child) { + color: var(--tg-ink-soft); } .tg-poet-page p:has(> a:only-child):hover,