# CHETS Offline Visual Editor Reference

## Purpose

`/editor.html` is the user-facing admin/editor for the static CHETS schedule site. It should support non-technical editing of any site page: monthly graphs, daily graphs, districts, tickets, STT, vacations, problems/needs, and support files.

## Desired user workflow

1. Open `/editor.html` and enter the editor password.
2. Select a page from the full site page list using section/category and search (works even in installed PWA/shortcut mode with no visible URL bar), or type a path such as `daily/23.05.26.html` or `months/2026-06.html` if needed.
3. Click **Відкрити**.
4. Click directly on a cell/text/image in the iframe.
5. Edit visually:
   - change text;
   - change background/text color;
   - copy one cell and paste into another;
   - clear a cell;
   - make text bold;
   - replace image URL or embed an uploaded image as a data URL;
   - for districts, click a table cell and press the dedicated `І дільниця` / `ІІ дільниця` / `ІІІ дільниця` button so text, class, background, and text color all change together.
6. Save/export the opened page if needed:
   - **Скачати HTML** for an editable/offline HTML file;
   - **Зберегти PNG MAX якість** for a high-resolution image download;
   - **PDF/Друк** for browser Save-as-PDF.
7. If offline: click **Зберегти локально**; later publish from local queue or export a JSON change package.
8. If online: click **Опублікувати** to send content through `/api/editor-file`.
   - **Скачати HTML** downloads the edited page as an HTML file;
   - **PDF/Друк** opens browser print/save-as-PDF.
7. If offline: click **Зберегти локально**; later publish from local queue or export a JSON change package.
8. If online: click **Опублікувати** to send content through `/api/editor-file`.

## Key implementation details

- Generate the page selector from actual deploy files under `/opt/data/vercel-project` so daily/monthly pages are not missing from the UI.
- Use iframe `srcdoc` for the selected HTML. Avoid leaving the entire iframe document in `designMode='on'` because the user experiences that as a stray cursor when they are trying to save/export; instead enable `contenteditable` only on the clicked cell/text element.
- Track `selectedEl`; highlight it with a temporary `.picked` class. Remove `.picked`, `contenteditable`, active focus, and text selection before saving/exporting HTML or PNG.
- For tables, `selectEl()` should prefer `el.closest('td,th')` before inner elements so operations affect the full cell. This is critical for `districts.html` cell background colors.
- Add a dedicated `applyDistrict(n)` helper for `І/ІІ/ІІІ` buttons. It should set `textContent`, remove status/district classes, add the right `area-N` class, and set inline background/text color + bold district styling.
- `visualHtml()` must return a valid JS string with escaped newline: `return '<!DOCTYPE html>\\n' + d.documentElement.outerHTML`.
- Avoid fragile inline `onclick="openQueued('...')"` construction for queued paths. Prefer `data-act` / `data-path` buttons and event binding after rendering.
- Keep a source-code tab for direct HTML/JSON/CSS/JS editing and a local queue tab for offline changes.
- Keep a **Створення / Creator** tab for templates/zаготовки. For the daily graph template, expose a date picker, auto-filled target path/title, two shift sections, 6 pair slots per shift, and a district selector (`І`, `ІІ`, `ІІІ`) per pair. Generated daily HTML should use normal CHETS daily markup (`pair-cell`, `name-cell`, `area-cell area-N`, `rowspan`, shift headings/counts) so it looks like the existing daily graph and remains editable in the visual editor.
- Creator templates should not require URL copying: after generating, provide buttons to open the generated file in the visual editor (via local queue), save locally, publish through `/api/editor-file`, and download HTML. Include a simple STT scaffold creator for `stt/DD.MM.26.html` with editable table rows.

## PNG / page export

The user expects the visual editor to save the opened graph/page directly to their computer with no extra placement instructions. Put the save buttons in the visual editor toolbar, not hidden in source/queue tabs.

Required buttons:

- **Скачати HTML**: download `visualHtml()` as a `.html` file.
- **Зберегти PNG MAX якість**: download a PNG file immediately to the browser downloads folder.
- **PDF/Друк**: call the iframe window `print()` so the browser can Save as PDF.

Implementation notes:

- Use a local bundled `html2canvas` first (`/assets/vendor/html2canvas.min.js`) to avoid `HTMLCanvasElement.toBlob(): Tainted canvases may not be exported` caused by page images. Download/vendor the file and add it to the service-worker cache list.
- Use `html2canvas(body, {backgroundColor:'#ffffff', scale: Math.min(5, Math.max(4, window.devicePixelRatio || 1)), useCORS:true, allowTaint:false, windowWidth, windowHeight, width, height})` for high-quality export.
- Show a success message with canvas dimensions and MB size, e.g. `PNG MAX збережено ✅ Розмір: 5000×7000, файл: 4.2 МБ`.
- Keep a fallback SVG/foreignObject path, but if the browser still blocks canvas export due to images, tell the user to use PDF/Друк rather than exposing raw technical errors.

## Page picker for PWA/no-URL use

The user may open the site through an installed app/shortcut or mobile browser where copying the current URL is inconvenient or impossible. The editor should therefore provide a first-class page browser:

