commit 6652590b843e0a3dda09dc2752785d7133d0de79 Author: Pigway Date: Fri Oct 2 00:03:48 2026 +0800 Build Pigway Frame Flow with bilingual UI, complete KoKonna driver and safe automation diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..453765d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.env +.env.* +.venv +__pycache__ +.pytest_cache +.ruff_cache +data +config +*.egg-info +build +dist diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..22151f7 --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +TZ=Asia/Shanghai +# Default local access. For NAS LAN access set this to the NAS LAN IP (recommended) +# or explicitly 0.0.0.0. No router, reverse proxy or firewall rules are modified. +WEB_BIND=127.0.0.1 +WEB_PORT=18787 +KOKONNA_MOCK=true +KOKONNA_WRITE_ENABLED=false +KOKONNA_API_KEY= +FRAME_FLOW_TOKEN= +# Optional secret files mounted read-only through your own Compose override: +# KOKONNA_API_KEY_FILE=/run/secrets/kokonna_api_key +# FRAME_FLOW_TOKEN_FILE=/run/secrets/frame_flow_token +CORS_ALLOWED_ORIGINS= +IMAGE_URL_ALLOWED_HOSTS= +BUILD_COMMIT=development +BUILD_TIME=unknown diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c865146 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +vendor/guooscar_kokonna.py -whitespace diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 0000000..2ee0045 --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,37 @@ +name: Verify Frame Flow +on: [push, pull_request, workflow_dispatch] +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.14' + - run: python -m pip install -e '.[test]' + - run: python -m ruff check pigway_frame_flow tests scripts + - run: python -m ruff format --check pigway_frame_flow tests scripts + - run: python -m mypy pigway_frame_flow + - run: python -m pytest -q + - run: python -m pip_audit -r requirements.lock --no-deps --disable-pip + - run: | + cp .env.example .env + python - <<'PY' + from pathlib import Path + import secrets + p=Path('.env') + p.write_text(p.read_text().replace('FRAME_FLOW_TOKEN=\n', 'FRAME_FLOW_TOKEN='+secrets.token_urlsafe(32)+'\n')) + PY + docker compose config --quiet + docker compose build --pull + mkdir -p data/images data/generated data/cache config + sudo chown -R 10333:10333 data config + docker compose up -d + for attempt in $(seq 1 30); do curl -fsS http://127.0.0.1:18787/ready >/dev/null && break || sleep 2; done + python scripts/smoke-test.py + docker compose restart pigway-frame-flow + for attempt in $(seq 1 30); do curl -fsS http://127.0.0.1:18787/ready >/dev/null && break || sleep 2; done + python scripts/smoke-test.py + docker inspect pigway-frame-flow --format '{{.HostConfig.RestartPolicy.Name}} {{.State.Health.Status}}' + docker image inspect pigway-frame-flow:latest --format '{{.Size}} {{.Architecture}}' + docker compose down diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c113c27 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.env +.env.* +!.env.example +.venv/ +__pycache__/ +.pytest_cache/ +.ruff_cache/ +data/ +*.egg-info/ +config/* +!config/.gitkeep +*.db* +*.log +build/ +dist/ + +.mypy_cache/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b954ced --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.14.5-slim-bookworm@sha256:ec58d916f9e24a6035cab2bdf07f6206c4cc092a16613c60597534711332d9d6 +ARG BUILD_COMMIT=development +ARG BUILD_TIME=unknown +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 TZ=Asia/Shanghai \ + DATA_DIR=/data CONFIG_DIR=/config BUILD_COMMIT=$BUILD_COMMIT BUILD_TIME=$BUILD_TIME +WORKDIR /app +# CJK fonts intentionally retained for readable Chinese rendering. +RUN apt-get update && apt-get install -y --no-install-recommends fonts-noto-cjk tzdata \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --gid 10333 frameflow && useradd --uid 10333 --gid 10333 --no-create-home frameflow \ + && mkdir -p /data/images /data/generated /data/cache /config \ + && chown -R 10333:10333 /data /config +COPY requirements.lock ./ +RUN pip install --no-cache-dir -r requirements.lock +COPY pigway_frame_flow ./pigway_frame_flow +COPY vendor ./vendor +USER 10333:10333 +EXPOSE 18787 +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:18787/health',timeout=3)" +CMD ["python", "-m", "uvicorn", "pigway_frame_flow.main:app", "--host", "0.0.0.0", "--port", "18787", "--workers", "1", "--no-access-log"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..61d52b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Pigway + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..0783ac4 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,33 @@ +# Notices + +Pigway Frame Flow is an independent project. KoKonna is a supported device +integration and is not affiliated with this project unless otherwise stated. +KoKonna names and marks belong to their respective owners. + +## Community protocol reference + +`@guooscar/kokonna-frame` 1.2.0 was downloaded from the official ClawHub API on +2026-10-01. Version metadata declares **MIT-0**; the ZIP does not include a separate +LICENSE. `vendor/guooscar_kokonna.py` preserves the original snapshot for provenance; +it is not imported or executed. `vendor/guooscar_protocol.py` adapts its raw base64 +upload payload and device call to an injected safe transport. No OpenClaw runtime +is installed or required. Original metadata is retained in `vendor/metadata.json`. + +The Home Assistant integration `patrickchen914/kokonna` (MIT) and +`nixihz/kokonna-skill` (README declares MIT, no root license detected) were inspected +for endpoint coverage. Their source code was not incorporated into runtime. + +## Third-party dependencies + +FastAPI, Starlette, Uvicorn, Requests, croniter, PyYAML, Pydantic and their transitive +dependencies retain their respective upstream licenses. Pillow uses its upstream +HPND license. Debian Noto CJK fonts retain their font package license. +Development dependencies (pytest, Ruff, mypy, pip-audit, HTTPX2, Playwright) retain +their upstream licenses and are not shipped in the runtime image. + +The project's MIT license applies to original project code, not a relicensing of +third-party code, fonts, or trademarks. Dependency versions are in requirements.lock. + +Swagger UI 5.33.1 is vendored from the official npm swagger-ui-dist package (Apache-2.0). +Its SHA-512 tarball integrity and package metadata are retained in vendor/swagger-metadata.json; +license is pigway_frame_flow/web/swagger-LICENSE. API docs work without external CDN access. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c0403cc --- /dev/null +++ b/README.md @@ -0,0 +1,161 @@ +# Pigway Frame Flow + +Pigway Frame Flow is a lightweight self-hosted platform for programmable smart-frame automation. + +独立运行的智能画框服务:中英文 Web UI、深浅色模式、设备驱动抽象、KoKonna 全部已公开接口、图片处理、模板、SQLite 调度与播放列表、临时覆盖恢复和 REST API。 + +KoKonna is a supported device integration and is not affiliated with this project unless otherwise stated. + +## Quick Start / 快速开始 + +```sh +python3 -m venv .venv +.venv/bin/python -m pip install -r requirements.lock +.venv/bin/python scripts/run_local.py +``` + +Open http://127.0.0.1:18787. The local demo creates a private `.env` with a random +FRAME_FLOW_TOKEN. Read that token locally in an editor and enter it on the login +screen. Do not paste a KoKonna API Key into Web UI. Demo always forces mock mode +and disables real writes. It leaves data persistent in ./data and ./config. + +打开页面后输入 `.env` 中的 FRAME_FLOW_TOKEN;Token 只留在当前页面内存,刷新后需重新输入。 + +## Architecture / 架构 + +``` +providers → templates / renderers → generated image + ↓ +Web UI / external REST → DisplayService → FrameDeviceDriver → KokonnaDriver +Scheduler / Playlist ─────────────┘ + SQLite: operations / history / jobs / overrides / assets +``` + +Package: pigway_frame_flow. Device composition is in core/registry.py; +vendor-specific code is in drivers/kokonna. One process, one worker, no Redis, +message broker, OpenClaw, Home Assistant or external application dependency. + +## Configuration / 配置 + +Copy `.env.example` to `.env`, set a strong FRAME_FLOW_TOKEN, chmod 600 .env. +- KOKONNA_MOCK=true: independent persistent demo, no cloud calls. +- KOKONNA_WRITE_ENABLED=false: live write protection, mandatory before verification. +- KOKONNA_API_KEY: device key, never stored in DB or returned to Web UI. +- *_FILE: optional Docker Secret equivalent. +- WEB_BIND/WEB_PORT: Compose host bind and port. Internal app binds container network. +- CORS_ALLOWED_ORIGINS: exact comma-separated origins; default empty. +- IMAGE_URL_ALLOWED_HOSTS: exact public HTTPS hosts; default disabled. Redirects denied. +- TZ=Asia/Shanghai; DATA_DIR=/data; CONFIG_DIR=/config. + +For multiple devices copy deploy/devices.yml.example into config/devices.yml, +reference a separate environment key per device. Jobs bind a device_id. Layout +currently targets 1200 × 1600; incompatible geometry is refused before writes. + +## Docker + +```sh +cp .env.example .env +# Configure a token; keep API key empty, mock true, writes false. +mkdir -p data/images data/generated data/cache config +sudo chown -R 10333:10333 data config +./scripts/build-verify.sh +docker compose up -d +python3 scripts/smoke-test.py +``` + +Image pigway-frame-flow:latest, service/container pigway-frame-flow, AMD64, +Python 3.14.5, pinned runtime dependencies, CJK fonts, non-root UID/GID 10333, +read-only root filesystem, writable /data and /config, no capabilities, no host +network, no Docker socket. Restart unless-stopped. JSON-file logs capped 10m × 3. +Healthcheck uses service /health, not cloud availability. No second log database. + +## Synology NAS + +Target `/volume1/docker/pigway-frame-flow/`. See [NAS deployment](docs/NAS.md). +Set WEB_BIND to the NAS LAN IP to restrict the published interface, or explicitly +0.0.0.0 when desired. Port 18787 must be free. No router/firewall/reverse proxy +rules are modified. Source and persistent volumes stay in this project directory. +No artificial logs directory is required. Compose creates its own bridge network. + +## KoKonna Setup / 真实设备 + +First deploy Mock and verify persistence. Then set KOKONNA_MOCK=false and fill +KOKONNA_API_KEY through private environment/secret. Keep writes false. /ready checks +configuration/DB/worker/token without a cloud call. Query device and images metadata; +compare model, geometry, timezone, heartbeat, battery, imageId, coverId, sync. +If different, stop and report. **Do not enable writes or perform the single test +until the owner explicitly authorizes it.** Test design: [REAL_DEVICE_TEST.md](docs/REAL_DEVICE_TEST.md). + +## Security + +All protected /api/v1 capabilities use Frame Flow Bearer auth and scope checks. +SQLite stores token hashes. Device keys live only in env/secret and runtime memory. +Cloud original-image endpoint embeds a key upstream; backend proxies it with no +redirects and no access/transport logging. Key never enters browser URLs or DB. +Request validation and exceptions use sanitized standard errors. Strong tokens, +trusted LAN and no public exposure are the intended deployment model. + +## API + +OpenAPI 3.x at /openapi.json; Swagger at /docs; stable /api/v1. Display/render/upload +return 202 + operation_id. Poll /api/v1/operations/{id}; completion requires matching +image ID and cloud synced flag. Idempotency-Key retained seven days. X-Request-ID +traces history. UNKNOWN writes block later mutations; read-only reconciliation +and explicit admin acknowledgement release the block, never replay a write. +The older /api/display route is retained and deprecated (same v1 payload). +See [external integration contract](docs/integrations/TICKTAR.md). + +## Scheduler / Playback + +Cron (5 fields), daily, weekdays, weekends, date and temporary tasks; Shanghai time. +Use source {"type":"restore"} for explicit restore jobs. Unique job/slot and persistent +idempotency prevent duplicate dispatch. Overdue tasks >5 minutes are skipped rather +than unexpectedly overwriting artwork after downtime. State persists across restart. +Overrides use priority then recency; lower priorities wait without uploading. +If another app changes the frame, expired overrides do not overwrite its artwork. +Frame Flow playlists support ordered/random cloud image IDs and >=5-minute intervals; +only one enabled playlist per device. They pause for overrides/unknown writes. +Native cloud slideshow mode/interval are readable; membership/editing are unavailable +in the official API. Avoid having native and Frame Flow playback compete. + +## Templates / 图片 + +Six JSON layout templates: daily-dashboard, weather, calendar, quote, photo-text, +system-alert. color and grayscale profiles. Photo data accepts photo_id = local asset +ID. Mock weather is clearly labeled; no unreliable lunar date is fabricated. +Contain preserves composition with margins; cover crops centrally; smart is an +edge-saliency heuristic. Always inspect preview; face-aware guarantees are not claimed. +Cloud uploads activate immediately, so gallery-only means **local staging**. + +## Development / Testing + +```sh +.venv/bin/python -m pip install -e '.[test]' +.venv/bin/python -m pytest -q +.venv/bin/python -m ruff check pigway_frame_flow tests scripts +.venv/bin/python -m ruff format --check pigway_frame_flow tests scripts +.venv/bin/python -m mypy pigway_frame_flow +.venv/bin/python -m pip_audit -r requirements.lock --no-deps --disable-pip +``` + +Tests mock every cloud request; no real-device writes. scripts/ui-smoke.cjs runs an +isolated headless browser and checks settings.mock before acting. CI builds AMD64, +validates Compose, starts a container, checks health/ready and restarts for persistence. + +## Known Limitations / 已知边界 + +Cloud acknowledgements do not prove physical display pixels. Model fields may be +omitted and are explicitly marked configured. No native slideshow membership API, +no cloud upload-only API, no automatic retries after uncertain writes. Cache has no +automatic quota/GC in 0.1.0; monitor data volume. Rendering assets are local, limited +to 25M pixels/20 MiB. This release uses a single worker and is not multi-replica. +Gallery reads have no pagination because upstream documents none. Web UI hides +secrets and lazy-loads thumbnails; generating a first thumbnail fetches one original. +Multi-client provisioning UI/webhook delivery are not implemented. Docker build/NAS +status is recorded honestly in docs/ACCEPTANCE.md. + +## Roadmap + +Additional drivers; scoped token administration; signed webhooks; cache retention; +face-aware cropping; public weather provider; Google Calendar, Home Assistant, MQTT, +NAS monitoring and AI integrations through the same API. External integration is optional. diff --git a/config/.gitkeep b/config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/devices.yml.example b/deploy/devices.yml.example new file mode 100644 index 0000000..5485852 --- /dev/null +++ b/deploy/devices.yml.example @@ -0,0 +1,10 @@ +devices: + living-room: + driver: kokonna + name: 客厅相框 + api_key_env: KOKONNA_API_KEY + enabled: true + # bedroom: + # driver: kokonna + # api_key_env: KOKONNA_BEDROOM_API_KEY + # enabled: true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..df8d6df --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +name: pigway-frame-flow +services: + pigway-frame-flow: + image: pigway-frame-flow:latest + container_name: pigway-frame-flow + platform: linux/amd64 + build: + context: . + args: + BUILD_COMMIT: ${BUILD_COMMIT:-development} + BUILD_TIME: ${BUILD_TIME:-unknown} + restart: unless-stopped + init: true + user: "10333:10333" + ports: + - "${WEB_BIND:-127.0.0.1}:${WEB_PORT:-18787}:18787" + env_file: .env + environment: + TZ: Asia/Shanghai + DATA_DIR: /data + CONFIG_DIR: /config + volumes: + - ./data:/data + - ./config:/config + read_only: true + tmpfs: + - /tmp:size=64m,mode=1777 + cap_drop: [ALL] + security_opt: + - no-new-privileges:true + pids_limit: 100 + mem_limit: 768m + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:18787/health',timeout=3)"] + interval: 30s + timeout: 5s + start_period: 20s + retries: 3 + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" diff --git a/docs/NAS.md b/docs/NAS.md new file mode 100644 index 0000000..0cede66 --- /dev/null +++ b/docs/NAS.md @@ -0,0 +1,115 @@ +# Synology NAS deployment / 群晖部署 + +Target: DS923+, DSM 7.3.1, AMD64, Container Manager. +Directory: /volume1/docker/pigway-frame-flow. Port: 18787. + +## 1. Package and upload / 打包上传 + +From the repository: + +```sh +python3 scripts/package-nas.py +scp -O dist/pigway-frame-flow-nas.tar.gz nas-example:/tmp/pigway-frame-flow-nas.tar.gz +ssh nas-example +``` + +The package contains tracked source/config examples/scripts only, never .env, +actual keys, data or .git. It is a deployment artifact, not a Git backup. +Use `git bundle create frame-flow.bundle --all` separately for history backups. + +## 2. Prepare project directory / 准备目录 + +Verify target is empty or explicitly backed up before extracting. Do not extract +onto another existing installation or other services. If the directory exists, +inspect it first; preserve its .env and data. + +```sh +mkdir -p /volume1/docker/pigway-frame-flow +# Only for an empty/new target: +tar -xzf /tmp/pigway-frame-flow-nas.tar.gz -C /volume1/docker +cd /volume1/docker/pigway-frame-flow +mkdir -p data/images data/generated data/cache config +sudo chown -R 10333:10333 data config +cp .env.example .env +chmod 600 .env +``` + +Edit `.env` on the NAS. Set FRAME_FLOW_TOKEN to a strong random token. API key +stays empty. KOKONNA_MOCK=true and KOKONNA_WRITE_ENABLED=false. +Set WEB_BIND to the actual NAS LAN address (recommended) or explicitly 0.0.0.0; +WEB_PORT=18787. The latter binds every NAS interface, so use the LAN IP when +strict LAN-only binding is required. No firewall/router/proxy edits are performed. +Copy deploy/devices.yml.example to config/devices.yml if multiple devices are desired. + +## 3. Validate, build, start / 验证构建启动 + +Synology Docker is /usr/local/bin/docker. The SSH user may require sudo. Do not +change socket ownership, groups, Docker network settings or other containers. + +```sh +sudo /usr/local/bin/docker compose config --quiet +sudo /usr/local/bin/docker compose build --pull +sudo /usr/local/bin/docker compose up -d +sudo /usr/local/bin/docker compose ps +sudo /usr/local/bin/docker compose logs --tail 100 +sudo /usr/local/bin/docker inspect pigway-frame-flow \ + --format 'id={{.Id}} started={{.State.StartedAt}} health={{.State.Health.Status}} restart={{.HostConfig.RestartPolicy.Name}} ports={{json .NetworkSettings.Ports}} mounts={{json .Mounts}} network={{json .NetworkSettings.Networks}}' +sudo /usr/local/bin/docker image inspect pigway-frame-flow:latest \ + --format 'id={{.Id}} size={{.Size}} architecture={{.Architecture}}' +``` + +Do not print unfiltered `docker inspect`: its environment can contain keys/tokens. +Likewise do not print `docker compose config` without --quiet on a live installation. +Run the read-only smoke checker from a machine with Python and set FRAME_FLOW_URL +to http://NAS_LAN_IP:18787; FRAME_FLOW_TOKEN stays in private env/file. + +Health: GET /health → 200. Readiness: GET /ready → 200 when SQLite, driver config, +auth and Scheduler are ready. Missing credentials in live mode → 503. The cloud +is not probed by readiness. Cloud loss does not cause healthcheck restart loops. + +## 4. Verify persistence / 持久化 + +Create a disabled test schedule in Web UI and locally stage one image (Mock only). +Record the job and asset IDs, then: + +```sh +sudo /usr/local/bin/docker compose restart pigway-frame-flow +``` + +Wait for healthy state, check /health and /ready, and verify the same job/asset +IDs and history remain. Database is data/frameflow.db with WAL/SHM alongside it; +back up while stopped or use SQLite backup API, not a lone live DB file copy. +Restart policy must read unless-stopped. Do not reboot the NAS for acceptance. + +## 5. Live read-only verification / 实机只读 + +After Mock acceptance, fill KOKONNA_API_KEY; set mock=false, keep writes=false. +Recreate only this service to apply env. Query device and images metadata only. +Verify known model/geometry/timezone/name against real data; missing vendor model +fields are not claimed verified. No original downloads are necessary. Stop on +mismatch; never try to fix cloud state automatically. Obtain explicit owner +permission before any upload/switch/delete. See REAL_DEVICE_TEST.md. + +## Layout + +``` +/volume1/docker/pigway-frame-flow/ + docker-compose.yml + Dockerfile + .env + requirements.lock + pigway_frame_flow/ + vendor/ + scripts/ + config/ + data/ + frameflow.db + images/ + generated/ + cache/ +``` + +Container runs UID/GID 10333:10333; only data/config need ownership. Avoid recursive +ownership changes outside this project's paths. Standard Compose bridge is private +and isolated by project name. Logs use stdout/stderr with Docker's capped json-file. +No logs directory or Docker socket mount. diff --git a/docs/REAL_DEVICE_TEST.md b/docs/REAL_DEVICE_TEST.md new file mode 100644 index 0000000..650c42d --- /dev/null +++ b/docs/REAL_DEVICE_TEST.md @@ -0,0 +1,27 @@ +# Real device test plan / 单次实机测试计划 + +**Not executed. Explicit owner authorization is required before any write.** +只读核对完成后询问:“真实设备只读验收完成,是否允许进行一次真实图片上传与显示测试?” + +1. GET Frame Flow device (driver POST /device); record current imageId AND coverId, + synced, geometry, model, rotation, timezone, firmware, heartbeat and battery. +2. POST /listImages for metadata only. Record count, current image, sorted IDs, + duplicates, name/date/dimensions anomalies. No download-all action. +3. Stop on discrepancy. Missing model fields must be reported as unavailable. +4. Once explicitly authorized, locally render **one** 1200×1600 test image containing + PIGWAY FRAME FLOW TEST and Shanghai timestamp, no private content. +5. Record an operation ID and previous image; one upload only, no write retries. +6. Poll device read-only no more often than 60 seconds, respecting the 20/min budget. + Confirm imageId and synced. If sent but response lost, mark UNKNOWN, block writes, + reconcile gallery/device; never upload again to guess the outcome. +7. Keep the test image visible at most 1–3 minutes, then restore the recorded artwork + with a separate tracked switch operation. Query once more to confirm restored + imageId and synced. Record coverId after restoration too. +8. Delete only that known test image after restoration is positively confirmed AND + it is not a base/previous/override/current reference. If ambiguous, leave it and + report. Never delete art or retry a timed-out delete. +9. Disable real writes again. Report each operation ID/result and remaining test image. + +The API cannot set coverId. Restoration guarantees a requested image switch when +confirmed, not arbitrary cover metadata restoration. Physical screen refresh has +no documented pixel telemetry; visual owner observation supplements cloud synced. diff --git a/docs/RESEARCH.md b/docs/RESEARCH.md new file mode 100644 index 0000000..22c54eb --- /dev/null +++ b/docs/RESEARCH.md @@ -0,0 +1,55 @@ +# KoKonna API research / 接口调查 + +Checked 2026-10-01 against https://kokonna.art/pages/openapi. +Base: https://api.kokonna.art/openapi. No fallback to legacy galaxyguide.cn. + +| Official endpoint | Frame Flow capability | +|---|---| +| POST /device | Device panel, standardized device model, charging, firmware, sync, cover, storage, native slideshow mode/interval | +| POST /upload | Rendered JPEG upload and immediate activation, asynchronous operation | +| POST /listImages | On-demand cloud metadata gallery, current marker, name/date/geometry/size | +| POST /deleteImage | Confirmed deletion with restore-reference protection | +| GET /image/:apikey/:imageId | Authenticated server proxy, original bytes, lazy cached thumbnails; key never sent to browser | +| POST /displayImageById | Switch through unified DisplayService | +| POST /displayImageByName | Explicit filename switch; ambiguous names rejected | + +## Native slideshow / 原生轮播 + +`switchType` (queue/random) and `switchMinute` are readable through device. +There is no documented playlist-membership API and no slideshow-setting API. +`listImages` returns the entire cloud gallery sorted by descending ID; it does +not establish playlist membership. No official pagination parameter is documented. +Frame Flow labels membership as unavailable, native editing unsupported, and +provides its own SQLite-backed playlists as an independent capability. + +## Cloud constraints / 云端边界 + +Official limit: 20 requests per minute per device key; local transport reserves +headroom at 18. Device state has a 60-second cache, gallery is loaded on demand. +Uploads automatically activate a new image; there is no upload-without-activation +flag. Gallery-only uploads are therefore local staging, not a fictitious cloud +operation. Maximum upstream request size is 50 MB; this service accepts 20 MiB +image files with bounded decoded pixels. No non-idempotent cloud write is retried. +Cloud acknowledgement is not physical refresh confirmation; /device synced and +imageId drive the operation state. Unconfirmed or interrupted writes become UNKNOWN +and block later device writes until read-only reconciliation/explicit acknowledgement. + +## Reuse / 复用 + +ClawHub API: https://clawhub.ai/api/v1/skills/kokonna-frame +Versioned ZIP: https://clawhub.ai/api/v1/download?slug=kokonna-frame&version=1.2.0 +Community package owner: guooscar; metadata license MIT-0. Reused protocol payload +and device request, with an injected safe transport. It is a skill/CLI, not a +proven NAS daemon, and its default host and screen field assumptions are dated. +We implement scheduling, storage, driver normalization, renderer, REST, auth, +operations, idempotency, recovery, bilingual UI and deployment independently. + +Other inspected repositories: +- https://github.com/patrickchen914/kokonna : Home Assistant integration, MIT; + depends on Home Assistant and had legacy host/key-in-image-URL behavior. +- https://github.com/nixihz/kokonna-skill : broader CLI/Hermes endpoint coverage, + README MIT, legacy host and raw upstream error messages. Not incorporated. +- https://github.com/zhengweiran66-eng/kokonna-product.html : product page. + +No real device key was used during development. Cloud host root returned 530 in +an unauthenticated research request; no claim of current real-device connectivity. diff --git a/docs/integrations/TICKTAR.md b/docs/integrations/TICKTAR.md new file mode 100644 index 0000000..0123c69 --- /dev/null +++ b/docs/integrations/TICKTAR.md @@ -0,0 +1,101 @@ +# Optional external integration contract + +Ticktar integration is optional. Frame Flow has no runtime dependency on Ticktar. +No external client receives a KoKonna API key, accesses the SQLite database, or +imports a vendor SDK. Web UI and external clients use the same application services. + +## Architecture + +External client → /api/v1 → Application / DisplayService → FrameDeviceDriver → KoKonna. +Configure the caller with FRAME_FLOW_URL and FRAME_FLOW_TOKEN only. No caller-specific +branch exists in core. Frame Flow starts and works with no integration configuration. + +## Authentication and permissions + +Authorization: Bearer FRAME_FLOW_TOKEN. The bootstrap token is provided through +.env or a secret file; database stores SHA-256 hash only. The client model supports +client_id, name, permissions, enabled, created_at, last_used_at. Permissions: +devices:read, images:read, images:delete, display:write, templates:read, render:write, +jobs:read, jobs:write, history:read, settings:write, admin. +0.1.0 seeds one admin token; provisioning scoped client rows is supported by middleware +but multi-token management UI is not implemented. Use a trusted server-to-server +connection; CORS is opt-in by exact origins, never wildcard by default. + +## Discovery and capabilities + +GET /api/v1/devices; GET /api/v1/devices/{id}; GET /api/v1/templates. +Device provides id/name/status, display geometry, power, current_content, firmware, +model/model_code, timezone, last_seen_at, capabilities. Model fallback is flagged +model_verified=false when the cloud omits model fields. Do not branch on driver: +check capabilities upload/gallery/delete/switch/battery/color/remote_refresh. +Native slideshow members/editing are unavailable in KoKonna's documented API. + +## Display + +POST /api/v1/display, Idempotency-Key: a-unique-client-request-key + +```json +{ + "device_id": "living-room", + "content": { + "type": "template", + "template": "daily-dashboard", + "data": {"title": "Good Morning", "events": []}, + "profile": "color" + }, + "options": {"duration": 1800, "restore_previous": true, "priority": 50}, + "source": {"client": "external-app", "request_id": "caller-trace"} +} +``` + +Content supports image (exactly one image_id/image_name/asset_id/base64), image_url +(HTTPS/public DNS/exact allowlist), template. html/markdown explicitly return +UNSUPPORTED_CAPABILITY. For upload-only use device /upload with display_now=false; +this is safe local staging because KoKonna cloud upload always activates an image. + +Returns HTTP 202: {operation_id, job_id, status:"accepted"}. job_id is a legacy +acceptance alias for operation_id, not a scheduler job ID. + +## Render and operations + +POST /api/v1/render with template content returns 202 and a local rendered asset +when completed; no device write. GET /api/v1/operations/{operation_id}: +QUEUED → RENDERING → UPLOADING → SYNCING → COMPLETED / FAILED / UNKNOWN. +Result includes image_id or asset_id/url. SYNCING confirms imageId plus synced +through read-only device status; there is no documented physical-screen telemetry. +Token must own the operation or be admin. Never interpret HTTP 202 as refreshed. + +## Idempotency + +Key is scoped to authenticated client, retained seven days. Same key and same +normalized action/body returns the same operation, even across restart. A different +body with the same key returns CONFLICT (409). Do not retry UNKNOWN with a new key. +Cloud writes have no automatic retries. UNKNOWN blocks further device writes; +admin /operations/{id}/reconcile reads device/gallery and may acknowledge a manually +verified outcome without reissuing a write. + +## Errors and tracing + +All API errors: {error:{code,message,request_id,details:{}}}. Stable codes include +INVALID_REQUEST, UNAUTHORIZED, FORBIDDEN, DEVICE_NOT_FOUND, DEVICE_OFFLINE, +UNSUPPORTED_CAPABILITY, RENDER_FAILED, UPLOAD_FAILED, WRITE_RESULT_UNKNOWN, +RATE_LIMITED, INTERNAL_ERROR, CONFIG_REQUIRED, WRITE_DISABLED, CONFLICT, NOT_FOUND, +CLOUD_UNAVAILABLE. Use code, not message text. Validation does not echo inputs. +X-Request-ID accepts a bounded safe identifier; it is returned on the response. +The authenticated client is authoritative; source.client is optional metadata. +History stores client_id/request_id/operation_id/device_id/action/result/timestamp. +No internal endpoints, headers, stacks, paths or secrets are returned. + +## Future webhook + +SQLite events and webhook_subscriptions reserve operation.completed/failed, +display.changed, device.online/offline/low_battery events. 0.1.0 emits operation/display +events only; no callback delivery, retry worker or registration API is implemented. +Future deliveries should use signed payloads and bounded retries. +OAuth, tenancy, RBAC management and generated SDKs are future work. + +## Compatibility + +/openapi.json and /docs are the contract. Snapshot tests cover paths, response +codes, standard device fields, security and operation enums. Static snapshot is +also supplied in docs/openapi.json. Breaking changes require /api/v2. diff --git a/docs/openapi.json b/docs/openapi.json new file mode 100644 index 0000000..5374a4b --- /dev/null +++ b/docs/openapi.json @@ -0,0 +1,6766 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Pigway Frame Flow API", + "version": "0.1.0" + }, + "paths": { + "/health": { + "get": { + "tags": [ + "Health" + ], + "summary": "Health", + "operationId": "health_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Health" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "Content Too Large", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad Gateway", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/ready": { + "get": { + "tags": [ + "Health" + ], + "summary": "Ready", + "operationId": "ready_ready_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Readiness" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "Content Too Large", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad Gateway", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Readiness" + } + } + } + } + } + } + }, + "/api/v1/version": { + "get": { + "tags": [ + "System" + ], + "summary": "Version", + "operationId": "version_api_v1_version_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Version" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "Content Too Large", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad Gateway", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/devices": { + "get": { + "tags": [ + "Devices" + ], + "summary": "Devices", + "operationId": "devices_api_v1_devices_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Device" + }, + "type": "array", + "title": "Response Devices Api V1 Devices Get" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "Content Too Large", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad Gateway", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/devices/{device_id}": { + "get": { + "tags": [ + "Devices" + ], + "summary": "Device", + "operationId": "device_api_v1_devices__device_id__get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "device_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Device Id" + } + }, + { + "name": "refresh", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Refresh" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Device" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/devices/{device_id}/images": { + "get": { + "tags": [ + "Images" + ], + "summary": "Images", + "operationId": "images_api_v1_devices__device_id__images_get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "device_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Device Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GalleryImage" + }, + "title": "Response Images Api V1 Devices Device Id Images Get" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/devices/{device_id}/images/{image_id}/file": { + "get": { + "tags": [ + "Images" + ], + "summary": "Image File", + "operationId": "image_file_api_v1_devices__device_id__images__image_id__file_get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "device_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Device Id" + } + }, + { + "name": "image_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Image Id" + } + }, + { + "name": "thumbnail", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Thumbnail" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + }, + "image/jpeg": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/display": { + "post": { + "summary": "Display", + "operationId": "display_api_display_post", + "deprecated": true, + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "idempotency-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Idempotency-Key" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DisplayRequest" + } + } + } + }, + "responses": { + "202": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Accepted" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/display": { + "post": { + "tags": [ + "Display" + ], + "summary": "Display", + "operationId": "display_api_v1_display_post", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "idempotency-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Idempotency-Key" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DisplayRequest" + } + } + } + }, + "responses": { + "202": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Accepted" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/devices/{device_id}/display": { + "post": { + "tags": [ + "Display" + ], + "summary": "Device Display", + "operationId": "device_display_api_v1_devices__device_id__display_post", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "device_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Device Id" + } + }, + { + "name": "idempotency-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Idempotency-Key" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DisplayRequest" + } + } + } + }, + "responses": { + "202": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Accepted" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/render": { + "post": { + "tags": [ + "Render" + ], + "summary": "Render", + "operationId": "render_api_v1_render_post", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "idempotency-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Idempotency-Key" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateContent" + } + } + } + }, + "responses": { + "202": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Accepted" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/devices/{device_id}/upload": { + "post": { + "tags": [ + "Images" + ], + "summary": "Upload", + "operationId": "upload_api_v1_devices__device_id__upload_post", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "device_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Device Id" + } + }, + { + "name": "display_now", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Display Now" + } + }, + { + "name": "idempotency-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Idempotency-Key" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageContent" + }, + { + "$ref": "#/components/schemas/URLContent" + } + ], + "title": "Payload" + } + } + } + }, + "responses": { + "202": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Accepted" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/preview": { + "post": { + "tags": [ + "Render" + ], + "summary": "Preview", + "operationId": "preview_api_v1_preview_post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_preview_api_v1_preview_post" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "Content Too Large", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad Gateway", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/assets": { + "get": { + "tags": [ + "Images" + ], + "summary": "List Assets", + "operationId": "list_assets_api_v1_assets_get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "default": 100, + "title": "Limit" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Asset" + }, + "title": "Response List Assets Api V1 Assets Get" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/assets/{asset_id}": { + "delete": { + "tags": [ + "Images" + ], + "summary": "Delete Asset", + "operationId": "delete_asset_api_v1_assets__asset_id__delete", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Asset Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + }, + "get": { + "tags": [ + "Images" + ], + "summary": "Asset File", + "operationId": "asset_file_api_v1_assets__asset_id__get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Asset Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + }, + "image/jpeg": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/devices/{device_id}/images/{image_id}": { + "delete": { + "tags": [ + "Images" + ], + "summary": "Delete Image", + "operationId": "delete_image_api_v1_devices__device_id__images__image_id__delete", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "device_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Device Id" + } + }, + { + "name": "image_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Image Id" + } + }, + { + "name": "confirm_current", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Confirm Current" + } + }, + { + "name": "idempotency-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Idempotency-Key" + } + } + ], + "responses": { + "202": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Accepted" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/devices/{device_id}/restore": { + "post": { + "tags": [ + "Display" + ], + "summary": "Restore", + "operationId": "restore_api_v1_devices__device_id__restore_post", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "device_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Device Id" + } + }, + { + "name": "idempotency-key", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Idempotency-Key" + } + } + ], + "responses": { + "202": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Accepted" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/operations": { + "get": { + "tags": [ + "Operations" + ], + "summary": "Operations", + "operationId": "operations_api_v1_operations_get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "default": 50, + "title": "Limit" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Operation" + }, + "title": "Response Operations Api V1 Operations Get" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/operations/{operation_id}": { + "get": { + "tags": [ + "Operations" + ], + "summary": "Operation", + "operationId": "operation_api_v1_operations__operation_id__get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "operation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Operation Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Operation" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/operations/{operation_id}/reconcile": { + "post": { + "tags": [ + "Operations" + ], + "summary": "Reconcile", + "operationId": "reconcile_api_v1_operations__operation_id__reconcile_post", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "operation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Operation Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReconcileRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Reconciliation" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/templates": { + "get": { + "tags": [ + "Templates" + ], + "summary": "Templates", + "operationId": "templates_api_v1_templates_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/TemplateInfo" + }, + "type": "array", + "title": "Response Templates Api V1 Templates Get" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "Content Too Large", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad Gateway", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/templates/{template_id}": { + "get": { + "tags": [ + "Templates" + ], + "summary": "Template", + "operationId": "template_api_v1_templates__template_id__get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "template_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Template Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateInfo" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/templates/{template_id}/config": { + "put": { + "tags": [ + "Templates" + ], + "summary": "Template Config", + "operationId": "template_config_api_v1_templates__template_id__config_put", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "template_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Template Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateConfig" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateConfig" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/jobs": { + "get": { + "tags": [ + "Automation" + ], + "summary": "Jobs", + "operationId": "jobs_api_v1_jobs_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Job" + }, + "type": "array", + "title": "Response Jobs Api V1 Jobs Get" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "Content Too Large", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad Gateway", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + }, + "post": { + "tags": [ + "Automation" + ], + "summary": "Create Job", + "operationId": "create_job_api_v1_jobs_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JobInput" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Job" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "Content Too Large", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad Gateway", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/jobs/{job_id}": { + "put": { + "tags": [ + "Automation" + ], + "summary": "Update Job", + "operationId": "update_job_api_v1_jobs__job_id__put", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Job Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JobInput" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Job" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + }, + "delete": { + "tags": [ + "Automation" + ], + "summary": "Delete Job", + "operationId": "delete_job_api_v1_jobs__job_id__delete", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Job Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/playlists": { + "get": { + "tags": [ + "Playlists" + ], + "summary": "Playlists", + "operationId": "playlists_api_v1_playlists_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Playlist" + }, + "type": "array", + "title": "Response Playlists Api V1 Playlists Get" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "Content Too Large", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad Gateway", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + }, + "post": { + "tags": [ + "Playlists" + ], + "summary": "Create Playlist", + "operationId": "create_playlist_api_v1_playlists_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaylistInput" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Playlist" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "Content Too Large", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad Gateway", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/playlists/{playlist_id}": { + "put": { + "tags": [ + "Playlists" + ], + "summary": "Update Playlist", + "operationId": "update_playlist_api_v1_playlists__playlist_id__put", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "playlist_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Playlist Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaylistInput" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Playlist" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + }, + "delete": { + "tags": [ + "Playlists" + ], + "summary": "Delete Playlist", + "operationId": "delete_playlist_api_v1_playlists__playlist_id__delete", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "playlist_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Playlist Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/history": { + "get": { + "tags": [ + "History" + ], + "summary": "History", + "operationId": "history_api_v1_history_get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "default": 100, + "title": "Limit" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/History" + }, + "title": "Response History Api V1 History Get" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } + }, + "/api/v1/settings": { + "get": { + "tags": [ + "Settings" + ], + "summary": "Settings View", + "operationId": "settings_view_api_v1_settings_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SettingsView" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "Content Too Large", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad Gateway", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + } + }, + "components": { + "schemas": { + "Accepted": { + "properties": { + "operation_id": { + "type": "string", + "title": "Operation Id" + }, + "job_id": { + "type": "string", + "title": "Job Id" + }, + "status": { + "type": "string", + "const": "accepted", + "title": "Status", + "default": "accepted" + } + }, + "type": "object", + "required": [ + "operation_id", + "job_id" + ], + "title": "Accepted" + }, + "Asset": { + "properties": { + "asset_id": { + "type": "string", + "title": "Asset Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "url": { + "type": "string", + "title": "Url" + }, + "created_at": { + "type": "string", + "title": "Created At" + } + }, + "type": "object", + "required": [ + "asset_id", + "name", + "url", + "created_at" + ], + "title": "Asset" + }, + "Body_preview_api_v1_preview_post": { + "properties": { + "file": { + "type": "string", + "contentMediaType": "application/octet-stream", + "title": "File" + }, + "fit": { + "$ref": "#/components/schemas/Fit", + "default": "contain" + }, + "profile": { + "$ref": "#/components/schemas/Profile", + "default": "color" + } + }, + "type": "object", + "required": [ + "file" + ], + "title": "Body_preview_api_v1_preview_post" + }, + "Capabilities": { + "properties": { + "upload": { + "type": "boolean", + "title": "Upload", + "default": true + }, + "gallery": { + "type": "boolean", + "title": "Gallery", + "default": true + }, + "delete": { + "type": "boolean", + "title": "Delete", + "default": true + }, + "switch": { + "type": "boolean", + "title": "Switch", + "default": true + }, + "remote_refresh": { + "type": "boolean", + "title": "Remote Refresh", + "default": true + }, + "battery": { + "type": "boolean", + "title": "Battery", + "default": true + }, + "color": { + "type": "boolean", + "title": "Color", + "default": true + }, + "cloud_upload_without_display": { + "type": "boolean", + "title": "Cloud Upload Without Display", + "default": false + } + }, + "type": "object", + "title": "Capabilities" + }, + "CurrentContent": { + "properties": { + "type": { + "type": "string", + "const": "image", + "title": "Type", + "default": "image" + }, + "id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Id" + }, + "cover_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Cover Id" + }, + "synced": { + "type": "boolean", + "title": "Synced" + } + }, + "type": "object", + "required": [ + "id", + "cover_id", + "synced" + ], + "title": "CurrentContent" + }, + "Device": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "driver": { + "type": "string", + "const": "kokonna", + "title": "Driver", + "default": "kokonna" + }, + "status": { + "type": "string", + "enum": [ + "online", + "offline" + ], + "title": "Status" + }, + "model": { + "type": "string", + "title": "Model" + }, + "model_code": { + "type": "string", + "title": "Model Code" + }, + "firmware": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Firmware" + }, + "display": { + "$ref": "#/components/schemas/DisplayGeometry" + }, + "power": { + "$ref": "#/components/schemas/Power" + }, + "current_content": { + "$ref": "#/components/schemas/CurrentContent" + }, + "last_seen_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Seen At" + }, + "timezone": { + "type": "string", + "title": "Timezone" + }, + "capabilities": { + "$ref": "#/components/schemas/Capabilities" + }, + "slideshow": { + "additionalProperties": true, + "type": "object", + "title": "Slideshow" + }, + "storage_used_bytes": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Storage Used Bytes" + }, + "model_verified": { + "type": "boolean", + "title": "Model Verified", + "default": false + } + }, + "type": "object", + "required": [ + "id", + "name", + "status", + "model", + "model_code", + "firmware", + "display", + "power", + "current_content", + "last_seen_at", + "timezone" + ], + "title": "Device" + }, + "DisplayGeometry": { + "properties": { + "width": { + "type": "integer", + "title": "Width" + }, + "height": { + "type": "integer", + "title": "Height" + }, + "rotation": { + "type": "integer", + "title": "Rotation" + }, + "color_mode": { + "$ref": "#/components/schemas/Profile", + "default": "color" + } + }, + "type": "object", + "required": [ + "width", + "height", + "rotation" + ], + "title": "DisplayGeometry" + }, + "DisplayRequest": { + "properties": { + "device_id": { + "type": "string", + "title": "Device Id", + "default": "living-room" + }, + "content": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImageContent" + }, + { + "$ref": "#/components/schemas/URLContent" + }, + { + "$ref": "#/components/schemas/TemplateContent" + }, + { + "$ref": "#/components/schemas/FutureContent" + } + ], + "title": "Content", + "discriminator": { + "propertyName": "type", + "mapping": { + "html": "#/components/schemas/FutureContent", + "image": "#/components/schemas/ImageContent", + "image_url": "#/components/schemas/URLContent", + "markdown": "#/components/schemas/FutureContent", + "template": "#/components/schemas/TemplateContent" + } + } + }, + "options": { + "$ref": "#/components/schemas/Options" + }, + "source": { + "$ref": "#/components/schemas/Source" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "content" + ], + "title": "DisplayRequest" + }, + "ErrorBody": { + "properties": { + "code": { + "$ref": "#/components/schemas/ErrorCode" + }, + "message": { + "type": "string", + "title": "Message" + }, + "request_id": { + "type": "string", + "title": "Request Id" + }, + "details": { + "additionalProperties": true, + "type": "object", + "title": "Details" + } + }, + "type": "object", + "required": [ + "code", + "message", + "request_id" + ], + "title": "ErrorBody" + }, + "ErrorCode": { + "type": "string", + "enum": [ + "INVALID_REQUEST", + "UNAUTHORIZED", + "FORBIDDEN", + "DEVICE_NOT_FOUND", + "DEVICE_OFFLINE", + "UNSUPPORTED_CAPABILITY", + "RENDER_FAILED", + "UPLOAD_FAILED", + "WRITE_RESULT_UNKNOWN", + "RATE_LIMITED", + "INTERNAL_ERROR", + "CLOUD_UNAVAILABLE", + "NOT_FOUND", + "CONFLICT", + "CONFIG_REQUIRED", + "WRITE_DISABLED" + ], + "title": "ErrorCode" + }, + "ErrorResponse": { + "properties": { + "error": { + "$ref": "#/components/schemas/ErrorBody" + } + }, + "type": "object", + "required": [ + "error" + ], + "title": "ErrorResponse" + }, + "Fit": { + "type": "string", + "enum": [ + "contain", + "cover", + "smart" + ], + "title": "Fit" + }, + "FutureContent": { + "properties": { + "type": { + "type": "string", + "enum": [ + "html", + "markdown" + ], + "title": "Type" + }, + "data": { + "type": "string", + "title": "Data", + "default": "" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "FutureContent" + }, + "GalleryImage": { + "properties": { + "width": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Width" + }, + "height": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Height" + }, + "size_bytes": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Size Bytes" + }, + "media_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Media Type" + }, + "image_id": { + "type": "integer", + "title": "Image Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created At" + }, + "current": { + "type": "boolean", + "title": "Current" + }, + "url": { + "type": "string", + "title": "Url" + } + }, + "type": "object", + "required": [ + "image_id", + "name", + "created_at", + "current", + "url" + ], + "title": "GalleryImage" + }, + "Health": { + "properties": { + "status": { + "type": "string", + "title": "Status" + }, + "version": { + "type": "string", + "title": "Version", + "default": "0.1.0" + } + }, + "type": "object", + "required": [ + "status" + ], + "title": "Health" + }, + "History": { + "properties": { + "id": { + "type": "integer", + "title": "Id" + }, + "client_id": { + "type": "string", + "title": "Client Id" + }, + "request_id": { + "type": "string", + "title": "Request Id" + }, + "operation_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Operation Id" + }, + "device_id": { + "type": "string", + "title": "Device Id" + }, + "action": { + "type": "string", + "title": "Action" + }, + "result": { + "type": "string", + "title": "Result" + }, + "timestamp": { + "type": "string", + "title": "Timestamp" + } + }, + "type": "object", + "required": [ + "id", + "client_id", + "request_id", + "operation_id", + "device_id", + "action", + "result", + "timestamp" + ], + "title": "History" + }, + "ImageContent": { + "properties": { + "type": { + "type": "string", + "const": "image", + "title": "Type" + }, + "image_id": { + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Image Id" + }, + "image_name": { + "anyOf": [ + { + "type": "string", + "maxLength": 120, + "minLength": 1 + }, + { + "type": "null" + } + ], + "title": "Image Name" + }, + "asset_id": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-f0-9]{32}$" + }, + { + "type": "null" + } + ], + "title": "Asset Id" + }, + "base64": { + "anyOf": [ + { + "type": "string", + "maxLength": 28000000 + }, + { + "type": "null" + } + ], + "title": "Base64" + }, + "name": { + "type": "string", + "maxLength": 120, + "minLength": 1, + "title": "Name", + "default": "image.jpg" + }, + "fit": { + "$ref": "#/components/schemas/Fit", + "default": "contain" + }, + "profile": { + "$ref": "#/components/schemas/Profile", + "default": "color" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "ImageContent" + }, + "Job": { + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "default": true + }, + "device_id": { + "type": "string", + "title": "Device Id", + "default": "living-room" + }, + "name": { + "type": "string", + "maxLength": 120, + "minLength": 1, + "title": "Name" + }, + "schedule": { + "$ref": "#/components/schemas/Schedule" + }, + "source": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/ImageContent" + }, + { + "$ref": "#/components/schemas/URLContent" + }, + { + "$ref": "#/components/schemas/TemplateContent" + }, + { + "$ref": "#/components/schemas/FutureContent" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "html": "#/components/schemas/FutureContent", + "image": "#/components/schemas/ImageContent", + "image_url": "#/components/schemas/URLContent", + "markdown": "#/components/schemas/FutureContent", + "template": "#/components/schemas/TemplateContent" + } + } + }, + { + "$ref": "#/components/schemas/RestoreContent" + } + ], + "title": "Source" + }, + "display_duration": { + "type": "integer", + "maximum": 604800.0, + "minimum": 0.0, + "title": "Display Duration", + "default": 0 + }, + "restore_previous": { + "type": "boolean", + "title": "Restore Previous", + "default": false + }, + "priority": { + "type": "integer", + "maximum": 100.0, + "minimum": 0.0, + "title": "Priority", + "default": 50 + }, + "id": { + "type": "string", + "title": "Id" + }, + "created_at": { + "type": "string", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "title": "Updated At" + }, + "last_run": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Run" + }, + "last_result": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Result" + }, + "next_run": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Run" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "schedule", + "source", + "id", + "created_at", + "updated_at" + ], + "title": "Job" + }, + "JobInput": { + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "default": true + }, + "device_id": { + "type": "string", + "title": "Device Id", + "default": "living-room" + }, + "name": { + "type": "string", + "maxLength": 120, + "minLength": 1, + "title": "Name" + }, + "schedule": { + "$ref": "#/components/schemas/Schedule" + }, + "source": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/ImageContent" + }, + { + "$ref": "#/components/schemas/URLContent" + }, + { + "$ref": "#/components/schemas/TemplateContent" + }, + { + "$ref": "#/components/schemas/FutureContent" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "html": "#/components/schemas/FutureContent", + "image": "#/components/schemas/ImageContent", + "image_url": "#/components/schemas/URLContent", + "markdown": "#/components/schemas/FutureContent", + "template": "#/components/schemas/TemplateContent" + } + } + }, + { + "$ref": "#/components/schemas/RestoreContent" + } + ], + "title": "Source" + }, + "display_duration": { + "type": "integer", + "maximum": 604800.0, + "minimum": 0.0, + "title": "Display Duration", + "default": 0 + }, + "restore_previous": { + "type": "boolean", + "title": "Restore Previous", + "default": false + }, + "priority": { + "type": "integer", + "maximum": 100.0, + "minimum": 0.0, + "title": "Priority", + "default": 50 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "schedule", + "source" + ], + "title": "JobInput" + }, + "Operation": { + "properties": { + "operation_id": { + "type": "string", + "title": "Operation Id" + }, + "status": { + "$ref": "#/components/schemas/OperationStatus" + }, + "device_id": { + "type": "string", + "title": "Device Id" + }, + "created_at": { + "type": "string", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "title": "Updated At" + }, + "result": { + "anyOf": [ + { + "$ref": "#/components/schemas/OperationResult" + }, + { + "type": "null" + } + ] + }, + "error": { + "anyOf": [ + { + "$ref": "#/components/schemas/ErrorBody" + }, + { + "type": "null" + } + ] + }, + "request_id": { + "type": "string", + "title": "Request Id" + }, + "client_id": { + "type": "string", + "title": "Client Id" + }, + "action": { + "type": "string", + "title": "Action" + } + }, + "type": "object", + "required": [ + "operation_id", + "status", + "device_id", + "created_at", + "updated_at", + "request_id", + "client_id", + "action" + ], + "title": "Operation" + }, + "OperationResult": { + "properties": { + "image_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Image Id" + }, + "asset_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Asset Id" + }, + "rendered_asset_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Rendered Asset Id" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "deferred": { + "type": "boolean", + "title": "Deferred", + "default": false + } + }, + "type": "object", + "title": "OperationResult" + }, + "OperationStatus": { + "type": "string", + "enum": [ + "QUEUED", + "RENDERING", + "UPLOADING", + "SYNCING", + "COMPLETED", + "FAILED", + "UNKNOWN" + ], + "title": "OperationStatus" + }, + "Options": { + "properties": { + "duration": { + "type": "integer", + "maximum": 604800.0, + "minimum": 0.0, + "title": "Duration", + "default": 0 + }, + "restore_previous": { + "type": "boolean", + "title": "Restore Previous", + "default": false + }, + "priority": { + "type": "integer", + "maximum": 100.0, + "minimum": 0.0, + "title": "Priority", + "default": 50 + } + }, + "additionalProperties": false, + "type": "object", + "title": "Options" + }, + "Playlist": { + "properties": { + "device_id": { + "type": "string", + "title": "Device Id", + "default": "living-room" + }, + "name": { + "type": "string", + "maxLength": 120, + "minLength": 1, + "title": "Name" + }, + "image_ids": { + "items": { + "type": "integer" + }, + "type": "array", + "maxItems": 100, + "minItems": 1, + "title": "Image Ids" + }, + "interval_minutes": { + "type": "integer", + "maximum": 10080.0, + "minimum": 5.0, + "title": "Interval Minutes", + "default": 60 + }, + "mode": { + "type": "string", + "enum": [ + "queue", + "random" + ], + "title": "Mode", + "default": "queue" + }, + "enabled": { + "type": "boolean", + "title": "Enabled", + "default": false + }, + "id": { + "type": "string", + "title": "Id" + }, + "position": { + "type": "integer", + "title": "Position" + }, + "next_run": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Run" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "image_ids", + "id", + "position", + "next_run" + ], + "title": "Playlist" + }, + "PlaylistInput": { + "properties": { + "device_id": { + "type": "string", + "title": "Device Id", + "default": "living-room" + }, + "name": { + "type": "string", + "maxLength": 120, + "minLength": 1, + "title": "Name" + }, + "image_ids": { + "items": { + "type": "integer" + }, + "type": "array", + "maxItems": 100, + "minItems": 1, + "title": "Image Ids" + }, + "interval_minutes": { + "type": "integer", + "maximum": 10080.0, + "minimum": 5.0, + "title": "Interval Minutes", + "default": 60 + }, + "mode": { + "type": "string", + "enum": [ + "queue", + "random" + ], + "title": "Mode", + "default": "queue" + }, + "enabled": { + "type": "boolean", + "title": "Enabled", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "image_ids" + ], + "title": "PlaylistInput" + }, + "Power": { + "properties": { + "battery_percent": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Battery Percent" + }, + "charging": { + "type": "boolean", + "title": "Charging" + } + }, + "type": "object", + "required": [ + "battery_percent", + "charging" + ], + "title": "Power" + }, + "Profile": { + "type": "string", + "enum": [ + "color", + "grayscale" + ], + "title": "Profile" + }, + "Readiness": { + "properties": { + "status": { + "type": "string", + "title": "Status" + }, + "checks": { + "additionalProperties": { + "type": "boolean" + }, + "type": "object", + "title": "Checks" + } + }, + "type": "object", + "required": [ + "status", + "checks" + ], + "title": "Readiness" + }, + "ReconcileRequest": { + "properties": { + "acknowledge": { + "type": "boolean", + "title": "Acknowledge", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "title": "ReconcileRequest" + }, + "Reconciliation": { + "properties": { + "operation_id": { + "type": "string", + "title": "Operation Id" + }, + "current_image_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Current Image Id" + }, + "expected_image_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Expected Image Id" + }, + "cloud_images": { + "items": { + "$ref": "#/components/schemas/GalleryImage" + }, + "type": "array", + "title": "Cloud Images" + }, + "resolved": { + "type": "boolean", + "title": "Resolved" + } + }, + "type": "object", + "required": [ + "operation_id", + "current_image_id", + "expected_image_id", + "cloud_images", + "resolved" + ], + "title": "Reconciliation" + }, + "RestoreContent": { + "properties": { + "type": { + "type": "string", + "const": "restore", + "title": "Type" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "RestoreContent" + }, + "Schedule": { + "properties": { + "type": { + "type": "string", + "enum": [ + "cron", + "daily", + "weekdays", + "weekends", + "date", + "temporary" + ], + "title": "Type" + }, + "cron": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cron" + }, + "time": { + "anyOf": [ + { + "type": "string", + "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$" + }, + { + "type": "null" + } + ], + "title": "Time" + }, + "at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "At" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "Schedule" + }, + "SettingsView": { + "properties": { + "version": { + "type": "string", + "title": "Version" + }, + "mock": { + "type": "boolean", + "title": "Mock" + }, + "write_enabled": { + "type": "boolean", + "title": "Write Enabled" + }, + "api_key_configured": { + "type": "boolean", + "title": "Api Key Configured" + }, + "api_token_configured": { + "type": "boolean", + "title": "Api Token Configured" + }, + "timezone": { + "type": "string", + "title": "Timezone" + }, + "device_id": { + "type": "string", + "title": "Device Id" + }, + "weather_provider": { + "type": "string", + "title": "Weather Provider" + } + }, + "type": "object", + "required": [ + "version", + "mock", + "write_enabled", + "api_key_configured", + "api_token_configured", + "timezone", + "device_id", + "weather_provider" + ], + "title": "SettingsView" + }, + "Source": { + "properties": { + "client": { + "anyOf": [ + { + "type": "string", + "maxLength": 80 + }, + { + "type": "null" + } + ], + "title": "Client" + }, + "request_id": { + "anyOf": [ + { + "type": "string", + "maxLength": 120 + }, + { + "type": "null" + } + ], + "title": "Request Id" + } + }, + "additionalProperties": false, + "type": "object", + "title": "Source" + }, + "TemplateConfig": { + "properties": { + "data": { + "additionalProperties": true, + "type": "object", + "title": "Data" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "data" + ], + "title": "TemplateConfig" + }, + "TemplateContent": { + "properties": { + "type": { + "type": "string", + "const": "template", + "title": "Type" + }, + "template": { + "type": "string", + "pattern": "^[a-z0-9-]{1,60}$", + "title": "Template" + }, + "data": { + "additionalProperties": true, + "type": "object", + "title": "Data" + }, + "profile": { + "$ref": "#/components/schemas/Profile", + "default": "color" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "template" + ], + "title": "TemplateContent" + }, + "TemplateInfo": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "width": { + "type": "integer", + "title": "Width", + "default": 1200 + }, + "height": { + "type": "integer", + "title": "Height", + "default": 1600 + }, + "fields": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Fields" + }, + "defaults": { + "additionalProperties": true, + "type": "object", + "title": "Defaults" + } + }, + "type": "object", + "required": [ + "id", + "name", + "fields", + "defaults" + ], + "title": "TemplateInfo" + }, + "URLContent": { + "properties": { + "type": { + "type": "string", + "const": "image_url", + "title": "Type" + }, + "url": { + "type": "string", + "maxLength": 2048, + "title": "Url" + }, + "name": { + "type": "string", + "maxLength": 120, + "title": "Name", + "default": "image.jpg" + }, + "fit": { + "$ref": "#/components/schemas/Fit", + "default": "contain" + }, + "profile": { + "$ref": "#/components/schemas/Profile", + "default": "color" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "url" + ], + "title": "URLContent" + }, + "Version": { + "properties": { + "product": { + "type": "string", + "title": "Product", + "default": "Pigway Frame Flow" + }, + "version": { + "type": "string", + "title": "Version", + "default": "0.1.0" + }, + "api_version": { + "type": "string", + "title": "Api Version", + "default": "v1" + }, + "commit": { + "type": "string", + "title": "Commit", + "default": "development" + }, + "build_time": { + "type": "string", + "title": "Build Time", + "default": "unknown" + } + }, + "type": "object", + "title": "Version" + } + }, + "securitySchemes": { + "HTTPBearer": { + "type": "http", + "scheme": "bearer" + } + } + } +} diff --git a/pigway_frame_flow/__init__.py b/pigway_frame_flow/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pigway_frame_flow/api/__init__.py b/pigway_frame_flow/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pigway_frame_flow/api/app.py b/pigway_frame_flow/api/app.py new file mode 100644 index 0000000..a523681 --- /dev/null +++ b/pigway_frame_flow/api/app.py @@ -0,0 +1,749 @@ +import logging +import os +import re +from contextlib import asynccontextmanager +from pathlib import Path +from typing import Any +from uuid import uuid4 + +from fastapi import Depends, FastAPI, File, Form, Header, Query, Request, UploadFile +from fastapi.concurrency import run_in_threadpool +from fastapi.exceptions import RequestValidationError +from fastapi.middleware.cors import CORSMiddleware +from fastapi.openapi.docs import get_swagger_ui_html +from fastapi.responses import FileResponse, JSONResponse, Response +from starlette.exceptions import HTTPException + +from pigway_frame_flow.core.auth import require, seed_client +from pigway_frame_flow.core.registry import DeviceRegistry +from pigway_frame_flow.display.service import DisplayService +from pigway_frame_flow.errors import ErrorCode as E +from pigway_frame_flow.errors import FlowError +from pigway_frame_flow.models import ( + Accepted, + Asset, + Device, + DisplayRequest, + ErrorResponse, + Fit, + GalleryImage, + Health, + History, + ImageContent, + Job, + JobInput, + Operation, + Playlist, + PlaylistInput, + Profile, + Readiness, + ReconcileRequest, + Reconciliation, + SettingsView, + TemplateConfig, + TemplateContent, + TemplateInfo, + URLContent, + Version, +) +from pigway_frame_flow.providers.weather import MockWeatherProvider +from pigway_frame_flow.renderers.images import Assets, convert +from pigway_frame_flow.renderers.templates import TemplateRenderer +from pigway_frame_flow.scheduler.engine import Scheduler +from pigway_frame_flow.settings import Settings +from pigway_frame_flow.storage.database import Database, encode + + +def create_app(settings=None, start_worker=True): + settings = settings or Settings() + for kind in ["images", "generated", "cache"]: + (settings.data_dir / kind).mkdir(parents=True, exist_ok=True) + settings.config_dir.mkdir(parents=True, exist_ok=True) + db = Database(settings.data_dir / "frameflow.db") + registry = DeviceRegistry(settings) + assets = Assets(db, settings) + renderer = TemplateRenderer(db, settings, MockWeatherProvider(), assets) + service = DisplayService(db, settings, registry, assets, renderer) + scheduler = Scheduler(service) + seed_client(db, settings.api_token) + for name in ["urllib3", "requests", "httpx", "httpcore"]: + logging.getLogger(name).setLevel(logging.CRITICAL) + + @asynccontextmanager + async def lifespan(app): + if start_worker: + scheduler.start() + yield + scheduler.stop() + db.close() + + error_responses: dict[int | str, dict[str, Any]] = { + code: {"model": ErrorResponse} + for code in [400, 401, 403, 404, 409, 413, 422, 429, 500, 502, 503] + } + app = FastAPI( + title="Pigway Frame Flow API", + docs_url=None, + version="0.1.0", + lifespan=lifespan, + responses=error_responses, + ) + app.state.service = service + app.state.scheduler = scheduler + app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_methods=["GET", "POST", "PUT", "DELETE"], + allow_headers=[ + "Authorization", + "Content-Type", + "Idempotency-Key", + "X-Request-ID", + ], + ) + + def error_response(code, message, status, request): + return JSONResponse( + status_code=status, + content={ + "error": { + "code": code, + "message": message, + "request_id": getattr(request.state, "request_id", uuid4().hex), + "details": {}, + } + }, + ) + + @app.middleware("http") + async def request_context(request, call_next): + supplied = request.headers.get("X-Request-ID", "") + request.state.request_id = ( + supplied + if re.fullmatch(r"[A-Za-z0-9_.:-]{1,120}", supplied) + else uuid4().hex + ) + if any( + s and s in request.state.request_id + for s in [settings.api_key, settings.api_token, *registry.keys] + ): + request.state.request_id = uuid4().hex + if request.method in ["POST", "PUT", "DELETE"]: + try: + chunks = [] + total = 0 + async for chunk in request.stream(): + total += len(chunk) + if total > 29_000_000: + return error_response( + E.INVALID_REQUEST, + "Request exceeds size limit", + 413, + request, + ) + chunks.append(chunk) + request._body = b"".join(chunks) + except Exception: + return error_response( + E.INVALID_REQUEST, "Invalid request body", 400, request + ) + try: + response = await call_next(request) + except Exception: + response = error_response( + E.INTERNAL_ERROR, "Internal service error", 500, request + ) + response.headers["X-Request-ID"] = request.state.request_id + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["Referrer-Policy"] = "no-referrer" + response.headers["Cache-Control"] = "no-store" + response.headers["Content-Security-Policy"] = ( + "default-src 'self'; img-src 'self' blob: data:; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; connect-src 'self'; frame-ancestors 'none'" + ) + return response + + @app.exception_handler(FlowError) + async def flow_error(request, exc): + return error_response(exc.code, exc.message, exc.status, request) + + @app.exception_handler(RequestValidationError) + async def validation_error(request, exc): + return error_response( + E.INVALID_REQUEST, "Request validation failed", 422, request + ) + + @app.exception_handler(HTTPException) + async def http_error(request, exc): + return error_response( + E.NOT_FOUND if exc.status_code == 404 else E.INVALID_REQUEST, + "Resource not found" if exc.status_code == 404 else "HTTP request rejected", + exc.status_code, + request, + ) + + def accepted(id): + return Accepted(operation_id=id, job_id=id) + + def submit(request, action, payload, device, key): + if key and not re.fullmatch(r"[A-Za-z0-9_.:-]{1,200}", key): + raise FlowError(E.INVALID_REQUEST, "Invalid Idempotency-Key") + return accepted( + service.submit( + action, + payload, + device, + request.state.client_id, + request.state.request_id, + key, + ) + ) + + def write_guard(device): + registry.get(device) + if not settings.mock and not settings.write_enabled: + raise FlowError(E.WRITE_DISABLED, "Real device writes are disabled", 403) + + @app.get("/health", response_model=Health, tags=["Health"]) + def health(): + return Health(status="ok") + + @app.get( + "/ready", + response_model=Readiness, + tags=["Health"], + responses={503: {"model": Readiness}}, + ) + def ready(): + checks = { + "sqlite": bool(db.one("SELECT 1 AS ok")), + "device_config": bool(registry.drivers) + and (settings.mock or all(registry.keys)), + "scheduler": scheduler.healthy() if start_worker else True, + "authentication": bool( + db.one("SELECT client_id FROM clients WHERE enabled=1") + ), + } + good = all(checks.values()) + return JSONResponse( + status_code=200 if good else 503, + content={"status": "ready" if good else "not_ready", "checks": checks}, + ) + + @app.get("/api/v1/version", response_model=Version, tags=["System"]) + def version(): + return Version( + commit=os.getenv("BUILD_COMMIT", "development"), + build_time=os.getenv("BUILD_TIME", "unknown"), + ) + + @app.get( + "/api/v1/devices", + response_model=list[Device], + tags=["Devices"], + dependencies=[Depends(require("devices:read"))], + ) + def devices(): + return [d.device() for d in registry.drivers.values()] + + @app.get( + "/api/v1/devices/{device_id}", + response_model=Device, + tags=["Devices"], + dependencies=[Depends(require("devices:read"))], + ) + def device(device_id: str, refresh: bool = False): + return registry.get(device_id).device(refresh) + + @app.get( + "/api/v1/devices/{device_id}/images", + response_model=list[GalleryImage], + tags=["Images"], + dependencies=[Depends(require("images:read"))], + ) + def images(device_id: str): + return registry.get(device_id).images() + + @app.get( + "/api/v1/devices/{device_id}/images/{image_id}/file", + tags=["Images"], + dependencies=[Depends(require("images:read"))], + responses={ + 200: { + "content": { + "image/jpeg": {"schema": {"type": "string", "format": "binary"}} + } + } + }, + ) + def image_file(device_id: str, image_id: int, thumbnail: bool = False): + if image_id < 1: + raise FlowError(E.INVALID_REQUEST, "Image ID must be positive") + driver = registry.get(device_id) + cached = settings.data_dir / "cache" / f"{device_id}-{image_id}.original" + if not cached.exists(): + data = driver.download(image_id) + # Validate and strip EXIF without changing original geometry. + from io import BytesIO + + from PIL import Image + + try: + with Image.open(BytesIO(data)) as source: + if source.width * source.height > 25_000_000: + raise ValueError() + if source.format not in {"JPEG", "PNG", "WEBP"}: + raise ValueError() + source.verify() + cached.write_bytes(data) + except Exception: + raise FlowError(E.INVALID_REQUEST, "Invalid cloud image") from None + if thumbnail: + thumb = cached.with_name(cached.stem + "-thumb.jpg") + if not thumb.exists(): + from PIL import Image + + with Image.open(cached) as img: + img.thumbnail((300, 400)) + img.save(thumb, "JPEG") + return FileResponse(thumb, media_type="image/jpeg") + from PIL import Image + + with Image.open(cached) as original: + media_type = Image.MIME.get( + original.format or "", "application/octet-stream" + ) + return FileResponse(cached, media_type=media_type) + + @app.post( + "/api/v1/display", + response_model=Accepted, + status_code=202, + tags=["Display"], + dependencies=[Depends(require("display:write"))], + ) + @app.post( + "/api/display", + response_model=Accepted, + status_code=202, + deprecated=True, + dependencies=[Depends(require("display:write"))], + ) + def display( + payload: DisplayRequest, + request: Request, + idempotency_key: str | None = Header(default=None), + ): + service.validate(payload) + write_guard(payload.device_id) + return submit( + request, + "display", + payload.model_dump(mode="json"), + payload.device_id, + idempotency_key, + ) + + @app.post( + "/api/v1/devices/{device_id}/display", + response_model=Accepted, + status_code=202, + tags=["Display"], + dependencies=[Depends(require("display:write"))], + ) + def device_display( + device_id: str, + payload: DisplayRequest, + request: Request, + idempotency_key: str | None = Header(default=None), + ): + payload = payload.model_copy(update={"device_id": device_id}) + return display(payload, request, idempotency_key) + + @app.post( + "/api/v1/render", + response_model=Accepted, + status_code=202, + tags=["Render"], + dependencies=[Depends(require("render:write"))], + ) + def render( + payload: TemplateContent, + request: Request, + idempotency_key: str | None = Header(default=None), + ): + renderer.info(payload.template) + return submit( + request, + "render", + payload.model_dump(mode="json"), + settings.device_id, + idempotency_key, + ) + + @app.post( + "/api/v1/devices/{device_id}/upload", + response_model=Accepted, + status_code=202, + tags=["Images"], + dependencies=[Depends(require("display:write"))], + ) + def upload( + device_id: str, + payload: ImageContent | URLContent, + request: Request, + display_now: bool = False, + idempotency_key: str | None = Header(default=None), + ): + registry.get(device_id) + if isinstance(payload, ImageContent) and ( + payload.image_id or payload.image_name + ): + raise FlowError(E.INVALID_REQUEST, "Upload needs image bytes or an asset") + if display_now: + return display( + DisplayRequest(device_id=device_id, content=payload), + request, + idempotency_key, + ) + return submit( + request, + "local-upload", + payload.model_dump(mode="json"), + device_id, + idempotency_key, + ) + + @app.post( + "/api/v1/preview", + response_model=Asset, + tags=["Render"], + dependencies=[Depends(require("render:write"))], + ) + async def preview( + request: Request, + file: UploadFile = File(), + fit: Fit = Form(default=Fit.contain), + profile: Profile = Form(default=Profile.color), + ): + data = await file.read(settings.max_bytes + 1) + if len(data) > settings.max_bytes: + raise FlowError(E.INVALID_REQUEST, "Image exceeds 20 MiB") + name = Path(file.filename or "image").name[:120] + asset = assets.put( + await run_in_threadpool(convert, data, fit, profile), + settings.sanitize(name), + ) + return asset + + @app.get( + "/api/v1/assets", + response_model=list[Asset], + tags=["Images"], + dependencies=[Depends(require("images:read"))], + ) + def list_assets(limit: int = Query(default=100, ge=1, le=200)): + return [ + assets.info(r["id"]) + for r in db.all( + "SELECT id FROM assets ORDER BY created_at DESC LIMIT ?", (limit,) + ) + ] + + @app.delete( + "/api/v1/assets/{asset_id}", + status_code=204, + tags=["Images"], + dependencies=[Depends(require("images:delete"))], + ) + def delete_asset(asset_id: str): + path = assets.path(asset_id) + reference = "%" + asset_id + "%" + if ( + db.one( + "SELECT id FROM operations WHERE payload LIKE ? AND status IN ('QUEUED','RENDERING','UPLOADING','SYNCING','UNKNOWN')", + (reference,), + ) + or db.one("SELECT id FROM jobs WHERE payload LIKE ?", (reference,)) + or db.one( + "SELECT template_id FROM template_config WHERE payload LIKE ?", + (reference,), + ) + ): + raise FlowError( + E.CONFLICT, + "Asset is referenced by a task, template or pending operation", + 409, + ) + path.unlink(missing_ok=True) + db.execute("DELETE FROM assets WHERE id=?", (asset_id,)) + return Response(status_code=204) + + @app.get( + "/api/v1/assets/{asset_id}", + tags=["Images"], + dependencies=[Depends(require("images:read"))], + responses={ + 200: { + "content": { + "image/jpeg": {"schema": {"type": "string", "format": "binary"}} + } + } + }, + ) + def asset_file(asset_id: str): + return FileResponse(assets.path(asset_id), media_type="image/jpeg") + + @app.delete( + "/api/v1/devices/{device_id}/images/{image_id}", + response_model=Accepted, + status_code=202, + tags=["Images"], + dependencies=[Depends(require("images:delete"))], + ) + def delete_image( + device_id: str, + image_id: int, + request: Request, + confirm_current: bool = False, + idempotency_key: str | None = Header(default=None), + ): + write_guard(device_id) + if image_id < 1: + raise FlowError(E.INVALID_REQUEST, "Image ID must be positive") + if ( + registry.get(device_id).get_current_image() == image_id + and not confirm_current + ): + raise FlowError(E.CONFLICT, "Confirm deleting the current image", 409) + return submit( + request, + "delete", + {"image_id": image_id, "confirm_current": confirm_current}, + device_id, + idempotency_key, + ) + + @app.post( + "/api/v1/devices/{device_id}/restore", + response_model=Accepted, + status_code=202, + tags=["Display"], + dependencies=[Depends(require("display:write"))], + ) + def restore( + device_id: str, + request: Request, + idempotency_key: str | None = Header(default=None), + ): + write_guard(device_id) + return submit(request, "restore", {}, device_id, idempotency_key) + + @app.get( + "/api/v1/operations", + response_model=list[Operation], + tags=["Operations"], + dependencies=[Depends(require("history:read"))], + ) + def operations(limit: int = Query(default=50, ge=1, le=200)): + return [ + service.operation(x["id"]) + for x in db.all( + "SELECT id FROM operations ORDER BY created_at DESC LIMIT ?", (limit,) + ) + ] + + @app.get( + "/api/v1/operations/{operation_id}", + response_model=Operation, + tags=["Operations"], + dependencies=[Depends(require("devices:read"))], + ) + def operation(operation_id: str, request: Request): + op = service.operation(operation_id) + if request.state.client_id not in ("admin", op["client_id"]): + raise FlowError(E.FORBIDDEN, "Operation belongs to another client", 403) + return op + + @app.post( + "/api/v1/operations/{operation_id}/reconcile", + response_model=Reconciliation, + tags=["Operations"], + dependencies=[Depends(require("admin"))], + ) + def reconcile(operation_id: str, payload: ReconcileRequest): + return service.reconcile(operation_id, payload.acknowledge) + + @app.get( + "/api/v1/templates", + response_model=list[TemplateInfo], + tags=["Templates"], + dependencies=[Depends(require("templates:read"))], + ) + def templates(): + return renderer.list() + + @app.get( + "/api/v1/templates/{template_id}", + response_model=TemplateInfo, + tags=["Templates"], + dependencies=[Depends(require("templates:read"))], + ) + def template(template_id: str): + return renderer.info(template_id) + + @app.put( + "/api/v1/templates/{template_id}/config", + response_model=TemplateConfig, + tags=["Templates"], + dependencies=[Depends(require("settings:write"))], + ) + def template_config(template_id: str, payload: TemplateConfig): + renderer.info(template_id) + db.execute( + "INSERT INTO template_config VALUES(?,?) ON CONFLICT(template_id) DO UPDATE SET payload=excluded.payload", + (template_id, encode(settings.sanitize(payload.data))), + ) + return TemplateConfig(data=settings.sanitize(payload.data)) + + @app.get( + "/api/v1/jobs", + response_model=list[Job], + tags=["Automation"], + dependencies=[Depends(require("jobs:read"))], + ) + def jobs(): + return scheduler.jobs() + + @app.post( + "/api/v1/jobs", + response_model=Job, + status_code=201, + tags=["Automation"], + dependencies=[Depends(require("jobs:write"))], + ) + def create_job(payload: JobInput): + return scheduler.save_job(payload) + + @app.put( + "/api/v1/jobs/{job_id}", + response_model=Job, + tags=["Automation"], + dependencies=[Depends(require("jobs:write"))], + ) + def update_job(job_id: str, payload: JobInput): + if not db.one("SELECT id FROM jobs WHERE id=?", (job_id,)): + raise FlowError(E.NOT_FOUND, "Job not found", 404) + return scheduler.save_job(payload, job_id) + + @app.delete( + "/api/v1/jobs/{job_id}", + status_code=204, + tags=["Automation"], + dependencies=[Depends(require("jobs:write"))], + ) + def delete_job(job_id: str): + db.execute("DELETE FROM jobs WHERE id=?", (job_id,)) + return Response(status_code=204) + + @app.get( + "/api/v1/playlists", + response_model=list[Playlist], + tags=["Playlists"], + dependencies=[Depends(require("jobs:read"))], + ) + def playlists(): + return scheduler.playlists() + + @app.post( + "/api/v1/playlists", + response_model=Playlist, + status_code=201, + tags=["Playlists"], + dependencies=[Depends(require("jobs:write"))], + ) + def create_playlist(payload: PlaylistInput): + return scheduler.save_playlist(payload) + + @app.put( + "/api/v1/playlists/{playlist_id}", + response_model=Playlist, + tags=["Playlists"], + dependencies=[Depends(require("jobs:write"))], + ) + def update_playlist(playlist_id: str, payload: PlaylistInput): + if not db.one("SELECT id FROM playlists WHERE id=?", (playlist_id,)): + raise FlowError(E.NOT_FOUND, "Playlist not found", 404) + return scheduler.save_playlist(payload, playlist_id) + + @app.delete( + "/api/v1/playlists/{playlist_id}", + status_code=204, + tags=["Playlists"], + dependencies=[Depends(require("jobs:write"))], + ) + def delete_playlist(playlist_id: str): + db.execute("DELETE FROM playlists WHERE id=?", (playlist_id,)) + return Response(status_code=204) + + @app.get( + "/api/v1/history", + response_model=list[History], + tags=["History"], + dependencies=[Depends(require("history:read"))], + ) + def history(limit: int = Query(default=100, ge=1, le=200)): + return db.all("SELECT * FROM history ORDER BY id DESC LIMIT ?", (limit,)) + + @app.get( + "/api/v1/settings", + response_model=SettingsView, + tags=["Settings"], + dependencies=[Depends(require("admin"))], + ) + def settings_view(): + return SettingsView( + version="0.1.0", + mock=settings.mock, + write_enabled=settings.write_enabled, + api_key_configured=bool(settings.api_key) + or all(registry.keys) + and bool(registry.keys), + api_token_configured=bool(settings.api_token), + timezone=settings.timezone, + device_id=settings.device_id, + weather_provider="mock", + ) + + @app.get("/docs", include_in_schema=False) + def api_docs(): + return get_swagger_ui_html( + openapi_url="/openapi.json", + title="Pigway Frame Flow · API Docs", + swagger_js_url="/ui/swagger-ui-bundle.js", + swagger_css_url="/ui/swagger-ui.css", + swagger_favicon_url='data:image/svg+xml,', + swagger_ui_parameters={ + "persistAuthorization": False, + "displayRequestDuration": True, + }, + ) + + root = Path(__file__).resolve().parent.parent / "web" + + @app.get("/", include_in_schema=False) + def index(): + return FileResponse(root / "index.html", media_type="text/html") + + @app.get("/ui/{filename}", include_in_schema=False) + def static(filename: str): + if filename not in [ + "app.js", + "style.css", + "i18n.js", + "swagger-ui-bundle.js", + "swagger-ui.css", + ]: + raise FlowError(E.NOT_FOUND, "Resource not found", 404) + return FileResponse(root / filename) + + return app diff --git a/pigway_frame_flow/core/__init__.py b/pigway_frame_flow/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pigway_frame_flow/core/auth.py b/pigway_frame_flow/core/auth.py new file mode 100644 index 0000000..6e39ad2 --- /dev/null +++ b/pigway_frame_flow/core/auth.py @@ -0,0 +1,64 @@ +import hashlib +import hmac +import json + +from fastapi import Request, Security +from fastapi.security import HTTPBearer + +from pigway_frame_flow.errors import ErrorCode as E +from pigway_frame_flow.errors import FlowError +from pigway_frame_flow.storage.database import encode, now_iso + +bearer = HTTPBearer(auto_error=False) +ALL_SCOPES = [ + "devices:read", + "images:read", + "images:delete", + "display:write", + "templates:read", + "render:write", + "jobs:read", + "jobs:write", + "history:read", + "settings:write", + "admin", +] + + +def seed_client(db, token): + if token: + digest = hashlib.sha256(token.encode()).hexdigest() + db.execute( + "INSERT INTO clients VALUES(?,?,?,?,?,?,?) ON CONFLICT(client_id) DO UPDATE SET token_hash=excluded.token_hash, enabled=1", + ("admin", digest, "Administrator", encode(ALL_SCOPES), now_iso(), None, 1), + ) + else: + db.execute("UPDATE clients SET enabled=0 WHERE client_id='admin'") + + +def require(scope): + def authorize(request: Request, credentials=Security(bearer)): + db = request.app.state.service.db + if not credentials: + raise FlowError(E.UNAUTHORIZED, "Frame Flow API token required", 401) + digest = hashlib.sha256(credentials.credentials.encode()).hexdigest() + for row in db.all("SELECT * FROM clients WHERE enabled=1"): + if hmac.compare_digest(digest, row["token_hash"]): + permissions = json.loads(row["permissions"]) + if scope not in permissions and "admin" not in permissions: + raise FlowError( + E.FORBIDDEN, "Token scope does not allow this action", 403 + ) + request.state.client_id = row["client_id"] + # Avoid writing last_used on every image or poll. + if not row["last_used_at"] or ( + now_iso()[:16] != row["last_used_at"][:16] + ): + db.execute( + "UPDATE clients SET last_used_at=? WHERE client_id=?", + (now_iso(), row["client_id"]), + ) + return row["client_id"] + raise FlowError(E.UNAUTHORIZED, "Invalid Frame Flow API token", 401) + + return authorize diff --git a/pigway_frame_flow/core/registry.py b/pigway_frame_flow/core/registry.py new file mode 100644 index 0000000..b37e817 --- /dev/null +++ b/pigway_frame_flow/core/registry.py @@ -0,0 +1,56 @@ +import yaml + +from pigway_frame_flow.drivers.base import FrameDeviceDriver +from pigway_frame_flow.drivers.kokonna.client import KokonnaClient +from pigway_frame_flow.drivers.kokonna.driver import KokonnaDriver +from pigway_frame_flow.drivers.kokonna.mock import MockClient +from pigway_frame_flow.errors import ErrorCode as E +from pigway_frame_flow.errors import FlowError + + +class DeviceRegistry: + def __init__(self, settings): + self.settings = settings + config = settings.config_dir / "devices.yml" + devices = ( + yaml.safe_load(config.read_text())["devices"] + if config.exists() + else { + settings.device_id: { + "driver": "kokonna", + "api_key_env": "KOKONNA_API_KEY", + "enabled": True, + } + } + ) + self.drivers: dict[str, FrameDeviceDriver] = {} + self.keys = [] + for id, entry in devices.items(): + if not entry.get("enabled", True): + continue + if entry.get("driver") != "kokonna": + raise ValueError("Unsupported configured device driver") + if not __import__("re").fullmatch(r"[a-z0-9-]{1,60}", id): + raise ValueError("Invalid configured device ID") + client: MockClient | KokonnaClient + if settings.mock: + root = settings.data_dir / "cache" / id + root.mkdir(parents=True, exist_ok=True) + client = MockClient(root) + else: + from pigway_frame_flow.settings import secret + + key = ( + settings.api_key + if entry.get("api_key_env", "KOKONNA_API_KEY") == "KOKONNA_API_KEY" + else secret(str(entry["api_key_env"])) + ) + self.keys.append(key) + settings.additional_secrets.append(key) + client = KokonnaClient(key, settings.write_enabled) + self.drivers[id] = KokonnaDriver(client, id, settings.sanitize) + + def get(self, id: str) -> FrameDeviceDriver: + if id not in self.drivers: + raise FlowError(E.DEVICE_NOT_FOUND, "Device not found", 404) + return self.drivers[id] diff --git a/pigway_frame_flow/display/__init__.py b/pigway_frame_flow/display/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pigway_frame_flow/display/service.py b/pigway_frame_flow/display/service.py new file mode 100644 index 0000000..74567aa --- /dev/null +++ b/pigway_frame_flow/display/service.py @@ -0,0 +1,510 @@ +import hashlib +import json +import threading +import time +from datetime import UTC, datetime, timedelta +from uuid import uuid4 + +from pigway_frame_flow.drivers.base import DriverRegistry +from pigway_frame_flow.errors import ErrorCode as E +from pigway_frame_flow.errors import FlowError +from pigway_frame_flow.models import ( + DisplayRequest, + FutureContent, + ImageContent, + TemplateContent, + URLContent, +) +from pigway_frame_flow.renderers.images import convert +from pigway_frame_flow.storage.database import encode, now_iso + + +class DisplayService: + """One persistent queue; injected driver registry. No vendor imports.""" + + def __init__(self, db, settings, registry: DriverRegistry, assets, renderer): + self.db, self.settings, self.registry, self.assets, self.renderer = ( + db, + settings, + registry, + assets, + renderer, + ) + self.lock = threading.RLock() + self.expiration_checked: dict[str, float] = {} + # A killed write cannot be replayed safely; pre-write work can resume. + db.execute( + "UPDATE operations SET status='UNKNOWN', updated_at=? WHERE write_started=1 AND status IN ('UPLOADING','RENDERING')", + (now_iso(),), + ) + db.execute( + "UPDATE operations SET status='QUEUED' WHERE write_started=0 AND status IN ('RENDERING','UPLOADING')" + ) + + def validate(self, request, writing=True): + self.registry.get(request.device_id) + if isinstance(request.content, FutureContent): + raise FlowError( + E.UNSUPPORTED_CAPABILITY, "Content type is unsupported", 422 + ) + if isinstance(request.content, TemplateContent): + self.renderer.info(request.content.template) + if writing and not self.settings.mock and not self.settings.write_enabled: + raise FlowError(E.WRITE_DISABLED, "Real device writes are disabled", 403) + + def submit(self, action, payload, device_id, client_id, request_id, key=None): + fingerprint = hashlib.sha256( + encode({"action": action, "payload": payload, "device": device_id}).encode() + ).hexdigest() + with self.db.transaction() as tx: + tx.execute("DELETE FROM idempotency WHERE expires_at? ORDER BY priority DESC,created_at DESC LIMIT 1", + (device_id, now_iso()), + ) + + def process_one(self): + with self.lock: + for op in self.db.all( + "SELECT * FROM operations WHERE status='QUEUED' ORDER BY created_at LIMIT 50" + ): + if op["action"] not in ("render", "local-upload") and self.blocked( + op["device_id"] + ): + continue + payload = json.loads(op["payload"]) + if op["action"] == "display": + priority = payload["options"]["priority"] + top = self.top(op["device_id"]) + if top and priority < top["priority"]: + continue + try: + self._execute(op, payload) + except FlowError as exc: + uncertain = exc.code == E.WRITE_RESULT_UNKNOWN + error = { + "code": exc.code, + "message": exc.message, + "request_id": op["request_id"], + "details": {}, + } + self.update( + op["id"], "UNKNOWN" if uncertain else "FAILED", error=error + ) + self.db.history(op, exc.code) + self.db.event( + "operation.failed", {"operation_id": op["id"], "code": exc.code} + ) + except Exception: + wrote = self.db.one( + "SELECT write_started FROM operations WHERE id=?", (op["id"],) + )["write_started"] + self.update( + op["id"], + "UNKNOWN" if wrote else "FAILED", + error={ + "code": E.WRITE_RESULT_UNKNOWN + if wrote + else E.INTERNAL_ERROR, + "message": "Write outcome unknown; reconcile device state" + if wrote + else "Operation failed", + "request_id": op["request_id"], + "details": {}, + }, + ) + self.db.history(op, "UNKNOWN" if wrote else "FAILED") + return True + return False + + def _execute(self, op, payload): + id, device_id, action = op["id"], op["device_id"], op["action"] + driver = self.registry.get(device_id) + if action in ("render", "local-upload"): + self.update(id, "RENDERING") + from pydantic import TypeAdapter + + from pigway_frame_flow.models import Content + + asset = self.content_asset(TypeAdapter(Content).validate_python(payload)) + self.update( + id, "COMPLETED", dict(asset_id=asset["asset_id"], url=asset["url"]) + ) + self.db.history(op, "COMPLETED") + return + device = driver.device(refresh=True) + if device.status == "offline": + raise FlowError(E.DEVICE_OFFLINE, "Target frame is offline", 409) + # B101 layout is deliberately not transposed based on rotation. + if (device.display.width, device.display.height) != (1200, 1600): + raise FlowError( + E.UNSUPPORTED_CAPABILITY, "Device geometry does not match renderer", 409 + ) + previous = device.current_content.id + state = self.state(device_id) + if action == "delete": + image_id = payload["image_id"] + protected = {state["previous_image_id"], state["base_image_id"]} | { + x["image_id"] + for x in self.db.all( + "SELECT image_id FROM overrides WHERE device_id=?", (device_id,) + ) + } + if image_id in protected: + raise FlowError(E.CONFLICT, "Image is needed for restoration", 409) + if image_id == previous and not payload["confirm_current"]: + raise FlowError(E.CONFLICT, "Current image needs confirmation", 409) + self._start_write(id) + driver.delete(image_id) + driver.invalidate() + self.update(id, "COMPLETED", {"image_id": image_id}) + self.db.history(op, "COMPLETED") + return + if action == "restore": + target = ( + state["base_image_id"] + if self.top(device_id) + else state["previous_image_id"] + ) + if target is None: + raise FlowError(E.CONFLICT, "No previous image to restore", 409) + self._start_write(id) + driver.display(target) + self.db.execute("DELETE FROM overrides WHERE device_id=?", (device_id,)) + self.db.execute( + "UPDATE device_state SET base_image_id=NULL,managed_image_id=? WHERE device_id=?", + (target, device_id), + ) + driver.invalidate() + self.update(id, "SYNCING", {"image_id": target}) + self.db.history(op, "ACCEPTED") + return + request = DisplayRequest.model_validate(payload) + content = request.content + self.update(id, "RENDERING") + asset = None + if isinstance(content, ImageContent) and ( + content.image_id or content.image_name + ): + image_id = content.image_id + if content.image_name: + matches = [x for x in driver.images() if x.name == content.image_name] + if len(matches) != 1: + raise FlowError( + E.CONFLICT, "Name must identify exactly one gallery image", 409 + ) + image_id = matches[0].image_id + else: + asset = self.content_asset(content) + image_id = None + self.db.execute( + "UPDATE device_state SET previous_image_id=? WHERE device_id=?", + (previous, device_id), + ) + if request.options.restore_previous and not self.top(device_id): + self.db.execute( + "UPDATE device_state SET base_image_id=? WHERE device_id=?", + (previous, device_id), + ) + if request.options.restore_previous and previous is None: + raise FlowError( + E.CONFLICT, "Temporary override requires an existing image", 409 + ) + self._start_write(id) + if asset: + self.update(id, "UPLOADING") + image_id = driver.upload(self.assets.path(asset["asset_id"]), asset["name"]) + elif isinstance(content, ImageContent) and content.image_name: + driver.display_by_name(content.image_name) + else: + driver.display(image_id) + result = {"image_id": image_id} + if asset: + result["rendered_asset_id"] = asset["asset_id"] + self.update(id, "SYNCING", result) + driver.invalidate() + if request.options.restore_previous: + expires = ( + datetime.now(UTC) + timedelta(seconds=request.options.duration) + ).isoformat() + self.db.execute( + "INSERT INTO overrides VALUES(?,?,?,?,?,?)", + (id, device_id, image_id, request.options.priority, expires, now_iso()), + ) + else: + self.db.execute("DELETE FROM overrides WHERE device_id=?", (device_id,)) + self.db.execute( + "UPDATE device_state SET base_image_id=NULL WHERE device_id=?", + (device_id,), + ) + self.db.execute( + "UPDATE device_state SET managed_image_id=? WHERE device_id=?", + (image_id, device_id), + ) + self.db.history(op, "ACCEPTED") + self.db.event( + "display.changed", + {"device_id": device_id, "image_id": image_id, "operation_id": id}, + ) + + def _start_write(self, id): + self.db.execute( + "UPDATE operations SET write_started=1,status='UPLOADING',updated_at=? WHERE id=?", + (now_iso(), id), + ) + + def sync(self): + for op in self.db.all("SELECT * FROM operations WHERE status='SYNCING'"): + try: + device = self.registry.get(op["device_id"]).device() + target = json.loads(op["result"])["image_id"] + if ( + device.current_content.id == target + and device.current_content.synced + ): + self.update(op["id"], "COMPLETED") + self.db.history(op, "COMPLETED") + self.db.event("operation.completed", {"operation_id": op["id"]}) + elif ( + datetime.now(UTC) - datetime.fromisoformat(op["updated_at"]) + ).total_seconds() > self.settings.sync_timeout: + self.update( + op["id"], + "UNKNOWN", + error={ + "code": E.WRITE_RESULT_UNKNOWN, + "message": "Frame refresh was not confirmed", + "request_id": op["request_id"], + "details": {}, + }, + ) + except FlowError: + pass + + def expire(self): + for device_id in self.registry.drivers: + if self.blocked(device_id): + continue + expired = self.db.all( + "SELECT * FROM overrides WHERE device_id=? AND expires_at<=?", + (device_id, now_iso()), + ) + if not expired: + continue + if not self.settings.mock: + last = self.expiration_checked.get(device_id, 0) + if time.monotonic() - last < 60: + continue + self.expiration_checked[device_id] = time.monotonic() + driver = self.registry.get(device_id) + state = self.state(device_id) + try: + device = driver.device(refresh=True) + if device.status == "offline": + continue + top = self.top(device_id) + target = top["image_id"] if top else state["base_image_id"] + if target is None: + continue + if device.current_content.id != state["managed_image_id"]: + # A user/app changed the artwork; do not overwrite it. + self.db.execute( + "DELETE FROM overrides WHERE device_id=?", (device_id,) + ) + self.db.execute( + "UPDATE device_state SET base_image_id=NULL WHERE device_id=?", + (device_id,), + ) + continue + if target != device.current_content.id: + id = self.submit( + "auto-restore", + {"image_id": target}, + device_id, + "scheduler", + "restore-" + uuid4().hex, + ) + op = self.db.one("SELECT * FROM operations WHERE id=?", (id,)) + self._start_write(id) + try: + driver.display(target) + except FlowError as exc: + self.update( + id, + "UNKNOWN" + if exc.code == E.WRITE_RESULT_UNKNOWN + else "FAILED", + error={ + "code": exc.code, + "message": exc.message, + "request_id": op["request_id"], + "details": {}, + }, + ) + continue + driver.invalidate() + self.update(id, "SYNCING", {"image_id": target}) + self.db.history(op, "ACCEPTED") + self.db.execute( + "DELETE FROM overrides WHERE device_id=? AND expires_at<=?", + (device_id, now_iso()), + ) + self.db.execute( + "UPDATE device_state SET managed_image_id=?,base_image_id=? WHERE device_id=?", + (target, state["base_image_id"] if top else None, device_id), + ) + except FlowError: + continue + + def reconcile(self, id, acknowledge=False): + with self.lock: + op = self.operation(id) + driver = self.registry.get(op["device_id"]) + device = driver.device(refresh=True) + images = driver.images() + expected = (op["result"] or {}).get("image_id") + resolved = op["status"] != "UNKNOWN" + if op["status"] == "UNKNOWN" and ( + acknowledge + or ( + expected + and expected == device.current_content.id + and device.current_content.synced + ) + ): + # Acknowledge releases the block; it never resends a write. + self.update( + id, + "COMPLETED" if expected == device.current_content.id else "FAILED", + {"image_id": device.current_content.id}, + ) + resolved = True + self.db.history( + dict( + id=id, + client_id="admin", + request_id=op["request_id"], + device_id=op["device_id"], + action="reconcile", + ), + "CONFIRMED" + if expected == device.current_content.id + else "ACKNOWLEDGED", + ) + return dict( + operation_id=id, + current_image_id=device.current_content.id, + expected_image_id=expected, + cloud_images=images, + resolved=resolved, + ) diff --git a/pigway_frame_flow/drivers/__init__.py b/pigway_frame_flow/drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pigway_frame_flow/drivers/base.py b/pigway_frame_flow/drivers/base.py new file mode 100644 index 0000000..4c5ccc8 --- /dev/null +++ b/pigway_frame_flow/drivers/base.py @@ -0,0 +1,22 @@ +from typing import Protocol + +from pigway_frame_flow.models import Device, GalleryImage + + +class FrameDeviceDriver(Protocol): + def invalidate(self) -> None: ... + def device(self, refresh: bool = False) -> Device: ... + def get_current_image(self) -> int | None: ... + def health(self) -> bool: ... + def display_by_name(self, name: str): ... + def images(self) -> list[GalleryImage]: ... + def upload(self, path, name: str) -> int: ... + def display(self, image_id: int): ... + def delete(self, image_id: int): ... + def download(self, image_id: int) -> bytes: ... + + +class DriverRegistry(Protocol): + drivers: dict[str, FrameDeviceDriver] + + def get(self, id: str) -> FrameDeviceDriver: ... diff --git a/pigway_frame_flow/drivers/kokonna/__init__.py b/pigway_frame_flow/drivers/kokonna/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pigway_frame_flow/drivers/kokonna/client.py b/pigway_frame_flow/drivers/kokonna/client.py new file mode 100644 index 0000000..88b1345 --- /dev/null +++ b/pigway_frame_flow/drivers/kokonna/client.py @@ -0,0 +1,144 @@ +"""Hardened adapter over the MIT-0 guooscar 1.2.0 device/upload methods. + +No UI, scheduler, renderer, environment lookup, or constructor cloud calls. +Never retry a write; a timeout may have occurred after cloud acceptance. +""" + +import threading +import time +from collections import deque +from urllib.parse import quote + +import requests + +from pigway_frame_flow.errors import ErrorCode as E +from pigway_frame_flow.errors import FlowError +from vendor import guooscar_protocol + +BASE_URL = "https://api.kokonna.art/openapi" + + +class SafeTransport: + def __init__(self, key, session=None): + self.key = key + self.session = session or requests.Session() + self.calls: deque[float] = deque() + self.lock = threading.Lock() + + def request(self, endpoint, payload=None, write=False, binary=False): + if not self.key: + raise FlowError(E.CONFIG_REQUIRED, "KoKonna configuration required", 503) + with self.lock: + current = time.monotonic() + while self.calls and current - self.calls[0] >= 60: + self.calls.popleft() + # Reserve capacity beneath the official 20/minute ceiling. + if len(self.calls) >= 18: + raise FlowError(E.RATE_LIMITED, "Device request budget exhausted", 429) + self.calls.append(current) + url = BASE_URL + endpoint + try: + response = self.session.request( + "GET" if binary else "POST", + url, + headers={} + if binary + else { + "Authorization": "Bearer " + self.key, + "Content-Type": "application/json", + }, + json=None if binary else payload or {}, + timeout=(5, 30), + allow_redirects=False, + ) + except requests.RequestException: + raise FlowError( + E.WRITE_RESULT_UNKNOWN if write else E.CLOUD_UNAVAILABLE, + "Cloud write outcome is unknown" + if write + else "Device cloud is unavailable", + 502, + ) from None + if response.status_code != 200: + code = ( + E.RATE_LIMITED + if response.status_code == 429 + else ( + E.WRITE_RESULT_UNKNOWN + if write and response.status_code >= 500 + else E.CLOUD_UNAVAILABLE + ) + ) + raise FlowError( + code, + "Device cloud request failed", + 429 if code == E.RATE_LIMITED else 502, + ) + if binary: + if len(response.content) > 20 * 1024 * 1024: + raise FlowError(E.INVALID_REQUEST, "Cloud image exceeds size limit") + return response.content + try: + value = response.json() + if not isinstance(value, dict): + raise ValueError() + # Cloud errors may echo credentials: never return raw messages. + if value.get("success") is False or ( + "message" in value and "id" not in value + ): + raise ValueError() + return value + except ValueError: + raise FlowError( + E.WRITE_RESULT_UNKNOWN if write else E.CLOUD_UNAVAILABLE, + "Invalid device cloud response", + 502, + ) from None + + +class KokonnaClient: + def __init__(self, key, write_enabled=False, session=None): + self.transport = SafeTransport(key, session) + self.write_enabled = write_enabled + + def guard(self): + if not self.write_enabled: + raise FlowError( + E.WRITE_DISABLED, + "Real device writes require explicit authorization", + 403, + ) + + def get_device(self): + return guooscar_protocol.device(self.transport) + + def list_images(self): + return self.transport.request("/listImages", {}) + + def upload(self, path, name): + self.guard() + value = guooscar_protocol.upload(self.transport, path.read_bytes(), name) + if not isinstance(value.get("id"), int) or value["id"] <= 0: + raise FlowError(E.WRITE_RESULT_UNKNOWN, "Upload ID not confirmed", 502) + return value["id"] + + def display(self, image_id): + self.guard() + return self.transport.request( + "/displayImageById", {"imageId": image_id}, write=True + ) + + def delete(self, image_id): + self.guard() + return self.transport.request("/deleteImage", {"imageId": image_id}, write=True) + + def download(self, image_id): + return self.transport.request( + f"/image/{quote(self.transport.key, safe='')}/{image_id}", binary=True + ) + + def display_by_name(self, name): + self.guard() + return self.transport.request( + "/displayImageByName", {"imageName": name}, write=True + ) diff --git a/pigway_frame_flow/drivers/kokonna/driver.py b/pigway_frame_flow/drivers/kokonna/driver.py new file mode 100644 index 0000000..1c67482 --- /dev/null +++ b/pigway_frame_flow/drivers/kokonna/driver.py @@ -0,0 +1,104 @@ +from pigway_frame_flow.models import ( + CurrentContent, + Device, + DisplayGeometry, + GalleryImage, + Power, +) + + +class KokonnaDriver: + def __init__(self, client, device_id, sanitize=lambda v: v): + self.client, self.device_id, self.sanitize = client, device_id, sanitize + self.cached: Device | None = None + self.cached_at = 0.0 + + def device(self, refresh=False): + import time + + if not refresh and self.cached and time.monotonic() - self.cached_at < 60: + return self.cached + d = self.sanitize(self.client.get_device()) + result = Device( + id=self.device_id, + name=d.get("nickname", "KoKonna"), + status="online" if d.get("online") else "offline", + model=d.get("model", "13.3-inch"), + model_code=d.get("modelCode", "B101"), + firmware=d.get("firmware"), + display=DisplayGeometry( + **{ + "width": d.get("screenWidth", 1200), + "height": d.get("screenHeight", 1600), + "rotation": d.get("screenRotate", 270), + } + ), + power=Power( + **{ + "battery_percent": d.get("batteryLevel"), + "charging": bool(d.get("isCharging")), + } + ), + current_content=CurrentContent( + **{ + "id": d.get("imageId"), + "cover_id": d.get("coverId"), + "synced": bool(d.get("synced")), + } + ), + slideshow={ + "mode": d.get("switchType"), + "interval_minutes": d.get("switchMinute"), + "members_available": False, + "editable": False, + }, + storage_used_bytes=d.get("sdUsedSize"), + model_verified=bool(d.get("model") and d.get("modelCode")), + last_seen_at=d.get("lastHeartbeat"), + timezone=d.get("timezone", "Asia/Shanghai"), + ) + + self.cached = result + self.cached_at = time.monotonic() + return result + + def get_current_image(self): + return self.device().current_content.id + + def health(self): + return bool(self.device()) + + def invalidate(self): + self.cached_at = 0.0 + + def display_by_name(self, name): + return self.client.display_by_name(name) + + def images(self): + data = self.sanitize(self.client.list_images()) + return [ + GalleryImage( + image_id=d["id"], + width=d.get("width"), + height=d.get("height"), + size_bytes=d.get("size"), + media_type=d.get("type"), + name=d.get("name", "image"), + created_at=d.get("createdAt"), + current=bool(d.get("current")), + url=f"/api/v1/devices/{self.device_id}/images/{d['id']}/file", + ) + for d in data.get("list", []) + ] + + def upload(self, path, name): + return self.client.upload(path, name) + + def display(self, image_id): + return self.client.display(image_id) + + def delete(self, image_id): + return self.client.delete(image_id) + + def download(self, image_id): + return self.client.download(image_id) diff --git a/pigway_frame_flow/drivers/kokonna/mock.py b/pigway_frame_flow/drivers/kokonna/mock.py new file mode 100644 index 0000000..755c140 --- /dev/null +++ b/pigway_frame_flow/drivers/kokonna/mock.py @@ -0,0 +1,108 @@ +"""Persistent mock implements the documented upload-immediately-activates behavior.""" + +import json + +from PIL import Image + +from pigway_frame_flow.errors import ErrorCode as E +from pigway_frame_flow.errors import FlowError +from pigway_frame_flow.storage.database import now_iso + + +class MockClient: + def __init__(self, root): + self.root = root + self.path = root / "mock-device.json" + if self.path.exists(): + self.state = json.loads(self.path.read_text()) + else: + self.state = { + "current": 1, + "counter": 1, + "images": [ + {"id": 1, "name": "Original Art.jpg", "createdAt": now_iso()} + ], + } + img = Image.new("RGB", (1200, 1600), "#efe2ca") + img.save(root / "mock-1.jpg") + self.save() + self.online = True + self.synced = True + self.uploads = 0 + self.displays = [] + + def save(self): + temporary = self.path.with_suffix(".tmp") + temporary.write_text(json.dumps(self.state)) + temporary.replace(self.path) + + def get_device(self): + return { + "nickname": "客厅相框", + "model": "13.3-inch", + "modelCode": "B101", + "firmware": "MOCK-B101", + "online": self.online, + "lastHeartbeat": now_iso(), + "batteryLevel": 40, + "isCharging": False, + "screenWidth": 1200, + "screenHeight": 1600, + "screenRotate": 270, + "timezone": "Asia/Shanghai", + "switchType": "queue", + "switchMinute": 60, + "sdUsedSize": 123456, + "imageId": self.state["current"], + "coverId": None, + "synced": self.synced, + } + + def list_images(self): + return { + "total": len(self.state["images"]), + "list": [ + dict(x, current=x["id"] == self.state["current"]) + for x in reversed(self.state["images"]) + ], + } + + def upload(self, path, name): + self.uploads += 1 + self.state["counter"] += 1 + image_id = self.state["counter"] + (self.root / f"mock-{image_id}.jpg").write_bytes(path.read_bytes()) + self.state["images"].append( + {"id": image_id, "name": name, "createdAt": now_iso()} + ) + self.state["current"] = image_id + self.save() + return image_id + + def display(self, image_id): + if image_id not in [x["id"] for x in self.state["images"]]: + raise FlowError(E.NOT_FOUND, "Image not found", 404) + self.displays.append(image_id) + self.state["current"] = image_id + self.save() + + def delete(self, image_id): + self.state["images"] = [x for x in self.state["images"] if x["id"] != image_id] + if self.state["current"] == image_id: + self.state["current"] = ( + self.state["images"][-1]["id"] if self.state["images"] else None + ) + (self.root / f"mock-{image_id}.jpg").unlink(missing_ok=True) + self.save() + + def download(self, image_id): + path = self.root / f"mock-{image_id}.jpg" + if not path.exists(): + raise FlowError(E.NOT_FOUND, "Image not found", 404) + return path.read_bytes() + + def display_by_name(self, name): + matches = [x for x in self.state["images"] if x["name"] == name] + if not matches: + raise FlowError(E.NOT_FOUND, "Image not found", 404) + return self.display(matches[-1]["id"]) diff --git a/pigway_frame_flow/errors.py b/pigway_frame_flow/errors.py new file mode 100644 index 0000000..c0ad4dc --- /dev/null +++ b/pigway_frame_flow/errors.py @@ -0,0 +1,26 @@ +from enum import StrEnum + + +class ErrorCode(StrEnum): + INVALID_REQUEST = "INVALID_REQUEST" + UNAUTHORIZED = "UNAUTHORIZED" + FORBIDDEN = "FORBIDDEN" + DEVICE_NOT_FOUND = "DEVICE_NOT_FOUND" + DEVICE_OFFLINE = "DEVICE_OFFLINE" + UNSUPPORTED_CAPABILITY = "UNSUPPORTED_CAPABILITY" + RENDER_FAILED = "RENDER_FAILED" + UPLOAD_FAILED = "UPLOAD_FAILED" + WRITE_RESULT_UNKNOWN = "WRITE_RESULT_UNKNOWN" + RATE_LIMITED = "RATE_LIMITED" + INTERNAL_ERROR = "INTERNAL_ERROR" + CLOUD_UNAVAILABLE = "CLOUD_UNAVAILABLE" + NOT_FOUND = "NOT_FOUND" + CONFLICT = "CONFLICT" + CONFIG_REQUIRED = "CONFIG_REQUIRED" + WRITE_DISABLED = "WRITE_DISABLED" + + +class FlowError(Exception): + def __init__(self, code: ErrorCode, message: str, status: int = 400): + self.code, self.message, self.status = code, message, status + super().__init__(message) diff --git a/pigway_frame_flow/main.py b/pigway_frame_flow/main.py new file mode 100644 index 0000000..a90d8fa --- /dev/null +++ b/pigway_frame_flow/main.py @@ -0,0 +1,3 @@ +from pigway_frame_flow.api.app import create_app + +app = create_app() diff --git a/pigway_frame_flow/models.py b/pigway_frame_flow/models.py new file mode 100644 index 0000000..662809b --- /dev/null +++ b/pigway_frame_flow/models.py @@ -0,0 +1,347 @@ +from datetime import datetime +from enum import StrEnum +from typing import Annotated, Any, Literal + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from pigway_frame_flow.errors import ErrorCode + + +class StrictModel(BaseModel): + model_config = ConfigDict(extra="forbid") + + +class Fit(StrEnum): + contain = "contain" + cover = "cover" + smart = "smart" + + +class Profile(StrEnum): + color = "color" + grayscale = "grayscale" + + +class ImageContent(StrictModel): + type: Literal["image"] + image_id: int | None = Field(default=None, gt=0) + image_name: str | None = Field(default=None, min_length=1, max_length=120) + asset_id: str | None = Field(default=None, pattern=r"^[a-f0-9]{32}$") + base64: str | None = Field(default=None, max_length=28_000_000) + name: str = Field(default="image.jpg", min_length=1, max_length=120) + fit: Fit = Fit.contain + profile: Profile = Profile.color + + @model_validator(mode="after") + def one_source(self): + if ( + sum( + x is not None + for x in [self.image_id, self.image_name, self.asset_id, self.base64] + ) + != 1 + ): + raise ValueError("Specify exactly one image_id, asset_id, or base64") + return self + + +class URLContent(StrictModel): + type: Literal["image_url"] + url: str = Field(max_length=2048) + name: str = Field(default="image.jpg", max_length=120) + fit: Fit = Fit.contain + profile: Profile = Profile.color + + +class TemplateContent(StrictModel): + type: Literal["template"] + template: str = Field(pattern=r"^[a-z0-9-]{1,60}$") + data: dict[str, Any] = Field(default_factory=dict) + profile: Profile = Profile.color + + +class FutureContent(StrictModel): + type: Literal["html", "markdown"] + data: str = "" + + +Content = Annotated[ + ImageContent | URLContent | TemplateContent | FutureContent, + Field(discriminator="type"), +] + + +class Options(StrictModel): + duration: int = Field(default=0, ge=0, le=604800) + restore_previous: bool = False + priority: int = Field(default=50, ge=0, le=100) + + @model_validator(mode="after") + def restoration(self): + if self.restore_previous and self.duration == 0: + raise ValueError("restore_previous requires a positive duration") + return self + + +class Source(StrictModel): + client: str | None = Field(default=None, max_length=80) + request_id: str | None = Field(default=None, max_length=120) + + +class DisplayRequest(StrictModel): + device_id: str = "living-room" + content: Content + options: Options = Field(default_factory=Options) + source: Source = Field(default_factory=Source) + + +class Accepted(BaseModel): + operation_id: str + job_id: str + status: Literal["accepted"] = "accepted" + + +class OperationStatus(StrEnum): + QUEUED = "QUEUED" + RENDERING = "RENDERING" + UPLOADING = "UPLOADING" + SYNCING = "SYNCING" + COMPLETED = "COMPLETED" + FAILED = "FAILED" + UNKNOWN = "UNKNOWN" + + +class ErrorBody(BaseModel): + code: ErrorCode + message: str + request_id: str + details: dict[str, Any] = Field(default_factory=dict) + + +class ErrorResponse(BaseModel): + error: ErrorBody + + +class OperationResult(BaseModel): + image_id: int | None = None + asset_id: str | None = None + rendered_asset_id: str | None = None + url: str | None = None + deferred: bool = False + + +class Operation(BaseModel): + operation_id: str + status: OperationStatus + device_id: str + created_at: str + updated_at: str + result: OperationResult | None = None + error: ErrorBody | None = None + request_id: str + client_id: str + action: str + + +class Capabilities(BaseModel): + upload: bool = True + gallery: bool = True + delete: bool = True + switch: bool = True + remote_refresh: bool = True + battery: bool = True + color: bool = True + cloud_upload_without_display: bool = False + + +class DisplayGeometry(BaseModel): + width: int + height: int + rotation: int + color_mode: Profile = Profile.color + + +class Power(BaseModel): + battery_percent: int | None + charging: bool + + +class CurrentContent(BaseModel): + type: Literal["image"] = "image" + id: int | None + cover_id: int | None + synced: bool + + +class Device(BaseModel): + id: str + name: str + driver: Literal["kokonna"] = "kokonna" + status: Literal["online", "offline"] + model: str + model_code: str + firmware: str | None + display: DisplayGeometry + power: Power + current_content: CurrentContent + last_seen_at: str | None + timezone: str + capabilities: Capabilities = Field(default_factory=Capabilities) + slideshow: dict[str, Any] = Field(default_factory=dict) + storage_used_bytes: int | None = None + model_verified: bool = False + + +class GalleryImage(BaseModel): + width: int | None = None + height: int | None = None + size_bytes: int | None = None + media_type: str | None = None + image_id: int + name: str + created_at: str | None + current: bool + url: str + + +class Schedule(StrictModel): + type: Literal["cron", "daily", "weekdays", "weekends", "date", "temporary"] + cron: str | None = None + time: str | None = Field(default=None, pattern=r"^([01]\d|2[0-3]):[0-5]\d$") + at: datetime | None = None + + +class RestoreContent(StrictModel): + type: Literal["restore"] + + +class JobInput(StrictModel): + enabled: bool = True + device_id: str = "living-room" + name: str = Field(min_length=1, max_length=120) + schedule: Schedule + source: Content | RestoreContent + display_duration: int = Field(default=0, ge=0, le=604800) + restore_previous: bool = False + priority: int = Field(default=50, ge=0, le=100) + + @model_validator(mode="after") + def validate_options(self): + Options( + duration=self.display_duration, + restore_previous=self.restore_previous, + priority=self.priority, + ) + return self + + +class Job(JobInput): + id: str + created_at: str + updated_at: str + last_run: str | None = None + last_result: str | None = None + next_run: str | None = None + + +class TemplateInfo(BaseModel): + id: str + name: str + width: int = 1200 + height: int = 1600 + fields: list[str] + defaults: dict[str, Any] + + +class TemplateConfig(StrictModel): + data: dict[str, Any] + + +class History(BaseModel): + id: int + client_id: str + request_id: str + operation_id: str | None + device_id: str + action: str + result: str + timestamp: str + + +class SettingsView(BaseModel): + version: str + mock: bool + write_enabled: bool + api_key_configured: bool + api_token_configured: bool + timezone: str + device_id: str + weather_provider: str + + +class Health(BaseModel): + status: str + version: str = "0.1.0" + + +class Readiness(BaseModel): + status: str + checks: dict[str, bool] + + +class Asset(BaseModel): + asset_id: str + name: str + url: str + created_at: str + + +class DeleteRequest(StrictModel): + confirm_current: bool = False + + +class RestoreRequest(StrictModel): + device_id: str = "living-room" + + +class Version(BaseModel): + product: str = "Pigway Frame Flow" + version: str = "0.1.0" + api_version: str = "v1" + commit: str = "development" + build_time: str = "unknown" + + +class PlaylistInput(StrictModel): + device_id: str = "living-room" + name: str = Field(min_length=1, max_length=120) + image_ids: list[int] = Field(min_length=1, max_length=100) + interval_minutes: int = Field(default=60, ge=5, le=10080) + mode: Literal["queue", "random"] = "queue" + enabled: bool = False + + @model_validator(mode="after") + def ids(self): + if any(x <= 0 for x in self.image_ids) or len(set(self.image_ids)) != len( + self.image_ids + ): + raise ValueError("Image IDs must be positive and unique") + return self + + +class Playlist(PlaylistInput): + id: str + position: int + next_run: str | None + + +class ReconcileRequest(StrictModel): + acknowledge: bool = False + + +class Reconciliation(BaseModel): + operation_id: str + current_image_id: int | None + expected_image_id: int | None + cloud_images: list[GalleryImage] + resolved: bool diff --git a/pigway_frame_flow/providers/__init__.py b/pigway_frame_flow/providers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pigway_frame_flow/providers/weather.py b/pigway_frame_flow/providers/weather.py new file mode 100644 index 0000000..93a9083 --- /dev/null +++ b/pigway_frame_flow/providers/weather.py @@ -0,0 +1,19 @@ +from typing import Protocol + + +class WeatherProvider(Protocol): + def get_current(self) -> dict: ... + def get_forecast(self) -> list[dict]: ... + + +class MockWeatherProvider: + def get_current(self): + return { + "location": "上海", + "temperature": "--", + "summary": "天气演示数据 · 尚未连接实时天气源", + "mock": True, + } + + def get_forecast(self): + return [] diff --git a/pigway_frame_flow/renderers/__init__.py b/pigway_frame_flow/renderers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pigway_frame_flow/renderers/images.py b/pigway_frame_flow/renderers/images.py new file mode 100644 index 0000000..7c66eb8 --- /dev/null +++ b/pigway_frame_flow/renderers/images.py @@ -0,0 +1,162 @@ +import base64 +import ipaddress +import socket +from io import BytesIO +from urllib.parse import urlsplit +from uuid import uuid4 + +from PIL import Image, ImageFilter, ImageOps, ImageStat + +from pigway_frame_flow.errors import ErrorCode as E +from pigway_frame_flow.errors import FlowError +from pigway_frame_flow.storage.database import now_iso + +Image.MAX_IMAGE_PIXELS = 25_000_000 + + +def convert(data, fit="contain", profile="color"): + try: + with Image.open(BytesIO(data)) as source: + if source.format not in {"JPEG", "PNG", "WEBP"}: + raise ValueError() + if source.width * source.height > 25_000_000: + raise ValueError() + img = ImageOps.exif_transpose(source).convert("RGBA") + canvas = Image.new("RGBA", img.size, "white") + canvas.alpha_composite(img) + img = canvas.convert("RGB") + if fit == "contain": + result = ImageOps.pad( + img, (1200, 1600), method=Image.Resampling.LANCZOS, color="white" + ) + else: + center = (0.5, 0.5) + if fit == "smart": + # Lightweight edge saliency: slide an aspect-correct window + # over a thumbnail; retain the region with most detail. + thumb = img.copy() + thumb.thumbnail((240, 240)) + edges = thumb.convert("L").filter(ImageFilter.FIND_EDGES) + w, h = edges.size + cw, ch = (int(h * 0.75), h) if w / h > 0.75 else (w, int(w / 0.75)) + choices = [] + for step in range(11): + x, y = int((w - cw) * step / 10), int((h - ch) * step / 10) + score = ImageStat.Stat(edges.crop((x, y, x + cw, y + ch))).sum[ + 0 + ] + # Favor centered crops in ties, suppress border artifacts. + choices.append((score - abs(step - 5) * 0.01, x, y)) + _, x, y = max(choices) + center = ( + (x / (w - cw)) if w != cw else 0.5, + (y / (h - ch)) if h != ch else 0.5, + ) + result = ImageOps.fit( + img, (1200, 1600), method=Image.Resampling.LANCZOS, centering=center + ) + if profile == "grayscale": + result = ImageOps.grayscale(result).convert("RGB") + out = BytesIO() + result.save(out, "JPEG", quality=93, optimize=True) + return out.getvalue() + except (OSError, ValueError, Image.DecompressionBombError): + raise FlowError(E.INVALID_REQUEST, "Unsupported or invalid image") from None + + +class Assets: + def __init__(self, db, settings): + self.db, self.settings = db, settings + + def put(self, data, name="image.jpg", kind="images"): + from pathlib import Path + + name = Path(name).stem[:110] + ".jpg" + asset_id = uuid4().hex + (self.settings.data_dir / kind / (asset_id + ".jpg")).write_bytes(data) + self.db.execute( + "INSERT INTO assets VALUES(?,?,?,?)", (asset_id, name, kind, now_iso()) + ) + return self.info(asset_id) + + def info(self, asset_id): + row = self.db.one("SELECT * FROM assets WHERE id=?", (asset_id,)) + if not row: + raise FlowError(E.NOT_FOUND, "Asset not found", 404) + return { + "asset_id": row["id"], + "name": row["name"], + "url": f"/api/v1/assets/{row['id']}", + "created_at": row["created_at"], + } + + def path(self, asset_id): + row = self.db.one("SELECT * FROM assets WHERE id=?", (asset_id,)) + if not row: + raise FlowError(E.NOT_FOUND, "Asset not found", 404) + return self.settings.data_dir / row["kind"] / (row["id"] + ".jpg") + + def decode(self, value): + try: + data = base64.b64decode(value, validate=True) + except ValueError: + raise FlowError(E.INVALID_REQUEST, "Invalid base64 image") from None + if len(data) > self.settings.max_bytes: + raise FlowError(E.INVALID_REQUEST, "Image exceeds 20 MiB") + return data + + def fetch_url(self, url): + parsed = urlsplit(url) + if ( + parsed.scheme != "https" + or not parsed.hostname + or parsed.username + or parsed.password + or parsed.port not in (None, 443) + ): + raise FlowError(E.INVALID_REQUEST, "Image URL requires HTTPS on port 443") + if parsed.hostname.lower() not in self.settings.image_url_hosts: + raise FlowError(E.FORBIDDEN, "Image host is not allowed", 403) + try: + addresses = socket.getaddrinfo( + parsed.hostname, 443, type=socket.SOCK_STREAM + ) + if not addresses or any( + not ipaddress.ip_address(x[4][0]).is_global for x in addresses + ): + raise FlowError( + E.FORBIDDEN, "Private image addresses are forbidden", 403 + ) + # Pin the validated resolution for this request, including TLS SNI, + # instead of performing a second DNS lookup (rebinding). + import http.client + import ssl + + ip = str(addresses[0][4][0]) + + class PinnedHTTPS(http.client.HTTPSConnection): + def connect(self): + sock = socket.create_connection((ip, 443), timeout=10) + self.sock = ssl.create_default_context().wrap_socket( + sock, server_hostname=parsed.hostname + ) + + connection = PinnedHTTPS(parsed.hostname, timeout=10) + try: + connection.request( + "GET", + (parsed.path or "/") + ("?" + parsed.query if parsed.query else ""), + ) + response = connection.getresponse() + if response.status != 200: + raise FlowError( + E.INVALID_REQUEST, "Image URL returned an invalid response" + ) + data = response.read(self.settings.max_bytes + 1) + if len(data) > self.settings.max_bytes: + raise FlowError(E.INVALID_REQUEST, "Image exceeds 20 MiB") + return data + finally: + connection.close() + except (OSError, ValueError): + raise FlowError(E.INVALID_REQUEST, "Unable to fetch image URL") from None diff --git a/pigway_frame_flow/renderers/templates.py b/pigway_frame_flow/renderers/templates.py new file mode 100644 index 0000000..db7115e --- /dev/null +++ b/pigway_frame_flow/renderers/templates.py @@ -0,0 +1,132 @@ +import json +from datetime import datetime +from io import BytesIO +from pathlib import Path +from zoneinfo import ZoneInfo + +from PIL import Image, ImageDraw, ImageFont, ImageOps + +from pigway_frame_flow.errors import ErrorCode as E +from pigway_frame_flow.errors import FlowError + + +class TemplateRenderer: + def __init__(self, db, settings, weather, assets): + self.db, self.settings, self.weather, self.assets = ( + db, + settings, + weather, + assets, + ) + self.root = Path(__file__).resolve().parent.parent / "templates" + + def info(self, template_id): + path = self.root / (template_id + ".json") + if not path.exists() or path.parent != self.root: + raise FlowError(E.NOT_FOUND, "Template not found", 404) + spec = json.loads(path.read_text()) + config = self.db.one( + "SELECT payload FROM template_config WHERE template_id=?", (template_id,) + ) + if config: + spec["defaults"].update(json.loads(config["payload"])) + return spec + + def list(self): + return [self.info(p.stem) for p in sorted(self.root.glob("*.json"))] + + def font(self, size): + for path in [ + self.settings.font_path, + "/System/Library/Fonts/PingFang.ttc", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + ]: + if Path(path).is_file(): + return ImageFont.truetype(path, size) + return ImageFont.load_default(size=size) + + def render(self, template_id, data, profile="color"): + spec = self.info(template_id) + conf = self.db.one( + "SELECT payload FROM template_config WHERE template_id=?", (template_id,) + ) + values = dict(spec["defaults"]) + if conf: + values.update(json.loads(conf["payload"])) + values.update(data) + current = datetime.now(ZoneInfo(self.settings.timezone)) + values.setdefault("date", current.strftime("%Y-%m-%d")) + values.setdefault( + "weekday", + [ + "星期一 / Monday", + "星期二 / Tuesday", + "星期三 / Wednesday", + "星期四 / Thursday", + "星期五 / Friday", + "星期六 / Saturday", + "星期日 / Sunday", + ][current.weekday()], + ) + values.setdefault("weather", self.weather.get_current()) + values["updated"] = current.strftime("%Y-%m-%d %H:%M") + img = Image.new("RGB", (1200, 1600), "white") + draw = ImageDraw.Draw(img) + accent = "#184d42" if profile == "color" else "#000000" + + def text(value, x, y, width, height, size): + font = self.font(size) + lines, line = [], "" + for char in str(value)[:3000]: + if char == "\n" or draw.textlength(line + char, font=font) > width: + lines.append(line) + line = "" if char == "\n" else char + else: + line += char + lines.append(line) + maxlines = max(1, height // (size + 14)) + for n, line in enumerate(lines[:maxlines]): + if n == maxlines - 1 and len(lines) > maxlines: + line = line[:-2] + "…" + draw.text( + (x, y + n * (size + 14)), + line, + font=font, + fill=accent if size >= 60 else "black", + ) + + for block in spec["blocks"]: + x, y, w, h = block["box"] + if block["kind"] == "rule": + draw.rectangle((x, y, x + w, y + h), fill=accent) + elif block["kind"] == "photo": + asset_id = values.get("photo_id") + if asset_id: + with Image.open(self.assets.path(asset_id)) as photo: + img.paste(ImageOps.fit(photo.convert("RGB"), (w, h)), (x, y)) + else: + draw.rectangle((x, y, x + w, y + h), outline="black", width=5) + text( + "家庭照片 / Family photo", + x + 35, + y + h // 2 - 30, + w - 70, + 100, + 36, + ) + else: + value = values.get(block.get("field"), block.get("text", "")) + if isinstance(value, list): + value = "\n".join( + "• " + + (str(v.get("title", "")) if isinstance(v, dict) else str(v)) + for v in value[:10] + ) + if isinstance(value, dict): + value = " · ".join(str(v) for k, v in value.items() if k != "mock") + text(value, x, y, w, h, block.get("size", 40)) + if profile == "grayscale": + img = ImageOps.grayscale(img).convert("RGB") + out = BytesIO() + img.save(out, "JPEG", quality=93) + return out.getvalue() diff --git a/pigway_frame_flow/scheduler/__init__.py b/pigway_frame_flow/scheduler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pigway_frame_flow/scheduler/engine.py b/pigway_frame_flow/scheduler/engine.py new file mode 100644 index 0000000..d008126 --- /dev/null +++ b/pigway_frame_flow/scheduler/engine.py @@ -0,0 +1,273 @@ +import json +import logging +import random +import threading +from datetime import UTC, datetime, timedelta +from uuid import uuid4 +from zoneinfo import ZoneInfo + +from croniter import croniter + +from pigway_frame_flow.errors import ErrorCode as E +from pigway_frame_flow.errors import FlowError +from pigway_frame_flow.models import ( + DisplayRequest, + ImageContent, + JobInput, + Options, + PlaylistInput, + RestoreContent, +) +from pigway_frame_flow.storage.database import encode, now_iso + + +def next_run(schedule, after=None): + after = after or datetime.now(UTC) + if schedule.type in ("date", "temporary"): + if not schedule.at or schedule.at.tzinfo is None: + raise FlowError(E.INVALID_REQUEST, "One-time date must include timezone") + return schedule.at.astimezone(UTC).isoformat() if schedule.at > after else None + if schedule.type == "cron": + expression = schedule.cron or "" + if len(expression.split()) != 5 or not croniter.is_valid(expression): + raise FlowError(E.INVALID_REQUEST, "Cron requires five valid fields") + else: + if not schedule.time: + raise FlowError(E.INVALID_REQUEST, "Daily schedule requires HH:MM") + hour, minute = schedule.time.split(":") + day = ( + "1-5" + if schedule.type == "weekdays" + else ("6,0" if schedule.type == "weekends" else "*") + ) + expression = f"{minute} {hour} * * {day}" + return ( + croniter(expression, after.astimezone(ZoneInfo("Asia/Shanghai"))) + .get_next(datetime) + .astimezone(UTC) + .isoformat() + ) + + +class Scheduler: + def __init__(self, service): + self.service, self.db = service, service.db + self.stop_event = threading.Event() + self.thread = None + self.tick_lock = threading.Lock() + + def jobs(self): + rows = [ + dict( + json.loads(x["payload"]), + id=x["id"], + created_at=x["created_at"], + updated_at=x["updated_at"], + last_run=x["last_run"], + last_result=x["last_result"], + next_run=x["next_run"], + ) + for x in self.db.all("SELECT * FROM jobs ORDER BY created_at DESC") + ] + + for row in rows: + if row["last_result"]: + operation = self.db.one( + "SELECT status FROM operations WHERE id=?", (row["last_result"],) + ) + if operation: + row["last_result"] = operation["status"] + return rows + + def save_job(self, job, id=None): + self.service.registry.get(job.device_id) + if job.source.type != "restore": + self.service.validate( + DisplayRequest(device_id=job.device_id, content=job.source), + writing=False, + ) + upcoming = next_run(job.schedule) + if job.enabled and upcoming is None: + raise FlowError(E.INVALID_REQUEST, "One-time task must be in the future") + id = id or uuid4().hex + now = now_iso() + self.db.execute( + "INSERT INTO jobs(id,payload,enabled,next_run,created_at,updated_at) VALUES(?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET payload=excluded.payload,enabled=excluded.enabled,next_run=excluded.next_run,updated_at=excluded.updated_at", + ( + id, + encode(self.service.settings.sanitize(job.model_dump(mode="json"))), + int(job.enabled), + upcoming, + now, + now, + ), + ) + return next(x for x in self.jobs() if x["id"] == id) + + def playlists(self): + return [ + dict( + json.loads(x["payload"]), + id=x["id"], + position=x["position"], + next_run=x["next_run"], + ) + for x in self.db.all("SELECT * FROM playlists") + ] + + def save_playlist(self, playlist, id=None): + self.service.registry.get(playlist.device_id) + for existing in self.playlists(): + if ( + existing["enabled"] + and playlist.enabled + and existing["device_id"] == playlist.device_id + and existing["id"] != id + ): + raise FlowError( + E.CONFLICT, "Only one Frame Flow playlist may run per device", 409 + ) + id = id or uuid4().hex + self.db.execute( + "INSERT INTO playlists VALUES(?,?,?,?) ON CONFLICT(id) DO UPDATE SET payload=excluded.payload,next_run=excluded.next_run", + ( + id, + encode(playlist.model_dump()), + -1, + now_iso() if playlist.enabled else None, + ), + ) + return next(x for x in self.playlists() if x["id"] == id) + + def tick(self, current=None): + current = current or datetime.now(UTC) + with self.tick_lock: + for row in self.db.all( + "SELECT * FROM jobs WHERE enabled=1 AND next_run<=?", + (current.isoformat(),), + ): + job = JobInput.model_validate_json(row["payload"]) + slot = row["next_run"] + if self.db.one( + "SELECT * FROM job_runs WHERE job_id=? AND scheduled_at=?", + (row["id"], slot), + ): + continue + # Skip overdue tasks (>5 minutes): avoid an unexpected artwork + # overwrite after downtime. One-time tasks are retired too. + late = (current - datetime.fromisoformat(slot)).total_seconds() > 300 + operation_id = None + if not late: + action = "restore" if job.source.type == "restore" else "display" + payload = ( + {} + if isinstance(job.source, RestoreContent) + else DisplayRequest( + device_id=job.device_id, + content=job.source, + options=Options( + duration=job.display_duration, + restore_previous=job.restore_previous, + priority=job.priority, + ), + ).model_dump(mode="json") + ) + operation_id = self.service.submit( + action, + payload, + job.device_id, + "scheduler", + "schedule-" + uuid4().hex, + "job-" + row["id"] + "-" + slot, + ) + following = next_run(job.schedule, current) + enabled = bool(following) + updated = job.model_copy(update={"enabled": enabled}).model_dump( + mode="json" + ) + with self.db.transaction() as tx: + tx.execute( + "INSERT OR IGNORE INTO job_runs VALUES(?,?,?)", + (row["id"], slot, operation_id), + ) + tx.execute( + "UPDATE jobs SET payload=?,enabled=?,next_run=?,last_run=?,last_result=? WHERE id=?", + ( + encode(updated), + int(enabled), + following, + now_iso(), + "SKIPPED_OVERDUE" if late else operation_id, + row["id"], + ), + ) + for row in self.db.all( + "SELECT * FROM playlists WHERE next_run<=?", (current.isoformat(),) + ): + p = PlaylistInput.model_validate_json(row["payload"]) + if ( + not p.enabled + or self.service.top(p.device_id) + or self.service.blocked(p.device_id) + ): + continue + if ( + not self.service.settings.mock + and not self.service.settings.write_enabled + ): + continue + pos = ( + random.randrange(len(p.image_ids)) + if p.mode == "random" + else (row["position"] + 1) % len(p.image_ids) + ) + request = DisplayRequest( + device_id=p.device_id, + content=ImageContent(type="image", image_id=p.image_ids[pos]), + options=Options(priority=0), + ) + self.service.submit( + "display", + request.model_dump(mode="json"), + p.device_id, + "scheduler", + "playlist-" + uuid4().hex, + "playlist-" + row["id"] + "-" + row["next_run"], + ) + following = ( + current + timedelta(minutes=p.interval_minutes) + ).isoformat() + self.db.execute( + "UPDATE playlists SET position=?,next_run=? WHERE id=?", + (pos, following, row["id"]), + ) + + def start(self): + self.thread = threading.Thread( + target=self.run, name="frame-flow-worker", daemon=True + ) + self.thread.start() + + def run(self): + while not self.stop_event.is_set(): + try: + self.tick() + self.service.expire() + processed = self.service.process_one() + self.service.sync() + except Exception: + logging.getLogger("frame-flow").error( + "Background cycle failed; details suppressed" + ) + processed = False + self.stop_event.wait(1 if processed else 5) + + def stop(self): + self.stop_event.set() + if self.thread: + self.thread.join(timeout=45) + + def healthy(self): + return bool( + self.thread and self.thread.is_alive() and not self.stop_event.is_set() + ) diff --git a/pigway_frame_flow/settings.py b/pigway_frame_flow/settings.py new file mode 100644 index 0000000..adf8b7f --- /dev/null +++ b/pigway_frame_flow/settings.py @@ -0,0 +1,62 @@ +import os +from dataclasses import dataclass, field +from pathlib import Path + + +def secret(name: str) -> str: + path = os.getenv(name + "_FILE") + return Path(path).read_text().strip() if path else os.getenv(name, "") + + +@dataclass +class Settings: + data_dir: Path = field(default_factory=lambda: Path(os.getenv("DATA_DIR", "/data"))) + config_dir: Path = field( + default_factory=lambda: Path(os.getenv("CONFIG_DIR", "/config")) + ) + mock: bool = field( + default_factory=lambda: os.getenv("KOKONNA_MOCK", "true").lower() == "true" + ) + write_enabled: bool = field( + default_factory=lambda: ( + os.getenv("KOKONNA_WRITE_ENABLED", "false").lower() == "true" + ) + ) + api_key: str = field(default_factory=lambda: secret("KOKONNA_API_KEY"), repr=False) + api_token: str = field( + default_factory=lambda: secret("FRAME_FLOW_TOKEN"), repr=False + ) + additional_secrets: list[str] = field(default_factory=list, repr=False) + device_id: str = "living-room" + timezone: str = "Asia/Shanghai" + cors_origins: list[str] = field( + default_factory=lambda: [ + x.strip() + for x in os.getenv("CORS_ALLOWED_ORIGINS", "").split(",") + if x.strip() + ] + ) + image_url_hosts: list[str] = field( + default_factory=lambda: [ + x.strip().lower() + for x in os.getenv("IMAGE_URL_ALLOWED_HOSTS", "").split(",") + if x.strip() + ] + ) + font_path: str = field( + default_factory=lambda: os.getenv( + "FONT_PATH", "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc" + ) + ) + max_bytes: int = 20 * 1024 * 1024 + idempotency_hours: int = 168 + sync_timeout: int = 600 + + def sanitize(self, value): + import json + + encoded = json.dumps(value, ensure_ascii=False) + for key in (self.api_key, self.api_token, *self.additional_secrets): + if key: + encoded = encoded.replace(key, "[REDACTED]") + return json.loads(encoded) diff --git a/pigway_frame_flow/storage/__init__.py b/pigway_frame_flow/storage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pigway_frame_flow/storage/database.py b/pigway_frame_flow/storage/database.py new file mode 100644 index 0000000..5c623b3 --- /dev/null +++ b/pigway_frame_flow/storage/database.py @@ -0,0 +1,103 @@ +import json +import sqlite3 +import threading +from contextlib import contextmanager +from datetime import UTC, datetime +from uuid import uuid4 + + +def now_iso(): + return datetime.now(UTC).isoformat() + + +def encode(value): + return json.dumps(value, ensure_ascii=False, separators=(",", ":")) + + +class Database: + def __init__(self, path): + self.lock = threading.RLock() + self.conn = sqlite3.connect(path, check_same_thread=False) + self.conn.row_factory = sqlite3.Row + self.conn.executescript(""" + PRAGMA journal_mode=WAL; + PRAGMA busy_timeout=5000; + PRAGMA synchronous=FULL; + CREATE TABLE IF NOT EXISTS playlists (id TEXT PRIMARY KEY, payload TEXT, position INTEGER, next_run TEXT); + CREATE TABLE IF NOT EXISTS operations ( + id TEXT PRIMARY KEY, status TEXT NOT NULL, action TEXT NOT NULL, + payload TEXT NOT NULL, device_id TEXT NOT NULL, client_id TEXT NOT NULL, + request_id TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, + result TEXT, error TEXT, write_started INTEGER DEFAULT 0); + CREATE TABLE IF NOT EXISTS idempotency ( + client_id TEXT, key TEXT, fingerprint TEXT NOT NULL, operation_id TEXT NOT NULL, + expires_at TEXT NOT NULL, PRIMARY KEY(client_id,key)); + CREATE TABLE IF NOT EXISTS jobs (id TEXT PRIMARY KEY, payload TEXT NOT NULL, + enabled INTEGER NOT NULL, next_run TEXT, created_at TEXT, updated_at TEXT, + last_run TEXT, last_result TEXT); + CREATE TABLE IF NOT EXISTS job_runs (job_id TEXT, scheduled_at TEXT, + operation_id TEXT, PRIMARY KEY(job_id,scheduled_at)); + CREATE TABLE IF NOT EXISTS history (id INTEGER PRIMARY KEY, client_id TEXT, + request_id TEXT, operation_id TEXT, device_id TEXT, action TEXT, + result TEXT, timestamp TEXT); + CREATE TABLE IF NOT EXISTS device_state (device_id TEXT PRIMARY KEY, + previous_image_id INTEGER, base_image_id INTEGER, managed_image_id INTEGER); + CREATE TABLE IF NOT EXISTS overrides (id TEXT PRIMARY KEY, device_id TEXT, + image_id INTEGER, priority INTEGER, expires_at TEXT, created_at TEXT); + CREATE TABLE IF NOT EXISTS template_config (template_id TEXT PRIMARY KEY, payload TEXT); + CREATE TABLE IF NOT EXISTS assets (id TEXT PRIMARY KEY, name TEXT, kind TEXT, created_at TEXT); + CREATE TABLE IF NOT EXISTS clients (client_id TEXT PRIMARY KEY, token_hash TEXT, + name TEXT, permissions TEXT, created_at TEXT, last_used_at TEXT, enabled INTEGER); + CREATE TABLE IF NOT EXISTS events (id TEXT PRIMARY KEY, event_type TEXT, + payload TEXT, created_at TEXT); + CREATE TABLE IF NOT EXISTS webhook_subscriptions (id TEXT PRIMARY KEY, + client_id TEXT, callback_url TEXT, event_types TEXT, enabled INTEGER, + created_at TEXT); + """) + + @contextmanager + def transaction(self): + with self.lock: + self.conn.execute("BEGIN IMMEDIATE") + try: + yield self.conn + self.conn.commit() + except Exception: + self.conn.rollback() + raise + + def execute(self, query, args=()): + with self.lock, self.conn: + return self.conn.execute(query, args) + + def one(self, query, args=()): + with self.lock: + row = self.conn.execute(query, args).fetchone() + return dict(row) if row else None + + def all(self, query, args=()): + with self.lock: + return [dict(r) for r in self.conn.execute(query, args).fetchall()] + + def history(self, op, result): + self.execute( + "INSERT INTO history(client_id,request_id,operation_id,device_id,action,result,timestamp) VALUES(?,?,?,?,?,?,?)", + ( + op["client_id"], + op["request_id"], + op.get("id"), + op["device_id"], + op["action"], + result, + now_iso(), + ), + ) + + def event(self, event_type, payload): + self.execute( + "INSERT INTO events VALUES(?,?,?,?)", + (uuid4().hex, event_type, encode(payload), now_iso()), + ) + + def close(self): + self.conn.close() diff --git a/pigway_frame_flow/templates/calendar.json b/pigway_frame_flow/templates/calendar.json new file mode 100644 index 0000000..e4536d9 --- /dev/null +++ b/pigway_frame_flow/templates/calendar.json @@ -0,0 +1,88 @@ +{ + "id": "calendar", + "name": "Calendar / 日历", + "width": 1200, + "height": 1600, + "fields": [ + "date", + "events", + "photo_id", + "title", + "weather", + "weekday" + ], + "defaults": { + "title": "日历 / Calendar", + "events": [ + "暂无日程 / No events" + ] + }, + "blocks": [ + { + "field": "title", + "box": [ + 72, + 60, + 1056, + 150 + ], + "size": 72, + "kind": "text" + }, + { + "field": "", + "box": [ + 72, + 235, + 1056, + 7 + ], + "size": 42, + "kind": "rule" + }, + { + "field": "updated", + "box": [ + 72, + 1500, + 1056, + 70 + ], + "size": 32, + "kind": "text" + }, + { + "field": "date", + "box": [ + 72, + 300, + 1056, + 100 + ], + "size": 66, + "kind": "text" + }, + { + "field": "weekday", + "box": [ + 72, + 430, + 1056, + 100 + ], + "size": 42, + "kind": "text" + }, + { + "field": "events", + "box": [ + 72, + 600, + 1056, + 800 + ], + "size": 54, + "kind": "text" + } + ] +} diff --git a/pigway_frame_flow/templates/daily-dashboard.json b/pigway_frame_flow/templates/daily-dashboard.json new file mode 100644 index 0000000..bb13d34 --- /dev/null +++ b/pigway_frame_flow/templates/daily-dashboard.json @@ -0,0 +1,124 @@ +{ + "id": "daily-dashboard", + "name": "Daily Dashboard / 家庭早报", + "width": 1200, + "height": 1600, + "fields": [ + "date", + "events", + "photo_id", + "quote", + "title", + "weather", + "weekday" + ], + "defaults": { + "title": "Good Morning / 早上好", + "events": [ + "一起吃早餐 / Breakfast together", + "给植物浇水 / Water the plants" + ], + "quote": "把平凡的日子过成喜欢的样子。" + }, + "blocks": [ + { + "field": "title", + "box": [ + 72, + 60, + 1056, + 150 + ], + "size": 72, + "kind": "text" + }, + { + "field": "", + "box": [ + 72, + 235, + 1056, + 7 + ], + "size": 42, + "kind": "rule" + }, + { + "field": "updated", + "box": [ + 72, + 1500, + 1056, + 70 + ], + "size": 32, + "kind": "text" + }, + { + "field": "date", + "box": [ + 72, + 275, + 1056, + 80 + ], + "size": 56, + "kind": "text" + }, + { + "field": "weekday", + "box": [ + 72, + 365, + 1056, + 65 + ], + "size": 42, + "kind": "text" + }, + { + "field": "weather", + "box": [ + 72, + 465, + 1056, + 180 + ], + "size": 42, + "kind": "text" + }, + { + "field": "events", + "box": [ + 72, + 690, + 1056, + 260 + ], + "size": 46, + "kind": "text" + }, + { + "field": "quote", + "box": [ + 72, + 1020, + 1056, + 150 + ], + "size": 46, + "kind": "text" + }, + { + "field": "photo_id", + "box": [ + 72, + 1210, + 1056, + 235 + ], + "size": 42, + "kind": "photo" + } + ] +} diff --git a/pigway_frame_flow/templates/photo-text.json b/pigway_frame_flow/templates/photo-text.json new file mode 100644 index 0000000..bdcaded --- /dev/null +++ b/pigway_frame_flow/templates/photo-text.json @@ -0,0 +1,76 @@ +{ + "id": "photo-text", + "name": "Photo + Text / 照片与文字", + "width": 1200, + "height": 1600, + "fields": [ + "date", + "events", + "message", + "photo_id", + "title", + "weather", + "weekday" + ], + "defaults": { + "title": "我们的时光 / Our moments", + "message": "记录值得记住的日常。" + }, + "blocks": [ + { + "field": "title", + "box": [ + 72, + 60, + 1056, + 150 + ], + "size": 72, + "kind": "text" + }, + { + "field": "", + "box": [ + 72, + 235, + 1056, + 7 + ], + "size": 42, + "kind": "rule" + }, + { + "field": "updated", + "box": [ + 72, + 1500, + 1056, + 70 + ], + "size": 32, + "kind": "text" + }, + { + "field": "photo_id", + "box": [ + 72, + 295, + 1056, + 920 + ], + "size": 42, + "kind": "photo" + }, + { + "field": "message", + "box": [ + 72, + 1280, + 1056, + 170 + ], + "size": 48, + "kind": "text" + } + ] +} diff --git a/pigway_frame_flow/templates/quote.json b/pigway_frame_flow/templates/quote.json new file mode 100644 index 0000000..9bc40a7 --- /dev/null +++ b/pigway_frame_flow/templates/quote.json @@ -0,0 +1,78 @@ +{ + "id": "quote", + "name": "Quote / 一句话", + "width": 1200, + "height": 1600, + "fields": [ + "author", + "date", + "events", + "photo_id", + "quote", + "title", + "weather", + "weekday" + ], + "defaults": { + "title": "今日一句 / A thought", + "quote": "慢下来,生活自有答案。", + "author": "Frame Flow" + }, + "blocks": [ + { + "field": "title", + "box": [ + 72, + 60, + 1056, + 150 + ], + "size": 72, + "kind": "text" + }, + { + "field": "", + "box": [ + 72, + 235, + 1056, + 7 + ], + "size": 42, + "kind": "rule" + }, + { + "field": "updated", + "box": [ + 72, + 1500, + 1056, + 70 + ], + "size": 32, + "kind": "text" + }, + { + "field": "quote", + "box": [ + 72, + 460, + 1056, + 620 + ], + "size": 72, + "kind": "text" + }, + { + "field": "author", + "box": [ + 72, + 1200, + 1056, + 100 + ], + "size": 42, + "kind": "text" + } + ] +} diff --git a/pigway_frame_flow/templates/system-alert.json b/pigway_frame_flow/templates/system-alert.json new file mode 100644 index 0000000..ea1369b --- /dev/null +++ b/pigway_frame_flow/templates/system-alert.json @@ -0,0 +1,76 @@ +{ + "id": "system-alert", + "name": "System Alert / 系统告警", + "width": 1200, + "height": 1600, + "fields": [ + "date", + "events", + "message", + "photo_id", + "title", + "weather", + "weekday" + ], + "defaults": { + "title": "SYSTEM ALERT / 系统告警", + "message": "需要检查设备状态 / Please check the device" + }, + "blocks": [ + { + "field": "title", + "box": [ + 72, + 60, + 1056, + 150 + ], + "size": 72, + "kind": "text" + }, + { + "field": "", + "box": [ + 72, + 235, + 1056, + 7 + ], + "size": 42, + "kind": "rule" + }, + { + "field": "updated", + "box": [ + 72, + 1500, + 1056, + 70 + ], + "size": 32, + "kind": "text" + }, + { + "field": "message", + "box": [ + 72, + 430, + 1056, + 650 + ], + "size": 72, + "kind": "text" + }, + { + "field": "details", + "box": [ + 72, + 1150, + 1056, + 260 + ], + "size": 42, + "kind": "text" + } + ] +} diff --git a/pigway_frame_flow/templates/weather.json b/pigway_frame_flow/templates/weather.json new file mode 100644 index 0000000..3f84050 --- /dev/null +++ b/pigway_frame_flow/templates/weather.json @@ -0,0 +1,85 @@ +{ + "id": "weather", + "name": "Weather / 天气", + "width": 1200, + "height": 1600, + "fields": [ + "date", + "events", + "photo_id", + "title", + "weather", + "weekday" + ], + "defaults": { + "title": "天气 / Weather" + }, + "blocks": [ + { + "field": "title", + "box": [ + 72, + 60, + 1056, + 150 + ], + "size": 72, + "kind": "text" + }, + { + "field": "", + "box": [ + 72, + 235, + 1056, + 7 + ], + "size": 42, + "kind": "rule" + }, + { + "field": "updated", + "box": [ + 72, + 1500, + 1056, + 70 + ], + "size": 32, + "kind": "text" + }, + { + "field": "date", + "box": [ + 72, + 285, + 1056, + 90 + ], + "size": 56, + "kind": "text" + }, + { + "field": "weather", + "box": [ + 72, + 440, + 1056, + 450 + ], + "size": 62, + "kind": "text" + }, + { + "field": "forecast", + "box": [ + 72, + 990, + 1056, + 400 + ], + "size": 44, + "kind": "text" + } + ] +} diff --git a/pigway_frame_flow/web/app.js b/pigway_frame_flow/web/app.js new file mode 100644 index 0000000..659af08 --- /dev/null +++ b/pigway_frame_flow/web/app.js @@ -0,0 +1,54 @@ +'use strict'; +const $=s=>document.querySelector(s); +const state={lang:localStorage.getItem('ff-language')||'zh',theme:localStorage.getItem('ff-theme')||'system',page:location.hash.slice(1)||'dashboard',token:'',device:'living-room',settings:null,devices:[],templates:[],gallery:[],assets:[],blobs:[],tab:'cloud',preview:null,templatePreview:null,selectedTemplate:null,epoch:0}; +const icons={dashboard:'◫',gallery:'▧',upload:'↥',templates:'▦',automation:'◷',playlists:'▤',history:'↺',settings:'⚙'}; +const t=k=>window.MESSAGES[state.lang][k]||k; +const esc=v=>String(v??'—').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); +const fmt=v=>v?new Date(v).toLocaleString(state.lang==='zh'?'zh-CN':'en-GB',{timeZone:'Asia/Shanghai'}):'—'; +const buttons=(id,text,primary=false)=>``; +let toastTimer; +function toast(message){$('#toast').textContent=message;$('#toast').style.display='block';clearTimeout(toastTimer);toastTimer=setTimeout(()=>$('#toast').style.display='none',6000);} +async function api(path,options={}){ + const headers={'Authorization':'Bearer '+state.token,...options.headers}; + if(options.body&&!(options.body instanceof FormData)){headers['Content-Type']='application/json';options.body=JSON.stringify(options.body);} + const response=await fetch(path,{...options,headers}); + if(!response.ok){let error;try{error=(await response.json()).error;}catch{} + const e=new Error((error?.code||response.status)+': '+(error?.code&&t(error.code)!==error.code?t(error.code):(error?.message||t('error'))));e.code=error?.code;throw e;} + if(response.status===204)return null; + return options.binary?await response.blob():await response.json(); +} +function bind(id,fn){const element=$('#'+id);if(element)element.onclick=async event=>{try{element.disabled=true;await fn(event);}catch(e){toast(e.message);}finally{element.disabled=false;}};} +function busy(fn){return async event=>{try{await fn(event);}catch(e){toast(e.message);}};} +function applyTheme(){const dark=state.theme==='dark'||(state.theme==='system'&&matchMedia('(prefers-color-scheme:dark)').matches);document.documentElement.dataset.theme=dark?'dark':'light';} +function shell(){document.documentElement.lang=state.lang==='zh'?'zh-CN':'en';$('#lang').textContent=state.lang==='zh'?'EN':'中文';$('#theme').title=t('theme');$('#nav').innerHTML=Object.entries(icons).map(([id,icon])=>``).join('');document.querySelectorAll('[data-page]').forEach(b=>b.onclick=()=>navigate(b.dataset.page));$('#page-title').textContent=t(state.page);$('#connection').textContent=state.settings?(state.settings.mock?t('mock'):(!state.settings.write_enabled?t('readOnly'):t('writable'))):t('noToken');$('#device').innerHTML=(state.devices.length?state.devices:[{id:state.device,name:state.device}]).map(d=>``).join('');} +function clearBlobs(){for(const url of state.blobs)URL.revokeObjectURL(url);state.blobs=[];} +async function blobImage(img,path){try{const blob=await api(path,{binary:true});if(!img.isConnected)return;const url=URL.createObjectURL(blob);state.blobs.push(url);img.src=url;}catch{img.alt=t('apiError');}} +function hydrateImages(){const observer=new IntersectionObserver(entries=>entries.forEach(entry=>{if(entry.isIntersecting){observer.unobserve(entry.target);blobImage(entry.target,entry.target.dataset.src);}}),{rootMargin:'100px'});document.querySelectorAll('img[data-src]').forEach(img=>observer.observe(img));} +function notice(){if(!state.settings)return '';return `
${state.settings.mock?t('mock'):(!state.settings.write_enabled?t('readOnlyWarning'):t('writable'))}
`;} +async function navigate(page=state.page){state.page=icons[page]?page:'dashboard';location.hash=state.page;shell();clearBlobs();state.epoch++;const epoch=state.epoch; + if(!state.token){login();return;} + $('#main').innerHTML='
'; + try{await views[state.page](epoch);}catch(e){if(epoch!==state.epoch)return;$('#main').innerHTML=`
${t('apiError')}

${esc(e.message)}

${buttons('retry','refresh')}
`;bind('retry',()=>navigate());} +} +function login(){ $('#main').innerHTML=`
FRAME FLOW

${t('connect')}

${t('tokenHint')}

`;$('#login-form').onsubmit=busy(async e=>{e.preventDefault();state.token=$('#api-token').value.trim();try{state.settings=await api('/api/v1/settings');try{state.devices=await api('/api/v1/devices');}catch(error){state.devices=[];toast(error.message);}state.templates=await api('/api/v1/templates');state.device=state.devices[0]?.id||state.device;await navigate();}catch(error){state.token='';toast(error.message);}});} +async function accepted(path,payload){const key='web-'+crypto.randomUUID();const result=await api(path,{method:'POST',body:payload,headers:{'Idempotency-Key':key}});toast(t('accepted'));return result.operation_id;} +async function waitOperation(id){for(let n=0;n<120;n++){const operation=await api('/api/v1/operations/'+id);if(operation.status==='COMPLETED')return operation;if(['FAILED','UNKNOWN'].includes(operation.status))throw new Error(operation.status==='UNKNOWN'?t('unknown'):(operation.error?.message||t('error')));await new Promise(resolve=>setTimeout(resolve,2000));}throw new Error(t('syncing'));} +function showModal(html){$('#modal-content').innerHTML=html;$('#modal').showModal();bind('modal-cancel',()=>$('#modal').close());} +async function confirmDialog(message,fn){showModal(`

${t('confirm')}

${esc(message)}

`);bind('modal-confirm',async()=>{await fn();$('#modal').close();});} +function detail(label,value){return `
${t(label)}${esc(value)}
`;} +const views={ + async dashboard(epoch){const d=await api('/api/v1/devices/'+state.device);if(epoch!==state.epoch)return;const s=d.slideshow;$('#main').innerHTML=notice()+`${d.power.battery_percent!==null&&d.power.battery_percent<20?`
${t('lowBattery')}
`:''}
● ${t(d.status).toUpperCase()}

${esc(d.name)}

${esc(d.model)} · ${esc(d.model_code)} · ${d.display.width} × ${d.display.height}

${buttons('go-upload','upload',true)}${buttons('restore','restore')}${buttons('refresh-device','refresh')}

${t('refreshInterval')}

${t('testLabel')}
${[['battery',d.power.battery_percent===null?'—':d.power.battery_percent+'%'],['current',d.current_content.id??'—'],['sync',t(d.current_content.synced?'synced':'syncing')],['firmware',d.firmware]].map(([l,v])=>`
${t(l)}
${esc(v)}
`).join('')}

${t('device')}

${detail('heartbeat',fmt(d.last_seen_at))}${detail('rotation',d.display.rotation+'°')}${detail('timezone',d.timezone)}${detail('coverId',d.current_content.cover_id??'—')}${detail('storage',d.storage_used_bytes===null?'—':(d.storage_used_bytes/1024/1024).toFixed(1)+' MiB')}${detail('model',t(d.model_verified?'modelVerified':'modelConfigured'))}

${t('cloudRotation')}

${detail('mode',s.mode?t(s.mode):'—')}${detail('interval',s.interval_minutes==null?'—':s.interval_minutes+' '+t('minutes'))}

${t('rotationNotice')}

${t('playlists')} →
`;bind('go-upload',()=>navigate('upload'));bind('refresh-device',async()=>{await api('/api/v1/devices/'+state.device+'?refresh=true');await navigate();});bind('restore',()=>accepted('/api/v1/devices/'+state.device+'/restore',{}));$('#go-playlists').onclick=e=>{e.preventDefault();navigate('playlists');};}, + async gallery(epoch){let items=state.tab==='cloud'?await api('/api/v1/devices/'+state.device+'/images'):await api('/api/v1/assets');if(epoch!==state.epoch)return;state.gallery=items;$('#main').innerHTML=notice()+`
${buttons('gallery-refresh','refresh')}${state.tab==='cloud'?buttons('show-name','displayByName'):''}${buttons('gallery-upload','upload',true)}
${state.tab==='local'?`
${t('uploadNotice')}
`:''}${items.length?``:`
${t('noImages')}${t('emptyHint')}
`}`;bind('cloud-tab',()=>{state.tab='cloud';return navigate();});bind('local-tab',()=>{state.tab='local';return navigate();});bind('gallery-refresh',()=>navigate());bind('gallery-upload',()=>navigate('upload'));bind('show-name',()=>{showModal(`

${t('displayByName')}

`);bind('show-name-confirm',async()=>{await accepted('/api/v1/display',{device_id:state.device,content:{type:'image',image_name:$('#image-name').value}});$('#modal').close();});});hydrateImages();document.querySelectorAll('[data-show]').forEach(b=>b.onclick=busy(()=>{const x=items[b.dataset.show];return accepted('/api/v1/display',{device_id:state.device,content:state.tab==='cloud'?{type:'image',image_id:x.image_id}:{type:'image',asset_id:x.asset_id}});}));document.querySelectorAll('[data-original]').forEach(b=>b.onclick=busy(async()=>{const x=items[b.dataset.original];showModal(`

${esc(x.name)}

${esc(x.name)}`);await blobImage($('#original-image'),x.url);}));document.querySelectorAll('[data-delete]').forEach(b=>b.onclick=()=>{const x=items[b.dataset.delete];confirmDialog(x.current?t('deleteCurrent'):t('deleteConfirm'),async()=>{if(state.tab==='local'){await api('/api/v1/assets/'+x.asset_id,{method:'DELETE'});}else{const result=await api(`/api/v1/devices/${state.device}/images/${x.image_id}?confirm_current=${x.current}`,{method:'DELETE',headers:{'Idempotency-Key':'web-'+crypto.randomUUID()}});await waitOperation(result.operation_id);}await navigate();});});}, + async upload(){state.preview=null;$('#main').innerHTML=notice()+`
${t('uploadNotice')}

${t('upload')}

${t('selectFile')}

${t('uploadHint')}

${buttons('make-preview','preview',true)}

${t('previewHint')}

${t('preview')}

${t('previewHint')}
`;bind('make-preview',async()=>{const file=$('#file').files[0];if(!file)throw new Error(t('selectFile'));const form=new FormData();form.append('file',file);form.append('fit',$('#fit').value);form.append('profile',$('#profile').value);state.preview=await api('/api/v1/preview',{method:'POST',body:form});$('#preview-box').className='';$('#preview-box').innerHTML='Preview';await blobImage($('#upload-preview'),state.preview.url);$('#save-local').disabled=false;$('#display-upload').disabled=false;});for(const id of ['file','fit','profile'])$('#'+id).onchange=()=>{state.preview=null;$('#save-local').disabled=true;$('#display-upload').disabled=true;};bind('save-local',()=>{toast(t('done'));state.tab='local';return navigate('gallery');});bind('display-upload',()=>accepted('/api/v1/display',{device_id:state.device,content:{type:'image',asset_id:state.preview.asset_id}}));}, + async templates(){state.templates=await api('/api/v1/templates');const selected=state.templates.find(x=>x.id===state.selectedTemplate)||state.templates[0];state.selectedTemplate=selected.id;state.templatePreview=null;$('#main').innerHTML=notice()+`
${t('templateHint')}
${state.templates.map((x,i)=>`
${['☀','▦','◒','▣','❝','☁'][i]}

${esc(x.name.split(' / ')[state.lang==='zh'?1:0]||x.name)}

1200 × 1600 · ${esc(x.id)}
`).join('')}

${t('templateData')}

${buttons('render-template','render',true)}${buttons('save-template','saveTemplate')}

${t('preview')}

${t('previewHint')}
`;document.querySelectorAll('[data-template]').forEach(b=>{b.onclick=()=>{state.selectedTemplate=b.dataset.template;navigate();};b.onkeydown=e=>{if(e.key==='Enter')b.click();};});bind('render-template',async()=>{const content={type:'template',template:selected.id,data:JSON.parse($('#template-data').value),profile:$('#template-profile').value};const id=await accepted('/api/v1/render',content);$('#template-preview').textContent=t('previewLoading');const op=await waitOperation(id);state.templatePreview=op.result.asset_id;$('#template-preview').className='';$('#template-preview').innerHTML='Template preview';await blobImage($('#template-image'),op.result.url);$('#display-template').disabled=false;});bind('save-template',async()=>{await api('/api/v1/templates/'+selected.id+'/config',{method:'PUT',body:{data:JSON.parse($('#template-data').value)}});toast(t('done'));});$('#template-data').oninput=()=>$('#display-template').disabled=true;$('#template-profile').onchange=()=>$('#display-template').disabled=true;bind('display-template',()=>accepted('/api/v1/display',{device_id:state.device,content:{type:'image',asset_id:state.templatePreview}}));}, + async automation(epoch){const jobs=await api('/api/v1/jobs');if(epoch!==state.epoch)return;$('#main').innerHTML=notice()+`

Asia/Shanghai · ${t('automation')}

${buttons('new-job','newJob',true)}
${jobs.length?`
${['name','schedule','status','nextRun','lastRun','result','action'].map(x=>``).join('')}${jobs.map((j,i)=>``).join('')}
${t(x)}
${esc(j.name)}
${esc(j.device_id)}
${t(j.schedule.type)}
${esc(j.schedule.time||j.schedule.cron||fmt(j.schedule.at))}
${t(j.enabled?'enabled':'disabled')}${fmt(j.next_run)}${fmt(j.last_run)}${esc(j.last_result)}
`:`
${t('noTasks')}
`}
`;bind('new-job',()=>jobEditor());document.querySelectorAll('[data-job-edit]').forEach(b=>b.onclick=()=>jobEditor(jobs[b.dataset.jobEdit]));document.querySelectorAll('[data-job-toggle]').forEach(b=>b.onclick=busy(async()=>{const j=jobs[b.dataset.jobToggle];await api('/api/v1/jobs/'+j.id,{method:'PUT',body:jobPayload(j,{enabled:!j.enabled})});await navigate();}));document.querySelectorAll('[data-job-delete]').forEach(b=>b.onclick=()=>confirmDialog(t('removeConfirm'),async()=>{await api('/api/v1/jobs/'+jobs[b.dataset.jobDelete].id,{method:'DELETE'});await navigate();}));}, + async playlists(epoch){const playlists=await api('/api/v1/playlists');if(epoch!==state.epoch)return;$('#main').innerHTML=notice()+`
${t('playlistNotice')}

${t('playlists')}

${buttons('new-playlist','newPlaylist',true)}
`;bind('new-playlist',()=>playlistEditor());document.querySelectorAll('[data-pl-edit]').forEach(b=>b.onclick=()=>playlistEditor(playlists[b.dataset.plEdit]));document.querySelectorAll('[data-pl-delete]').forEach(b=>b.onclick=()=>confirmDialog(t('removeConfirm'),async()=>{await api('/api/v1/playlists/'+playlists[b.dataset.plDelete].id,{method:'DELETE'});await navigate();}));}, + async history(epoch){const ops=await api('/api/v1/operations');if(epoch!==state.epoch)return;$('#main').innerHTML=`

${t('history')}

${buttons('history-refresh','refresh')}
${ops.some(x=>x.status==='UNKNOWN')?`
${t('unknown')}
`:''}
${ops.length?`${['created','action','status','result','client','requestId'].map(x=>``).join('')}${ops.map((o,i)=>``).join('')}
${t(x)}
${fmt(o.created_at)}${esc(o.action)}
${esc(o.device_id)}
${t(o.status)}${o.status==='UNKNOWN'?`
`:''}
${esc(o.result?.image_id||o.result?.asset_id||o.error?.code||'—')}${esc(o.client_id)}${esc(o.request_id)}
${esc(o.operation_id)}
`:`
${t('noHistory')}
`}
`;bind('history-refresh',()=>navigate());document.querySelectorAll('[data-reconcile]').forEach(b=>b.onclick=busy(async()=>{const o=ops[b.dataset.reconcile];const r=await api('/api/v1/operations/'+o.operation_id+'/reconcile',{method:'POST',body:{acknowledge:false}});showModal(`

${t('reconcile')}

${t('current')}: ${esc(r.current_image_id)}

${t('result')}: ${esc(r.expected_image_id)}

${t('unknown')}

`);bind('acknowledge',async()=>{await api('/api/v1/operations/'+o.operation_id+'/reconcile',{method:'POST',body:{acknowledge:true}});$('#modal').close();await navigate();});}));}, + async settings(){state.settings=await api('/api/v1/settings');const s=state.settings;$('#main').innerHTML=notice()+`

${t('settings')}

${t('tokenHint')}

${buttons('logout','logout')}

${t('auth')}

${detail('version',s.version)}${detail('writeProtection',t(!s.write_enabled?'yes':'no'))}${detail('keyConfigured',t(s.api_key_configured?'yes':'no'))}${detail('timezone',s.timezone)}

${t('refreshInterval')}

${t('apiDocs')} →

GET /openapi.json · /api/v1

`;$('#theme-select').onchange=()=>{state.theme=$('#theme-select').value;localStorage.setItem('ff-theme',state.theme);applyTheme();};$('#language-select').onchange=()=>{state.lang=$('#language-select').value;localStorage.setItem('ff-language',state.lang);navigate();};bind('logout',()=>{state.token='';state.settings=null;shell();login();});} +}; +function jobPayload(j,changes={}){return {name:j.name,enabled:j.enabled,device_id:j.device_id,schedule:j.schedule,source:j.source,display_duration:j.display_duration,restore_previous:j.restore_previous,priority:j.priority,...changes};} +function jobEditor(job){const j=job||{name:'',enabled:true,device_id:state.device,schedule:{type:'daily',time:'07:00'},source:{type:'template',template:'daily-dashboard',data:{}},display_duration:5400,restore_previous:true,priority:50};showModal(`

${t(job?'edit':'newJob')}

`);function update(){const type=$('#job-schedule').value;$('#schedule-label').textContent=t(type==='cron'?'cron':['date','temporary'].includes(type)?'at':'time');$('#schedule-value').type=type==='cron'?'text':['date','temporary'].includes(type)?'datetime-local':'time';}$('#job-schedule').onchange=()=>{update();$('#schedule-value').value='';};update();$('#job-form').onsubmit=busy(async e=>{e.preventDefault();const type=$('#job-schedule').value,value=$('#schedule-value').value;const schedule={type};if(type==='cron')schedule.cron=value;else if(['date','temporary'].includes(type))schedule.at=value+':00+08:00';else schedule.time=value;const payload=jobPayload(j,{name:$('#job-name').value,schedule,source:JSON.parse($('#job-source').value),display_duration:Number($('#job-duration').value),priority:Number($('#job-priority').value),restore_previous:$('#job-restore').checked,enabled:$('#job-enabled').checked});await api('/api/v1/jobs'+(job?'/'+job.id:''),{method:job?'PUT':'POST',body:payload});$('#modal').close();await navigate();});} +function playlistEditor(p){p=p||{name:'',image_ids:[],interval_minutes:60,mode:'queue',enabled:false};showModal(`

${t('playlists')}

`);$('#playlist-form').onsubmit=busy(async e=>{e.preventDefault();await api('/api/v1/playlists'+(p.id?'/'+p.id:''),{method:p.id?'PUT':'POST',body:{name:$('#playlist-name').value,device_id:p.device_id||state.device,image_ids:$('#playlist-images').value.split(',').map(x=>Number(x.trim())),interval_minutes:Number($('#playlist-interval').value),mode:$('#playlist-mode').value,enabled:$('#playlist-enabled').checked}});$('#modal').close();await navigate();});} +$('#lang').onclick=()=>{state.lang=state.lang==='zh'?'en':'zh';localStorage.setItem('ff-language',state.lang);navigate();};$('#theme').onclick=()=>{state.theme=document.documentElement.dataset.theme==='dark'?'light':'dark';localStorage.setItem('ff-theme',state.theme);applyTheme();};$('#device').onchange=()=>{state.device=$('#device').value;navigate();};window.addEventListener('hashchange',()=>{const page=location.hash.slice(1);if(page!==state.page)navigate(page);});matchMedia('(prefers-color-scheme:dark)').addEventListener('change',applyTheme); +applyTheme();navigate();api('/api/v1/version').then(v=>$('#footer').innerHTML=`Pigway Frame Flow · ${t('footer')}v${esc(v.version)} · ${esc(v.commit.slice(0,8))} · ${esc(v.build_time)}`).catch(()=>{}); +setInterval(()=>{if(state.token&&state.page==='dashboard'&&!$('#modal').open)navigate();},60000); diff --git a/pigway_frame_flow/web/i18n.js b/pigway_frame_flow/web/i18n.js new file mode 100644 index 0000000..c1964ca --- /dev/null +++ b/pigway_frame_flow/web/i18n.js @@ -0,0 +1,4 @@ +window.MESSAGES={ +zh:{INVALID_REQUEST:'请求无效,请检查输入',UNAUTHORIZED:'API Token 无效或未填写',FORBIDDEN:'没有执行此操作的权限',DEVICE_NOT_FOUND:'找不到目标设备',DEVICE_OFFLINE:'画框离线',UNSUPPORTED_CAPABILITY:'设备或内容类型不支持此功能',WRITE_RESULT_UNKNOWN:'写入结果未知,请先核对设备状态',RATE_LIMITED:'请求过于频繁,请稍后再试',WRITE_DISABLED:'真实设备写入已关闭',CONFIG_REQUIRED:'请先配置设备',CLOUD_UNAVAILABLE:'设备云端暂时不可用',NOT_FOUND:'找不到请求的内容',CONFLICT:'配置冲突或内容仍被恢复任务引用,请先检查状态',INTERNAL_ERROR:'服务内部错误',lastScheduled:'上次调度',displayByName:'按文件名显示',QUEUED:'排队中',RENDERING:'生成中',UPLOADING:'发送中',SYNCING:'等待同步',COMPLETED:'已完成',FAILED:'失败',UNKNOWN:'结果未知',modelVerified:'API 已验证',modelConfigured:'设备配置值',dashboard:'总览',gallery:'图库',upload:'上传图片',templates:'模板',automation:'自动化',playlists:'播放列表',history:'历史记录',settings:'设置',refresh:'刷新',restore:'恢复上一张',online:'在线',offline:'离线',battery:'电量',firmware:'固件',current:'当前图片',heartbeat:'最近心跳',device:'设备信息',rotation:'旋转角度',timezone:'时区',sync:'同步状态',synced:'已同步',syncing:'等待同步',cloud:'云端图库',local:'本地图库',preview:'预览',display:'显示到画框',delete:'删除',original:'查看图片',name:'名称',created:'创建时间',save:'保存',cancel:'取消',confirm:'确认',loading:'正在加载…',mode:'适配方式',profile:'渲染模式',contain:'完整显示 · 留白',cover:'铺满屏幕 · 中心裁切',smart:'智能裁切 · 检查预览',color:'彩色',grayscale:'灰度',staging:'仅保存到本地图库',uploadHint:'JPG、PNG、WebP · 最大 20 MiB · 1200 × 1600',uploadNotice:'KoKonna 云端上传会立即切换画框。仅保存图库时使用本地暂存,不会改变设备。',selectFile:'选择图片',previewHint:'先选择图片并预览,再决定保存或显示。smart 使用构图启发式,不保证识别人脸。',mock:'演示模式',readOnly:'真实设备 · 只读',writable:'真实设备 · 已授权写入',cloudRotation:'设备轮播',queue:'顺序',random:'随机',minutes:'分钟',rotationNotice:'可读取设备轮播模式与间隔;官方接口不提供具体轮播成员,也不支持修改规则。云端图库不是已确认的轮播队列。',playlistNotice:'以下是 Frame Flow 管理的播放列表,与 KoKonna 内置轮播独立。临时告警期间暂停,结束后继续。建议在厂商 App 中关闭内置自动轮播以避免竞争。',newPlaylist:'新建播放列表',newJob:'新建任务',interval:'切换间隔(分钟)',imageIds:'图片 ID(逗号分隔)',enabled:'启用',disabled:'暂停',edit:'编辑',remove:'移除',schedule:'时间安排',source:'内容来源',duration:'显示时长(秒)',restoreAfter:'到期恢复之前图片',priority:'优先级(0–100)',cron:'Cron',daily:'每天',weekdays:'工作日',weekends:'周末',date:'指定日期',temporary:'临时任务',time:'时间(上海时区)',at:'执行日期',lastRun:'上次运行',nextRun:'下次运行',result:'结果',templateData:'模板数据(JSON)',render:'生成预览',templateHint:'模板与数据、设备驱动分离。预览成功后再推送。天气为明确标记的 Mock。',template:'模板',operation:'操作',action:'动作',requestId:'请求 ID',client:'调用方',noImages:'图库为空',emptyHint:'上传图片或生成模板,开始布置你的画框。',noTasks:'还没有任务',noHistory:'暂无历史记录',connect:'连接 Frame Flow',token:'Frame Flow API Token',tokenHint:'输入服务自己的 API Token。不要在这里输入 KoKonna API Key。Token 只保留在当前页面内存,刷新后需重新输入。',logout:'断开连接',auth:'鉴权已启用',apiDocs:'API 文档',version:'版本',writeProtection:'真实写入保护',keyConfigured:'设备密钥已配置',yes:'是',no:'否',lowBattery:'电量较低,建议充电',unknown:'写入结果未知,需要核对设备状态',reconcile:'只读核对',acknowledge:'已人工核对,解除阻塞',accepted:'操作已受理;请在历史记录查看同步结果。',deleteCurrent:'这是当前显示图片。删除后画框将切换到其他图片。是否继续?',deleteConfirm:'确认删除此图片?此操作不可撤销。',removeConfirm:'确认移除此配置?',error:'操作失败',done:'已完成',apiError:'设备云端异常,请稍后刷新',noToken:'输入 API Token 后可管理画框',status:'状态',systemTheme:'跟随系统',theme:'外观',language:'语言',dark:'深色',light:'浅色',refreshInterval:'设备信息缓存 60 秒;图库仅按需读取',model:'型号',coverId:'封面 ID',saveTemplate:'保存模板默认数据',previewLoading:'正在生成预览…',storage:'设备存储占用',readOnlyWarning:'真实写入已关闭。可以预览和配置任务,但不会上传、切换或删除真实设备图片。',footer:'独立智能画框服务',photos:'画框内容',count:'张图片',testLabel:'示意画面 · 不代表当前真实内容'}, +en:{INVALID_REQUEST:'Invalid request; check your input',UNAUTHORIZED:'Missing or invalid API Token',FORBIDDEN:'This token does not allow this action',DEVICE_NOT_FOUND:'Device not found',DEVICE_OFFLINE:'Target frame is offline',UNSUPPORTED_CAPABILITY:'Unsupported device capability or content type',WRITE_RESULT_UNKNOWN:'Write outcome unknown; reconcile device state',RATE_LIMITED:'Request budget exceeded; try later',WRITE_DISABLED:'Real device writes are disabled',CONFIG_REQUIRED:'Device configuration required',CLOUD_UNAVAILABLE:'Device cloud is unavailable',NOT_FOUND:'Requested content not found',CONFLICT:'Conflicting configuration or restore reference; check current state',INTERNAL_ERROR:'Internal service error',lastScheduled:'Last scheduled',displayByName:'Display by filename',QUEUED:'Queued',RENDERING:'Rendering',UPLOADING:'Sending',SYNCING:'Awaiting sync',COMPLETED:'Completed',FAILED:'Failed',UNKNOWN:'Unknown outcome',modelVerified:'API verified',modelConfigured:'Configured model',dashboard:'Dashboard',gallery:'Gallery',upload:'Upload',templates:'Templates',automation:'Automation',playlists:'Playlists',history:'History',settings:'Settings',refresh:'Refresh',restore:'Restore previous',online:'Online',offline:'Offline',battery:'Battery',firmware:'Firmware',current:'Current image',heartbeat:'Last heartbeat',device:'Device details',rotation:'Rotation',timezone:'Timezone',sync:'Sync state',synced:'Synced',syncing:'Awaiting sync',cloud:'Cloud gallery',local:'Local gallery',preview:'Preview',display:'Display on frame',delete:'Delete',original:'View image',name:'Name',created:'Created',save:'Save',cancel:'Cancel',confirm:'Confirm',loading:'Loading…',mode:'Image fit',profile:'Renderer profile',contain:'Contain · white margins',cover:'Cover · center crop',smart:'Smart crop · inspect preview',color:'Color',grayscale:'Grayscale',staging:'Save to local gallery only',uploadHint:'JPG, PNG, WebP · up to 20 MiB · 1200 × 1600',uploadNotice:'KoKonna cloud uploads immediately activate the image. Gallery-only uploads are staged locally without changing the device.',selectFile:'Choose an image',previewHint:'Preview your image before saving or displaying it. Smart crop uses saliency heuristics and does not guarantee face detection.',mock:'Demo mode',readOnly:'Live device · read only',writable:'Live device · writes authorized',cloudRotation:'Device slideshow',queue:'Sequential',random:'Random',minutes:'minutes',rotationNotice:'Device slideshow mode and interval are readable. The official API does not expose slideshow membership or rule editing. Cloud gallery is not a verified playback queue.',playlistNotice:'These playlists are managed by Frame Flow independently of KoKonna’s slideshow. Playback pauses during temporary overrides. Disable native slideshow in the vendor app to avoid competing switches.',newPlaylist:'New playlist',newJob:'New job',interval:'Interval (minutes)',imageIds:'Image IDs (comma-separated)',enabled:'Enabled',disabled:'Paused',edit:'Edit',remove:'Remove',schedule:'Schedule',source:'Content source',duration:'Display duration (seconds)',restoreAfter:'Restore previous when expired',priority:'Priority (0–100)',cron:'Cron',daily:'Daily',weekdays:'Weekdays',weekends:'Weekends',date:'Specific date',temporary:'One-time task',time:'Time (Shanghai)',at:'Run at',lastRun:'Last run',nextRun:'Next run',result:'Result',templateData:'Template data (JSON)',render:'Generate preview',templateHint:'Templates are independent of providers and device drivers. Preview before displaying. Weather uses explicitly labeled mock data.',template:'Template',operation:'Operation',action:'Action',requestId:'Request ID',client:'Client',noImages:'Your gallery is empty',emptyHint:'Upload an image or render a template to make this frame yours.',noTasks:'No jobs yet',noHistory:'No history yet',connect:'Connect to Frame Flow',token:'Frame Flow API Token',tokenHint:'Use the service API Token, not the KoKonna API Key. The token stays only in this page’s memory; re-enter it after reload.',logout:'Disconnect',auth:'Authentication enabled',apiDocs:'API documentation',version:'Version',writeProtection:'Real write protection',keyConfigured:'Device key configured',yes:'Yes',no:'No',lowBattery:'Battery is low. Charge your frame.',unknown:'Write outcome unknown. Reconcile device state.',reconcile:'Read-only reconcile',acknowledge:'Manually verified; release block',accepted:'Operation accepted. Check History for synchronization results.',deleteCurrent:'This is the current image. Deleting it will make the device switch to another image. Continue?',deleteConfirm:'Delete this image? This cannot be undone.',removeConfirm:'Remove this configuration?',error:'Action failed',done:'Completed',apiError:'Device cloud error. Try refreshing later.',noToken:'Enter an API Token to manage your frames',status:'Status',systemTheme:'System',theme:'Appearance',language:'Language',dark:'Dark',light:'Light',refreshInterval:'Device state cached for 60 seconds; gallery loaded on demand',model:'Model',coverId:'Cover ID',saveTemplate:'Save template defaults',previewLoading:'Rendering preview…',storage:'Device storage used',readOnlyWarning:'Real writes are disabled. Preview and configure jobs safely; live device uploads, switches and deletes are blocked.',footer:'Independent smart frame service',photos:'Frame content',count:'images',testLabel:'Illustration · not the device’s current image'} +}; diff --git a/pigway_frame_flow/web/index.html b/pigway_frame_flow/web/index.html new file mode 100644 index 0000000..609e51a --- /dev/null +++ b/pigway_frame_flow/web/index.html @@ -0,0 +1,3 @@ + +Pigway Frame Flow +
PIGWAY FRAME FLOW

diff --git a/pigway_frame_flow/web/style.css b/pigway_frame_flow/web/style.css new file mode 100644 index 0000000..cf37e30 --- /dev/null +++ b/pigway_frame_flow/web/style.css @@ -0,0 +1,4 @@ +:root{color-scheme:light;--bg:#f4f6f5;--panel:#fff;--soft:#edf2f0;--ink:#182e27;--muted:#687970;--border:#dee6e1;--accent:#167253;--accent-soft:#e1f2e8;--bad:#b43636;--shadow:0 12px 34px #203e2a08} +:root[data-theme=dark]{color-scheme:dark;--bg:#111917;--panel:#1b2521;--soft:#24322b;--ink:#edf4ee;--muted:#a1b3a8;--border:#34463c;--accent:#8cddb1;--accent-soft:#254735;--bad:#ff9696;--shadow:0 12px 34px #0002} +*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC',sans-serif}button,input,select,textarea{font:inherit}button,a,input,select,textarea{outline-offset:4px}button{cursor:pointer;border:1px solid var(--border);border-radius:10px;background:var(--panel);color:var(--ink);padding:10px 16px;font-weight:600}button:hover{background:var(--soft)}button:disabled{opacity:.45;cursor:not-allowed}button.primary{background:var(--accent);color:var(--panel);border-color:var(--accent)}button.danger{color:var(--bad)}a{color:var(--accent);text-decoration:none}small{font-size:12px;color:var(--muted)}.sidebar{position:fixed;inset:0 auto 0 0;width:244px;background:var(--panel);border-right:1px solid var(--border);padding:32px 20px;display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:12px;color:var(--ink);font-size:20px;font-weight:750;letter-spacing:-.6px}.brand small{display:block;font-size:9px;letter-spacing:.2px;font-weight:500}.brand-icon{width:43px;height:43px;background:var(--accent);color:var(--panel);border-radius:13px;display:flex;align-items:center;justify-content:center;position:relative;font-size:25px}.brand-icon span{font-size:15px;margin-top:-18px}nav{margin-top:42px;display:grid;gap:8px}nav button{border:0;display:flex;gap:14px;align-items:center;text-align:left;color:var(--muted);padding:13px 16px;font-weight:500;background:transparent}nav button.active{background:var(--accent-soft);color:var(--accent);font-weight:650}nav .nav-icon{width:22px;text-align:center;font-size:19px}.sidebar-bottom{margin-top:auto;padding:20px 10px;border-top:1px solid var(--border);font-size:12px;color:var(--muted)}.sidebar-bottom small{display:block;margin-top:8px}.live-dot{display:inline-block;width:7px;height:7px;border-radius:100%;background:var(--accent);margin-right:8px}.workspace{margin-left:244px;padding:0 40px}header{padding:33px 0 28px;display:flex;justify-content:space-between;align-items:center;gap:16px}h1{font-size:30px;font-weight:650;letter-spacing:-1px;margin:4px 0 0}h2{font-size:21px;line-height:1.4;margin:0 0 12px;letter-spacing:-.3px}h3{font-size:17px;margin:0 0 12px}.eyebrow{font-size:10px;letter-spacing:2px;color:var(--muted);font-weight:650}.toolbar,.actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.icon-button{padding:9px 12px}.card{background:var(--panel);border:1px solid var(--border);border-radius:18px;padding:25px;box-shadow:var(--shadow)}.hero{display:grid;grid-template-columns:1.4fr 1fr;min-height:370px;background:var(--panel);overflow:hidden;padding:0}.hero-info{padding:35px}.hero h2{font-size:31px;margin:22px 0 0}.hero p{color:var(--muted);margin:4px 0 24px}.frame-demo{background:var(--soft);padding:35px;display:flex;align-items:center;justify-content:center;position:relative}.mini-art{width:185px;height:246px;background:#faf8ef;border:10px solid #72523b;box-shadow:0 18px 30px #0002;position:relative;overflow:hidden}.mini-art:before{content:'';position:absolute;bottom:-40px;left:-30px;background:#8cab82;width:180px;height:180px;border-radius:45%;transform:rotate(25deg)}.mini-art:after{content:'';position:absolute;bottom:-65px;right:-70px;background:#456a56;width:180px;height:230px;border-radius:45%;transform:rotate(-20deg)}.mini-art .sun{position:absolute;top:25px;right:25px;width:55px;height:55px;border-radius:50%;background:#dba25e}.frame-label{position:absolute;bottom:9px;font-size:10px;letter-spacing:1px;color:var(--muted)}.badge{display:inline-flex;align-items:center;gap:7px;font-size:11px;letter-spacing:.6px;border-radius:30px;padding:5px 12px;color:var(--accent);background:var(--accent-soft);font-weight:750}.badge.bad{background:#af343415;color:var(--bad)}.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px;margin:20px 0}.stat .value{font-size:24px;font-weight:650;letter-spacing:-.5px;margin-top:6px;word-break:break-word}.stat .label{font-size:12px;color:var(--muted)}.columns{display:grid;grid-template-columns:1.25fr 1fr;gap:20px;margin:20px 0}.detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}.detail-grid strong{display:block;font-size:14px;overflow-wrap:anywhere}.detail-grid small{display:block}.hint{font-size:13px;color:var(--muted);margin:12px 0}.notice{background:var(--accent-soft);border:1px solid var(--border);padding:14px 18px;border-radius:12px;margin-bottom:20px;font-size:13px}.notice.warning{color:var(--bad);background:#b4363610}.section-heading{display:flex;align-items:center;justify-content:space-between;gap:15px;margin-bottom:18px}.gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:20px}.image-card{padding:12px}.image-card .thumb{width:100%;aspect-ratio:3/4;background:var(--soft);border-radius:10px;object-fit:contain;display:block}.image-card h3{font-size:14px;margin:14px 0 2px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.image-card .actions{margin-top:12px}.image-card button{font-size:12px;padding:7px 10px}.empty{text-align:center;padding:64px 24px;color:var(--muted)}.empty strong{display:block;font-size:20px;color:var(--ink);margin-bottom:10px}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}label{display:block;font-size:13px;font-weight:650;margin:0 0 7px}input,textarea,select{width:100%;border:1px solid var(--border);border-radius:9px;background:var(--panel);color:var(--ink);padding:10px 12px}textarea{min-height:180px;resize:vertical;font:13px/1.7 ui-monospace,monospace}.toolbar select{width:auto;max-width:190px}input[type=checkbox]{width:auto;accent-color:var(--accent)}.field{margin-bottom:18px}.dropzone{border:2px dashed var(--border);border-radius:16px;padding:45px 25px;text-align:center;background:var(--soft)}.dropzone input{margin-top:15px}.preview{width:100%;max-height:620px;object-fit:contain;background:var(--soft);border-radius:12px}.template-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}.template-card{cursor:pointer;min-height:160px;display:flex;flex-direction:column;justify-content:space-between}.template-card:hover,.template-card.selected{border-color:var(--accent);background:var(--accent-soft)}.template-card span{font-size:35px}.table-wrap{overflow:auto}table{border-collapse:collapse;width:100%;font-size:13px}th{text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);padding:10px 14px}td{border-top:1px solid var(--border);padding:14px;vertical-align:top}td code{font-size:11px;overflow-wrap:anywhere}.row-actions{display:flex;gap:7px}.row-actions button{padding:5px 9px;font-size:12px}.tabs{display:flex;gap:6px;background:var(--soft);padding:5px;border-radius:12px;margin-bottom:20px;width:fit-content}.tabs button{border:0;background:transparent}.tabs button.active{background:var(--panel);box-shadow:var(--shadow)}footer{padding:30px 0;font-size:11px;color:var(--muted);display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}#toast{position:fixed;bottom:28px;right:28px;padding:14px 20px;background:var(--ink);color:var(--panel);border-radius:12px;max-width:430px;z-index:200;display:none;box-shadow:var(--shadow)}dialog{width:min(600px,calc(100vw - 32px));border:1px solid var(--border);border-radius:18px;padding:28px;background:var(--panel);color:var(--ink)}dialog::backdrop{background:#0007}.modal-actions{display:flex;justify-content:flex-end;gap:12px;margin-top:24px}.check-row{display:flex;align-items:center;gap:10px}.spinner{width:25px;height:25px;border:3px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin 1s linear infinite;margin:40px auto}@keyframes spin{to{transform:rotate(360deg)}}.wide{grid-column:1/-1} +@media(min-width:1600px){.workspace{max-width:1600px;margin:auto;padding-left:280px}.sidebar{left:max(0px,calc((100vw - 1600px)/2))}}@media(max-width:1050px){.workspace{padding:0 24px}.stats{grid-template-columns:repeat(2,1fr)}.columns{grid-template-columns:1fr}.template-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:720px){.sidebar{width:100%;height:64px;inset:auto 0 0;padding:0 6px;border-top:1px solid var(--border);z-index:100;overflow:auto}.brand,.sidebar-bottom{display:none}nav{margin:0;display:flex;min-width:660px;gap:0}nav button{display:flex;flex-direction:column;gap:0;padding:6px 11px;font-size:10px;border-radius:0;flex:1}.workspace{margin:0;padding:0 18px 75px}header{padding:22px 0;align-items:flex-start}h1{font-size:25px}.toolbar{gap:6px}.toolbar select{width:120px;font-size:12px;padding:9px}.hero{grid-template-columns:1fr}.hero-info{padding:25px}.hero h2{font-size:26px}.frame-demo{display:none}.stats{gap:10px}.stat{padding:18px}.stat .value{font-size:20px}.card{padding:20px}.form-grid{grid-template-columns:1fr}.template-grid{gap:10px}.gallery{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.image-card{padding:10px}.image-card .actions{gap:5px}.image-card button{font-size:11px;padding:6px}#toast{bottom:85px;right:18px;left:18px;max-width:none}.section-heading{align-items:flex-start;flex-wrap:wrap}} diff --git a/pigway_frame_flow/web/swagger-LICENSE b/pigway_frame_flow/web/swagger-LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/pigway_frame_flow/web/swagger-LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pigway_frame_flow/web/swagger-ui-bundle.js b/pigway_frame_flow/web/swagger-ui-bundle.js new file mode 100644 index 0000000..a9fd9e5 --- /dev/null +++ b/pigway_frame_flow/web/swagger-ui-bundle.js @@ -0,0 +1,2 @@ +/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */ +!function webpackUniversalModuleDefinition(s,o){"object"==typeof exports&&"object"==typeof module?module.exports=o():"function"==typeof define&&define.amd?define([],o):"object"==typeof exports?exports.SwaggerUIBundle=o():s.SwaggerUIBundle=o()}(this,(()=>(()=>{var s={67526(s,o){"use strict";o.byteLength=function byteLength(s){var o=getLens(s),i=o[0],a=o[1];return 3*(i+a)/4-a},o.toByteArray=function toByteArray(s){var o,i,_=getLens(s),w=_[0],x=_[1],C=new u(function _byteLength(s,o,i){return 3*(o+i)/4-i}(0,w,x)),j=0,L=x>0?w-4:w;for(i=0;i>16&255,C[j++]=o>>8&255,C[j++]=255&o;2===x&&(o=a[s.charCodeAt(i)]<<2|a[s.charCodeAt(i+1)]>>4,C[j++]=255&o);1===x&&(o=a[s.charCodeAt(i)]<<10|a[s.charCodeAt(i+1)]<<4|a[s.charCodeAt(i+2)]>>2,C[j++]=o>>8&255,C[j++]=255&o);return C},o.fromByteArray=function fromByteArray(s){for(var o,a=s.length,u=a%3,_=[],w=16383,x=0,C=a-u;xC?C:x+w));1===u?(o=s[a-1],_.push(i[o>>2]+i[o<<4&63]+"==")):2===u&&(o=(s[a-2]<<8)+s[a-1],_.push(i[o>>10]+i[o>>4&63]+i[o<<2&63]+"="));return _.join("")};for(var i=[],a=[],u="undefined"!=typeof Uint8Array?Uint8Array:Array,_="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",w=0;w<64;++w)i[w]=_[w],a[_.charCodeAt(w)]=w;function getLens(s){var o=s.length;if(o%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var i=s.indexOf("=");return-1===i&&(i=o),[i,i===o?0:4-i%4]}function encodeChunk(s,o,a){for(var u,_,w=[],x=o;x>18&63]+i[_>>12&63]+i[_>>6&63]+i[63&_]);return w.join("")}a["-".charCodeAt(0)]=62,a["_".charCodeAt(0)]=63},48287(s,o,i){"use strict";const a=i(67526),u=i(251),_="function"==typeof Symbol&&"function"==typeof Symbol.for?Symbol.for("nodejs.util.inspect.custom"):null;o.Buffer=Buffer,o.SlowBuffer=function SlowBuffer(s){+s!=s&&(s=0);return Buffer.alloc(+s)},o.INSPECT_MAX_BYTES=50;const w=2147483647;function createBuffer(s){if(s>w)throw new RangeError('The value "'+s+'" is invalid for option "size"');const o=new Uint8Array(s);return Object.setPrototypeOf(o,Buffer.prototype),o}function Buffer(s,o,i){if("number"==typeof s){if("string"==typeof o)throw new TypeError('The "string" argument must be of type string. Received type number');return allocUnsafe(s)}return from(s,o,i)}function from(s,o,i){if("string"==typeof s)return function fromString(s,o){"string"==typeof o&&""!==o||(o="utf8");if(!Buffer.isEncoding(o))throw new TypeError("Unknown encoding: "+o);const i=0|byteLength(s,o);let a=createBuffer(i);const u=a.write(s,o);u!==i&&(a=a.slice(0,u));return a}(s,o);if(ArrayBuffer.isView(s))return function fromArrayView(s){if(isInstance(s,Uint8Array)){const o=new Uint8Array(s);return fromArrayBuffer(o.buffer,o.byteOffset,o.byteLength)}return fromArrayLike(s)}(s);if(null==s)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof s);if(isInstance(s,ArrayBuffer)||s&&isInstance(s.buffer,ArrayBuffer))return fromArrayBuffer(s,o,i);if("undefined"!=typeof SharedArrayBuffer&&(isInstance(s,SharedArrayBuffer)||s&&isInstance(s.buffer,SharedArrayBuffer)))return fromArrayBuffer(s,o,i);if("number"==typeof s)throw new TypeError('The "value" argument must not be of type number. Received type number');const a=s.valueOf&&s.valueOf();if(null!=a&&a!==s)return Buffer.from(a,o,i);const u=function fromObject(s){if(Buffer.isBuffer(s)){const o=0|checked(s.length),i=createBuffer(o);return 0===i.length||s.copy(i,0,0,o),i}if(void 0!==s.length)return"number"!=typeof s.length||numberIsNaN(s.length)?createBuffer(0):fromArrayLike(s);if("Buffer"===s.type&&Array.isArray(s.data))return fromArrayLike(s.data)}(s);if(u)return u;if("undefined"!=typeof Symbol&&null!=Symbol.toPrimitive&&"function"==typeof s[Symbol.toPrimitive])return Buffer.from(s[Symbol.toPrimitive]("string"),o,i);throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof s)}function assertSize(s){if("number"!=typeof s)throw new TypeError('"size" argument must be of type number');if(s<0)throw new RangeError('The value "'+s+'" is invalid for option "size"')}function allocUnsafe(s){return assertSize(s),createBuffer(s<0?0:0|checked(s))}function fromArrayLike(s){const o=s.length<0?0:0|checked(s.length),i=createBuffer(o);for(let a=0;a=w)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+w.toString(16)+" bytes");return 0|s}function byteLength(s,o){if(Buffer.isBuffer(s))return s.length;if(ArrayBuffer.isView(s)||isInstance(s,ArrayBuffer))return s.byteLength;if("string"!=typeof s)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof s);const i=s.length,a=arguments.length>2&&!0===arguments[2];if(!a&&0===i)return 0;let u=!1;for(;;)switch(o){case"ascii":case"latin1":case"binary":return i;case"utf8":case"utf-8":return utf8ToBytes(s).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*i;case"hex":return i>>>1;case"base64":return base64ToBytes(s).length;default:if(u)return a?-1:utf8ToBytes(s).length;o=(""+o).toLowerCase(),u=!0}}function slowToString(s,o,i){let a=!1;if((void 0===o||o<0)&&(o=0),o>this.length)return"";if((void 0===i||i>this.length)&&(i=this.length),i<=0)return"";if((i>>>=0)<=(o>>>=0))return"";for(s||(s="utf8");;)switch(s){case"hex":return hexSlice(this,o,i);case"utf8":case"utf-8":return utf8Slice(this,o,i);case"ascii":return asciiSlice(this,o,i);case"latin1":case"binary":return latin1Slice(this,o,i);case"base64":return base64Slice(this,o,i);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return utf16leSlice(this,o,i);default:if(a)throw new TypeError("Unknown encoding: "+s);s=(s+"").toLowerCase(),a=!0}}function swap(s,o,i){const a=s[o];s[o]=s[i],s[i]=a}function bidirectionalIndexOf(s,o,i,a,u){if(0===s.length)return-1;if("string"==typeof i?(a=i,i=0):i>2147483647?i=2147483647:i<-2147483648&&(i=-2147483648),numberIsNaN(i=+i)&&(i=u?0:s.length-1),i<0&&(i=s.length+i),i>=s.length){if(u)return-1;i=s.length-1}else if(i<0){if(!u)return-1;i=0}if("string"==typeof o&&(o=Buffer.from(o,a)),Buffer.isBuffer(o))return 0===o.length?-1:arrayIndexOf(s,o,i,a,u);if("number"==typeof o)return o&=255,"function"==typeof Uint8Array.prototype.indexOf?u?Uint8Array.prototype.indexOf.call(s,o,i):Uint8Array.prototype.lastIndexOf.call(s,o,i):arrayIndexOf(s,[o],i,a,u);throw new TypeError("val must be string, number or Buffer")}function arrayIndexOf(s,o,i,a,u){let _,w=1,x=s.length,C=o.length;if(void 0!==a&&("ucs2"===(a=String(a).toLowerCase())||"ucs-2"===a||"utf16le"===a||"utf-16le"===a)){if(s.length<2||o.length<2)return-1;w=2,x/=2,C/=2,i/=2}function read(s,o){return 1===w?s[o]:s.readUInt16BE(o*w)}if(u){let a=-1;for(_=i;_x&&(i=x-C),_=i;_>=0;_--){let i=!0;for(let a=0;au&&(a=u):a=u;const _=o.length;let w;for(a>_/2&&(a=_/2),w=0;w>8,u=i%256,_.push(u),_.push(a);return _}(o,s.length-i),s,i,a)}function base64Slice(s,o,i){return 0===o&&i===s.length?a.fromByteArray(s):a.fromByteArray(s.slice(o,i))}function utf8Slice(s,o,i){i=Math.min(s.length,i);const a=[];let u=o;for(;u239?4:o>223?3:o>191?2:1;if(u+w<=i){let i,a,x,C;switch(w){case 1:o<128&&(_=o);break;case 2:i=s[u+1],128==(192&i)&&(C=(31&o)<<6|63&i,C>127&&(_=C));break;case 3:i=s[u+1],a=s[u+2],128==(192&i)&&128==(192&a)&&(C=(15&o)<<12|(63&i)<<6|63&a,C>2047&&(C<55296||C>57343)&&(_=C));break;case 4:i=s[u+1],a=s[u+2],x=s[u+3],128==(192&i)&&128==(192&a)&&128==(192&x)&&(C=(15&o)<<18|(63&i)<<12|(63&a)<<6|63&x,C>65535&&C<1114112&&(_=C))}}null===_?(_=65533,w=1):_>65535&&(_-=65536,a.push(_>>>10&1023|55296),_=56320|1023&_),a.push(_),u+=w}return function decodeCodePointsArray(s){const o=s.length;if(o<=x)return String.fromCharCode.apply(String,s);let i="",a=0;for(;aa.length?(Buffer.isBuffer(o)||(o=Buffer.from(o)),o.copy(a,u)):Uint8Array.prototype.set.call(a,o,u);else{if(!Buffer.isBuffer(o))throw new TypeError('"list" argument must be an Array of Buffers');o.copy(a,u)}u+=o.length}return a},Buffer.byteLength=byteLength,Buffer.prototype._isBuffer=!0,Buffer.prototype.swap16=function swap16(){const s=this.length;if(s%2!=0)throw new RangeError("Buffer size must be a multiple of 16-bits");for(let o=0;oi&&(s+=" ... "),""},_&&(Buffer.prototype[_]=Buffer.prototype.inspect),Buffer.prototype.compare=function compare(s,o,i,a,u){if(isInstance(s,Uint8Array)&&(s=Buffer.from(s,s.offset,s.byteLength)),!Buffer.isBuffer(s))throw new TypeError('The "target" argument must be one of type Buffer or Uint8Array. Received type '+typeof s);if(void 0===o&&(o=0),void 0===i&&(i=s?s.length:0),void 0===a&&(a=0),void 0===u&&(u=this.length),o<0||i>s.length||a<0||u>this.length)throw new RangeError("out of range index");if(a>=u&&o>=i)return 0;if(a>=u)return-1;if(o>=i)return 1;if(this===s)return 0;let _=(u>>>=0)-(a>>>=0),w=(i>>>=0)-(o>>>=0);const x=Math.min(_,w),C=this.slice(a,u),j=s.slice(o,i);for(let s=0;s>>=0,isFinite(i)?(i>>>=0,void 0===a&&(a="utf8")):(a=i,i=void 0)}const u=this.length-o;if((void 0===i||i>u)&&(i=u),s.length>0&&(i<0||o<0)||o>this.length)throw new RangeError("Attempt to write outside buffer bounds");a||(a="utf8");let _=!1;for(;;)switch(a){case"hex":return hexWrite(this,s,o,i);case"utf8":case"utf-8":return utf8Write(this,s,o,i);case"ascii":case"latin1":case"binary":return asciiWrite(this,s,o,i);case"base64":return base64Write(this,s,o,i);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return ucs2Write(this,s,o,i);default:if(_)throw new TypeError("Unknown encoding: "+a);a=(""+a).toLowerCase(),_=!0}},Buffer.prototype.toJSON=function toJSON(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};const x=4096;function asciiSlice(s,o,i){let a="";i=Math.min(s.length,i);for(let u=o;ua)&&(i=a);let u="";for(let a=o;ai)throw new RangeError("Trying to access beyond buffer length")}function checkInt(s,o,i,a,u,_){if(!Buffer.isBuffer(s))throw new TypeError('"buffer" argument must be a Buffer instance');if(o>u||o<_)throw new RangeError('"value" argument is out of bounds');if(i+a>s.length)throw new RangeError("Index out of range")}function wrtBigUInt64LE(s,o,i,a,u){checkIntBI(o,a,u,s,i,7);let _=Number(o&BigInt(4294967295));s[i++]=_,_>>=8,s[i++]=_,_>>=8,s[i++]=_,_>>=8,s[i++]=_;let w=Number(o>>BigInt(32)&BigInt(4294967295));return s[i++]=w,w>>=8,s[i++]=w,w>>=8,s[i++]=w,w>>=8,s[i++]=w,i}function wrtBigUInt64BE(s,o,i,a,u){checkIntBI(o,a,u,s,i,7);let _=Number(o&BigInt(4294967295));s[i+7]=_,_>>=8,s[i+6]=_,_>>=8,s[i+5]=_,_>>=8,s[i+4]=_;let w=Number(o>>BigInt(32)&BigInt(4294967295));return s[i+3]=w,w>>=8,s[i+2]=w,w>>=8,s[i+1]=w,w>>=8,s[i]=w,i+8}function checkIEEE754(s,o,i,a,u,_){if(i+a>s.length)throw new RangeError("Index out of range");if(i<0)throw new RangeError("Index out of range")}function writeFloat(s,o,i,a,_){return o=+o,i>>>=0,_||checkIEEE754(s,0,i,4),u.write(s,o,i,a,23,4),i+4}function writeDouble(s,o,i,a,_){return o=+o,i>>>=0,_||checkIEEE754(s,0,i,8),u.write(s,o,i,a,52,8),i+8}Buffer.prototype.slice=function slice(s,o){const i=this.length;(s=~~s)<0?(s+=i)<0&&(s=0):s>i&&(s=i),(o=void 0===o?i:~~o)<0?(o+=i)<0&&(o=0):o>i&&(o=i),o>>=0,o>>>=0,i||checkOffset(s,o,this.length);let a=this[s],u=1,_=0;for(;++_>>=0,o>>>=0,i||checkOffset(s,o,this.length);let a=this[s+--o],u=1;for(;o>0&&(u*=256);)a+=this[s+--o]*u;return a},Buffer.prototype.readUint8=Buffer.prototype.readUInt8=function readUInt8(s,o){return s>>>=0,o||checkOffset(s,1,this.length),this[s]},Buffer.prototype.readUint16LE=Buffer.prototype.readUInt16LE=function readUInt16LE(s,o){return s>>>=0,o||checkOffset(s,2,this.length),this[s]|this[s+1]<<8},Buffer.prototype.readUint16BE=Buffer.prototype.readUInt16BE=function readUInt16BE(s,o){return s>>>=0,o||checkOffset(s,2,this.length),this[s]<<8|this[s+1]},Buffer.prototype.readUint32LE=Buffer.prototype.readUInt32LE=function readUInt32LE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),(this[s]|this[s+1]<<8|this[s+2]<<16)+16777216*this[s+3]},Buffer.prototype.readUint32BE=Buffer.prototype.readUInt32BE=function readUInt32BE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),16777216*this[s]+(this[s+1]<<16|this[s+2]<<8|this[s+3])},Buffer.prototype.readBigUInt64LE=defineBigIntMethod((function readBigUInt64LE(s){validateNumber(s>>>=0,"offset");const o=this[s],i=this[s+7];void 0!==o&&void 0!==i||boundsError(s,this.length-8);const a=o+256*this[++s]+65536*this[++s]+this[++s]*2**24,u=this[++s]+256*this[++s]+65536*this[++s]+i*2**24;return BigInt(a)+(BigInt(u)<>>=0,"offset");const o=this[s],i=this[s+7];void 0!==o&&void 0!==i||boundsError(s,this.length-8);const a=o*2**24+65536*this[++s]+256*this[++s]+this[++s],u=this[++s]*2**24+65536*this[++s]+256*this[++s]+i;return(BigInt(a)<>>=0,o>>>=0,i||checkOffset(s,o,this.length);let a=this[s],u=1,_=0;for(;++_=u&&(a-=Math.pow(2,8*o)),a},Buffer.prototype.readIntBE=function readIntBE(s,o,i){s>>>=0,o>>>=0,i||checkOffset(s,o,this.length);let a=o,u=1,_=this[s+--a];for(;a>0&&(u*=256);)_+=this[s+--a]*u;return u*=128,_>=u&&(_-=Math.pow(2,8*o)),_},Buffer.prototype.readInt8=function readInt8(s,o){return s>>>=0,o||checkOffset(s,1,this.length),128&this[s]?-1*(255-this[s]+1):this[s]},Buffer.prototype.readInt16LE=function readInt16LE(s,o){s>>>=0,o||checkOffset(s,2,this.length);const i=this[s]|this[s+1]<<8;return 32768&i?4294901760|i:i},Buffer.prototype.readInt16BE=function readInt16BE(s,o){s>>>=0,o||checkOffset(s,2,this.length);const i=this[s+1]|this[s]<<8;return 32768&i?4294901760|i:i},Buffer.prototype.readInt32LE=function readInt32LE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),this[s]|this[s+1]<<8|this[s+2]<<16|this[s+3]<<24},Buffer.prototype.readInt32BE=function readInt32BE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),this[s]<<24|this[s+1]<<16|this[s+2]<<8|this[s+3]},Buffer.prototype.readBigInt64LE=defineBigIntMethod((function readBigInt64LE(s){validateNumber(s>>>=0,"offset");const o=this[s],i=this[s+7];void 0!==o&&void 0!==i||boundsError(s,this.length-8);const a=this[s+4]+256*this[s+5]+65536*this[s+6]+(i<<24);return(BigInt(a)<>>=0,"offset");const o=this[s],i=this[s+7];void 0!==o&&void 0!==i||boundsError(s,this.length-8);const a=(o<<24)+65536*this[++s]+256*this[++s]+this[++s];return(BigInt(a)<>>=0,o||checkOffset(s,4,this.length),u.read(this,s,!0,23,4)},Buffer.prototype.readFloatBE=function readFloatBE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),u.read(this,s,!1,23,4)},Buffer.prototype.readDoubleLE=function readDoubleLE(s,o){return s>>>=0,o||checkOffset(s,8,this.length),u.read(this,s,!0,52,8)},Buffer.prototype.readDoubleBE=function readDoubleBE(s,o){return s>>>=0,o||checkOffset(s,8,this.length),u.read(this,s,!1,52,8)},Buffer.prototype.writeUintLE=Buffer.prototype.writeUIntLE=function writeUIntLE(s,o,i,a){if(s=+s,o>>>=0,i>>>=0,!a){checkInt(this,s,o,i,Math.pow(2,8*i)-1,0)}let u=1,_=0;for(this[o]=255&s;++_>>=0,i>>>=0,!a){checkInt(this,s,o,i,Math.pow(2,8*i)-1,0)}let u=i-1,_=1;for(this[o+u]=255&s;--u>=0&&(_*=256);)this[o+u]=s/_&255;return o+i},Buffer.prototype.writeUint8=Buffer.prototype.writeUInt8=function writeUInt8(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,1,255,0),this[o]=255&s,o+1},Buffer.prototype.writeUint16LE=Buffer.prototype.writeUInt16LE=function writeUInt16LE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,2,65535,0),this[o]=255&s,this[o+1]=s>>>8,o+2},Buffer.prototype.writeUint16BE=Buffer.prototype.writeUInt16BE=function writeUInt16BE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,2,65535,0),this[o]=s>>>8,this[o+1]=255&s,o+2},Buffer.prototype.writeUint32LE=Buffer.prototype.writeUInt32LE=function writeUInt32LE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,4,4294967295,0),this[o+3]=s>>>24,this[o+2]=s>>>16,this[o+1]=s>>>8,this[o]=255&s,o+4},Buffer.prototype.writeUint32BE=Buffer.prototype.writeUInt32BE=function writeUInt32BE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,4,4294967295,0),this[o]=s>>>24,this[o+1]=s>>>16,this[o+2]=s>>>8,this[o+3]=255&s,o+4},Buffer.prototype.writeBigUInt64LE=defineBigIntMethod((function writeBigUInt64LE(s,o=0){return wrtBigUInt64LE(this,s,o,BigInt(0),BigInt("0xffffffffffffffff"))})),Buffer.prototype.writeBigUInt64BE=defineBigIntMethod((function writeBigUInt64BE(s,o=0){return wrtBigUInt64BE(this,s,o,BigInt(0),BigInt("0xffffffffffffffff"))})),Buffer.prototype.writeIntLE=function writeIntLE(s,o,i,a){if(s=+s,o>>>=0,!a){const a=Math.pow(2,8*i-1);checkInt(this,s,o,i,a-1,-a)}let u=0,_=1,w=0;for(this[o]=255&s;++u>>=0,!a){const a=Math.pow(2,8*i-1);checkInt(this,s,o,i,a-1,-a)}let u=i-1,_=1,w=0;for(this[o+u]=255&s;--u>=0&&(_*=256);)s<0&&0===w&&0!==this[o+u+1]&&(w=1),this[o+u]=(s/_|0)-w&255;return o+i},Buffer.prototype.writeInt8=function writeInt8(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,1,127,-128),s<0&&(s=255+s+1),this[o]=255&s,o+1},Buffer.prototype.writeInt16LE=function writeInt16LE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,2,32767,-32768),this[o]=255&s,this[o+1]=s>>>8,o+2},Buffer.prototype.writeInt16BE=function writeInt16BE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,2,32767,-32768),this[o]=s>>>8,this[o+1]=255&s,o+2},Buffer.prototype.writeInt32LE=function writeInt32LE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,4,2147483647,-2147483648),this[o]=255&s,this[o+1]=s>>>8,this[o+2]=s>>>16,this[o+3]=s>>>24,o+4},Buffer.prototype.writeInt32BE=function writeInt32BE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,4,2147483647,-2147483648),s<0&&(s=4294967295+s+1),this[o]=s>>>24,this[o+1]=s>>>16,this[o+2]=s>>>8,this[o+3]=255&s,o+4},Buffer.prototype.writeBigInt64LE=defineBigIntMethod((function writeBigInt64LE(s,o=0){return wrtBigUInt64LE(this,s,o,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))})),Buffer.prototype.writeBigInt64BE=defineBigIntMethod((function writeBigInt64BE(s,o=0){return wrtBigUInt64BE(this,s,o,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))})),Buffer.prototype.writeFloatLE=function writeFloatLE(s,o,i){return writeFloat(this,s,o,!0,i)},Buffer.prototype.writeFloatBE=function writeFloatBE(s,o,i){return writeFloat(this,s,o,!1,i)},Buffer.prototype.writeDoubleLE=function writeDoubleLE(s,o,i){return writeDouble(this,s,o,!0,i)},Buffer.prototype.writeDoubleBE=function writeDoubleBE(s,o,i){return writeDouble(this,s,o,!1,i)},Buffer.prototype.copy=function copy(s,o,i,a){if(!Buffer.isBuffer(s))throw new TypeError("argument should be a Buffer");if(i||(i=0),a||0===a||(a=this.length),o>=s.length&&(o=s.length),o||(o=0),a>0&&a=this.length)throw new RangeError("Index out of range");if(a<0)throw new RangeError("sourceEnd out of bounds");a>this.length&&(a=this.length),s.length-o>>=0,i=void 0===i?this.length:i>>>0,s||(s=0),"number"==typeof s)for(u=o;u=a+4;i-=3)o=`_${s.slice(i-3,i)}${o}`;return`${s.slice(0,i)}${o}`}function checkIntBI(s,o,i,a,u,_){if(s>i||s3?0===o||o===BigInt(0)?`>= 0${a} and < 2${a} ** ${8*(_+1)}${a}`:`>= -(2${a} ** ${8*(_+1)-1}${a}) and < 2 ** ${8*(_+1)-1}${a}`:`>= ${o}${a} and <= ${i}${a}`,new C.ERR_OUT_OF_RANGE("value",u,s)}!function checkBounds(s,o,i){validateNumber(o,"offset"),void 0!==s[o]&&void 0!==s[o+i]||boundsError(o,s.length-(i+1))}(a,u,_)}function validateNumber(s,o){if("number"!=typeof s)throw new C.ERR_INVALID_ARG_TYPE(o,"number",s)}function boundsError(s,o,i){if(Math.floor(s)!==s)throw validateNumber(s,i),new C.ERR_OUT_OF_RANGE(i||"offset","an integer",s);if(o<0)throw new C.ERR_BUFFER_OUT_OF_BOUNDS;throw new C.ERR_OUT_OF_RANGE(i||"offset",`>= ${i?1:0} and <= ${o}`,s)}E("ERR_BUFFER_OUT_OF_BOUNDS",(function(s){return s?`${s} is outside of buffer bounds`:"Attempt to access memory outside buffer bounds"}),RangeError),E("ERR_INVALID_ARG_TYPE",(function(s,o){return`The "${s}" argument must be of type number. Received type ${typeof o}`}),TypeError),E("ERR_OUT_OF_RANGE",(function(s,o,i){let a=`The value of "${s}" is out of range.`,u=i;return Number.isInteger(i)&&Math.abs(i)>2**32?u=addNumericalSeparator(String(i)):"bigint"==typeof i&&(u=String(i),(i>BigInt(2)**BigInt(32)||i<-(BigInt(2)**BigInt(32)))&&(u=addNumericalSeparator(u)),u+="n"),a+=` It must be ${o}. Received ${u}`,a}),RangeError);const j=/[^+/0-9A-Za-z-_]/g;function utf8ToBytes(s,o){let i;o=o||1/0;const a=s.length;let u=null;const _=[];for(let w=0;w55295&&i<57344){if(!u){if(i>56319){(o-=3)>-1&&_.push(239,191,189);continue}if(w+1===a){(o-=3)>-1&&_.push(239,191,189);continue}u=i;continue}if(i<56320){(o-=3)>-1&&_.push(239,191,189),u=i;continue}i=65536+(u-55296<<10|i-56320)}else u&&(o-=3)>-1&&_.push(239,191,189);if(u=null,i<128){if((o-=1)<0)break;_.push(i)}else if(i<2048){if((o-=2)<0)break;_.push(i>>6|192,63&i|128)}else if(i<65536){if((o-=3)<0)break;_.push(i>>12|224,i>>6&63|128,63&i|128)}else{if(!(i<1114112))throw new Error("Invalid code point");if((o-=4)<0)break;_.push(i>>18|240,i>>12&63|128,i>>6&63|128,63&i|128)}}return _}function base64ToBytes(s){return a.toByteArray(function base64clean(s){if((s=(s=s.split("=")[0]).trim().replace(j,"")).length<2)return"";for(;s.length%4!=0;)s+="=";return s}(s))}function blitBuffer(s,o,i,a){let u;for(u=0;u=o.length||u>=s.length);++u)o[u+i]=s[u];return u}function isInstance(s,o){return s instanceof o||null!=s&&null!=s.constructor&&null!=s.constructor.name&&s.constructor.name===o.name}function numberIsNaN(s){return s!=s}const L=function(){const s="0123456789abcdef",o=new Array(256);for(let i=0;i<16;++i){const a=16*i;for(let u=0;u<16;++u)o[a+u]=s[i]+s[u]}return o}();function defineBigIntMethod(s){return"undefined"==typeof BigInt?BufferBigIntNotDefined:s}function BufferBigIntNotDefined(){throw new Error("BigInt not supported")}},13144(s,o,i){"use strict";var a=i(66743),u=i(11002),_=i(10076),w=i(47119);s.exports=w||a.call(_,u)},12205(s,o,i){"use strict";var a=i(66743),u=i(11002),_=i(13144);s.exports=function applyBind(){return _(a,u,arguments)}},11002(s){"use strict";s.exports=Function.prototype.apply},10076(s){"use strict";s.exports=Function.prototype.call},73126(s,o,i){"use strict";var a=i(66743),u=i(69675),_=i(10076),w=i(13144);s.exports=function callBindBasic(s){if(s.length<1||"function"!=typeof s[0])throw new u("a function is required");return w(a,_,s)}},47119(s){"use strict";s.exports="undefined"!=typeof Reflect&&Reflect&&Reflect.apply},10487(s,o,i){"use strict";var a=i(96897),u=i(30655),_=i(73126),w=i(12205);s.exports=function callBind(s){var o=_(arguments),i=s.length-(arguments.length-1);return a(o,1+(i>0?i:0),!0)},u?u(s.exports,"apply",{value:w}):s.exports.apply=w},36556(s,o,i){"use strict";var a=i(70453),u=i(73126),_=u([a("%String.prototype.indexOf%")]);s.exports=function callBoundIntrinsic(s,o){var i=a(s,!!o);return"function"==typeof i&&_(s,".prototype.")>-1?u([i]):i}},17965(s,o,i){"use strict";var a=i(16426),u={"text/plain":"Text","text/html":"Url",default:"Text"};s.exports=function copy(s,o){var i,_,w,x,C,j,L=!1;o||(o={}),i=o.debug||!1;try{if(w=a(),x=document.createRange(),C=document.getSelection(),(j=document.createElement("span")).textContent=s,j.ariaHidden="true",j.style.all="unset",j.style.position="fixed",j.style.top=0,j.style.clip="rect(0, 0, 0, 0)",j.style.whiteSpace="pre",j.style.webkitUserSelect="text",j.style.MozUserSelect="text",j.style.msUserSelect="text",j.style.userSelect="text",j.addEventListener("copy",(function(a){if(a.stopPropagation(),o.format)if(a.preventDefault(),void 0===a.clipboardData){i&&console.warn("unable to use e.clipboardData"),i&&console.warn("trying IE specific stuff"),window.clipboardData.clearData();var _=u[o.format]||u.default;window.clipboardData.setData(_,s)}else a.clipboardData.clearData(),a.clipboardData.setData(o.format,s);o.onCopy&&(a.preventDefault(),o.onCopy(a.clipboardData))})),document.body.appendChild(j),x.selectNodeContents(j),C.addRange(x),!document.execCommand("copy"))throw new Error("copy command was unsuccessful");L=!0}catch(a){i&&console.error("unable to copy using execCommand: ",a),i&&console.warn("trying IE specific stuff");try{window.clipboardData.setData(o.format||"text",s),o.onCopy&&o.onCopy(window.clipboardData),L=!0}catch(a){i&&console.error("unable to copy using clipboardData: ",a),i&&console.error("falling back to prompt"),_=function format(s){var o=(/mac os x/i.test(navigator.userAgent)?"⌘":"Ctrl")+"+C";return s.replace(/#{\s*key\s*}/g,o)}("message"in o?o.message:"Copy to clipboard: #{key}, Enter"),window.prompt(_,s)}}finally{C&&("function"==typeof C.removeRange?C.removeRange(x):C.removeAllRanges()),j&&document.body.removeChild(j),w()}return L}},2205(s,o,i){var a;a=void 0!==i.g?i.g:this,s.exports=function(s){if(s.CSS&&s.CSS.escape)return s.CSS.escape;var cssEscape=function(s){if(0==arguments.length)throw new TypeError("`CSS.escape` requires an argument.");for(var o,i=String(s),a=i.length,u=-1,_="",w=i.charCodeAt(0);++u=1&&o<=31||127==o||0==u&&o>=48&&o<=57||1==u&&o>=48&&o<=57&&45==w?"\\"+o.toString(16)+" ":0==u&&1==a&&45==o||!(o>=128||45==o||95==o||o>=48&&o<=57||o>=65&&o<=90||o>=97&&o<=122)?"\\"+i.charAt(u):i.charAt(u):_+="�";return _};return s.CSS||(s.CSS={}),s.CSS.escape=cssEscape,cssEscape}(a)},81919(s,o,i){"use strict";var a=i(48287).Buffer;function isSpecificValue(s){return s instanceof a||s instanceof Date||s instanceof RegExp}function cloneSpecificValue(s){if(s instanceof a){var o=a.alloc?a.alloc(s.length):new a(s.length);return s.copy(o),o}if(s instanceof Date)return new Date(s.getTime());if(s instanceof RegExp)return new RegExp(s);throw new Error("Unexpected situation")}function deepCloneArray(s){var o=[];return s.forEach((function(s,i){"object"==typeof s&&null!==s?Array.isArray(s)?o[i]=deepCloneArray(s):isSpecificValue(s)?o[i]=cloneSpecificValue(s):o[i]=u({},s):o[i]=s})),o}function safeGetProperty(s,o){return"__proto__"===o?void 0:s[o]}var u=s.exports=function(){if(arguments.length<1||"object"!=typeof arguments[0])return!1;if(arguments.length<2)return arguments[0];var s,o,i=arguments[0];return Array.prototype.slice.call(arguments,1).forEach((function(a){"object"!=typeof a||null===a||Array.isArray(a)||Object.keys(a).forEach((function(_){return o=safeGetProperty(i,_),(s=safeGetProperty(a,_))===i?void 0:"object"!=typeof s||null===s?void(i[_]=s):Array.isArray(s)?void(i[_]=deepCloneArray(s)):isSpecificValue(s)?void(i[_]=cloneSpecificValue(s)):"object"!=typeof o||null===o||Array.isArray(o)?void(i[_]=u({},s)):void(i[_]=u(o,s))}))})),i}},14744(s){"use strict";var o=function isMergeableObject(s){return function isNonNullObject(s){return!!s&&"object"==typeof s}(s)&&!function isSpecial(s){var o=Object.prototype.toString.call(s);return"[object RegExp]"===o||"[object Date]"===o||function isReactElement(s){return s.$$typeof===i}(s)}(s)};var i="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function cloneUnlessOtherwiseSpecified(s,o){return!1!==o.clone&&o.isMergeableObject(s)?deepmerge(function emptyTarget(s){return Array.isArray(s)?[]:{}}(s),s,o):s}function defaultArrayMerge(s,o,i){return s.concat(o).map((function(s){return cloneUnlessOtherwiseSpecified(s,i)}))}function getKeys(s){return Object.keys(s).concat(function getEnumerableOwnPropertySymbols(s){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(s).filter((function(o){return Object.propertyIsEnumerable.call(s,o)})):[]}(s))}function propertyIsOnObject(s,o){try{return o in s}catch(s){return!1}}function mergeObject(s,o,i){var a={};return i.isMergeableObject(s)&&getKeys(s).forEach((function(o){a[o]=cloneUnlessOtherwiseSpecified(s[o],i)})),getKeys(o).forEach((function(u){(function propertyIsUnsafe(s,o){return propertyIsOnObject(s,o)&&!(Object.hasOwnProperty.call(s,o)&&Object.propertyIsEnumerable.call(s,o))})(s,u)||(propertyIsOnObject(s,u)&&i.isMergeableObject(o[u])?a[u]=function getMergeFunction(s,o){if(!o.customMerge)return deepmerge;var i=o.customMerge(s);return"function"==typeof i?i:deepmerge}(u,i)(s[u],o[u],i):a[u]=cloneUnlessOtherwiseSpecified(o[u],i))})),a}function deepmerge(s,i,a){(a=a||{}).arrayMerge=a.arrayMerge||defaultArrayMerge,a.isMergeableObject=a.isMergeableObject||o,a.cloneUnlessOtherwiseSpecified=cloneUnlessOtherwiseSpecified;var u=Array.isArray(i);return u===Array.isArray(s)?u?a.arrayMerge(s,i,a):mergeObject(s,i,a):cloneUnlessOtherwiseSpecified(i,a)}deepmerge.all=function deepmergeAll(s,o){if(!Array.isArray(s))throw new Error("first argument should be an array");return s.reduce((function(s,i){return deepmerge(s,i,o)}),{})};var a=deepmerge;s.exports=a},30041(s,o,i){"use strict";var a=i(30655),u=i(58068),_=i(69675),w=i(75795);s.exports=function defineDataProperty(s,o,i){if(!s||"object"!=typeof s&&"function"!=typeof s)throw new _("`obj` must be an object or a function`");if("string"!=typeof o&&"symbol"!=typeof o)throw new _("`property` must be a string or a symbol`");if(arguments.length>3&&"boolean"!=typeof arguments[3]&&null!==arguments[3])throw new _("`nonEnumerable`, if provided, must be a boolean or null");if(arguments.length>4&&"boolean"!=typeof arguments[4]&&null!==arguments[4])throw new _("`nonWritable`, if provided, must be a boolean or null");if(arguments.length>5&&"boolean"!=typeof arguments[5]&&null!==arguments[5])throw new _("`nonConfigurable`, if provided, must be a boolean or null");if(arguments.length>6&&"boolean"!=typeof arguments[6])throw new _("`loose`, if provided, must be a boolean");var x=arguments.length>3?arguments[3]:null,C=arguments.length>4?arguments[4]:null,j=arguments.length>5?arguments[5]:null,L=arguments.length>6&&arguments[6],B=!!w&&w(s,o);if(a)a(s,o,{configurable:null===j&&B?B.configurable:!j,enumerable:null===x&&B?B.enumerable:!x,value:i,writable:null===C&&B?B.writable:!C});else{if(!L&&(x||C||j))throw new u("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.");s[o]=i}}},78004(s){"use strict";class SubRange{constructor(s,o){this.low=s,this.high=o,this.length=1+o-s}overlaps(s){return!(this.highs.high)}touches(s){return!(this.high+1s.high)}add(s){return new SubRange(Math.min(this.low,s.low),Math.max(this.high,s.high))}subtract(s){return s.low<=this.low&&s.high>=this.high?[]:s.low>this.low&&s.highs+o.length),0)}add(s,o){var _add=s=>{for(var o=0;o{for(var o=0;o{for(var o=0;o{for(var i=o.low;i<=o.high;)s.push(i),i++;return s}),[])}subranges(){return this.ranges.map((s=>({low:s.low,high:s.high,length:1+s.high-s.low})))}}s.exports=DRange},7176(s,o,i){"use strict";var a,u=i(73126),_=i(75795);try{a=[].__proto__===Array.prototype}catch(s){if(!s||"object"!=typeof s||!("code"in s)||"ERR_PROTO_ACCESS"!==s.code)throw s}var w=!!a&&_&&_(Object.prototype,"__proto__"),x=Object,C=x.getPrototypeOf;s.exports=w&&"function"==typeof w.get?u([w.get]):"function"==typeof C&&function getDunder(s){return C(null==s?s:x(s))}},30655(s){"use strict";var o=Object.defineProperty||!1;if(o)try{o({},"a",{value:1})}catch(s){o=!1}s.exports=o},41237(s){"use strict";s.exports=EvalError},69383(s){"use strict";s.exports=Error},79290(s){"use strict";s.exports=RangeError},79538(s){"use strict";s.exports=ReferenceError},58068(s){"use strict";s.exports=SyntaxError},69675(s){"use strict";s.exports=TypeError},35345(s){"use strict";s.exports=URIError},79612(s){"use strict";s.exports=Object},37007(s){"use strict";var o,i="object"==typeof Reflect?Reflect:null,a=i&&"function"==typeof i.apply?i.apply:function ReflectApply(s,o,i){return Function.prototype.apply.call(s,o,i)};o=i&&"function"==typeof i.ownKeys?i.ownKeys:Object.getOwnPropertySymbols?function ReflectOwnKeys(s){return Object.getOwnPropertyNames(s).concat(Object.getOwnPropertySymbols(s))}:function ReflectOwnKeys(s){return Object.getOwnPropertyNames(s)};var u=Number.isNaN||function NumberIsNaN(s){return s!=s};function EventEmitter(){EventEmitter.init.call(this)}s.exports=EventEmitter,s.exports.once=function once(s,o){return new Promise((function(i,a){function errorListener(i){s.removeListener(o,resolver),a(i)}function resolver(){"function"==typeof s.removeListener&&s.removeListener("error",errorListener),i([].slice.call(arguments))}eventTargetAgnosticAddListener(s,o,resolver,{once:!0}),"error"!==o&&function addErrorHandlerIfEventEmitter(s,o,i){"function"==typeof s.on&&eventTargetAgnosticAddListener(s,"error",o,i)}(s,errorListener,{once:!0})}))},EventEmitter.EventEmitter=EventEmitter,EventEmitter.prototype._events=void 0,EventEmitter.prototype._eventsCount=0,EventEmitter.prototype._maxListeners=void 0;var _=10;function checkListener(s){if("function"!=typeof s)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof s)}function _getMaxListeners(s){return void 0===s._maxListeners?EventEmitter.defaultMaxListeners:s._maxListeners}function _addListener(s,o,i,a){var u,_,w;if(checkListener(i),void 0===(_=s._events)?(_=s._events=Object.create(null),s._eventsCount=0):(void 0!==_.newListener&&(s.emit("newListener",o,i.listener?i.listener:i),_=s._events),w=_[o]),void 0===w)w=_[o]=i,++s._eventsCount;else if("function"==typeof w?w=_[o]=a?[i,w]:[w,i]:a?w.unshift(i):w.push(i),(u=_getMaxListeners(s))>0&&w.length>u&&!w.warned){w.warned=!0;var x=new Error("Possible EventEmitter memory leak detected. "+w.length+" "+String(o)+" listeners added. Use emitter.setMaxListeners() to increase limit");x.name="MaxListenersExceededWarning",x.emitter=s,x.type=o,x.count=w.length,function ProcessEmitWarning(s){console&&console.warn&&console.warn(s)}(x)}return s}function onceWrapper(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function _onceWrap(s,o,i){var a={fired:!1,wrapFn:void 0,target:s,type:o,listener:i},u=onceWrapper.bind(a);return u.listener=i,a.wrapFn=u,u}function _listeners(s,o,i){var a=s._events;if(void 0===a)return[];var u=a[o];return void 0===u?[]:"function"==typeof u?i?[u.listener||u]:[u]:i?function unwrapListeners(s){for(var o=new Array(s.length),i=0;i0&&(w=o[0]),w instanceof Error)throw w;var x=new Error("Unhandled error."+(w?" ("+w.message+")":""));throw x.context=w,x}var C=_[s];if(void 0===C)return!1;if("function"==typeof C)a(C,this,o);else{var j=C.length,L=arrayClone(C,j);for(i=0;i=0;_--)if(i[_]===o||i[_].listener===o){w=i[_].listener,u=_;break}if(u<0)return this;0===u?i.shift():function spliceOne(s,o){for(;o+1=0;a--)this.removeListener(s,o[a]);return this},EventEmitter.prototype.listeners=function listeners(s){return _listeners(this,s,!0)},EventEmitter.prototype.rawListeners=function rawListeners(s){return _listeners(this,s,!1)},EventEmitter.listenerCount=function(s,o){return"function"==typeof s.listenerCount?s.listenerCount(o):listenerCount.call(s,o)},EventEmitter.prototype.listenerCount=listenerCount,EventEmitter.prototype.eventNames=function eventNames(){return this._eventsCount>0?o(this._events):[]}},85587(s,o,i){"use strict";var a=i(26311),u=create(Error);function create(s){return FormattedError.displayName=s.displayName||s.name,FormattedError;function FormattedError(o){return o&&(o=a.apply(null,arguments)),new s(o)}}s.exports=u,u.eval=create(EvalError),u.range=create(RangeError),u.reference=create(ReferenceError),u.syntax=create(SyntaxError),u.type=create(TypeError),u.uri=create(URIError),u.create=create},82682(s,o,i){"use strict";var a=i(69600),u=Object.prototype.toString,_=Object.prototype.hasOwnProperty;s.exports=function forEach(s,o,i){if(!a(o))throw new TypeError("iterator must be a function");var w;arguments.length>=3&&(w=i),function isArray(s){return"[object Array]"===u.call(s)}(s)?function forEachArray(s,o,i){for(var a=0,u=s.length;a0?parseInt(i):null};x1&&"boolean"!=typeof o)throw new L('"allowMissing" argument must be a boolean');if(null===Xe(/^%?[^%]*%?$/,s))throw new j("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var i=function stringToPath(s){var o=Ye(s,0,1),i=Ye(s,-1);if("%"===o&&"%"!==i)throw new j("invalid intrinsic syntax, expected closing `%`");if("%"===i&&"%"!==o)throw new j("invalid intrinsic syntax, expected opening `%`");var a=[];return He(s,Qe,(function(s,o,i,u){a[a.length]=i?He(u,et,"$1"):o||s})),a}(s),a=i.length>0?i[0]:"",u=tt("%"+a+"%",o),_=u.name,w=u.value,x=!1,C=u.alias;C&&(a=C[0],We(i,ze([0,1],C)));for(var B=1,$=!0;B=i.length){var Y=ae(w,z);w=($=!!Y)&&"get"in Y&&!("originalValue"in Y.get)?Y.get:w[z]}else $=$e(w,z),w=w[z];$&&!x&&(xe[_]=w)}}return w}},71064(s,o,i){"use strict";var a=i(79612);s.exports=a.getPrototypeOf||null},48648(s){"use strict";s.exports="undefined"!=typeof Reflect&&Reflect.getPrototypeOf||null},93628(s,o,i){"use strict";var a=i(48648),u=i(71064),_=i(7176);s.exports=a?function getProto(s){return a(s)}:u?function getProto(s){if(!s||"object"!=typeof s&&"function"!=typeof s)throw new TypeError("getProto: not an object");return u(s)}:_?function getProto(s){return _(s)}:null},6549(s){"use strict";s.exports=Object.getOwnPropertyDescriptor},75795(s,o,i){"use strict";var a=i(6549);if(a)try{a([],"length")}catch(s){a=null}s.exports=a},30592(s,o,i){"use strict";var a=i(30655),u=function hasPropertyDescriptors(){return!!a};u.hasArrayLengthDefineBug=function hasArrayLengthDefineBug(){if(!a)return null;try{return 1!==a([],"length",{value:1}).length}catch(s){return!0}},s.exports=u},64039(s,o,i){"use strict";var a="undefined"!=typeof Symbol&&Symbol,u=i(41333);s.exports=function hasNativeSymbols(){return"function"==typeof a&&("function"==typeof Symbol&&("symbol"==typeof a("foo")&&("symbol"==typeof Symbol("bar")&&u())))}},41333(s){"use strict";s.exports=function hasSymbols(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var s={},o=Symbol("test"),i=Object(o);if("string"==typeof o)return!1;if("[object Symbol]"!==Object.prototype.toString.call(o))return!1;if("[object Symbol]"!==Object.prototype.toString.call(i))return!1;for(var a in s[o]=42,s)return!1;if("function"==typeof Object.keys&&0!==Object.keys(s).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(s).length)return!1;var u=Object.getOwnPropertySymbols(s);if(1!==u.length||u[0]!==o)return!1;if(!Object.prototype.propertyIsEnumerable.call(s,o))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var _=Object.getOwnPropertyDescriptor(s,o);if(42!==_.value||!0!==_.enumerable)return!1}return!0}},49092(s,o,i){"use strict";var a=i(41333);s.exports=function hasToStringTagShams(){return a()&&!!Symbol.toStringTag}},9957(s,o,i){"use strict";var a=Function.prototype.call,u=Object.prototype.hasOwnProperty,_=i(66743);s.exports=_.call(a,u)},45981(s){function deepFreeze(s){return s instanceof Map?s.clear=s.delete=s.set=function(){throw new Error("map is read-only")}:s instanceof Set&&(s.add=s.clear=s.delete=function(){throw new Error("set is read-only")}),Object.freeze(s),Object.getOwnPropertyNames(s).forEach((function(o){var i=s[o];"object"!=typeof i||Object.isFrozen(i)||deepFreeze(i)})),s}var o=deepFreeze,i=deepFreeze;o.default=i;class Response{constructor(s){void 0===s.data&&(s.data={}),this.data=s.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}}function escapeHTML(s){return s.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function inherit(s,...o){const i=Object.create(null);for(const o in s)i[o]=s[o];return o.forEach((function(s){for(const o in s)i[o]=s[o]})),i}const emitsWrappingTags=s=>!!s.kind;class HTMLRenderer{constructor(s,o){this.buffer="",this.classPrefix=o.classPrefix,s.walk(this)}addText(s){this.buffer+=escapeHTML(s)}openNode(s){if(!emitsWrappingTags(s))return;let o=s.kind;s.sublanguage||(o=`${this.classPrefix}${o}`),this.span(o)}closeNode(s){emitsWrappingTags(s)&&(this.buffer+="")}value(){return this.buffer}span(s){this.buffer+=``}}class TokenTree{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(s){this.top.children.push(s)}openNode(s){const o={kind:s,children:[]};this.add(o),this.stack.push(o)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(s){return this.constructor._walk(s,this.rootNode)}static _walk(s,o){return"string"==typeof o?s.addText(o):o.children&&(s.openNode(o),o.children.forEach((o=>this._walk(s,o))),s.closeNode(o)),s}static _collapse(s){"string"!=typeof s&&s.children&&(s.children.every((s=>"string"==typeof s))?s.children=[s.children.join("")]:s.children.forEach((s=>{TokenTree._collapse(s)})))}}class TokenTreeEmitter extends TokenTree{constructor(s){super(),this.options=s}addKeyword(s,o){""!==s&&(this.openNode(o),this.addText(s),this.closeNode())}addText(s){""!==s&&this.add(s)}addSublanguage(s,o){const i=s.root;i.kind=o,i.sublanguage=!0,this.add(i)}toHTML(){return new HTMLRenderer(this,this.options).value()}finalize(){return!0}}function source(s){return s?"string"==typeof s?s:s.source:null}const a=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;const u="[a-zA-Z]\\w*",_="[a-zA-Z_]\\w*",w="\\b\\d+(\\.\\d+)?",x="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",C="\\b(0b[01]+)",j={begin:"\\\\[\\s\\S]",relevance:0},L={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[j]},B={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[j]},$={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},COMMENT=function(s,o,i={}){const a=inherit({className:"comment",begin:s,end:o,contains:[]},i);return a.contains.push($),a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),a},z=COMMENT("//","$"),U=COMMENT("/\\*","\\*/"),V=COMMENT("#","$"),Y={className:"number",begin:w,relevance:0},Z={className:"number",begin:x,relevance:0},ee={className:"number",begin:C,relevance:0},ie={className:"number",begin:w+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},ae={begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[j,{begin:/\[/,end:/\]/,relevance:0,contains:[j]}]}]},ce={className:"title",begin:u,relevance:0},le={className:"title",begin:_,relevance:0},pe={begin:"\\.\\s*"+_,relevance:0};var de=Object.freeze({__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:u,UNDERSCORE_IDENT_RE:_,NUMBER_RE:w,C_NUMBER_RE:x,BINARY_NUMBER_RE:C,RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(s={})=>{const o=/^#![ ]*\//;return s.binary&&(s.begin=function concat(...s){return s.map((s=>source(s))).join("")}(o,/.*\b/,s.binary,/\b.*/)),inherit({className:"meta",begin:o,end:/$/,relevance:0,"on:begin":(s,o)=>{0!==s.index&&o.ignoreMatch()}},s)},BACKSLASH_ESCAPE:j,APOS_STRING_MODE:L,QUOTE_STRING_MODE:B,PHRASAL_WORDS_MODE:$,COMMENT,C_LINE_COMMENT_MODE:z,C_BLOCK_COMMENT_MODE:U,HASH_COMMENT_MODE:V,NUMBER_MODE:Y,C_NUMBER_MODE:Z,BINARY_NUMBER_MODE:ee,CSS_NUMBER_MODE:ie,REGEXP_MODE:ae,TITLE_MODE:ce,UNDERSCORE_TITLE_MODE:le,METHOD_GUARD:pe,END_SAME_AS_BEGIN:function(s){return Object.assign(s,{"on:begin":(s,o)=>{o.data._beginMatch=s[1]},"on:end":(s,o)=>{o.data._beginMatch!==s[1]&&o.ignoreMatch()}})}});function skipIfhasPrecedingDot(s,o){"."===s.input[s.index-1]&&o.ignoreMatch()}function beginKeywords(s,o){o&&s.beginKeywords&&(s.begin="\\b("+s.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",s.__beforeBegin=skipIfhasPrecedingDot,s.keywords=s.keywords||s.beginKeywords,delete s.beginKeywords,void 0===s.relevance&&(s.relevance=0))}function compileIllegal(s,o){Array.isArray(s.illegal)&&(s.illegal=function either(...s){return"("+s.map((s=>source(s))).join("|")+")"}(...s.illegal))}function compileMatch(s,o){if(s.match){if(s.begin||s.end)throw new Error("begin & end are not supported with match");s.begin=s.match,delete s.match}}function compileRelevance(s,o){void 0===s.relevance&&(s.relevance=1)}const fe=["of","and","for","in","not","or","if","then","parent","list","value"];function compileKeywords(s,o,i="keyword"){const a={};return"string"==typeof s?compileList(i,s.split(" ")):Array.isArray(s)?compileList(i,s):Object.keys(s).forEach((function(i){Object.assign(a,compileKeywords(s[i],o,i))})),a;function compileList(s,i){o&&(i=i.map((s=>s.toLowerCase()))),i.forEach((function(o){const i=o.split("|");a[i[0]]=[s,scoreForKeyword(i[0],i[1])]}))}}function scoreForKeyword(s,o){return o?Number(o):function commonKeyword(s){return fe.includes(s.toLowerCase())}(s)?0:1}function compileLanguage(s,{plugins:o}){function langRe(o,i){return new RegExp(source(o),"m"+(s.case_insensitive?"i":"")+(i?"g":""))}class MultiRegex{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(s,o){o.position=this.position++,this.matchIndexes[this.matchAt]=o,this.regexes.push([o,s]),this.matchAt+=function countMatchGroups(s){return new RegExp(s.toString()+"|").exec("").length-1}(s)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const s=this.regexes.map((s=>s[1]));this.matcherRe=langRe(function join(s,o="|"){let i=0;return s.map((s=>{i+=1;const o=i;let u=source(s),_="";for(;u.length>0;){const s=a.exec(u);if(!s){_+=u;break}_+=u.substring(0,s.index),u=u.substring(s.index+s[0].length),"\\"===s[0][0]&&s[1]?_+="\\"+String(Number(s[1])+o):(_+=s[0],"("===s[0]&&i++)}return _})).map((s=>`(${s})`)).join(o)}(s),!0),this.lastIndex=0}exec(s){this.matcherRe.lastIndex=this.lastIndex;const o=this.matcherRe.exec(s);if(!o)return null;const i=o.findIndex(((s,o)=>o>0&&void 0!==s)),a=this.matchIndexes[i];return o.splice(0,i),Object.assign(o,a)}}class ResumableMultiRegex{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(s){if(this.multiRegexes[s])return this.multiRegexes[s];const o=new MultiRegex;return this.rules.slice(s).forEach((([s,i])=>o.addRule(s,i))),o.compile(),this.multiRegexes[s]=o,o}resumingScanAtSamePosition(){return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(s,o){this.rules.push([s,o]),"begin"===o.type&&this.count++}exec(s){const o=this.getMatcher(this.regexIndex);o.lastIndex=this.lastIndex;let i=o.exec(s);if(this.resumingScanAtSamePosition())if(i&&i.index===this.lastIndex);else{const o=this.getMatcher(0);o.lastIndex=this.lastIndex+1,i=o.exec(s)}return i&&(this.regexIndex+=i.position+1,this.regexIndex===this.count&&this.considerAll()),i}}if(s.compilerExtensions||(s.compilerExtensions=[]),s.contains&&s.contains.includes("self"))throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return s.classNameAliases=inherit(s.classNameAliases||{}),function compileMode(o,i){const a=o;if(o.isCompiled)return a;[compileMatch].forEach((s=>s(o,i))),s.compilerExtensions.forEach((s=>s(o,i))),o.__beforeBegin=null,[beginKeywords,compileIllegal,compileRelevance].forEach((s=>s(o,i))),o.isCompiled=!0;let u=null;if("object"==typeof o.keywords&&(u=o.keywords.$pattern,delete o.keywords.$pattern),o.keywords&&(o.keywords=compileKeywords(o.keywords,s.case_insensitive)),o.lexemes&&u)throw new Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");return u=u||o.lexemes||/\w+/,a.keywordPatternRe=langRe(u,!0),i&&(o.begin||(o.begin=/\B|\b/),a.beginRe=langRe(o.begin),o.endSameAsBegin&&(o.end=o.begin),o.end||o.endsWithParent||(o.end=/\B|\b/),o.end&&(a.endRe=langRe(o.end)),a.terminatorEnd=source(o.end)||"",o.endsWithParent&&i.terminatorEnd&&(a.terminatorEnd+=(o.end?"|":"")+i.terminatorEnd)),o.illegal&&(a.illegalRe=langRe(o.illegal)),o.contains||(o.contains=[]),o.contains=[].concat(...o.contains.map((function(s){return function expandOrCloneMode(s){s.variants&&!s.cachedVariants&&(s.cachedVariants=s.variants.map((function(o){return inherit(s,{variants:null},o)})));if(s.cachedVariants)return s.cachedVariants;if(dependencyOnParent(s))return inherit(s,{starts:s.starts?inherit(s.starts):null});if(Object.isFrozen(s))return inherit(s);return s}("self"===s?o:s)}))),o.contains.forEach((function(s){compileMode(s,a)})),o.starts&&compileMode(o.starts,i),a.matcher=function buildModeRegex(s){const o=new ResumableMultiRegex;return s.contains.forEach((s=>o.addRule(s.begin,{rule:s,type:"begin"}))),s.terminatorEnd&&o.addRule(s.terminatorEnd,{type:"end"}),s.illegal&&o.addRule(s.illegal,{type:"illegal"}),o}(a),a}(s)}function dependencyOnParent(s){return!!s&&(s.endsWithParent||dependencyOnParent(s.starts))}function BuildVuePlugin(s){const o={props:["language","code","autodetect"],data:function(){return{detectedLanguage:"",unknownLanguage:!1}},computed:{className(){return this.unknownLanguage?"":"hljs "+this.detectedLanguage},highlighted(){if(!this.autoDetect&&!s.getLanguage(this.language))return console.warn(`The language "${this.language}" you specified could not be found.`),this.unknownLanguage=!0,escapeHTML(this.code);let o={};return this.autoDetect?(o=s.highlightAuto(this.code),this.detectedLanguage=o.language):(o=s.highlight(this.language,this.code,this.ignoreIllegals),this.detectedLanguage=this.language),o.value},autoDetect(){return!this.language||function hasValueOrEmptyAttribute(s){return Boolean(s||""===s)}(this.autodetect)},ignoreIllegals:()=>!0},render(s){return s("pre",{},[s("code",{class:this.className,domProps:{innerHTML:this.highlighted}})])}};return{Component:o,VuePlugin:{install(s){s.component("highlightjs",o)}}}}const ye={"after:highlightElement":({el:s,result:o,text:i})=>{const a=nodeStream(s);if(!a.length)return;const u=document.createElement("div");u.innerHTML=o.value,o.value=function mergeStreams(s,o,i){let a=0,u="";const _=[];function selectStream(){return s.length&&o.length?s[0].offset!==o[0].offset?s[0].offset"}function close(s){u+=""}function render(s){("start"===s.event?open:close)(s.node)}for(;s.length||o.length;){let o=selectStream();if(u+=escapeHTML(i.substring(a,o[0].offset)),a=o[0].offset,o===s){_.reverse().forEach(close);do{render(o.splice(0,1)[0]),o=selectStream()}while(o===s&&o.length&&o[0].offset===a);_.reverse().forEach(open)}else"start"===o[0].event?_.push(o[0].node):_.pop(),render(o.splice(0,1)[0])}return u+escapeHTML(i.substr(a))}(a,nodeStream(u),i)}};function tag(s){return s.nodeName.toLowerCase()}function nodeStream(s){const o=[];return function _nodeStream(s,i){for(let a=s.firstChild;a;a=a.nextSibling)3===a.nodeType?i+=a.nodeValue.length:1===a.nodeType&&(o.push({event:"start",offset:i,node:a}),i=_nodeStream(a,i),tag(a).match(/br|hr|img|input/)||o.push({event:"stop",offset:i,node:a}));return i}(s,0),o}const be={},error=s=>{console.error(s)},warn=(s,...o)=>{console.log(`WARN: ${s}`,...o)},deprecated=(s,o)=>{be[`${s}/${o}`]||(console.log(`Deprecated as of ${s}. ${o}`),be[`${s}/${o}`]=!0)},Se=escapeHTML,_e=inherit,we=Symbol("nomatch");var xe=function(s){const i=Object.create(null),a=Object.create(null),u=[];let _=!0;const w=/(^(<[^>]+>|\t|)+|\n)/gm,x="Could not find the language '{}', did you forget to load/include a language module?",C={disableAutodetect:!0,name:"Plain text",contains:[]};let j={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:null,__emitter:TokenTreeEmitter};function shouldNotHighlight(s){return j.noHighlightRe.test(s)}function highlight(s,o,i,a){let u="",_="";"object"==typeof o?(u=s,i=o.ignoreIllegals,_=o.language,a=void 0):(deprecated("10.7.0","highlight(lang, code, ...args) has been deprecated."),deprecated("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),_=s,u=o);const w={code:u,language:_};fire("before:highlight",w);const x=w.result?w.result:_highlight(w.language,w.code,i,a);return x.code=w.code,fire("after:highlight",x),x}function _highlight(s,o,a,w){function keywordData(s,o){const i=L.case_insensitive?o[0].toLowerCase():o[0];return Object.prototype.hasOwnProperty.call(s.keywords,i)&&s.keywords[i]}function processBuffer(){null!=z.subLanguage?function processSubLanguage(){if(""===Y)return;let s=null;if("string"==typeof z.subLanguage){if(!i[z.subLanguage])return void V.addText(Y);s=_highlight(z.subLanguage,Y,!0,U[z.subLanguage]),U[z.subLanguage]=s.top}else s=highlightAuto(Y,z.subLanguage.length?z.subLanguage:null);z.relevance>0&&(Z+=s.relevance),V.addSublanguage(s.emitter,s.language)}():function processKeywords(){if(!z.keywords)return void V.addText(Y);let s=0;z.keywordPatternRe.lastIndex=0;let o=z.keywordPatternRe.exec(Y),i="";for(;o;){i+=Y.substring(s,o.index);const a=keywordData(z,o);if(a){const[s,u]=a;if(V.addText(i),i="",Z+=u,s.startsWith("_"))i+=o[0];else{const i=L.classNameAliases[s]||s;V.addKeyword(o[0],i)}}else i+=o[0];s=z.keywordPatternRe.lastIndex,o=z.keywordPatternRe.exec(Y)}i+=Y.substr(s),V.addText(i)}(),Y=""}function startNewMode(s){return s.className&&V.openNode(L.classNameAliases[s.className]||s.className),z=Object.create(s,{parent:{value:z}}),z}function endOfMode(s,o,i){let a=function startsWith(s,o){const i=s&&s.exec(o);return i&&0===i.index}(s.endRe,i);if(a){if(s["on:end"]){const i=new Response(s);s["on:end"](o,i),i.isMatchIgnored&&(a=!1)}if(a){for(;s.endsParent&&s.parent;)s=s.parent;return s}}if(s.endsWithParent)return endOfMode(s.parent,o,i)}function doIgnore(s){return 0===z.matcher.regexIndex?(Y+=s[0],1):(ae=!0,0)}function doBeginMatch(s){const o=s[0],i=s.rule,a=new Response(i),u=[i.__beforeBegin,i["on:begin"]];for(const i of u)if(i&&(i(s,a),a.isMatchIgnored))return doIgnore(o);return i&&i.endSameAsBegin&&(i.endRe=function escape(s){return new RegExp(s.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")}(o)),i.skip?Y+=o:(i.excludeBegin&&(Y+=o),processBuffer(),i.returnBegin||i.excludeBegin||(Y=o)),startNewMode(i),i.returnBegin?0:o.length}function doEndMatch(s){const i=s[0],a=o.substr(s.index),u=endOfMode(z,s,a);if(!u)return we;const _=z;_.skip?Y+=i:(_.returnEnd||_.excludeEnd||(Y+=i),processBuffer(),_.excludeEnd&&(Y=i));do{z.className&&V.closeNode(),z.skip||z.subLanguage||(Z+=z.relevance),z=z.parent}while(z!==u.parent);return u.starts&&(u.endSameAsBegin&&(u.starts.endRe=u.endRe),startNewMode(u.starts)),_.returnEnd?0:i.length}let C={};function processLexeme(i,u){const w=u&&u[0];if(Y+=i,null==w)return processBuffer(),0;if("begin"===C.type&&"end"===u.type&&C.index===u.index&&""===w){if(Y+=o.slice(u.index,u.index+1),!_){const o=new Error("0 width match regex");throw o.languageName=s,o.badRule=C.rule,o}return 1}if(C=u,"begin"===u.type)return doBeginMatch(u);if("illegal"===u.type&&!a){const s=new Error('Illegal lexeme "'+w+'" for mode "'+(z.className||"")+'"');throw s.mode=z,s}if("end"===u.type){const s=doEndMatch(u);if(s!==we)return s}if("illegal"===u.type&&""===w)return 1;if(ie>1e5&&ie>3*u.index){throw new Error("potential infinite loop, way more iterations than matches")}return Y+=w,w.length}const L=getLanguage(s);if(!L)throw error(x.replace("{}",s)),new Error('Unknown language: "'+s+'"');const B=compileLanguage(L,{plugins:u});let $="",z=w||B;const U={},V=new j.__emitter(j);!function processContinuations(){const s=[];for(let o=z;o!==L;o=o.parent)o.className&&s.unshift(o.className);s.forEach((s=>V.openNode(s)))}();let Y="",Z=0,ee=0,ie=0,ae=!1;try{for(z.matcher.considerAll();;){ie++,ae?ae=!1:z.matcher.considerAll(),z.matcher.lastIndex=ee;const s=z.matcher.exec(o);if(!s)break;const i=processLexeme(o.substring(ee,s.index),s);ee=s.index+i}return processLexeme(o.substr(ee)),V.closeAllNodes(),V.finalize(),$=V.toHTML(),{relevance:Math.floor(Z),value:$,language:s,illegal:!1,emitter:V,top:z}}catch(i){if(i.message&&i.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:i.message,context:o.slice(ee-100,ee+100),mode:i.mode},sofar:$,relevance:0,value:Se(o),emitter:V};if(_)return{illegal:!1,relevance:0,value:Se(o),emitter:V,language:s,top:z,errorRaised:i};throw i}}function highlightAuto(s,o){o=o||j.languages||Object.keys(i);const a=function justTextHighlightResult(s){const o={relevance:0,emitter:new j.__emitter(j),value:Se(s),illegal:!1,top:C};return o.emitter.addText(s),o}(s),u=o.filter(getLanguage).filter(autoDetection).map((o=>_highlight(o,s,!1)));u.unshift(a);const _=u.sort(((s,o)=>{if(s.relevance!==o.relevance)return o.relevance-s.relevance;if(s.language&&o.language){if(getLanguage(s.language).supersetOf===o.language)return 1;if(getLanguage(o.language).supersetOf===s.language)return-1}return 0})),[w,x]=_,L=w;return L.second_best=x,L}const L={"before:highlightElement":({el:s})=>{j.useBR&&(s.innerHTML=s.innerHTML.replace(/\n/g,"").replace(//g,"\n"))},"after:highlightElement":({result:s})=>{j.useBR&&(s.value=s.value.replace(/\n/g,"
"))}},B=/^(<[^>]+>|\t)+/gm,$={"after:highlightElement":({result:s})=>{j.tabReplace&&(s.value=s.value.replace(B,(s=>s.replace(/\t/g,j.tabReplace))))}};function highlightElement(s){let o=null;const i=function blockLanguage(s){let o=s.className+" ";o+=s.parentNode?s.parentNode.className:"";const i=j.languageDetectRe.exec(o);if(i){const o=getLanguage(i[1]);return o||(warn(x.replace("{}",i[1])),warn("Falling back to no-highlight mode for this block.",s)),o?i[1]:"no-highlight"}return o.split(/\s+/).find((s=>shouldNotHighlight(s)||getLanguage(s)))}(s);if(shouldNotHighlight(i))return;fire("before:highlightElement",{el:s,language:i}),o=s;const u=o.textContent,_=i?highlight(u,{language:i,ignoreIllegals:!0}):highlightAuto(u);fire("after:highlightElement",{el:s,result:_,text:u}),s.innerHTML=_.value,function updateClassName(s,o,i){const u=o?a[o]:i;s.classList.add("hljs"),u&&s.classList.add(u)}(s,i,_.language),s.result={language:_.language,re:_.relevance,relavance:_.relevance},_.second_best&&(s.second_best={language:_.second_best.language,re:_.second_best.relevance,relavance:_.second_best.relevance})}const initHighlighting=()=>{if(initHighlighting.called)return;initHighlighting.called=!0,deprecated("10.6.0","initHighlighting() is deprecated. Use highlightAll() instead.");document.querySelectorAll("pre code").forEach(highlightElement)};let z=!1;function highlightAll(){if("loading"===document.readyState)return void(z=!0);document.querySelectorAll("pre code").forEach(highlightElement)}function getLanguage(s){return s=(s||"").toLowerCase(),i[s]||i[a[s]]}function registerAliases(s,{languageName:o}){"string"==typeof s&&(s=[s]),s.forEach((s=>{a[s.toLowerCase()]=o}))}function autoDetection(s){const o=getLanguage(s);return o&&!o.disableAutodetect}function fire(s,o){const i=s;u.forEach((function(s){s[i]&&s[i](o)}))}"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(function boot(){z&&highlightAll()}),!1),Object.assign(s,{highlight,highlightAuto,highlightAll,fixMarkup:function deprecateFixMarkup(s){return deprecated("10.2.0","fixMarkup will be removed entirely in v11.0"),deprecated("10.2.0","Please see https://github.com/highlightjs/highlight.js/issues/2534"),function fixMarkup(s){return j.tabReplace||j.useBR?s.replace(w,(s=>"\n"===s?j.useBR?"
":s:j.tabReplace?s.replace(/\t/g,j.tabReplace):s)):s}(s)},highlightElement,highlightBlock:function deprecateHighlightBlock(s){return deprecated("10.7.0","highlightBlock will be removed entirely in v12.0"),deprecated("10.7.0","Please use highlightElement now."),highlightElement(s)},configure:function configure(s){s.useBR&&(deprecated("10.3.0","'useBR' will be removed entirely in v11.0"),deprecated("10.3.0","Please see https://github.com/highlightjs/highlight.js/issues/2559")),j=_e(j,s)},initHighlighting,initHighlightingOnLoad:function initHighlightingOnLoad(){deprecated("10.6.0","initHighlightingOnLoad() is deprecated. Use highlightAll() instead."),z=!0},registerLanguage:function registerLanguage(o,a){let u=null;try{u=a(s)}catch(s){if(error("Language definition for '{}' could not be registered.".replace("{}",o)),!_)throw s;error(s),u=C}u.name||(u.name=o),i[o]=u,u.rawDefinition=a.bind(null,s),u.aliases&®isterAliases(u.aliases,{languageName:o})},unregisterLanguage:function unregisterLanguage(s){delete i[s];for(const o of Object.keys(a))a[o]===s&&delete a[o]},listLanguages:function listLanguages(){return Object.keys(i)},getLanguage,registerAliases,requireLanguage:function requireLanguage(s){deprecated("10.4.0","requireLanguage will be removed entirely in v11."),deprecated("10.4.0","Please see https://github.com/highlightjs/highlight.js/pull/2844");const o=getLanguage(s);if(o)return o;throw new Error("The '{}' language is required, but not loaded.".replace("{}",s))},autoDetection,inherit:_e,addPlugin:function addPlugin(s){!function upgradePluginAPI(s){s["before:highlightBlock"]&&!s["before:highlightElement"]&&(s["before:highlightElement"]=o=>{s["before:highlightBlock"](Object.assign({block:o.el},o))}),s["after:highlightBlock"]&&!s["after:highlightElement"]&&(s["after:highlightElement"]=o=>{s["after:highlightBlock"](Object.assign({block:o.el},o))})}(s),u.push(s)},vuePlugin:BuildVuePlugin(s).VuePlugin}),s.debugMode=function(){_=!1},s.safeMode=function(){_=!0},s.versionString="10.7.3";for(const s in de)"object"==typeof de[s]&&o(de[s]);return Object.assign(s,de),s.addPlugin(L),s.addPlugin(ye),s.addPlugin($),s}({});s.exports=xe},35344(s){function concat(...s){return s.map((s=>function source(s){return s?"string"==typeof s?s:s.source:null}(s))).join("")}s.exports=function bash(s){const o={},i={begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[o]}]};Object.assign(o,{className:"variable",variants:[{begin:concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},i]});const a={className:"subst",begin:/\$\(/,end:/\)/,contains:[s.BACKSLASH_ESCAPE]},u={begin:/<<-?\s*(?=\w+)/,starts:{contains:[s.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,className:"string"})]}},_={className:"string",begin:/"/,end:/"/,contains:[s.BACKSLASH_ESCAPE,o,a]};a.contains.push(_);const w={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},s.NUMBER_MODE,o]},x=s.SHEBANG({binary:`(${["fish","bash","zsh","sh","csh","ksh","tcsh","dash","scsh"].join("|")})`,relevance:10}),C={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[s.inherit(s.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z._-]+\b/,keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp"},contains:[x,s.SHEBANG(),C,w,s.HASH_COMMENT_MODE,u,_,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},o]}}},73402(s){function concat(...s){return s.map((s=>function source(s){return s?"string"==typeof s?s:s.source:null}(s))).join("")}s.exports=function http(s){const o="HTTP/(2|1\\.[01])",i={className:"attribute",begin:concat("^",/[A-Za-z][A-Za-z0-9-]*/,"(?=\\:\\s)"),starts:{contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$",relevance:0}}]}},a=[i,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{begin:"^(?="+o+" \\d{3})",end:/$/,contains:[{className:"meta",begin:o},{className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/,contains:a}},{begin:"(?=^[A-Z]+ (.*?) "+o+"$)",end:/$/,contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{className:"meta",begin:o},{className:"keyword",begin:"[A-Z]+"}],starts:{end:/\b\B/,illegal:/\S/,contains:a}},s.inherit(i,{relevance:0})]}}},95089(s){const o="[A-Za-z$_][0-9A-Za-z$_]*",i=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],a=["true","false","null","undefined","NaN","Infinity"],u=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);function lookahead(s){return concat("(?=",s,")")}function concat(...s){return s.map((s=>function source(s){return s?"string"==typeof s?s:s.source:null}(s))).join("")}s.exports=function javascript(s){const _=o,w="<>",x="",C={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(s,o)=>{const i=s[0].length+s.index,a=s.input[i];"<"!==a?">"===a&&(((s,{after:o})=>{const i="",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:s.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:j,contains:ce}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:w,end:x},{begin:C.begin,"on:begin":C.isTrulyOpeningTag,end:C.end}],subLanguage:"xml",contains:[{begin:C.begin,end:C.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[{;]/,excludeEnd:!0,keywords:j,contains:["self",s.inherit(s.TITLE_MODE,{begin:_}),le],illegal:/%/},{beginKeywords:"while if switch catch for"},{className:"function",begin:s.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:!0,contains:[le,s.inherit(s.TITLE_MODE,{begin:_})]},{variants:[{begin:"\\."+_},{begin:"\\$"+_}],relevance:0},{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"[\]]/,contains:[{beginKeywords:"extends"},s.UNDERSCORE_TITLE_MODE]},{begin:/\b(?=constructor)/,end:/[{;]/,excludeEnd:!0,contains:[s.inherit(s.TITLE_MODE,{begin:_}),"self",le]},{begin:"(get|set)\\s+(?="+_+"\\()",end:/\{/,keywords:"get set",contains:[s.inherit(s.TITLE_MODE,{begin:_}),{begin:/\(\)/},le]},{begin:/\$[(.]/}]}}},65772(s){s.exports=function json(s){const o={literal:"true false null"},i=[s.C_LINE_COMMENT_MODE,s.C_BLOCK_COMMENT_MODE],a=[s.QUOTE_STRING_MODE,s.C_NUMBER_MODE],u={end:",",endsWithParent:!0,excludeEnd:!0,contains:a,keywords:o},_={begin:/\{/,end:/\}/,contains:[{className:"attr",begin:/"/,end:/"/,contains:[s.BACKSLASH_ESCAPE],illegal:"\\n"},s.inherit(u,{begin:/:/})].concat(i),illegal:"\\S"},w={begin:"\\[",end:"\\]",contains:[s.inherit(u)],illegal:"\\S"};return a.push(_,w),i.forEach((function(s){a.push(s)})),{name:"JSON",contains:a,keywords:o,illegal:"\\S"}}},26571(s){s.exports=function powershell(s){const o={$pattern:/-?[A-z\.\-]+\b/,keyword:"if else foreach return do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch hidden static parameter",built_in:"ac asnp cat cd CFS chdir clc clear clhy cli clp cls clv cnsn compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo|0 epal epcsv epsn erase etsn exsn fc fhx fl ft fw gal gbp gc gcb gci gcm gcs gdr gerr ghy gi gin gjb gl gm gmo gp gps gpv group gsn gsnp gsv gtz gu gv gwmi h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc scb select set shcm si sl sleep sls sort sp spjb spps spsv start stz sujb sv swmi tee trcm type wget where wjb write"},i={begin:"`[\\s\\S]",relevance:0},a={className:"variable",variants:[{begin:/\$\B/},{className:"keyword",begin:/\$this/},{begin:/\$[\w\d][\w\d_:]*/}]},u={className:"string",variants:[{begin:/"/,end:/"/},{begin:/@"/,end:/^"@/}],contains:[i,a,{className:"variable",begin:/\$[A-z]/,end:/[^A-z]/}]},_={className:"string",variants:[{begin:/'/,end:/'/},{begin:/@'/,end:/^'@/}]},w=s.inherit(s.COMMENT(null,null),{variants:[{begin:/#/,end:/$/},{begin:/<#/,end:/#>/}],contains:[{className:"doctag",variants:[{begin:/\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/},{begin:/\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/}]}]}),x={className:"built_in",variants:[{begin:"(".concat("Add|Clear|Close|Copy|Enter|Exit|Find|Format|Get|Hide|Join|Lock|Move|New|Open|Optimize|Pop|Push|Redo|Remove|Rename|Reset|Resize|Search|Select|Set|Show|Skip|Split|Step|Switch|Undo|Unlock|Watch|Backup|Checkpoint|Compare|Compress|Convert|ConvertFrom|ConvertTo|Dismount|Edit|Expand|Export|Group|Import|Initialize|Limit|Merge|Mount|Out|Publish|Restore|Save|Sync|Unpublish|Update|Approve|Assert|Build|Complete|Confirm|Deny|Deploy|Disable|Enable|Install|Invoke|Register|Request|Restart|Resume|Start|Stop|Submit|Suspend|Uninstall|Unregister|Wait|Debug|Measure|Ping|Repair|Resolve|Test|Trace|Connect|Disconnect|Read|Receive|Send|Write|Block|Grant|Protect|Revoke|Unblock|Unprotect|Use|ForEach|Sort|Tee|Where",")+(-)[\\w\\d]+")}]},C={className:"class",beginKeywords:"class enum",end:/\s*[{]/,excludeEnd:!0,relevance:0,contains:[s.TITLE_MODE]},j={className:"function",begin:/function\s+/,end:/\s*\{|$/,excludeEnd:!0,returnBegin:!0,relevance:0,contains:[{begin:"function",relevance:0,className:"keyword"},{className:"title",begin:/\w[\w\d]*((-)[\w\d]+)*/,relevance:0},{begin:/\(/,end:/\)/,className:"params",relevance:0,contains:[a]}]},L={begin:/using\s/,end:/$/,returnBegin:!0,contains:[u,_,{className:"keyword",begin:/(using|assembly|command|module|namespace|type)/}]},B={variants:[{className:"operator",begin:"(".concat("-and|-as|-band|-bnot|-bor|-bxor|-casesensitive|-ccontains|-ceq|-cge|-cgt|-cle|-clike|-clt|-cmatch|-cne|-cnotcontains|-cnotlike|-cnotmatch|-contains|-creplace|-csplit|-eq|-exact|-f|-file|-ge|-gt|-icontains|-ieq|-ige|-igt|-ile|-ilike|-ilt|-imatch|-in|-ine|-inotcontains|-inotlike|-inotmatch|-ireplace|-is|-isnot|-isplit|-join|-le|-like|-lt|-match|-ne|-not|-notcontains|-notin|-notlike|-notmatch|-or|-regex|-replace|-shl|-shr|-split|-wildcard|-xor",")\\b")},{className:"literal",begin:/(-)[\w\d]+/,relevance:0}]},$={className:"function",begin:/\[.*\]\s*[\w]+[ ]??\(/,end:/$/,returnBegin:!0,relevance:0,contains:[{className:"keyword",begin:"(".concat(o.keyword.toString().replace(/\s/g,"|"),")\\b"),endsParent:!0,relevance:0},s.inherit(s.TITLE_MODE,{endsParent:!0})]},z=[$,w,i,s.NUMBER_MODE,u,_,x,a,{className:"literal",begin:/\$(null|true|false)\b/},{className:"selector-tag",begin:/@\B/,relevance:0}],U={begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[].concat("self",z,{begin:"("+["string","char","byte","int","long","bool","decimal","single","double","DateTime","xml","array","hashtable","void"].join("|")+")",className:"built_in",relevance:0},{className:"type",begin:/[\.\w\d]+/,relevance:0})};return $.contains.unshift(U),{name:"PowerShell",aliases:["ps","ps1"],case_insensitive:!0,keywords:o,contains:z.concat(C,j,L,B,U)}}},17285(s){function source(s){return s?"string"==typeof s?s:s.source:null}function lookahead(s){return concat("(?=",s,")")}function concat(...s){return s.map((s=>source(s))).join("")}function either(...s){return"("+s.map((s=>source(s))).join("|")+")"}s.exports=function xml(s){const o=concat(/[A-Z_]/,function optional(s){return concat("(",s,")?")}(/[A-Z0-9_.-]*:/),/[A-Z0-9_.-]*/),i={className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},a={begin:/\s/,contains:[{className:"meta-keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},u=s.inherit(a,{begin:/\(/,end:/\)/}),_=s.inherit(s.APOS_STRING_MODE,{className:"meta-string"}),w=s.inherit(s.QUOTE_STRING_MODE,{className:"meta-string"}),x={endsWithParent:!0,illegal:/`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin://,relevance:10,contains:[a,w,_,u,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin://,contains:[a,u,w,_]}]}]},s.COMMENT(//,{relevance:10}),{begin://,relevance:10},i,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:/)/,end:/>/,keywords:{name:"style"},contains:[x],starts:{end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:/)/,end:/>/,keywords:{name:"script"},contains:[x],starts:{end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:/<>|<\/>/},{className:"tag",begin:concat(//,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name",begin:o,relevance:0,starts:x}]},{className:"tag",begin:concat(/<\//,lookahead(concat(o,/>/))),contains:[{className:"name",begin:o,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}}},17533(s){s.exports=function yaml(s){var o="true false yes no null",i="[\\w#;/?:@&=+$,.~*'()[\\]]+",a={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[s.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},u=s.inherit(a,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),_={className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},w={end:",",endsWithParent:!0,excludeEnd:!0,keywords:o,relevance:0},x={begin:/\{/,end:/\}/,contains:[w],illegal:"\\n",relevance:0},C={begin:"\\[",end:"\\]",contains:[w],illegal:"\\n",relevance:0},j=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$",relevance:10},{className:"string",begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+i},{className:"type",begin:"!<"+i+">"},{className:"type",begin:"!"+i},{className:"type",begin:"!!"+i},{className:"meta",begin:"&"+s.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+s.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",relevance:0},s.HASH_COMMENT_MODE,{beginKeywords:o,keywords:{literal:o}},_,{className:"number",begin:s.C_NUMBER_RE+"\\b",relevance:0},x,C,a],L=[...j];return L.pop(),L.push(u),w.contains=L,{name:"YAML",case_insensitive:!0,aliases:["yml"],contains:j}}},251(s,o){o.read=function(s,o,i,a,u){var _,w,x=8*u-a-1,C=(1<>1,L=-7,B=i?u-1:0,$=i?-1:1,z=s[o+B];for(B+=$,_=z&(1<<-L)-1,z>>=-L,L+=x;L>0;_=256*_+s[o+B],B+=$,L-=8);for(w=_&(1<<-L)-1,_>>=-L,L+=a;L>0;w=256*w+s[o+B],B+=$,L-=8);if(0===_)_=1-j;else{if(_===C)return w?NaN:1/0*(z?-1:1);w+=Math.pow(2,a),_-=j}return(z?-1:1)*w*Math.pow(2,_-a)},o.write=function(s,o,i,a,u,_){var w,x,C,j=8*_-u-1,L=(1<>1,$=23===u?Math.pow(2,-24)-Math.pow(2,-77):0,z=a?0:_-1,U=a?1:-1,V=o<0||0===o&&1/o<0?1:0;for(o=Math.abs(o),isNaN(o)||o===1/0?(x=isNaN(o)?1:0,w=L):(w=Math.floor(Math.log(o)/Math.LN2),o*(C=Math.pow(2,-w))<1&&(w--,C*=2),(o+=w+B>=1?$/C:$*Math.pow(2,1-B))*C>=2&&(w++,C/=2),w+B>=L?(x=0,w=L):w+B>=1?(x=(o*C-1)*Math.pow(2,u),w+=B):(x=o*Math.pow(2,B-1)*Math.pow(2,u),w=0));u>=8;s[i+z]=255&x,z+=U,x/=256,u-=8);for(w=w<0;s[i+z]=255&w,z+=U,w/=256,j-=8);s[i+z-U]|=128*V}},49568(s,o,i){"use strict";i.r(o),i.d(o,{Collection:()=>w,Iterable:()=>Zt,List:()=>wt,Map:()=>ct,OrderedMap:()=>jt,OrderedSet:()=>Vt,PairSorting:()=>Ht,Range:()=>Rt,Record:()=>Kt,Repeat:()=>Xt,Seq:()=>le,Set:()=>Ft,Stack:()=>Tt,fromJS:()=>fromJS,get:()=>get,getIn:()=>getIn$1,has:()=>has,hasIn:()=>hasIn$1,hash:()=>hash,is:()=>is,isAssociative:()=>isAssociative,isCollection:()=>isCollection,isImmutable:()=>isImmutable,isIndexed:()=>isIndexed,isKeyed:()=>isKeyed,isList:()=>isList,isMap:()=>isMap,isOrdered:()=>isOrdered,isOrderedMap:()=>isOrderedMap,isOrderedSet:()=>isOrderedSet,isPlainObject:()=>isPlainObject,isRecord:()=>isRecord,isSeq:()=>isSeq,isSet:()=>isSet,isStack:()=>isStack,isValueObject:()=>isValueObject,merge:()=>merge,mergeDeep:()=>mergeDeep$1,mergeDeepWith:()=>mergeDeepWith$1,mergeWith:()=>mergeWith,remove:()=>remove,removeIn:()=>removeIn,set:()=>set,setIn:()=>setIn$1,update:()=>update$1,updateIn:()=>updateIn,version:()=>Qt});var a="@@__IMMUTABLE_INDEXED__@@";function isIndexed(s){return Boolean(s&&s[a])}var u="@@__IMMUTABLE_KEYED__@@";function isKeyed(s){return Boolean(s&&s[u])}function isAssociative(s){return isKeyed(s)||isIndexed(s)}var _="@@__IMMUTABLE_ITERABLE__@@";function isCollection(s){return Boolean(s&&s[_])}var w=function Collection(s){return isCollection(s)?s:le(s)},x=function(s){function KeyedCollection(s){return isKeyed(s)?s:pe(s)}return s&&(KeyedCollection.__proto__=s),KeyedCollection.prototype=Object.create(s&&s.prototype),KeyedCollection.prototype.constructor=KeyedCollection,KeyedCollection}(w),C=function(s){function IndexedCollection(s){return isIndexed(s)?s:de(s)}return s&&(IndexedCollection.__proto__=s),IndexedCollection.prototype=Object.create(s&&s.prototype),IndexedCollection.prototype.constructor=IndexedCollection,IndexedCollection}(w),j=function(s){function SetCollection(s){return isCollection(s)&&!isAssociative(s)?s:fe(s)}return s&&(SetCollection.__proto__=s),SetCollection.prototype=Object.create(s&&s.prototype),SetCollection.prototype.constructor=SetCollection,SetCollection}(w);w.Keyed=x,w.Indexed=C,w.Set=j;var L="function"==typeof Symbol&&Symbol.iterator,B="@@iterator",$=L||B,z=function Iterator(s){this.next=s};function iteratorValue(s,o,i,a){var u=0===s?o:1===s?i:[o,i];return a?a.value=u:a={value:u,done:!1},a}function iteratorDone(){return{value:void 0,done:!0}}function hasIterator(s){return!!Array.isArray(s)||!!getIteratorFn(s)}function isIterator(s){return!(!s||"function"!=typeof s.next)}function getIterator(s){var o=getIteratorFn(s);return o&&o.call(s)}function getIteratorFn(s){var o=s&&(L&&s[L]||s[B]);if("function"==typeof o)return o}z.prototype.toString=function toString(){return"[Iterator]"},z.KEYS=0,z.VALUES=1,z.ENTRIES=2,z.prototype.inspect=z.prototype.toSource=function(){return this.toString()},z.prototype[$]=function(){return this};var U="delete",V=32,Y=31,Z={};function SetRef(s){s&&(s.value=!0)}function OwnerID(){}function ensureSize(s){return void 0===s.size&&(s.size=s.__iterate(returnTrue)),s.size}function wrapIndex(s,o){if("number"!=typeof o){var i=o>>>0;if(""+i!==o||4294967295===i)return NaN;o=i}return o<0?ensureSize(s)+o:o}function returnTrue(){return!0}function wholeSlice(s,o,i){return(0===s&&!isNeg(s)||void 0!==i&&s<=-i)&&(void 0===o||void 0!==i&&o>=i)}function resolveBegin(s,o){return resolveIndex(s,o,0)}function resolveEnd(s,o){return resolveIndex(s,o,o)}function resolveIndex(s,o,i){return void 0===s?i:isNeg(s)?o===1/0?o:0|Math.max(0,o+s):void 0===o||o===s?s:0|Math.min(o,s)}function isNeg(s){return s<0||0===s&&1/s==-1/0}var ee="@@__IMMUTABLE_RECORD__@@";function isRecord(s){return Boolean(s&&s[ee])}function isImmutable(s){return isCollection(s)||isRecord(s)}var ie="@@__IMMUTABLE_ORDERED__@@";function isOrdered(s){return Boolean(s&&s[ie])}var ae="@@__IMMUTABLE_SEQ__@@";function isSeq(s){return Boolean(s&&s[ae])}var ce=Object.prototype.hasOwnProperty;function isArrayLike(s){return!(!Array.isArray(s)&&"string"!=typeof s)||s&&"object"==typeof s&&Number.isInteger(s.length)&&s.length>=0&&(0===s.length?1===Object.keys(s).length:s.hasOwnProperty(s.length-1))}var le=function(s){function Seq(s){return null==s?emptySequence():isImmutable(s)?s.toSeq():function seqFromValue(s){var o=maybeIndexedSeqFromValue(s);if(o)return function isEntriesIterable(s){var o=getIteratorFn(s);return o&&o===s.entries}(s)?o.fromEntrySeq():function isKeysIterable(s){var o=getIteratorFn(s);return o&&o===s.keys}(s)?o.toSetSeq():o;if("object"==typeof s)return new be(s);throw new TypeError("Expected Array or collection object of values, or keyed object: "+s)}(s)}return s&&(Seq.__proto__=s),Seq.prototype=Object.create(s&&s.prototype),Seq.prototype.constructor=Seq,Seq.prototype.toSeq=function toSeq(){return this},Seq.prototype.toString=function toString(){return this.__toString("Seq {","}")},Seq.prototype.cacheResult=function cacheResult(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},Seq.prototype.__iterate=function __iterate(s,o){var i=this._cache;if(i){for(var a=i.length,u=0;u!==a;){var _=i[o?a-++u:u++];if(!1===s(_[1],_[0],this))break}return u}return this.__iterateUncached(s,o)},Seq.prototype.__iterator=function __iterator(s,o){var i=this._cache;if(i){var a=i.length,u=0;return new z((function(){if(u===a)return{value:void 0,done:!0};var _=i[o?a-++u:u++];return iteratorValue(s,_[0],_[1])}))}return this.__iteratorUncached(s,o)},Seq}(w),pe=function(s){function KeyedSeq(s){return null==s?emptySequence().toKeyedSeq():isCollection(s)?isKeyed(s)?s.toSeq():s.fromEntrySeq():isRecord(s)?s.toSeq():keyedSeqFromValue(s)}return s&&(KeyedSeq.__proto__=s),KeyedSeq.prototype=Object.create(s&&s.prototype),KeyedSeq.prototype.constructor=KeyedSeq,KeyedSeq.prototype.toKeyedSeq=function toKeyedSeq(){return this},KeyedSeq}(le),de=function(s){function IndexedSeq(s){return null==s?emptySequence():isCollection(s)?isKeyed(s)?s.entrySeq():s.toIndexedSeq():isRecord(s)?s.toSeq().entrySeq():indexedSeqFromValue(s)}return s&&(IndexedSeq.__proto__=s),IndexedSeq.prototype=Object.create(s&&s.prototype),IndexedSeq.prototype.constructor=IndexedSeq,IndexedSeq.of=function of(){return IndexedSeq(arguments)},IndexedSeq.prototype.toIndexedSeq=function toIndexedSeq(){return this},IndexedSeq.prototype.toString=function toString(){return this.__toString("Seq [","]")},IndexedSeq}(le),fe=function(s){function SetSeq(s){return(isCollection(s)&&!isAssociative(s)?s:de(s)).toSetSeq()}return s&&(SetSeq.__proto__=s),SetSeq.prototype=Object.create(s&&s.prototype),SetSeq.prototype.constructor=SetSeq,SetSeq.of=function of(){return SetSeq(arguments)},SetSeq.prototype.toSetSeq=function toSetSeq(){return this},SetSeq}(le);le.isSeq=isSeq,le.Keyed=pe,le.Set=fe,le.Indexed=de,le.prototype[ae]=!0;var ye=function(s){function ArraySeq(s){this._array=s,this.size=s.length}return s&&(ArraySeq.__proto__=s),ArraySeq.prototype=Object.create(s&&s.prototype),ArraySeq.prototype.constructor=ArraySeq,ArraySeq.prototype.get=function get(s,o){return this.has(s)?this._array[wrapIndex(this,s)]:o},ArraySeq.prototype.__iterate=function __iterate(s,o){for(var i=this._array,a=i.length,u=0;u!==a;){var _=o?a-++u:u++;if(!1===s(i[_],_,this))break}return u},ArraySeq.prototype.__iterator=function __iterator(s,o){var i=this._array,a=i.length,u=0;return new z((function(){if(u===a)return{value:void 0,done:!0};var _=o?a-++u:u++;return iteratorValue(s,_,i[_])}))},ArraySeq}(de),be=function(s){function ObjectSeq(s){var o=Object.keys(s).concat(Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(s):[]);this._object=s,this._keys=o,this.size=o.length}return s&&(ObjectSeq.__proto__=s),ObjectSeq.prototype=Object.create(s&&s.prototype),ObjectSeq.prototype.constructor=ObjectSeq,ObjectSeq.prototype.get=function get(s,o){return void 0===o||this.has(s)?this._object[s]:o},ObjectSeq.prototype.has=function has(s){return ce.call(this._object,s)},ObjectSeq.prototype.__iterate=function __iterate(s,o){for(var i=this._object,a=this._keys,u=a.length,_=0;_!==u;){var w=a[o?u-++_:_++];if(!1===s(i[w],w,this))break}return _},ObjectSeq.prototype.__iterator=function __iterator(s,o){var i=this._object,a=this._keys,u=a.length,_=0;return new z((function(){if(_===u)return{value:void 0,done:!0};var w=a[o?u-++_:_++];return iteratorValue(s,w,i[w])}))},ObjectSeq}(pe);be.prototype[ie]=!0;var Se,_e=function(s){function CollectionSeq(s){this._collection=s,this.size=s.length||s.size}return s&&(CollectionSeq.__proto__=s),CollectionSeq.prototype=Object.create(s&&s.prototype),CollectionSeq.prototype.constructor=CollectionSeq,CollectionSeq.prototype.__iterateUncached=function __iterateUncached(s,o){if(o)return this.cacheResult().__iterate(s,o);var i=getIterator(this._collection),a=0;if(isIterator(i))for(var u;!(u=i.next()).done&&!1!==s(u.value,a++,this););return a},CollectionSeq.prototype.__iteratorUncached=function __iteratorUncached(s,o){if(o)return this.cacheResult().__iterator(s,o);var i=getIterator(this._collection);if(!isIterator(i))return new z(iteratorDone);var a=0;return new z((function(){var o=i.next();return o.done?o:iteratorValue(s,a++,o.value)}))},CollectionSeq}(de);function emptySequence(){return Se||(Se=new ye([]))}function keyedSeqFromValue(s){var o=maybeIndexedSeqFromValue(s);if(o)return o.fromEntrySeq();if("object"==typeof s)return new be(s);throw new TypeError("Expected Array or collection object of [k, v] entries, or keyed object: "+s)}function indexedSeqFromValue(s){var o=maybeIndexedSeqFromValue(s);if(o)return o;throw new TypeError("Expected Array or collection object of values: "+s)}function maybeIndexedSeqFromValue(s){return isArrayLike(s)?new ye(s):hasIterator(s)?new _e(s):void 0}function asImmutable(){return this.__ensureOwner()}function asMutable(){return this.__ownerID?this:this.__ensureOwner(new OwnerID)}var we="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function imul(s,o){var i=65535&(s|=0),a=65535&(o|=0);return i*a+((s>>>16)*a+i*(o>>>16)<<16>>>0)|0};function smi(s){return s>>>1&1073741824|3221225471&s}var xe=Object.prototype.valueOf;function hash(s){if(null==s)return hashNullish(s);if("function"==typeof s.hashCode)return smi(s.hashCode(s));var o=function valueOf(s){return s.valueOf!==xe&&"function"==typeof s.valueOf?s.valueOf(s):s}(s);if(null==o)return hashNullish(o);switch(typeof o){case"boolean":return o?1108378657:1108378656;case"number":return function hashNumber(s){if(s!=s||s===1/0)return 0;var o=0|s;o!==s&&(o^=4294967295*s);for(;s>4294967295;)o^=s/=4294967295;return smi(o)}(o);case"string":return o.length>Ye?function cachedHashString(s){var o=et[s];void 0===o&&(o=hashString(s),Qe===Xe&&(Qe=0,et={}),Qe++,et[s]=o);return o}(o):hashString(o);case"object":case"function":return function hashJSObj(s){var o;if($e&&void 0!==(o=qe.get(s)))return o;if(void 0!==(o=s[He]))return o;if(!Re){if(void 0!==(o=s.propertyIsEnumerable&&s.propertyIsEnumerable[He]))return o;if(void 0!==(o=function getIENodeHash(s){if(s&&s.nodeType>0)switch(s.nodeType){case 1:return s.uniqueID;case 9:return s.documentElement&&s.documentElement.uniqueID}}(s)))return o}if(o=nextHash(),$e)qe.set(s,o);else{if(void 0!==Te&&!1===Te(s))throw new Error("Non-extensible objects are not allowed as keys.");if(Re)Object.defineProperty(s,He,{enumerable:!1,configurable:!1,writable:!1,value:o});else if(void 0!==s.propertyIsEnumerable&&s.propertyIsEnumerable===s.constructor.prototype.propertyIsEnumerable)s.propertyIsEnumerable=function(){return this.constructor.prototype.propertyIsEnumerable.apply(this,arguments)},s.propertyIsEnumerable[He]=o;else{if(void 0===s.nodeType)throw new Error("Unable to set a non-enumerable property on object.");s[He]=o}}return o}(o);case"symbol":return function hashSymbol(s){var o=ze[s];if(void 0!==o)return o;return o=nextHash(),ze[s]=o,o}(o);default:if("function"==typeof o.toString)return hashString(o.toString());throw new Error("Value type "+typeof o+" cannot be hashed.")}}function hashNullish(s){return null===s?1108378658:1108378659}function hashString(s){for(var o=0,i=0;i=0&&(C.get=function(o,i){return(o=wrapIndex(this,o))>=0&&o<_?s.get(o+w,i):i}),C.__iterateUncached=function(o,i){var u=this;if(0===_)return 0;if(i)return this.cacheResult().__iterate(o,i);var x=0,C=!0,j=0;return s.__iterate((function(s,i){if(!C||!(C=x++_)return{value:void 0,done:!0};var s=u.next();return a||1===o||s.done?s:iteratorValue(o,C-1,0===o?void 0:s.value[1],s)}))},C}function skipWhileFactory(s,o,i,a){var u=makeSequence(s);return u.__iterateUncached=function(u,_){var w=this;if(_)return this.cacheResult().__iterate(u,_);var x=!0,C=0;return s.__iterate((function(s,_,j){if(!x||!(x=o.call(i,s,_,j)))return C++,u(s,a?_:C-1,w)})),C},u.__iteratorUncached=function(u,_){var w=this;if(_)return this.cacheResult().__iterator(u,_);var x=s.__iterator(2,_),C=!0,j=0;return new z((function(){var s,_,L;do{if((s=x.next()).done)return a||1===u?s:iteratorValue(u,j++,0===u?void 0:s.value[1],s);var B=s.value;_=B[0],L=B[1],C&&(C=o.call(i,L,_,w))}while(C);return 2===u?s:iteratorValue(u,_,L,s)}))},u}rt.prototype.cacheResult=tt.prototype.cacheResult=nt.prototype.cacheResult=st.prototype.cacheResult=cacheResultThrough;var ot=function(s){function ConcatSeq(s){this._wrappedIterables=s.flatMap((function(s){return s._wrappedIterables?s._wrappedIterables:[s]})),this.size=this._wrappedIterables.reduce((function(s,o){if(void 0!==s){var i=o.size;if(void 0!==i)return s+i}}),0),this[u]=this._wrappedIterables[0][u],this[a]=this._wrappedIterables[0][a],this[ie]=this._wrappedIterables[0][ie]}return s&&(ConcatSeq.__proto__=s),ConcatSeq.prototype=Object.create(s&&s.prototype),ConcatSeq.prototype.constructor=ConcatSeq,ConcatSeq.prototype.__iterateUncached=function __iterateUncached(s,o){if(0!==this._wrappedIterables.length){if(o)return this.cacheResult().__iterate(s,o);for(var i=0,a=isKeyed(this),u=a?2:1,_=this._wrappedIterables[i].__iterator(u,o),w=!0,x=0;w;){for(var C=_.next();C.done;){if(++i===this._wrappedIterables.length)return x;C=(_=this._wrappedIterables[i].__iterator(u,o)).next()}w=!1!==(a?s(C.value[1],C.value[0],this):s(C.value,x,this)),x++}return x}},ConcatSeq.prototype.__iteratorUncached=function __iteratorUncached(s,o){var i=this;if(0===this._wrappedIterables.length)return new z(iteratorDone);if(o)return this.cacheResult().__iterator(s,o);var a=0,u=this._wrappedIterables[a].__iterator(s,o);return new z((function(){for(var _=u.next();_.done;){if(++a===i._wrappedIterables.length)return _;_=(u=i._wrappedIterables[a].__iterator(s,o)).next()}return _}))},ConcatSeq}(le);function flattenFactory(s,o,i){var a=makeSequence(s);return a.__iterateUncached=function(u,_){if(_)return this.cacheResult().__iterate(u,_);var w=0,x=!1;return function flatDeep(s,C){s.__iterate((function(s,_){return(!o||C0}function zipWithFactory(s,o,i,a){var u=makeSequence(s),_=new ye(i).map((function(s){return s.size}));return u.size=a?_.max():_.min(),u.__iterate=function(s,o){for(var i,a=this.__iterator(1,o),u=0;!(i=a.next()).done&&!1!==s(i.value,u++,this););return u},u.__iteratorUncached=function(s,u){var _=i.map((function(s){return s=w(s),getIterator(u?s.reverse():s)})),x=0,C=!1;return new z((function(){var i;return C||(i=_.map((function(s){return s.next()})),C=a?i.every((function(s){return s.done})):i.some((function(s){return s.done}))),C?{value:void 0,done:!0}:iteratorValue(s,x++,o.apply(null,i.map((function(s){return s.value}))))}))},u}function reify(s,o){return s===o?s:isSeq(s)?o:s.constructor(o)}function validateEntry(s){if(s!==Object(s))throw new TypeError("Expected [K, V] tuple: "+s)}function collectionClass(s){return isKeyed(s)?x:isIndexed(s)?C:j}function makeSequence(s){return Object.create((isKeyed(s)?pe:isIndexed(s)?de:fe).prototype)}function cacheResultThrough(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):le.prototype.cacheResult.call(this)}function defaultComparator(s,o){return void 0===s&&void 0===o?0:void 0===s?1:void 0===o?-1:s>o?1:s0;)o[i]=arguments[i+1];if("function"!=typeof s)throw new TypeError("Invalid merger function: "+s);return mergeIntoKeyedWith(this,o,s)}function mergeIntoKeyedWith(s,o,i){for(var a=[],u=0;u0;)o[i]=arguments[i+1];return mergeWithSources(s,o)}function mergeWith(s,o){for(var i=[],a=arguments.length-2;a-- >0;)i[a]=arguments[a+2];return mergeWithSources(o,i,s)}function mergeDeep$1(s){for(var o=[],i=arguments.length-1;i-- >0;)o[i]=arguments[i+1];return mergeDeepWithSources(s,o)}function mergeDeepWith$1(s,o){for(var i=[],a=arguments.length-2;a-- >0;)i[a]=arguments[a+2];return mergeDeepWithSources(o,i,s)}function mergeDeepWithSources(s,o,i){return mergeWithSources(s,o,function deepMergerWith(s){function deepMerger(o,i,a){return isDataStructure(o)&&isDataStructure(i)&&function areMergeable(s,o){var i=le(s),a=le(o);return isIndexed(i)===isIndexed(a)&&isKeyed(i)===isKeyed(a)}(o,i)?mergeWithSources(o,[i],deepMerger):s?s(o,i,a):i}return deepMerger}(i))}function mergeWithSources(s,o,i){if(!isDataStructure(s))throw new TypeError("Cannot merge into non-data-structure value: "+s);if(isImmutable(s))return"function"==typeof i&&s.mergeWith?s.mergeWith.apply(s,[i].concat(o)):s.merge?s.merge.apply(s,o):s.concat.apply(s,o);for(var a=Array.isArray(s),u=s,_=a?C:x,w=a?function(o){u===s&&(u=shallowCopy(u)),u.push(o)}:function(o,a){if(!isProtoKey(a)){var _=ce.call(u,a),w=_&&i?i(u[a],o,a):o;_&&w===u[a]||(u===s&&(u=shallowCopy(u)),u[a]=w)}},j=0;j0;)o[i]=arguments[i+1];return mergeDeepWithSources(this,o,s)}function mergeDeepIn(s){for(var o=[],i=arguments.length-1;i-- >0;)o[i]=arguments[i+1];return updateIn(this,s,emptyMap(),(function(s){return mergeDeepWithSources(s,o)}))}function mergeIn(s){for(var o=[],i=arguments.length-1;i-- >0;)o[i]=arguments[i+1];return updateIn(this,s,emptyMap(),(function(s){return mergeWithSources(s,o)}))}function setIn$1(s,o,i){return updateIn(s,o,Z,(function(){return i}))}function setIn(s,o){return setIn$1(this,s,o)}function update(s,o,i){return 1===arguments.length?s(this):update$1(this,s,o,i)}function updateIn$1(s,o,i){return updateIn(this,s,o,i)}function wasAltered(){return this.__altered}function withMutations(s){var o=this.asMutable();return s(o),o.wasAltered()?o.__ensureOwner(this.__ownerID):this}var at="@@__IMMUTABLE_MAP__@@";function isMap(s){return Boolean(s&&s[at])}function invariant(s,o){if(!s)throw new Error(o)}function assertNotInfinite(s){invariant(s!==1/0,"Cannot perform this action with an infinite size.")}var ct=function(s){function Map(o){return null==o?emptyMap():isMap(o)&&!isOrdered(o)?o:emptyMap().withMutations((function(i){var a=s(o);assertNotInfinite(a.size),a.forEach((function(s,o){return i.set(o,s)}))}))}return s&&(Map.__proto__=s),Map.prototype=Object.create(s&&s.prototype),Map.prototype.constructor=Map,Map.prototype.toString=function toString(){return this.__toString("Map {","}")},Map.prototype.get=function get(s,o){return this._root?this._root.get(0,void 0,s,o):o},Map.prototype.set=function set(s,o){return updateMap(this,s,o)},Map.prototype.remove=function remove(s){return updateMap(this,s,Z)},Map.prototype.deleteAll=function deleteAll(s){var o=w(s);return 0===o.size?this:this.withMutations((function(s){o.forEach((function(o){return s.remove(o)}))}))},Map.prototype.clear=function clear(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):emptyMap()},Map.prototype.sort=function sort(s){return jt(sortFactory(this,s))},Map.prototype.sortBy=function sortBy(s,o){return jt(sortFactory(this,o,s))},Map.prototype.map=function map(s,o){var i=this;return this.withMutations((function(a){a.forEach((function(u,_){a.set(_,s.call(o,u,_,i))}))}))},Map.prototype.__iterator=function __iterator(s,o){return new yt(this,s,o)},Map.prototype.__iterate=function __iterate(s,o){var i=this,a=0;return this._root&&this._root.iterate((function(o){return a++,s(o[1],o[0],i)}),o),a},Map.prototype.__ensureOwner=function __ensureOwner(s){return s===this.__ownerID?this:s?makeMap(this.size,this._root,s,this.__hash):0===this.size?emptyMap():(this.__ownerID=s,this.__altered=!1,this)},Map}(x);ct.isMap=isMap;var lt=ct.prototype;lt[at]=!0,lt[U]=lt.remove,lt.removeAll=lt.deleteAll,lt.setIn=setIn,lt.removeIn=lt.deleteIn=deleteIn,lt.update=update,lt.updateIn=updateIn$1,lt.merge=lt.concat=merge$1,lt.mergeWith=mergeWith$1,lt.mergeDeep=mergeDeep,lt.mergeDeepWith=mergeDeepWith,lt.mergeIn=mergeIn,lt.mergeDeepIn=mergeDeepIn,lt.withMutations=withMutations,lt.wasAltered=wasAltered,lt.asImmutable=asImmutable,lt["@@transducer/init"]=lt.asMutable=asMutable,lt["@@transducer/step"]=function(s,o){return s.set(o[0],o[1])},lt["@@transducer/result"]=function(s){return s.asImmutable()};var ut=function ArrayMapNode(s,o){this.ownerID=s,this.entries=o};ut.prototype.get=function get(s,o,i,a){for(var u=this.entries,_=0,w=u.length;_=vt)return function createNodes(s,o,i,a){s||(s=new OwnerID);for(var u=new mt(s,hash(i),[i,a]),_=0;_>>s)&Y),_=this.bitmap;return _&u?this.nodes[popCount(_&u-1)].get(s+5,o,i,a):a},pt.prototype.update=function update(s,o,i,a,u,_,w){void 0===i&&(i=hash(a));var x=(0===o?i:i>>>o)&Y,C=1<=bt)return function expandNodes(s,o,i,a,u){for(var _=0,w=new Array(V),x=0;0!==i;x++,i>>>=1)w[x]=1&i?o[_++]:void 0;return w[a]=u,new ht(s,_+1,w)}(s,$,j,x,U);if(L&&!U&&2===$.length&&isLeafNode($[1^B]))return $[1^B];if(L&&U&&1===$.length&&isLeafNode(U))return U;var ee=s&&s===this.ownerID,ie=L?U?j:j^C:j|C,ae=L?U?setAt($,B,U,ee):function spliceOut(s,o,i){var a=s.length-1;if(i&&o===a)return s.pop(),s;for(var u=new Array(a),_=0,w=0;w>>s)&Y,_=this.nodes[u];return _?_.get(s+5,o,i,a):a},ht.prototype.update=function update(s,o,i,a,u,_,w){void 0===i&&(i=hash(a));var x=(0===o?i:i>>>o)&Y,C=u===Z,j=this.nodes,L=j[x];if(C&&!L)return this;var B=updateNode(L,s,o+5,i,a,u,_,w);if(B===L)return this;var $=this.count;if(L){if(!B&&--$=_t&&(a=this._buildIndex()),void 0!==a){var u=a[hashCollisionKey(s)];if(void 0!==u)for(var _=0;_>>i)&Y,x=(0===i?a:a>>>i)&Y,C=w===x?[mergeIntoNode(s,o,i+5,a,u)]:(_=new mt(o,a,u),w>1&1431655765))+(s>>2&858993459))+(s>>4)&252645135,s+=s>>8,127&(s+=s>>16)}function setAt(s,o,i,a){var u=a?s:arrCopy(s);return u[o]=i,u}var vt=8,bt=16,St=8,_t=16;function coerceKeyPath(s){if(isArrayLike(s)&&"string"!=typeof s)return s;if(isOrdered(s))return s.toArray();throw new TypeError("Invalid keyPath: expected Ordered Collection or Array: "+s)}function quoteString(s){try{return"string"==typeof s?JSON.stringify(s):String(s)}catch(o){return JSON.stringify(s)}}function has(s,o){return isImmutable(s)?s.has(o):isDataStructure(s)&&ce.call(s,o)}function get(s,o,i){return isImmutable(s)?s.get(o,i):has(s,o)?"function"==typeof s.get?s.get(o):s[o]:i}function remove(s,o){if(!isDataStructure(s))throw new TypeError("Cannot update non-data-structure value: "+s);if(isImmutable(s)){if(!s.remove)throw new TypeError("Cannot update immutable value without .remove() method: "+s);return s.remove(o)}if(!ce.call(s,o))return s;var i=shallowCopy(s);return Array.isArray(i)?i.splice(o,1):delete i[o],i}function set(s,o,i){if(isProtoKey(o))return s;if(!isDataStructure(s))throw new TypeError("Cannot update non-data-structure value: "+s);if(isImmutable(s)){if(!s.set)throw new TypeError("Cannot update immutable value without .set() method: "+s);return s.set(o,i)}if(ce.call(s,o)&&i===s[o])return s;var a=shallowCopy(s);return a[o]=i,a}function updateIn(s,o,i,a){a||(a=i,i=void 0);var u=updateInDeeply(isImmutable(s),s,coerceKeyPath(o),0,i,a);return u===Z?i:u}function updateInDeeply(s,o,i,a,u,_){var w=o===Z;if(a===i.length){var x=w?u:o,C=_(x);return C===x?o:C}if(!w&&!isDataStructure(o))throw new TypeError("Cannot update within non-data-structure value in path ["+Array.from(i).slice(0,a).map(quoteString)+"]: "+o);var j=i[a],L=w?Z:get(o,j,Z),B=updateInDeeply(L===Z?s:isImmutable(L),L,i,a+1,u,_);return B===L?o:B===Z?remove(o,j):set(w?s?emptyMap():{}:o,j,B)}function removeIn(s,o){return updateIn(s,o,(function(){return Z}))}function deleteIn(s){return removeIn(this,s)}var Et="@@__IMMUTABLE_LIST__@@";function isList(s){return Boolean(s&&s[Et])}var wt=function(s){function List(o){var i=emptyList();if(null==o)return i;if(isList(o))return o;var a=s(o),u=a.size;return 0===u?i:(assertNotInfinite(u),u>0&&u=0&&s=s.size||o<0)return s.withMutations((function(s){o<0?setListBounds(s,o).set(0,i):setListBounds(s,0,o+1).set(o,i)}));o+=s._origin;var a=s._tail,u=s._root,_={value:!1};o>=getTailOffset(s._capacity)?a=updateVNode(a,s.__ownerID,0,o,i,_):u=updateVNode(u,s.__ownerID,s._level,o,i,_);if(!_.value)return s;if(s.__ownerID)return s._root=u,s._tail=a,s.__hash=void 0,s.__altered=!0,s;return makeList(s._origin,s._capacity,s._level,u,a)}(this,s,o)},List.prototype.remove=function remove(s){return this.has(s)?0===s?this.shift():s===this.size-1?this.pop():this.splice(s,1):this},List.prototype.insert=function insert(s,o){return this.splice(s,0,o)},List.prototype.clear=function clear(){return 0===this.size?this:this.__ownerID?(this.size=this._origin=this._capacity=0,this._level=5,this._root=this._tail=this.__hash=void 0,this.__altered=!0,this):emptyList()},List.prototype.push=function push(){var s=arguments,o=this.size;return this.withMutations((function(i){setListBounds(i,0,o+s.length);for(var a=0;a>>o&Y;if(a>=this.array.length)return new kt([],s);var u,_=0===a;if(o>0){var w=this.array[a];if((u=w&&w.removeBefore(s,o-5,i))===w&&_)return this}if(_&&!u)return this;var x=editableVNode(this,s);if(!_)for(var C=0;C>>o&Y;if(u>=this.array.length)return this;if(o>0){var _=this.array[u];if((a=_&&_.removeAfter(s,o-5,i))===_&&u===this.array.length-1)return this}var w=editableVNode(this,s);return w.array.splice(u+1),a&&(w.array[u]=a),w};var Ot={};function iterateList(s,o){var i=s._origin,a=s._capacity,u=getTailOffset(a),_=s._tail;return iterateNodeOrLeaf(s._root,s._level,0);function iterateNodeOrLeaf(s,w,x){return 0===w?function iterateLeaf(s,w){var x=w===u?_&&_.array:s&&s.array,C=w>i?0:i-w,j=a-w;j>V&&(j=V);return function(){if(C===j)return Ot;var s=o?--j:C++;return x&&x[s]}}(s,x):function iterateNode(s,u,_){var w,x=s&&s.array,C=_>i?0:i-_>>u,j=1+(a-_>>u);j>V&&(j=V);return function(){for(;;){if(w){var s=w();if(s!==Ot)return s;w=null}if(C===j)return Ot;var i=o?--j:C++;w=iterateNodeOrLeaf(x&&x[i],u-5,_+(i<>>i&Y,C=s&&x0){var j=s&&s.array[x],L=updateVNode(j,o,i-5,a,u,_);return L===j?s:((w=editableVNode(s,o)).array[x]=L,w)}return C&&s.array[x]===u?s:(_&&SetRef(_),w=editableVNode(s,o),void 0===u&&x===w.array.length-1?w.array.pop():w.array[x]=u,w)}function editableVNode(s,o){return o&&s&&o===s.ownerID?s:new kt(s?s.array.slice():[],o)}function listNodeFor(s,o){if(o>=getTailOffset(s._capacity))return s._tail;if(o<1<0;)i=i.array[o>>>a&Y],a-=5;return i}}function setListBounds(s,o,i){!function validateListBoundsRequest(s,o,i){var a=s._origin+(void 0===o?0:o),u=void 0===i?s._capacity:i<0?s._capacity+i:s._origin+i;if(Number.isFinite(u)&&u>At||Number.isFinite(a)&&a<-At||Number.isFinite(u)&&Number.isFinite(a)&&u-a>At)throw new RangeError("Invalid List size: a List cannot hold more than "+At+" (2 ** 30) values.")}(s,o,i),void 0!==o&&(o|=0),void 0!==i&&(i|=0);var a=s.__ownerID||new OwnerID,u=s._origin,_=s._capacity,w=u+o,x=void 0===i?_:i<0?_+i:u+i;if(w===u&&x===_)return s;if(w>=x)return s.clear();for(var C=s._level,j=s._root,L=0;w+L<0;)j=new kt(j&&j.array.length?[void 0,j]:[],a),L+=levelCapacity(C+=5);L&&(w+=L,u+=L,x+=L,_+=L);for(var B=getTailOffset(_),$=getTailOffset(x);$>=levelCapacity(C+5);)j=new kt(j&&j.array.length?[j]:[],a),C+=5;var z=s._tail,U=$B?new kt([],a):z;if(z&&$>B&&w<_&&z.array.length){for(var V=j=editableVNode(j,a),Z=C;Z>5;Z-=5){var ee=B>>>Z&Y;V=V.array[ee]=editableVNode(V.array[ee],a)}V.array[B>>>5&Y]=z}if(x<_&&(U=U&&U.removeAfter(a,0,x)),w>=$)w-=$,x-=$,C=5,j=void 0,U=U&&U.removeBefore(a,0,w);else if(w>u||$>>C&Y;if(ie!==$>>>C&Y)break;ie&&(L+=(1<u&&(j=j.removeBefore(a,C,w-L)),j&&$>>5<<5}var At=Math.pow(2,30);function levelCapacity(s){return s<31?1<=V&&w.size>=2*_.size?(a=(u=w.filter((function(s,o){return void 0!==s&&x!==o}))).toKeyedSeq().map((function(s){return s[0]})).flip().toMap(),s.__ownerID&&(a.__ownerID=u.__ownerID=s.__ownerID)):(a=_.remove(o),u=x===w.size-1?w.pop():w.set(x,void 0))}else if(C){if(i===w.get(x)[1])return s;a=_,u=w.set(x,[o,i])}else a=_.set(o,w.size),u=w.set(w.size,[o,i]);return s.__ownerID?(s.size=a.size,s._map=a,s._list=u,s.__hash=void 0,s.__altered=!0,s):makeOrderedMap(a,u)}jt.isOrderedMap=isOrderedMap,jt.prototype[ie]=!0,jt.prototype[U]=jt.prototype.remove;var Pt="@@__IMMUTABLE_STACK__@@";function isStack(s){return Boolean(s&&s[Pt])}var Tt=function(s){function Stack(s){return null==s?emptyStack():isStack(s)?s:emptyStack().pushAll(s)}return s&&(Stack.__proto__=s),Stack.prototype=Object.create(s&&s.prototype),Stack.prototype.constructor=Stack,Stack.of=function of(){return this(arguments)},Stack.prototype.toString=function toString(){return this.__toString("Stack [","]")},Stack.prototype.get=function get(s,o){var i=this._head;for(s=wrapIndex(this,s);i&&s--;)i=i.next;return i?i.value:o},Stack.prototype.peek=function peek(){return this._head&&this._head.value},Stack.prototype.push=function push(){var s=arguments;if(0===arguments.length)return this;for(var o=this.size+arguments.length,i=this._head,a=arguments.length-1;a>=0;a--)i={value:s[a],next:i};return this.__ownerID?(this.size=o,this._head=i,this.__hash=void 0,this.__altered=!0,this):makeStack(o,i)},Stack.prototype.pushAll=function pushAll(o){if(0===(o=s(o)).size)return this;if(0===this.size&&isStack(o))return o;assertNotInfinite(o.size);var i=this.size,a=this._head;return o.__iterate((function(s){i++,a={value:s,next:a}}),!0),this.__ownerID?(this.size=i,this._head=a,this.__hash=void 0,this.__altered=!0,this):makeStack(i,a)},Stack.prototype.pop=function pop(){return this.slice(1)},Stack.prototype.clear=function clear(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):emptyStack()},Stack.prototype.slice=function slice(o,i){if(wholeSlice(o,i,this.size))return this;var a=resolveBegin(o,this.size);if(resolveEnd(i,this.size)!==this.size)return s.prototype.slice.call(this,o,i);for(var u=this.size-a,_=this._head;a--;)_=_.next;return this.__ownerID?(this.size=u,this._head=_,this.__hash=void 0,this.__altered=!0,this):makeStack(u,_)},Stack.prototype.__ensureOwner=function __ensureOwner(s){return s===this.__ownerID?this:s?makeStack(this.size,this._head,s,this.__hash):0===this.size?emptyStack():(this.__ownerID=s,this.__altered=!1,this)},Stack.prototype.__iterate=function __iterate(s,o){var i=this;if(o)return new ye(this.toArray()).__iterate((function(o,a){return s(o,a,i)}),o);for(var a=0,u=this._head;u&&!1!==s(u.value,a++,this);)u=u.next;return a},Stack.prototype.__iterator=function __iterator(s,o){if(o)return new ye(this.toArray()).__iterator(s,o);var i=0,a=this._head;return new z((function(){if(a){var o=a.value;return a=a.next,iteratorValue(s,i++,o)}return{value:void 0,done:!0}}))},Stack}(C);Tt.isStack=isStack;var It,Nt=Tt.prototype;function makeStack(s,o,i,a){var u=Object.create(Nt);return u.size=s,u._head=o,u.__ownerID=i,u.__hash=a,u.__altered=!1,u}function emptyStack(){return It||(It=makeStack(0))}function reduce(s,o,i,a,u,_){return assertNotInfinite(s.size),s.__iterate((function(s,_,w){u?(u=!1,i=s):i=o.call(a,i,s,_,w)}),_),i}function keyMapper(s,o){return o}function entryMapper(s,o){return[o,s]}function not(s){return function(){for(var o=[],i=arguments.length;i--;)o[i]=arguments[i];return!s.apply(this,o)}}function neg(s){return function(){for(var o=[],i=arguments.length;i--;)o[i]=arguments[i];return-s.apply(this,o)}}function defaultNegComparator(s,o){return so?-1:0}function deepEqual(s,o){if(s===o)return!0;if(!isCollection(o)||void 0!==s.size&&void 0!==o.size&&s.size!==o.size||void 0!==s.__hash&&void 0!==o.__hash&&s.__hash!==o.__hash||isKeyed(s)!==isKeyed(o)||isIndexed(s)!==isIndexed(o)||isOrdered(s)!==isOrdered(o))return!1;if(0===s.size&&0===o.size)return!0;var i=!isAssociative(s);if(isOrdered(s)){var a=s.entries();return o.every((function(s,o){var u=a.next().value;return u&&is(u[1],s)&&(i||is(u[0],o))}))&&a.next().done}var u=!1;if(void 0===s.size)if(void 0===o.size)"function"==typeof s.cacheResult&&s.cacheResult();else{u=!0;var _=s;s=o,o=_}var w=!0,x=o.__iterate((function(o,a){if(i?!s.has(o):u?!is(o,s.get(a,Z)):!is(s.get(a,Z),o))return w=!1,!1}));return w&&s.size===x}Nt[Pt]=!0,Nt.shift=Nt.pop,Nt.unshift=Nt.push,Nt.unshiftAll=Nt.pushAll,Nt.withMutations=withMutations,Nt.wasAltered=wasAltered,Nt.asImmutable=asImmutable,Nt["@@transducer/init"]=Nt.asMutable=asMutable,Nt["@@transducer/step"]=function(s,o){return s.unshift(o)},Nt["@@transducer/result"]=function(s){return s.asImmutable()};var Mt,Rt=function(s){function Range(s,o,i){if(void 0===i&&(i=1),!(this instanceof Range))return new Range(s,o,i);if(invariant(0!==i,"Cannot step a Range by 0"),invariant(void 0!==s,"You must define a start value when using Range"),invariant(void 0!==o,"You must define an end value when using Range"),i=Math.abs(i),o=0&&o=0&&i>>-15,461845907),o=we(o<<13|o>>>-13,5),o=o+3864292196^s,o=we(o^o>>>16,2246822507),o=we(o^o>>>13,3266489909),o=smi(o^o>>>16),o}(s.size,a)}function hashMerge(s,o){return s^o+2654435769+(s<<6)+(s>>2)}function mixin(s,o){var keyCopier=function(i){s.prototype[i]=o[i]};return Object.keys(o).forEach(keyCopier),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(o).forEach(keyCopier),s}Bt[Dt]=!0,Bt[U]=Bt.remove,Bt.merge=Bt.concat=Bt.union,Bt.withMutations=withMutations,Bt.asImmutable=asImmutable,Bt["@@transducer/init"]=Bt.asMutable=asMutable,Bt["@@transducer/step"]=function(s,o){return s.add(o)},Bt["@@transducer/result"]=function(s){return s.asImmutable()},Bt.__empty=emptySet,Bt.__make=makeSet,w.Iterator=z,mixin(w,{toArray:function toArray(){assertNotInfinite(this.size);var s=new Array(this.size||0),o=isKeyed(this),i=0;return this.__iterate((function(a,u){s[i++]=o?[u,a]:a})),s},toIndexedSeq:function toIndexedSeq(){return new rt(this)},toJS:function toJS$1(){return toJS(this)},toKeyedSeq:function toKeyedSeq(){return new tt(this,!0)},toMap:function toMap(){return ct(this.toKeyedSeq())},toObject,toOrderedMap:function toOrderedMap(){return jt(this.toKeyedSeq())},toOrderedSet:function toOrderedSet(){return Vt(isKeyed(this)?this.valueSeq():this)},toSet:function toSet(){return Ft(isKeyed(this)?this.valueSeq():this)},toSetSeq:function toSetSeq(){return new nt(this)},toSeq:function toSeq(){return isIndexed(this)?this.toIndexedSeq():isKeyed(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function toStack(){return Tt(isKeyed(this)?this.valueSeq():this)},toList:function toList(){return wt(isKeyed(this)?this.valueSeq():this)},toString:function toString(){return"[Collection]"},__toString:function __toString(s,o){return 0===this.size?s+o:s+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+o},concat:function concat(){for(var s=[],o=arguments.length;o--;)s[o]=arguments[o];return reify(this,function concatFactory(s,o){var i=isKeyed(s),a=[s].concat(o).map((function(s){return isCollection(s)?i&&(s=x(s)):s=i?keyedSeqFromValue(s):indexedSeqFromValue(Array.isArray(s)?s:[s]),s})).filter((function(s){return 0!==s.size}));if(0===a.length)return s;if(1===a.length){var u=a[0];if(u===s||i&&isKeyed(u)||isIndexed(s)&&isIndexed(u))return u}return new ot(a)}(this,s))},includes:function includes(s){return this.some((function(o){return is(o,s)}))},entries:function entries(){return this.__iterator(2)},every:function every(s,o){assertNotInfinite(this.size);var i=!0;return this.__iterate((function(a,u,_){if(!s.call(o,a,u,_))return i=!1,!1})),i},filter:function filter(s,o){return reify(this,filterFactory(this,s,o,!0))},partition:function partition(s,o){return function partitionFactory(s,o,i){var a=isKeyed(s),u=[[],[]];s.__iterate((function(_,w){u[o.call(i,_,w,s)?1:0].push(a?[w,_]:_)}));var _=collectionClass(s);return u.map((function(o){return reify(s,_(o))}))}(this,s,o)},find:function find(s,o,i){var a=this.findEntry(s,o);return a?a[1]:i},forEach:function forEach(s,o){return assertNotInfinite(this.size),this.__iterate(o?s.bind(o):s)},join:function join(s){assertNotInfinite(this.size),s=void 0!==s?""+s:",";var o="",i=!0;return this.__iterate((function(a){i?i=!1:o+=s,o+=null!=a?a.toString():""})),o},keys:function keys(){return this.__iterator(0)},map:function map(s,o){return reify(this,mapFactory(this,s,o))},reduce:function reduce$1(s,o,i){return reduce(this,s,o,i,arguments.length<2,!1)},reduceRight:function reduceRight(s,o,i){return reduce(this,s,o,i,arguments.length<2,!0)},reverse:function reverse(){return reify(this,reverseFactory(this,!0))},slice:function slice(s,o){return reify(this,sliceFactory(this,s,o,!0))},some:function some(s,o){assertNotInfinite(this.size);var i=!1;return this.__iterate((function(a,u,_){if(s.call(o,a,u,_))return i=!0,!1})),i},sort:function sort(s){return reify(this,sortFactory(this,s))},values:function values(){return this.__iterator(1)},butLast:function butLast(){return this.slice(0,-1)},isEmpty:function isEmpty(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function count(s,o){return ensureSize(s?this.toSeq().filter(s,o):this)},countBy:function countBy(s,o){return function countByFactory(s,o,i){var a=ct().asMutable();return s.__iterate((function(u,_){a.update(o.call(i,u,_,s),0,(function(s){return s+1}))})),a.asImmutable()}(this,s,o)},equals:function equals(s){return deepEqual(this,s)},entrySeq:function entrySeq(){var s=this;if(s._cache)return new ye(s._cache);var o=s.toSeq().map(entryMapper).toIndexedSeq();return o.fromEntrySeq=function(){return s.toSeq()},o},filterNot:function filterNot(s,o){return this.filter(not(s),o)},findEntry:function findEntry(s,o,i){var a=i;return this.__iterate((function(i,u,_){if(s.call(o,i,u,_))return a=[u,i],!1})),a},findKey:function findKey(s,o){var i=this.findEntry(s,o);return i&&i[0]},findLast:function findLast(s,o,i){return this.toKeyedSeq().reverse().find(s,o,i)},findLastEntry:function findLastEntry(s,o,i){return this.toKeyedSeq().reverse().findEntry(s,o,i)},findLastKey:function findLastKey(s,o){return this.toKeyedSeq().reverse().findKey(s,o)},first:function first(s){return this.find(returnTrue,null,s)},flatMap:function flatMap(s,o){return reify(this,function flatMapFactory(s,o,i){var a=collectionClass(s);return s.toSeq().map((function(u,_){return a(o.call(i,u,_,s))})).flatten(!0)}(this,s,o))},flatten:function flatten(s){return reify(this,flattenFactory(this,s,!0))},fromEntrySeq:function fromEntrySeq(){return new st(this)},get:function get(s,o){return this.find((function(o,i){return is(i,s)}),void 0,o)},getIn,groupBy:function groupBy(s,o){return function groupByFactory(s,o,i){var a=isKeyed(s),u=(isOrdered(s)?jt():ct()).asMutable();s.__iterate((function(_,w){u.update(o.call(i,_,w,s),(function(s){return(s=s||[]).push(a?[w,_]:_),s}))}));var _=collectionClass(s);return u.map((function(o){return reify(s,_(o))})).asImmutable()}(this,s,o)},has:function has(s){return this.get(s,Z)!==Z},hasIn:function hasIn(s){return hasIn$1(this,s)},isSubset:function isSubset(s){return s="function"==typeof s.includes?s:w(s),this.every((function(o){return s.includes(o)}))},isSuperset:function isSuperset(s){return(s="function"==typeof s.isSubset?s:w(s)).isSubset(this)},keyOf:function keyOf(s){return this.findKey((function(o){return is(o,s)}))},keySeq:function keySeq(){return this.toSeq().map(keyMapper).toIndexedSeq()},last:function last(s){return this.toSeq().reverse().first(s)},lastKeyOf:function lastKeyOf(s){return this.toKeyedSeq().reverse().keyOf(s)},max:function max(s){return maxFactory(this,s)},maxBy:function maxBy(s,o){return maxFactory(this,o,s)},min:function min(s){return maxFactory(this,s?neg(s):defaultNegComparator)},minBy:function minBy(s,o){return maxFactory(this,o?neg(o):defaultNegComparator,s)},rest:function rest(){return this.slice(1)},skip:function skip(s){return 0===s?this:this.slice(Math.max(0,s))},skipLast:function skipLast(s){return 0===s?this:this.slice(0,-Math.max(0,s))},skipWhile:function skipWhile(s,o){return reify(this,skipWhileFactory(this,s,o,!0))},skipUntil:function skipUntil(s,o){return this.skipWhile(not(s),o)},sortBy:function sortBy(s,o){return reify(this,sortFactory(this,o,s))},take:function take(s){return this.slice(0,Math.max(0,s))},takeLast:function takeLast(s){return this.slice(-Math.max(0,s))},takeWhile:function takeWhile(s,o){return reify(this,function takeWhileFactory(s,o,i){var a=makeSequence(s);return a.__iterateUncached=function(a,u){var _=this;if(u)return this.cacheResult().__iterate(a,u);var w=0;return s.__iterate((function(s,u,x){return o.call(i,s,u,x)&&++w&&a(s,u,_)})),w},a.__iteratorUncached=function(a,u){var _=this;if(u)return this.cacheResult().__iterator(a,u);var w=s.__iterator(2,u),x=!0;return new z((function(){if(!x)return{value:void 0,done:!0};var s=w.next();if(s.done)return s;var u=s.value,C=u[0],j=u[1];return o.call(i,j,C,_)?2===a?s:iteratorValue(a,C,j,s):(x=!1,{value:void 0,done:!0})}))},a}(this,s,o))},takeUntil:function takeUntil(s,o){return this.takeWhile(not(s),o)},update:function update(s){return s(this)},valueSeq:function valueSeq(){return this.toIndexedSeq()},hashCode:function hashCode(){return this.__hash||(this.__hash=hashCollection(this))}});var qt=w.prototype;qt[_]=!0,qt[$]=qt.values,qt.toJSON=qt.toArray,qt.__toStringMapper=quoteString,qt.inspect=qt.toSource=function(){return this.toString()},qt.chain=qt.flatMap,qt.contains=qt.includes,mixin(x,{flip:function flip(){return reify(this,flipFactory(this))},mapEntries:function mapEntries(s,o){var i=this,a=0;return reify(this,this.toSeq().map((function(u,_){return s.call(o,[_,u],a++,i)})).fromEntrySeq())},mapKeys:function mapKeys(s,o){var i=this;return reify(this,this.toSeq().flip().map((function(a,u){return s.call(o,a,u,i)})).flip())}});var $t=x.prototype;$t[u]=!0,$t[$]=qt.entries,$t.toJSON=toObject,$t.__toStringMapper=function(s,o){return quoteString(o)+": "+quoteString(s)},mixin(C,{toKeyedSeq:function toKeyedSeq(){return new tt(this,!1)},filter:function filter(s,o){return reify(this,filterFactory(this,s,o,!1))},findIndex:function findIndex(s,o){var i=this.findEntry(s,o);return i?i[0]:-1},indexOf:function indexOf(s){var o=this.keyOf(s);return void 0===o?-1:o},lastIndexOf:function lastIndexOf(s){var o=this.lastKeyOf(s);return void 0===o?-1:o},reverse:function reverse(){return reify(this,reverseFactory(this,!1))},slice:function slice(s,o){return reify(this,sliceFactory(this,s,o,!1))},splice:function splice(s,o){var i=arguments.length;if(o=Math.max(o||0,0),0===i||2===i&&!o)return this;s=resolveBegin(s,s<0?this.count():this.size);var a=this.slice(0,s);return reify(this,1===i?a:a.concat(arrCopy(arguments,2),this.slice(s+o)))},findLastIndex:function findLastIndex(s,o){var i=this.findLastEntry(s,o);return i?i[0]:-1},first:function first(s){return this.get(0,s)},flatten:function flatten(s){return reify(this,flattenFactory(this,s,!1))},get:function get(s,o){return(s=wrapIndex(this,s))<0||this.size===1/0||void 0!==this.size&&s>this.size?o:this.find((function(o,i){return i===s}),void 0,o)},has:function has(s){return(s=wrapIndex(this,s))>=0&&(void 0!==this.size?this.size===1/0||s2?[]:void 0,{"":s})}function fromJSWith(s,o,i,a,u,_){if("string"!=typeof i&&!isImmutable(i)&&(isArrayLike(i)||hasIterator(i)||isPlainObject(i))){if(~s.indexOf(i))throw new TypeError("Cannot convert circular structure to Immutable");s.push(i),u&&""!==a&&u.push(a);var w=o.call(_,a,le(i).map((function(a,_){return fromJSWith(s,o,a,_,u,i)})),u&&u.slice());return s.pop(),u&&u.pop(),w}return i}function defaultConverter(s,o){return isIndexed(o)?o.toList():isKeyed(o)?o.toMap():o.toSet()}var Qt="5.1.9",Zt=w},56698(s){"function"==typeof Object.create?s.exports=function inherits(s,o){o&&(s.super_=o,s.prototype=Object.create(o.prototype,{constructor:{value:s,enumerable:!1,writable:!0,configurable:!0}}))}:s.exports=function inherits(s,o){if(o){s.super_=o;var TempCtor=function(){};TempCtor.prototype=o.prototype,s.prototype=new TempCtor,s.prototype.constructor=s}}},69600(s){"use strict";var o,i,a=Function.prototype.toString,u="object"==typeof Reflect&&null!==Reflect&&Reflect.apply;if("function"==typeof u&&"function"==typeof Object.defineProperty)try{o=Object.defineProperty({},"length",{get:function(){throw i}}),i={},u((function(){throw 42}),null,o)}catch(s){s!==i&&(u=null)}else u=null;var _=/^\s*class\b/,w=function isES6ClassFunction(s){try{var o=a.call(s);return _.test(o)}catch(s){return!1}},x=function tryFunctionToStr(s){try{return!w(s)&&(a.call(s),!0)}catch(s){return!1}},C=Object.prototype.toString,j="function"==typeof Symbol&&!!Symbol.toStringTag,L=!(0 in[,]),B=function isDocumentDotAll(){return!1};if("object"==typeof document){var $=document.all;C.call($)===C.call(document.all)&&(B=function isDocumentDotAll(s){if((L||!s)&&(void 0===s||"object"==typeof s))try{var o=C.call(s);return("[object HTMLAllCollection]"===o||"[object HTML document.all class]"===o||"[object HTMLCollection]"===o||"[object Object]"===o)&&null==s("")}catch(s){}return!1})}s.exports=u?function isCallable(s){if(B(s))return!0;if(!s)return!1;if("function"!=typeof s&&"object"!=typeof s)return!1;try{u(s,null,o)}catch(s){if(s!==i)return!1}return!w(s)&&x(s)}:function isCallable(s){if(B(s))return!0;if(!s)return!1;if("function"!=typeof s&&"object"!=typeof s)return!1;if(j)return x(s);if(w(s))return!1;var o=C.call(s);return!("[object Function]"!==o&&"[object GeneratorFunction]"!==o&&!/^\[object HTML/.test(o))&&x(s)}},35680(s,o,i){"use strict";var a=i(25767);s.exports=function isTypedArray(s){return!!a(s)}},64634(s){var o={}.toString;s.exports=Array.isArray||function(s){return"[object Array]"==o.call(s)}},5419(s){s.exports=function(s,o,i,a){var u=new Blob(void 0!==a?[a,s]:[s],{type:i||"application/octet-stream"});if(void 0!==window.navigator.msSaveBlob)window.navigator.msSaveBlob(u,o);else{var _=window.URL&&window.URL.createObjectURL?window.URL.createObjectURL(u):window.webkitURL.createObjectURL(u),w=document.createElement("a");w.style.display="none",w.href=_,w.setAttribute("download",o),void 0===w.download&&w.setAttribute("target","_blank"),document.body.appendChild(w),w.click(),setTimeout((function(){document.body.removeChild(w),window.URL.revokeObjectURL(_)}),200)}}},20181(s,o,i){var a=/^\s+|\s+$/g,u=/^[-+]0x[0-9a-f]+$/i,_=/^0b[01]+$/i,w=/^0o[0-7]+$/i,x=parseInt,C="object"==typeof i.g&&i.g&&i.g.Object===Object&&i.g,j="object"==typeof self&&self&&self.Object===Object&&self,L=C||j||Function("return this")(),B=Object.prototype.toString,$=Math.max,z=Math.min,now=function(){return L.Date.now()};function isObject(s){var o=typeof s;return!!s&&("object"==o||"function"==o)}function toNumber(s){if("number"==typeof s)return s;if(function isSymbol(s){return"symbol"==typeof s||function isObjectLike(s){return!!s&&"object"==typeof s}(s)&&"[object Symbol]"==B.call(s)}(s))return NaN;if(isObject(s)){var o="function"==typeof s.valueOf?s.valueOf():s;s=isObject(o)?o+"":o}if("string"!=typeof s)return 0===s?s:+s;s=s.replace(a,"");var i=_.test(s);return i||w.test(s)?x(s.slice(2),i?2:8):u.test(s)?NaN:+s}s.exports=function debounce(s,o,i){var a,u,_,w,x,C,j=0,L=!1,B=!1,U=!0;if("function"!=typeof s)throw new TypeError("Expected a function");function invokeFunc(o){var i=a,_=u;return a=u=void 0,j=o,w=s.apply(_,i)}function shouldInvoke(s){var i=s-C;return void 0===C||i>=o||i<0||B&&s-j>=_}function timerExpired(){var s=now();if(shouldInvoke(s))return trailingEdge(s);x=setTimeout(timerExpired,function remainingWait(s){var i=o-(s-C);return B?z(i,_-(s-j)):i}(s))}function trailingEdge(s){return x=void 0,U&&a?invokeFunc(s):(a=u=void 0,w)}function debounced(){var s=now(),i=shouldInvoke(s);if(a=arguments,u=this,C=s,i){if(void 0===x)return function leadingEdge(s){return j=s,x=setTimeout(timerExpired,o),L?invokeFunc(s):w}(C);if(B)return x=setTimeout(timerExpired,o),invokeFunc(C)}return void 0===x&&(x=setTimeout(timerExpired,o)),w}return o=toNumber(o)||0,isObject(i)&&(L=!!i.leading,_=(B="maxWait"in i)?$(toNumber(i.maxWait)||0,o):_,U="trailing"in i?!!i.trailing:U),debounced.cancel=function cancel(){void 0!==x&&clearTimeout(x),j=0,a=C=u=x=void 0},debounced.flush=function flush(){return void 0===x?w:trailingEdge(now())},debounced}},55580(s,o,i){var a=i(56110)(i(9325),"DataView");s.exports=a},21549(s,o,i){var a=i(22032),u=i(63862),_=i(66721),w=i(12749),x=i(35749);function Hash(s){var o=-1,i=null==s?0:s.length;for(this.clear();++o-1}},70695(s,o,i){var a=i(78096),u=i(72428),_=i(56449),w=i(3656),x=i(30361),C=i(37167),j=Object.prototype.hasOwnProperty;s.exports=function arrayLikeKeys(s,o){var i=_(s),L=!i&&u(s),B=!i&&!L&&w(s),$=!i&&!L&&!B&&C(s),z=i||L||B||$,U=z?a(s.length,String):[],V=U.length;for(var Y in s)!o&&!j.call(s,Y)||z&&("length"==Y||B&&("offset"==Y||"parent"==Y)||$&&("buffer"==Y||"byteLength"==Y||"byteOffset"==Y)||x(Y,V))||U.push(Y);return U}},34932(s){s.exports=function arrayMap(s,o){for(var i=-1,a=null==s?0:s.length,u=Array(a);++i0&&i(j)?o>1?baseFlatten(j,o-1,i,_,w):a(w,j):_||(w[w.length]=j)}return w}},86649(s,o,i){var a=i(83221)();s.exports=a},30641(s,o,i){var a=i(86649),u=i(95950);s.exports=function baseForOwn(s,o){return s&&a(s,o,u)}},47422(s,o,i){var a=i(31769),u=i(77797);s.exports=function baseGet(s,o){for(var i=0,_=(o=a(o,s)).length;null!=s&&i<_;)s=s[u(o[i++])];return i&&i==_?s:void 0}},82199(s,o,i){var a=i(14528),u=i(56449);s.exports=function baseGetAllKeys(s,o,i){var _=o(s);return u(s)?_:a(_,i(s))}},72552(s,o,i){var a=i(51873),u=i(659),_=i(59350),w=a?a.toStringTag:void 0;s.exports=function baseGetTag(s){return null==s?void 0===s?"[object Undefined]":"[object Null]":w&&w in Object(s)?u(s):_(s)}},20426(s){var o=Object.prototype.hasOwnProperty;s.exports=function baseHas(s,i){return null!=s&&o.call(s,i)}},28077(s){s.exports=function baseHasIn(s,o){return null!=s&&o in Object(s)}},96131(s,o,i){var a=i(2523),u=i(85463),_=i(76959);s.exports=function baseIndexOf(s,o,i){return o==o?_(s,o,i):a(s,u,i)}},27534(s,o,i){var a=i(72552),u=i(40346);s.exports=function baseIsArguments(s){return u(s)&&"[object Arguments]"==a(s)}},60270(s,o,i){var a=i(87068),u=i(40346);s.exports=function baseIsEqual(s,o,i,_,w){return s===o||(null==s||null==o||!u(s)&&!u(o)?s!=s&&o!=o:a(s,o,i,_,baseIsEqual,w))}},87068(s,o,i){var a=i(37217),u=i(25911),_=i(21986),w=i(50689),x=i(5861),C=i(56449),j=i(3656),L=i(37167),B="[object Arguments]",$="[object Array]",z="[object Object]",U=Object.prototype.hasOwnProperty;s.exports=function baseIsEqualDeep(s,o,i,V,Y,Z){var ee=C(s),ie=C(o),ae=ee?$:x(s),ce=ie?$:x(o),le=(ae=ae==B?z:ae)==z,pe=(ce=ce==B?z:ce)==z,de=ae==ce;if(de&&j(s)){if(!j(o))return!1;ee=!0,le=!1}if(de&&!le)return Z||(Z=new a),ee||L(s)?u(s,o,i,V,Y,Z):_(s,o,ae,i,V,Y,Z);if(!(1&i)){var fe=le&&U.call(s,"__wrapped__"),ye=pe&&U.call(o,"__wrapped__");if(fe||ye){var be=fe?s.value():s,Se=ye?o.value():o;return Z||(Z=new a),Y(be,Se,i,V,Z)}}return!!de&&(Z||(Z=new a),w(s,o,i,V,Y,Z))}},29172(s,o,i){var a=i(5861),u=i(40346);s.exports=function baseIsMap(s){return u(s)&&"[object Map]"==a(s)}},41799(s,o,i){var a=i(37217),u=i(60270);s.exports=function baseIsMatch(s,o,i,_){var w=i.length,x=w,C=!_;if(null==s)return!x;for(s=Object(s);w--;){var j=i[w];if(C&&j[2]?j[1]!==s[j[0]]:!(j[0]in s))return!1}for(;++wu?0:u+o),(i=i>u?u:i)<0&&(i+=u),u=o>i?0:i-o>>>0,o>>>=0;for(var _=Array(u);++a=u?s:a(s,o,i)}},49653(s,o,i){var a=i(37828);s.exports=function cloneArrayBuffer(s){var o=new s.constructor(s.byteLength);return new a(o).set(new a(s)),o}},93290(s,o,i){s=i.nmd(s);var a=i(9325),u=o&&!o.nodeType&&o,_=u&&s&&!s.nodeType&&s,w=_&&_.exports===u?a.Buffer:void 0,x=w?w.allocUnsafe:void 0;s.exports=function cloneBuffer(s,o){if(o)return s.slice();var i=s.length,a=x?x(i):new s.constructor(i);return s.copy(a),a}},76169(s,o,i){var a=i(49653);s.exports=function cloneDataView(s,o){var i=o?a(s.buffer):s.buffer;return new s.constructor(i,s.byteOffset,s.byteLength)}},73201(s){var o=/\w*$/;s.exports=function cloneRegExp(s){var i=new s.constructor(s.source,o.exec(s));return i.lastIndex=s.lastIndex,i}},93736(s,o,i){var a=i(51873),u=a?a.prototype:void 0,_=u?u.valueOf:void 0;s.exports=function cloneSymbol(s){return _?Object(_.call(s)):{}}},71961(s,o,i){var a=i(49653);s.exports=function cloneTypedArray(s,o){var i=o?a(s.buffer):s.buffer;return new s.constructor(i,s.byteOffset,s.length)}},91596(s){var o=Math.max;s.exports=function composeArgs(s,i,a,u){for(var _=-1,w=s.length,x=a.length,C=-1,j=i.length,L=o(w-x,0),B=Array(j+L),$=!u;++C1?i[_-1]:void 0,x=_>2?i[2]:void 0;for(w=s.length>3&&"function"==typeof w?(_--,w):void 0,x&&u(i[0],i[1],x)&&(w=_<3?void 0:w,_=1),o=Object(o);++a<_;){var C=i[a];C&&s(o,C,a,w)}return o}))}},38329(s,o,i){var a=i(64894);s.exports=function createBaseEach(s,o){return function(i,u){if(null==i)return i;if(!a(i))return s(i,u);for(var _=i.length,w=o?_:-1,x=Object(i);(o?w--:++w<_)&&!1!==u(x[w],w,x););return i}}},83221(s){s.exports=function createBaseFor(s){return function(o,i,a){for(var u=-1,_=Object(o),w=a(o),x=w.length;x--;){var C=w[s?x:++u];if(!1===i(_[C],C,_))break}return o}}},11842(s,o,i){var a=i(82819),u=i(9325);s.exports=function createBind(s,o,i){var _=1&o,w=a(s);return function wrapper(){return(this&&this!==u&&this instanceof wrapper?w:s).apply(_?i:this,arguments)}}},12507(s,o,i){var a=i(28754),u=i(49698),_=i(63912),w=i(13222);s.exports=function createCaseFirst(s){return function(o){o=w(o);var i=u(o)?_(o):void 0,x=i?i[0]:o.charAt(0),C=i?a(i,1).join(""):o.slice(1);return x[s]()+C}}},45539(s,o,i){var a=i(40882),u=i(50828),_=i(66645),w=RegExp("['’]","g");s.exports=function createCompounder(s){return function(o){return a(_(u(o).replace(w,"")),s,"")}}},82819(s,o,i){var a=i(39344),u=i(23805);s.exports=function createCtor(s){return function(){var o=arguments;switch(o.length){case 0:return new s;case 1:return new s(o[0]);case 2:return new s(o[0],o[1]);case 3:return new s(o[0],o[1],o[2]);case 4:return new s(o[0],o[1],o[2],o[3]);case 5:return new s(o[0],o[1],o[2],o[3],o[4]);case 6:return new s(o[0],o[1],o[2],o[3],o[4],o[5]);case 7:return new s(o[0],o[1],o[2],o[3],o[4],o[5],o[6])}var i=a(s.prototype),_=s.apply(i,o);return u(_)?_:i}}},77078(s,o,i){var a=i(91033),u=i(82819),_=i(37471),w=i(18073),x=i(11287),C=i(36306),j=i(9325);s.exports=function createCurry(s,o,i){var L=u(s);return function wrapper(){for(var u=arguments.length,B=Array(u),$=u,z=x(wrapper);$--;)B[$]=arguments[$];var U=u<3&&B[0]!==z&&B[u-1]!==z?[]:C(B,z);return(u-=U.length)-1?x[C?o[j]:j]:void 0}}},37471(s,o,i){var a=i(91596),u=i(53320),_=i(58523),w=i(82819),x=i(18073),C=i(11287),j=i(68294),L=i(36306),B=i(9325);s.exports=function createHybrid(s,o,i,$,z,U,V,Y,Z,ee){var ie=128&o,ae=1&o,ce=2&o,le=24&o,pe=512&o,de=ce?void 0:w(s);return function wrapper(){for(var fe=arguments.length,ye=Array(fe),be=fe;be--;)ye[be]=arguments[be];if(le)var Se=C(wrapper),_e=_(ye,Se);if($&&(ye=a(ye,$,z,le)),U&&(ye=u(ye,U,V,le)),fe-=_e,le&&fe1&&ye.reverse(),ie&&ZL))return!1;var $=C.get(s),z=C.get(o);if($&&z)return $==o&&z==s;var U=-1,V=!0,Y=2&i?new a:void 0;for(C.set(s,o),C.set(o,s);++U1?"& ":"")+i[u],i=i.join(a>2?", ":" "),s.replace(o,"{\n/* [wrapped with "+i+"] */\n")}},45891(s,o,i){var a=i(51873),u=i(72428),_=i(56449),w=a?a.isConcatSpreadable:void 0;s.exports=function isFlattenable(s){return _(s)||u(s)||!!(w&&s&&s[w])}},30361(s){var o=/^(?:0|[1-9]\d*)$/;s.exports=function isIndex(s,i){var a=typeof s;return!!(i=null==i?9007199254740991:i)&&("number"==a||"symbol"!=a&&o.test(s))&&s>-1&&s%1==0&&s-1}},31175(s,o,i){var a=i(26025);s.exports=function listCacheSet(s,o){var i=this.__data__,u=a(i,s);return u<0?(++this.size,i.push([s,o])):i[u][1]=o,this}},63040(s,o,i){var a=i(21549),u=i(80079),_=i(68223);s.exports=function mapCacheClear(){this.size=0,this.__data__={hash:new a,map:new(_||u),string:new a}}},17670(s,o,i){var a=i(12651);s.exports=function mapCacheDelete(s){var o=a(this,s).delete(s);return this.size-=o?1:0,o}},90289(s,o,i){var a=i(12651);s.exports=function mapCacheGet(s){return a(this,s).get(s)}},4509(s,o,i){var a=i(12651);s.exports=function mapCacheHas(s){return a(this,s).has(s)}},72949(s,o,i){var a=i(12651);s.exports=function mapCacheSet(s,o){var i=a(this,s),u=i.size;return i.set(s,o),this.size+=i.size==u?0:1,this}},20317(s){s.exports=function mapToArray(s){var o=-1,i=Array(s.size);return s.forEach((function(s,a){i[++o]=[a,s]})),i}},67197(s){s.exports=function matchesStrictComparable(s,o){return function(i){return null!=i&&(i[s]===o&&(void 0!==o||s in Object(i)))}}},62224(s,o,i){var a=i(50104);s.exports=function memoizeCapped(s){var o=a(s,(function(s){return 500===i.size&&i.clear(),s})),i=o.cache;return o}},3209(s,o,i){var a=i(91596),u=i(53320),_=i(36306),w="__lodash_placeholder__",x=128,C=Math.min;s.exports=function mergeData(s,o){var i=s[1],j=o[1],L=i|j,B=L<131,$=j==x&&8==i||j==x&&256==i&&s[7].length<=o[8]||384==j&&o[7].length<=o[8]&&8==i;if(!B&&!$)return s;1&j&&(s[2]=o[2],L|=1&i?0:4);var z=o[3];if(z){var U=s[3];s[3]=U?a(U,z,o[4]):z,s[4]=U?_(s[3],w):o[4]}return(z=o[5])&&(U=s[5],s[5]=U?u(U,z,o[6]):z,s[6]=U?_(s[5],w):o[6]),(z=o[7])&&(s[7]=z),j&x&&(s[8]=null==s[8]?o[8]:C(s[8],o[8])),null==s[9]&&(s[9]=o[9]),s[0]=o[0],s[1]=L,s}},48152(s,o,i){var a=i(28303),u=a&&new a;s.exports=u},81042(s,o,i){var a=i(56110)(Object,"create");s.exports=a},3650(s,o,i){var a=i(74335)(Object.keys,Object);s.exports=a},90181(s){s.exports=function nativeKeysIn(s){var o=[];if(null!=s)for(var i in Object(s))o.push(i);return o}},86009(s,o,i){s=i.nmd(s);var a=i(34840),u=o&&!o.nodeType&&o,_=u&&s&&!s.nodeType&&s,w=_&&_.exports===u&&a.process,x=function(){try{var s=_&&_.require&&_.require("util").types;return s||w&&w.binding&&w.binding("util")}catch(s){}}();s.exports=x},59350(s){var o=Object.prototype.toString;s.exports=function objectToString(s){return o.call(s)}},74335(s){s.exports=function overArg(s,o){return function(i){return s(o(i))}}},56757(s,o,i){var a=i(91033),u=Math.max;s.exports=function overRest(s,o,i){return o=u(void 0===o?s.length-1:o,0),function(){for(var _=arguments,w=-1,x=u(_.length-o,0),C=Array(x);++w0){if(++i>=800)return arguments[0]}else i=0;return s.apply(void 0,arguments)}}},51420(s,o,i){var a=i(80079);s.exports=function stackClear(){this.__data__=new a,this.size=0}},90938(s){s.exports=function stackDelete(s){var o=this.__data__,i=o.delete(s);return this.size=o.size,i}},63605(s){s.exports=function stackGet(s){return this.__data__.get(s)}},29817(s){s.exports=function stackHas(s){return this.__data__.has(s)}},80945(s,o,i){var a=i(80079),u=i(68223),_=i(53661);s.exports=function stackSet(s,o){var i=this.__data__;if(i instanceof a){var w=i.__data__;if(!u||w.length<199)return w.push([s,o]),this.size=++i.size,this;i=this.__data__=new _(w)}return i.set(s,o),this.size=i.size,this}},76959(s){s.exports=function strictIndexOf(s,o,i){for(var a=i-1,u=s.length;++a=o||i<0||Y&&s-U>=L}function timerExpired(){var s=u();if(shouldInvoke(s))return trailingEdge(s);$=setTimeout(timerExpired,function remainingWait(s){var i=o-(s-z);return Y?x(i,L-(s-U)):i}(s))}function trailingEdge(s){return $=void 0,Z&&C?invokeFunc(s):(C=j=void 0,B)}function debounced(){var s=u(),i=shouldInvoke(s);if(C=arguments,j=this,z=s,i){if(void 0===$)return function leadingEdge(s){return U=s,$=setTimeout(timerExpired,o),V?invokeFunc(s):B}(z);if(Y)return clearTimeout($),$=setTimeout(timerExpired,o),invokeFunc(z)}return void 0===$&&($=setTimeout(timerExpired,o)),B}return o=_(o)||0,a(i)&&(V=!!i.leading,L=(Y="maxWait"in i)?w(_(i.maxWait)||0,o):L,Z="trailing"in i?!!i.trailing:Z),debounced.cancel=function cancel(){void 0!==$&&clearTimeout($),U=0,C=z=j=$=void 0},debounced.flush=function flush(){return void 0===$?B:trailingEdge(u())},debounced}},50828(s,o,i){var a=i(24647),u=i(13222),_=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,w=RegExp("[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]","g");s.exports=function deburr(s){return(s=u(s))&&s.replace(_,a).replace(w,"")}},75288(s){s.exports=function eq(s,o){return s===o||s!=s&&o!=o}},60680(s,o,i){var a=i(13222),u=/[\\^$.*+?()[\]{}|]/g,_=RegExp(u.source);s.exports=function escapeRegExp(s){return(s=a(s))&&_.test(s)?s.replace(u,"\\$&"):s}},7309(s,o,i){var a=i(62006)(i(24713));s.exports=a},24713(s,o,i){var a=i(2523),u=i(15389),_=i(61489),w=Math.max;s.exports=function findIndex(s,o,i){var x=null==s?0:s.length;if(!x)return-1;var C=null==i?0:_(i);return C<0&&(C=w(x+C,0)),a(s,u(o,3),C)}},35970(s,o,i){var a=i(83120);s.exports=function flatten(s){return(null==s?0:s.length)?a(s,1):[]}},73424(s,o,i){var a=i(16962),u=i(2874),_=Array.prototype.push;function baseAry(s,o){return 2==o?function(o,i){return s(o,i)}:function(o){return s(o)}}function cloneArray(s){for(var o=s?s.length:0,i=Array(o);o--;)i[o]=s[o];return i}function wrapImmutable(s,o){return function(){var i=arguments.length;if(i){for(var a=Array(i);i--;)a[i]=arguments[i];var u=a[0]=o.apply(void 0,a);return s.apply(void 0,a),u}}}s.exports=function baseConvert(s,o,i,w){var x="function"==typeof o,C=o===Object(o);if(C&&(w=i,i=o,o=void 0),null==i)throw new TypeError;w||(w={});var j=!("cap"in w)||w.cap,L=!("curry"in w)||w.curry,B=!("fixed"in w)||w.fixed,$=!("immutable"in w)||w.immutable,z=!("rearg"in w)||w.rearg,U=x?i:u,V="curry"in w&&w.curry,Y="fixed"in w&&w.fixed,Z="rearg"in w&&w.rearg,ee=x?i.runInContext():void 0,ie=x?i:{ary:s.ary,assign:s.assign,clone:s.clone,curry:s.curry,forEach:s.forEach,isArray:s.isArray,isError:s.isError,isFunction:s.isFunction,isWeakMap:s.isWeakMap,iteratee:s.iteratee,keys:s.keys,rearg:s.rearg,toInteger:s.toInteger,toPath:s.toPath},ae=ie.ary,ce=ie.assign,le=ie.clone,pe=ie.curry,de=ie.forEach,fe=ie.isArray,ye=ie.isError,be=ie.isFunction,Se=ie.isWeakMap,_e=ie.keys,we=ie.rearg,xe=ie.toInteger,Pe=ie.toPath,Te=_e(a.aryMethod),Re={castArray:function(s){return function(){var o=arguments[0];return fe(o)?s(cloneArray(o)):s.apply(void 0,arguments)}},iteratee:function(s){return function(){var o=arguments[1],i=s(arguments[0],o),a=i.length;return j&&"number"==typeof o?(o=o>2?o-2:1,a&&a<=o?i:baseAry(i,o)):i}},mixin:function(s){return function(o){var i=this;if(!be(i))return s(i,Object(o));var a=[];return de(_e(o),(function(s){be(o[s])&&a.push([s,i.prototype[s]])})),s(i,Object(o)),de(a,(function(s){var o=s[1];be(o)?i.prototype[s[0]]=o:delete i.prototype[s[0]]})),i}},nthArg:function(s){return function(o){var i=o<0?1:xe(o)+1;return pe(s(o),i)}},rearg:function(s){return function(o,i){var a=i?i.length:0;return pe(s(o,i),a)}},runInContext:function(o){return function(i){return baseConvert(s,o(i),w)}}};function castCap(s,o){if(j){var i=a.iterateeRearg[s];if(i)return function iterateeRearg(s,o){return overArg(s,(function(s){var i=o.length;return function baseArity(s,o){return 2==o?function(o,i){return s.apply(void 0,arguments)}:function(o){return s.apply(void 0,arguments)}}(we(baseAry(s,i),o),i)}))}(o,i);var u=!x&&a.iterateeAry[s];if(u)return function iterateeAry(s,o){return overArg(s,(function(s){return"function"==typeof s?baseAry(s,o):s}))}(o,u)}return o}function castFixed(s,o,i){if(B&&(Y||!a.skipFixed[s])){var u=a.methodSpread[s],w=u&&u.start;return void 0===w?ae(o,i):function flatSpread(s,o){return function(){for(var i=arguments.length,a=i-1,u=Array(i);i--;)u[i]=arguments[i];var w=u[o],x=u.slice(0,o);return w&&_.apply(x,w),o!=a&&_.apply(x,u.slice(o+1)),s.apply(this,x)}}(o,w)}return o}function castRearg(s,o,i){return z&&i>1&&(Z||!a.skipRearg[s])?we(o,a.methodRearg[s]||a.aryRearg[i]):o}function cloneByPath(s,o){for(var i=-1,a=(o=Pe(o)).length,u=a-1,_=le(Object(s)),w=_;null!=w&&++i1?pe(o,i):o}(0,u=castCap(_,u),s),!1}})),!u})),u||(u=w),u==o&&(u=V?pe(u,1):function(){return o.apply(this,arguments)}),u.convert=createConverter(_,o),u.placeholder=o.placeholder=i,u}if(!C)return wrap(o,i,U);var qe=i,$e=[];return de(Te,(function(s){de(a.aryMethod[s],(function(s){var o=qe[a.remap[s]||s];o&&$e.push([s,wrap(s,o,qe)])}))})),de(_e(qe),(function(s){var o=qe[s];if("function"==typeof o){for(var i=$e.length;i--;)if($e[i][0]==s)return;o.convert=createConverter(s,o),$e.push([s,o])}})),de($e,(function(s){qe[s[0]]=s[1]})),qe.convert=function convertLib(s){return qe.runInContext.convert(s)(void 0)},qe.placeholder=qe,de(_e(qe),(function(s){de(a.realToAlias[s]||[],(function(o){qe[o]=qe[s]}))})),qe}},16962(s,o){o.aliasToReal={each:"forEach",eachRight:"forEachRight",entries:"toPairs",entriesIn:"toPairsIn",extend:"assignIn",extendAll:"assignInAll",extendAllWith:"assignInAllWith",extendWith:"assignInWith",first:"head",conforms:"conformsTo",matches:"isMatch",property:"get",__:"placeholder",F:"stubFalse",T:"stubTrue",all:"every",allPass:"overEvery",always:"constant",any:"some",anyPass:"overSome",apply:"spread",assoc:"set",assocPath:"set",complement:"negate",compose:"flowRight",contains:"includes",dissoc:"unset",dissocPath:"unset",dropLast:"dropRight",dropLastWhile:"dropRightWhile",equals:"isEqual",identical:"eq",indexBy:"keyBy",init:"initial",invertObj:"invert",juxt:"over",omitAll:"omit",nAry:"ary",path:"get",pathEq:"matchesProperty",pathOr:"getOr",paths:"at",pickAll:"pick",pipe:"flow",pluck:"map",prop:"get",propEq:"matchesProperty",propOr:"getOr",props:"at",symmetricDifference:"xor",symmetricDifferenceBy:"xorBy",symmetricDifferenceWith:"xorWith",takeLast:"takeRight",takeLastWhile:"takeRightWhile",unapply:"rest",unnest:"flatten",useWith:"overArgs",where:"conformsTo",whereEq:"isMatch",zipObj:"zipObject"},o.aryMethod={1:["assignAll","assignInAll","attempt","castArray","ceil","create","curry","curryRight","defaultsAll","defaultsDeepAll","floor","flow","flowRight","fromPairs","invert","iteratee","memoize","method","mergeAll","methodOf","mixin","nthArg","over","overEvery","overSome","rest","reverse","round","runInContext","spread","template","trim","trimEnd","trimStart","uniqueId","words","zipAll"],2:["add","after","ary","assign","assignAllWith","assignIn","assignInAllWith","at","before","bind","bindAll","bindKey","chunk","cloneDeepWith","cloneWith","concat","conformsTo","countBy","curryN","curryRightN","debounce","defaults","defaultsDeep","defaultTo","delay","difference","divide","drop","dropRight","dropRightWhile","dropWhile","endsWith","eq","every","filter","find","findIndex","findKey","findLast","findLastIndex","findLastKey","flatMap","flatMapDeep","flattenDepth","forEach","forEachRight","forIn","forInRight","forOwn","forOwnRight","get","groupBy","gt","gte","has","hasIn","includes","indexOf","intersection","invertBy","invoke","invokeMap","isEqual","isMatch","join","keyBy","lastIndexOf","lt","lte","map","mapKeys","mapValues","matchesProperty","maxBy","meanBy","merge","mergeAllWith","minBy","multiply","nth","omit","omitBy","overArgs","pad","padEnd","padStart","parseInt","partial","partialRight","partition","pick","pickBy","propertyOf","pull","pullAll","pullAt","random","range","rangeRight","rearg","reject","remove","repeat","restFrom","result","sampleSize","some","sortBy","sortedIndex","sortedIndexOf","sortedLastIndex","sortedLastIndexOf","sortedUniqBy","split","spreadFrom","startsWith","subtract","sumBy","take","takeRight","takeRightWhile","takeWhile","tap","throttle","thru","times","trimChars","trimCharsEnd","trimCharsStart","truncate","union","uniqBy","uniqWith","unset","unzipWith","without","wrap","xor","zip","zipObject","zipObjectDeep"],3:["assignInWith","assignWith","clamp","differenceBy","differenceWith","findFrom","findIndexFrom","findLastFrom","findLastIndexFrom","getOr","includesFrom","indexOfFrom","inRange","intersectionBy","intersectionWith","invokeArgs","invokeArgsMap","isEqualWith","isMatchWith","flatMapDepth","lastIndexOfFrom","mergeWith","orderBy","padChars","padCharsEnd","padCharsStart","pullAllBy","pullAllWith","rangeStep","rangeStepRight","reduce","reduceRight","replace","set","slice","sortedIndexBy","sortedLastIndexBy","transform","unionBy","unionWith","update","xorBy","xorWith","zipWith"],4:["fill","setWith","updateWith"]},o.aryRearg={2:[1,0],3:[2,0,1],4:[3,2,0,1]},o.iterateeAry={dropRightWhile:1,dropWhile:1,every:1,filter:1,find:1,findFrom:1,findIndex:1,findIndexFrom:1,findKey:1,findLast:1,findLastFrom:1,findLastIndex:1,findLastIndexFrom:1,findLastKey:1,flatMap:1,flatMapDeep:1,flatMapDepth:1,forEach:1,forEachRight:1,forIn:1,forInRight:1,forOwn:1,forOwnRight:1,map:1,mapKeys:1,mapValues:1,partition:1,reduce:2,reduceRight:2,reject:1,remove:1,some:1,takeRightWhile:1,takeWhile:1,times:1,transform:2},o.iterateeRearg={mapKeys:[1],reduceRight:[1,0]},o.methodRearg={assignInAllWith:[1,0],assignInWith:[1,2,0],assignAllWith:[1,0],assignWith:[1,2,0],differenceBy:[1,2,0],differenceWith:[1,2,0],getOr:[2,1,0],intersectionBy:[1,2,0],intersectionWith:[1,2,0],isEqualWith:[1,2,0],isMatchWith:[2,1,0],mergeAllWith:[1,0],mergeWith:[1,2,0],padChars:[2,1,0],padCharsEnd:[2,1,0],padCharsStart:[2,1,0],pullAllBy:[2,1,0],pullAllWith:[2,1,0],rangeStep:[1,2,0],rangeStepRight:[1,2,0],setWith:[3,1,2,0],sortedIndexBy:[2,1,0],sortedLastIndexBy:[2,1,0],unionBy:[1,2,0],unionWith:[1,2,0],updateWith:[3,1,2,0],xorBy:[1,2,0],xorWith:[1,2,0],zipWith:[1,2,0]},o.methodSpread={assignAll:{start:0},assignAllWith:{start:0},assignInAll:{start:0},assignInAllWith:{start:0},defaultsAll:{start:0},defaultsDeepAll:{start:0},invokeArgs:{start:2},invokeArgsMap:{start:2},mergeAll:{start:0},mergeAllWith:{start:0},partial:{start:1},partialRight:{start:1},without:{start:1},zipAll:{start:0}},o.mutate={array:{fill:!0,pull:!0,pullAll:!0,pullAllBy:!0,pullAllWith:!0,pullAt:!0,remove:!0,reverse:!0},object:{assign:!0,assignAll:!0,assignAllWith:!0,assignIn:!0,assignInAll:!0,assignInAllWith:!0,assignInWith:!0,assignWith:!0,defaults:!0,defaultsAll:!0,defaultsDeep:!0,defaultsDeepAll:!0,merge:!0,mergeAll:!0,mergeAllWith:!0,mergeWith:!0},set:{set:!0,setWith:!0,unset:!0,update:!0,updateWith:!0}},o.realToAlias=function(){var s=Object.prototype.hasOwnProperty,i=o.aliasToReal,a={};for(var u in i){var _=i[u];s.call(a,_)?a[_].push(u):a[_]=[u]}return a}(),o.remap={assignAll:"assign",assignAllWith:"assignWith",assignInAll:"assignIn",assignInAllWith:"assignInWith",curryN:"curry",curryRightN:"curryRight",defaultsAll:"defaults",defaultsDeepAll:"defaultsDeep",findFrom:"find",findIndexFrom:"findIndex",findLastFrom:"findLast",findLastIndexFrom:"findLastIndex",getOr:"get",includesFrom:"includes",indexOfFrom:"indexOf",invokeArgs:"invoke",invokeArgsMap:"invokeMap",lastIndexOfFrom:"lastIndexOf",mergeAll:"merge",mergeAllWith:"mergeWith",padChars:"pad",padCharsEnd:"padEnd",padCharsStart:"padStart",propertyOf:"get",rangeStep:"range",rangeStepRight:"rangeRight",restFrom:"rest",spreadFrom:"spread",trimChars:"trim",trimCharsEnd:"trimEnd",trimCharsStart:"trimStart",zipAll:"zip"},o.skipFixed={castArray:!0,flow:!0,flowRight:!0,iteratee:!0,mixin:!0,rearg:!0,runInContext:!0},o.skipRearg={add:!0,assign:!0,assignIn:!0,bind:!0,bindKey:!0,concat:!0,difference:!0,divide:!0,eq:!0,gt:!0,gte:!0,isEqual:!0,lt:!0,lte:!0,matchesProperty:!0,merge:!0,multiply:!0,overArgs:!0,partial:!0,partialRight:!0,propertyOf:!0,random:!0,range:!0,rangeRight:!0,subtract:!0,zip:!0,zipObject:!0,zipObjectDeep:!0}},47934(s,o,i){s.exports={ary:i(64626),assign:i(74733),clone:i(32629),curry:i(49747),forEach:i(83729),isArray:i(56449),isError:i(23546),isFunction:i(1882),isWeakMap:i(47886),iteratee:i(33855),keys:i(88984),rearg:i(84195),toInteger:i(61489),toPath:i(42072)}},56367(s,o,i){s.exports=i(77731)},79920(s,o,i){var a=i(73424),u=i(47934);s.exports=function convert(s,o,i){return a(u,s,o,i)}},2874(s){s.exports={}},77731(s,o,i){var a=i(79920)("set",i(63560));a.placeholder=i(2874),s.exports=a},58156(s,o,i){var a=i(47422);s.exports=function get(s,o,i){var u=null==s?void 0:a(s,o);return void 0===u?i:u}},61448(s,o,i){var a=i(20426),u=i(49326);s.exports=function has(s,o){return null!=s&&u(s,o,a)}},80631(s,o,i){var a=i(28077),u=i(49326);s.exports=function hasIn(s,o){return null!=s&&u(s,o,a)}},83488(s){s.exports=function identity(s){return s}},72428(s,o,i){var a=i(27534),u=i(40346),_=Object.prototype,w=_.hasOwnProperty,x=_.propertyIsEnumerable,C=a(function(){return arguments}())?a:function(s){return u(s)&&w.call(s,"callee")&&!x.call(s,"callee")};s.exports=C},56449(s){var o=Array.isArray;s.exports=o},64894(s,o,i){var a=i(1882),u=i(30294);s.exports=function isArrayLike(s){return null!=s&&u(s.length)&&!a(s)}},83693(s,o,i){var a=i(64894),u=i(40346);s.exports=function isArrayLikeObject(s){return u(s)&&a(s)}},53812(s,o,i){var a=i(72552),u=i(40346);s.exports=function isBoolean(s){return!0===s||!1===s||u(s)&&"[object Boolean]"==a(s)}},3656(s,o,i){s=i.nmd(s);var a=i(9325),u=i(89935),_=o&&!o.nodeType&&o,w=_&&s&&!s.nodeType&&s,x=w&&w.exports===_?a.Buffer:void 0,C=(x?x.isBuffer:void 0)||u;s.exports=C},62193(s,o,i){var a=i(88984),u=i(5861),_=i(72428),w=i(56449),x=i(64894),C=i(3656),j=i(55527),L=i(37167),B=Object.prototype.hasOwnProperty;s.exports=function isEmpty(s){if(null==s)return!0;if(x(s)&&(w(s)||"string"==typeof s||"function"==typeof s.splice||C(s)||L(s)||_(s)))return!s.length;var o=u(s);if("[object Map]"==o||"[object Set]"==o)return!s.size;if(j(s))return!a(s).length;for(var i in s)if(B.call(s,i))return!1;return!0}},2404(s,o,i){var a=i(60270);s.exports=function isEqual(s,o){return a(s,o)}},23546(s,o,i){var a=i(72552),u=i(40346),_=i(11331);s.exports=function isError(s){if(!u(s))return!1;var o=a(s);return"[object Error]"==o||"[object DOMException]"==o||"string"==typeof s.message&&"string"==typeof s.name&&!_(s)}},1882(s,o,i){var a=i(72552),u=i(23805);s.exports=function isFunction(s){if(!u(s))return!1;var o=a(s);return"[object Function]"==o||"[object GeneratorFunction]"==o||"[object AsyncFunction]"==o||"[object Proxy]"==o}},30294(s){s.exports=function isLength(s){return"number"==typeof s&&s>-1&&s%1==0&&s<=9007199254740991}},87730(s,o,i){var a=i(29172),u=i(27301),_=i(86009),w=_&&_.isMap,x=w?u(w):a;s.exports=x},5187(s){s.exports=function isNull(s){return null===s}},98023(s,o,i){var a=i(72552),u=i(40346);s.exports=function isNumber(s){return"number"==typeof s||u(s)&&"[object Number]"==a(s)}},23805(s){s.exports=function isObject(s){var o=typeof s;return null!=s&&("object"==o||"function"==o)}},40346(s){s.exports=function isObjectLike(s){return null!=s&&"object"==typeof s}},11331(s,o,i){var a=i(72552),u=i(28879),_=i(40346),w=Function.prototype,x=Object.prototype,C=w.toString,j=x.hasOwnProperty,L=C.call(Object);s.exports=function isPlainObject(s){if(!_(s)||"[object Object]"!=a(s))return!1;var o=u(s);if(null===o)return!0;var i=j.call(o,"constructor")&&o.constructor;return"function"==typeof i&&i instanceof i&&C.call(i)==L}},38440(s,o,i){var a=i(16038),u=i(27301),_=i(86009),w=_&&_.isSet,x=w?u(w):a;s.exports=x},85015(s,o,i){var a=i(72552),u=i(56449),_=i(40346);s.exports=function isString(s){return"string"==typeof s||!u(s)&&_(s)&&"[object String]"==a(s)}},44394(s,o,i){var a=i(72552),u=i(40346);s.exports=function isSymbol(s){return"symbol"==typeof s||u(s)&&"[object Symbol]"==a(s)}},37167(s,o,i){var a=i(4901),u=i(27301),_=i(86009),w=_&&_.isTypedArray,x=w?u(w):a;s.exports=x},47886(s,o,i){var a=i(5861),u=i(40346);s.exports=function isWeakMap(s){return u(s)&&"[object WeakMap]"==a(s)}},33855(s,o,i){var a=i(9999),u=i(15389);s.exports=function iteratee(s){return u("function"==typeof s?s:a(s,1))}},95950(s,o,i){var a=i(70695),u=i(88984),_=i(64894);s.exports=function keys(s){return _(s)?a(s):u(s)}},37241(s,o,i){var a=i(70695),u=i(72903),_=i(64894);s.exports=function keysIn(s){return _(s)?a(s,!0):u(s)}},68090(s){s.exports=function last(s){var o=null==s?0:s.length;return o?s[o-1]:void 0}},50104(s,o,i){var a=i(53661);function memoize(s,o){if("function"!=typeof s||null!=o&&"function"!=typeof o)throw new TypeError("Expected a function");var memoized=function(){var i=arguments,a=o?o.apply(this,i):i[0],u=memoized.cache;if(u.has(a))return u.get(a);var _=s.apply(this,i);return memoized.cache=u.set(a,_)||u,_};return memoized.cache=new(memoize.Cache||a),memoized}memoize.Cache=a,s.exports=memoize},55364(s,o,i){var a=i(85250),u=i(20999)((function(s,o,i){a(s,o,i)}));s.exports=u},6048(s){s.exports=function negate(s){if("function"!=typeof s)throw new TypeError("Expected a function");return function(){var o=arguments;switch(o.length){case 0:return!s.call(this);case 1:return!s.call(this,o[0]);case 2:return!s.call(this,o[0],o[1]);case 3:return!s.call(this,o[0],o[1],o[2])}return!s.apply(this,o)}}},63950(s){s.exports=function noop(){}},10124(s,o,i){var a=i(9325);s.exports=function(){return a.Date.now()}},90179(s,o,i){var a=i(34932),u=i(9999),_=i(19931),w=i(31769),x=i(21791),C=i(53138),j=i(38816),L=i(83349),B=j((function(s,o){var i={};if(null==s)return i;var j=!1;o=a(o,(function(o){return o=w(o,s),j||(j=o.length>1),o})),x(s,L(s),i),j&&(i=u(i,7,C));for(var B=o.length;B--;)_(i,o[B]);return i}));s.exports=B},50583(s,o,i){var a=i(47237),u=i(17255),_=i(28586),w=i(77797);s.exports=function property(s){return _(s)?a(w(s)):u(s)}},84195(s,o,i){var a=i(66977),u=i(38816),_=u((function(s,o){return a(s,256,void 0,void 0,void 0,o)}));s.exports=_},40860(s,o,i){var a=i(40882),u=i(80909),_=i(15389),w=i(85558),x=i(56449);s.exports=function reduce(s,o,i){var C=x(s)?a:w,j=arguments.length<3;return C(s,_(o,4),i,j,u)}},63560(s,o,i){var a=i(73170);s.exports=function set(s,o,i){return null==s?s:a(s,o,i)}},42426(s,o,i){var a=i(14248),u=i(15389),_=i(90916),w=i(56449),x=i(36800);s.exports=function some(s,o,i){var C=w(s)?a:_;return i&&x(s,o,i)&&(o=void 0),C(s,u(o,3))}},63345(s){s.exports=function stubArray(){return[]}},89935(s){s.exports=function stubFalse(){return!1}},17400(s,o,i){var a=i(99374),u=1/0;s.exports=function toFinite(s){return s?(s=a(s))===u||s===-1/0?17976931348623157e292*(s<0?-1:1):s==s?s:0:0===s?s:0}},61489(s,o,i){var a=i(17400);s.exports=function toInteger(s){var o=a(s),i=o%1;return o==o?i?o-i:o:0}},80218(s,o,i){var a=i(13222);s.exports=function toLower(s){return a(s).toLowerCase()}},99374(s,o,i){var a=i(54128),u=i(23805),_=i(44394),w=/^[-+]0x[0-9a-f]+$/i,x=/^0b[01]+$/i,C=/^0o[0-7]+$/i,j=parseInt;s.exports=function toNumber(s){if("number"==typeof s)return s;if(_(s))return NaN;if(u(s)){var o="function"==typeof s.valueOf?s.valueOf():s;s=u(o)?o+"":o}if("string"!=typeof s)return 0===s?s:+s;s=a(s);var i=x.test(s);return i||C.test(s)?j(s.slice(2),i?2:8):w.test(s)?NaN:+s}},42072(s,o,i){var a=i(34932),u=i(23007),_=i(56449),w=i(44394),x=i(61802),C=i(77797),j=i(13222);s.exports=function toPath(s){return _(s)?a(s,C):w(s)?[s]:u(x(j(s)))}},69884(s,o,i){var a=i(21791),u=i(37241);s.exports=function toPlainObject(s){return a(s,u(s))}},13222(s,o,i){var a=i(77556);s.exports=function toString(s){return null==s?"":a(s)}},55808(s,o,i){var a=i(12507)("toUpperCase");s.exports=a},66645(s,o,i){var a=i(1733),u=i(45434),_=i(13222),w=i(22225);s.exports=function words(s,o,i){return s=_(s),void 0===(o=i?void 0:o)?u(s)?w(s):a(s):s.match(o)||[]}},53758(s,o,i){var a=i(30980),u=i(56017),_=i(94033),w=i(56449),x=i(40346),C=i(80257),j=Object.prototype.hasOwnProperty;function lodash(s){if(x(s)&&!w(s)&&!(s instanceof a)){if(s instanceof u)return s;if(j.call(s,"__wrapped__"))return C(s)}return new u(s)}lodash.prototype=_.prototype,lodash.prototype.constructor=lodash,s.exports=lodash},47248(s,o,i){var a=i(16547),u=i(51234);s.exports=function zipObject(s,o){return u(s||[],o||[],a)}},43768(s,o,i){"use strict";var a=i(45981),u=i(85587);o.highlight=highlight,o.highlightAuto=function highlightAuto(s,o){var i,w,x,C,j=o||{},L=j.subset||a.listLanguages(),B=j.prefix,$=L.length,z=-1;null==B&&(B=_);if("string"!=typeof s)throw u("Expected `string` for value, got `%s`",s);w={relevance:0,language:null,value:[]},i={relevance:0,language:null,value:[]};for(;++z<$;)C=L[z],a.getLanguage(C)&&((x=highlight(C,s,o)).language=C,x.relevance>w.relevance&&(w=x),x.relevance>i.relevance&&(w=i,i=x));w.language&&(i.secondBest=w);return i},o.registerLanguage=function registerLanguage(s,o){a.registerLanguage(s,o)},o.listLanguages=function listLanguages(){return a.listLanguages()},o.registerAlias=function registerAlias(s,o){var i,u=s;o&&((u={})[s]=o);for(i in u)a.registerAliases(u[i],{languageName:i})},Emitter.prototype.addText=function text(s){var o,i,a=this.stack;if(""===s)return;o=a[a.length-1],(i=o.children[o.children.length-1])&&"text"===i.type?i.value+=s:o.children.push({type:"text",value:s})},Emitter.prototype.addKeyword=function addKeyword(s,o){this.openNode(o),this.addText(s),this.closeNode()},Emitter.prototype.addSublanguage=function addSublanguage(s,o){var i=this.stack,a=i[i.length-1],u=s.rootNode.children,_=o?{type:"element",tagName:"span",properties:{className:[o]},children:u}:u;a.children=a.children.concat(_)},Emitter.prototype.openNode=function open(s){var o=this.stack,i=this.options.classPrefix+s,a=o[o.length-1],u={type:"element",tagName:"span",properties:{className:[i]},children:[]};a.children.push(u),o.push(u)},Emitter.prototype.closeNode=function close(){this.stack.pop()},Emitter.prototype.closeAllNodes=noop,Emitter.prototype.finalize=noop,Emitter.prototype.toHTML=function toHtmlNoop(){return""};var _="hljs-";function highlight(s,o,i){var w,x=a.configure({}),C=(i||{}).prefix;if("string"!=typeof s)throw u("Expected `string` for name, got `%s`",s);if(!a.getLanguage(s))throw u("Unknown language: `%s` is not registered",s);if("string"!=typeof o)throw u("Expected `string` for value, got `%s`",o);if(null==C&&(C=_),a.configure({__emitter:Emitter,classPrefix:C}),w=a.highlight(o,{language:s,ignoreIllegals:!0}),a.configure(x||{}),w.errorRaised)throw w.errorRaised;return{relevance:w.relevance,language:w.language,value:w.emitter.rootNode.children}}function Emitter(s){this.options=s,this.rootNode={children:[]},this.stack=[this.rootNode]}function noop(){}},71514(s){"use strict";s.exports=Math.abs},58968(s){"use strict";s.exports=Math.floor},94459(s){"use strict";s.exports=Number.isNaN||function isNaN(s){return s!=s}},6188(s){"use strict";s.exports=Math.max},68002(s){"use strict";s.exports=Math.min},75880(s){"use strict";s.exports=Math.pow},70414(s){"use strict";s.exports=Math.round},73093(s,o,i){"use strict";var a=i(94459);s.exports=function sign(s){return a(s)||0===s?s:s<0?-1:1}},92340(s,o,i){const a=i(6048);function coerceElementMatchingCallback(s){return"string"==typeof s?o=>o.element===s:s.constructor&&s.extend?o=>o instanceof s:s}class ArraySlice{constructor(s){this.elements=s||[]}toValue(){return this.elements.map((s=>s.toValue()))}map(s,o){return this.elements.map(s,o)}flatMap(s,o){return this.map(s,o).reduce(((s,o)=>s.concat(o)),[])}compactMap(s,o){const i=[];return this.forEach((a=>{const u=s.bind(o)(a);u&&i.push(u)})),i}filter(s,o){return s=coerceElementMatchingCallback(s),new ArraySlice(this.elements.filter(s,o))}reject(s,o){return s=coerceElementMatchingCallback(s),new ArraySlice(this.elements.filter(a(s),o))}find(s,o){return s=coerceElementMatchingCallback(s),this.elements.find(s,o)}forEach(s,o){this.elements.forEach(s,o)}reduce(s,o){return this.elements.reduce(s,o)}includes(s){return this.elements.some((o=>o.equals(s)))}shift(){return this.elements.shift()}unshift(s){this.elements.unshift(this.refract(s))}push(s){return this.elements.push(this.refract(s)),this}add(s){this.push(s)}get(s){return this.elements[s]}getValue(s){const o=this.elements[s];if(o)return o.toValue()}get length(){return this.elements.length}get isEmpty(){return 0===this.elements.length}get first(){return this.elements[0]}}"undefined"!=typeof Symbol&&(ArraySlice.prototype[Symbol.iterator]=function symbol(){return this.elements[Symbol.iterator]()}),s.exports=ArraySlice},55973(s){class KeyValuePair{constructor(s,o){this.key=s,this.value=o}clone(){const s=new KeyValuePair;return this.key&&(s.key=this.key.clone()),this.value&&(s.value=this.value.clone()),s}}s.exports=KeyValuePair},3110(s,o,i){const a=i(5187),u=i(85015),_=i(98023),w=i(53812),x=i(23805),C=i(85105),j=i(86804);class Namespace{constructor(s){this.elementMap={},this.elementDetection=[],this.Element=j.Element,this.KeyValuePair=j.KeyValuePair,s&&s.noDefault||this.useDefault(),this._attributeElementKeys=[],this._attributeElementArrayKeys=[]}use(s){return s.namespace&&s.namespace({base:this}),s.load&&s.load({base:this}),this}useDefault(){return this.register("null",j.NullElement).register("string",j.StringElement).register("number",j.NumberElement).register("boolean",j.BooleanElement).register("array",j.ArrayElement).register("object",j.ObjectElement).register("member",j.MemberElement).register("ref",j.RefElement).register("link",j.LinkElement),this.detect(a,j.NullElement,!1).detect(u,j.StringElement,!1).detect(_,j.NumberElement,!1).detect(w,j.BooleanElement,!1).detect(Array.isArray,j.ArrayElement,!1).detect(x,j.ObjectElement,!1),this}register(s,o){return this._elements=void 0,this.elementMap[s]=o,this}unregister(s){return this._elements=void 0,delete this.elementMap[s],this}detect(s,o,i){return void 0===i||i?this.elementDetection.unshift([s,o]):this.elementDetection.push([s,o]),this}toElement(s){if(s instanceof this.Element)return s;let o;for(let i=0;i{const o=s[0].toUpperCase()+s.substr(1);this._elements[o]=this.elementMap[s]}))),this._elements}get serialiser(){return new C(this)}}C.prototype.Namespace=Namespace,s.exports=Namespace},10866(s,o,i){const a=i(6048),u=i(92340);class ObjectSlice extends u{map(s,o){return this.elements.map((i=>s.bind(o)(i.value,i.key,i)))}filter(s,o){return new ObjectSlice(this.elements.filter((i=>s.bind(o)(i.value,i.key,i))))}reject(s,o){return this.filter(a(s.bind(o)))}forEach(s,o){return this.elements.forEach(((i,a)=>{s.bind(o)(i.value,i.key,i,a)}))}keys(){return this.map(((s,o)=>o.toValue()))}values(){return this.map((s=>s.toValue()))}}s.exports=ObjectSlice},86804(s,o,i){const a=i(10316),u=i(41067),_=i(71167),w=i(40239),x=i(12242),C=i(6233),j=i(87726),L=i(61045),B=i(86303),$=i(14540),z=i(92340),U=i(10866),V=i(55973);function refract(s){if(s instanceof a)return s;if("string"==typeof s)return new _(s);if("number"==typeof s)return new w(s);if("boolean"==typeof s)return new x(s);if(null===s)return new u;if(Array.isArray(s))return new C(s.map(refract));if("object"==typeof s){return new L(s)}return s}a.prototype.ObjectElement=L,a.prototype.RefElement=$,a.prototype.MemberElement=j,a.prototype.refract=refract,z.prototype.refract=refract,s.exports={Element:a,NullElement:u,StringElement:_,NumberElement:w,BooleanElement:x,ArrayElement:C,MemberElement:j,ObjectElement:L,LinkElement:B,RefElement:$,refract,ArraySlice:z,ObjectSlice:U,KeyValuePair:V}},86303(s,o,i){const a=i(10316);s.exports=class LinkElement extends a{constructor(s,o,i){super(s||[],o,i),this.element="link"}get relation(){return this.attributes.get("relation")}set relation(s){this.attributes.set("relation",s)}get href(){return this.attributes.get("href")}set href(s){this.attributes.set("href",s)}}},14540(s,o,i){const a=i(10316);s.exports=class RefElement extends a{constructor(s,o,i){super(s||[],o,i),this.element="ref",this.path||(this.path="element")}get path(){return this.attributes.get("path")}set path(s){this.attributes.set("path",s)}}},34035(s,o,i){const a=i(3110),u=i(86804);o.g$=a,o.KeyValuePair=i(55973),o.G6=u.ArraySlice,o.ot=u.ObjectSlice,o.Hg=u.Element,o.Om=u.StringElement,o.kT=u.NumberElement,o.bd=u.BooleanElement,o.Os=u.NullElement,o.wE=u.ArrayElement,o.Sh=u.ObjectElement,o.Pr=u.MemberElement,o.sI=u.RefElement,o.Ft=u.LinkElement,o.e=u.refract,i(85105),i(75147)},6233(s,o,i){const a=i(6048),u=i(10316),_=i(92340);class ArrayElement extends u{constructor(s,o,i){super(s||[],o,i),this.element="array"}primitive(){return"array"}get(s){return this.content[s]}getValue(s){const o=this.get(s);if(o)return o.toValue()}getIndex(s){return this.content[s]}set(s,o){return this.content[s]=this.refract(o),this}remove(s){const o=this.content.splice(s,1);return o.length?o[0]:null}map(s,o){return this.content.map(s,o)}flatMap(s,o){return this.map(s,o).reduce(((s,o)=>s.concat(o)),[])}compactMap(s,o){const i=[];return this.forEach((a=>{const u=s.bind(o)(a);u&&i.push(u)})),i}filter(s,o){return new _(this.content.filter(s,o))}reject(s,o){return this.filter(a(s),o)}reduce(s,o){let i,a;void 0!==o?(i=0,a=this.refract(o)):(i=1,a="object"===this.primitive()?this.first.value:this.first);for(let o=i;o{s.bind(o)(i,this.refract(a))}))}shift(){return this.content.shift()}unshift(s){this.content.unshift(this.refract(s))}push(s){return this.content.push(this.refract(s)),this}add(s){this.push(s)}findElements(s,o){const i=o||{},a=!!i.recursive,u=void 0===i.results?[]:i.results;return this.forEach(((o,i,_)=>{a&&void 0!==o.findElements&&o.findElements(s,{results:u,recursive:a}),s(o,i,_)&&u.push(o)})),u}find(s){return new _(this.findElements(s,{recursive:!0}))}findByElement(s){return this.find((o=>o.element===s))}findByClass(s){return this.find((o=>o.classes.includes(s)))}getById(s){return this.find((o=>o.id.toValue()===s)).first}includes(s){return this.content.some((o=>o.equals(s)))}contains(s){return this.includes(s)}empty(){return new this.constructor([])}"fantasy-land/empty"(){return this.empty()}concat(s){return new this.constructor(this.content.concat(s.content))}"fantasy-land/concat"(s){return this.concat(s)}"fantasy-land/map"(s){return new this.constructor(this.map(s))}"fantasy-land/chain"(s){return this.map((o=>s(o)),this).reduce(((s,o)=>s.concat(o)),this.empty())}"fantasy-land/filter"(s){return new this.constructor(this.content.filter(s))}"fantasy-land/reduce"(s,o){return this.content.reduce(s,o)}get length(){return this.content.length}get isEmpty(){return 0===this.content.length}get first(){return this.getIndex(0)}get second(){return this.getIndex(1)}get last(){return this.getIndex(this.length-1)}}ArrayElement.empty=function empty(){return new this},ArrayElement["fantasy-land/empty"]=ArrayElement.empty,"undefined"!=typeof Symbol&&(ArrayElement.prototype[Symbol.iterator]=function symbol(){return this.content[Symbol.iterator]()}),s.exports=ArrayElement},12242(s,o,i){const a=i(10316);s.exports=class BooleanElement extends a{constructor(s,o,i){super(s,o,i),this.element="boolean"}primitive(){return"boolean"}}},10316(s,o,i){const a=i(2404),u=i(55973),_=i(92340);class Element{constructor(s,o,i){o&&(this.meta=o),i&&(this.attributes=i),this.content=s}freeze(){Object.isFrozen(this)||(this._meta&&(this.meta.parent=this,this.meta.freeze()),this._attributes&&(this.attributes.parent=this,this.attributes.freeze()),this.children.forEach((s=>{s.parent=this,s.freeze()}),this),this.content&&Array.isArray(this.content)&&Object.freeze(this.content),Object.freeze(this))}primitive(){}clone(){const s=new this.constructor;return s.element=this.element,this.meta.length&&(s._meta=this.meta.clone()),this.attributes.length&&(s._attributes=this.attributes.clone()),this.content?this.content.clone?s.content=this.content.clone():Array.isArray(this.content)?s.content=this.content.map((s=>s.clone())):s.content=this.content:s.content=this.content,s}toValue(){return this.content instanceof Element?this.content.toValue():this.content instanceof u?{key:this.content.key.toValue(),value:this.content.value?this.content.value.toValue():void 0}:this.content&&this.content.map?this.content.map((s=>s.toValue()),this):this.content}toRef(s){if(""===this.id.toValue())throw Error("Cannot create reference to an element that does not contain an ID");const o=new this.RefElement(this.id.toValue());return s&&(o.path=s),o}findRecursive(...s){if(arguments.length>1&&!this.isFrozen)throw new Error("Cannot find recursive with multiple element names without first freezing the element. Call `element.freeze()`");const o=s.pop();let i=new _;const append=(s,o)=>(s.push(o),s),checkElement=(s,i)=>{i.element===o&&s.push(i);const a=i.findRecursive(o);return a&&a.reduce(append,s),i.content instanceof u&&(i.content.key&&checkElement(s,i.content.key),i.content.value&&checkElement(s,i.content.value)),s};return this.content&&(this.content.element&&checkElement(i,this.content),Array.isArray(this.content)&&this.content.reduce(checkElement,i)),s.isEmpty||(i=i.filter((o=>{let i=o.parents.map((s=>s.element));for(const o in s){const a=s[o],u=i.indexOf(a);if(-1===u)return!1;i=i.splice(0,u)}return!0}))),i}set(s){return this.content=s,this}equals(s){return a(this.toValue(),s)}getMetaProperty(s,o){if(!this.meta.hasKey(s)){if(this.isFrozen){const s=this.refract(o);return s.freeze(),s}this.meta.set(s,o)}return this.meta.get(s)}setMetaProperty(s,o){this.meta.set(s,o)}get element(){return this._storedElement||"element"}set element(s){this._storedElement=s}get content(){return this._content}set content(s){if(s instanceof Element)this._content=s;else if(s instanceof _)this.content=s.elements;else if("string"==typeof s||"number"==typeof s||"boolean"==typeof s||"null"===s||null==s)this._content=s;else if(s instanceof u)this._content=s;else if(Array.isArray(s))this._content=s.map(this.refract);else{if("object"!=typeof s)throw new Error("Cannot set content to given value");this._content=Object.keys(s).map((o=>new this.MemberElement(o,s[o])))}}get meta(){if(!this._meta){if(this.isFrozen){const s=new this.ObjectElement;return s.freeze(),s}this._meta=new this.ObjectElement}return this._meta}set meta(s){s instanceof this.ObjectElement?this._meta=s:this.meta.set(s||{})}get attributes(){if(!this._attributes){if(this.isFrozen){const s=new this.ObjectElement;return s.freeze(),s}this._attributes=new this.ObjectElement}return this._attributes}set attributes(s){s instanceof this.ObjectElement?this._attributes=s:this.attributes.set(s||{})}get id(){return this.getMetaProperty("id","")}set id(s){this.setMetaProperty("id",s)}get classes(){return this.getMetaProperty("classes",[])}set classes(s){this.setMetaProperty("classes",s)}get title(){return this.getMetaProperty("title","")}set title(s){this.setMetaProperty("title",s)}get description(){return this.getMetaProperty("description","")}set description(s){this.setMetaProperty("description",s)}get links(){return this.getMetaProperty("links",[])}set links(s){this.setMetaProperty("links",s)}get isFrozen(){return Object.isFrozen(this)}get parents(){let{parent:s}=this;const o=new _;for(;s;)o.push(s),s=s.parent;return o}get children(){if(Array.isArray(this.content))return new _(this.content);if(this.content instanceof u){const s=new _([this.content.key]);return this.content.value&&s.push(this.content.value),s}return this.content instanceof Element?new _([this.content]):new _}get recursiveChildren(){const s=new _;return this.children.forEach((o=>{s.push(o),o.recursiveChildren.forEach((o=>{s.push(o)}))})),s}}s.exports=Element},87726(s,o,i){const a=i(55973),u=i(10316);s.exports=class MemberElement extends u{constructor(s,o,i,u){super(new a,i,u),this.element="member",this.key=s,this.value=o}get key(){return this.content.key}set key(s){this.content.key=this.refract(s)}get value(){return this.content.value}set value(s){this.content.value=this.refract(s)}}},41067(s,o,i){const a=i(10316);s.exports=class NullElement extends a{constructor(s,o,i){super(s||null,o,i),this.element="null"}primitive(){return"null"}set(){return new Error("Cannot set the value of null")}}},40239(s,o,i){const a=i(10316);s.exports=class NumberElement extends a{constructor(s,o,i){super(s,o,i),this.element="number"}primitive(){return"number"}}},61045(s,o,i){const a=i(6048),u=i(23805),_=i(6233),w=i(87726),x=i(10866);s.exports=class ObjectElement extends _{constructor(s,o,i){super(s||[],o,i),this.element="object"}primitive(){return"object"}toValue(){return this.content.reduce(((s,o)=>(s[o.key.toValue()]=o.value?o.value.toValue():void 0,s)),{})}get(s){const o=this.getMember(s);if(o)return o.value}getMember(s){if(void 0!==s)return this.content.find((o=>o.key.toValue()===s))}remove(s){let o=null;return this.content=this.content.filter((i=>i.key.toValue()!==s||(o=i,!1))),o}getKey(s){const o=this.getMember(s);if(o)return o.key}set(s,o){if(u(s))return Object.keys(s).forEach((o=>{this.set(o,s[o])})),this;const i=s,a=this.getMember(i);return a?a.value=o:this.content.push(new w(i,o)),this}keys(){return this.content.map((s=>s.key.toValue()))}values(){return this.content.map((s=>s.value.toValue()))}hasKey(s){return this.content.some((o=>o.key.equals(s)))}items(){return this.content.map((s=>[s.key.toValue(),s.value.toValue()]))}map(s,o){return this.content.map((i=>s.bind(o)(i.value,i.key,i)))}compactMap(s,o){const i=[];return this.forEach(((a,u,_)=>{const w=s.bind(o)(a,u,_);w&&i.push(w)})),i}filter(s,o){return new x(this.content).filter(s,o)}reject(s,o){return this.filter(a(s),o)}forEach(s,o){return this.content.forEach((i=>s.bind(o)(i.value,i.key,i)))}}},71167(s,o,i){const a=i(10316);s.exports=class StringElement extends a{constructor(s,o,i){super(s,o,i),this.element="string"}primitive(){return"string"}get length(){return this.content.length}}},75147(s,o,i){const a=i(85105);s.exports=class JSON06Serialiser extends a{serialise(s){if(!(s instanceof this.namespace.elements.Element))throw new TypeError(`Given element \`${s}\` is not an Element instance`);let o;s._attributes&&s.attributes.get("variable")&&(o=s.attributes.get("variable"));const i={element:s.element};s._meta&&s._meta.length>0&&(i.meta=this.serialiseObject(s.meta));const a="enum"===s.element||-1!==s.attributes.keys().indexOf("enumerations");if(a){const o=this.enumSerialiseAttributes(s);o&&(i.attributes=o)}else if(s._attributes&&s._attributes.length>0){let{attributes:a}=s;a.get("metadata")&&(a=a.clone(),a.set("meta",a.get("metadata")),a.remove("metadata")),"member"===s.element&&o&&(a=a.clone(),a.remove("variable")),a.length>0&&(i.attributes=this.serialiseObject(a))}if(a)i.content=this.enumSerialiseContent(s,i);else if(this[`${s.element}SerialiseContent`])i.content=this[`${s.element}SerialiseContent`](s,i);else if(void 0!==s.content){let a;o&&s.content.key?(a=s.content.clone(),a.key.attributes.set("variable",o),a=this.serialiseContent(a)):a=this.serialiseContent(s.content),this.shouldSerialiseContent(s,a)&&(i.content=a)}else this.shouldSerialiseContent(s,s.content)&&s instanceof this.namespace.elements.Array&&(i.content=[]);return i}shouldSerialiseContent(s,o){return"parseResult"===s.element||"httpRequest"===s.element||"httpResponse"===s.element||"category"===s.element||"link"===s.element||void 0!==o&&(!Array.isArray(o)||0!==o.length)}refSerialiseContent(s,o){return delete o.attributes,{href:s.toValue(),path:s.path.toValue()}}sourceMapSerialiseContent(s){return s.toValue()}dataStructureSerialiseContent(s){return[this.serialiseContent(s.content)]}enumSerialiseAttributes(s){const o=s.attributes.clone(),i=o.remove("enumerations")||new this.namespace.elements.Array([]),a=o.get("default");let u=o.get("samples")||new this.namespace.elements.Array([]);if(a&&a.content&&(a.content.attributes&&a.content.attributes.remove("typeAttributes"),o.set("default",new this.namespace.elements.Array([a.content]))),u.forEach((s=>{s.content&&s.content.element&&s.content.attributes.remove("typeAttributes")})),s.content&&0!==i.length&&u.unshift(s.content),u=u.map((s=>s instanceof this.namespace.elements.Array?[s]:new this.namespace.elements.Array([s.content]))),u.length&&o.set("samples",u),o.length>0)return this.serialiseObject(o)}enumSerialiseContent(s){if(s._attributes){const o=s.attributes.get("enumerations");if(o&&o.length>0)return o.content.map((s=>{const o=s.clone();return o.attributes.remove("typeAttributes"),this.serialise(o)}))}if(s.content){const o=s.content.clone();return o.attributes.remove("typeAttributes"),[this.serialise(o)]}return[]}deserialise(s){if("string"==typeof s)return new this.namespace.elements.String(s);if("number"==typeof s)return new this.namespace.elements.Number(s);if("boolean"==typeof s)return new this.namespace.elements.Boolean(s);if(null===s)return new this.namespace.elements.Null;if(Array.isArray(s))return new this.namespace.elements.Array(s.map(this.deserialise,this));const o=this.namespace.getElementClass(s.element),i=new o;i.element!==s.element&&(i.element=s.element),s.meta&&this.deserialiseObject(s.meta,i.meta),s.attributes&&this.deserialiseObject(s.attributes,i.attributes);const a=this.deserialiseContent(s.content);if(void 0===a&&null!==i.content||(i.content=a),"enum"===i.element){i.content&&i.attributes.set("enumerations",i.content);let s=i.attributes.get("samples");if(i.attributes.remove("samples"),s){const a=s;s=new this.namespace.elements.Array,a.forEach((a=>{a.forEach((a=>{const u=new o(a);u.element=i.element,s.push(u)}))}));const u=s.shift();i.content=u?u.content:void 0,i.attributes.set("samples",s)}else i.content=void 0;let a=i.attributes.get("default");if(a&&a.length>0){a=a.get(0);const s=new o(a);s.element=i.element,i.attributes.set("default",s)}}else if("dataStructure"===i.element&&Array.isArray(i.content))[i.content]=i.content;else if("category"===i.element){const s=i.attributes.get("meta");s&&(i.attributes.set("metadata",s),i.attributes.remove("meta"))}else"member"===i.element&&i.key&&i.key._attributes&&i.key._attributes.getValue("variable")&&(i.attributes.set("variable",i.key.attributes.get("variable")),i.key.attributes.remove("variable"));return i}serialiseContent(s){if(s instanceof this.namespace.elements.Element)return this.serialise(s);if(s instanceof this.namespace.KeyValuePair){const o={key:this.serialise(s.key)};return s.value&&(o.value=this.serialise(s.value)),o}return s&&s.map?s.map(this.serialise,this):s}deserialiseContent(s){if(s){if(s.element)return this.deserialise(s);if(s.key){const o=new this.namespace.KeyValuePair(this.deserialise(s.key));return s.value&&(o.value=this.deserialise(s.value)),o}if(s.map)return s.map(this.deserialise,this)}return s}shouldRefract(s){return!!(s._attributes&&s.attributes.keys().length||s._meta&&s.meta.keys().length)||"enum"!==s.element&&(s.element!==s.primitive()||"member"===s.element)}convertKeyToRefract(s,o){return this.shouldRefract(o)?this.serialise(o):"enum"===o.element?this.serialiseEnum(o):"array"===o.element?o.map((o=>this.shouldRefract(o)||"default"===s?this.serialise(o):"array"===o.element||"object"===o.element||"enum"===o.element?o.children.map((s=>this.serialise(s))):o.toValue())):"object"===o.element?(o.content||[]).map(this.serialise,this):o.toValue()}serialiseEnum(s){return s.children.map((s=>this.serialise(s)))}serialiseObject(s){const o={};return s.forEach(((s,i)=>{if(s){const a=i.toValue();o[a]=this.convertKeyToRefract(a,s)}})),o}deserialiseObject(s,o){Object.keys(s).forEach((i=>{o.set(i,this.deserialise(s[i]))}))}}},85105(s){s.exports=class JSONSerialiser{constructor(s){this.namespace=s||new this.Namespace}serialise(s){if(!(s instanceof this.namespace.elements.Element))throw new TypeError(`Given element \`${s}\` is not an Element instance`);const o={element:s.element};s._meta&&s._meta.length>0&&(o.meta=this.serialiseObject(s.meta)),s._attributes&&s._attributes.length>0&&(o.attributes=this.serialiseObject(s.attributes));const i=this.serialiseContent(s.content);return void 0!==i&&(o.content=i),o}deserialise(s){if(!s.element)throw new Error("Given value is not an object containing an element name");const o=new(this.namespace.getElementClass(s.element));o.element!==s.element&&(o.element=s.element),s.meta&&this.deserialiseObject(s.meta,o.meta),s.attributes&&this.deserialiseObject(s.attributes,o.attributes);const i=this.deserialiseContent(s.content);return void 0===i&&null!==o.content||(o.content=i),o}serialiseContent(s){if(s instanceof this.namespace.elements.Element)return this.serialise(s);if(s instanceof this.namespace.KeyValuePair){const o={key:this.serialise(s.key)};return s.value&&(o.value=this.serialise(s.value)),o}if(s&&s.map){if(0===s.length)return;return s.map(this.serialise,this)}return s}deserialiseContent(s){if(s){if(s.element)return this.deserialise(s);if(s.key){const o=new this.namespace.KeyValuePair(this.deserialise(s.key));return s.value&&(o.value=this.deserialise(s.value)),o}if(s.map)return s.map(this.deserialise,this)}return s}serialiseObject(s){const o={};if(s.forEach(((s,i)=>{s&&(o[i.toValue()]=this.serialise(s))})),0!==Object.keys(o).length)return o}deserialiseObject(s,o){Object.keys(s).forEach((i=>{o.set(i,this.deserialise(s[i]))}))}}},76578(s){"use strict";s.exports=["Float16Array","Float32Array","Float64Array","Int8Array","Int16Array","Int32Array","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array"]},65606(s){var o,i,a=s.exports={};function defaultSetTimout(){throw new Error("setTimeout has not been defined")}function defaultClearTimeout(){throw new Error("clearTimeout has not been defined")}function runTimeout(s){if(o===setTimeout)return setTimeout(s,0);if((o===defaultSetTimout||!o)&&setTimeout)return o=setTimeout,setTimeout(s,0);try{return o(s,0)}catch(i){try{return o.call(null,s,0)}catch(i){return o.call(this,s,0)}}}!function(){try{o="function"==typeof setTimeout?setTimeout:defaultSetTimout}catch(s){o=defaultSetTimout}try{i="function"==typeof clearTimeout?clearTimeout:defaultClearTimeout}catch(s){i=defaultClearTimeout}}();var u,_=[],w=!1,x=-1;function cleanUpNextTick(){w&&u&&(w=!1,u.length?_=u.concat(_):x=-1,_.length&&drainQueue())}function drainQueue(){if(!w){var s=runTimeout(cleanUpNextTick);w=!0;for(var o=_.length;o;){for(u=_,_=[];++x1)for(var i=1;i_)throw new RangeError("requested too many random bytes");var i=w.allocUnsafe(s);if(s>0)if(s>u)for(var C=0;C=0||(u[i]=s[i]);return u}(s,o);if(Object.getOwnPropertySymbols){var _=Object.getOwnPropertySymbols(s);for(a=0;a<_.length;a++)i=_[a],o.indexOf(i)>=0||Object.prototype.propertyIsEnumerable.call(s,i)&&(u[i]=s[i])}return u}function ownKeys(s,o){var i=Object.keys(s);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(s);o&&(a=a.filter((function(o){return Object.getOwnPropertyDescriptor(s,o).enumerable}))),i.push.apply(i,a)}return i}function _objectSpread(s){for(var o=1;o=a?i.notify(s):o.length>u.length&&i.notify(_objectSpread(_objectSpread({},s),{},{target:_objectSpread(_objectSpread({},s.target),{},{value:""})}))}))})),_defineProperty(_assertThisInitialized(i),"onKeyDown",(function(s){"Enter"===s.key&&i.forceNotify(s);var o=i.props.onKeyDown;o&&(s.persist(),o(s))})),_defineProperty(_assertThisInitialized(i),"onBlur",(function(s){i.forceNotify(s);var o=i.props.onBlur;o&&(s.persist(),o(s))})),_defineProperty(_assertThisInitialized(i),"createNotifier",(function(s){if(s<0)i.notify=function(){return null};else if(0===s)i.notify=i.doNotify;else{var o=(0,u.default)((function(s){i.isDebouncing=!1,i.doNotify(s)}),s);i.notify=function(s){i.isDebouncing=!0,o(s)},i.flush=function(){return o.flush()},i.cancel=function(){i.isDebouncing=!1,o.cancel()}}})),_defineProperty(_assertThisInitialized(i),"doNotify",(function(){i.props.onChange.apply(void 0,arguments)})),_defineProperty(_assertThisInitialized(i),"forceNotify",(function(s){var o=i.props.debounceTimeout;if(i.isDebouncing||!(o>0)){i.cancel&&i.cancel();var a=i.state.value,u=i.props.minLength;a.length>=u?i.doNotify(s):i.doNotify(_objectSpread(_objectSpread({},s),{},{target:_objectSpread(_objectSpread({},s.target),{},{value:a})}))}})),i.isDebouncing=!1,i.state={value:void 0===s.value||null===s.value?"":s.value};var a=i.props.debounceTimeout;return i.createNotifier(a),i}return function _createClass(s,o,i){return o&&_defineProperties(s.prototype,o),i&&_defineProperties(s,i),Object.defineProperty(s,"prototype",{writable:!1}),s}(DebounceInput,[{key:"componentDidUpdate",value:function componentDidUpdate(s){if(!this.isDebouncing){var o=this.props,i=o.value,a=o.debounceTimeout,u=s.debounceTimeout,_=s.value,w=this.state.value;void 0!==i&&_!==i&&w!==i&&this.setState({value:i}),a!==u&&this.createNotifier(a)}}},{key:"componentWillUnmount",value:function componentWillUnmount(){this.flush&&this.flush()}},{key:"render",value:function render(){var s,o,i=this.props,u=i.element,w=(i.onChange,i.value,i.minLength,i.debounceTimeout,i.forceNotifyByEnter),x=i.forceNotifyOnBlur,C=i.onKeyDown,j=i.onBlur,L=i.inputRef,B=_objectWithoutProperties(i,_),$=this.state.value;s=w?{onKeyDown:this.onKeyDown}:C?{onKeyDown:C}:{},o=x?{onBlur:this.onBlur}:j?{onBlur:j}:{};var z=L?{ref:L}:{};return a.default.createElement(u,_objectSpread(_objectSpread(_objectSpread(_objectSpread({},B),{},{onChange:this.onChange,value:$},s),o),z))}}]),DebounceInput}(a.default.PureComponent);o.DebounceInput=w,_defineProperty(w,"defaultProps",{element:"input",type:"text",onKeyDown:void 0,onBlur:void 0,value:void 0,minLength:0,debounceTimeout:100,forceNotifyByEnter:!0,forceNotifyOnBlur:!0,inputRef:void 0})},24677(s,o,i){"use strict";var a=i(81214).DebounceInput;a.DebounceInput=a,s.exports=a},22551(s,o,i){"use strict";var a=i(96540),u=i(69982);function p(s){for(var o="https://reactjs.org/docs/error-decoder.html?invariant="+s,i=1;i