---
name: chets-transit-schedules
description: Generate and manage Ukrainian transit (CHETS) controller schedules — daily graphs, district assignments, pair formation, and HTML report generation from Excel data. Handles the specific domain logic of controller pairings by district within shifts.
---

# CHETS Transit Schedule Generator

## Domain Context

CHETS (Черкаси ЕлектроТранСпорт) is a Ukrainian trolleybus operator. This skill covers generating HTML schedule pages from Excel data sources.

## Key Concepts

- **Controllers (контролери)**: Ticket inspectors assigned to routes
- **Pairs (пари)**: Two controllers working together (defined in monthly schedule)
- **Districts (дільниці)**: Work areas numbered І, ІІ, ІІІ (shown with Roman numerals)
- **Shifts (зміни)**: Shift 1 (early) and Shift 2 (late)
- **Status codes**: `1`=shift1, `2`=shift2, `В`=day off, `ТН`=sick leave, `БЗ`=medical, `ОВ`=vacation, `дон`=duty

## File Structure

```
/tmp/schedule_site/
├── months/2026-05.html    # Monthly schedule (pairs, days, statuses)
├── daily/DD.MM.26.html     # Daily schedule pages
├── districts.html          # District assignment table
├── tickets/YYYY-MM.html    # Ticket counts per month
├── stt/DD.MM.26.html       # STT reports
└── vercel-project/         # Deployed site copy
```

## Data Sources

1. **Monthly schedule** (`months/2026-05.html`): Defines pair numbers, controller names, daily statuses
2. **Districts** (`districts.html`): District assignments per controller per day (days 1-19)
3. **Daily schedules** (`daily/*.html`): Generated pages with pair groupings and districts
4. **Excel files** (`Дільниці.xlsx`): Source data — parse with `zipfile` + `xml.etree.ElementTree`

## Excel Parsing Pattern

```python
import zipfile, xml.etree.ElementTree as ET, re

with zipfile.ZipFile(path, 'r') as z:
    with z.open('xl/sharedStrings.xml') as f:
        tree = ET.parse(f)
        root = tree.getroot()
        ns = {'s': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
        strings = [si.find('s:t', ns).text or '' for si in root.findall('s:si', ns)]
    with z.open('xl/worksheets/sheet1.xml') as f:
        tree = ET.parse(f)
        root = tree.getroot()
        rows = root.findall('.//s:sheetData/s:row', ns)

# Column A=pair_num, B=name, C=day1, D=day2, ...
# day_num = col_num - 2 (C=3, so day = 3-2 = 1)
```

## District Color Coding & CSS

| District | Class | BG Color | Text Color |
|----------|-------|----------|------------|
| І | `area-1` | #e8f5e9 | #166534 |
| ІІ | `area-2` | #fff7dc | #854d0e |
| ІІІ | `area-3` | #eef2ff | #3730a3 |
| В | `area-off` | #DCEAF5 | #1f4e79 |
| ТН | `area-tn` / `area-sick` | #FFF68B | #000 |
| БЗ | `area-bz` | #00B0F0 | #fff |
| дон | `area-duty` | #FFA500 | #000 |
| ОВ | `area-vac` | #333 | #fff |

Daily status-card visual convention: match the user-approved `01.07.26` sample. Only the header stripe (`h3`) is colored; the surname/list body is always clean white with black text (`color:#111!important`). Use: `Вихідний` header light gray `#e5e7eb`, `Лікарняний/ТН` header yellow `#FFF68B`, `Без збереження/БЗ` header pale blue `#DCEAF5`, `Відпустка/ОВ` header black `#333`, donor orange `#FFA500` only when at least one person exists. Empty values should show `—`, not `немає`/`Немає`. Avoid large full-card colored backgrounds behind surnames. When normalizing legacy May pages, append a final `!important` status-card CSS block and normalize both `<div class="status-card ...">` and `<section class="status-card ...">` wrappers so older duplicate CSS cannot override the final look.

```css
/* Pair separator */
.pair-separator td { border-top: 3px solid #1f2937 !important; }

/* Table cells */
.pair-cell { width: 54px; text-align: center; font-weight: 900; }
.name-cell { font-weight: 800; }
.area-cell { width: 28%; min-width: 100px; font-weight: 700; }
.pairno { width: 20px; }
.weekend-th { background: #f3f4f6; color: #6b7280; }

/* Status cards — user-approved daily style */
.status-card{background:#fff!important;color:#111!important;overflow:hidden!important;border-radius:8px!important}
.status-card h3{font-size:12px!important;text-align:center!important;padding:6px!important;line-height:1.05!important;font-weight:900!important;border-bottom:1px solid rgba(0,0,0,.12)!important}
.status-list{background:#fff!important;color:#111!important;list-style:none!important;padding:6px!important;display:flex!important;flex-wrap:wrap!important;gap:3px 8px!important;align-content:flex-start!important}
.status-list li{color:#111!important;background:#fff!important;border-bottom:0!important;padding:0!important;font-weight:800!important;font-size:10.5px!important;line-height:1.1!important;white-space:nowrap!important;text-align:left!important}
.status-card.gray{border:1px solid #d1d5db!important;background:#fff!important;color:#111!important}.status-card.gray h3{background:#e5e7eb!important;color:#111827!important}
.status-card.yellow{border:1px solid #e8d767!important;background:#fff!important;color:#111!important}.status-card.yellow h3{background:#FFF68B!important;color:#000!important}
.status-card.cyan{border:1px solid #b7cfe6!important;background:#fff!important;color:#111!important}.status-card.cyan h3{background:#DCEAF5!important;color:#1f4e79!important}
.status-card.black{border:1px solid #333!important;background:#fff!important;color:#111!important}.status-card.black h3{background:#333!important;color:#fff!important}
.status-card.orange{border:1px solid #FFA500!important;background:#fff!important;color:#111!important}.status-card.orange h3{background:#FFA500!important;color:#000!important}

/* Group header for districts.html: keep subtle so it doesn't dominate the table */
.group-header td {
  background: #dbeafe !important;
  color: #1e3a8a !important;
  text-align: left;
  font-weight: 800;
  font-size: 11px;
  padding: 1px 6px !important;
  height: 9px !important;
  line-height: 1.05;
}

/* Table header: inside the horizontally/vertically scrollable .table-wrap, keep top:0.
   Do NOT offset it for the sticky nav (e.g. top:48px), or the blue day header appears dropped/misaligned above the table. */
th { position: sticky; top: 0; background: #4472C4; color: white; z-index: 3; }
```

## Pair Formation Logic (Daily Schedule)

1. Group working controllers by shift (status `1` or `2`)
2. Within each shift, group by district
3. Form pairs of 2 from same-district group
4. If odd person out → attach to existing pair with same district (max 3 per pair)
5. District shown once per pair/trio. In daily HTML, use a single `area-cell area-N` with `rowspan="2"` or `rowspan="3"` so the colored district block visually covers everyone assigned to that district.

### Manual voice-dictated daily updates

When the user dictates a date and controller pairs/trios:

- Treat “перша/друга/третя” as district І/ІІ/ІІІ, not shift numbers.
- Normalize common ASR surname variants to the canonical names already present in the HTML table (e.g. Шраменко, Зозуля, Яремака, Ярошенко, Хорошковська, Пісоцька, Романенко, Мотайленко, Юрченко, Андрющенко, Турчанович).
- **No automatic cascade by default:** the user explicitly does not want linked graph/page changes unless they command them. If they name one specific page/date/graph, change only that target. Do not independently update daily/monthly/district pages, shifts, colors, or broad ranges just because they are “related.” Only sync linked pages when the user says “там теж”, “прив’язати”, “і в дільницях”, “і в щоденному графіку”, etc.
- Update the specific `daily/DD.MM.26.html` first, preserving shift headings and counts unless the user changes shift membership.
- For each pair/trio, render the district with `rowspan` across all people in that group.
- Special mixed-district trio visual: if the user asks for one trio but two district meanings in the same cell “по діагоналі” (e.g. `ІІ` yellow plus `І` green), use one `area-cell area-mixed-*` with `rowspan="3"`, a diagonal gradient split, a **white** diagonal divider (not black), and larger bold centered labels inside each colored half. In `districts.html`, represent this compactly as combined text like `ІІ/І` for the affected controllers/day.
- Sync `districts.html` only when explicitly requested for the same date/page.
- Verify by printing the resulting daily table rows and any explicitly requested synced districts column before deploying.


- Fixed top navigation menu: when applying site-wide sticky/fixed menu CSS, keep it visually centered at the top (`justify-content:center`, centered link text) while preserving horizontal scroll/wrap behavior on narrow screens. Verify at least one main page and `problems.html` after bulk CSS changes.

Daily schedule pages should use the 19.05-style header so the date and weekday do not visually merge. Avoid legacy `<div class="date-pill">DATE<span>weekday</span></div>` markup on generated/manual pages.

**Preferred header center:**
```html
<div class="day-hero">
  <div class="day-number-box">
    <span class="date-line">20 травня 2026 року</span>
    <span class="weekday-line">середа</span>
  </div>
</div>
```

Required CSS already used by known-good pages:
```css
.day-number-box{display:inline-flex;flex-direction:column;align-items:center;justify-content:center;min-width:210px;border-radius:999px;background:#eef3ff;color:#1f4e9e;text-align:center;padding:7px 18px;border:1px solid #c8d6f4}
.date-line{font-size:18px;line-height:1.15;font-weight:900}
.weekday-line{margin-top:2px;font-size:11px;font-weight:700;opacity:.78;text-transform:lowercase}
```

For daily nav labels, prefer full day-month labels like `← 19.05` and `21.05 →`, matching 19.05. After bulk regeneration or manual edits for daily pages, verify the header snippet for each changed file and check that `date-pill` is absent.

## Weekend Daily Status Layout

For weekend daily graphs, the user expects the off-duty/status section to match the earlier good weekend pages (e.g. previous weekend examples), not a tall vertical column. When updating weekend pages:

