Сервер: миграция 001 со всей схемой storage.md, store на modernc.org/sqlite (WAL, foreign_keys, один писатель), фоновая чистка раз в час, argon2id с параметрами ADR-021 и сверкой constant-time, сессии по SHA-256 токена, cookie bare_session, глобальная проверка Origin, девять эндпоинтов аккаунта. Ник в журнал не попадает: для /api/ пишется шаблон маршрута. Клиент: crypto.js по crypto.md построчно — мастер из пароля, два независимых ключа из мастера, ключевой блоб с ником в AAD, отпечаток от сырой точки; db.js со всеми хранилищами версии 1; экран входа и регистрации, настройки со сменой пароля, выходом и удалением аккаунта. Пароль не покидает клиент: проверено на боевом сервере — ни пароля, ни priv.d ни в одном теле запроса, вход на втором устройстве даёт тот же отпечаток. ADR-027: код internal для 500, причина только в журнале. ADR-028: тексты состояний клиента сведены в ui.md. ADR-029: вход под другим ником стирает историю только после подтверждения. ADR-030: верхняя граница итераций KDF, проверка границ на обеих сторонах. ADR-031: служебный выход перед повторным входом не заканчивает сеанс. ADR-032: каталог состояния 0700, файлы базы 0600. Прямые зависимости: modernc.org/sqlite, golang.org/x/crypto. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
39 lines
1.3 KiB
JavaScript
39 lines
1.3 KiB
JavaScript
// Каркас: сайдбар со списком чатов и место под экран — docs/ui.md, «Каркас»
|
|
// и «Список чатов». Чаты появятся на этапе 2, секции пока пустые.
|
|
|
|
import { el, mark } from "./dom.js";
|
|
|
|
// frame отдаёт корень и место под экран. screen — что показывать
|
|
// на мобильном, где виден один экран за раз: "list" или "screen".
|
|
export function frame(ctx, screen) {
|
|
const root = el("div", "shell");
|
|
root.dataset.screen = screen;
|
|
const main = el("main", "main");
|
|
root.append(side(ctx), main);
|
|
return { root, main };
|
|
}
|
|
|
|
function side(ctx) {
|
|
const nav = el("nav", "side");
|
|
|
|
const brand = el("div", "brand");
|
|
brand.append(mark(), el("span", null, "bare"));
|
|
nav.append(brand);
|
|
|
|
const list = el("div", "list");
|
|
for (const title of ["каналы", "личные"]) {
|
|
list.append(el("h2", "section", title), el("ul", "items"));
|
|
}
|
|
nav.append(list);
|
|
|
|
const me = el("button", "me");
|
|
me.type = "button";
|
|
const dot = el("i");
|
|
dot.setAttribute("aria-hidden", "true");
|
|
me.append(dot, el("span", null, `ты: @${ctx.me.nick}`));
|
|
me.addEventListener("click", () => ctx.go("#/settings"));
|
|
nav.append(me);
|
|
|
|
return nav;
|
|
}
|