offplan · online
Plan · stage1

Stage 1 — Wave 2 Chunk 4: §5 Visibility

Approvedplanstage1priority P0
Ratified
2026-05-08
Created
2026-05-08
Priority
P0
Tags
ux, security, architecture

Goal

Закрыть 2 пробела из CONV-22 What's Next по §5:

  1. PIN-protected preset — 4-й вариант Public Visibility (защищённая страница для pre-launch teaser scenario), уже задекларирован в §1.2 Object Builder line 732 + Phase 1.10 v4.3 callout, но в §5 не определён.
  2. Custom preset cleanup — текущий «Custom» preset с granular toggle'ами <details> блоком запутывает модель; убираем из Stage 1 как не-MVP.

Pattern Chunk 2/3: surgical правки + 1 новая subsection в §5 + light phase callouts. Все изменения внутри launch-plan-stage-1.html + changelog v4.8 + workstream update. Существующая §4.2 Permissions matrix получает minor label update.

Locked Decisions (CONV-25 interview)

# Тема Решение
A1 Visibility model granularity Per-project в Stage 1, без org-default. Org-default Visibility deferred Stage 2 (если многопроектные студии попросят). Каждый проект — independent preset choice в Object Builder + Project Settings. Reasoning: средне 5 проектов на org в первом milestone — выбрать preset 5 раз не больно; убирает confusion «какой layer wins».
A2 Preset name «PIN-protected» — consistent с §1.2 line 732 declaration; чёткая mental model для Owner'а («это страница, на которую нужен PIN»).
A3 PIN scope Один PIN на проект. Set / change permission: Owner / Admin (matching §4.2 row «Public Visibility settings»). Sales Manager — нет (это strategic positioning decision, не operational).
A4 Anonymous viewer experience Blur + PIN entry overlay. Весь content проекта рендерится с CSS filter: blur(...) (силуэты + цвета видны, деталей не разобрать) + поверх — fixed PIN entry card с двумя CTA: «Enter PIN» (input + submit) + «Contact us» (mailto / tel). Не Private hero, не splash gate page — teaser-curiosity без полезной информации. Реализация — копеечная (CSS filter на content wrapper).
A5 Cookie lifetime 30 дней после правильного ввода PIN'а (pin_<project_id> cookie). Re-entry required только если: cookie expired, или Owner изменил PIN, или user в private/incognito browsing.
A6 After-PIN view **Owner выбирает: Discovery Full sales** (secondary dropdown в Object Builder и Project Settings рядом с PIN config полем). Pre-launch без финальных цен → Discovery (рендеры + 360° + counter «N units», без цен и availability badges). С готовыми ценами → Full sales (всё видно). Гибкая модель: PIN — это «gate», After-PIN — это «что разблокируется».
A7 Buyer-token bypass Yes?b=<token> ссылки bypass'ят PIN entirely. Buyer не знает PIN, agent знает; token = pre-auth со стороны agent'а. Эта behaviour — fundamental к Phase 1.10 buyer flow (state #2 «Buyer с tokenised ссылкой»).
A8 Custom preset cleanup Удаляется полностью из Stage 1. §5 имеет 4 preset'а: Private / Discovery / Full sales / PIN-protected. <details> Advanced overrides блок (6 toggle'ов: Show prices / availability / floorplates / price range / inquiry / heart actions) → Stage 2 placeholder с пометкой «deferred». В Stage 1 хватает 4 preset'ов.
A9 «Set up your organisation» wizard Без изменений — wizard остаётся: types (Studio/Agency/Developer multi-select) + brand (logo + accent color) + invite команды. Visibility туда не добавляется (org-default = Stage 2).
A10 PIN config UX — contact fields Когда Owner / Admin выбирает PIN-protected preset → в Project Settings разворачиваются 2 контактных поля: email (required) + phone (optional). Эти контакты показываются на PIN entry screen для anonymous'а. Per-project (на каждом проекте может быть свой контакт — секретарь / менеджер / агент).
A11 Контакт source Per-project (не org-level public contact info блок). Каждый PIN-protected проект имеет свой email + phone в Project Settings → PIN config.
A12 PIN screen «Contact us» action mailto:<email>?subject=Access request: <Project name> + tel:<phone> ссылки — нативные browser actions, ноль backend'а. Кастомная contact-form с сохранением в БД → Stage 2. Если phone не заполнен — показывается только email кнопка.