- Keep the working controllers table above, but make the status cards compact underneath.
- The `Вихідний` card should show all off-duty names flowing horizontally in wrapped rows (`display:flex; flex-wrap:wrap` on `.status-list` and `display:inline; white-space:nowrap` on list items), so it looks like a small compact table/list rather than one long column.
- Good compact CSS pattern:
```css
.status-grid{display:grid;grid-template-columns:1.4fr repeat(3,.72fr);gap:6px;align-items:start}
.status-card{border:1px solid #ccc;border-radius:7px;overflow:hidden;min-height:0;background:#fff}
.status-card h3{font-size:12px;text-align:center;padding:5px 6px;border-bottom:1px solid #ccc;background:#eee}
.status-list{list-style:none;padding:4px 6px}
.status-list{display:flex;flex-wrap:wrap;gap:2px 6px;align-content:flex-start}
.status-list li{border-bottom:0;padding:2px 0;font-weight:800;font-size:11px;line-height:1.12;text-align:center}
.status-list li{display:inline;border-bottom:0;padding:0;font-size:10.5px;line-height:1.08;white-space:nowrap}
```
- Use the final status-card CSS from **District Color Coding & CSS**. For older May files, look for duplicate legacy rules later in `<style>` such as `.status-card.gray{background:#DCEAF5...}` or `.status-card.blue{background:#00B0F0...}`; append the final `!important` block before `</style>` so the approved look wins.
- Normalize status-card classes from the visible title, not from whatever legacy class is present: `Вихідний` → `gray`, `Лікарняний/ТН` → `yellow`, `Без збереження/БЗ` → `cyan`, `Відпустка/ОВ` → `black`, `дон` → `orange`. Apply this to both `<div>` and `<section>` cards.
- Verify by comparing a May page and a scaffold page (e.g. `daily/15.05.26.html` and `daily/01.07.26.html`): card classes should be `gray/yellow/cyan/black`, final CSS should contain `#e5e7eb!important`, `#FFF68B!important`, `#DCEAF5!important`, `#333!important`, and list bodies should contain `background:#fff!important` plus black text.

## Daily Schedule Table Markup

Legibility preference for daily pages: controller surnames and district cells should be bold in all daily graphs (weekday and weekend). Keep/ensure CSS equivalent to `.name-cell{font-weight:900!important}` and `.area-cell{font-weight:900!important}` when editing or generating daily pages; validate CSS syntax after automated replacements so declarations do not get concatenated without semicolons.

Status/working-row conflict rule: if a controller has an absence/status in the source monthly graph or the user says they should be `БЗ`/`ТН`/`ОВ`/`дон` for that date, they must not also appear in the working наряд table. Remove them from the daily working pair/trio, leave them only in the appropriate status card, adjust the shift count/header, and update `districts.html` for that date to the status value rather than a district. If their former partner still works, pair/merge that partner according to the user's latest instruction and renumber groups.

When a user flags a controller as an absence/status on a daily page (e.g. “Савченко — БЗ, чому дублює і БЗ і наряд?”): treat the monthly graph/status as authoritative for that person/date. Remove that controller from the working shift table, keep them only in the matching status card, reduce the shift count in the `<h2>`, and sync the same date in `districts.html` to the status cell (`area-bz`, `area-tn`, `area-vac`, etc.) so they are not simultaneously shown as a district assignment. Verify by counting that the name appears once on the daily page (in status only) and is absent from all `.name-cell` working rows.

For explicit person + date-range status corrections like “Романенко з 22 по 25.05 — ТН”, update the whole CHETS surface for that month: `months/YYYY-MM.html`, the homepage/current month copy if applicable (`index.html`), `districts.html` or `districts/YYYY-MM.html`, `tab.html`/monthly tab page, and every affected `daily/DD.MM.26.html`. In monthly/index use status styling (`ТН` = `yellow`, weekend `weekend yellow`); in districts use `area area-tn` but keep the page’s plain status-cell visual rule; in tab use `<td class="status">ТН</td>`. Recompute controller totals and the DS summary row after replacing shift cells with statuses. In daily pages, remove the controller from any working pair/trio, decrement rowspans and shift counts, add them to the correct status card, update card counts, and verify they are absent from working `.name-cell` rows and present in the status card for every affected day.

Use this structure for manual or regenerated daily pages when pairs/trios are known:

```html
<tr class="pair-separator">
  <td class="pair-cell">4</td>
  <td class="name-cell">Битко І.В.</td>
  <td class="area-cell area-2" rowspan="3">ІІ дільниця</td>
</tr>
<tr><td class="pair-cell"></td><td class="name-cell">Ніколіч А.О.</td></tr>
<tr><td class="pair-cell"></td><td class="name-cell">Добшина Л.В.</td></tr>
```

- Use `rowspan` equal to the group size (2 or 3), not repeated empty area cells, when the user wants the district color to “cover” the whole pair/trio.
- Keep the first row of each new group marked with `pair-separator` except the first group in a shift.
- After manual daily edits, verify the rendered logical rows and deploy both the daily file and any synchronized `districts.html` changes.

## District Page (districts.html) Structure

When adding month scaffolds for the district section, create `districts/YYYY-MM.html` for every requested month (e.g. July–December), not just empty links. Build rows from the corresponding `months/YYYY-MM.html` controller names/order, keep only absence/status codes (`В`, `ТН`, `БЗ`, `дон`, `ОВ`) as visible status cells, and leave normal working `1`/`2` cells as blank `area-empty` cells for later І/ІІ/ІІІ entry. Generate the correct number of day columns (30/31) and weekend header classes. Update the month switcher on existing `districts.html`/`districts/YYYY-MM.html`, add the new files to `editor.html` `ALL_PAGES` for offline editing/cache, bump the service worker, deploy, and verify live pages for first/last scaffold month.

- Controllers grouped by monthly schedule pairs (2 per row)
- Columns: Пара | Працівник | Day 1 | ... | Day 31
- Do **not** insert blue group separator rows (`<tr class="group-header">…</tr>`) between controller pairs/groups; the user found those blue stripes visually noisy. If present, remove the rows and any `.group-header` CSS from `districts.html`.
- In `districts.html`, only district cells (`І`, `ІІ`, `ІІІ`) should be visually colored. All letter/status cells (`В`, `ТН`, `БЗ`, `ОВ`, `дон`, and similar non-district marks) should have a plain white background with muted gray text so they do not visually compete with district assignments. Keep district І/ІІ/ІІІ colors distinct and make their roman numerals large/bold enough to scan quickly.
- Districts from daily schedules (11-19 and manually dictated later days) replace shift numbers (1/2)
- Fall back to last known district for days 20-31
- Make district numerals more legible than status cells: `.area-1,.area-2,.area-3{font-size:15px!important;font-weight:950!important;letter-spacing:.2px}` while leaving status labels (`В`, `ТН`, `БЗ`, `ОВ`, `дон`) at the normal table size.
- In `districts.html`, non-district status cells must be visually plain: use one white/muted rule such as `.area-off,.area-sick,.area-tn,.area-bz,.area-duty,.area-vac{background:#fff!important;color:#6b7280!important}`. Do not use yellow/blue/black/orange status colors on this page; only І/ІІ/ІІІ stay colored.
- Districts from daily schedules (11-19) replace shift numbers (1/2)
- Fall back to last known district for days 20-31

## Daily Month Project Pages

When the user asks for “плитки” / “проєкт” for a new month’s daily graphs (e.g. June) and does not provide district assignments:

- Generate clickable tiles in `daily/YYYY-MM.html` for every day of the month; remove `href="#"` and `missing` from all day tiles.
- Create `daily/DD.MM.26.html` pages from the corresponding monthly graph (`months/YYYY-MM.html`), grouping workers by status `1`/`2` into І/ІІ shift tables and listing statuses (`В`, `ТН`, `БЗ`, `ОВ`, `дон`) in compact cards.
- Leave district cells blank/`—` and label the pages as project/draft; do not infer or fill districts unless the user explicitly gives them.
- Preserve monthly graph and `districts.html`; do not change them while creating daily project pages.
- Verify live month index has zero `href="#"`, one link per calendar day, and spot-check day pages (first/middle/last) contain `І зміна`, `ІІ зміна`, and a draft note.

### Binding daily pages to a month district table

When the user says “щоденні прив’яжи за <місяць>”, “подивися щоб щоденні працювали відповідно до цих дільниць”, or similar after filling `districts/YYYY-MM.html`, sync the daily pages for that month from the district table. Reusable helper: `scripts/sync_daily_from_districts.py`.

- Treat `districts/YYYY-MM.html` as the source for district cells only; do not change shift membership, status cards, monthly graph, or controller presence.
- For each `daily/DD.MM.26.html`, update each row with `<td class="name-cell">NAME</td>` and an adjacent `area-cell`:
  - `І` → `<td class="area-cell area-1">І дільниця</td>`
  - `ІІ` → `<td class="area-cell area-2">ІІ дільниця</td>`
  - `ІІІ` → `<td class="area-cell area-3">ІІІ дільниця</td>`
  - non-district/blank/status (`—`, `В`, `ОВ`, `ТН`, `БЗ`, `дон`) → keep or set the daily district cell to `<td class="area-cell">—</td>` rather than leaving stale colored districts.
- Apply to both `/opt/data/vercel-project` and `/tmp/schedule_site/vercel-project`, back up changed daily pages, and bump `service-worker.js` cache.
- Verify representative dates by extracting daily rows for recently dictated pairs (e.g. day 8/10/13/15/19) and comparing class/text to the district table. For bulk month syncs, run a full consistency scan across every daily row with an `area-cell`; expected result is `errors=0` before deploying. Then verify live daily pages plus the service worker version.

## Tab / Timesheet (`tab.html`, `tab/YYYY-MM.html`)

Use this when maintaining the `Табель` section.

- The May tab page is `tab.html`; later month tabs should exist as `tab/YYYY-MM.html` (e.g. `tab/2026-06.html` through `tab/2026-12.html`) and be linked from the month switcher.
- Build month tab pages from the matching `months/YYYY-MM.html` monthly graph: parse controller rows only, skip group/header/summary rows such as names containing `зміна` or `ДС`, sort controllers alphabetically, convert working cells `1`/`2` into visible `8` hour cells, and leave statuses (`В`, `ТН`, `БЗ`, `ОВ`, `дон`) on a plain white/muted background.
- Visual convention: `1`/first shift → red/pink `8` (`.eight1`, `.total-green` styling), `2`/second shift → blue `8` (`.eight2`, `.total-red` styling). Even if class names are legacy (`total-green`/`total-red`), the meaning shown to the user is **І зміна** and **ІІ зміна**.
- The two far-right totals headers must read exactly `І зміна` and `ІІ зміна` — **no** `8`, no `Червоні`, no `Сині` — because the visible `8` cells already make the hours obvious and the shorter headers fit mobile better. Use classes like `<th rowspan="2" class="shift-total-head shift-total-1">І зміна</th>` and `<th rowspan="2" class="shift-total-head shift-total-2">ІІ зміна</th>` with matching red/blue header colors.
- When applying a tab header/visual fix, apply it to **all** tab month pages from May through December, not only `tab.html`. Verify live `tab.html`, a middle month such as `tab/2026-07.html`, and `tab/2026-12.html`: the two headers should be `['І зміна', 'ІІ зміна']`, old labels absent, month switcher active state correct, and cache bumped.

## Monthly Schedule DS Summary Row

For monthly controller graphs like `months/2026-06.html`, the user may ask to restore/add the `ДС` row below the last controller (e.g. after `Юрченко Т.С.`). This row is a daily staffing summary, not a controller row.

