diff --git a/.tmp-concepts/.gitkeep b/.tmp-concepts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-concepts/bootstrap-manifest.md b/.tmp-concepts/bootstrap-manifest.md new file mode 100644 index 0000000..97517bd --- /dev/null +++ b/.tmp-concepts/bootstrap-manifest.md @@ -0,0 +1,21 @@ +--- +title: Bootstrap Manifest +type: concept +updated: 2026-05-18 +generator: project-bootstrap@1.11.0 +--- + +# Bootstrap Manifest + +Skills used to initialize this project's `.wiki/` and `.tasks/` layout, with their versions at install time. + +| Skill | Version | Role | +|---|---|---| +| `project-bootstrap` | 1.11.0 | orchestrator | +| `setup-wiki` | 1.0.0 | wiki canonical layout | +| `setup-tasks` | 1.0.0 | tasks canonical layout | +| `project-discipline` | 0.1.1 | cross-project policy | +| `setup-interns` | 0.3.0 | interns MCP server install (one-time, per machine) | +| `using-interns` | 0.2.0 | interns runtime policy + per-session permission grant | + +This file is overwritten if `project-bootstrap` is re-run on the same project. For history, use `git log .wiki/concepts/bootstrap-manifest.md`. diff --git a/.tmp-concepts/cms-admin-assets-root-folder-seed.md b/.tmp-concepts/cms-admin-assets-root-folder-seed.md new file mode 100644 index 0000000..5c603a7 --- /dev/null +++ b/.tmp-concepts/cms-admin-assets-root-folder-seed.md @@ -0,0 +1,92 @@ +--- +title: Admin assets crash — missing root AssetsFolder seed +type: concept +tags: [cms, admin, data-seed, mssql, gotcha, null-check] +sources: [../sources/iis-host-migration-2026-05-19.md] +updated: 2026-05-19 +--- + +# CMS admin assets crash — root AssetsFolder seed + +`AssetsJsonViewModelBuilder.cs:22` (admin /admin/assets/.../getList) падает NullReferenceException на любом site, где **root row `Folders` (Discriminator='AssetsFolder', LoweredPath='/', OwnerId=SiteId)** отсутствует. Root создаётся lazy (при first asset upload через admin), и sites которые никогда не использовали admin assets UI — без root. + +Связано: [[cms-server-port-leak-fix]] (нашли при verification после port-leak fix), [[recovery-architecture-snapshot]] (был open issue #8). + +## Симптом + +``` +GET /admin/assets//getList?path= +→ 500 NullReferenceException + +[NullReferenceException] + MoreThenCms.Admin.ViewModels.Builders.AssetsJsonViewModelBuilder.Build(...) + AssetsJsonViewModelBuilder.cs:22 + MoreThenCms.Web.Admin.Controllers.AssetsController.GetList(Guid ownerId, String path) + AssetsController.cs:46 +``` + +В `AssetsController.GetList` (line 46): +```csharp +var model = _assetsViewModelBuilder.Build( + _assetsFoldersService.GetFolderByPath(ExecutionContext, ownerId, path), ...); +``` + +`GetFolderByPath` (`MoreThenCms\Assets\Services\AssetsFoldersService.cs:47-54`) делает `repo.Find(p => p.LoweredPath == path && p.OwnerId == ownerId)` — возвращает **null** если row нет. `Build` дальше делает `new JValue(model.ParentPath)` без null-check → crash. + +Frontend код (`AssetsAppFunc.cs:66-86`) делает proper null-check → HTTP 404. Только admin view-model builder упустил. + +## Кого затрагивает (snapshot 2026-05-19) + +После recovery: **15 sites без root** (из ~50). Sites где admin assets когда-либо открывался → root есть. Остальные: +- emspb.ru, pilorama98.ru, labtools.pro, kupimknigi.spb.ru, sestech.ru, aquamax.spb.ru, artmone.pro, priemka-kvartiry.ru, profund.spb.ru, ics-artmaterials.com, _voda-indigo.ru +- 4 sites с NULL `PrimaryDomain` (legacy/test data) + +Проверочный запрос: +```sql +SELECT s.SiteId, s.PrimaryDomain, + CASE WHEN EXISTS (SELECT 1 FROM Folders f + WHERE f.OwnerId = s.SiteId + AND f.LoweredPath = '/' + AND f.Discriminator = 'AssetsFolder') + THEN 'OK' ELSE 'NO ROOT' END AS RootStatus +FROM Sites s +ORDER BY RootStatus, s.PrimaryDomain; +``` + +## Fix (применён 2026-05-19) + +DB seed — один INSERT для всех missing roots, идемпотентный (`WHERE NOT EXISTS`): + +```sql +DECLARE @CreatedById uniqueidentifier = 'E4CC416B-5D13-4757-8EFE-03CBEA15B18C'; -- existing admin user +DECLARE @Inserted TABLE (FolderId uniqueidentifier, OwnerId uniqueidentifier); + +INSERT INTO Folders (FolderId, OwnerId, Path, LoweredPath, + DateCreated, UtcDateCreated, CreatedById, Discriminator) +OUTPUT INSERTED.FolderId, INSERTED.OwnerId INTO @Inserted +SELECT NEWID(), s.SiteId, '/', '/', + SYSDATETIME(), SYSUTCDATETIME(), @CreatedById, 'AssetsFolder' +FROM Sites s +WHERE NOT EXISTS (SELECT 1 FROM Folders f + WHERE f.OwnerId = s.SiteId + AND f.LoweredPath = '/' + AND f.Discriminator = 'AssetsFolder'); + +SELECT FolderId, OwnerId FROM @Inserted; -- save for atomic revert +``` + +15 rows вставлены. Inserted FolderId/OwnerId сохранены в `.tasks/cms-admin-assets-root-folders-seed.inserted-rows.txt` для atomic revert (`DELETE FROM Folders WHERE FolderId IN (...)`). + +После: `/admin/assets//getList?path=` возвращает HTTP 302 → /login для unauth (normal auth path), вместо 500. Authorized users видят empty assets list. + +## Долгосрочный TODO (не сделано — нет рабочего build env) + +`AssetsJsonViewModelBuilder.Build` должна null-guard'иться — возвращать empty JObject ({parentPath:"", path:"/", folders:[], files:[]}) когда model null. Это **defensive code**, плюс auto-create root в `GetFolderByPath` для root path (как обычно делают админ-репозитории). + +Требует recompile `MoreThenCms.Admin.dll` — отложено до восстановления полного build pipeline (gitea/build/registry — см. [[vds-kzntsv-bootstrap]] task). + +## Возможно затрагивает ещё (open для следующей сессии) + +`Folders` table — polymorphic: `AssetsFolder`, `ImagesFolder` (OwnerId=ThemeId), `ScriptsFolder` (ThemeId), `StylesheetsFolder` (ThemeId). Аналогичная null-falling логика может быть в admin views для Themes (Stylesheets/Scripts/Images). Не проверено, не reported user'ом. + +Если будет — same pattern: `SELECT s.SiteId vs Folders root по соответствующему Discriminator` + seed. diff --git a/.tmp-concepts/cms-config-rewrite-pattern.md b/.tmp-concepts/cms-config-rewrite-pattern.md new file mode 100644 index 0000000..7740130 --- /dev/null +++ b/.tmp-concepts/cms-config-rewrite-pattern.md @@ -0,0 +1,125 @@ +--- +title: Web.config rewrite — кодировка и connection-string patterns +type: concept +tags: [iis, webconfig, encoding, powershell, traceback] +sources: [../sources/nas-recovery-session-2026-05-18.md] +updated: 2026-05-19 +--- + +# Web.config rewrite на Windows: encoding pitfall + +## Сценарий + +После cross-host миграции CMS в VM, нужно переписать connection strings (`192.168.1.10` → `10.0.2.2` или другой host). Делается через SSH/PowerShell `Get-Content`+`-replace`+`Set-Content`. + +## Pitfall: Set-Content без -Encoding + +```powershell +# WRONG — на PowerShell 5.1 пишет UTF-16 LE +(Get-Content $f -Raw) -replace 'old', 'new' | Set-Content $f -NoNewline +``` + +**Симптом** после этого: +- IIS возвращает `HTTP 500.19 - Internal Server Error`, "Файл конфигурации создан в неправильном формате XML", error code `0x8007000d`. +- В детале: первые строки файла показывают артефакты `????????` (UTF-16 в окне UTF-8 ожидаемой проверки). + +**Корень:** PowerShell 5.1 (Windows встроенный) `Set-Content` без `-Encoding` использует **default = UTF-16 LE** (Unicode). IIS ожидает UTF-8 (BOM или без BOM). + +## Правильный способ + +```powershell +$utf8WithBom = New-Object System.Text.UTF8Encoding($true) +$content = Get-Content $file -Raw +$new = $content -replace 'old', 'new' +[System.IO.File]::WriteAllText($file, $new, $utf8WithBom) +``` + +Или, если хочется без BOM: +```powershell +$utf8NoBom = New-Object System.Text.UTF8Encoding($false) +[System.IO.File]::WriteAllText($file, $new, $utf8NoBom) +``` + +ASP.NET / IIS работают и с BOM, и без — но для дефолтного XML конфига Microsoft предпочитает с BOM. + +### PowerShell 7+ workaround + +В Core: `Set-Content -Encoding utf8` пишет UTF-8 БЕЗ BOM (отличается от PS 5!). `-Encoding utf8BOM` — с BOM. + +## Connection string patterns в CMS + +Нашли в `MoreThenCms.WebUI/Web.config` source (development): + +```xml + +``` + +В production (внутри OVA, после восстановления): + +```xml + +``` + +То есть в prod CMS использует **SQL login `snolla`**, не Integrated Security. Сам логин остался в `master.mdf` после восстановления — никаких дополнительных шагов не понадобилось. + +## Mass-edit нескольких сайтов + +Под Windows VM было 4 Web.config с одинаковым паттерном: + +- `C:\inetpub\wwwroot\MoreThenCms.Web\Web.config` +- `C:\inetpub\wwwroot\Snolla.IdentityManager\Web.config` +- `C:\stayer\stostayer.old\web.config` +- (плюс `C:\stayer\MoreThenCms.Web\` — main stostayer без 192.168.1.10 reference) + +Скрипт через SSH в VM: + +```powershell +$files = @( + 'C:\inetpub\wwwroot\MoreThenCms.Web\Web.config', + 'C:\inetpub\wwwroot\Snolla.IdentityManager\Web.config', + 'C:\stayer\stostayer.old\web.config' +) +$utf8WithBom = New-Object System.Text.UTF8Encoding($true) +foreach ($f in $files) { + $content = Get-Content $f -Raw + $new = $content -replace [regex]::Escape('192.168.1.10'), '10.0.2.2' + [System.IO.File]::WriteAllText($f, $new, $utf8WithBom) +} +# затем +iisreset +``` + +## Storage providers (Azure / MinIO) + +В `appSettings` нашли: + +```xml + + + + + + +``` + +Используется **Azure Storage SDK с custom endpoint**. В production endpoint указывает на **MinIO** (S3-compat но **не** Azure-compat — пользователь упомянул "не так всё" и обещал пояснить). Точная схема подключения — TBD, надо изучать `MoreThenCms.FileStorage.Azure.AzureCloudStorage` класс. + +## `[regex]::Escape` для безопасной замены IP + +`192.168.1.10` без escape — точки в regex matchят любой char. Лучше escape: + +```powershell +$pattern = [regex]::Escape('192.168.1.10') +$new = $content -replace $pattern, '10.0.2.2' +``` + +Связано: [[snolla-recovery-vm]], [[traefik-on-windows-docker-desktop]]. diff --git a/.tmp-concepts/cms-maljarka-https-mode-crash.md b/.tmp-concepts/cms-maljarka-https-mode-crash.md new file mode 100644 index 0000000..24e94ca --- /dev/null +++ b/.tmp-concepts/cms-maljarka-https-mode-crash.md @@ -0,0 +1,102 @@ +--- +title: CMS 502 при HTTPS-mode для конкретного hostname (maljarka.tandemmebel.ru) +type: concept +tags: [cms, iis, gotcha, url-rewrite, https] +sources: [] +updated: 2026-05-21 +--- + +# CMS 502 при HTTPS-mode для maljarka.tandemmebel.ru + +CMS код (или CMS DB config) для `maljarka.tandemmebel.ru` падает когда IIS request попадает с включённым HTTPS-context (`HTTPS=on`/`SERVER_PORT=443`). Тот же hostname работает (200 OK) когда request интерпретируется как plain HTTP. Другие cms hosts (`tandemmebel.ru`, `emspb.ru`, `pilorama98.ru` и т.д.) работают нормально в HTTPS-mode. + +## Симптом + +Через traefik HTTPS chain: +``` +GET https://maljarka.tandemmebel.ru/ → 502 Bad Gateway +``` + +## Bisect-trace + +| Direct backend request (внутри traefik container) | Headers | Result | +|---|---|---| +| `wget http://host.docker.internal:8089/ -H 'Host: maljarka.tandemmebel.ru'` | base | **200 OK** (43730 bytes, "Малярка от Тандеммебель") | +| same + `X-Forwarded-For: 1.2.3.4` | XFF only | **200 OK** | +| same + `X-Forwarded-Proto: https` | XFP=https | **502** ← триггер | +| same + ALL X-Forwarded-* | full | **502** | +| `Host: tandemmebel.ru` + XFP=https | sibling control | 301 (correct) | + +`X-Forwarded-Proto: https` — единственная переменная которая триггерит 502 для maljarka.tandemmebel.ru. + +## Mechanism + +После [[cms-server-port-leak-fix]] в `C:\sites\snolla\Web.config` добавлен URL Rewrite rule: + +```xml + + + + + + + + + + + + +``` + +Когда traefik проксит request через HTTPS entrypoint, он добавляет `X-Forwarded-Proto: https` (стандартное поведение). IIS URL Rewrite rule срабатывает → ставит `HTTPS=on` / `SERVER_PORT=443`. CMS код (`MoreThenCms.Web`) видит request как HTTPS. + +Для большинства hostname'ов CMS строит URLs корректно в HTTPS-context. Для `maljarka.tandemmebel.ru` — что-то падает (NullRef в URL builder, missing config, redirect loop, etc.) → ASP.NET unhandled exception → IIS возвращает 502. + +## Where to investigate (next session) + +1. **IIS Logs** — `C:\inetpub\logs\LogFiles\W3SVC*\` для site `snolla` — найти request с `Host: maljarka.tandemmebel.ru` + XFP=https → status code + sub-status. +2. **ASP.NET Event Log** — `eventvwr.msc` → Application log → ASP.NET 4.0 errors с stack trace. +3. **CMS DB** — table с per-host config (если есть field "https URL" / "base URL"); может для maljarka.tandemmebel.ru эта запись `NULL`/empty. +4. **CMS source** — grep по `siteRoot`, `BaseUrl`, `HttpsUrl` в коде CMS; искать где условие `IsSecureConnection` или `Request.IsHttps` ветвит логику. +5. **`Url.SiteRoot()`** — уже паттерн из [[cms-server-port-leak-fix]]; возможно тут другой helper падает specifically на этом host. + +## Workarounds + +### Workaround A: Strip X-Forwarded-Proto для maljarka в traefik (быстро, но скрывает CMS bug) + +В `maljarka.yml` добавить middleware: +```yaml +http: + routers: + maljarka: + ... + middlewares: [maljarka-strip-xfp] + middlewares: + maljarka-strip-xfp: + headers: + customRequestHeaders: + X-Forwarded-Proto: "" +``` + +Side effect: maljarka страница может содержать `http://...` asset links вместо `https://...` — mixed content warnings в браузере. + +### Workaround B: Per-host bypass в URL Rewrite rule + +В `C:\sites\snolla\Web.config` обновить rule: +```xml + + + + +``` + +Workaround C: hot-fix DB / Web.config с правильным базовым URL для maljarka.tandemmebel.ru (нужно исследование CMS). + +## Применено + +Не применено. Diagnosed only — fix отложен до CMS code investigation. Side-finding из [[traefik-maljarka-502-bug]]. + +## Ссылки + +- URL Rewrite rule origin: [[cms-server-port-leak-fix]] +- Other CMS-side 5xx bugs (паттерн): [[cms-admin-assets-root-folder-seed]] (другой NullRef), `emspb /admin/assets 500` (snapshot open issue #8). diff --git a/.tmp-concepts/cms-server-port-leak-fix.md b/.tmp-concepts/cms-server-port-leak-fix.md new file mode 100644 index 0000000..d14b60d --- /dev/null +++ b/.tmp-concepts/cms-server-port-leak-fix.md @@ -0,0 +1,174 @@ +--- +title: CMS port-leak fix — URL Rewrite serverVariables на host IIS +type: concept +tags: [iis, url-rewrite, traefik, x-forwarded, asp-net, gotcha] +sources: [../sources/iis-host-migration-2026-05-19.md] +updated: 2026-05-19 +--- + +# CMS port-leak fix + +Решение для open issue #1 из [[recovery-architecture-snapshot]] (раньше «X-Forwarded headers не настроены, :4443 leak») и админ-utечки `:8089` обнаруженной 2026-05-19 вечером после attempt 2 host-IIS миграции. Документирует **root cause**, **почему VM работала**, **почему обходной path-B на Windows Docker Desktop невозможен**, и **что в итоге применено**. + +Связано: [[traefik-on-windows-docker-desktop]] Pitfall 5, [[iis-host-migration-2026-05-19]] Phase 10, [[docker-host-loopback-detect]]. + +## Симптом + +Admin URLs формата (после attempt 2 миграции на host-IIS:8089): +- `https://emspb.snolla.com:8089/admin/assets//getList?path=` +- `https://emspb.snolla.com:8089/admin/themes/getImageSizes/` +- `https://emspb.snolla.com:8089/admin/templates/editors.tmpl.html?v=2.006` + +Browser HSTS upgrade'ит `http://...:8089` → `https://...:8089` → TCP open, TLS handshake fails (8089 = plain HTTP) → admin SPA ломается. + +Аналогично, ранее (issue #1) — CMS делал HTTP→HTTPS redirect с `:4443` (traefik external port). + +## Root cause + +`MoreThenCms.Admin\Mis\Web\Mvc\UrlHelpers\UrlHelpers.cs:14-37` `Url.SiteRoot()`: + +```csharp +var port = context.Request.ServerVariables["SERVER_PORT"]; +if (usePort) { + if (port == null || port == "80" || port == "443") port = ""; + else port = ":" + port; +} +var protocol = context.Request.ServerVariables["SERVER_PORT_SECURE"]; +if (protocol == null || protocol == "0") protocol = "http://"; else protocol = "https://"; +var sOut = protocol + context.Request.ServerVariables["SERVER_NAME"] + port + appPath; +``` + +Читает **socket-level** server variables. На IIS site `snolla` binding `*:8089` HTTP → `SERVER_PORT=8089`, `SERVER_PORT_SECURE=0` → формирует `http://emspb.snolla.com:8089` → рендерится в Razor: + +```cshtml +@* C:\sites\snolla\Views\Shared\_Layout.cshtml:229 *@ +mis.siteRoot = '@Url.SiteRoot().Replace("http:", "https:")' + '/admin'; +@* и _LogInLayout.cshtml:156 *@ +mis.siteRoot = '@Url.SiteRoot()'; +``` + +Замена `http:→https:` в Layout была *прошлым* частичным patch'ем — не убирает порт. Десятки .cshtml в admin также используют `Url.SiteRoot()` для inline `