- Add category selectors for visual and source tabs (`visualCategory`, `sourceCategory`) with labels such as `Основні розділи`, `Графіки на місяць`, `Щоденні графіки`, `Дільниці`, `Квиток контролера`, `Звіт СТТ`, `Службові файли`.
- Add quick-search inputs (`visualPageSearch`, `sourcePageSearch`) that match both human labels and raw paths, so queries like `табель`, `дільниця`, `8 червня`, `08.06`, or `квиток` narrow the list.
- Render option labels as human text plus path, e.g. `Щоденний графік — 8 червня 2026 — daily/08.06.26.html`; retain the raw path as `value` for loading/saving.
- Include month-specific district files (`districts/YYYY-MM.html`) and `tab.html`. These were easy to miss when only static hardcoded options were used.
- Keep the custom path input as an escape hatch, but clear it when the user changes the picker so the selected human-readable page wins.
- After changing page classes, update `ALL_PAGES` and service-worker offline caching so **Підготувати весь сайт офлайн** caches the same selectable pages.

## API allowlist and publishing safety

When broadening editable pages, update `api/editor-file.js` `okPath()` allowlist. Current useful allowlist includes:

- top-level HTML: `index.html`, `daily.html`, `timesheet.html`, `stt.html`, `vacations.html`, `archive.html`, `editor.html`, `problems.html`, `needs.html`, `voice.html`, `install.html`, `emergency.html`, `tab.html`
- folders: `months/`, `daily/`, `districts/`, `tickets/`, `stt/`
- data/config: `assets/data/`, `assets/editor-config.json`, `assets/site-operations.json`
- text restore docs: `RESTORE_README.txt`

Do not allow `api/`, `.git/`, dotfiles, `..`, backslashes, or arbitrary binary writes through this API.

Publishing safety lesson from CHETS: browser/editor `POST /api/editor-file` writes to the linked GitHub repository. If that repo is stale relative to the local Vercel deploy source, the GitHub write can trigger a Vercel rebuild from the old repository snapshot and roll back live pages. Before enabling direct editor publish, verify GitHub/source parity for `editor.html`, `service-worker.js`, daily/monthly indexes, generated page files, and API code. If parity is not guaranteed, disable/guard POST publishing with a clear 409 message and keep **Зберегти локально**, queue export/import, and **Скачати HTML** available; publish via Hermes/Vercel deploy instead. After any accidental rollback, force deploy the trusted local tree, bump service-worker cache, and verify editor + affected pages live after the alias settles.

When Creator adds month templates, generate both index pages and actual target pages. For daily months, `daily/YYYY-MM.html` must contain real `/daily/DD.MM.26.html` links (no `href="#"`), and the linked day files must exist. Update `ALL_PAGES` from actual files and service-worker offline cache/version so page picker/offline mode sees the same pages.

## Offline cache

The service worker should cache all relevant static HTML/JSON/CSS/JS/image pages, not only the home page. Add a `CACHE_NOW` message handler that accepts a URL list and adds those files to the cache. The editor’s **Підготувати весь сайт офлайн** button should post that list.

## PNG / page export pitfalls

- Browser canvas export can fail with `Tainted canvases may not be exported` when the page contains images. The durable fix used here is to vendor `html2canvas` locally (`assets/vendor/html2canvas.min.js`), include it in `service-worker.js` cache assets, and have the editor dynamically load it for PNG export before falling back to SVG `foreignObject` capture.
- Export buttons should be separate from edit controls in a visually prominent save bar. The user complained that a save attempt just showed a cursor; avoid ambiguous placement/labels. Use a clear label like **💾 Зберегти сторінку картинкою PNG** and make the handler download immediately.
- Before capture: call `removeAllRanges()`, blur active elements/window, remove `.picked`, remove `contenteditable`, and set iframe document `designMode='off'`. This prevents caret/selection/highlight artifacts appearing in the saved PNG/HTML.
- Success wording should tell the user the file went to browser Downloads; don't ask where to put it unless browser limitations force a file picker.

## Verification checklist

Before deploy:

```bash
cd /opt/data/vercel-project
node --check api/editor-file.js
node --check service-worker.js
node --check assets/vendor/html2canvas.min.js >/dev/null  # if vendored for PNG export
python3 -c "from pathlib import Path; import re; html=Path('editor.html').read_text(); scripts=re.findall(r'<script>(.*?)</script>', html, re.S); Path('/tmp/editor-script.js').write_text('\\n'.join(scripts)); print(len(scripts), Path('/tmp/editor-script.js').stat().st_size)"
python3 -c "from pathlib import Path; import re; html=Path('editor.html').read_text(); scripts=re.findall(r'<script>(.*?)</script>', html, re.S); Path('/tmp/editor-script.js').write_text('\\n'.join(scripts)); print(len(scripts), Path('/tmp/editor-script.js').stat().st_size)"
node --check /tmp/editor-script.js
```

After deploy:

```bash
curl -sI -L https://vercel-project-xi-five.vercel.app/editor.html | grep -iE 'HTTP|content-length|last-modified|content-type'
curl -s https://vercel-project-xi-five.vercel.app/editor.html | grep -o 'Візуальне редагування' | head -1
python3 - <<'PY'
import urllib.request, re, json
html=urllib.request.urlopen('https://vercel-project-xi-five.vercel.app/editor.html', timeout=20).read().decode('utf-8')
assert all(x in html for x in ['visualCategory','visualPageSearch','sourceCategory','sourcePageSearch'])
pages=json.loads(re.search(r'const ALL_PAGES=(\[.*?\]);', html).group(1))
for p in ['tab.html','daily/08.06.26.html','districts/2026-06.html']:
    assert p in pages, p
print('editor page picker ok', len(pages))
PY
curl -s https://vercel-project-xi-five.vercel.app/service-worker.js | grep -E 'CACHE_NAME|CACHE_NOW' | head
```

## User-facing language

The user wants this as “редагування на новий рівень”: direct visual editing of cells, colors, pictures, letters, and numbers, with local/offline saving and later publication. Keep explanations short and practical.