Required behavior:
- Insert `<tr class="ds-summary">` immediately after the last controller row and before `</tbody>`.
- First cells: blank row number + `ДС` label.
- For each weekday column, count visible controller cells with exact text `1` and exact text `2` across controller rows only.
- Render `1` count in red and `2` count in blue in the same day cell, e.g. `<div class="shift-1">13</div><div class="shift-2">11</div>`.
- Leave weekend columns blank; do not count or display weekend totals.
- Do not include totals columns in the count; leave the DS total cells blank.
- Add CSS if missing:
```css
.ds-summary td{background:#fffdf5!important;font-weight:900!important;height:24px;border-top:3px solid #1f2937!important}
.ds-summary .name{background:#fff7dc!important;color:#111!important;text-align:center;font-weight:900!important}
.ds-summary .shift-1{color:#d92d20;font-weight:900;font-size:11px;line-height:1}
.ds-summary .shift-2{color:#2563eb;font-weight:900;font-size:11px;line-height:1;margin-top:1px}
.ds-summary .weekend{background:#EEF2F5!important;color:#94a3b8!important}
```
- Make insertion idempotent: remove any existing `<tr class="ds-summary">...</tr>` before rebuilding.
- Verify by printing weekday counts and confirming the summary row exists before deploying.

## Ticket Month Overview (`tickets/YYYY-MM.html`)

The user explicitly prefers the compact tile layout for the controller-ticket month page. Do not revert to centered day tiles.

**Required tile layout:**
- Month pages in the tickets section should be arranged as a real calendar grid, not a free-flowing responsive tile list: `.month-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));...}`.
- Add a weekday header row `Пн Вт Ср Чт Пт Сб Нд` and insert leading empty cells before day 1 according to `calendar.weekday(year, month, 1)` so the first day lands on the correct weekday. Apply this to every ticket month page, not just the current month, when the user says “всі місяці”.
- Preserve the approved May (`tickets/2026-05.html`) visual when correcting later months: if the user says May is ideal, do not touch May while making June–December match it.
- Date/day number and weekday should be blue (not red) and top-left; date slightly larger and bold.
- Controller surnames/count lines on tiles should be black, bold, compact, and close together (`.tile-people` / `.tile-person`).
- Daily total count should be a large red number only. Do **not** include `квиток/квитки/квитків` on the month tile; remove those words from June–December scaffold tiles so they match May.
- Use an empty `<div class="tile-people"></div>` in zero-count scaffold tiles if needed so layout positions remain May-like.
- Tile background should be light blue, with weekends a subtly different blue; tile container must be `position: relative`, enough right/bottom padding for the large count.

**Known-good CSS pattern:**
```css
.month-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:7px;margin-top:10px}
.day-tile{display:flex;min-height:78px;border:2px solid #93c5fd;border-radius:12px;background:#dbeafe;text-decoration:none;color:#1f2937;box-shadow:0 2px 7px rgba(30,64,175,.12);padding:7px 8px;position:relative;overflow:hidden}
.day-tile.weekend{background:#cfe8ff;border-color:#7fb7ee}
.tile-date{font-size:17px;font-weight:950;position:absolute;top:5px;left:8px;color:#1e40af;line-height:1}
.tile-weekday{font-size:10px;color:#1e40af;position:absolute;top:22px;left:8px;font-weight:900}
.tile-people{display:flex;flex-direction:column;gap:0;margin-top:34px;margin-right:45px;font-size:10px;font-weight:950;color:#111827;line-height:1.05;overflow:hidden}
.tile-person{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#111827;font-weight:950;line-height:1.05}
.tile-count{font-size:27px;font-weight:950;color:#d92d20;position:absolute;bottom:5px;right:8px;line-height:1;text-align:right}
```

**Monthly summary compacting:** If the user says the “підсумок за місяць” takes too much space, keep the summary but narrow it (`max-width` around `760px`, centered), reduce padding/row height, hide explanatory `.summary-note`, make table cells very bold, and apply the same CSS to `tickets/YYYY-MM.html` and `tickets/YYYY-MM-summary.html` for every scaffolded month.

**Deployment-source pitfall:** a correct version may exist in `/tmp/schedule_site/vercel-project/` while `/opt/data/vercel-project/` (the deploy directory) is stale. Before deploying ticket layout fixes, compare/copy the correct `tickets/YYYY-MM.html` into `/opt/data/vercel-project/tickets/` and verify the CSS contains `position:relative`, `.tile-people`, and large `.tile-count`.

### Ticket reports from Gmail / `Zvit shtrafy` ZIPs

Use this when the user uploads `Gmail.zip` or files named like `DD.MM.2026 Zvit shtrafy KP Chets.xlsx` and asks to fill the “Квиток контролера” tiles.

- Unzip the archive and process only `Zvit shtrafy` XLSX files; ignore `.eml` files for tile totals.
- These XLSX files may declare worksheet dimension `A1`, so `openpyxl` can show an empty 1×1 sheet. Parse `xl/worksheets/sheet1.xml` directly with `zipfile` + `xml.etree.ElementTree`, reading `inlineStr` text.
- Relevant columns in sheet XML:
  - `B` = row number (`№ з/п`) or `Всього:` marker
  - `D` = date (often only first row of a date block)
  - `E` = payment time (may be blank on some rows)
  - `F` = controller full name (`Прізвище Імʼя`)
  - `G` = validator serial
  - `L` = card/cashless operation count
  - `M` = QR operation count
- For each data row where `B` is numeric and `F` is present, count `int(L)+int(M)`; if both are zero/blank, fall back to `1` operation. Aggregate by surname for the tile. Normalize known spelling variants for display (e.g. `Бітко` → `Битко`).
- If a `Zvit shtrafy` XLSX contains only headers/summary and no numeric data rows (for example `Всього: 0`), do **not** present it as imported ticket data. Keep the day at `0`, optionally show a clear note on the daily page that the source file is empty, and make the 08:00 checker report it as `файл є, але порожній`, because the user expects missing/empty ticket files to be flagged.
- Update both the month tile page `tickets/YYYY-MM.html` and the linked daily pages `tickets/DD.MM.26.html` for the affected days. The tile should show compact `tile-person` lines (`Прізвище N`) and the large daily total. The daily page should show cards per controller with ticket count and available source details (operation count, times if present).
- Recompute the month summary from the visible tile data after updates so totals do not duplicate old full-name rows.
- Write both `/opt/data/vercel-project` and `/tmp/schedule_site/vercel-project`, deploy, then verify live month tiles for each imported date and at least one linked day page.

## STT Reports (`stt.html` and `stt/DD.MM.26.html`)

Use this when the user uploads SmartTicket files named like `DD.MM.2026 Zvit kontrol KP CHETS.xlsx` and asks to put reports into the STT section.

- STT index tile rule (FINAL approved external appearance): the `stt.html` month tiles must be visually clean and grouped by month sections, not one flat May-only grid. For the 2026 site, include month sections from `Травень 2026` through `Грудень 2026`. On every tile show only the blue `DD.MM` date label in the upper-left (for example `04.05`, `20.06`, `04.07`). If the linked `stt/DD.MM.26.html` page contains real uploaded/report table data, also show exactly red **`СТТ`** in the lower-right. Do **not** show controller names, surnames, counts, `контр.`, `перев.`, short-duration alerts, weekday text, `готово`, or `заготовка` on the external tile. Placeholder/empty days show only the `DD.MM` date and no marker. Clicking a tile opens the internal report table, where all details remain available. When regenerating `stt.html`, derive readiness from real rows inside the linked page, not merely from placeholder file existence. Keep opened report pages/table styling unchanged.
- STT pages must keep the global compact install shortcut style (`Кнопка`) consistent with the homepage. When regenerating `stt.html` or any `stt/DD.MM.26.html`, include the compact `.install-floating` CSS (`top:44px`, `right:10px`, small 17–18px trolleybus image, `font-size:9px`) so the button does not become a large trolleybus on the left/top of the STT page. Patch both existing STT HTML pages and `scripts/generate_stt_reports.py` so the fix survives future report generation.
- Opened STT report pages (`stt/DD.MM.26.html`) must include internal previous/next navigation like the known-good 20–21.05 pages: a `.day-nav` block after the date hero with `← попередній звіт` and `наступний звіт →`. Apply it from 22.05 onward and across month boundaries (e.g. `31.05 → 01.06`, `01.06 ← 31.05`) for all scaffolded future days/months, not only days with real report data. Also patch `scripts/generate_stt_reports.py` and the skill copy so newly generated STT pages keep the arrows.

STT duration alert rule: duration analysis and row coloring belong inside the opened report pages (`stt/DD.MM.26.html`) only.

Important STT parser pitfall: old pages may have table rows like `<td class="time stt-duration-cell">07:59:00</td>` or durations with seconds. Do **not** use regex that expects `class="stt-duration-cell"` exactly, and do not trust the stats header alone. A tile is ready when the linked page has at least one real row with a `.stt-name-cell` and a `.stt-duration-cell` duration in `H:MM`, `HH:MM`, or `HH:MM:SS` format. This fixes cases where 12.05, 13.05, and 20.05 had internal tables but the month tiles showed `заготовка` because the parser missed old class order/seconds format. Verify `stt.html` by checking all real-data days have `<span class="tile-status">СТТ</span>` and placeholders have none. Reusable helper: `scripts/mark_stt_near_full_shift_alerts.py`.

Reusable script: `scripts/generate_stt_reports.py`

```bash
python3 /opt/data/skills/software-development/chets-transit-schedules/scripts/generate_stt_reports.py \
  --root /opt/data/vercel-project \
  --mirror /tmp/schedule_site/vercel-project \
  /path/to/15.05.2026\ Zvit\ kontrol\ KP\ CHETS.xlsx \
  /path/to/16.05.2026\ Zvit\ kontrol\ KP\ CHETS.xlsx
```

Automatic import: keep `/opt/data/vercel-project/scripts/auto_import_stt_reports.py` and `/opt/data/scripts/chets_stt_auto_import.py` in sync with the generator. The cron job `CHETS STT auto import and highlight` runs every 30 minutes and should stay silent when no valid new files are found. It scans `assets/stt-reports`, `/opt/data/cache/processed_documents/chets`, and `/opt/data/cache/documents`; validates candidate `Zvit kontrol` XLSX files by parsing real rows (do not trust filename alone, because empty/date-only files can overwrite good reports); runs the generator with `--root /opt/data/vercel-project --mirror /tmp/schedule_site/vercel-project`; bumps `service-worker.js`; and force-deploys. If you change STT duration CSS or parsing, patch both the skill copy of `generate_stt_reports.py` and the deployed copy under `vercel-project/scripts/`, then run `python3 -m py_compile` on both.

