- Обернуть renderClientHistory в try/catch — показывает текст ошибки
- Добавить .catch() в mount() для перехвата unhandled promise rejection
- Версия clients.js: 20260518c
- Добавить проверку data.error после fetchClients() в renderClientHistory
- Сравнивать client_tg_id как строки (String(c.client_tg_id) === String(clientKey))
чтобы избежать 5937498515 !== '5937498515'
- Показывать явное сообщение если клиент не найден вместо пустой страницы
- Версия clients.js: 20260518b
- Заголовок 'История подборов' → 'Карточка клиента'
- Убрать инлайн-монтирование Proposals.mountManager в карточке клиента
- Оставить только кнопку 'Открыть →' для перехода к подборам
- Версия clients.js: 20260518a
- lint_css.py: проверка контраста по WCAG 4.5:1 для HEX-цветов,
разделение ошибок/предупреждений, проверка против всех тем
- config.py: SHIPMENTS_FILE_ID обновлён на актуальный из AI АНАЛИТИКА
ARRIVALS_FILE_ID сброшен в пустой (ID пока не найден)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
.client-name и .client-phone → color: var(--card), текст сливается
с фоном карточки во всех темах. Аватар и счётчики подборов остаются видимыми.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- quickActions: «Новый клиент» → «Заказы» (clipboard → #/assembly),
убрали дублирующую кнопку «Сборки» из быстрых действий
- «Свободный день»: текст теперь использует var(--ink)/var(--muted) вместо
rgba белых значений, которые были невидимы на светлом фоне --card;
заголовок — шрифт карточки ×1.3 (17.5px 600), описание — моно uppercase 9.5px
- styles.css: добавлены явные стили .client-card/.client-name/.client-phone/
.client-avatar и др. — исправлен невидимый текст в карточках клиентов
во всех темах (Foundry, Boardroom, Atelier)
- splash: minShow 1200 → 840 мс (−30%)
- index.html: версия ресурсов → 20260517c
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Every "Назад" / "На главную" button was calling location.reload() which
triggered a full page reload → splash screen appeared again. Fix: replace
reload() with routeByHash() call (global router function from app.js) which
re-renders the role-appropriate home screen from cached window.__zovMe
without any network round-trips.
Affected files: app.js, clients.js, measurements.js, request.js,
assembly.js, podbor.js. Bump asset versions to 20260517b to bust cache.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Every /api/me call was reading the entire Users sheet via get_all_values(),
exhausting the 60 reads/minute quota under any load. Added a 90-second
TTL cache keyed by sheet name:
- _cached_get_all_values(): returns cached data or refreshes on miss/expiry
- _invalidate_cache(): called after every write (append_row, append_named_row,
update_cell_by_key) so stale data is never served after mutations
- Patched: find_row, update_cell_by_key, list_users_with_role
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracted from reference dashboards (computed styles, class structure, color usage):
B Foundry: r-card 0px, body lh 1.5, Archivo 800 display,
dark #15140F header (palette + greeting full-width),
wide 0.18em kicker tracking, heavy section labels
C Boardroom: r-card 0px, r-tag 999px (pills), body lh 1.12,
Geist 400 display (restrained), dark petrol header,
copper accent on greeting
D Atelier: body lh 1.1, Manrope 700 display, white card header
on dove bg, ink-bottom-border divider,
prominent uppercase section labels
Also: role-card border-radius switched to var(--r-card) from hardcoded 16px
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Manager: suit, lapels, tie, pocket square, face with hair/eyes/nose
Client: house with chimney+smoke, pane windows, paneled door, garden bush
Staff: dark hard hat, face with stubble+brows, work jacket, wrench in hand
All use feTurbulence displacement filter for hand-drawn line wobble
and diagonal hatching lines for shadow/volume.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_ensure_measurements_sheet() now:
1. Creates sheet with canonical headers if missing
2. Adds any missing columns
3. If column ORDER doesn't match _measurement_columns() — migrates all
data rows in-place: reads by column name, rewrites in canonical order
@app.on_event("startup") calls _ensure_measurements_sheet() via
asyncio.to_thread so column order is always corrected on deploy,
not just on first client_create.
This guarantees append_named_row() always finds columns in expected
positions, eliminating the silent data-in-wrong-column bug.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: _row_for_measurement() returned a positional list based on
_measurement_columns() order, but the actual Google Sheet may have
columns in a different order (if the sheet was created before new
columns were added and _ensure_measurements_sheet() appended them
at the end rather than in the middle). Values ended up in wrong
columns — client_name, manager_tg_id etc. were misaligned, so
_handle_clients couldn't match any rows and returned an empty list.
Fix:
- _row_for_measurement() now returns dict {col_name: value}
- sheets.append_named_row() reads real headers from the sheet and
builds the positional row accordingly — safe regardless of column order
- All three Measurements append calls updated to use append_named_row
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Premature `};` at line 207 closed the ZAMER_PICTS object early,
leaving wall1/wall2/etc. as orphan code — syntax error on load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fetchClients() was not sending initDataUnsafe, so on Telegram Desktop
(where initData can be unreliable) _handle_clients returned
{"error":"invalid_init_data"} and the frontend showed an empty list
instead of an error — making newly created clients appear missing.
- fetchClients(): add initDataUnsafe to request body (matches renderManagerHome pattern)
- renderList(): surface data.error explicitly instead of silent empty state
- _handle_client_create: gps_lat/gps_lng None → "" to avoid "None" strings in sheet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Manager: person with V-collar and tie (professional silhouette)
- Client: house outline with roof, door, windows and door knob
- Staff: hard-hat person silhouette with wrench in hand
- Bump cache version to 20260516g across all assets
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. _xlsx_auth_manager: возвращал (tg_id, user) при успехе → callers делали
`if err: return err` и возвращали dict пользователя вместо данных.
Исправлено: возвращает (tg_id, None) при успешной авторизации.
2. Promise.all с 4 запросами: ошибка Drive (сервис-аккаунт не добавлен к файлу)
роняла весь дашборд. Исправлено: складские запросы изолированы в отдельный
.then/.catch — дашборд замеров отрисовывается независимо.
3. Секции склада теперь появляются с задержкой (non-blocking), а не блокируют
отрисовку главного экрана.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Добавлен /api/arrivals для «Поступление заказов на склад СПб.xlsx»
(Drive ID захардкожен в ARRIVALS_FILE_ID, дефолт = 1kgrDEIGcVMFnSdZs1Y...)
- _parse_xlsx_groups() — единый парсер для обоих файлов:
* находит строку заголовков динамически (первая строка с «Товар»),
чтобы корректно работать с файлом «Поступление» (2 строки шапки перед хедером)
* пропускает разделители «Кухни» / «Дозаказы» внутри листа
* пропускает шаблонные пустые строки (Заказ/Дозаказ без данных)
- _xlsx_auth_manager() — вынесена общая проверка initData + роль
- config: поле arrivals_file_id
- frontend: вторая секция «📥 Поступление в СПб» на дашборде менеджера;
renderManagerShipments принимает label-параметр и переиспользуется для обоих файлов;
оба запроса загружаются параллельно с измерениями
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- backend: новый модуль drive.py (Google Drive download + 5-мин кэш)
- backend: /api/shipments — читает xlsx из Drive, парсит листы «ЗОВ ДД.ММ.ГГ»,
возвращает позиции (Заказ/Дозаказ) сгруппированные по дате отгрузки с завода
- config: поле shipments_file_id (SHIPMENTS_FILE_ID env; дефолт = ID ОТГРУЗКИ.xlsx)
- frontend: секция «📦 Отгрузки» на главной менеджера (после активных проектов),
загружается параллельно с замерами и pending; показывает последние 3 партии
- CSS: стили .ship-group / .ship-row / .ship-badge / .ship-check
- deps: добавлен openpyxl>=3.1.0
ВАЖНО после деплоя: добавить сервис-аккаунт как Viewer к ОТГРУЗКИ.xlsx в Drive
и прописать SHIPMENTS_FILE_ID в /opt/zov-tech/deploy/.env на сервере.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Убрана широкая кнопка btn-primary над поиском
- Добавлен FAB (56×56, position:fixed, bottom-right) — кружок с «+»
всегда виден поверх списка при любой прокрутке
- .client-list: padding-bottom 84px — последняя карточка не прячется
- Пустое состояние: обновлён текст («Нажмите + чтобы завести первого»)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Добавлены 4 детальных обучающих SVG в ZAMER_PICTS:
- wall1: фронтальный вид чистой стены — ширина L, высота H,
4 угла (ЛВ/ПВ/ЛН/ПН), дуга α° (замер угла), БАЗА: ПУ, Lc (середина)
- wall2: стена с дверным проёмом — общая L=A+Ш+B, высота двери В,
три сегмента ниже с цветовым разделением
- wall3: стена с окном + коммуникации — Ш_ОК/В_ОК/П (подоконник),
розетка R1 с двумя привязками (A горизонталь→ПУ, B вертикаль↑пол),
труба Wc1 с привязками C и D
- topview_comms: вид сверху с пунктирными линиями привязки точек
R1 и Wc1 к базовым углам стен
zamer-checklist.md: @pict:wall1 в разделе 2, @pict:wall3 в разделе 4,
@pict:wall2 в разделе 6, @pict:topview_comms в разделе 8
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: новый GET /api/daily_reminders (внутренний, Bearer-токен)
сканирует Measurements, находит клиентов у которых сегодня годовщина
contract_date (по МСК), дедуплицирует по manager+client_key
- Backend config: поле internal_secret (INTERNAL_SECRET)
- Bot: фоновая задача _anniversary_scheduler — каждый день в 09:00 МСК
вызывает эндпоинт и рассылает менеджерам HTML-сообщение с годовщиной
- Bot config: поля internal_secret + backend_url (BACKEND_URL)
- deploy/.env.example: добавлены INTERNAL_SECRET
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- backend: _handle_measurement_add_photos — дозагрузка фото (до 20 шт за раз)
сохраняет в PHOTOS_DIR, дописывает в колонку photos через update_cell_by_key
- clients.js: renderPhotoUploadBlock — выбор файлов, превью сетка 3 колонки,
конвертация в dataURL, POST /api/measurement_add_photos, счётчик в заголовке
- podbor.css: .photo-upload-block, .photo-preview-grid, .photo-upload-actions
- index.html: cache bump → v=20260514p
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- clients.js: renderList — поле поиска над списком, renderFiltered() фильтрует без API-запроса
- фильтрация: имя (подстрока), телефон (только цифры), номер договора
- счётчик обновляется: «Найдено N из M» при поиске, полная статистика без запроса
- podbor.css: .client-search-wrap, .client-search, .client-search-meta
- index.html: cache bump → v=20260514n
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- measurements.js: renderClientPicker — одно поле адреса заменено на addr-grid
- добавлен локальный _splitAddr() — разбирает строку адреса обратно в поля
- при выборе клиента из пикера адрес автоматически раскладывается по полям
- каждое поле собирает state.address через readAndSaveAddr()
- index.html: cache bump → v=20260514l
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- clients.js: new client + edit client forms get подъезд and этаж fields (optional)
- clients.js: address string includes подъезд/этаж when filled
- clients.js: splitAddress parses подъезд/этаж from stored address string
- clients.js: geocoder now checks result.kind — only shows ✓ for house/street precision;
locality/province match shows warning "улица не найдена" without saving coords
- podbor.css: addr-grid grows to 3 rows (город/улица, дом/кв, подъезд/этаж)
- index.html: cache bump → v=20260514j
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- measurements.js: replace manual name/phone inputs with client picker overlay
(search by ФИО or phone, sorted alphabetically, picks from /api/clients)
- clients.js: new-client and edit-client forms now use 4 split address fields
(город, улица, дом, кв./офис) with geocode validation on save
- clients.js: add splitAddress() helper to pre-fill edit form from stored address
- clients.js: voice engine refactored (continuous=false + auto-restart, no duplication)
- clients.js: note block view/edit toggle (textarea closes after save)
- podbor.css: styles for picker overlay, picker-row, chosen-card, addr-grid, geo-status
- backend main.py: _handle_client_create and _handle_client_update accept gps_lat/gps_lng
- index.html: cache bump → v=20260514i
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>