Strategic insight: PIN-protected — не «preset которого не было», а формализация уже задекларированного 4-го варианта из §1.2 Object Builder. Custom cleanup — параллельная работа: убираем «гибрид» который смущал mental model (как preset чтоли, но это и не preset а override mode).

Approach

Approach B (Restructured + PIN-protected как отдельная subsection):

Pattern Chunk 3 (CONV-25 build): anchor IDs, cross-links, comparison-style spec, callout style.

Steps

A. §5 lead paragraph — rewrite «global per Organisation» → «per project»

A1. Заменить параграф (current line 980-985) — explicitly per-project model + remove Custom mention from preset count.

<h4 style="color:var(--navy); margin:20px 0 10px;">Public Visibility settings — 4 preset'а (per-project)</h4>
<p style="margin-bottom:10px;">Per-project setting (Object Builder + Project Settings → <strong>Public Visibility</strong>). Определяет какие поля видит <strong>anonymous visitor</strong> (state #1 из таблицы выше) на публичной странице конкретного проекта — цены, availability, floorplates, или (для PIN-protected) blurred тизер с PIN-gate. На Buyer-flow (state #2) и team members (state #4) не влияет. <strong>Org-default Visibility — deferred to Stage 2</strong> (если многопроектные студии попросят).</p>

B. Preset table — 4 rows (Custom удалён, PIN-protected добавлен)

B1. Заменить existing 4-row preset table (lines 987-995). Custom row → PIN-protected row.

<table style="margin:8px 0; font-size:13px;">
  <thead><tr><th style="width:130px;">Preset</th><th>Что видит anonymous</th></tr></thead>
  <tbody>
    <tr><td><strong>Private</strong></td><td>Только hero (logo, branding, tagline) + «Request access» CTA. Никаких проектов, юнитов, рендеров.</td></tr>
    <tr><td><strong>Discovery</strong> <em style="color:var(--gold);">(по умолчанию для нового проекта)</em></td><td>Все проекты Organisation'ы видны — hero, рендеры, 360°, floorplates, список юнитов. <strong>Цены и индивидуальные статусы юнитов скрыты</strong> — отображается только counter «N units available» на проект. CTA «Request access» открывает запрос на доступ к Full sales view.</td></tr>
    <tr><td><strong>Full sales</strong></td><td>Всё что в Discovery + цены per unit + availability badges per unit (<code>available / reserved / sold</code>). Полноценный public sales-сайт — anonymous видит каждый юнит во всех проектах Organisation'ы независимо от stock allocation assignments.</td></tr>
    <tr><td><strong>PIN-protected</strong></td><td>Содержимое проекта показывается с CSS blur (силуэты + цвета без деталей); поверх — PIN entry overlay с «Enter PIN» + «Contact us» CTA. Правильный PIN → 30-day cookie + разблокируется выбранный After-PIN view (Discovery или Full sales). Buyer-token (<code>?b=...</code>) bypass'ит PIN. Полный spec — см. <a href="#fd-visibility-pin">§5.1 ниже</a>.</td></tr>
  </tbody>
</table>

C. Advanced overrides <details> — repurpose как Stage 2 placeholder

C1. Заменить existing <details> блок (lines 997-1007) — short Stage 2 deferral note без granular toggles.

<details>
<summary class="task-label">Granular field-level overrides — deferred to Stage 2 <span class="count">(deferred)</span></summary>
<p style="font-size:13px; color:var(--text-mid);">В первой итерации planning'а §5 предполагался <strong>«Custom» preset</strong> с granular toggle'ами (show prices / availability / floorplates / price range / inquiry / heart actions). На <em>2026-05-08 (CONV-25)</em> мы удалили этот preset из Stage 1 — 4 базовых preset'а (Private / Discovery / Full sales / PIN-protected) покрывают MVP-needs студий. Granular per-field overrides вернутся в Stage 2 если многопроектная студия запросит гибрид (например, «Discovery + show prices» — между Discovery и Full sales). Heart / «Связаться» actions для anonymous всегда скрыты (требуют buyer-token).</p>
</details>

D. NEW subsection §5.1 — PIN-protected detailed spec

D1. Добавить new subsection после existing <p> "Per-project override" (line 1009 — он сейчас говорит «Stage 2», его удалить заодно since он ставит в conflict с A1's per-project лозунгом). Insert before closing </div> на line 1010.

<h4 id="fd-visibility-pin" style="color:var(--navy); margin:24px 0 10px;">5.1 — PIN-protected preset (deep spec)</h4>
<p style="margin-bottom:10px;"><strong>Use case:</strong> застройщик / студия хочет показать проект приватно (pre-launch teaser, VIP-listing, ранний preview для investor'ов) — отдаёт URL + PIN устно или в одном email. Anonymous, не зная PIN, видит только blurred тизер; знающие PIN — открывают полную страницу.</p>

<div style="margin:12px 0; padding:14px 18px; background:var(--sand-50); border-left:3px solid var(--gold); border-radius:0 6px 6px 0;">
  <strong style="color:var(--navy); font-size:14px;">Anonymous visitor flow (без правильного cookie)</strong>
  <ol style="margin:8px 0 0 20px; font-size:13.5px; color:var(--text-mid);">
    <li>URL: <code>{slug}.offplan.online/projects/{project-slug}</code> — обычный sales-app URL.</li>
    <li>Содержимое проекта рендерится в полном объёме (как After-PIN preset выглядел бы) **с CSS <code>filter: blur(12-16px)</code> на content wrapper** — силуэты hero, рендеров, floorplate, units list видны, но не читаемы.</li>
    <li>Поверх blur'а — fixed PIN entry overlay (centered card, brandbook v2 styling) с:
      <ul style="margin:4px 0 0 16px;">
        <li>Заголовок: «<em>Project Name</em>» (project-name виден, чтобы пользователь понял на что зашёл) + строка «Эта страница защищена. Введите PIN для доступа.»</li>
        <li>Input field — PIN (numeric 4-8 цифр или alphanumeric password — Owner определяет при setup'е, free-format string)</li>
        <li>Кнопка «Open project» (submit)</li>
        <li>Sub-block «Не знаете PIN?» с двумя actions: <strong>Email us</strong> (`mailto:<contact_email>?subject=Access request: <Project name>`) + <strong>Call us</strong> (`tel:<contact_phone>` если phone не пустой; иначе кнопка скрыта).</li>
      </ul>
    </li>
    <li>Submit → server validates PIN → если correct → <strong>set cookie <code>pin_<project_id>=<random_token></code> на 30 дней</strong>, redirect на same URL → теперь user видит After-PIN preset (Discovery или Full sales — what Owner выбрал). Если incorrect → form shows error «Wrong PIN» + tracks attempt в audit log; rate-limit 5 attempts → 30s cooldown.</li>
  </ol>
</div>

<div style="margin:12px 0; padding:14px 18px; background:var(--sand-50); border-left:3px solid var(--gold); border-radius:0 6px 6px 0;">
  <strong style="color:var(--navy); font-size:14px;">PIN bypass cases (visitor видит After-PIN preset напрямую)</strong>
  <ul style="margin:8px 0 0 20px; font-size:13.5px; color:var(--text-mid);">
    <li><strong>Valid 30-day cookie</strong> на этом проекте — re-entry не нужен.</li>
    <li><strong>Buyer-token URL</strong> (<code>?b=&lt;token&gt;</code>) — token = pre-auth со стороны Sales Agent'а, который buyer'у эту ссылку прислал. Buyer не должен знать PIN (он его не должен видеть). Token валиден → bypass PIN entirely → render полная unit-page (state #2 в viewer states таблице).</li>
    <li><strong>Logged-in team member</strong> owning Organisation — Owner / Admin / Sales Manager / Content Editor / Sales Agent → state #4 в таблице, всегда Full sales (PIN не нужен). Logged-in member guest organisation — same: bypass PIN (он уже authenticated в системе с правом на этот проект).</li>
    <li><strong>Owner изменил PIN</strong> → все existing cookies invalidated (security default — изменение PIN это «отозвать access у тех кто PIN знает»). Visitors с invalid cookie возвращаются на blurred PIN entry screen.</li>
  </ul>
</div>

<div style="margin:12px 0; padding:14px 18px; background:#fff; border:1px solid var(--border); border-radius:6px;">
  <strong style="color:var(--navy); font-size:14px;">PIN config UI (Project Settings → Public Visibility = PIN-protected)</strong>
  <p style="font-size:13.5px; color:var(--text-mid); margin:8px 0;">Когда Owner / Admin выбирает «PIN-protected» preset для проекта (в Object Builder при создании ИЛИ в Project Settings потом) — разворачиваются 4 поля:</p>
  <ul style="margin:0 0 0 20px; font-size:13.5px;">
    <li><strong>PIN</strong> (required) — text input, free-format string (PIN или password — Owner решает; min 4 char). Hashed at rest (bcrypt / argon2 — implementation Phase 1.3).</li>
    <li><strong>After PIN: visitor sees</strong> (required) — radio / dropdown: <code>Discovery</code> (по умолчанию) | <code>Full sales</code>. Pre-launch без финальных цен → Discovery. С готовыми ценами → Full sales.</li>
    <li><strong>Contact email</strong> (required) — email visitor'а который не знает PIN отправит запрос на доступ. Default = Owner email; editable. Per-project — может быть отдельный «секретарь / менеджер / агент» на каждый проект.</li>
    <li><strong>Contact phone</strong> (optional) — phone number в международном формате. Если заполнен — на PIN screen появляется кнопка «Call us» рядом с «Email us».</li>
  </ul>
  <p style="font-size:13px; color:var(--text-mid); margin-top:8px;">Permission to set / change PIN: <strong>Owner / Admin</strong> (matching <a href="#fd-access">§4.2</a> row «Public Visibility settings»). Sales Manager / Content Editor / Sales Agent — нет.</p>
</div>

<div style="margin:12px 0; padding:14px 18px; background:#fff; border:1px solid var(--border); border-radius:6px;">
  <strong style="color:var(--navy); font-size:14px;">Audit log</strong>
  <ul style="margin:8px 0 0 20px; font-size:13.5px; color:var(--text-mid);">
    <li>PIN set / changed: «User X set / changed PIN for Project Y at TIMESTAMP» — visible to Owner / Admin (НЕ показывает сам PIN value).</li>
    <li>Failed PIN attempts (>3 в час с одного IP) — flag в operator panel (Phase 1.4) для anti-brute-force monitoring.</li>
    <li>Successful PIN entry — НЕ pers-attributed event (anonymous visitor); только aggregate counter «N PIN-passed views в day» для analytics.</li>
  </ul>
  <p style="font-size:13px; color:var(--text-mid); margin-top:8px;">Schema-level дизайн audit таблицы — Phase 1.3 implementation level (consistent с §4.6 View-as-Agent audit).</p>
</div>

<p style="font-size:13px; color:var(--text-mid); margin-top:14px;"><strong>Cross-link:</strong> <a href="#phase-1-2">Phase 1.2 Object Builder</a> — surface этих 4 полей в creation flow · <a href="#phase-1-10">Phase 1.10 Sales App</a> — routing logic для blur + PIN entry + cookie + buyer-token bypass · <a href="#phase-1-4">Phase 1.4 Operator Dashboard</a> — operator может видеть «Project X is PIN-protected» (для support troubleshooting) но не PIN value.</p>

E. Viewer states table — light note про «PIN-passed» substate

E1. Добавить one-line note под viewer states table (after line 973 closing </table>) — короткое пояснение что state #1 anonymous может иметь substate «PIN-passed cookie» когда проект на PIN-protected preset.

<p style="margin:8px 0 0; font-size:13px; color:var(--text-mid);"><strong>Substate (PIN-protected projects):</strong> visitor в state #1 может иметь valid <code>pin_&lt;project_id&gt;</code> cookie от предыдущего входа — рендер тогда такой же как у соответствующего After-PIN preset (Discovery или Full sales), без blur. См. <a href="#fd-visibility-pin">§5.1</a>.</p>

F. §4.2 Permissions matrix — minor label update

F1. Update row label (current line ~890 в launch-plan-stage-1.html — «Public Visibility settings (Private / Discovery / Full sales)»):

<tr><td>Public Visibility settings (Private / Discovery / Full sales / PIN-protected)</td><td>✅</td><td>✅</td><td>—</td><td>—</td><td>—</td></tr>

(Permissions без изменений: Owner / Admin only, остальные —. PIN setting falls под этот row.)

G. Phase callouts touchpoints (light updates)

G1. Phase 1.2 (Object Builder) — add bullet в существующий v4.5 callout (Atelier rework), либо в v4.6 если есть; или create new v4.8 sub-bullet:

«v4.8 (CONV-25): PIN-protected preset config (4 поля — PIN / After-PIN view / contact email / contact phone) разворачивается в Object Builder когда Owner выбирает PIN-protected. См. <a href="#fd-visibility-pin">§5.1</a>.»

G2. Phase 1.10 (Sales App) — add bullet в существующий v4.4 / v4.7 callout:

«v4.8 (CONV-25): sales-app routing handles PIN gate. Anonymous request на PIN-protected project → render с CSS blur + overlay; valid cookie → After-PIN preset; buyer-token → bypass; logged-in member owning/guest org → bypass. Implementation note: middleware checks (a) buyer token presence/validity, (b) cookie presence/validity, (c) auth status — sequence from least to most invasive.»

G3. Phase 1.4 (Operator Dashboard) — add bullet в существующий v4.4 / v4.7 callout:

«v4.8 (CONV-25): operator может видеть «Project X is PIN-protected» (флаг в Project list / Project drawer) для support troubleshooting (e.g., «client says PIN doesn't work»), но не видит самого PIN value (security — operator не должен знать пользовательские secrets). Anti-brute-force flag — failed PIN attempts >3/hour highlighted.»

H. Changelog v4.8 entry

H1. Append v4.8 entry в launch-plan-changelog.html (поверх v4.7):

I. Workstream update

I1. workstreams/stage1-roman-integration.md:

J. Preview repo sync

J1. Sync 2 файла в ~/code/offplan-online/preview/plan/:

Commit + push в preview (analogous Chunks 2/3 sync).

Files

Dependencies

Testing

Risks

  1. §5.1 размер — может вырасти больше чем 150 lines если все 4 boxes (flow / bypass / config / audit) детально расписаны. Mitigate: использовать compact bullet style, не embedded sub-tables. Если в /build выходит >200 lines — split на 5.1 (anonymous + bypass) и 5.2 (config + audit).
  2. Audit log scope creep — детали аудита легко могут перетянуть в Phase 1.8 (Security infra). Mitigate: упомянуть как requirement без deep dive в схему таблицы — это уровень Phase 1.3 implementation (consistent с pattern §4.6 View-as-Agent).
  3. Phase 1.2 Object Builder UI — 4 conditional поля (только при PIN-protected) добавляют UI complexity в creation flow. Mitigate: light callout, не deep redesign — детали = Phase 1.2 build / Atelier mockup update separately.
  4. Custom cleanup может сломать cross-refs — нужно проверить что нигде в плане нет ссылок на «Custom preset» или old <details> Advanced overrides anchor (если был). Mitigate: grep на «Custom» before commit.
  5. Phase 1.10 routing complexity — middleware logic с тремя bypass cases (cookie / buyer-token / auth) может быть сложнее чем в текущем 1.10 spec. Mitigate: light callout с sequence note (least → most invasive); полный spec routing'а = Phase 1.10 build.

Workstreams

Evaluation

Done when:

Definition of NOT done (deferred):

Open Questions

  1. «Request access» CTAs в Private + Discovery preset'ах — currently §5 не указывает куда они ведут. Логично применить ту же модель что и для PIN: per-project contact (email + phone). Но это additional scope; решение по этому может появиться при Chunk 4 /build inline (если place позволяет) или в отдельном мини-патче после Chunk 4.
  2. PIN format constraint — free-format string с min 4 char (decision A10) даёт максимальную гибкость, но Owner может ввести «1234» (weakly secure). Stage 1: не enforce'им strength (это not security-critical pre-launch teaser, не payment portal). Stage 2: можем добавить «PIN strength» indicator + min-length recommendation.
  3. Brute-force protection scope — rate-limit 5 attempts → 30s cooldown (decision в плане). IP-based throttling + flag operator dashboard это acceptable для Stage 1; full WAF / IP block / CAPTCHA — Stage 2 if abuse occurs.
  4. What if Owner deletes PIN-protected project — cookies становятся orphan'ами (но безвредны — pin_<id> для несуществующего id просто игнорируется при routing'е). Не block'ит, документировать в Phase 1.10 routing spec.