Behavior:
- Parses `Zvit kontrol` files only. Columns: B=№, C=Валідатор, D=Контролер, E=Дата перевірки та час, F=Кількість перевірок успішних, G=Кількість перевірок не успішних.
- Generates one `stt/DD.MM.26.html` page per file with: controller, first check, last check, duration from first to last, ✅ successful, ❌ unsuccessful.
- Generated report rows must include duration classes and cell classes at creation time, not as a later manual cleanup: `stt-under8`, `stt-under7`, `stt-under6`, `stt-under5`, plus `stt-name-cell` and `stt-duration-cell`. Use thresholds `<8h`, `<7h`, `<6h`, `<5h`; `<5h` is bright red with black text.
- Updates `stt.html` so the day appears as a starred/ready report link.
- Writes both deploy source (`/opt/data/vercel-project`) and mirror (`/tmp/schedule_site/vercel-project`) when `--mirror` is supplied.
- After generation, deploy with the Vercel command below and verify live `stt.html` plus each report URL.

- Month index tiles (`stt.html`) should show the red `СТТ` marker only for days whose linked `stt/DD.MM.26.html` report actually contains data. Empty scaffold pages that contain `Даних поки немає` or `<div class="empty">` are not ready reports: remove `has-report` and remove the `СТТ` marker. Do **not** label empty tiles as `заготовка`; empty external tiles show only the `DD.MM` date number. Verify by scanning linked STT pages and comparing `tile-status`/`СТТ` count to real non-empty reports before deploy.
- When expanding the STT index to future months, create empty placeholder pages for every day (`stt/DD.MM.26.html`) so tiles never lead to 404, but ensure those placeholder pages contain `Даних поки немає` so `collect_existing_meta()` does not treat them as ready. Add these new STT pages to `service-worker.js` `CORE_ASSETS`, bump the cache version, deploy with `--force`, then verify live `stt.html`, one future placeholder such as `stt/04.06.26.html`, one real report such as `stt/20.05.26.html`, and the service-worker version.

Pitfalls:
- SmartTicket XLSX files may declare worksheet dimension `A1`, causing `openpyxl` read-only mode to show `max_row=1` even when rows exist. The script parses `xl/worksheets/sheet1.xml` directly and reads `inlineStr` cells; do not trust workbook dimensions for these files.
- Duration should match existing CHETS STT pages: round up to the next full minute when seconds remain (e.g. 09:00:21→17:31:38 becomes `08:32`).
- Keep machine/report totals grounded in F+G sums; the source row count should match total checks for these files.

## Problems Page (`problems.html`)

Use this when the user gives “проблеми за DD.MM” followed by machine numbers.

- Page structure is a calendar: `<article class="day-tile" data-day="D">` containing `<div class="problem-lines">` rows.
- The user wants compact problem day tiles so more days fit on screen: smaller tile height/padding, smaller machine rows, but keep numbers readable.
- Empty problem tile rows should remain visually blank: do not show placeholder labels like `№` or `машина` in empty cells. Preserve blank dashed/input-like slots for future manual entry.
- Empty problem tile rows should remain visually blank. For filled rows, the latest user preference is a red machine-number block with white text (`.problem-row.filled .machine-slot{background:#d92d20;color:#fff}`) while the order number remains subtle on white. This supersedes the earlier white-background/red-text preference when the user asks for “білим по червоному”.
- The monthly summary under the problem day tiles should be visually larger than the tiles and arranged cleanly in 5 columns on desktop, with responsive 3/2 columns on narrow screens to avoid overlap. For May, the heading should read `Підсумок за травень` (not `Підсумок проблем...`), and day references should be labeled `дата:` (not `дні:`). Aggregate every non-placeholder machine number from that month, show `machine — count` with the machine number in red and the count in blue, plus small date references. Rebuild the summary from the visible tile data after edits so counts stay grounded.
- The problems page must also have an obvious daily digest/зведення, not only the per-machine monthly aggregate. When the user asks “де зведка?”, add/update a block before the monthly summary such as `Зведення по днях — <місяць>` containing: total visible rows by day, count of unique machines, count of days with problems, a grouped list of days without entered problems, top frequent machines, and one compact card per calendar day with date/weekday/count/machine list. Include a separate approved/headline total only when the user explicitly supplied one for this workflow; otherwise the visible per-day sum is the total and stale unsupported headline values must be removed.
- Daily digest stat meaning: `Внесено по днях` is the sum of currently visible per-day problem rows/cards for that month only and may be the only official total when the user says the month total is the sum by days. Do **not** invent or preserve a separate `Затверджений підсумок` value unless the user explicitly provided that separate approved headline total in the current problem workflow. If the user asks “звідки число N?” about this block, explain it as the per-day visible sum and verify the value by summing the month’s `.daily-count span` values or visible filled rows before answering. If a stale/unsupported `Затверджений підсумок` appears (e.g. 183 for May when the user confirms total is 115), remove it.
- If the user explicitly dictates a headline total such as “150 проблем” / “151”, add or update a compact banner above the summary grid like `Загальна кількість проблем: 151`. Do not append a redundant trailing word after the number if they ask to remove it; keep the visible text short. When appending new problem machines, increase this headline total by the number of newly added rows only if that headline is known to be user-approved/explicit for the current month. If a headline total conflicts with the recalculated visible day sum and the user says the total is the sum by days, replace/remove the stale headline rather than carrying it forward. Rebuild the machine summary from tiles after edits and verify all displayed totals from the same source.
- If the user says “додати/добавити” problems for a day that already has entries, or simply gives machine numbers plus a date (e.g. `372, 312, 389 проблеми 21.05`), append the new machine numbers after the existing ones, preserve existing order, avoid duplicates if the same number is already present, and renumber sequentially. Treat later explicit duplicate-avoidance phrases (“якщо повторюється — не став”, “не дублюй”, “якщо такого номера немає — додати”) as append-unique, not replacement. User confirmed this 22.05 workflow is correct: check the existing day tile first, add only missing machine numbers, do not duplicate repeated numbers, increment the approved headline total only by the number of newly added rows, rebuild the month summary, bump service-worker cache, deploy, and verify live with cache-busting.
- After editing a problem day, rebuild the relevant month’s summary from only that month’s `<article>` tiles (not the whole year/scaffold months). Verify the summary rows for newly added machines include the new date and counts before deploying.
- **Month-summary parsing pitfall:** `problems.html` contains many month sections and each month has its own `problem-summary-grid`; a broad regex ending at `footer` or the last summary can accidentally preserve/rebuild the wrong month summary or miss the newly edited day. When editing May, delimit from `Травень 2026` to `<div class="problem-summary"><h3>Підсумок за травень` for source tiles, and replace only that summary block up to the next `</section><section`. Preserve the approved headline total by incrementing it when appending new rows; rebuild machine rows from visible May tiles so counts like `397 — дата: ...22` include the edited day.
- Deployment-source pitfall: `/tmp/schedule_site/vercel-project/problems.html` can be stale. Prefer updating/verifying `/opt/data/vercel-project/problems.html` first, then copy that corrected file to the mirror path before deploy so old mirror content does not overwrite or lose existing entries.
- If the user gives a fresh replacement list without “add/append”, replace that day’s placeholder/problem rows with one row per dictated machine, preserving dictated order and numbering from 1:
```html
<div class="problem-row filled"><span class="order-slot">1</span><span class="machine-slot">392</span></div>
```
- If the user says a machine entry was only a test/probe (`проба`, `пробно`, `тест`) and asks to remove it, restore that day to the standard three empty placeholder rows (`№` / `машина`) and ensure there are no `.problem-row filled` rows for that day.
- Keep machine numbers exactly as dictated, including 4-digit values like `2035` and strings like `У03` if present.
- Update both `/opt/data/vercel-project/problems.html` (deploy source) and `/tmp/schedule_site/vercel-project/problems.html` (mirror), deploy, then verify the live page by extracting the `data-day` article and printing `(order, machine)` pairs (or `filled_count=0` when clearing a test entry).


## STT Reports (`stt.html`, `stt/DD.MM.26.html`)

Use this when the user sends `Zvit kontrol KP CHETS.xlsx` and asks to add daily STT reports.

- The XLSX files exported by SmartTicket may store all values as inline strings in `xl/worksheets/sheet1.xml`; `openpyxl` can report `max_row=1` and miss the data. Parse sheet XML directly when needed.
- Columns in the report: B=№, C=Валідатор, D=Контролер, E=Дата перевірки та час, F=Кількість перевірок успішних, G=Кількість перевірок не успішних.
- Group by controller; for each controller compute first check, last check, duration from first to last, sum of successful and unsuccessful checks. Sort by first check time.
- Duration should match existing STT pages: round up to the next full minute when seconds remain, then display `HH:MM`.
- Generate/update `stt/DD.MM.26.html` and `stt.html` index. In each report table, classify duration rows (`stt-under8`, `stt-under6`, `stt-under5`); `<5h` is approved as bright red background with black text, not black/yellow. Verify live index rows and per-day stats (`контролерів`, `усього перевірок`, `успішних`, `не успішних`) after deploy.
- Reusable implementation details: `references/stt-report-generation.md`.

## Vacations / Absences Page (`vacations.html`)

- The user wants this page to stay as the yearly vacation overview with month tiles (січень–грудень) and surname rows. Do **not** replace the page with a one-month absence/status report. If a recalculation is needed, preserve the existing month-card layout and update/add rows inside the relevant month tile(s) plus the yearly surname summary.
- For May specifically, use the active homepage monthly graph (`index.html`, visible as “Графік на місяць — Травень”) as the source of truth, not a duplicate/stale `months/2026-05.html` or an editor draft. If both files exist, ensure `months/2026-05.html` is synchronized to `index.html` before using it.

When the user asks to “перерахувати від місячного графіка” відпустки/лікарняні/без збереження:

- Use the monthly graph as source of truth; do not infer from daily/district pages.
- For May 2026 specifically, treat the homepage `/index.html` (“Графік на місяць” / visible main May graph) as the canonical May monthly graph. If `months/2026-05.html` diverges or appears as a duplicate/older May page, sync it to the homepage rather than using it as a separate source.
- Count only explicit absence/status cells from the monthly schedule: `ОВ`, `ТН`, `БЗ`, `дон`. Do not count `В` or shifts `1/2`.
- Count only explicit absence/status cells from the monthly schedule: `ОВ`, `ТН`, `БЗ`, `дон`. Do not count `В` or shifts `1/2`.
- Preserve the yearly month-card page: update/add rows inside the relevant month tile(s) and update the yearly surname summary. Do not replace `vacations.html` with a separate one-month status report unless the user explicitly requests a new page.
- Verify named edge cases from the user (e.g. Савченко БЗ 4 days; Касьяненко ТН/ОВ/БЗ) before deploy.
- If a temporary replacement breaks the yearly month-tile page, restore `vacations.html` from the latest archive (`assets/archives/schedule-site_daily.zip` or stable archive) and verify: `months-grid` exists, all 12 month names are present, `Підсумок по прізвищах` exists, and the named row such as `Савченко С.П. — 4 — без збереження` is present before deploy.

