Десктоп-упаковщик пишет имена в UTF-8, но не ставит bit-11 (UTF-8) в zip → fflate
декодирует кириллицу как latin1 (мохибейк), и пути из manifest.json (UTF-8) не
находят записи. parseZip строит индекс с восстановлением latin1→utf8 и резолвит
манифест устойчиво; fallback-ветка без дублей (имя восстанавливается только для показа).
Один ключ встречается как legacy EOS... (бэкфилл из снапшота) и как PUB_K1_...
(подписи нового SDK) — разные строки, строгое сравнение в checkActiveKey давало
active:false. Добавлен canonicalPublicKey (PublicKey.from().toString()) в shared;
C6 сравнивает обе стороны в канонической форме. Инцидент: документ Авдеев-Муравьев.
UI переведён на минималистичную систему с бордо-акцентом и мягкими поверхностями:
- Заголовок документа берётся из meta.title (чистая кириллица), а не из имени
файла zip — устраняет mojibake «Ð овЅ». В шапку добавлены подписант
(«Фамилия И.О.» из сертификата) и дата подписи.
- Inter Variable (latin+cyrillic) подключён реально (@fontsource-variable),
раньше был только в font-family → fallback.
- Эмодзи-иконки (✅❌⚠) заменены на SVG в цветных кружках (StatusIcon).
- Карточки: мягкая тень + hairline вместо резкой рамки; кнопки/вердикт-бейдж/
sync-индикатор — единые токены поверхностей и статусов.
- Палитра статусов отделена от бордо (вердикт ≠ бренд).
typecheck чист, vite собирает без ошибок.
Хэш-цепочка SDK склеивает звенья в РАЗНЫХ регистрах (Crypto.sha256 → строчные
meta_hash/hash; doc_hash из фабрики → ПРОПИСНОЙ), а `.sig` хранит их в едином
(часто верхнем) регистре. Верификатор склеивал сырые строки .sig → C3/C4 падали
на валидных документах.
- C3: composite = SHA-256(meta_hash.toLowerCase + doc_hash.toUpperCase).
- C4: signed_hash = SHA-256(hash.toLowerCase + signed_at[без мс/Z]); SDK берёт
signed_at как toISOString().split('.')[0], а .sig кладёт с `.000`.
- C6: signed_at без таймзоны → роут с format:date-time отдавал HTTP 400; на фронте
помечаем `Z` (signed_at семантически UTC).
Регистры и формат signed_at выведены из исходников SDK (Document/Crypto) и
ДОКАЗАНЫ на реальном .sig: после фикса C1/C3/C4/C5 = PASS (крипто-подпись
verifyDigest валидна). C2 на legacy-доках — отдельный формат meta, не этот фикс.
Два дефекта отображения синка:
1. SPA: new URL(path, '') бросал TypeError при пустом VITE_API_BASE → health-проба
всегда null → «индекс недоступен», хотя бэкенд synced. Фолбэк base на
window.location.origin (vite-proxy/nginx шлёт /v1 на api). vite.config —
фикс-порт 5174 (strictPort) + proxy /v1 → :3030, наружу один ssh-проброс.
2. health.current_block брался из последней строки permission_history → «висел» на
блоке последней смены прав (6303) в тихие периоды и читался как зависание.
Теперь current = block_num tip-записи стрима (getParserHead) = докуда реально
обработал парсер; растёт у головы в реальном времени. Побочно чинит C6:
beyond_index сравнивал at с устаревшим current_block_time (31 мая) и ложно
отбраковывал свежие запросы.
parser2@1.1.3 фиксит XtrimSupervisor (XTRIM MINID по un-acked pending,
а не lastDeliveredId) + null-гард в parseStreamEntries. До фикса индексатор
детерминированно падал в краш-лупе на entries-read=172094.
Проверено на live mono-ai-1 (484k блоков, genesis resync):
entries-read 15690 → 189608 монотонно, точка 172094 пройдена без краша;
crash_lines=0, restarts=0, supervisor рестартов индексатора=0;
stream_lag=0, status=synced. permission_history заполнен (genesis-блоки).
Поллер get_info ведёт head_block независимо от permission-событий; статус определяется лагом Redis-группы (вычитали стрим до хвоста), а не дельтой блоков. Supervisor вскрыл баг parser2@1.1.2 (XtrimSupervisor триммит un-acked pending → parseStreamEntries падает на null fields обрезанной записи) —消ит детерминированно, отчёт парсер-агенту.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>