## PWA / link-preview / shortcut branding

Use this when the user asks to rename the site “ярлик”, installed app, Telegram/link preview, the install button, or the title shown above the trolleybus image. Header logos/images should stay proportional with `object-fit:contain`; do **not** horizontally stretch or vertically squash the left/right Черкаси banner images even if trying to make a long strip — the user rejected that look.

### Public page copy cleanup

When the user asks to remove “пояснення”, “написи”, “підписи”, or says “ми і так знаємо / не треба писати”, simplify the public site copy aggressively but surgically:
- Keep section headers and functional labels only (e.g. `Місячний графік — Травень 2026`, `Щоденні графіки`, `Квиток контролера — Травень 2026`, `Табель`, `Потреби`).
- Remove helper/subtitle text like `Дані перенесені з Excel`, `оновлено з Excel`, `Плитки відкривають...`, `На плитці — дата...`, `Контролери відсортовані...`, `Пари як у місячному графіку...`, `Загальний список...`, `Як користуватись...`, `30 рядків...`, and similar explanatory notes.
- Preserve useful content blocks the user approves (e.g. problems зведення/підсумок) and data tables/tiles; only remove prose explanations, not the actual records.
- Apply to both deploy root and mirror, bump `service-worker.js`, deploy, and verify live with cache-busting that the removed phrases are absent.

Layout preferences from CHETS site feedback:
- Current approved mobile install control: the floating install shortcut should be a **small neat button labeled `Кнопка`**, not the larger `Встановити`. It should sit between the fixed top menu and the Черкаси/header-logo strip (roughly `top:44px; right:10px`), about half the previous size (`font-size:9px`, small trolleybus icon around `17–18px × 11–12px`), so it is above the word/strip “Черкаси” and does not crowd the menu. Popup should open just below it (around `top:68px`). Use a high z-index (`9000+`) and keep `.nav{padding-right:0}` so the menu layout is not squeezed. Apply this consistently to generated STT index pages and every `stt/DD.MM.26.html` report page as well as normal site pages; patch `scripts/generate_stt_reports.py` so future STT regeneration does not reintroduce the large/left-shifted trolleybus button. Verify live `/`, `/stt.html`, at least one `stt/DD.MM.26.html`, and `/problems.html` show `<span>Кнопка</span>`, no `<span>Встановити</span>`, and the compact CSS (`top:44px`, `font-size:9px`).
- If the user says the install/download button disappeared, restore the visible control, but keep the current compact `Кнопка` style unless they explicitly ask for the old large top-right version.ip “Черкаси” and does not crowd the menu. Popup should open just below it (around `top:68px`). Use a high z-index (`9000+`) and keep `.nav{padding-right:0}` so the menu layout is not squeezed. Apply this consistently to generated STT index pages and every `stt/DD.MM.26.html` report page as well as normal site pages; patch `scripts/generate_stt_reports.py` so future STT regeneration does not reintroduce the large/left-shifted trolleybus button. Verify live `/`, `/stt.html`, at least one `stt/DD.MM.26.html`, and `/problems.html` show `<span>Кнопка</span>`, no `<span>Встановити</span>`, and the compact CSS (`top:44px`, `font-size:9px`).
- If the user says the install/download button disappeared, restore the visible control, but keep the current compact `Кнопка` style unless they explicitly ask for the old large top-right version.
- If the install button appears oversized or shifted on one section (notably `stt.html` or generated `stt/DD.MM.26.html` report pages), patch both the visible HTML pages and the generator that creates them (e.g. `scripts/generate_stt_reports.py` plus the skill copy) with the compact `.install-floating` CSS (`top:44px`, `right:10px`, `font-size:9px`, tiny trolleybus image). Verify every STT page containing `<span>Кнопка</span>` also contains the compact rule so the next auto-generated report does not reintroduce the large button.
- If the install button disappears entirely, add/restore a visible `<a href="/install.html" class="install-floating">…Кнопка…</a>` or existing `button#quickInstallBtn`, but keep the compact between-menu-and-logo placement above; do not revert to a big top-right menu-level pill unless the user explicitly asks.
- Header logos should read as a wide black-and-white strip: left city image, modestly larger center trolleybus, and right “Черкаси — найкраще місто” image. Safe adjustment is to increase left/right `max-width` while preserving aspect ratio with `object-fit:contain`; do **not** squeeze vertically or use `object-fit:fill` on the side images unless the user explicitly accepts that look. The user rejected the squeezed/stretched “Черкаси” look; if they say the pictures are ugly/wrong, restore the previous contain-based sizes (known-good pattern: left about `max-width:390px;max-height:54px`, center about `180px/72px`, right about `442px/54px`) and keep the install button/STT changes intact.

Update all branding surfaces, not only visible page text:
- `manifest.json`: `name`, `short_name`, `description`; keep icon entries valid.
- HTML metadata: replace every `<meta name="apple-mobile-web-app-title" ...>` across the site.
- Homepage/link preview: update `index.html` `<title>` and the visible `<h1>`. Also add/update explicit OpenGraph/Twitter tags near the head metadata so Telegram/link previews do not depend on stale title inference:
  - `<meta property="og:title" content="...">`
  - `<meta property="og:description" content="...">`
  - `<meta property="og:image" content="https://vercel-project-xi-five.vercel.app/assets/app-icon-512.png">` (or the requested trolleybus image)
  - `<meta property="og:url" content="https://vercel-project-xi-five.vercel.app/">`
  - `<meta name="twitter:card" content="summary_large_image">`
- Telegram caches previews aggressively. After deploy, verify the raw live HTML has the new title/OG tags; if Telegram still shows the old title in a chat, it may be preview cache rather than site state. A cache-busting URL like `/?v=NN` can force a fresh preview when sharing.
- App icons: update `assets/app-icon.svg` and regenerate `assets/app-icon-192.png` + `assets/app-icon-512.png` when the user wants text drawn on the shortcut icon. A reliable fallback is `ffmpeg` with `drawtext` over `assets/trolleybus-center.jpg` (e.g. black `Контролери` / `КП ЧЕТС` above the trolleybus).
- Bump `service-worker.js` cache (`chets-schedule-vNN`) so old shortcut/manifest/icon assets are refreshed.
- Apply the same changes to both `/opt/data/vercel-project` and `/tmp/schedule_site/vercel-project`.
- Verify before deploy: `python3 -m json.tool manifest.json`, `node --check service-worker.js`, extract live/local `<title>`, `<h1>`, and `og:title`, check manifest `name/short_name`, and confirm the PNG icon has a valid PNG signature. If possible, visually inspect the generated icon.
- After deploy, verify live `/`, `/manifest.json`, `/service-worker.js`, and `/assets/app-icon-512.png`.

  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:title" content="Контролери КП ЧЕТС">
  <meta name="twitter:image" content="https://vercel-project-xi-five.vercel.app/assets/app-icon-512.png">
  ```
  After deploy, verify these tags live. If Telegram still shows the old title/image in an existing chat preview, explain that Telegram caches previews; the site can be correct while Telegram keeps an old cached card until reposted/refreshed.
- App icons: update `assets/app-icon.svg` and regenerate `assets/app-icon-192.png` + `assets/app-icon-512.png` when the user wants text drawn on the shortcut icon. A reliable fallback is `ffmpeg` with `drawtext` over `assets/trolleybus-center.jpg` (e.g. black `Контролери` / `КП ЧЕТС` above the trolleybus).
- Bump `service-worker.js` cache (`chets-schedule-vNN`) so old shortcut/manifest/icon assets are refreshed.
- Apply the same changes to both `/opt/data/vercel-project` and `/tmp/schedule_site/vercel-project`.
- Verify before deploy: `python3 -m json.tool manifest.json`, `node --check service-worker.js`, extract live/local `<title>`, `<h1>`, and `og:title`, check manifest `name/short_name`, and confirm the PNG icon has a valid PNG signature. If possible, visually inspect the generated icon.
- After deploy, verify live `/`, `/manifest.json`, `/service-worker.js`, and `/assets/app-icon-512.png`.

## Vercel Deployment

```bash
export PATH="/opt/data/.local/bin:$PATH"
cd /opt/data/vercel-project && vercel deploy --prod
```

If live pages show stale generated HTML even after a successful deploy (for example, local `daily/2026-06.html` has real links but live still has `href="#"`, or a daily page title belongs to the wrong month), do a forced deployment from `/opt/data/vercel-project`:
```bash
cd /opt/data/vercel-project && vercel deploy --prod --force
```
Then verify with cache-busting query strings (`?v=$(date +%s)`) against the canonical alias, not only the deployment URL. This captures the fix pattern; do not assume files disappeared until local project files and live no-cache responses are compared.

Use the configured Vercel authentication for the environment (CLI login or environment-provided token). Do not write raw deployment tokens into skills, handoff docs, or user-facing replies; redact secrets as `[REDACTED]`.
If `vercel` not found: `npm install -g vercel --prefix /opt/data/.local`

**Deployed URL**: https://vercel-project-xi-five.vercel.app

Deployment/cache pitfall: if live pages show stale content after a normal deploy (e.g. month index still has `href="#"`, daily page title does not match the local file, or Vercel uploads only a tiny delta), run a forced production deploy and verify with a cache-busting query:

```bash
cd /opt/data/vercel-project
export PATH="/opt/data/.local/bin:$PATH"
vercel deploy --prod --force
python3 - <<'PY'
import urllib.request, re, time
base='https://vercel-project-xi-five.vercel.app'
for rel in ['daily/2026-06.html','daily/22.06.26.html','service-worker.js']:
    s=urllib.request.urlopen(base+'/'+rel+'?v='+str(int(time.time())), timeout=20).read().decode('utf-8','ignore')
    print(rel, re.search(r'<title>(.*?)</title>', s, re.S).group(1) if '<title>' in s else re.search(r'chets-schedule-v\\d+', s).group(0), 'href#', s.count('href="#"'))
PY
```

When fixing stale/offline visible pages, also bump `service-worker.js` cache (`chets-schedule-vNN`) and redeploy so installed/PWA users refresh cached HTML.

## Extracting Districts from Daily HTML Schedules

Daily schedule HTML files (`daily/DD.MM.26.html`) contain district assignments in **two different formats**:

### Format A — Days 11-14 (simple)
```html
<td class="name-cell">Прізвище</td>
<td class="area-cell">І</td>          <!-- direct text: І, ІІ, or ІІІ -->
```
Parse with regex: `<td class="name-cell">([^<]+)</td>\s*<td class="area-cell">([^<]*)</td>`

### Format B — Days 15+ (class-based with pair column, legacy empty district cells)
```html
<td class="pair-cell">1</td>
<td class="name-cell">Прізвище</td>
<td class="area-cell area-1">І дільниця</td>   <!-- only FIRST row of pair has text -->
...
<td class="pair-cell"></td>
<td class="name-cell">Інший</td>
<td class="area-cell area-1"></td>             <!-- second in pair: empty text, same class -->
```

### Format C — Preferred manual daily format (rowspan covers pair/trio)
```html
<tr><td class="pair-cell">1</td><td class="name-cell">Перший</td><td class="area-cell area-2" rowspan="2">ІІ дільниця</td></tr>
<tr><td class="pair-cell"></td><td class="name-cell">Другий</td></tr>
```

For Format C, propagate the district from the rowspanned area cell to the next `rowspan-1` controller rows.

**Critical**: District text appears only in the first row of each pair/trio. In legacy Format B, subsequent rows may have empty `area-cell` text; in preferred Format C, subsequent rows have no area cell because the first row uses `rowspan`. Propagate the district from the first row to all rows in the group.

Parse by splitting into shift sections first (`<h2>...</h2><table>...</table>`), then extracting rows with regex that captures `pair-cell`, `name-cell`, and `area-cell` classes.

### District Normalization
```python
DISTRICT_MAP = {
    "І дільниця": "І", "ІІ дільниця": "ІІ", "ІІІ дільниця": "ІІІ",
    "І": "І", "ІІ": "ІІ", "ІІІ": "ІІІ",
}
# Also map area-class to district:
# area-1 → І, area-2 → ІІ, area-3 → ІІІ
```

### Updating districts.html with Extracted Data
1. For each controller, find their `<tr>` by matching `<td class="name">NAME</td>`
2. Extract all `<td class="area...">` cells from that row
3. Replace cells for days 11-19 with district data (cell index = day - 1)
4. Keep existing data for days 1-10 and 20-31

- Deployment/cache pitfall: if live pages show stale generated content even after a normal Vercel deploy (e.g. month index still has `href="#"` or daily pages show the wrong generated title while local files are correct), run `vercel deploy --prod --force`, then bump `service-worker.js` cache and deploy again if needed. Verify with cache-busting query strings (`?v=<timestamp>`) and concrete content checks such as number of real links, `href="#"` count, and expected page title.

## Common Pitfalls

1. **xlsx column mapping**: `day_num = col_num - 2` (C=3, day=1)
2. **District fallback**: For days 20-31, use last known district from days 1-19
3. **Pair separator class**: On first row of each pair only
4. **Vercel auth**: Use the configured Vercel authentication for the environment (CLI login or an environment-provided token). Never store raw deployment tokens in skills, handoff docs, or user-facing replies; redact secrets as `[REDACTED]`.
5. **Vercel CLI path**: May need `npm install -g vercel --prefix /opt/data/.local` if not found; then `export PATH="/opt/data/.local/bin:$PATH"`
6. **Server timeouts**: Don't create zip/tar archives on server for site backups
7. **District format**: Can be "І" or "І дільниця" — normalize both
8. **re.DOTALL**: Python requires exact case `re.DOTALL`, not `re.DotALL`
9. **User style**: Concise responses — just do it, don't narrate steps
10. **Daily file naming**: Files use `DD.MM.26.html` format (e.g., `11.05.26.html`), NOT `YYYY-MM-DD.html`
11. **Archive script ROOT**: Must run from `/opt/data/vercel-project/` — the script hardcodes this path. Running from `/tmp/` creates archives in the wrong location.
12. **ВП → ОВ replacement**: All `ВП` (відпустка) must be replaced with `ОВ` (Основна відпустка) everywhere. Also, `В` (вихідний) inside black-block vacation periods becomes `ОВ` too (weekends during vacation are part of the vacation).
13. **Weekly archive size**: Should be ~1.2 MB (same as daily). If it's gigabytes, the archive is corrupted/broken — delete and regenerate.
14. **Python f-string + HTML**: Don't use f-strings with HTML containing `"` quotes — Python will throw `SyntaxError`. Use string concatenation (`+`) or `.format()` instead. If you must use f-strings, escape quotes as `\"` but this is fragile.
15. **District source of truth**: When user says "forget about shifts" — always extract districts directly from daily HTML schedule files, never infer from shift numbers. Daily graphs are the authoritative source.
16. **Voice-dictated district corrections**: User may dictate entries as “date → surnames/pairs → first/second/third”. Treat first/second/third as districts and write `І/ІІ/ІІІ` in `districts.html` for that date column. Normalize imperfect voice transcription against the known controller list (e.g., Зозуля/Шраменко, Гавриленко/Пилипенко, Битко/Ніколіч, Добшина/Мельник, etc.), verify a few updated cells, copy to the mirror project path, and deploy.
18. **Manual daily schedule corrections**: If the user dictates a daily graph (e.g. “20 травня: Шраменко/Зозуля — третя…”), update `daily/DD.MM.26.html` with the exact pair/trio grouping and `rowspan` district cells. Then sync the same date in `districts.html` for those controllers. The daily page and district table must not disagree.
19. **Moving a controller into another pair/trio**: When the user says to “перетягнути” one controller into another pair, remove that controller’s standalone group, append them to the target group, set `rowspan` to the new group size (usually 3), and renumber the remaining groups sequentially within the same shift. Preserve the shift heading/count unless the user explicitly changes shift membership. Sync the same date column in `districts.html` for every affected controller, including the moved person and any pair whose district changed.
20. **Voice correction priority**: If a new voice correction contradicts the previous deployed daily page (e.g. Гавриленко/Пилипенко І → ІІІ), treat the latest correction as authoritative; update both the daily page and `districts.html`, then verify by printing the logical daily rows with propagated rowspan districts plus the exact day cells from `districts.html` before deploying.
21. **Two-way daily/district consistency**: If you manually change `districts.html` for a controller/date because the user corrected a district, immediately check whether a corresponding `daily/DD.MM.26.html` page exists. If it exists and contains that controller/pair, update the daily page's `area-cell area-N`, visible district text (`І/ІІ/ІІІ дільниця`), and `rowspan` group consistently too. Do not leave the table correct but the daily graph stale; the user explicitly expects “там теж” to be updated. Verify the live deployed daily URL as well as `districts.html` after deploy.
21. **Single-date pair/trio district corrections**: If the user names one controller and says to change their district on a specific date (e.g. “Перетятько 27 травня з другої в третю”, “Тарарєєва 26 травня друга дільниця”), inspect that daily page first to identify the full `rowspan` group. Update the whole pair/trio in `daily/DD.MM.26.html` and sync the same day column in `districts.html` for every controller covered by that district cell, not only the spoken surname. Example: if `Затокова Л.М.` + `Тарарєєва М.М.` share `rowspan="2"`, changing Тарарєєва to ІІ means both rows/cells become ІІ unless the user explicitly says to split the pair. If the named correction creates a new pair across existing groups (e.g. “Ковтун Перетятько І дільниця 21.05” or “Перетятько і Пісоцька в першу зміну третя дільниця”), move only the named controllers into that new pair/trio, remove them from their old groups, reduce old `rowspan`s, adjust the target/new `rowspan`, renumber groups, and preserve the shift count unless membership changes. Sync `districts.html` for the named/new group members only unless a whole existing rowspanned group remains together; verify neighboring former partners (e.g. Бобух/Дибля, Ковтун/Савченко) did not get unintended day-cell changes. For parsing full `districts.html` rows, remember cells are `pairno`, `name`, then day 1..31, so day N is regex cell index `N+1` (day 22 = index 23); if parsing only the substring after the name cell, day N is index `N-1`. Print exact day cells for named people and nearby affected former partners before deploying, then verify live daily snippet and `districts.html` after deploy.
23. **Daily group visual ordering**: When the user complains that daily pairs are “врозкид” or asks for “перша, друга, третя дільниця” order, reorder whole pair/trio blocks *within each shift/table* by `area-1`, then `area-2`, then `area-3`. Preserve the controller names and district assignments; only move complete groups and renumber pair numbers sequentially. Verify by printing each table’s group order as `(pair_no, area_class, first_name)` before deploying.
24. **Merging a singleton into a same-district pair**: If a daily page has one controller alone in a district and the user says to “приєднати до пари” or “зробити трійку”, remove the singleton group, append that controller as a blank `pair-cell` row under the named pair, change the target district cell to `rowspan="3"`, and renumber following groups. Usually `districts.html` does not change if the district remains the same; still verify the daily logical rows after deploy.
25. **Off-day corrections in districts table**: If the user says a controller/pair is “вихідні/вихідний/В” for specific dates (ASR may sound like “в ехідмін/в ехідлі”), set those `districts.html` day cells to `<td class="area area-off">В</td>` rather than a district. Cross-check the monthly graph (`months/2026-05.html` and `index.html`) and the daily pages: if the people are already absent from the daily graph and monthly cells are `В`, only `districts.html` needs changing. Verify the exact day cells before deploy.
22. **Bulk range dictation cleanup**: When user dictates ranges like “23-24”, “25-29 включно”, or “з 23 по 27 включно”, expand inclusive ranges and update both daily pages and `districts.html`. After bulk replacement, scan `districts.html` for any remaining raw numeric `1/2/3` cells in the affected date range; if the user says “замість одиничок поставити третю дільницю”, convert those `1` cells to `ІІІ` with `area-3`, then verify no raw numeric cells remain.
23. **Emoji in HTML**: Avoid emoji characters (🎫 etc.) in Python string concatenation — they cause `UnicodeEncodeError: surrogates not allowed`. Use HTML entities instead (e.g., `&#127937;` for 🎫).

## PWA / mobile shortcut branding

Use this when the user asks to change the site shortcut/app label (“ярлик”, “кнопка”, installed app name) or the trolleybus icon text.

- Update `manifest.json`: set `name`, `short_name`, and `description` to the requested label (e.g. `Контролери КП ЧЕТС`). Keep existing `start_url`, `scope`, display settings, and icons unless explicitly changing them.
- Update `<meta name="apple-mobile-web-app-title" ...>` across HTML pages so iOS home-screen shortcuts use the same label.
- Update the browser `<title>` and the visible home-page `<h1>` if the user also asks for the top site heading; make the heading black/bold when they ask “красиво чорним”.
- If changing icon branding, update both `assets/app-icon.svg` and raster icons `assets/app-icon-192.png`, `assets/app-icon-512.png`. A reliable local pattern is `ffmpeg` with a white canvas, `drawtext` for the black Ukrainian label, and overlay of `assets/trolleybus-center.jpg`; verify visually with the image tool.
- Bump `service-worker.js` cache version after changing manifest/icons/headings so old installed/offline assets refresh.
- Apply changes to both `/opt/data/vercel-project` and `/tmp/schedule_site/vercel-project`, deploy, then verify live `/manifest.json`, `/service-worker.js`, `/`, and icon PNG signature/appearance.

## Offline Visual Editor (`/editor.html`)

The site has a non-technical visual editor for the user to edit pages directly. Use it when the user asks to improve self-editing, offline work, copying cells, changing colors/text/images, publishing local changes, or adding a “Створення / Creator” area with ready-made templates/zаготовки for new pages.

**Live-source pitfall learned:** ordinary Visual open must bypass *both* local drafts and GitHub/API. A stale `localStorage` draft can show an old May graph, but `/api/editor-file` can also show a different old version if it reads from a GitHub snapshot older than the deployed Vercel site. For Visual → Open, fetch the static live path directly with a timestamp and no-store (`fetch('/'+path+'?v='+Date.now(), {cache:'no-store'})`) and show status like `актуальний сайт (live, без GitHub/API)`. Reserve API/GitHub reads for explicit source workflows only when repository sync is proven.

Core requirements:
- The editor must work when the site is opened as an installed mobile/desktop app or shortcut where there is no visible URL bar. Do not require the user to copy/paste page URLs.
- Page picker must include all relevant site pages, especially `daily/*.html`, `months/*.html`, `districts.html`, `districts/YYYY-MM.html`, `tab.html`, `tickets/*`, `stt/*`, and support pages; do not leave daily/monthly/district month pages inaccessible.
- **May monthly graph source rule:** the user treats the homepage `/` / `index.html` as the authoritative May monthly graph. In the editor, do not expose `months/2026-05.html` as a separate selectable duplicate and do not let a stale localStorage draft for `months/2026-05.html` override the real homepage. Normalize `months/2026-05.html` → `index.html`, delete/ignore `changes()['months/2026-05.html']`, and load May with cache reload when fixing stale editor views.
- Prefer a human-readable page picker over raw file names: category dropdown (`Основні розділи`, `Графіки на місяць`, `Щоденні графіки`, `Дільниці`, `Квиток контролера`, `Звіт СТТ`, `Службові файли`), quick search, and option labels like `Щоденний графік — 8 червня 2026 — daily/08.06.26.html`.
- Build/update the editor page list from actual project files where possible instead of relying on stale hardcoded options; when adding a new page class, update both visual and source-code selectors plus `ALL_PAGES` for offline caching.
- Visual editing should happen inside an iframe via direct click selection of cells/text/images.
- Support text edits, background/text colors, bold, copy/paste cell contents/styles, clear cell, replace image URL, and embed an uploaded image as a data URL.
- Support text edits, background/text colors, bold, copy/paste cell contents/styles, clear cell, replace image URL, and embed an uploaded image as a data URL.
- Full-cell copy/paste is a key UX requirement for large monthly graphs, tab pages, and district tables: copying a cell must copy the visible text/HTML plus class/status, inline style, computed background, text color, font weight, borders, alignment, colspan/rowspan, and relevant `data-*` attributes. Provide both buttons (`Копіювати всю клітинку` / `Вставити всю клітинку`) and keyboard shortcuts (Ctrl/Cmd+C then Ctrl/Cmd+V after selecting target cell). Do not make the user separately choose the letter and background color for status/shift cells.
- For table pages (especially `districts.html`), click-selection must prefer the enclosing `td/th`, not inner text/span/div, so background/color/class changes apply to the whole cell.
- Provide dedicated district buttons `І дільниця`, `ІІ дільниця`, `ІІІ дільниця` that update text, class (`area-1/2/3`), background, text color, font weight, and font size together; do not expect the user to manually set text and color separately.
- Provide dedicated district buttons `І дільниця`, `ІІ дільниця`, `ІІІ дільниця` that update text, class (`area-1/2/3`), background, text color, font weight, and font size together; do not expect the user to manually set text and color separately.
- Support saving the currently opened visual page to the user's computer: download edited HTML, high-resolution PNG capture, and browser Print/Save-as-PDF from the iframe.
- For PNG export, use a local `html2canvas` asset first; fall back to SVG/foreignObject only if needed. Use max-quality scaling (4x–5x where feasible), and report pixel dimensions + MB size after download. The safe draft queue (`Папка чернеток`) should also expose a per-draft **`Скачати PNG`** action, not only PNG from the currently opened visual iframe; implement by rendering the saved HTML in an offscreen iframe and passing it through the same high-resolution `html2canvas` path.
- Offline mode must save to a clearly labeled safe draft folder/queue (`localStorage`) and support export/import of a JSON change package. Browser publishing must stay disabled/protected unless the GitHub/Vercel source is proven synchronized; the visible UI should say “Папка чернеток” / “Публікує тільки Hermes” so the user cannot accidentally publish or roll back the live site.
- **Critical editor loading rule:** ordinary **Візуальне редагування → Відкрити** must load the current live/site file and ignore `localStorage` drafts. Draft HTML from `localStorage` may be opened only through **Папка чернеток** or explicit Creator-draft buttons. Otherwise stale saved drafts (especially `index.html` / May month graph) can silently override the real site in the iframe and make the user think the live graph is old. Implement this as separate paths, e.g. `loadPath(path, allowLocal=false)`, `openVisual()` calls `loadPath(path,false)`, while `openQueued()`/Creator calls `openVisualDraft(path)`.
- **Live-source pitfall:** ordinary visual opening must also bypass the GitHub/API read path. If `apiGet()` reads from an older GitHub snapshot, the editor can show a second kind of “old but different” page even after local drafts are ignored. For visual opens, fetch the deployed static file directly with cache busting/no-store, e.g. `fetch('/'+path+'?v='+Date.now(), {cache:'no-store'})`, and report a status like `актуальний сайт (live, без GitHub/API)`. Keep API/GitHub only for explicitly intended source operations; browser POST publishing stays protected until GitHub is synchronized.
- **Canonical May graph rule:** for May 2026 the main monthly graph is `/index.html`. In editor pickers, label it as `Графік на місяць — Травень (головний актуальний)`, treat `months/2026-05.html` as an alias to `index.html`, and do not offer a separate stale `months/2026-05.html` choice. If a stale `localStorage` draft exists for `months/2026-05.html`, delete/ignore it. This prevents the editor from pulling the unfinished old May duplicate.calls `openVisualDraft(path)`.
- **Live-source pitfall:** ordinary visual opening must also bypass the GitHub/API read path. If `apiGet()` reads from an older GitHub snapshot, the editor can show a second kind of “old but different” page even after local drafts are ignored. For visual opens, fetch the deployed static file directly with cache busting/no-store, e.g. `fetch('/'+path+'?v='+Date.now(), {cache:'no-store'})`, and report a status like `актуальний сайт (live, без GitHub/API)`. Keep API/GitHub only for explicitly intended source operations; browser POST publishing stays protected until GitHub is synchronized.
- **Canonical May graph rule:** for May 2026 the main monthly graph is `/index.html`. In editor pickers, label it as `Графік на місяць — Травень (головний актуальний)`, treat `months/2026-05.html` as an alias to `index.html`, and do not offer a separate stale `months/2026-05.html` choice. If a stale `localStorage` draft exists for `months/2026-05.html`, delete/ignore it. This prevents the editor from pulling the unfinished old May duplicate.
- Creator draft safety UX: provide a “Папка чернеток Creator” area where generated/edited pages can be opened, downloaded as individual HTML, exported as `chets-creator-drafts-YYYY-MM-DD.json`, or optionally saved to a user-chosen local computer folder via `showDirectoryPicker` when the browser supports it. Saving to this folder must not call `/api/editor-file` and must not publish to internet.
- User UX preference: the page-save action must be a prominent unambiguous button (e.g. “💾 Зберегти сторінку картинкою PNG”) in its own save bar, not a small ambiguous “PNG” button near edit controls. Clicking save should download immediately and should not place a cursor or enter edit mode.
- User UX preference: the page-save action must be a prominent unambiguous button (e.g. “💾 Зберегти сторінку картинкою PNG” / “Скачати PNG”) in its own save bar or draft action row, not a tiny ambiguous control near edit buttons. Clicking save/download should download immediately and should not place a cursor or enter edit mode.
- For normal site pages that include the shared save panel (`assets/save-page.js`), the user prefers a clean UI with **only** the button text `💾 Зберегти сторінку` visible. Do not show extra explanatory text under it such as “Повзунок прибрав…”, “Автопідгонка…”, “місячний графік на 2 листки”, “щоденний на 1 листок”, or “вибрати частину” unless explicitly requested. The site should silently auto-fit/print; avoid making the user read usage instructions after the button.
- Add/maintain a **Створення / Creator** tab when the user asks for “заготовки” in the editor. It should be a first-class tab alongside visual/source/queue/help, not hidden in source code.
- Daily schedule Creator draft rules (approved 2026-05-24): daily drafts are iterative, not one-shot. Each click on **“Відкрити поточну заготовку візуально”** after changing Creator fields must rebuild/save the current generated HTML into the browser draft queue and open that exact draft visually, so changes are not lost before app close/reload. Drafts in **Папка чернеток** must be reopenable for visual editing and saveable over the same draft. For daily Creator controller dropdowns, order names by the selected date’s monthly graph (May from `index.html`, later months from `months/YYYY-MM.html`), not alphabetically. Once a controller is selected in one field for that daily draft, hide/disable that surname in all other controller dropdowns for the same day to prevent assigning the same controller twice. When rendering the daily draft, merge consecutive controllers with the same district into visual groups: exactly 3 consecutive same-district controllers become one trio with `rowspan="3"`; exactly 4 become two pairs (2+2) separated by a pair separator; 5+ should prefer trio(s) plus pair/single as needed, while district changes always break the run.
- Daily schedule Creator template requirements: date picker auto-fills `daily/DD.MM.26.html` and Ukrainian date title; two shift sections (`І зміна`, `ІІ зміна`); 6 pair slots per shift; each pair has Controller 1, Controller 2, and a district select `І/ІІ/ІІІ`; generated HTML uses the same daily graph visual structure, `rowspan` district cells, colored district classes, and can be opened in the visual editor, saved locally, published, or downloaded.
- Daily schedule Creator controller fields should be dropdowns/selects populated from the canonical controller list (currently 29 names from the active monthly graph), not free-text inputs, so the user can tap/select names without typing and avoids surname spelling drift.
- Daily schedule Creator should include status selection lists for `В`, `ТН`, `БЗ`, `ОВ` populated from the same month-ordered controller list. After a controller is selected for a shift/district, hide them from all remaining shift and status selects; after selecting a surname in one status, hide it from the other status/shift fields. Hidden backing textareas may be kept only for template generation compatibility.L uses the same daily graph visual structure, `rowspan` district cells, colored district classes, and can be opened in the visual editor, saved locally, published, or downloaded.
- Daily schedule Creator controller fields should be dropdowns/selects populated from the canonical controller list (currently 29 names from the active monthly graph), not free-text inputs, so the user can tap/select names without typing and avoids surname spelling drift.
- Daily schedule Creator should include status selection lists for `В`, `ТН`, `БЗ`, `ОВ` populated from the same month-ordered controller list. After a controller is selected for a shift/district, hide them from all remaining shift and status selects; after selecting a surname in one status, hide it from the other status/shift fields. Hidden backing textareas may be kept only for template generation compatibility.
- Daily schedule Creator should include status selection lists for `В`, `ТН`, `БЗ`, `ОВ` populated from the same month-ordered controller list. After a controller is selected for a shift/district, hide them from all remaining shift and status selects; after selecting a surname in one status, hide it from the other status/shift fields. Hidden backing textareas may be kept only for template generation compatibility.nd can be opened in the visual editor, saved locally, published, or downloaded.
- Daily schedule Creator template requirements: date picker auto-fills `daily/DD.MM.26.html` and Ukrainian date title; two shift sections (`І зміна`, `ІІ зміна`); 6 pair slots per shift; each pair has Controller 1, Controller 2, and a district select `І/ІІ/ІІІ`; generated HTML uses the same daily graph visual structure, `rowspan` district cells, colored district classes, and can be opened in the visual editor, saved locally, published, or downloaded.
- Daily schedule Creator controller fields should be dropdowns/selects populated from the canonical controller list (currently 29 names from the active monthly graph), not free-text inputs, so the user can tap/select names without typing and avoids surname spelling drift.
- Daily schedule Creator should include status selection lists for `В`, `ТН`, `БЗ`, `ОВ` populated from the same month-ordered controller list. After a controller is selected for a shift/district, hide them from all remaining shift and status selects; after selecting a surname in one status, hide it from the other status/shift fields. Hidden backing textareas may be kept only for template generation compatibility.L uses the same daily graph visual structure, `rowspan` district cells, colored district classes, and can be opened in the visual editor, saved locally, published, or downloaded.
- Daily schedule Creator controller fields should be dropdowns/selects populated from the canonical controller list (currently 29 names from the active monthly graph), not free-text inputs, so the user can tap/select names without typing and avoids surname spelling drift.
- Daily schedule Creator should include status selection lists for `В`, `ТН`, `БЗ`, `ОВ` populated from the same month-ordered controller list. After a controller is selected for a shift/district, hide them from all remaining shift and status selects; after selecting a surname in one status, hide it from the other status/shift fields. Hidden backing textareas may be kept only for template generation compatibility.
- STT Creator template requirements: date picker creates a simple `stt/DD.MM.26.html` scaffold with editable report table fields; support CSV/TXT paste/upload in the browser for rows like `Контролер;перший;останній;успішних;неуспішних`.
- Creator must make the path from draft to visual editing obvious for a nontechnical user. The primary button should be labeled like **“Відкрити поточну заготовку візуально”** (not a vague “Open visual”), and the visual tab should also have a **“Поточна заготовка з Creator”** button. Clicking either should save the current generated HTML to local storage under its target path, switch to the visual editor iframe, and show a success message that the current Creator draft is open and editable.
- Creator should include offline-friendly scaffolds for `Квиток контролера` (`tickets/DD.MM.26.html` from typed surname/count rows), `Дільниця` (`districts/YYYY-MM.html`), `Місячний графік` (`months/YYYY-MM.html`), `Табель` (`tab/YYYY-MM.html`), and `Проблеми` month pages. All generated HTML should preview immediately in an iframe and place generated HTML in a textarea for direct inspection/copying; saving should use the same local queue/publish API as the visual editor.
- Creator UX wording matters: the button that sends a generated scaffold into the visual editor should be explicit, e.g. **“Відкрити поточну заготовку візуально”**. Also provide a visual-editor toolbar button like **“Поточна заготовка з Creator”** so the user can reopen the latest generated local draft without copying a path. The flow should be: create scaffold → open visually → edit like a normal page → save locally/publish.
- Creator UX detail: the user expects a clear button labeled like `Відкрити поточну заготовку візуально` / `Поточна заготовка з Creator`. Pressing it must put the generated HTML into the local queue, switch to `Візуальне редагування`, load that exact draft in the iframe, and show a success message that it is the current Creator draft. Do not make the user manually copy a path or search for the generated draft.
- Offline mode must save to local queue (`localStorage`) and support export/import of a JSON change package. Enable online `/api/editor-file` publishing only when the GitHub/Vercel source is in sync with `/opt/data/vercel-project`; otherwise guard POST publishing so the user cannot trigger a rollback from a stale repo. If a browser/Creator publish causes the live site to lose recent generated pages or editor features, suspect an automatic Vercel rebuild from an older GitHub snapshot: restore from `/opt/data/vercel-project`, force deploy, bump the service worker, and keep editor publishing blocked until the repo source is synchronized. Local save/download must remain available.
- Service worker should cache the full static site via a `CACHE_NOW` message so the editor can work without internet after preparation.
- When expanding editable files, also update `api/editor-file.js` `okPath()` allowlist while keeping `api/`, dotfiles, `..`, backslashes, and arbitrary binary writes blocked. District month files require the `districts/` prefix in the allowlist.
- Before deploy, syntax-check `api/editor-file.js`, `service-worker.js`, and the inline `<script>` extracted from `editor.html` with `node --check`. Also verify the deployed `/editor.html` contains the picker controls and that `ALL_PAGES` includes representative new pages (`daily/DD.MM.26.html`, `districts/YYYY-MM.html`, `tab.html`).

Detailed implementation notes and verification commands: `references/offline-visual-editor.md`.

## Archive / Backup System

The site has a rolling archive system managed by `rolling_archive.py`:

**Script location**: `/tmp/schedule_site/scripts/rolling_archive.py`
**Archive storage**: `/opt/data/vercel-project/assets/archives/`

Three archive tiers:
1. **Initial** (`schedule-site_initial.zip`) — stable baseline, created once
2. **Weekly** (`schedule-site_weekly.zip`) — refreshed every 7 days
3. **Daily** (`schedule-site_daily.zip`) — overwritten every run

When the user asks for a full archive suitable for restoring the site or handing it to another agent, create/update root-level recovery files before archiving:

- `RESTORE_HANDOFF.md` — concise restore handoff: site purpose, directory map (`months/`, `daily/`, `districts/`, `tickets/`, `stt/`, `assets/`, `scripts/`, `editor.html`, APIs), restore steps (`python3 -m http.server` or Vercel deploy), current live/archive URLs, and the user's key operating rules.
- `MACHINE_OPERATING_MANUAL.md` — expanded machine-readable operating manual for a future AI/agent: how to edit every major section, create pages/buttons, process Telegram/Gmail/Drive/Excel inputs, update month/daily/district/tickets/STT/problems pages, deploy via Vercel, bump service-worker cache, archive/restore, and avoid rollbacks/cascade mistakes.
- `LAST_KNOWN_STATE.json` — compact machine-readable state: live URL, project/mirror roots, service-worker cache version, file inventory, and non-secret operating rules.
- `agent-knowledge/chets-transit-schedules/` — copy the relevant skill `SKILL.md`, `references/`, and reusable `scripts/` into the project before deep archiving so the ZIP carries algorithms even if Hermes skill memory is unavailable.

Keep all documents secret-free (tokens as `[REDACTED]`) and explicitly state the no-cascade rule: only change named pages unless the user explicitly asks to sync. Then create both rolling archives and a separate full deep archive. The deep archive should be named like `assets/archives/CHETS_FULL_DEEP_ARCHIVE_YYYYMMDD_HHMM.zip` plus a stable `CHETS_FULL_DEEP_ARCHIVE_LATEST.zip`, and include the whole site + handoff/manual/state + agent-knowledge while excluding `.git`, `.vercel`, `node_modules`, `__pycache__`, and nested ZIP recursion. Patch `rolling_archive.py` so its prune step preserves `CHETS_FULL_DEEP_ARCHIVE_*.zip`, `CHETS_FULL_DEEP_ARCHIVE_LATEST.zip`, and `deep-archive-latest.json`; otherwise the next daily archive can delete the user’s emergency handoff ZIP. Add a visible “Повний deep-архів” card/link to `archive.html`, bump `service-worker.js`, force deploy, and verify the live ZIP signature (`PK`), size, SHA, and required members (`RESTORE_HANDOFF.md`, `MACHINE_OPERATING_MANUAL.md`, `LAST_KNOWN_STATE.json`, `scripts/create_deep_archive.py`, `agent-knowledge/…/SKILL.md`).

**Run manually**:
```bash
cd /opt/data/vercel-project && python3 scripts/rolling_archive.py
```

**Force all archive tiers to current stable state** (use after the user says to archive “все”: stable/main, weekly, and daily):
```bash
cd /opt/data/vercel-project && ROLLING_ARCHIVE_VERBOSE=1 FORCE_MAIN_RESTORE=1 FORCE_WEEKLY=1 python3 scripts/rolling_archive.py
```
Then deploy and verify `/archive.html` plus the three ZIP URLs return HTTP 200 and sizes around the normal archive size for the current site (about 2 MB after adding the machine manual; do not assume the older ~1.2 MB baseline).

**Force weekly refresh**: `cd /opt/data/vercel-project && FORCE_WEEKLY=1 python3 scripts/rolling_archive.py`

**Force main restore refresh**: `cd /opt/data/vercel-project && FORCE_MAIN_RESTORE=1 python3 scripts/rolling_archive.py`

**Cron jobs** (created via Hermes cron):
- `CHETS Daily Archive` — `0 18 * * *` (daily 18:00 UTC, вечір України)
- `CHETS Weekly Archive` — `10 18 * * 0` (Sunday 18:10 UTC, вечір України, with `FORCE_WEEKLY=1`)

**Archive page**: `archive.html` is auto-generated by the script with download links and status.

**Important**: Archives are stored in `/opt/data/vercel-project/assets/archives/` (the deployed project root), NOT in `/tmp/`. The script's `ROOT` variable points to `/opt/data/vercel-project`.

**Manifest**: `assets/archives/archive-manifest.txt` — updated on each run with timestamps and sizes.

## Generation Scripts

- Daily schedules: `/tmp/gen_daily_v4.py`
- Districts page: `/tmp/gen_districts_final.py`
- Extract districts from daily HTML: `scripts/extract_daily_districts.py` (in skill dir)
- Sync daily pages from monthly district table: `scripts/sync_daily_from_districts.py`
- Rolling archive: `/tmp/schedule_site/scripts/rolling_archive.py`
- Per-controller ticket pages: see `references/ticket-page-generation.md`
- Monthly ticket overview tile layout: see `references/month-ticket-overview.md`
- Problems page updates: see section below.
- STT report generation: see `references/stt-report-generation.md`
- Daily project pages for a new month: see section above.
- Run from `/tmp/` directory (except archive — run from `/opt/data/vercel-project/`)
