Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1a38c9fba |
@@ -1,9 +0,0 @@
|
|||||||
.git
|
|
||||||
.gitignore
|
|
||||||
node_modules
|
|
||||||
**/node_modules
|
|
||||||
backend/data
|
|
||||||
data
|
|
||||||
*.db
|
|
||||||
*.db-*
|
|
||||||
.DS_Store
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
APP_PORT=8080
|
|
||||||
IMAGE=repo.ssp-itinfra.com/admin/shooting-event:amd64-latest
|
|
||||||
ADMIN_USER=datwyler
|
|
||||||
ADMIN_PASS=datwyler
|
|
||||||
GEMINI_API_KEY=replace-with-your-key
|
|
||||||
GEMINI_MODEL=gemini-2.0-flash
|
|
||||||
21
.gitignore
vendored
@@ -1,21 +0,0 @@
|
|||||||
# macOS
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Node
|
|
||||||
frontend/node_modules/
|
|
||||||
frontend/dist/
|
|
||||||
|
|
||||||
node_modules/
|
|
||||||
dist/
|
|
||||||
# Go binaries
|
|
||||||
bin/
|
|
||||||
backend/backend
|
|
||||||
|
|
||||||
# Runtime/generated web assets
|
|
||||||
backend/web/
|
|
||||||
|
|
||||||
# SQLite runtime data
|
|
||||||
backend/data/
|
|
||||||
*.db
|
|
||||||
*.db-shm
|
|
||||||
*.db-wal
|
|
||||||
182
API_CHANGES.md
@@ -1,182 +0,0 @@
|
|||||||
# API Changes (Not Yet Reflected in `backend/openapi/openapi.json`)
|
|
||||||
|
|
||||||
This document lists additive API changes currently implemented in the Go backend.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Existing endpoints remain compatible.
|
|
||||||
|
|
||||||
Added capabilities:
|
|
||||||
|
|
||||||
1. Position board persistence for live/admin ordering.
|
|
||||||
2. Stage session control (`start`/`end`) for event flow.
|
|
||||||
3. Stage history retrieval.
|
|
||||||
4. New state fields (`positionBoards`, `current_stage`, `last_stage`).
|
|
||||||
|
|
||||||
## 1) Position Board Endpoint
|
|
||||||
|
|
||||||
- Method: `PUT`
|
|
||||||
- Path: `/api/admin/positions/:board`
|
|
||||||
- Auth: admin bearer token (`Authorization: Bearer <token>`)
|
|
||||||
|
|
||||||
### Supported `:board` values
|
|
||||||
|
|
||||||
- `preliminary`
|
|
||||||
- `final`
|
|
||||||
- `prelim_tiebreak`
|
|
||||||
- `final_tiebreak`
|
|
||||||
|
|
||||||
### Request body
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"slots": [
|
|
||||||
{ "playerId": 12, "groupKey": "A", "position": 1 },
|
|
||||||
{ "playerId": 15, "groupKey": "A", "position": 2 },
|
|
||||||
{ "playerId": 20, "groupKey": "B", "position": 1 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Fields:
|
|
||||||
|
|
||||||
- `playerId` (integer, required)
|
|
||||||
- `groupKey` (string, required)
|
|
||||||
- `position` (integer > 0, required)
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- The request replaces all saved slots for the target board.
|
|
||||||
- For `preliminary`, `players.group_code` is synchronized from `groupKey`.
|
|
||||||
- Special value `UNASSIGNED` maps to empty player group.
|
|
||||||
|
|
||||||
### Response
|
|
||||||
|
|
||||||
- `200 OK` with full updated admin state payload.
|
|
||||||
|
|
||||||
## 2) Stage Session Control Endpoints
|
|
||||||
|
|
||||||
### Start stage
|
|
||||||
|
|
||||||
- Method: `POST`
|
|
||||||
- Path: `/api/admin/stage/start`
|
|
||||||
- Auth: admin bearer token
|
|
||||||
|
|
||||||
Request body:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"phase": "preliminary",
|
|
||||||
"group": "A",
|
|
||||||
"round": 2
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- `phase` must be one of:
|
|
||||||
- `preliminary`
|
|
||||||
- `prelim_tiebreak`
|
|
||||||
- `final`
|
|
||||||
- `final_tiebreak`
|
|
||||||
- `group` validation depends on phase:
|
|
||||||
- `preliminary`: group code string (or `UNASSIGNED`)
|
|
||||||
- `final`: `"1"` or `"2"`
|
|
||||||
- tie-break phases: positive numeric string
|
|
||||||
- `round` normalization:
|
|
||||||
- `preliminary`: `1..3`
|
|
||||||
- `final`: `1..2`
|
|
||||||
- tie-break phases: always `1`
|
|
||||||
- If another stage is active (started, not ended), API returns `409`.
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
- `200 OK` with full updated admin state payload.
|
|
||||||
|
|
||||||
### End stage
|
|
||||||
|
|
||||||
- Method: `POST`
|
|
||||||
- Path: `/api/admin/stage/end`
|
|
||||||
- Auth: admin bearer token
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- Ends the currently active stage by setting `ended_at`.
|
|
||||||
- If no active stage exists, API returns `400`.
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
- `200 OK` with full updated admin state payload.
|
|
||||||
|
|
||||||
## 3) Stage History Endpoint
|
|
||||||
|
|
||||||
- Method: `GET`
|
|
||||||
- Path: `/api/admin/stage/history`
|
|
||||||
- Auth: admin bearer token
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"phase": "preliminary",
|
|
||||||
"group": "A",
|
|
||||||
"round": 2,
|
|
||||||
"startedAt": "2026-04-30T08:27:13Z",
|
|
||||||
"endedAt": "2026-04-30T08:28:01Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4) New State Fields
|
|
||||||
|
|
||||||
Both endpoints now include these fields:
|
|
||||||
|
|
||||||
- `GET /api/state`
|
|
||||||
- `GET /api/admin/state`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"positionBoards": {
|
|
||||||
"preliminary": {
|
|
||||||
"12": { "groupKey": "A", "position": 1 }
|
|
||||||
},
|
|
||||||
"final": {
|
|
||||||
"12": { "groupKey": "1", "position": 3 }
|
|
||||||
},
|
|
||||||
"prelim_tiebreak": {},
|
|
||||||
"final_tiebreak": {}
|
|
||||||
},
|
|
||||||
"current_stage": {
|
|
||||||
"id": 7,
|
|
||||||
"phase": "preliminary",
|
|
||||||
"group": "A",
|
|
||||||
"round": 2,
|
|
||||||
"startedAt": "2026-04-30T09:00:00Z"
|
|
||||||
},
|
|
||||||
"last_stage": {
|
|
||||||
"id": 6,
|
|
||||||
"phase": "final",
|
|
||||||
"group": "1",
|
|
||||||
"round": 1,
|
|
||||||
"startedAt": "2026-04-30T08:45:00Z",
|
|
||||||
"endedAt": "2026-04-30T08:55:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- `positionBoards` keys are player IDs as strings.
|
|
||||||
- `current_stage` is `null` when no stage is active.
|
|
||||||
- `last_stage` is `null` when no stage session has ever started.
|
|
||||||
- `last_stage` is the most recently started session (ended or still active).
|
|
||||||
|
|
||||||
## Compatibility Impact
|
|
||||||
|
|
||||||
- Additive only; existing consumers remain functional.
|
|
||||||
- Consumers can ignore unknown fields safely.
|
|
||||||
- Consumers needing control/monitoring can use new stage endpoints and fields.
|
|
||||||
22
Dockerfile
@@ -1,22 +0,0 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
|
||||||
|
|
||||||
# FROM --platform=$BUILDPLATFORM golang:1.24 AS backend-builder
|
|
||||||
# WORKDIR /app/backend
|
|
||||||
# COPY backend/go.mod backend/go.sum ./
|
|
||||||
# RUN go mod download
|
|
||||||
# COPY backend/ ./
|
|
||||||
# ARG TARGETARCH
|
|
||||||
# RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH:-amd64} go build -trimpath -ldflags="-s -w" -o /out/shooting-event .
|
|
||||||
|
|
||||||
FROM --platform=$TARGETPLATFORM alpine:3.21
|
|
||||||
RUN addgroup -S app && adduser -S app -G app && apk add --no-cache ca-certificates
|
|
||||||
WORKDIR /app
|
|
||||||
COPY bin/shooting-event-amd64 /app/shooting-event
|
|
||||||
COPY frontend/dist /app/web
|
|
||||||
RUN mkdir -p /app/data && chown -R app:app /app
|
|
||||||
USER app
|
|
||||||
ENV PORT=8080
|
|
||||||
ENV DB_PATH=/app/data/shooting.db
|
|
||||||
ENV WEB_DIR=/app/web
|
|
||||||
EXPOSE 8080
|
|
||||||
ENTRYPOINT ["/app/shooting-event"]
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM node:20-alpine AS frontend-builder
|
|
||||||
WORKDIR /app/frontend
|
|
||||||
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
|
||||||
RUN corepack enable && pnpm install --frozen-lockfile
|
|
||||||
COPY frontend/ ./
|
|
||||||
RUN pnpm build
|
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM elixir:1.18.4-otp-28 AS phoenix-builder
|
|
||||||
ENV MIX_ENV=prod
|
|
||||||
WORKDIR /app/phoenix
|
|
||||||
|
|
||||||
RUN mix local.hex --force && mix local.rebar --force
|
|
||||||
|
|
||||||
COPY phoenix/mix.exs phoenix/mix.lock ./
|
|
||||||
RUN mix deps.get --only prod
|
|
||||||
RUN mix deps.compile
|
|
||||||
|
|
||||||
COPY phoenix/config ./config
|
|
||||||
COPY phoenix/lib ./lib
|
|
||||||
COPY phoenix/priv ./priv
|
|
||||||
RUN mix compile
|
|
||||||
RUN mix release
|
|
||||||
|
|
||||||
FROM --platform=$TARGETPLATFORM elixir:1.18.4-otp-28
|
|
||||||
RUN groupadd -r app && useradd -r -g app app
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY --from=phoenix-builder /app/phoenix/_build/prod/rel/shooting_event_phx /app
|
|
||||||
COPY --from=frontend-builder /app/frontend/dist /app/web
|
|
||||||
|
|
||||||
RUN mkdir -p /app/data && chown -R app:app /app
|
|
||||||
USER app
|
|
||||||
|
|
||||||
ENV PHX_SERVER=true
|
|
||||||
ENV PORT=8080
|
|
||||||
ENV DB_PATH=/app/data/shooting.db
|
|
||||||
ENV WEB_DIR=/app/web
|
|
||||||
ENV SECRET_KEY_BASE=pxh_v1_V2Y3TXF4Rk9OUm5FdlVQc2s3M2hqd0t1Q2pwYUx4N3Vjb2I5Wk1qXzJ5RWRmQnJ
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
ENTRYPOINT ["/app/bin/shooting_event_phx"]
|
|
||||||
CMD ["start"]
|
|
||||||
83
Makefile
@@ -1,83 +0,0 @@
|
|||||||
SHELL := /bin/bash
|
|
||||||
|
|
||||||
PNPM ?= pnpm
|
|
||||||
GO ?= go
|
|
||||||
CONTAINER_REG ?= repo.ssp-itinfra.com/admin
|
|
||||||
IMAGE_NAME ?= shooting-event-2
|
|
||||||
PHOENIX_IMAGE_NAME ?= $(IMAGE_NAME)-phoenix
|
|
||||||
ARCH ?= amd64
|
|
||||||
BUILD_DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
BUILD_VERSION ?= $(shell git describe --tags --always --dirty)
|
|
||||||
|
|
||||||
.PHONY: install dev dev-backend dev-frontend build build-frontend build-backend phoenix-dev phoenix-build docker-build docker-run docker-build-phoenix docker-run-phoenix clean
|
|
||||||
|
|
||||||
install:
|
|
||||||
cd frontend && $(PNPM) install
|
|
||||||
cd backend && $(GO) mod tidy
|
|
||||||
cd phoenix && mix deps.get
|
|
||||||
|
|
||||||
dev-backend:
|
|
||||||
cd backend && $(GO) run .
|
|
||||||
|
|
||||||
dev-frontend:
|
|
||||||
cd frontend && $(PNPM) dev
|
|
||||||
|
|
||||||
dev:
|
|
||||||
@set -euo pipefail; \
|
|
||||||
trap 'kill 0' INT TERM EXIT; \
|
|
||||||
$(MAKE) dev-backend & \
|
|
||||||
$(MAKE) dev-frontend & \
|
|
||||||
wait
|
|
||||||
|
|
||||||
build-frontend:
|
|
||||||
cd frontend && $(PNPM) build
|
|
||||||
|
|
||||||
build-backend:
|
|
||||||
mkdir -p bin
|
|
||||||
cd backend && $(GO) build -o ../bin/shooting-event .
|
|
||||||
|
|
||||||
build-backend-amd64:
|
|
||||||
mkdir -p bin
|
|
||||||
cd backend && GOARCH=amd64 GOOS=linux $(GO) build -o ../bin/shooting-event-amd64 .
|
|
||||||
build: build-frontend
|
|
||||||
rm -rf backend/web
|
|
||||||
mkdir -p backend/web
|
|
||||||
cp -R frontend/dist/. backend/web/
|
|
||||||
$(MAKE) build-backend
|
|
||||||
|
|
||||||
phoenix-dev:
|
|
||||||
cd phoenix && mix deps.get && mix ecto.create && mix ecto.migrate && PORT=$${PORT:-8080} mix phx.server
|
|
||||||
|
|
||||||
phoenix-build:
|
|
||||||
cd phoenix && MIX_ENV=prod mix deps.get --only prod && MIX_ENV=prod mix compile
|
|
||||||
|
|
||||||
docker-build:
|
|
||||||
docker buildx build --load --platform=linux/$(ARCH) -t $(CONTAINER_REG)/$(IMAGE_NAME):$(ARCH) .
|
|
||||||
docker tag $(CONTAINER_REG)/$(IMAGE_NAME):$(ARCH) $(CONTAINER_REG)/$(IMAGE_NAME):$(ARCH)-latest
|
|
||||||
docker tag $(CONTAINER_REG)/$(IMAGE_NAME):$(ARCH) $(CONTAINER_REG)/$(IMAGE_NAME):$(ARCH)-$(BUILD_VERSION)
|
|
||||||
docker push $(CONTAINER_REG)/$(IMAGE_NAME):$(ARCH)
|
|
||||||
docker push $(CONTAINER_REG)/$(IMAGE_NAME):$(ARCH)-latest
|
|
||||||
docker push $(CONTAINER_REG)/$(IMAGE_NAME):$(ARCH)-$(BUILD_VERSION)
|
|
||||||
|
|
||||||
docker-run:
|
|
||||||
mkdir -p data
|
|
||||||
docker run --rm -p 8080:8080 -v $(PWD)/data:/app/data $(CONTAINER_REG)/$(IMAGE_NAME):$(ARCH)
|
|
||||||
|
|
||||||
docker-build-phoenix:
|
|
||||||
docker buildx build --load --platform=linux/$(ARCH) -f Dockerfile.phoenix -t $(CONTAINER_REG)/$(PHOENIX_IMAGE_NAME):$(ARCH) .
|
|
||||||
docker tag $(CONTAINER_REG)/$(PHOENIX_IMAGE_NAME):$(ARCH) $(CONTAINER_REG)/$(PHOENIX_IMAGE_NAME):$(ARCH)-latest
|
|
||||||
docker tag $(CONTAINER_REG)/$(PHOENIX_IMAGE_NAME):$(ARCH) $(CONTAINER_REG)/$(PHOENIX_IMAGE_NAME):$(ARCH)-$(BUILD_VERSION)
|
|
||||||
docker push $(CONTAINER_REG)/$(PHOENIX_IMAGE_NAME):$(ARCH)
|
|
||||||
docker push $(CONTAINER_REG)/$(PHOENIX_IMAGE_NAME):$(ARCH)-latest
|
|
||||||
docker push $(CONTAINER_REG)/$(PHOENIX_IMAGE_NAME):$(ARCH)-$(BUILD_VERSION)
|
|
||||||
|
|
||||||
docker-run-phoenix:
|
|
||||||
mkdir -p data
|
|
||||||
docker run --rm -p 8080:8080 -v $(PWD)/data:/app/data $(CONTAINER_REG)/$(PHOENIX_IMAGE_NAME):$(ARCH)
|
|
||||||
|
|
||||||
release:
|
|
||||||
${MAKE} build
|
|
||||||
$(MAKE) docker-build
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -rf bin frontend/dist backend/web
|
|
||||||
161
README.md
@@ -1,161 +0,0 @@
|
|||||||
# Datwyler Shooting Event System
|
|
||||||
|
|
||||||
Production-ready full-stack web app based on your original live score concept.
|
|
||||||
|
|
||||||
## Stack
|
|
||||||
|
|
||||||
- Frontend: Vue 3 + Vite (pnpm)
|
|
||||||
- Backend: Go + Echo
|
|
||||||
- Database: SQLite
|
|
||||||
- Packaging: Single Docker image (frontend + backend)
|
|
||||||
|
|
||||||
## Main Features
|
|
||||||
|
|
||||||
- Bilingual UI: Arabic and English
|
|
||||||
- Runtime RTL/LTR switching
|
|
||||||
- Admin avatar crop/fit tool (drag + zoom before saving)
|
|
||||||
- AI score advisor for proof images (Gemini-powered suggestion + optional apply)
|
|
||||||
- Two clean modes:
|
|
||||||
- View Only screen for players/coaches/audience
|
|
||||||
- Admin Control Panel (login required)
|
|
||||||
- Admin credentials (default):
|
|
||||||
- Username: `datwyler`
|
|
||||||
- Password: `datwyler`
|
|
||||||
|
|
||||||
## Tournament Flow Implemented
|
|
||||||
|
|
||||||
1. Admin registers players and assigns groups (no hard 6-player limit).
|
|
||||||
2. View screen shows group assignment clearly.
|
|
||||||
3. Admin enters preliminary scores.
|
|
||||||
4. Overall ranking auto-calculates and highlights top 12 finalists.
|
|
||||||
5. If rank #12 cutoff is tied, qualification tie-break stage appears.
|
|
||||||
6. Top 12 split into final groups (1-6 and 7-12 seeds).
|
|
||||||
7. Admin enters final scores.
|
|
||||||
8. Podium is determined automatically.
|
|
||||||
9. If top-3 tie exists, podium tie-break stage appears.
|
|
||||||
|
|
||||||
## API Documentation (OpenAPI)
|
|
||||||
|
|
||||||
Interactive docs are now built in:
|
|
||||||
|
|
||||||
- Swagger UI: `GET /api/docs`
|
|
||||||
- OpenAPI JSON: `GET /api/openapi.json`
|
|
||||||
|
|
||||||
### Testing with Auth in Swagger UI
|
|
||||||
|
|
||||||
1. Open `/api/docs`.
|
|
||||||
2. Use the **Login & Authorize** form at the top:
|
|
||||||
- Username: `datwyler` (default)
|
|
||||||
- Password: `datwyler` (default)
|
|
||||||
3. The page calls `POST /api/admin/login`, retrieves the token, and auto-authorizes Swagger.
|
|
||||||
4. Use **Try it out** on any admin endpoint.
|
|
||||||
|
|
||||||
### Stage Values
|
|
||||||
|
|
||||||
For score update/proof/advice endpoints (`/api/admin/scores/{stage}/...`), use:
|
|
||||||
|
|
||||||
- `prelim_r1`
|
|
||||||
- `prelim_r2`
|
|
||||||
- `prelim_r3`
|
|
||||||
- `final_r1`
|
|
||||||
- `final_r2`
|
|
||||||
- `prelim_tiebreak`
|
|
||||||
- `final_tiebreak`
|
|
||||||
|
|
||||||
For reset endpoint (`POST /api/admin/scores/{stage}/reset`), use:
|
|
||||||
|
|
||||||
- `preliminary`
|
|
||||||
- `final`
|
|
||||||
- `prelim_tiebreak`
|
|
||||||
- `final_tiebreak`
|
|
||||||
|
|
||||||
## Local Development
|
|
||||||
|
|
||||||
Install dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make install
|
|
||||||
```
|
|
||||||
|
|
||||||
Run backend + frontend together:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- Backend dev port is `18081`.
|
|
||||||
- Frontend runs on `5173` (or next free port, e.g. `5174` if busy).
|
|
||||||
- Frontend proxy is configured so `/api/*` works from Vite dev server.
|
|
||||||
|
|
||||||
Run individually:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make dev-backend
|
|
||||||
make dev-frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make build
|
|
||||||
```
|
|
||||||
|
|
||||||
This builds frontend assets, copies them into backend `web/`, and compiles backend binary.
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
Build image:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make docker-build ARCH=amd64
|
|
||||||
# or
|
|
||||||
make docker-build ARCH=arm64
|
|
||||||
```
|
|
||||||
|
|
||||||
Run image:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make docker-run ARCH=amd64
|
|
||||||
# or
|
|
||||||
make docker-run ARCH=arm64
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker Compose (Production)
|
|
||||||
|
|
||||||
1. Copy environment template:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Edit `.env` for production credentials/tag.
|
|
||||||
|
|
||||||
3. Start service:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Check health:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
5. View logs:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
## Runtime Environment Variables
|
|
||||||
|
|
||||||
- `PORT` (default `8080`)
|
|
||||||
- `DB_PATH` (default `./data/shooting.db`)
|
|
||||||
- `WEB_DIR` (default `./web`)
|
|
||||||
- `ADMIN_USER` (default `datwyler`)
|
|
||||||
- `ADMIN_PASS` (default `datwyler`)
|
|
||||||
- `GEMINI_API_KEY` (required for AI score advisor)
|
|
||||||
- `GEMINI_MODEL` (default `gemini-2.0-flash`)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
GEMINI_API_KEY=AIzaSyATpv4fmHpjPPLk-BEy4fCBL_r1EWtiWDc
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a *App) handleScoreAdvice(c echo.Context) error {
|
|
||||||
stage, err := validateStage(c.Param("stage"))
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
playerID, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid player id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.TrimSpace(a.cfg.GeminiAPIKey) == "" {
|
|
||||||
return writeError(c, http.StatusServiceUnavailable, "gemini api key is not configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
proofImageData, err := a.loadScoreProofImage(stage, playerID)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return writeError(c, http.StatusBadRequest, "no proof image found for this score")
|
|
||||||
}
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("load proof image: %v", err))
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(proofImageData) == "" {
|
|
||||||
return writeError(c, http.StatusBadRequest, "no proof image found for this score")
|
|
||||||
}
|
|
||||||
|
|
||||||
currentScore, _ := a.loadCurrentScore(stage, playerID)
|
|
||||||
|
|
||||||
advice, err := a.generateScoreAdvice(stage, playerID, proofImageData, currentScore)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusBadGateway, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, advice)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) loadScoreProofImage(stage string, playerID int) (string, error) {
|
|
||||||
var imageData string
|
|
||||||
err := a.db.QueryRow(`SELECT image_data FROM score_attachments WHERE stage = ? AND player_id = ?`, stage, playerID).Scan(&imageData)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return imageData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) loadCurrentScore(stage string, playerID int) (int, error) {
|
|
||||||
var score int
|
|
||||||
err := a.db.QueryRow(`SELECT score FROM scores WHERE stage = ? AND player_id = ?`, stage, playerID).Scan(&score)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return score, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) buildAdviceResponse(stage string, playerID int, raw scoreAdviceModelResponse) ScoreAdviceResponse {
|
|
||||||
advised := clampInt(raw.AdvisedScore, 0, 100)
|
|
||||||
reason := strings.TrimSpace(raw.Reason)
|
|
||||||
if reason == "" {
|
|
||||||
reason = "AI estimated the score from visible impacts."
|
|
||||||
}
|
|
||||||
|
|
||||||
return ScoreAdviceResponse{
|
|
||||||
Stage: stage,
|
|
||||||
PlayerID: playerID,
|
|
||||||
AdvisedScore: advised,
|
|
||||||
Reason: reason,
|
|
||||||
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func clampInt(value, min, max int) int {
|
|
||||||
if value < min {
|
|
||||||
return min
|
|
||||||
}
|
|
||||||
if value > max {
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed openapi/openapi.json
|
|
||||||
var openAPISpecJSON string
|
|
||||||
|
|
||||||
func (a *App) handleOpenAPISpec(c echo.Context) error {
|
|
||||||
return c.Blob(http.StatusOK, "application/json; charset=utf-8", []byte(openAPISpecJSON))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleOpenAPIDocs(c echo.Context) error {
|
|
||||||
html := fmt.Sprintf(`<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>Shooting Event API Docs</title>
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
||||||
background: #f7f9fc;
|
|
||||||
}
|
|
||||||
.docs-top {
|
|
||||||
background: #ffffff;
|
|
||||||
border-bottom: 1px solid #dde5f3;
|
|
||||||
padding: 14px 16px;
|
|
||||||
}
|
|
||||||
.docs-title {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #1b1f40;
|
|
||||||
}
|
|
||||||
.docs-help {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #4c5773;
|
|
||||||
}
|
|
||||||
.auth-panel {
|
|
||||||
margin-top: 12px;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.auth-panel input {
|
|
||||||
height: 36px;
|
|
||||||
border: 1px solid #cdd7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.auth-panel button {
|
|
||||||
height: 36px;
|
|
||||||
border: 1px solid #20284f;
|
|
||||||
background: #20284f;
|
|
||||||
color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.auth-status {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #38446b;
|
|
||||||
}
|
|
||||||
#swagger-ui {
|
|
||||||
max-width: 1300px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<section class="docs-top">
|
|
||||||
<h1 class="docs-title">Shooting Event OpenAPI Docs</h1>
|
|
||||||
<p class="docs-help">
|
|
||||||
Use <strong>Try it out</strong> for live testing. For admin endpoints: login with the form below,
|
|
||||||
then your token is auto-attached to Swagger Authorize.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form id="auth-form" class="auth-panel">
|
|
||||||
<input id="admin-user" type="text" value="datwyler" autocomplete="username" placeholder="Admin username" />
|
|
||||||
<input id="admin-pass" type="password" value="datwyler" autocomplete="current-password" placeholder="Admin password" />
|
|
||||||
<button type="submit">Login & Authorize</button>
|
|
||||||
<span id="auth-status" class="auth-status"></span>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div id="swagger-ui"></div>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
|
||||||
<script>
|
|
||||||
const ui = SwaggerUIBundle({
|
|
||||||
url: %q,
|
|
||||||
dom_id: '#swagger-ui',
|
|
||||||
deepLinking: true,
|
|
||||||
displayRequestDuration: true,
|
|
||||||
persistAuthorization: true,
|
|
||||||
docExpansion: 'list',
|
|
||||||
tryItOutEnabled: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loginAndAuthorize(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const status = document.getElementById('auth-status');
|
|
||||||
status.textContent = 'Logging in...';
|
|
||||||
|
|
||||||
const username = document.getElementById('admin-user').value || '';
|
|
||||||
const password = document.getElementById('admin-pass').value || '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/admin/login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username, password }),
|
|
||||||
});
|
|
||||||
const payload = await response.json().catch(() => ({}));
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(payload.message || 'Login failed');
|
|
||||||
}
|
|
||||||
if (!payload.token) {
|
|
||||||
throw new Error('Login succeeded but token was not returned');
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.preauthorizeApiKey('bearerAuth', payload.token);
|
|
||||||
status.textContent = 'Authorized until ' + (payload.expiresAt || 'token expiry');
|
|
||||||
} catch (error) {
|
|
||||||
status.textContent = 'Auth failed: ' + error.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('auth-form').addEventListener('submit', loginAndAuthorize);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>`, "/api/openapi.json")
|
|
||||||
|
|
||||||
return c.HTML(http.StatusOK, html)
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/subtle"
|
|
||||||
"encoding/hex"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a *App) isAdminRequest(c echo.Context) bool {
|
|
||||||
header := strings.TrimSpace(c.Request().Header.Get(echo.HeaderAuthorization))
|
|
||||||
if header == "" || !strings.HasPrefix(strings.ToLower(header), "bearer ") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
token := strings.TrimSpace(header[7:])
|
|
||||||
if token == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return a.sessions.ValidateToken(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) adminOnly(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
a.sessions.PurgeExpired()
|
|
||||||
|
|
||||||
header := strings.TrimSpace(c.Request().Header.Get(echo.HeaderAuthorization))
|
|
||||||
if header == "" || !strings.HasPrefix(strings.ToLower(header), "bearer ") {
|
|
||||||
return writeError(c, http.StatusUnauthorized, "missing admin token")
|
|
||||||
}
|
|
||||||
token := strings.TrimSpace(header[7:])
|
|
||||||
if token == "" || !a.sessions.ValidateToken(token) {
|
|
||||||
return writeError(c, http.StatusUnauthorized, "invalid or expired admin token")
|
|
||||||
}
|
|
||||||
return next(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) verifyAdmin(username, password string) bool {
|
|
||||||
u := strings.TrimSpace(username)
|
|
||||||
p := strings.TrimSpace(password)
|
|
||||||
if u == "" || p == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
userMatch := subtle.ConstantTimeCompare([]byte(u), []byte(a.cfg.AdminUser)) == 1
|
|
||||||
passMatch := subtle.ConstantTimeCompare([]byte(p), []byte(a.cfg.AdminPass)) == 1
|
|
||||||
return userMatch && passMatch
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SessionStore) CreateToken() (string, time.Time, error) {
|
|
||||||
raw := make([]byte, 32)
|
|
||||||
if _, err := rand.Read(raw); err != nil {
|
|
||||||
return "", time.Time{}, err
|
|
||||||
}
|
|
||||||
token := hex.EncodeToString(raw)
|
|
||||||
expires := time.Now().UTC().Add(s.duration)
|
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
s.tokens[token] = expires
|
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
return token, expires, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SessionStore) ValidateToken(token string) bool {
|
|
||||||
s.mu.RLock()
|
|
||||||
expires, ok := s.tokens[token]
|
|
||||||
s.mu.RUnlock()
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return time.Now().UTC().Before(expires)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SessionStore) DeleteToken(token string) {
|
|
||||||
s.mu.Lock()
|
|
||||||
delete(s.tokens, token)
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SessionStore) PurgeExpired() {
|
|
||||||
now := time.Now().UTC()
|
|
||||||
s.mu.Lock()
|
|
||||||
for token, expiry := range s.tokens {
|
|
||||||
if now.After(expiry) {
|
|
||||||
delete(s.tokens, token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
||||||
BIN
backend/backend
Executable file
@@ -1,36 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Port string
|
|
||||||
DBPath string
|
|
||||||
WebDir string
|
|
||||||
AdminUser string
|
|
||||||
AdminPass string
|
|
||||||
GeminiAPIKey string
|
|
||||||
GeminiModel string
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadConfig() Config {
|
|
||||||
return Config{
|
|
||||||
Port: envOrDefault("PORT", "8080"),
|
|
||||||
DBPath: envOrDefault("DB_PATH", "./data/shooting.db"),
|
|
||||||
WebDir: envOrDefault("WEB_DIR", "./web"),
|
|
||||||
AdminUser: envOrDefault("ADMIN_USER", "datwyler"),
|
|
||||||
AdminPass: envOrDefault("ADMIN_PASS", "datwyler"),
|
|
||||||
GeminiAPIKey: envOrDefault("GEMINI_API_KEY", "AIzaSyATpv4fmHpjPPLk-BEy4fCBL_r1EWtiWDc"),
|
|
||||||
GeminiModel: envOrDefault("GEMINI_MODEL", "gemini-3.1-flash-lite-preview"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func envOrDefault(key, fallback string) string {
|
|
||||||
v := strings.TrimSpace(os.Getenv(key))
|
|
||||||
if v == "" {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
BIN
backend/data/shooting.db
Normal file
BIN
backend/data/shooting.db-shm
Normal file
BIN
backend/data/shooting.db-wal
Normal file
118
backend/db.go
@@ -1,118 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initDB(cfg Config) (*sql.DB, error) {
|
|
||||||
if err := os.MkdirAll(filepath.Dir(cfg.DBPath), 0o755); err != nil {
|
|
||||||
return nil, fmt.Errorf("create data dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := sql.Open("sqlite", cfg.DBPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("open sqlite: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
db.SetMaxOpenConns(1)
|
|
||||||
db.SetMaxIdleConns(1)
|
|
||||||
|
|
||||||
if _, err := db.Exec(`
|
|
||||||
PRAGMA foreign_keys = ON;
|
|
||||||
PRAGMA journal_mode = WAL;
|
|
||||||
PRAGMA busy_timeout = 5000;
|
|
||||||
`); err != nil {
|
|
||||||
return nil, fmt.Errorf("sqlite pragmas: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
schema := `
|
|
||||||
CREATE TABLE IF NOT EXISTS players (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name_ar TEXT NOT NULL,
|
|
||||||
name_en TEXT NOT NULL,
|
|
||||||
group_code TEXT NOT NULL DEFAULT '',
|
|
||||||
image_data TEXT NOT NULL DEFAULT '',
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS scores (
|
|
||||||
stage TEXT NOT NULL,
|
|
||||||
player_id INTEGER NOT NULL,
|
|
||||||
score INTEGER NOT NULL DEFAULT 0,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY(stage, player_id),
|
|
||||||
FOREIGN KEY(player_id) REFERENCES players(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS score_attachments (
|
|
||||||
stage TEXT NOT NULL,
|
|
||||||
player_id INTEGER NOT NULL,
|
|
||||||
image_data TEXT NOT NULL DEFAULT '',
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY(stage, player_id),
|
|
||||||
FOREIGN KEY(player_id) REFERENCES players(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS player_positions (
|
|
||||||
board TEXT NOT NULL,
|
|
||||||
player_id INTEGER NOT NULL,
|
|
||||||
group_key TEXT NOT NULL DEFAULT '',
|
|
||||||
position INTEGER NOT NULL DEFAULT 1,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY(board, player_id),
|
|
||||||
FOREIGN KEY(player_id) REFERENCES players(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS app_settings (
|
|
||||||
key TEXT PRIMARY KEY,
|
|
||||||
value TEXT NOT NULL DEFAULT '',
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS stage_sessions (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
phase TEXT NOT NULL,
|
|
||||||
group_key TEXT NOT NULL DEFAULT '',
|
|
||||||
round INTEGER NOT NULL DEFAULT 1,
|
|
||||||
started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
ended_at DATETIME
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT OR IGNORE INTO app_settings(key, value) VALUES
|
|
||||||
('view_proof_in_view', '0'),
|
|
||||||
('live_active_view', 'group_live'),
|
|
||||||
('live_show_group_live', '1'),
|
|
||||||
('live_group_display_mode', 'rotate'),
|
|
||||||
('live_group_fixed_code', ''),
|
|
||||||
('live_show_prelim_tie', '1'),
|
|
||||||
('live_show_prelim_overall', '1'),
|
|
||||||
('live_show_final_groups', '1'),
|
|
||||||
('live_final_display_mode', 'rotate'),
|
|
||||||
('live_final_fixed_group', '1'),
|
|
||||||
('live_show_final_tie', '1'),
|
|
||||||
('live_show_podium', '1'),
|
|
||||||
('live_rotation_interval_sec', '5'),
|
|
||||||
('live_rotation_player_count', '12');
|
|
||||||
|
|
||||||
INSERT OR IGNORE INTO scores(stage, player_id, score)
|
|
||||||
SELECT 'prelim_r1', player_id, score FROM scores WHERE stage = 'preliminary';
|
|
||||||
INSERT OR IGNORE INTO scores(stage, player_id, score)
|
|
||||||
SELECT 'final_r1', player_id, score FROM scores WHERE stage = 'final';
|
|
||||||
|
|
||||||
INSERT OR IGNORE INTO score_attachments(stage, player_id, image_data)
|
|
||||||
SELECT 'prelim_r1', player_id, image_data FROM score_attachments WHERE stage = 'preliminary';
|
|
||||||
INSERT OR IGNORE INTO score_attachments(stage, player_id, image_data)
|
|
||||||
SELECT 'final_r1', player_id, image_data FROM score_attachments WHERE stage = 'final';
|
|
||||||
`
|
|
||||||
if _, err := db.Exec(schema); err != nil {
|
|
||||||
return nil, fmt.Errorf("apply schema: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a *App) handleEvents(c echo.Context) error {
|
|
||||||
res := c.Response()
|
|
||||||
req := c.Request()
|
|
||||||
|
|
||||||
res.Header().Set(echo.HeaderContentType, "text/event-stream")
|
|
||||||
res.Header().Set("Cache-Control", "no-cache, no-transform")
|
|
||||||
res.Header().Set("Connection", "keep-alive")
|
|
||||||
res.Header().Set("X-Accel-Buffering", "no")
|
|
||||||
res.WriteHeader(http.StatusOK)
|
|
||||||
|
|
||||||
flusher, ok := res.Writer.(http.Flusher)
|
|
||||||
if !ok {
|
|
||||||
return writeError(c, http.StatusInternalServerError, "streaming not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := fmt.Fprintf(res, "event: ready\ndata: {\"ts\":\"%s\"}\n\n", time.Now().UTC().Format(time.RFC3339)); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
flusher.Flush()
|
|
||||||
|
|
||||||
id, events := a.events.Subscribe()
|
|
||||||
defer a.events.Unsubscribe(id)
|
|
||||||
|
|
||||||
keepAlive := time.NewTicker(20 * time.Second)
|
|
||||||
defer keepAlive.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-req.Context().Done():
|
|
||||||
return nil
|
|
||||||
case <-events:
|
|
||||||
if _, err := fmt.Fprintf(res, "event: state\ndata: {\"ts\":\"%s\"}\n\n", time.Now().UTC().Format(time.RFC3339)); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
flusher.Flush()
|
|
||||||
case t := <-keepAlive.C:
|
|
||||||
if _, err := fmt.Fprintf(res, ": ping %d\n\n", t.Unix()); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
flusher.Flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *EventHub) Subscribe() (int, <-chan struct{}) {
|
|
||||||
h.mu.Lock()
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
h.nextID++
|
|
||||||
id := h.nextID
|
|
||||||
ch := make(chan struct{}, 1)
|
|
||||||
h.subscribers[id] = ch
|
|
||||||
return id, ch
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *EventHub) Unsubscribe(id int) {
|
|
||||||
h.mu.Lock()
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
if ch, ok := h.subscribers[id]; ok {
|
|
||||||
delete(h.subscribers, id)
|
|
||||||
close(ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *EventHub) Broadcast() {
|
|
||||||
h.mu.RLock()
|
|
||||||
defer h.mu.RUnlock()
|
|
||||||
for _, ch := range h.subscribers {
|
|
||||||
select {
|
|
||||||
case ch <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type scoreAdviceModelResponse struct {
|
|
||||||
AdvisedScore int `json:"advisedScore"`
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type geminiGenerateRequest struct {
|
|
||||||
Contents []geminiContent `json:"contents"`
|
|
||||||
GenerationConfig geminiGenerationConfig `json:"generationConfig,omitempty"`
|
|
||||||
SafetySettings []map[string]interface{} `json:"safetySettings,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type geminiContent struct {
|
|
||||||
Role string `json:"role,omitempty"`
|
|
||||||
Parts []geminiPart `json:"parts"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type geminiPart struct {
|
|
||||||
Text string `json:"text,omitempty"`
|
|
||||||
InlineData *geminiInlineData `json:"inline_data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type geminiInlineData struct {
|
|
||||||
MimeType string `json:"mime_type"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type geminiGenerationConfig struct {
|
|
||||||
Temperature float64 `json:"temperature,omitempty"`
|
|
||||||
ResponseMimeType string `json:"responseMimeType,omitempty"`
|
|
||||||
MaxOutputTokens int `json:"maxOutputTokens,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type geminiGenerateResponse struct {
|
|
||||||
Candidates []struct {
|
|
||||||
Content struct {
|
|
||||||
Parts []struct {
|
|
||||||
Text string `json:"text"`
|
|
||||||
} `json:"parts"`
|
|
||||||
} `json:"content"`
|
|
||||||
} `json:"candidates"`
|
|
||||||
Error *struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
} `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) generateScoreAdvice(stage string, playerID int, imageData string, currentScore int) (ScoreAdviceResponse, error) {
|
|
||||||
mimeType, rawBase64, err := parseDataURI(imageData)
|
|
||||||
if err != nil {
|
|
||||||
return ScoreAdviceResponse{}, fmt.Errorf("invalid proof image data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
model := strings.TrimSpace(a.cfg.GeminiModel)
|
|
||||||
if model == "" {
|
|
||||||
model = "gemini-2.0-flash"
|
|
||||||
}
|
|
||||||
|
|
||||||
requestPayload := geminiGenerateRequest{
|
|
||||||
Contents: []geminiContent{
|
|
||||||
{
|
|
||||||
Role: "user",
|
|
||||||
Parts: []geminiPart{
|
|
||||||
{Text: scoreAdvicePrompt(stage, currentScore)},
|
|
||||||
{
|
|
||||||
InlineData: &geminiInlineData{
|
|
||||||
MimeType: mimeType,
|
|
||||||
Data: rawBase64,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GenerationConfig: geminiGenerationConfig{
|
|
||||||
Temperature: 0,
|
|
||||||
ResponseMimeType: "application/json",
|
|
||||||
MaxOutputTokens: 100,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := json.Marshal(requestPayload)
|
|
||||||
if err != nil {
|
|
||||||
return ScoreAdviceResponse{}, fmt.Errorf("marshal gemini request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint := fmt.Sprintf(
|
|
||||||
"https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent?key=%s",
|
|
||||||
url.PathEscape(model),
|
|
||||||
url.QueryEscape(a.cfg.GeminiAPIKey),
|
|
||||||
)
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return ScoreAdviceResponse{}, fmt.Errorf("create gemini request: %w", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: 25 * time.Second}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return ScoreAdviceResponse{}, fmt.Errorf("call gemini api: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 2_000_000))
|
|
||||||
if err != nil {
|
|
||||||
return ScoreAdviceResponse{}, fmt.Errorf("read gemini response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode >= 300 {
|
|
||||||
return ScoreAdviceResponse{}, fmt.Errorf("gemini api status %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
|
|
||||||
}
|
|
||||||
|
|
||||||
var generated geminiGenerateResponse
|
|
||||||
if err := json.Unmarshal(respBody, &generated); err != nil {
|
|
||||||
return ScoreAdviceResponse{}, fmt.Errorf("decode gemini response: %w", err)
|
|
||||||
}
|
|
||||||
if generated.Error != nil && strings.TrimSpace(generated.Error.Message) != "" {
|
|
||||||
return ScoreAdviceResponse{}, fmt.Errorf("gemini api error: %s", generated.Error.Message)
|
|
||||||
}
|
|
||||||
if len(generated.Candidates) == 0 || len(generated.Candidates[0].Content.Parts) == 0 {
|
|
||||||
return ScoreAdviceResponse{}, fmt.Errorf("gemini returned no advice")
|
|
||||||
}
|
|
||||||
|
|
||||||
rawText := strings.TrimSpace(generated.Candidates[0].Content.Parts[0].Text)
|
|
||||||
jsonText := extractJSONObject(rawText)
|
|
||||||
if jsonText == "" {
|
|
||||||
return ScoreAdviceResponse{}, fmt.Errorf("gemini response was not valid json")
|
|
||||||
}
|
|
||||||
|
|
||||||
var modelResponse scoreAdviceModelResponse
|
|
||||||
if err := json.Unmarshal([]byte(jsonText), &modelResponse); err != nil {
|
|
||||||
return ScoreAdviceResponse{}, fmt.Errorf("parse gemini advice json: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.buildAdviceResponse(stage, playerID, modelResponse), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseDataURI(dataURI string) (string, string, error) {
|
|
||||||
value := strings.TrimSpace(dataURI)
|
|
||||||
if !strings.HasPrefix(value, "data:") {
|
|
||||||
return "", "", fmt.Errorf("expected data uri")
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(value, ",", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return "", "", fmt.Errorf("invalid data uri format")
|
|
||||||
}
|
|
||||||
|
|
||||||
header := parts[0]
|
|
||||||
payload := strings.TrimSpace(parts[1])
|
|
||||||
if payload == "" {
|
|
||||||
return "", "", fmt.Errorf("empty data uri payload")
|
|
||||||
}
|
|
||||||
|
|
||||||
mimeType := "image/jpeg"
|
|
||||||
if semicolon := strings.Index(header, ";"); semicolon > len("data:") {
|
|
||||||
mimeType = strings.TrimSpace(header[len("data:"):semicolon])
|
|
||||||
}
|
|
||||||
if mimeType == "" {
|
|
||||||
mimeType = "image/jpeg"
|
|
||||||
}
|
|
||||||
|
|
||||||
return mimeType, payload, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractJSONObject(raw string) string {
|
|
||||||
text := strings.TrimSpace(raw)
|
|
||||||
if text == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(text, "```") {
|
|
||||||
text = strings.TrimPrefix(text, "```json")
|
|
||||||
text = strings.TrimPrefix(text, "```")
|
|
||||||
text = strings.TrimSuffix(strings.TrimSpace(text), "```")
|
|
||||||
text = strings.TrimSpace(text)
|
|
||||||
}
|
|
||||||
start := strings.Index(text, "{")
|
|
||||||
end := strings.LastIndex(text, "}")
|
|
||||||
if start == -1 || end == -1 || end <= start {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(text[start : end+1])
|
|
||||||
}
|
|
||||||
|
|
||||||
func scoreAdvicePrompt(stage string, currentScore int) string {
|
|
||||||
return fmt.Sprintf(`Target scoring assistant.
|
|
||||||
Stage: %s. Current score: %d.
|
|
||||||
Return STRICT JSON only:
|
|
||||||
{"advisedScore":<int 0..100>,"reason":"<max 18 words>"}
|
|
||||||
Do not add markdown or extra fields.`, stage, currentScore)
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
module shooting-event/backend
|
|
||||||
|
|
||||||
go 1.24.0
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/labstack/echo/v4 v4.13.4
|
|
||||||
modernc.org/sqlite v1.39.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/labstack/gommon v0.4.2 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
|
||||||
golang.org/x/crypto v0.38.0 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
|
||||||
golang.org/x/net v0.40.0 // indirect
|
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
|
||||||
golang.org/x/text v0.25.0 // indirect
|
|
||||||
golang.org/x/time v0.11.0 // indirect
|
|
||||||
modernc.org/libc v1.66.10 // indirect
|
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
|
||||||
modernc.org/memory v1.11.0 // indirect
|
|
||||||
)
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
|
|
||||||
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
|
|
||||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
|
||||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
|
||||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
|
||||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
|
||||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
|
||||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
|
||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
|
||||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
|
||||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
|
||||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
|
||||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
|
||||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
|
||||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
|
||||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
|
||||||
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
|
||||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
|
||||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
|
||||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
|
||||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
|
||||||
modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
|
|
||||||
modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
|
||||||
@@ -1,643 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a *App) handleHealth(c echo.Context) error {
|
|
||||||
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleGetState(c echo.Context) error {
|
|
||||||
state, err := a.readState(a.isAdminRequest(c))
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleAdminLogin(c echo.Context) error {
|
|
||||||
var req AdminLoginRequest
|
|
||||||
if err := c.Bind(&req); err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !a.verifyAdmin(req.Username, req.Password) {
|
|
||||||
return writeError(c, http.StatusUnauthorized, "invalid credentials")
|
|
||||||
}
|
|
||||||
|
|
||||||
token, expiry, err := a.sessions.CreateToken()
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, "failed to create session")
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, map[string]any{
|
|
||||||
"token": token,
|
|
||||||
"expiresAt": expiry.Format(time.RFC3339),
|
|
||||||
"username": a.cfg.AdminUser,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleAdminLogout(c echo.Context) error {
|
|
||||||
token := strings.TrimSpace(strings.TrimPrefix(c.Request().Header.Get(echo.HeaderAuthorization), "Bearer "))
|
|
||||||
a.sessions.DeleteToken(token)
|
|
||||||
return c.JSON(http.StatusOK, map[string]bool{"ok": true})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleUpdateAdminSettings(c echo.Context) error {
|
|
||||||
var req AdminSettingsUpdateRequest
|
|
||||||
if err := c.Bind(&req); err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid request body")
|
|
||||||
}
|
|
||||||
if req.ViewProofInView == nil && req.LiveMode == nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "at least one settings field is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.updateSettings(req); err != nil {
|
|
||||||
if strings.Contains(err.Error(), "no settings to update") {
|
|
||||||
return writeError(c, http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
a.events.Broadcast()
|
|
||||||
|
|
||||||
state, err := a.readState(true)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleStartCurrentStage(c echo.Context) error {
|
|
||||||
var req StageControlStartRequest
|
|
||||||
if err := c.Bind(&req); err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
phase, ok := normalizeStagePhase(req.Phase)
|
|
||||||
if !ok {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid phase")
|
|
||||||
}
|
|
||||||
groupKey, ok := normalizeStageGroup(phase, req.GroupKey)
|
|
||||||
if !ok {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid group for selected phase")
|
|
||||||
}
|
|
||||||
round := normalizeStageRound(phase, req.Round)
|
|
||||||
|
|
||||||
current, err := a.readCurrentStage()
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
if current != nil {
|
|
||||||
return writeError(c, http.StatusConflict, "there is already an active stage. end it first")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = a.db.Exec(`
|
|
||||||
INSERT INTO stage_sessions(phase, group_key, round, started_at)
|
|
||||||
VALUES(?, ?, ?, CURRENT_TIMESTAMP)
|
|
||||||
`, phase, groupKey, round)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("start stage: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
a.events.Broadcast()
|
|
||||||
|
|
||||||
state, err := a.readState(true)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleEndCurrentStage(c echo.Context) error {
|
|
||||||
current, err := a.readCurrentStage()
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
if current == nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "no active stage to end")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = a.db.Exec(`
|
|
||||||
UPDATE stage_sessions
|
|
||||||
SET ended_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = ? AND ended_at IS NULL
|
|
||||||
`, current.ID)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("end stage: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
a.events.Broadcast()
|
|
||||||
|
|
||||||
state, err := a.readState(true)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleGetStageHistory(c echo.Context) error {
|
|
||||||
items, err := a.readStageHistory()
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, map[string]any{
|
|
||||||
"items": items,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleCreatePlayer(c echo.Context) error {
|
|
||||||
var req PlayerCreateRequest
|
|
||||||
if err := c.Bind(&req); err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
nameAr := strings.TrimSpace(req.NameAr)
|
|
||||||
nameEn := strings.TrimSpace(req.NameEn)
|
|
||||||
group := normalizeGroup(req.GroupCode)
|
|
||||||
if nameAr == "" || nameEn == "" {
|
|
||||||
return writeError(c, http.StatusBadRequest, "both Arabic and English names are required")
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := a.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, "failed to start transaction")
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
res, err := tx.Exec(`
|
|
||||||
INSERT INTO players(name_ar, name_en, group_code, image_data)
|
|
||||||
VALUES(?, ?, ?, '')
|
|
||||||
`, nameAr, nameEn, group)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("create player: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
id64, err := res.LastInsertId()
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, "failed to read new player id")
|
|
||||||
}
|
|
||||||
playerID := int(id64)
|
|
||||||
|
|
||||||
for _, stage := range scoreStages {
|
|
||||||
if _, err := tx.Exec(`INSERT OR IGNORE INTO scores(stage, player_id, score) VALUES(?, ?, 0)`, stage, playerID); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("create score row: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, "failed to commit create player")
|
|
||||||
}
|
|
||||||
|
|
||||||
a.events.Broadcast()
|
|
||||||
|
|
||||||
state, err := a.readState(true)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(http.StatusCreated, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleUpdatePlayer(c echo.Context) error {
|
|
||||||
playerID, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid player id")
|
|
||||||
}
|
|
||||||
|
|
||||||
var req PlayerUpdateRequest
|
|
||||||
if err := c.Bind(&req); err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
updates := []string{}
|
|
||||||
args := []any{}
|
|
||||||
|
|
||||||
if req.NameAr != nil {
|
|
||||||
nameAr := strings.TrimSpace(*req.NameAr)
|
|
||||||
if nameAr == "" {
|
|
||||||
return writeError(c, http.StatusBadRequest, "arabic name cannot be empty")
|
|
||||||
}
|
|
||||||
updates = append(updates, "name_ar = ?")
|
|
||||||
args = append(args, nameAr)
|
|
||||||
}
|
|
||||||
if req.NameEn != nil {
|
|
||||||
nameEn := strings.TrimSpace(*req.NameEn)
|
|
||||||
if nameEn == "" {
|
|
||||||
return writeError(c, http.StatusBadRequest, "english name cannot be empty")
|
|
||||||
}
|
|
||||||
updates = append(updates, "name_en = ?")
|
|
||||||
args = append(args, nameEn)
|
|
||||||
}
|
|
||||||
if req.GroupCode != nil {
|
|
||||||
updates = append(updates, "group_code = ?")
|
|
||||||
args = append(args, normalizeGroup(*req.GroupCode))
|
|
||||||
}
|
|
||||||
if req.ImageData != nil {
|
|
||||||
img := strings.TrimSpace(*req.ImageData)
|
|
||||||
if len(img) > 2_500_000 {
|
|
||||||
return writeError(c, http.StatusBadRequest, "image payload too large")
|
|
||||||
}
|
|
||||||
updates = append(updates, "image_data = ?")
|
|
||||||
args = append(args, img)
|
|
||||||
}
|
|
||||||
if req.ClearImage {
|
|
||||||
updates = append(updates, "image_data = ''")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(updates) == 0 {
|
|
||||||
return writeError(c, http.StatusBadRequest, "no fields to update")
|
|
||||||
}
|
|
||||||
|
|
||||||
updates = append(updates, "updated_at = CURRENT_TIMESTAMP")
|
|
||||||
args = append(args, playerID)
|
|
||||||
|
|
||||||
query := fmt.Sprintf("UPDATE players SET %s WHERE id = ?", strings.Join(updates, ", "))
|
|
||||||
res, err := a.db.Exec(query, args...)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("update player: %v", err))
|
|
||||||
}
|
|
||||||
affected, err := res.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, "failed to check update result")
|
|
||||||
}
|
|
||||||
if affected == 0 {
|
|
||||||
return writeError(c, http.StatusNotFound, "player not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
a.events.Broadcast()
|
|
||||||
|
|
||||||
state, err := a.readState(true)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleDeletePlayer(c echo.Context) error {
|
|
||||||
playerID, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid player id")
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := a.db.Exec(`DELETE FROM players WHERE id = ?`, playerID)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("delete player: %v", err))
|
|
||||||
}
|
|
||||||
affected, err := res.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, "failed to check delete result")
|
|
||||||
}
|
|
||||||
if affected == 0 {
|
|
||||||
return writeError(c, http.StatusNotFound, "player not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
a.events.Broadcast()
|
|
||||||
|
|
||||||
state, err := a.readState(true)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleAutoGroupPlayers(c echo.Context) error {
|
|
||||||
var req AutoGroupPlayersRequest
|
|
||||||
if err := c.Bind(&req); err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
groups := normalizeGroups(req.Groups)
|
|
||||||
if len(groups) == 0 {
|
|
||||||
return writeError(c, http.StatusBadRequest, "at least one group is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := a.db.Query(`SELECT id FROM players ORDER BY id ASC`)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("query players: %v", err))
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
playerIDs := []int{}
|
|
||||||
for rows.Next() {
|
|
||||||
var id int
|
|
||||||
if err := rows.Scan(&id); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("scan player: %v", err))
|
|
||||||
}
|
|
||||||
playerIDs = append(playerIDs, id)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("read players: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
||||||
rng.Shuffle(len(playerIDs), func(i, j int) {
|
|
||||||
playerIDs[i], playerIDs[j] = playerIDs[j], playerIDs[i]
|
|
||||||
})
|
|
||||||
|
|
||||||
tx, err := a.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, "failed to start transaction")
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
for i, playerID := range playerIDs {
|
|
||||||
groupCode := groups[i%len(groups)]
|
|
||||||
if _, err := tx.Exec(`UPDATE players SET group_code = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, groupCode, playerID); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("assign player group: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, "failed to commit group assignment")
|
|
||||||
}
|
|
||||||
|
|
||||||
a.events.Broadcast()
|
|
||||||
|
|
||||||
state, err := a.readState(true)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleUpdatePositionBoard(c echo.Context) error {
|
|
||||||
board, ok := normalizePositionBoard(c.Param("board"))
|
|
||||||
if !ok {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid position board")
|
|
||||||
}
|
|
||||||
|
|
||||||
var req PositionBoardUpdateRequest
|
|
||||||
if err := c.Bind(&req); err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
seen := map[int]bool{}
|
|
||||||
for _, slot := range req.Slots {
|
|
||||||
if slot.PlayerID <= 0 {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid player id")
|
|
||||||
}
|
|
||||||
if seen[slot.PlayerID] {
|
|
||||||
return writeError(c, http.StatusBadRequest, "duplicate player id in slots")
|
|
||||||
}
|
|
||||||
seen[slot.PlayerID] = true
|
|
||||||
if strings.TrimSpace(slot.GroupKey) == "" {
|
|
||||||
return writeError(c, http.StatusBadRequest, "groupKey is required")
|
|
||||||
}
|
|
||||||
if slot.Position <= 0 {
|
|
||||||
return writeError(c, http.StatusBadRequest, "position must be positive")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := a.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, "failed to start position transaction")
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
if _, err := tx.Exec(`DELETE FROM player_positions WHERE board = ?`, board); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("clear board positions: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, slot := range req.Slots {
|
|
||||||
groupKey := normalizePositionGroupKey(board, slot.GroupKey, "")
|
|
||||||
if _, err := tx.Exec(`
|
|
||||||
INSERT INTO player_positions(board, player_id, group_key, position, updated_at)
|
|
||||||
VALUES(?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
||||||
`, board, slot.PlayerID, groupKey, slot.Position); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("save board position: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if board == positionBoardPreliminary {
|
|
||||||
groupCode := strings.TrimSpace(groupKey)
|
|
||||||
if strings.EqualFold(groupCode, positionBoardUnassigned) {
|
|
||||||
groupCode = ""
|
|
||||||
}
|
|
||||||
if _, err := tx.Exec(`UPDATE players SET group_code = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, groupCode, slot.PlayerID); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("update player group from board: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, "failed to commit positions")
|
|
||||||
}
|
|
||||||
|
|
||||||
a.events.Broadcast()
|
|
||||||
|
|
||||||
state, err := a.readState(true)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleUpdateScore(c echo.Context) error {
|
|
||||||
stage, err := validateStage(c.Param("stage"))
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
playerID, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid player id")
|
|
||||||
}
|
|
||||||
|
|
||||||
var req ScoreUpdateRequest
|
|
||||||
if err := c.Bind(&req); err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid request body")
|
|
||||||
}
|
|
||||||
if req.Score < 0 || req.Score > 100 {
|
|
||||||
return writeError(c, http.StatusBadRequest, "score must be between 0 and 100")
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := a.db.Exec(`
|
|
||||||
INSERT INTO scores(stage, player_id, score)
|
|
||||||
VALUES(?, ?, ?)
|
|
||||||
ON CONFLICT(stage, player_id) DO UPDATE SET score = excluded.score, updated_at = CURRENT_TIMESTAMP
|
|
||||||
`, stage, playerID, req.Score)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("update score: %v", err))
|
|
||||||
}
|
|
||||||
if _, err := res.RowsAffected(); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, "failed to check score update")
|
|
||||||
}
|
|
||||||
|
|
||||||
a.events.Broadcast()
|
|
||||||
|
|
||||||
state, err := a.readState(true)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleResetStageScores(c echo.Context) error {
|
|
||||||
targetStages, err := resolveResetStages(c.Param("stage"))
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
var req ResetStageRequest
|
|
||||||
if err := c.Bind(&req); err != nil && !errors.Is(err, io.EOF) {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := a.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, "failed to start reset transaction")
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
for _, stage := range targetStages {
|
|
||||||
if _, err := tx.Exec(`UPDATE scores SET score = 0, updated_at = CURRENT_TIMESTAMP WHERE stage = ?`, stage); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("reset stage scores: %v", err))
|
|
||||||
}
|
|
||||||
if req.ResetProofs {
|
|
||||||
if _, err := tx.Exec(`DELETE FROM score_attachments WHERE stage = ?`, stage); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("reset stage proofs: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("commit stage reset: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
a.events.Broadcast()
|
|
||||||
|
|
||||||
state, err := a.readState(true)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveResetStages(stage string) ([]string, error) {
|
|
||||||
switch strings.ToLower(strings.TrimSpace(stage)) {
|
|
||||||
case "preliminary":
|
|
||||||
return append([]string{}, preliminaryRoundStages...), nil
|
|
||||||
case "final":
|
|
||||||
stages := append([]string{}, finalRoundStages...)
|
|
||||||
stages = append(stages, "final_tiebreak")
|
|
||||||
return stages, nil
|
|
||||||
case "prelim_tiebreak", "final_tiebreak":
|
|
||||||
normalized, err := validateStage(stage)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return []string{normalized}, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid stage")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleUpdateScoreProof(c echo.Context) error {
|
|
||||||
stage, err := validateStage(c.Param("stage"))
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
playerID, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid player id")
|
|
||||||
}
|
|
||||||
|
|
||||||
var req ScoreProofUpdateRequest
|
|
||||||
if err := c.Bind(&req); err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
imageData := strings.TrimSpace(req.ImageData)
|
|
||||||
if imageData == "" {
|
|
||||||
return writeError(c, http.StatusBadRequest, "imageData is required")
|
|
||||||
}
|
|
||||||
if len(imageData) > 5_000_000 {
|
|
||||||
return writeError(c, http.StatusBadRequest, "image payload too large")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := a.db.Exec(`
|
|
||||||
INSERT INTO score_attachments(stage, player_id, image_data)
|
|
||||||
VALUES(?, ?, ?)
|
|
||||||
ON CONFLICT(stage, player_id) DO UPDATE SET image_data = excluded.image_data, updated_at = CURRENT_TIMESTAMP
|
|
||||||
`, stage, playerID, imageData); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("update score proof: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
a.events.Broadcast()
|
|
||||||
|
|
||||||
state, err := a.readState(true)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) handleDeleteScoreProof(c echo.Context) error {
|
|
||||||
stage, err := validateStage(c.Param("stage"))
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
playerID, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusBadRequest, "invalid player id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := a.db.Exec(`DELETE FROM score_attachments WHERE stage = ? AND player_id = ?`, stage, playerID); err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, fmt.Sprintf("delete score proof: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
a.events.Broadcast()
|
|
||||||
|
|
||||||
state, err := a.readState(true)
|
|
||||||
if err != nil {
|
|
||||||
return writeError(c, http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateStage(stage string) (string, error) {
|
|
||||||
normalized := strings.ToLower(strings.TrimSpace(stage))
|
|
||||||
for _, allowed := range scoreStages {
|
|
||||||
if normalized == allowed {
|
|
||||||
return normalized, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("invalid stage")
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeGroup(group string) string {
|
|
||||||
return strings.TrimSpace(group)
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeGroups(groups []string) []string {
|
|
||||||
seen := map[string]bool{}
|
|
||||||
out := []string{}
|
|
||||||
for _, group := range groups {
|
|
||||||
normalized := normalizeGroup(group)
|
|
||||||
if normalized == "" || seen[normalized] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[normalized] = true
|
|
||||||
out = append(out, normalized)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeError(c echo.Context, status int, message string) error {
|
|
||||||
return c.JSON(status, map[string]string{"message": message})
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/labstack/echo/v4/middleware"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
cfg := loadConfig()
|
|
||||||
|
|
||||||
db, err := initDB(cfg)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("init db: %v", err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
app := &App{
|
|
||||||
db: db,
|
|
||||||
cfg: cfg,
|
|
||||||
sessions: &SessionStore{
|
|
||||||
tokens: map[string]time.Time{},
|
|
||||||
duration: 8 * time.Hour,
|
|
||||||
},
|
|
||||||
events: &EventHub{
|
|
||||||
subscribers: map[int]chan struct{}{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
e := echo.New()
|
|
||||||
e.HideBanner = true
|
|
||||||
e.HidePort = true
|
|
||||||
e.Use(middleware.Recover())
|
|
||||||
e.Use(middleware.Logger())
|
|
||||||
e.Use(middleware.RequestID())
|
|
||||||
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
|
||||||
AllowOrigins: []string{"*"},
|
|
||||||
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodOptions},
|
|
||||||
AllowHeaders: []string{echo.HeaderContentType, echo.HeaderAuthorization},
|
|
||||||
}))
|
|
||||||
|
|
||||||
registerAPIRoutes(e, app)
|
|
||||||
registerWebRoutes(e, cfg)
|
|
||||||
|
|
||||||
addr := ":" + cfg.Port
|
|
||||||
log.Printf("listening on %s", addr)
|
|
||||||
if err := e.Start(addr); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
log.Fatalf("server error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var preliminaryRoundStages = []string{"prelim_r1", "prelim_r2", "prelim_r3"}
|
|
||||||
var finalRoundStages = []string{"final_r1", "final_r2"}
|
|
||||||
var tieBreakStages = []string{"prelim_tiebreak", "final_tiebreak"}
|
|
||||||
|
|
||||||
var scoreStages = append(append(append([]string{}, preliminaryRoundStages...), finalRoundStages...), tieBreakStages...)
|
|
||||||
|
|
||||||
type App struct {
|
|
||||||
db *sql.DB
|
|
||||||
cfg Config
|
|
||||||
sessions *SessionStore
|
|
||||||
events *EventHub
|
|
||||||
}
|
|
||||||
|
|
||||||
type SessionStore struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
tokens map[string]time.Time
|
|
||||||
duration time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventHub struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
nextID int
|
|
||||||
subscribers map[int]chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type CompetitionMeta struct {
|
|
||||||
TitleAr string `json:"titleAr"`
|
|
||||||
TitleEn string `json:"titleEn"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Player struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
NameAr string `json:"nameAr"`
|
|
||||||
NameEn string `json:"nameEn"`
|
|
||||||
GroupCode string `json:"groupCode"`
|
|
||||||
ImageData string `json:"imageData"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RankingRow struct {
|
|
||||||
PlayerID int `json:"playerId"`
|
|
||||||
NameAr string `json:"nameAr"`
|
|
||||||
NameEn string `json:"nameEn"`
|
|
||||||
GroupCode string `json:"groupCode"`
|
|
||||||
ImageData string `json:"imageData"`
|
|
||||||
Score int `json:"score"`
|
|
||||||
TieBreak int `json:"tieBreak"`
|
|
||||||
Rank int `json:"rank"`
|
|
||||||
Seed int `json:"seed"`
|
|
||||||
FinalGroup int `json:"finalGroup"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TieBreakInfo struct {
|
|
||||||
Required bool `json:"required"`
|
|
||||||
Resolved bool `json:"resolved"`
|
|
||||||
Slots int `json:"slots"`
|
|
||||||
PlayerIDs []int `json:"playerIds"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DerivedState struct {
|
|
||||||
PreliminaryRanking RankingBundle `json:"preliminaryRanking"`
|
|
||||||
Finalists []RankingRow `json:"finalists"`
|
|
||||||
FinalGroups FinalGroups `json:"finalGroups"`
|
|
||||||
FinalRanking RankingBundle `json:"finalRanking"`
|
|
||||||
Podium []RankingRow `json:"podium"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RankingBundle struct {
|
|
||||||
Rows []RankingRow `json:"rows"`
|
|
||||||
TieBreak TieBreakInfo `json:"tieBreak"`
|
|
||||||
Unresolved bool `json:"unresolved"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FinalGroups struct {
|
|
||||||
Group1 []RankingRow `json:"group1"`
|
|
||||||
Group2 []RankingRow `json:"group2"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StateResponse struct {
|
|
||||||
Competition CompetitionMeta `json:"competition"`
|
|
||||||
Players []Player `json:"players"`
|
|
||||||
Scores map[string]map[string]int `json:"scores"`
|
|
||||||
ScoreProofs map[string]map[string]string `json:"scoreProofs,omitempty"`
|
|
||||||
PositionBoards map[string]map[string]PositionSlot `json:"positionBoards"`
|
|
||||||
Settings AppSettings `json:"settings"`
|
|
||||||
Derived DerivedState `json:"derived"`
|
|
||||||
CurrentStage *StageSessionState `json:"current_stage"`
|
|
||||||
LastStage *StageSessionState `json:"last_stage"`
|
|
||||||
ServerTime string `json:"serverTime"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StageSessionState struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Phase string `json:"phase"`
|
|
||||||
GroupKey string `json:"group"`
|
|
||||||
Round int `json:"round"`
|
|
||||||
StartedAt string `json:"startedAt"`
|
|
||||||
EndedAt string `json:"endedAt,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PositionSlot struct {
|
|
||||||
GroupKey string `json:"groupKey"`
|
|
||||||
Position int `json:"position"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppSettings struct {
|
|
||||||
ViewProofInView bool `json:"viewProofInView"`
|
|
||||||
LiveMode LiveModeSettings `json:"liveMode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LiveModeSettings struct {
|
|
||||||
ActiveView string `json:"activeView"`
|
|
||||||
ShowGroupLive bool `json:"showGroupLive"`
|
|
||||||
GroupDisplayMode string `json:"groupDisplayMode"`
|
|
||||||
GroupFixedCode string `json:"groupFixedCode"`
|
|
||||||
ShowPrelimTie bool `json:"showPrelimTie"`
|
|
||||||
ShowPrelimOverall bool `json:"showPrelimOverall"`
|
|
||||||
ShowFinalGroups bool `json:"showFinalGroups"`
|
|
||||||
FinalDisplayMode string `json:"finalDisplayMode"`
|
|
||||||
FinalFixedGroup int `json:"finalFixedGroup"`
|
|
||||||
ShowFinalTie bool `json:"showFinalTie"`
|
|
||||||
ShowPodium bool `json:"showPodium"`
|
|
||||||
RotationIntervalSec int `json:"rotationIntervalSec"`
|
|
||||||
RotationPlayerCount int `json:"rotationPlayerCount"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdminLoginRequest struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlayerCreateRequest struct {
|
|
||||||
NameAr string `json:"nameAr"`
|
|
||||||
NameEn string `json:"nameEn"`
|
|
||||||
GroupCode string `json:"groupCode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlayerUpdateRequest struct {
|
|
||||||
NameAr *string `json:"nameAr"`
|
|
||||||
NameEn *string `json:"nameEn"`
|
|
||||||
GroupCode *string `json:"groupCode"`
|
|
||||||
ImageData *string `json:"imageData"`
|
|
||||||
ClearImage bool `json:"clearImage"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ScoreUpdateRequest struct {
|
|
||||||
Score int `json:"score"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ScoreProofUpdateRequest struct {
|
|
||||||
ImageData string `json:"imageData"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdminSettingsUpdateRequest struct {
|
|
||||||
ViewProofInView *bool `json:"viewProofInView"`
|
|
||||||
LiveMode *LiveModeSettingsUpdateRequest `json:"liveMode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LiveModeSettingsUpdateRequest struct {
|
|
||||||
ActiveView *string `json:"activeView"`
|
|
||||||
ShowGroupLive *bool `json:"showGroupLive"`
|
|
||||||
GroupDisplayMode *string `json:"groupDisplayMode"`
|
|
||||||
GroupFixedCode *string `json:"groupFixedCode"`
|
|
||||||
ShowPrelimTie *bool `json:"showPrelimTie"`
|
|
||||||
ShowPrelimOverall *bool `json:"showPrelimOverall"`
|
|
||||||
ShowFinalGroups *bool `json:"showFinalGroups"`
|
|
||||||
FinalDisplayMode *string `json:"finalDisplayMode"`
|
|
||||||
FinalFixedGroup *int `json:"finalFixedGroup"`
|
|
||||||
ShowFinalTie *bool `json:"showFinalTie"`
|
|
||||||
ShowPodium *bool `json:"showPodium"`
|
|
||||||
RotationIntervalSec *int `json:"rotationIntervalSec"`
|
|
||||||
RotationPlayerCount *int `json:"rotationPlayerCount"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResetStageRequest struct {
|
|
||||||
ResetProofs bool `json:"resetProofs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AutoGroupPlayersRequest struct {
|
|
||||||
Groups []string `json:"groups"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PositionBoardUpdateRequest struct {
|
|
||||||
Slots []PositionBoardSlotInput `json:"slots"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PositionBoardSlotInput struct {
|
|
||||||
PlayerID int `json:"playerId"`
|
|
||||||
GroupKey string `json:"groupKey"`
|
|
||||||
Position int `json:"position"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ScoreAdviceResponse struct {
|
|
||||||
Stage string `json:"stage"`
|
|
||||||
PlayerID int `json:"playerId"`
|
|
||||||
AdvisedScore int `json:"advisedScore"`
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
GeneratedAt string `json:"generatedAt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StageControlStartRequest struct {
|
|
||||||
Phase string `json:"phase"`
|
|
||||||
GroupKey string `json:"group"`
|
|
||||||
Round int `json:"round"`
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
func registerAPIRoutes(e *echo.Echo, app *App) {
|
|
||||||
api := e.Group("/api")
|
|
||||||
api.GET("/docs", app.handleOpenAPIDocs)
|
|
||||||
api.GET("/docs/", app.handleOpenAPIDocs)
|
|
||||||
api.GET("/openapi.json", app.handleOpenAPISpec)
|
|
||||||
|
|
||||||
api.GET("/health", app.handleHealth)
|
|
||||||
api.GET("/state", app.handleGetState)
|
|
||||||
api.GET("/events", app.handleEvents)
|
|
||||||
|
|
||||||
api.POST("/admin/login", app.handleAdminLogin)
|
|
||||||
api.POST("/admin/logout", app.handleAdminLogout, app.adminOnly)
|
|
||||||
api.GET("/admin/state", app.handleGetState, app.adminOnly)
|
|
||||||
api.GET("/admin/stage/history", app.handleGetStageHistory, app.adminOnly)
|
|
||||||
api.PUT("/admin/settings", app.handleUpdateAdminSettings, app.adminOnly)
|
|
||||||
api.POST("/admin/stage/start", app.handleStartCurrentStage, app.adminOnly)
|
|
||||||
api.POST("/admin/stage/end", app.handleEndCurrentStage, app.adminOnly)
|
|
||||||
api.POST("/admin/players", app.handleCreatePlayer, app.adminOnly)
|
|
||||||
api.POST("/admin/players/auto-group", app.handleAutoGroupPlayers, app.adminOnly)
|
|
||||||
api.PUT("/admin/positions/:board", app.handleUpdatePositionBoard, app.adminOnly)
|
|
||||||
api.PUT("/admin/players/:id", app.handleUpdatePlayer, app.adminOnly)
|
|
||||||
api.DELETE("/admin/players/:id", app.handleDeletePlayer, app.adminOnly)
|
|
||||||
api.PUT("/admin/scores/:stage/:id", app.handleUpdateScore, app.adminOnly)
|
|
||||||
api.PUT("/admin/scores/:stage/:id/proof", app.handleUpdateScoreProof, app.adminOnly)
|
|
||||||
api.DELETE("/admin/scores/:stage/:id/proof", app.handleDeleteScoreProof, app.adminOnly)
|
|
||||||
api.POST("/admin/scores/:stage/:id/advice", app.handleScoreAdvice, app.adminOnly)
|
|
||||||
api.POST("/admin/scores/:stage/reset", app.handleResetStageScores, app.adminOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerWebRoutes(e *echo.Echo, cfg Config) {
|
|
||||||
e.GET("/*", func(c echo.Context) error {
|
|
||||||
requestPath := strings.TrimPrefix(c.Param("*"), "/")
|
|
||||||
if strings.HasPrefix(requestPath, "api") {
|
|
||||||
return echo.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
if requestPath == "" {
|
|
||||||
requestPath = "index.html"
|
|
||||||
}
|
|
||||||
|
|
||||||
clean := strings.TrimPrefix(filepath.Clean("/"+requestPath), "/")
|
|
||||||
assetPath := filepath.Join(cfg.WebDir, clean)
|
|
||||||
if stat, err := os.Stat(assetPath); err == nil && !stat.IsDir() {
|
|
||||||
return c.File(assetPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
indexPath := filepath.Join(cfg.WebDir, "index.html")
|
|
||||||
if stat, err := os.Stat(indexPath); err == nil && !stat.IsDir() {
|
|
||||||
return c.File(indexPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.String(http.StatusNotFound, "frontend build not found. run make build")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,366 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
settingViewProofInView = "view_proof_in_view"
|
|
||||||
settingLiveActiveView = "live_active_view"
|
|
||||||
settingLiveShowGroupLive = "live_show_group_live"
|
|
||||||
settingLiveGroupDisplayMode = "live_group_display_mode"
|
|
||||||
settingLiveGroupFixedCode = "live_group_fixed_code"
|
|
||||||
settingLiveShowPrelimTie = "live_show_prelim_tie"
|
|
||||||
settingLiveShowPrelimOverall = "live_show_prelim_overall"
|
|
||||||
settingLiveShowFinalGroups = "live_show_final_groups"
|
|
||||||
settingLiveFinalDisplayMode = "live_final_display_mode"
|
|
||||||
settingLiveFinalFixedGroup = "live_final_fixed_group"
|
|
||||||
settingLiveShowFinalTie = "live_show_final_tie"
|
|
||||||
settingLiveShowPodium = "live_show_podium"
|
|
||||||
settingLiveRotationInterval = "live_rotation_interval_sec"
|
|
||||||
settingLiveRotationPlayers = "live_rotation_player_count"
|
|
||||||
)
|
|
||||||
|
|
||||||
func defaultLiveModeSettings() LiveModeSettings {
|
|
||||||
return LiveModeSettings{
|
|
||||||
ActiveView: "group_live",
|
|
||||||
ShowGroupLive: true,
|
|
||||||
GroupDisplayMode: "rotate",
|
|
||||||
GroupFixedCode: "",
|
|
||||||
ShowPrelimTie: true,
|
|
||||||
ShowPrelimOverall: true,
|
|
||||||
ShowFinalGroups: true,
|
|
||||||
FinalDisplayMode: "rotate",
|
|
||||||
FinalFixedGroup: 1,
|
|
||||||
ShowFinalTie: true,
|
|
||||||
ShowPodium: true,
|
|
||||||
RotationIntervalSec: 5,
|
|
||||||
RotationPlayerCount: 12,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) readSettings() (AppSettings, error) {
|
|
||||||
live := defaultLiveModeSettings()
|
|
||||||
|
|
||||||
viewProofRaw, err := a.readSettingValue(settingViewProofInView)
|
|
||||||
if err != nil {
|
|
||||||
return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingViewProofInView, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw, err := a.readSettingValue(settingLiveShowGroupLive); err == nil {
|
|
||||||
live.ShowGroupLive = settingBool(raw)
|
|
||||||
} else {
|
|
||||||
return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveShowGroupLive, err)
|
|
||||||
}
|
|
||||||
if raw, err := a.readSettingValue(settingLiveActiveView); err == nil {
|
|
||||||
live.ActiveView = normalizeLiveActiveView(raw, live.ActiveView)
|
|
||||||
} else {
|
|
||||||
return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveActiveView, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw, err := a.readSettingValue(settingLiveGroupDisplayMode); err == nil {
|
|
||||||
live.GroupDisplayMode = normalizeLiveDisplayMode(raw, live.GroupDisplayMode)
|
|
||||||
} else {
|
|
||||||
return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveGroupDisplayMode, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw, err := a.readSettingValue(settingLiveGroupFixedCode); err == nil {
|
|
||||||
live.GroupFixedCode = strings.ToUpper(strings.TrimSpace(raw))
|
|
||||||
} else {
|
|
||||||
return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveGroupFixedCode, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw, err := a.readSettingValue(settingLiveShowPrelimTie); err == nil {
|
|
||||||
live.ShowPrelimTie = settingBool(raw)
|
|
||||||
} else {
|
|
||||||
return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveShowPrelimTie, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw, err := a.readSettingValue(settingLiveShowPrelimOverall); err == nil {
|
|
||||||
live.ShowPrelimOverall = settingBool(raw)
|
|
||||||
} else {
|
|
||||||
return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveShowPrelimOverall, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw, err := a.readSettingValue(settingLiveShowFinalGroups); err == nil {
|
|
||||||
live.ShowFinalGroups = settingBool(raw)
|
|
||||||
} else {
|
|
||||||
return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveShowFinalGroups, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw, err := a.readSettingValue(settingLiveFinalDisplayMode); err == nil {
|
|
||||||
live.FinalDisplayMode = normalizeLiveDisplayMode(raw, live.FinalDisplayMode)
|
|
||||||
} else {
|
|
||||||
return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveFinalDisplayMode, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw, err := a.readSettingValue(settingLiveFinalFixedGroup); err == nil {
|
|
||||||
live.FinalFixedGroup = normalizeLiveFinalFixedGroup(raw, live.FinalFixedGroup)
|
|
||||||
} else {
|
|
||||||
return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveFinalFixedGroup, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw, err := a.readSettingValue(settingLiveShowFinalTie); err == nil {
|
|
||||||
live.ShowFinalTie = settingBool(raw)
|
|
||||||
} else {
|
|
||||||
return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveShowFinalTie, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw, err := a.readSettingValue(settingLiveShowPodium); err == nil {
|
|
||||||
live.ShowPodium = settingBool(raw)
|
|
||||||
} else {
|
|
||||||
return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveShowPodium, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw, err := a.readSettingValue(settingLiveRotationInterval); err == nil {
|
|
||||||
live.RotationIntervalSec = normalizeRotationInterval(raw, live.RotationIntervalSec)
|
|
||||||
} else {
|
|
||||||
return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveRotationInterval, err)
|
|
||||||
}
|
|
||||||
if raw, err := a.readSettingValue(settingLiveRotationPlayers); err == nil {
|
|
||||||
live.RotationPlayerCount = normalizeRotationPlayerCount(raw, live.RotationPlayerCount)
|
|
||||||
} else {
|
|
||||||
return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveRotationPlayers, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return AppSettings{
|
|
||||||
ViewProofInView: settingBool(viewProofRaw),
|
|
||||||
LiveMode: live,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) updateSettings(req AdminSettingsUpdateRequest) error {
|
|
||||||
tx, err := a.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("begin settings transaction: %w", err)
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
hasUpdate := false
|
|
||||||
|
|
||||||
if req.ViewProofInView != nil {
|
|
||||||
hasUpdate = true
|
|
||||||
if err := upsertSetting(tx, settingViewProofInView, settingString(*req.ViewProofInView)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.LiveMode != nil {
|
|
||||||
patch := req.LiveMode
|
|
||||||
if patch.ActiveView != nil {
|
|
||||||
hasUpdate = true
|
|
||||||
value := normalizeLiveActiveView(*patch.ActiveView, "group_live")
|
|
||||||
if err := upsertSetting(tx, settingLiveActiveView, value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patch.ShowGroupLive != nil {
|
|
||||||
hasUpdate = true
|
|
||||||
if err := upsertSetting(tx, settingLiveShowGroupLive, settingString(*patch.ShowGroupLive)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patch.GroupDisplayMode != nil {
|
|
||||||
hasUpdate = true
|
|
||||||
value := normalizeLiveDisplayMode(*patch.GroupDisplayMode, "rotate")
|
|
||||||
if err := upsertSetting(tx, settingLiveGroupDisplayMode, value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patch.GroupFixedCode != nil {
|
|
||||||
hasUpdate = true
|
|
||||||
value := strings.ToUpper(strings.TrimSpace(*patch.GroupFixedCode))
|
|
||||||
if err := upsertSetting(tx, settingLiveGroupFixedCode, value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patch.ShowPrelimTie != nil {
|
|
||||||
hasUpdate = true
|
|
||||||
if err := upsertSetting(tx, settingLiveShowPrelimTie, settingString(*patch.ShowPrelimTie)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patch.ShowPrelimOverall != nil {
|
|
||||||
hasUpdate = true
|
|
||||||
if err := upsertSetting(tx, settingLiveShowPrelimOverall, settingString(*patch.ShowPrelimOverall)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patch.ShowFinalGroups != nil {
|
|
||||||
hasUpdate = true
|
|
||||||
if err := upsertSetting(tx, settingLiveShowFinalGroups, settingString(*patch.ShowFinalGroups)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patch.FinalDisplayMode != nil {
|
|
||||||
hasUpdate = true
|
|
||||||
value := normalizeLiveDisplayMode(*patch.FinalDisplayMode, "rotate")
|
|
||||||
if err := upsertSetting(tx, settingLiveFinalDisplayMode, value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patch.FinalFixedGroup != nil {
|
|
||||||
hasUpdate = true
|
|
||||||
value := normalizeLiveFinalFixedGroupInt(*patch.FinalFixedGroup)
|
|
||||||
if err := upsertSetting(tx, settingLiveFinalFixedGroup, strconv.Itoa(value)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patch.ShowFinalTie != nil {
|
|
||||||
hasUpdate = true
|
|
||||||
if err := upsertSetting(tx, settingLiveShowFinalTie, settingString(*patch.ShowFinalTie)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patch.ShowPodium != nil {
|
|
||||||
hasUpdate = true
|
|
||||||
if err := upsertSetting(tx, settingLiveShowPodium, settingString(*patch.ShowPodium)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patch.RotationIntervalSec != nil {
|
|
||||||
hasUpdate = true
|
|
||||||
value := normalizeRotationIntervalInt(*patch.RotationIntervalSec)
|
|
||||||
if err := upsertSetting(tx, settingLiveRotationInterval, strconv.Itoa(value)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patch.RotationPlayerCount != nil {
|
|
||||||
hasUpdate = true
|
|
||||||
value := normalizeRotationPlayerCountInt(*patch.RotationPlayerCount)
|
|
||||||
if err := upsertSetting(tx, settingLiveRotationPlayers, strconv.Itoa(value)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasUpdate {
|
|
||||||
return fmt.Errorf("no settings to update")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return fmt.Errorf("commit settings transaction: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func upsertSetting(tx *sql.Tx, key string, value string) error {
|
|
||||||
_, err := tx.Exec(`
|
|
||||||
INSERT INTO app_settings(key, value, updated_at)
|
|
||||||
VALUES(?, ?, CURRENT_TIMESTAMP)
|
|
||||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
|
|
||||||
`, key, value)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("update app setting %s: %w", key, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) readSettingValue(key string) (string, error) {
|
|
||||||
var raw string
|
|
||||||
err := a.db.QueryRow(`SELECT value FROM app_settings WHERE key = ?`, key).Scan(&raw)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func settingBool(value string) bool {
|
|
||||||
switch strings.TrimSpace(strings.ToLower(value)) {
|
|
||||||
case "1", "true", "yes", "on":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func settingString(value bool) string {
|
|
||||||
if value {
|
|
||||||
return "1"
|
|
||||||
}
|
|
||||||
return "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeLiveDisplayMode(value string, fallback string) string {
|
|
||||||
normalized := strings.TrimSpace(strings.ToLower(value))
|
|
||||||
if normalized == "fixed" {
|
|
||||||
return "fixed"
|
|
||||||
}
|
|
||||||
if normalized == "rotate" {
|
|
||||||
return "rotate"
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(strings.ToLower(fallback)) == "fixed" {
|
|
||||||
return "fixed"
|
|
||||||
}
|
|
||||||
return "rotate"
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeLiveFinalFixedGroup(value string, fallback int) int {
|
|
||||||
parsed, err := strconv.Atoi(strings.TrimSpace(value))
|
|
||||||
if err != nil {
|
|
||||||
return normalizeLiveFinalFixedGroupInt(fallback)
|
|
||||||
}
|
|
||||||
return normalizeLiveFinalFixedGroupInt(parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeLiveFinalFixedGroupInt(value int) int {
|
|
||||||
if value == 2 {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeRotationInterval(raw string, fallback int) int {
|
|
||||||
parsed, err := strconv.Atoi(strings.TrimSpace(raw))
|
|
||||||
if err != nil {
|
|
||||||
return normalizeRotationIntervalInt(fallback)
|
|
||||||
}
|
|
||||||
return normalizeRotationIntervalInt(parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeRotationIntervalInt(value int) int {
|
|
||||||
if value < 3 {
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
if value > 30 {
|
|
||||||
return 30
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeRotationPlayerCount(raw string, fallback int) int {
|
|
||||||
parsed, err := strconv.Atoi(strings.TrimSpace(raw))
|
|
||||||
if err != nil {
|
|
||||||
return normalizeRotationPlayerCountInt(fallback)
|
|
||||||
}
|
|
||||||
return normalizeRotationPlayerCountInt(parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeRotationPlayerCountInt(value int) int {
|
|
||||||
if value < 3 {
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
if value > 40 {
|
|
||||||
return 40
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeLiveActiveView(value string, fallback string) string {
|
|
||||||
switch strings.TrimSpace(strings.ToLower(value)) {
|
|
||||||
case "group_live", "prelim_tie", "prelim_overall", "final_groups", "final_tie", "final_overall", "podium":
|
|
||||||
return strings.TrimSpace(strings.ToLower(value))
|
|
||||||
default:
|
|
||||||
if strings.TrimSpace(strings.ToLower(fallback)) != "" {
|
|
||||||
return strings.TrimSpace(strings.ToLower(fallback))
|
|
||||||
}
|
|
||||||
return "group_live"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
stagePhasePreliminary = "preliminary"
|
|
||||||
stagePhasePrelimTie = "prelim_tiebreak"
|
|
||||||
stagePhaseFinal = "final"
|
|
||||||
stagePhaseFinalTie = "final_tiebreak"
|
|
||||||
)
|
|
||||||
|
|
||||||
var allowedStagePhases = map[string]bool{
|
|
||||||
stagePhasePreliminary: true,
|
|
||||||
stagePhasePrelimTie: true,
|
|
||||||
stagePhaseFinal: true,
|
|
||||||
stagePhaseFinalTie: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeStagePhase(raw string) (string, bool) {
|
|
||||||
phase := strings.ToLower(strings.TrimSpace(raw))
|
|
||||||
if !allowedStagePhases[phase] {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return phase, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeStageGroup(phase string, raw string) (string, bool) {
|
|
||||||
group := strings.TrimSpace(raw)
|
|
||||||
switch phase {
|
|
||||||
case stagePhasePreliminary:
|
|
||||||
if strings.EqualFold(group, positionBoardUnassigned) {
|
|
||||||
return positionBoardUnassigned, true
|
|
||||||
}
|
|
||||||
group = normalizePreliminaryGroupKey(group)
|
|
||||||
if group == "" {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return group, true
|
|
||||||
case stagePhaseFinal:
|
|
||||||
parsed, err := strconv.Atoi(group)
|
|
||||||
if err != nil || (parsed != 1 && parsed != 2) {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return strconv.Itoa(parsed), true
|
|
||||||
case stagePhasePrelimTie, stagePhaseFinalTie:
|
|
||||||
parsed, err := strconv.Atoi(group)
|
|
||||||
if err != nil || parsed <= 0 {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return strconv.Itoa(parsed), true
|
|
||||||
default:
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeStageRound(phase string, round int) int {
|
|
||||||
switch phase {
|
|
||||||
case stagePhasePreliminary:
|
|
||||||
if round < 1 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if round > 3 {
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
return round
|
|
||||||
case stagePhaseFinal:
|
|
||||||
if round < 1 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if round > 2 {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
return round
|
|
||||||
default:
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeTimestamp(raw string) string {
|
|
||||||
trimmed := strings.TrimSpace(raw)
|
|
||||||
if trimmed == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if parsed, err := time.Parse(time.RFC3339Nano, trimmed); err == nil {
|
|
||||||
return parsed.UTC().Format(time.RFC3339)
|
|
||||||
}
|
|
||||||
if parsed, err := time.Parse("2006-01-02 15:04:05", trimmed); err == nil {
|
|
||||||
return parsed.UTC().Format(time.RFC3339)
|
|
||||||
}
|
|
||||||
return trimmed
|
|
||||||
}
|
|
||||||
|
|
||||||
func scanStageSession(scanner interface{ Scan(dest ...any) error }) (*StageSessionState, error) {
|
|
||||||
var item StageSessionState
|
|
||||||
var ended sql.NullString
|
|
||||||
if err := scanner.Scan(&item.ID, &item.Phase, &item.GroupKey, &item.Round, &item.StartedAt, &ended); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item.Phase = strings.ToLower(strings.TrimSpace(item.Phase))
|
|
||||||
item.GroupKey = strings.TrimSpace(item.GroupKey)
|
|
||||||
item.StartedAt = normalizeTimestamp(item.StartedAt)
|
|
||||||
if ended.Valid {
|
|
||||||
item.EndedAt = normalizeTimestamp(ended.String)
|
|
||||||
}
|
|
||||||
return &item, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) readCurrentStage() (*StageSessionState, error) {
|
|
||||||
row := a.db.QueryRow(`
|
|
||||||
SELECT id, phase, group_key, round, started_at, ended_at
|
|
||||||
FROM stage_sessions
|
|
||||||
WHERE ended_at IS NULL
|
|
||||||
ORDER BY started_at DESC, id DESC
|
|
||||||
LIMIT 1
|
|
||||||
`)
|
|
||||||
item, err := scanStageSession(row)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("read current stage: %w", err)
|
|
||||||
}
|
|
||||||
return item, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) readLastStage() (*StageSessionState, error) {
|
|
||||||
row := a.db.QueryRow(`
|
|
||||||
SELECT id, phase, group_key, round, started_at, ended_at
|
|
||||||
FROM stage_sessions
|
|
||||||
ORDER BY started_at DESC, id DESC
|
|
||||||
LIMIT 1
|
|
||||||
`)
|
|
||||||
item, err := scanStageSession(row)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("read last stage: %w", err)
|
|
||||||
}
|
|
||||||
return item, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) readStageHistory() ([]StageSessionState, error) {
|
|
||||||
rows, err := a.db.Query(`
|
|
||||||
SELECT id, phase, group_key, round, started_at, ended_at
|
|
||||||
FROM stage_sessions
|
|
||||||
ORDER BY started_at DESC, id DESC
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("query stage history: %w", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
items := make([]StageSessionState, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
item, scanErr := scanStageSession(rows)
|
|
||||||
if scanErr != nil {
|
|
||||||
return nil, fmt.Errorf("scan stage history: %w", scanErr)
|
|
||||||
}
|
|
||||||
items = append(items, *item)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("iterate stage history: %w", err)
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
728
backend/state.go
@@ -1,728 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
positionBoardPreliminary = "preliminary"
|
|
||||||
positionBoardFinal = "final"
|
|
||||||
positionBoardPrelimTie = "prelim_tiebreak"
|
|
||||||
positionBoardFinalTie = "final_tiebreak"
|
|
||||||
positionBoardUnassigned = "UNASSIGNED"
|
|
||||||
)
|
|
||||||
|
|
||||||
var allowedPositionBoards = map[string]bool{
|
|
||||||
positionBoardPreliminary: true,
|
|
||||||
positionBoardFinal: true,
|
|
||||||
positionBoardPrelimTie: true,
|
|
||||||
positionBoardFinalTie: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) readState(includeAllProofs bool) (StateResponse, error) {
|
|
||||||
players, err := a.readPlayers()
|
|
||||||
if err != nil {
|
|
||||||
return StateResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.ensureScoreRows(players); err != nil {
|
|
||||||
return StateResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
settings, err := a.readSettings()
|
|
||||||
if err != nil {
|
|
||||||
return StateResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
scoreMap, err := a.readScores(players)
|
|
||||||
if err != nil {
|
|
||||||
return StateResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
includeScoreProofs := includeAllProofs || settings.ViewProofInView
|
|
||||||
var scoreProofs map[string]map[int]string
|
|
||||||
if includeScoreProofs {
|
|
||||||
scoreProofs, err = a.readScoreProofs(players)
|
|
||||||
if err != nil {
|
|
||||||
return StateResponse{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
derived := computeDerived(players, scoreMap)
|
|
||||||
positionBoards, err := a.readPositionBoards(players, derived)
|
|
||||||
if err != nil {
|
|
||||||
return StateResponse{}, err
|
|
||||||
}
|
|
||||||
currentStage, err := a.readCurrentStage()
|
|
||||||
if err != nil {
|
|
||||||
return StateResponse{}, err
|
|
||||||
}
|
|
||||||
lastStage, err := a.readLastStage()
|
|
||||||
if err != nil {
|
|
||||||
return StateResponse{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
response := StateResponse{
|
|
||||||
Competition: CompetitionMeta{
|
|
||||||
TitleAr: "بطولة دويتوايلر للرماية",
|
|
||||||
TitleEn: "Datwyler Shooting Event",
|
|
||||||
},
|
|
||||||
Players: players,
|
|
||||||
Scores: scoreMapToJSON(scoreMap),
|
|
||||||
PositionBoards: positionBoardMapToJSON(positionBoards),
|
|
||||||
Settings: settings,
|
|
||||||
Derived: derived,
|
|
||||||
CurrentStage: currentStage,
|
|
||||||
LastStage: lastStage,
|
|
||||||
ServerTime: time.Now().UTC().Format(time.RFC3339),
|
|
||||||
}
|
|
||||||
if includeScoreProofs {
|
|
||||||
response.ScoreProofs = scoreProofMapToJSON(scoreProofs)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) readPlayers() ([]Player, error) {
|
|
||||||
rows, err := a.db.Query(`
|
|
||||||
SELECT id, name_ar, name_en, group_code, image_data
|
|
||||||
FROM players
|
|
||||||
ORDER BY id ASC
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("query players: %w", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
players := []Player{}
|
|
||||||
for rows.Next() {
|
|
||||||
var p Player
|
|
||||||
if err := rows.Scan(&p.ID, &p.NameAr, &p.NameEn, &p.GroupCode, &p.ImageData); err != nil {
|
|
||||||
return nil, fmt.Errorf("scan player: %w", err)
|
|
||||||
}
|
|
||||||
players = append(players, p)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("iterate players: %w", err)
|
|
||||||
}
|
|
||||||
return players, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) readScoreProofs(players []Player) (map[string]map[int]string, error) {
|
|
||||||
proofs := map[string]map[int]string{}
|
|
||||||
for _, stage := range scoreStages {
|
|
||||||
proofs[stage] = map[int]string{}
|
|
||||||
}
|
|
||||||
for _, p := range players {
|
|
||||||
for _, stage := range scoreStages {
|
|
||||||
proofs[stage][p.ID] = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := a.db.Query(`SELECT stage, player_id, image_data FROM score_attachments`)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("query score attachments: %w", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var stage string
|
|
||||||
var playerID int
|
|
||||||
var imageData string
|
|
||||||
if err := rows.Scan(&stage, &playerID, &imageData); err != nil {
|
|
||||||
return nil, fmt.Errorf("scan score attachment: %w", err)
|
|
||||||
}
|
|
||||||
stage = strings.ToLower(strings.TrimSpace(stage))
|
|
||||||
stageMap, ok := proofs[stage]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
stageMap[playerID] = imageData
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("iterate score attachments: %w", err)
|
|
||||||
}
|
|
||||||
return proofs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func scoreProofMapToJSON(proofMap map[string]map[int]string) map[string]map[string]string {
|
|
||||||
out := map[string]map[string]string{}
|
|
||||||
for stage, stageMap := range proofMap {
|
|
||||||
out[stage] = map[string]string{}
|
|
||||||
for playerID, imageData := range stageMap {
|
|
||||||
if strings.TrimSpace(imageData) == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out[stage][strconv.Itoa(playerID)] = imageData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func positionBoardMapToJSON(positionMap map[string]map[int]PositionSlot) map[string]map[string]PositionSlot {
|
|
||||||
out := map[string]map[string]PositionSlot{}
|
|
||||||
for board := range allowedPositionBoards {
|
|
||||||
out[board] = map[string]PositionSlot{}
|
|
||||||
}
|
|
||||||
for board, boardMap := range positionMap {
|
|
||||||
if _, ok := out[board]; !ok {
|
|
||||||
out[board] = map[string]PositionSlot{}
|
|
||||||
}
|
|
||||||
for playerID, slot := range boardMap {
|
|
||||||
out[board][strconv.Itoa(playerID)] = slot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) readPositionBoards(players []Player, derived DerivedState) (map[string]map[int]PositionSlot, error) {
|
|
||||||
defaults := defaultPositionBoards(players, derived)
|
|
||||||
merged := map[string]map[int]PositionSlot{}
|
|
||||||
for board, boardMap := range defaults {
|
|
||||||
merged[board] = map[int]PositionSlot{}
|
|
||||||
for playerID, slot := range boardMap {
|
|
||||||
merged[board][playerID] = slot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := a.db.Query(`SELECT board, player_id, group_key, position FROM player_positions`)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("query player positions: %w", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var boardRaw string
|
|
||||||
var playerID int
|
|
||||||
var groupKey string
|
|
||||||
var position int
|
|
||||||
if err := rows.Scan(&boardRaw, &playerID, &groupKey, &position); err != nil {
|
|
||||||
return nil, fmt.Errorf("scan player position: %w", err)
|
|
||||||
}
|
|
||||||
board, ok := normalizePositionBoard(boardRaw)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
boardMap := merged[board]
|
|
||||||
if boardMap == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
defaultSlot, exists := boardMap[playerID]
|
|
||||||
if !exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pos := normalizePositionValue(position, defaultSlot.Position)
|
|
||||||
group := normalizePositionGroupKey(board, groupKey, defaultSlot.GroupKey)
|
|
||||||
boardMap[playerID] = PositionSlot{
|
|
||||||
GroupKey: group,
|
|
||||||
Position: pos,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("iterate player positions: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultPositionBoards(players []Player, derived DerivedState) map[string]map[int]PositionSlot {
|
|
||||||
out := map[string]map[int]PositionSlot{
|
|
||||||
positionBoardPreliminary: {},
|
|
||||||
positionBoardFinal: {},
|
|
||||||
positionBoardPrelimTie: {},
|
|
||||||
positionBoardFinalTie: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
preRows := make([]Player, 0, len(players))
|
|
||||||
preRows = append(preRows, players...)
|
|
||||||
sort.SliceStable(preRows, func(i, j int) bool {
|
|
||||||
ga := normalizePreliminaryGroupKey(preRows[i].GroupCode)
|
|
||||||
gb := normalizePreliminaryGroupKey(preRows[j].GroupCode)
|
|
||||||
if ga != gb {
|
|
||||||
return ga < gb
|
|
||||||
}
|
|
||||||
return preRows[i].ID < preRows[j].ID
|
|
||||||
})
|
|
||||||
preGroupCounts := map[string]int{}
|
|
||||||
for _, player := range preRows {
|
|
||||||
group := normalizePreliminaryGroupKey(player.GroupCode)
|
|
||||||
preGroupCounts[group]++
|
|
||||||
out[positionBoardPreliminary][player.ID] = PositionSlot{
|
|
||||||
GroupKey: group,
|
|
||||||
Position: preGroupCounts[group],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalGroupCounts := map[string]int{}
|
|
||||||
for _, row := range derived.FinalGroups.Group1 {
|
|
||||||
group := "1"
|
|
||||||
finalGroupCounts[group]++
|
|
||||||
out[positionBoardFinal][row.PlayerID] = PositionSlot{GroupKey: group, Position: finalGroupCounts[group]}
|
|
||||||
}
|
|
||||||
for _, row := range derived.FinalGroups.Group2 {
|
|
||||||
group := "2"
|
|
||||||
finalGroupCounts[group]++
|
|
||||||
out[positionBoardFinal][row.PlayerID] = PositionSlot{GroupKey: group, Position: finalGroupCounts[group]}
|
|
||||||
}
|
|
||||||
|
|
||||||
preRowsByID := map[int]RankingRow{}
|
|
||||||
for _, row := range derived.PreliminaryRanking.Rows {
|
|
||||||
preRowsByID[row.PlayerID] = row
|
|
||||||
}
|
|
||||||
preTieRows := make([]RankingRow, 0, len(derived.PreliminaryRanking.TieBreak.PlayerIDs))
|
|
||||||
for _, id := range derived.PreliminaryRanking.TieBreak.PlayerIDs {
|
|
||||||
row, ok := preRowsByID[id]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
preTieRows = append(preTieRows, row)
|
|
||||||
}
|
|
||||||
sort.SliceStable(preTieRows, func(i, j int) bool {
|
|
||||||
if preTieRows[i].TieBreak != preTieRows[j].TieBreak {
|
|
||||||
return preTieRows[i].TieBreak > preTieRows[j].TieBreak
|
|
||||||
}
|
|
||||||
if preTieRows[i].Score != preTieRows[j].Score {
|
|
||||||
return preTieRows[i].Score > preTieRows[j].Score
|
|
||||||
}
|
|
||||||
return preTieRows[i].PlayerID < preTieRows[j].PlayerID
|
|
||||||
})
|
|
||||||
for idx, row := range preTieRows {
|
|
||||||
groupKey := strconv.Itoa((idx / 6) + 1)
|
|
||||||
position := (idx % 6) + 1
|
|
||||||
out[positionBoardPrelimTie][row.PlayerID] = PositionSlot{GroupKey: groupKey, Position: position}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalRowsByID := map[int]RankingRow{}
|
|
||||||
for _, row := range derived.FinalRanking.Rows {
|
|
||||||
finalRowsByID[row.PlayerID] = row
|
|
||||||
}
|
|
||||||
finalTieRows := make([]RankingRow, 0, len(derived.FinalRanking.TieBreak.PlayerIDs))
|
|
||||||
for _, id := range derived.FinalRanking.TieBreak.PlayerIDs {
|
|
||||||
row, ok := finalRowsByID[id]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
finalTieRows = append(finalTieRows, row)
|
|
||||||
}
|
|
||||||
sort.SliceStable(finalTieRows, func(i, j int) bool {
|
|
||||||
if finalTieRows[i].TieBreak != finalTieRows[j].TieBreak {
|
|
||||||
return finalTieRows[i].TieBreak > finalTieRows[j].TieBreak
|
|
||||||
}
|
|
||||||
if finalTieRows[i].Score != finalTieRows[j].Score {
|
|
||||||
return finalTieRows[i].Score > finalTieRows[j].Score
|
|
||||||
}
|
|
||||||
return finalTieRows[i].PlayerID < finalTieRows[j].PlayerID
|
|
||||||
})
|
|
||||||
for idx, row := range finalTieRows {
|
|
||||||
groupKey := strconv.Itoa((idx / 6) + 1)
|
|
||||||
position := (idx % 6) + 1
|
|
||||||
out[positionBoardFinalTie][row.PlayerID] = PositionSlot{GroupKey: groupKey, Position: position}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizePositionBoard(board string) (string, bool) {
|
|
||||||
normalized := strings.ToLower(strings.TrimSpace(board))
|
|
||||||
if !allowedPositionBoards[normalized] {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return normalized, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizePreliminaryGroupKey(groupCode string) string {
|
|
||||||
normalized := strings.ToUpper(strings.TrimSpace(groupCode))
|
|
||||||
if normalized == "" {
|
|
||||||
return positionBoardUnassigned
|
|
||||||
}
|
|
||||||
return normalized
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizePositionGroupKey(board, groupKey, fallback string) string {
|
|
||||||
if board == positionBoardPreliminary {
|
|
||||||
normalized := normalizePreliminaryGroupKey(groupKey)
|
|
||||||
if normalized == "" {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
return normalized
|
|
||||||
}
|
|
||||||
normalized := strings.TrimSpace(groupKey)
|
|
||||||
if normalized == "" {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
return normalized
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizePositionValue(position int, fallback int) int {
|
|
||||||
if position > 0 {
|
|
||||||
return position
|
|
||||||
}
|
|
||||||
if fallback > 0 {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) ensureScoreRows(players []Player) error {
|
|
||||||
if len(players) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tx, err := a.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("begin score row ensure tx: %w", err)
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
for _, p := range players {
|
|
||||||
for _, stage := range scoreStages {
|
|
||||||
if _, err := tx.Exec(`INSERT OR IGNORE INTO scores(stage, player_id, score) VALUES(?, ?, 0)`, stage, p.ID); err != nil {
|
|
||||||
return fmt.Errorf("ensure score row (%s,%d): %w", stage, p.ID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return fmt.Errorf("commit score row ensure tx: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) readScores(players []Player) (map[string]map[int]int, error) {
|
|
||||||
scores := map[string]map[int]int{}
|
|
||||||
for _, stage := range scoreStages {
|
|
||||||
scores[stage] = map[int]int{}
|
|
||||||
}
|
|
||||||
for _, p := range players {
|
|
||||||
for _, stage := range scoreStages {
|
|
||||||
scores[stage][p.ID] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := a.db.Query(`SELECT stage, player_id, score FROM scores`)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("query scores: %w", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var stage string
|
|
||||||
var playerID int
|
|
||||||
var score int
|
|
||||||
if err := rows.Scan(&stage, &playerID, &score); err != nil {
|
|
||||||
return nil, fmt.Errorf("scan score: %w", err)
|
|
||||||
}
|
|
||||||
stage = strings.ToLower(stage)
|
|
||||||
if _, ok := scores[stage]; !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
scores[stage][playerID] = score
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("iterate scores: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return scores, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func scoreMapToJSON(scoreMap map[string]map[int]int) map[string]map[string]int {
|
|
||||||
out := map[string]map[string]int{}
|
|
||||||
for stage, stageMap := range scoreMap {
|
|
||||||
out[stage] = map[string]int{}
|
|
||||||
for playerID, value := range stageMap {
|
|
||||||
out[stage][strconv.Itoa(playerID)] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func computeDerived(players []Player, scores map[string]map[int]int) DerivedState {
|
|
||||||
playerByID := map[int]Player{}
|
|
||||||
for _, p := range players {
|
|
||||||
playerByID[p.ID] = p
|
|
||||||
}
|
|
||||||
|
|
||||||
preRows := []RankingRow{}
|
|
||||||
for _, p := range players {
|
|
||||||
preRows = append(preRows, RankingRow{
|
|
||||||
PlayerID: p.ID,
|
|
||||||
NameAr: p.NameAr,
|
|
||||||
NameEn: p.NameEn,
|
|
||||||
GroupCode: p.GroupCode,
|
|
||||||
ImageData: p.ImageData,
|
|
||||||
Score: scoreTotal(scores, p.ID, preliminaryRoundStages),
|
|
||||||
TieBreak: scores["prelim_tiebreak"][p.ID],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.SliceStable(preRows, func(i, j int) bool {
|
|
||||||
if preRows[i].Score != preRows[j].Score {
|
|
||||||
return preRows[i].Score > preRows[j].Score
|
|
||||||
}
|
|
||||||
return preRows[i].PlayerID < preRows[j].PlayerID
|
|
||||||
})
|
|
||||||
assignDenseRankByScore(preRows)
|
|
||||||
|
|
||||||
preTie := TieBreakInfo{Required: false, Resolved: true, Slots: 0, PlayerIDs: []int{}}
|
|
||||||
finalists := []RankingRow{}
|
|
||||||
|
|
||||||
if len(preRows) <= 12 {
|
|
||||||
for i := range preRows {
|
|
||||||
row := preRows[i]
|
|
||||||
row.Seed = i + 1
|
|
||||||
finalists = append(finalists, row)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cutoff := preRows[11].Score
|
|
||||||
above := []RankingRow{}
|
|
||||||
atCutoff := []RankingRow{}
|
|
||||||
for _, row := range preRows {
|
|
||||||
if row.Score > cutoff {
|
|
||||||
above = append(above, row)
|
|
||||||
} else if row.Score == cutoff {
|
|
||||||
atCutoff = append(atCutoff, row)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slots := 12 - len(above)
|
|
||||||
if slots < 0 {
|
|
||||||
slots = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(atCutoff) <= slots {
|
|
||||||
finalists = append(finalists, above...)
|
|
||||||
finalists = append(finalists, atCutoff...)
|
|
||||||
} else {
|
|
||||||
preTie.Required = true
|
|
||||||
preTie.Slots = slots
|
|
||||||
preTie.Resolved = true
|
|
||||||
for _, row := range atCutoff {
|
|
||||||
preTie.PlayerIDs = append(preTie.PlayerIDs, row.PlayerID)
|
|
||||||
}
|
|
||||||
sort.Ints(preTie.PlayerIDs)
|
|
||||||
|
|
||||||
sort.SliceStable(atCutoff, func(i, j int) bool {
|
|
||||||
if atCutoff[i].TieBreak != atCutoff[j].TieBreak {
|
|
||||||
return atCutoff[i].TieBreak > atCutoff[j].TieBreak
|
|
||||||
}
|
|
||||||
return atCutoff[i].PlayerID < atCutoff[j].PlayerID
|
|
||||||
})
|
|
||||||
|
|
||||||
if slots > 0 {
|
|
||||||
boundary := atCutoff[slots-1].TieBreak
|
|
||||||
greater := 0
|
|
||||||
equal := 0
|
|
||||||
for _, row := range atCutoff {
|
|
||||||
if row.TieBreak > boundary {
|
|
||||||
greater++
|
|
||||||
} else if row.TieBreak == boundary {
|
|
||||||
equal++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if greater < slots && greater+equal > slots {
|
|
||||||
preTie.Resolved = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalists = append(finalists, above...)
|
|
||||||
if slots > len(atCutoff) {
|
|
||||||
slots = len(atCutoff)
|
|
||||||
}
|
|
||||||
finalists = append(finalists, atCutoff[:slots]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.SliceStable(finalists, func(i, j int) bool {
|
|
||||||
if finalists[i].Score != finalists[j].Score {
|
|
||||||
return finalists[i].Score > finalists[j].Score
|
|
||||||
}
|
|
||||||
if preTie.Required {
|
|
||||||
if finalists[i].TieBreak != finalists[j].TieBreak {
|
|
||||||
return finalists[i].TieBreak > finalists[j].TieBreak
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return finalists[i].PlayerID < finalists[j].PlayerID
|
|
||||||
})
|
|
||||||
assignDenseRankBy(finalists, func(a, b RankingRow) bool {
|
|
||||||
if a.Score != b.Score {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if preTie.Required {
|
|
||||||
return a.TieBreak == b.TieBreak
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range finalists {
|
|
||||||
finalists[i].Seed = i + 1
|
|
||||||
if i < 6 {
|
|
||||||
finalists[i].FinalGroup = 1
|
|
||||||
} else {
|
|
||||||
finalists[i].FinalGroup = 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalGroup1 := []RankingRow{}
|
|
||||||
finalGroup2 := []RankingRow{}
|
|
||||||
for _, row := range finalists {
|
|
||||||
if row.FinalGroup == 1 {
|
|
||||||
finalGroup1 = append(finalGroup1, row)
|
|
||||||
} else {
|
|
||||||
finalGroup2 = append(finalGroup2, row)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalRows := []RankingRow{}
|
|
||||||
for _, finalist := range finalists {
|
|
||||||
p := playerByID[finalist.PlayerID]
|
|
||||||
finalRows = append(finalRows, RankingRow{
|
|
||||||
PlayerID: finalist.PlayerID,
|
|
||||||
NameAr: p.NameAr,
|
|
||||||
NameEn: p.NameEn,
|
|
||||||
GroupCode: p.GroupCode,
|
|
||||||
ImageData: p.ImageData,
|
|
||||||
Score: scoreTotal(scores, finalist.PlayerID, finalRoundStages),
|
|
||||||
TieBreak: scores["final_tiebreak"][finalist.PlayerID],
|
|
||||||
Seed: finalist.Seed,
|
|
||||||
FinalGroup: finalist.FinalGroup,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.SliceStable(finalRows, func(i, j int) bool {
|
|
||||||
if finalRows[i].Score != finalRows[j].Score {
|
|
||||||
return finalRows[i].Score > finalRows[j].Score
|
|
||||||
}
|
|
||||||
return finalRows[i].Seed < finalRows[j].Seed
|
|
||||||
})
|
|
||||||
|
|
||||||
finalTie := TieBreakInfo{Required: false, Resolved: true, Slots: 0, PlayerIDs: []int{}}
|
|
||||||
tiedTop := map[int]bool{}
|
|
||||||
if len(finalRows) > 0 {
|
|
||||||
i := 0
|
|
||||||
for i < len(finalRows) {
|
|
||||||
j := i + 1
|
|
||||||
for j < len(finalRows) && finalRows[j].Score == finalRows[i].Score {
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
if j-i > 1 && finalRows[i].Score > 0 {
|
|
||||||
startPos := i + 1
|
|
||||||
endPos := j
|
|
||||||
if startPos <= 3 || endPos <= 3 || (startPos < 3 && endPos > 3) {
|
|
||||||
for k := i; k < j; k++ {
|
|
||||||
tiedTop[finalRows[k].PlayerID] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i = j
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tiedTop) > 0 {
|
|
||||||
finalTie.Required = true
|
|
||||||
for id := range tiedTop {
|
|
||||||
finalTie.PlayerIDs = append(finalTie.PlayerIDs, id)
|
|
||||||
}
|
|
||||||
sort.Ints(finalTie.PlayerIDs)
|
|
||||||
|
|
||||||
sort.SliceStable(finalRows, func(i, j int) bool {
|
|
||||||
if finalRows[i].Score != finalRows[j].Score {
|
|
||||||
return finalRows[i].Score > finalRows[j].Score
|
|
||||||
}
|
|
||||||
iti := tiedTop[finalRows[i].PlayerID]
|
|
||||||
itj := tiedTop[finalRows[j].PlayerID]
|
|
||||||
if iti && itj && finalRows[i].TieBreak != finalRows[j].TieBreak {
|
|
||||||
return finalRows[i].TieBreak > finalRows[j].TieBreak
|
|
||||||
}
|
|
||||||
return finalRows[i].Seed < finalRows[j].Seed
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if finalTie.Required {
|
|
||||||
assignDenseRankBy(finalRows, func(a, b RankingRow) bool {
|
|
||||||
if a.Score != b.Score {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if tiedTop[a.PlayerID] && tiedTop[b.PlayerID] {
|
|
||||||
return a.TieBreak == b.TieBreak
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
if len(finalRows) >= 3 {
|
|
||||||
third := finalRows[2]
|
|
||||||
greater := 0
|
|
||||||
equal := 0
|
|
||||||
for _, row := range finalRows {
|
|
||||||
if row.Score > third.Score {
|
|
||||||
greater++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if row.Score == third.Score {
|
|
||||||
itied := tiedTop[row.PlayerID]
|
|
||||||
ttied := tiedTop[third.PlayerID]
|
|
||||||
if itied && ttied {
|
|
||||||
if row.TieBreak > third.TieBreak {
|
|
||||||
greater++
|
|
||||||
} else if row.TieBreak == third.TieBreak {
|
|
||||||
equal++
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
equal++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if greater < 3 && greater+equal > 3 {
|
|
||||||
finalTie.Resolved = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
assignDenseRankByScore(finalRows)
|
|
||||||
}
|
|
||||||
|
|
||||||
podium := []RankingRow{}
|
|
||||||
for i := 0; i < len(finalRows) && i < 3; i++ {
|
|
||||||
podium = append(podium, finalRows[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
return DerivedState{
|
|
||||||
PreliminaryRanking: RankingBundle{Rows: preRows, TieBreak: preTie, Unresolved: preTie.Required && !preTie.Resolved},
|
|
||||||
Finalists: finalists,
|
|
||||||
FinalGroups: FinalGroups{Group1: finalGroup1, Group2: finalGroup2},
|
|
||||||
FinalRanking: RankingBundle{Rows: finalRows, TieBreak: finalTie, Unresolved: finalTie.Required && !finalTie.Resolved},
|
|
||||||
Podium: podium,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func scoreTotal(scores map[string]map[int]int, playerID int, stages []string) int {
|
|
||||||
total := 0
|
|
||||||
for _, stage := range stages {
|
|
||||||
stageMap, ok := scores[stage]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
total += stageMap[playerID]
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
func assignDenseRankByScore(rows []RankingRow) {
|
|
||||||
assignDenseRankBy(rows, func(a, b RankingRow) bool {
|
|
||||||
return a.Score == b.Score
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func assignDenseRankBy(rows []RankingRow, isEqual func(a, b RankingRow) bool) {
|
|
||||||
if len(rows) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
currentRank := 1
|
|
||||||
rows[0].Rank = currentRank
|
|
||||||
for i := 1; i < len(rows); i++ {
|
|
||||||
if !isEqual(rows[i], rows[i-1]) {
|
|
||||||
currentRank = i + 1
|
|
||||||
}
|
|
||||||
rows[i].Rank = currentRank
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
17
backend/web/assets/index-CXf6Y6x9.js
Normal file
1
backend/web/assets/index-Le6fiRrS.css
Normal file
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -7,9 +7,10 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Cairo:wght@400;600;700;800&family=IBM+Plex+Mono:wght@500;600;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Cairo:wght@400;600;700;800&family=IBM+Plex+Mono:wght@500;600;700&display=swap" rel="stylesheet" />
|
||||||
|
<script type="module" crossorigin src="/assets/index-CXf6Y6x9.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-Le6fiRrS.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
BIN
bin/shooting-event
Executable file
BIN
bin/shooting-event-amd64
Executable file
BIN
data/shooting.db
Normal file
BIN
data/shooting_phoenix.db
Normal file
BIN
data/shooting_phoenix.db-shm
Normal file
BIN
data/shooting_phoenix.db-wal
Normal file
BIN
data/test-api.db
Normal file
BIN
data/test-api.db-shm
Normal file
BIN
data/test-api.db-wal
Normal file
@@ -1,32 +0,0 @@
|
|||||||
services:
|
|
||||||
shooting-event:
|
|
||||||
container_name: shooting-event
|
|
||||||
image: ${IMAGE:-repo.ssp-itinfra.com/admin/shooting-event:amd64-latest}
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "${APP_PORT:-8080}:8080"
|
|
||||||
environment:
|
|
||||||
PORT: "8080"
|
|
||||||
DB_PATH: /app/data/shooting.db
|
|
||||||
WEB_DIR: /app/web
|
|
||||||
ADMIN_USER: ${ADMIN_USER:-datwyler}
|
|
||||||
ADMIN_PASS: ${ADMIN_PASS:-datwyler}
|
|
||||||
GEMINI_API_KEY: ${GEMINI_API_KEY:-}
|
|
||||||
GEMINI_MODEL: ${GEMINI_MODEL:-gemini-2.0-flash}
|
|
||||||
volumes:
|
|
||||||
- shooting_event_data:/app/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/api/health >/dev/null 2>&1 || exit 1"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
start_period: 15s
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
shooting_event_data:
|
|
||||||
name: shooting_event_data
|
|
||||||
BIN
frontend/dist/arture.png
vendored
Normal file
|
After Width: | Height: | Size: 39 KiB |
17
frontend/dist/assets/index-CXf6Y6x9.js
vendored
Normal file
1
frontend/dist/assets/index-Le6fiRrS.css
vendored
Normal file
BIN
frontend/dist/bg.png
vendored
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
frontend/dist/bg_live.png
vendored
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
frontend/dist/datwyler_logo.png
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
16
frontend/dist/index.html
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Shooting Event Tracker</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Cairo:wght@400;600;700;800&family=IBM+Plex+Mono:wght@500;600;700&display=swap" rel="stylesheet" />
|
||||||
|
<script type="module" crossorigin src="/assets/index-CXf6Y6x9.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-Le6fiRrS.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
frontend/dist/logo.png
vendored
Normal file
|
After Width: | Height: | Size: 32 KiB |
63
frontend/dist/logo.svg
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 583.65 114.56">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #008ca8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #ec2f23;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #1b1f40;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<path class="cls-3" d="M545.23,98.25l4.31-7.15c.28-.46.13-1.06-.33-1.33-2.22-1.32-8.46-4.34-10.8-5.19-.64-.23-.88-.38-1.41.22-.65.73-4.89,7.9-4.78,8.41.1.46,8,4.3,11.13,5.65.69.3,1.5.04,1.9-.61Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-3" d="M392.94,85.7c5.81.93,14.01.79,19.19-2.28,3.16-1.87,4.61-5.35,5.87-8.63l1.32,4.87c3.19,8.82,8.21,15.31,18.58,12.74,10.58-2.63,13.61-18.14,12.73-27.42-.65-6.87-2.57-13.59-3.36-20.44l1.61-.11,3.36-9.01c.02-.45-.27-.62-.56-.87-2.28-1.94-6.47-3.24-8.09-6.06-1.38-2.41-.88-4.59-.39-7.13-.32-.06-.53.03-.81.16-1.84.87-5.37,7.2-5.99,9.22-.44,1.44-.52,2.45,0,3.88.32.91,1.51,2.23,1.64,2.78.19.82-.16,1.4-.15,2.08.29,10.93,4.43,22.34,6.47,33,.24,1.27,1.13,5.76.62,6.65-.87,1.53-4.98,2.6-6.65,2.73-13.69,1.08-17.23-22.88-17.26-24.5-.02-1.63,3.27-6.22,4.17-8.05.29-.6,1.45-2.94,1.31-3.43l-.52-.21-5.3,7.35c-.93-3.2-1.96-6.43-2.15-9.79-.69-.95-1.17.03-1.63.61-2.69,3.35-3.93,5.91-3.64,10.33.04.65.56,1.64.19,2.14-7.63,4.11-16.83,6.5-21.56,14.35-1.4,2.33-4.47,9.4-4.06,11.96.39,2.45,3.05,2.76,5.05,3.08ZM398.75,71.81c5.39-3.26,11.6-5.51,16.24-9.94.49,2.58,1.29,5.14,1.62,7.75.07.57.35,1.65,0,2.1-.33.43-3.52,2.14-4.2,2.44-7.89,3.39-16.94.64-17.15.36-.11-.47,2.94-2.38,3.48-2.71Z"/>
|
||||||
|
<path class="cls-3" d="M384.64,43.49l10.08,4.98c.43.21.94.06,1.19-.35l3.86-6.41c3.08.93,5.9,2.66,8.65,4.33.21.12.41.05.58-.09l5.38-7.11c.28-.37.19-.89-.2-1.14-1.87-1.21-7.19-4.06-9.28-4.89-1.71-.67-1.63-.51-2.65.83-1.28,1.68-2.21,3.59-3.39,5.34-.64.19-6.86-3.71-9.28-4.05-.37-.05-.74.13-.94.45l-4.34,6.88c-.27.43-.11,1,.35,1.23Z"/>
|
||||||
|
<path class="cls-1" d="M412.38,30.98c5.42-3.61,12.17-5.45,18.12-8.01,1.55-.67,4.04-1.64,5.19-2.83.46-.48,2.17-3.45,2.03-3.98l-.35-.22c-1.06.57-2.11,1.19-3.21,1.69-5.69,2.56-12.2,4.4-17.73,7.18-3.1,1.56-5.78,3.95-6.06,7.62.12.14,1.76-1.28,2-1.44Z"/>
|
||||||
|
<path class="cls-1" d="M544.19,36.97c.53-.43,2.67-2.92,2.87-2.97,1.09-.27,2.33,2.56,3.41.76,1.27-2.13.29-4.12-1.85-5.02,3.63-5.13-1.82-8.77-5.51-4-2.26,2.93-2.19,6,1.66,7.24.3.4-5.23,3.9-5.71,4.19-2.66,1.58-5.51,2.96-8.26,4.39-.68.35-2.27.92-2.73,1.27-.28.21-.32.27-.27.64,8.41-1.44,14.63-5.05,16.39-6.5ZM544.77,28.23c-.28-1.72,3.07-1.19,2.34.92-.62.13-2.23-.22-2.34-.92Z"/>
|
||||||
|
<path class="cls-1" d="M487.55,49.01c.45-.37,2.28-2.5,2.46-2.54.93-.23,2,2.19,2.92.65,1.09-1.82.25-3.53-1.58-4.3,3.11-4.39-1.56-7.51-4.72-3.42-1.93,2.51-1.88,5.14,1.42,6.2.26.35-4.47,3.34-4.89,3.58-2.28,1.35-4.72,2.53-7.07,3.75-.58.3-1.94.78-2.33,1.08-.24.18-.28.23-.23.55,7.19-1.23,12.52-4.32,14.03-5.56ZM488.04,41.53c-.24-1.47,2.63-1.02,2,.79-.53.12-1.91-.19-2-.79Z"/>
|
||||||
|
<path class="cls-2" d="M475.87,43.99c.16-.42,1.45-4.23.57-4.22l-2.7,2.54c-.95.9-2.32,1.27-3.56.88-2.75-.86-3.43-4.62-3.2-7.68.37-4.86,3.22-9.04,4.99-13.43-.05-.06-.59.33-.71.44-4.34,4.2-8.86,15.7-7.73,21.64,1.59,8.37,9.66,6.77,12.34-.16Z"/>
|
||||||
|
<path class="cls-3" d="M560.15,59.59c-.48-6.52-4.32-9.98-8.51-15.45-.96-.25-1.27.94-1.6,1.63-.78,1.57-2.91,6.79-2.81,8.35.16,2.1,4.24,6.97,5.41,9.28.31.61,1.54,3.25,1.24,3.73-.45.71-7.04,2.47-8.23,2.74-4.77,1.09-9.7,1.58-14.5,2.56-.26-.23,6.7-5.19,9.46-13.62,1.79-5.5-6.64-10.43-11.05-11.41-7.4-1.67-15.05,3.33-20.35,7.96l-4.23,4.17c.47-1.51,1.23-2.9,1.67-4.43,2.64-9.01-.34-20.1-.44-20.34.63-.09,1.27.71,1.67-.01.87-2.38,1.4-4.98,2.45-7.3.22-.5,1.13-1.55,1-1.95-5.81-3.68-12.82-6.43-10.61-13.11-.27-1.24-2.06,1.48-2.25,1.79-1.2,1.99-3.4,6.92-3.4,9.17,0,3.07,2.12,4.11,2.46,5.87.12.63-.18,1.95-.16,2.8.14,4.15,2.12,9.94,2.96,14.21,1.93,9.78,2.55,16.11-4.81,23.74-.72.75-9.19,3.71-10.2,2.78-2.26-6.39-10.51-19.18-18.41-12.73-3.52,2.87-6.25,8.73-7.48,13.04-2.5,8.83,4.32,10.97,11.65,11.28,2.5.12,4.52-.38,6.9-.52,1.11-.08,1.99-.09,1.23,1.22-.92,1.58-15.56,16.28-29.89,15.6-2.72-.13-10.67-1.83-14.21-2.81-.34-.09-.56.36-.27.57.75.54,10.15,7.16,16.92,8.41,1.27.23,3.27.21,4.65.21,0,0-1.44.38-1.44.7,0,.1.12.65,1.23.89.83.18,2.45.26,2.45.26,9.13-.07,15.98-8.55,19.69-13.13,2.55-3.14,4.57-6.66,5.78-10.36.14-.41.17-1.55.6-1.7.53-.18,26.49-2.3,34.5-1.32,9.25,1.15,24.69-1.36,32.76-6.16,4.68-2.78,8.59-11.22,8.2-16.59ZM465.63,71.52c-.88-1.54,1.33-3.08,2.77-3.09,1.67-.01,6.42,4.66,6.26,5.29-1.81-.05-8.13-.62-9.03-2.2ZM500.54,70.49c-.08-.31.13-.36.26-.53.7-.92,1.9-1.73,2.74-2.56,4.37-4.31,11.57-10.71,18.14-10.36,3.91.21,7.97,2.72,10.69,5.41.06.28-.23.36-.4.47-.84.56-2.8,1.31-3.79,1.7-8.75,3.39-18.36,4.8-27.64,5.89Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M217.93,56.12c2.78-1.56,3.83-5.94,4.41-8.84l-.31-.22c-1.28,2.08-4,4.11-6.31,2.14-2.91-2.49-1.56-7.07-.57-10.19.99-3.12,2.59-5.94,3.94-8.89,0,0-12.84,14.54-7.4,24.01,1.32,2.29,3.82,3.34,6.23,1.98Z"/>
|
||||||
|
<path class="cls-1" d="M339.55,40.67c-.21.38-1.07,2.03-1.03,2.32.02.13.31.35.47.12,5.9-4.13,13.01-5.94,19.63-8.59,4.02-1.61,7.78-2.67,10.3-6.45.73-1.09,1.59-2.51,1.51-3.86l-2.7,1.79c-8.01,5.05-23.37,5.98-28.18,14.67Z"/>
|
||||||
|
<path class="cls-2" d="M251.93,48.45c2.1,1.09,3.59-.9,4.9-2.21,2.22,1.02,4.11.54,5.3-1.62,1.28-2.33.53-4.16-.14-6.51-.85-.84-1.13,1.52-1.14,2.06,0,1.09.59,2.4-.66,3.18-1.34.83-2.38-.6-2.52-1.85-.07-.63,0-1.46,0-2.12,0-.74-.76.64-.82.77-.47,1.06-.45,1.75-.67,2.78-.59,2.71-3.83,3.62-4,.25-.02-.42.22-1.76.17-1.92-.3-1.14-1.3,1.41-1.38,1.65-.51,1.6-.84,4.62.95,5.55Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-3" d="M371.43,67.19c-1.92-5.4-10.44-12.79-16.07-15.06-2.28-.92-4.29-.92-5.95,0-2.69,1.49-3.83,5.03-4.31,7.74-.25,1.55-.59,4.2,0,4.62l.2.14.24-.06c.68-.17,1.25-.53,1.8-.88.74-.46,1.37-.86,2.21-.86,2.34,0,8.55,5.01,11.36,7.7,1.77,1.69,3.56,3.77,5.08,5.88-.43.22-1.49.65-4.1,1.45l-.11.03c-3.7,1.13-7.91,2.17-11.53,2.85-.22.04-.46.09-.72.14-2.51.5-6.71,1.33-7.93-.74-.15-.26-.25-.58-.35-.91-.16-.55-.35-1.17-.87-1.63l-.15-.14h-.2c-.48,0-.73.14-1.62,3.9-1.22,5.13-.98,8.35.76,10.12,1.07,1.08,2.69,1.61,4.97,1.61,1.81,0,4.05-.31,7.26-1.01,3.87-.85,15.21-3.89,17.47-6.22.98-1.01,1.5-2.63,1.67-3.28,1.25-4.65,2.41-11.09.88-15.39Z"/>
|
||||||
|
<path class="cls-3" d="M334.01,41.61l-.26.06.26-.07c-1.94-8.37-3.96-17.03-3.69-24.32,0-.1.05-.23.1-.36.13-.34.33-.85-.03-1.44l-.12-.2-.23-.05c-.98-.2-1.59.44-2.05.91l-.1.11c-6.98,7.12-6.41,14.05-4.94,22.37.83,4.72,2,9.24,3.13,13.64,2.89,11.24,5.63,21.85,2.6,34.3-.08.31-.46,1.01-.77,1.57-.48.87-.75,1.38-.79,1.73-.05.49.17,1.06.68,1.18.04,0,.08.02.14.02.29,0,.61-.16,2.15-2.17,5.72-7.49,8.5-19.01,7.26-30.08-.61-5.44-2-11.42-3.34-17.2Z"/>
|
||||||
|
<path class="cls-3" d="M272.08,56.27l.25-.08-.25.08c2.16,6.63,4.2,12.89,3.73,20.06-.06.93-.34,2.15-.62,3.35-.33,1.43-.65,2.78-.63,3.76,0,.32.02.69.5,1.03l.39.27.31-.36c2.71-3.13,4.14-7.58,5.09-11.1,2.62-9.78,1.49-16.82-.91-26.14,0,0-.82-3.09-1.35-4.88-1.28-4.27-3.42-8.98-3.15-12.78.01-.17.09-.5.17-.84.25-1.09.54-2.32-.02-2.95-.13-.15-.43-.4-.96-.35-1.13.11-4.09,4.59-4.11,4.63-3.24,5.18-2.75,8.94-1.68,14.66.76,4.03,2.02,7.9,3.24,11.65Z"/>
|
||||||
|
<path class="cls-3" d="M287.73,37.28l10.4,5.14c.2.1.42.15.65.15.52,0,1-.27,1.27-.71l3.77-6.26c2.94.96,5.68,2.63,8.33,4.24l.09.06c.4.23.85.16,1.28-.24l5.55-7.34c.23-.3.32-.68.25-1.05-.07-.37-.28-.7-.6-.9-2.01-1.3-7.55-4.26-9.67-5.09-1.85-.73-2.16-.56-3.11.71l-.24.32c-.79,1.04-1.45,2.16-2.12,3.28-.39.65-.79,1.33-1.21,1.97-.54-.19-1.77-.83-2.8-1.36-2.2-1.13-4.93-2.54-6.44-2.76-.6-.09-1.19.19-1.51.71l-4.48,7.1c-.21.34-.27.75-.17,1.14.11.39.38.71.74.89Z"/>
|
||||||
|
<path class="cls-3" d="M269.89,96.65l.1.06c.41.23.85.15,1.28-.24l5.55-7.34c.23-.3.32-.68.25-1.05s-.28-.7-.6-.9c-2.01-1.3-7.55-4.26-9.67-5.09-1.85-.73-2.16-.56-3.11.71l-.24.32c-.78,1.03-1.45,2.15-2.1,3.25-.39.66-.8,1.35-1.23,2-.54-.19-1.77-.83-2.8-1.36-2.2-1.13-4.93-2.54-6.44-2.76-.6-.09-1.19.19-1.51.71l-4.48,7.1c-.21.34-.27.75-.17,1.14.11.39.38.71.74.89l10.4,5.14c.2.1.42.15.65.15.52,0,1-.27,1.27-.71l3.77-6.26c2.94.96,5.68,2.63,8.33,4.24Z"/>
|
||||||
|
<path class="cls-2" d="M321.83,98.85h0c-.22.09-.44.3-.81.67-.29.28-.69.67-.94.79-1.38.68-2.96.36-3.76-.76-1.41-1.97-.24-5.48.76-7.94.22-.53.57-1.17.92-1.81.43-.78.87-1.59,1.08-2.25.13-.4.23-.71.03-1.27l-.11-.29-.3-.05c-.64-.12-.95.33-1.11.56-.02.03-.04.06-.07.09-2.28,2.79-6.98,10.67-6.09,15.95.29,1.71,1.13,3.01,2.5,3.85.79.48,1.61.73,2.46.73.73,0,1.47-.19,2.2-.55,2.26-1.13,4.14-3.97,4.37-6.6.02-.2-.02-.78-.4-1.05-.21-.15-.48-.18-.73-.07Z"/>
|
||||||
|
<path class="cls-2" d="M311.94,13.76h0c-.15.07-.31.21-.58.47-.21.2-.49.47-.67.56-.97.48-2.09.26-2.66-.54-1-1.4-.17-3.88.54-5.62.15-.38.4-.83.65-1.28.3-.55.62-1.12.77-1.59.09-.28.16-.5.02-.9l-.07-.2-.21-.04c-.45-.08-.67.23-.79.4-.02.02-.03.05-.05.07-1.61,1.97-4.94,7.55-4.31,11.29.2,1.21.8,2.13,1.77,2.72.56.34,1.14.52,1.74.52.52,0,1.04-.13,1.56-.39,1.6-.8,2.93-2.81,3.09-4.67.01-.14-.01-.55-.28-.75-.15-.11-.34-.13-.51-.05Z"/>
|
||||||
|
<path class="cls-3" d="M324.39,61.48c-.46-4.68-6.51-11.83-8.36-13.91-.37-.4-.7-.72-1.03-.98l-.06-.04h-.07c-.37-.07-.58.22-.71.39l-.04.05c-1.1,1.4-3.82,6.09-4.01,7.99-.23,2.25,1.86,4.95,3.53,7.13.52.68,1.02,1.32,1.39,1.88.08.13.19.29.32.47.63.92,2.09,3.06,2.07,3.74,0,.12-.22.8-3.83,2.4-1.45.63-3.84,1.5-4.41,1.5-1.17-4.42-6.15-11.55-11.48-12.92-2.47-.64-4.66.05-6.51,2.05-2.31,2.49-4.2,6.39-5.2,9.22-1.55,4.38-2.29,7.83-.87,10.45.99,1.81,2.94,3.04,5.98,3.76,3.75.89,8.59.71,12.73-.47-.07.19-.18.42-.28.62-1.7,3.37-15.28,16.69-29.4,16.03-2.54-.12-9.74-2.12-13.6-3.2l-.94-.26c-.28-.08-.56.06-.69.32-.13.26-.05.57.18.74.97.7,1.98,1.4,3.02,2.08,3.88,2.52,9.54,5.71,14.49,6.61.93.17,2.79.1,3.98.1-.07.04-.71.2-.76.25-.1.1-.15.21-.14.33,0,.09.06.86,1.47,1.17.82.18,1.64.27,2.45.27,3.53,0,7.57-1.23,10.55-3.2,4.24-2.81,13.94-12.5,15.28-23.12,5.5-2.28,9.66-4.87,12.1-10,.77-1.6,3.23-7.14,2.82-11.46h0ZM298.91,73.94c-.72.07-2.88-.02-3.4-.08-1.46-.18-4.19-.96-4.43-1.88-.11-.4.28-1.4.66-1.7.5-.39,1.37-.39,2.47-.02,1.99.68,4.16,2.4,4.7,3.69Z"/>
|
||||||
|
<path class="cls-3" d="M267.79,59.46c-1.01-2.05-2.43-4.14-4.23-6.49-.09-.12-.19-.28-.28-.45-.33-.56-.79-1.32-1.46-1.29-.44.01-.8.33-1.1.96-.8,1.61-1.9,4.71-2.49,6.41l-.16.43c-.44,1.25-.95,2.67-.37,4.02.49,1.14,1.32,2.37,2.14,3.57.88,1.28,2.46,4.25,2.46,4.38,0,.01-.07.16-.57.35-.66.24-9.67,2.56-11.43-4.79-1.35-5.65-6.8-34.72-9.24-47.44-.47-2.44-1.6-4.55-2.34-3.71-.8.9-5.73,6.31-6.12,7.53-.09.31-.24.53-.15.89,0,.04.01.08.03.12,3.66,10.92,5.66,23.65,7.3,35.05,0,.01.01.03.01.04.31,2.12.64,4.16,1.04,6.11-.01.45-.2.9-.52,1.37-.55.73-1.52,1.46-2.73,2.05-1.05.51-2.21.86-3.09.96-.88.09-1.78.07-2.71-.08-2.87-.44-5.53-2.01-7.09-4.18-.17-.24-.35-.59-.53-.93-.25-.49-.52-1.01-.83-1.33l-.07-.07c-.51-.59-.94-.48-1.2-.32-.44.23-.72.92-1.05,1.81l-.08.24c-.8,2.04-2.18,6.03-2.33,8.09-.05.71.01,1.41.07,2.09l.04.35.16.15c1.84,1.56,3.33,2.93,4.6,4.51l.2.23c.88,1.08,1.06,1.34,1.09,1.38.2.71-.12,1.8-.92,3.18-.86,1.49-2.17,3.01-3.02,4l-.43.49c-13.12,15.57-24.05,12.43-28.72,11.08-.47-.13-.88-.24-1.22-.33l-.76-.17.12.77c.59,3.86,8.51,7.34,8.86,7.49l.17.05c.81.13,1.64.21,2.43.27-.11.17-.15.39-.13.61l.03.35.32.12c.44.17.9.31,1.38.4.68.13,1.38.2,2.16.2,1.13,0,2.38-.15,3.85-.45,11.24-2.32,18.68-13.46,21.06-23.48.07-.29.15-.77.27-1.46.12-.75.44-2.71.63-2.97.16-.2,1.13-.41,1.6-.52.36-.08.69-.15.95-.23,4.9-1.66,7.94-4.18,9.58-8.01,4.06,10.53,12.35,10.27,13.62,10.3,4.15.08,7.65-2.12,9.62-6.01.48-.96.9-1.86,1.28-2.74,2.61-6.15,2.5-10.42.32-14.91Z"/>
|
||||||
|
</g>
|
||||||
|
<path class="cls-1" d="M263.48,30.62c.13-.89-.04-.9-.76-.62-2.19.82-4.7,1.96-6.84,2.95-2.94,1.36-5.86,2.56-6.26,6.24.16.17,2.1-1.17,2.42-1.34,2.38-1.27,4.98-2.14,7.41-3.3,1.63-.78,3.73-1.94,4.02-3.92Z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-3" d="M49.47,41.16c-.18.16-.38.22-.61.1-2.87-1.75-5.83-3.55-9.05-4.53l-4.04,6.71c-.26.43-.8.58-1.24.36l-10.55-5.21c-.48-.24-.65-.83-.36-1.28l4.54-7.2c.21-.34.61-.53,1-.48,2.53.36,9.03,4.43,9.7,4.23,1.23-1.82,2.21-3.83,3.55-5.59,1.07-1.41.98-1.58,2.77-.87,2.19.86,7.75,3.85,9.71,5.12.41.26.5.81.21,1.19l-5.63,7.44Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M115.09,48.87c8.37-4.02,18.12-7.1,26.81-10.56,5.22-2.08,9.22-2.94,11.61-8.6.15-.36.88-2.04.68-2.27l-3.54,1.92c-10.25,4.41-21.04,7.42-31.32,11.77-3.89,1.65-9.13,3.8-11.8,7.15-1.55,1.94-2.79,4.69-3.22,7.14l.36.21c3.1-2.61,6.76-4.99,10.42-6.75Z"/>
|
||||||
|
<path class="cls-1" d="M191.25,86.23c-8.15,3.26-17.96,5.21-25.73,8.78-3.2,1.47-5.5,3.98-6.5,7.36.26.05.44-.06.66-.14,2.34-.85,4.92-2.43,7.33-3.39,8.27-3.3,17.66-5.33,25.68-9.19,3.11-1.5,6.29-4.34,7.44-7.66-1.15.52-2.2,1.2-3.33,1.76-1.8.88-3.7,1.74-5.56,2.49Z"/>
|
||||||
|
<path class="cls-2" d="M141.79,56.7c1.9.04,2.21-1.19,3.19-2.26.16-.17,1.2-1.24,1.29-1.26.78-.19,1.62.6,2.3.71,3.33.53,6.08-2.7,6.16-5.81.02-.66-.97-5.78-1.26-5.98-.57-.39-1.14,1.15-1.21,1.56-.25,1.51.44,4.42-.59,5.49-.99,1.02-2.4.7-3.25-.3-.96-1.14-.75-2.62-.85-3.98-.06-.9-.49-.32-.75.15-1.22,2.15-.69,6.93-3.99,7.11-3.16.17-2.45-3.89-2.23-5.91-.54-.54-1.47,1.85-1.61,2.23-1,2.84-1.5,8.16,2.8,8.25Z"/>
|
||||||
|
<path class="cls-1" d="M69.89,49.89c3.39-1.55,9.76-3.16,11.37-6.74.2-.44.72-1.93-.17-1.8-3.48,1.66-7.18,2.86-10.66,4.51-3.02,1.43-6.09,3.25-6.67,6.86l.3.18c1.98-.92,3.83-2.1,5.82-3.01Z"/>
|
||||||
|
<path class="cls-2" d="M69.76,42.61c-.02.4.24.41.49.29.49-.23,1.84-4.67,2-5.45,1.41-6.83-1.25-13.04-6.48-17.42-.51-.42-1.85-1.63-2.47-1.35-.85.39-2.28,3.33-2.13,4.27.22,1.36,2.26,2.29,3.2,3.09,4.58,3.93,6.88,7.95,5.96,14.19-.11.77-.53,1.64-.57,2.38Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-3" d="M194.12,38.09l3.92-8.92c.17-.48.16-.87-.03-1.19-.1-.17-.35-.31-2.33-1.16-.64-.28-1.25-.54-1.47-.66-4.53-2.45-6.82-5.24-6.81-8.29,0-.25.11-.67.22-1.08.23-.85.42-1.59-.01-1.89-.38-.27-.93.05-1.42.39-2.16,1.53-5.87,8.92-5.5,11.73.12.91.63,1.72,1.08,2.44.32.5.61.98.68,1.35.13.69.12,1.71.12,2.7,0,.85-.01,1.73.07,2.45.14,1.27.41,2.65.66,3.99.1.52.2,1.03.29,1.53.73,4.03,1.66,8.09,2.57,12.02,1.23,5.34,2.51,10.86,3.27,16.38-.26.38-2.31,1.21-2.93,1.32-6.58,1.18-9.6-5.39-10.98-11.11-2.08-8.64-3.48-17.89-4.84-26.83-.59-3.87-1.19-7.87-1.84-11.64l.17-3.44-.11-.1c-.15-.15-.34-.22-.57-.2-.25.02-1.01.09-3.12,3.05-4.42,6.19-4.12,8.32-3.1,15.38.54,3.77,1.36,7.69,2.15,11.48,1.08,5.21,2.2,10.6,2.65,15.71.15,1.75-.16,2.65-1.33,3.85-2.46,2.5-5.33,2.72-7.31,2.47-2.54-.33-5.15-1.65-7.16-3.62-.22-.21-.53-.63-.83-1.03-.44-.58-.85-1.13-1.16-1.35-.22-.15-.58-.4-1.03-.08l-.07.05-4.3,9.93-.03.12c-.03,1.1,1.7,2.68,3.96,4.63.74.63,1.37,1.18,1.7,1.55.19.76-1.79,3.84-2.93,5.24-5.59,6.92-16.72,13.68-26.31,13.23-2.8-.13-10.92-2.12-14.56-3.13-.35-.1-.58.36-.28.58,3.09,2.23,10.85,7.46,17.39,8.65,1.31.24,3.53.26,4.93.26l-.84.21-.69.17-.07.14c-.06.14-.07.29,0,.42.13.27.49.46,1.22.65.74.19,1.55.27,2.41.27,3.53,0,7.81-1.44,10.4-3.16,7.58-5.01,12.84-13.13,14.45-22.3,2.38-.09,6.55-.59,9.72-2.85,2.1-1.49,3.93-4.35,5.02-7.77l.98,3.14c2.23,5.21,6.28,8.5,10.83,8.8,4.27.28,8.26-2.2,10.97-6.78,3.65-6.18,4.95-12.61,3.98-19.64-.41-2.99-1.09-6.01-1.75-8.93-.65-2.88-1.32-5.84-1.73-8.78.4.16,1.08.36,1.63-.32Z"/>
|
||||||
|
<path class="cls-3" d="M72.15,88.67c1.62,0,3.24-.28,4.8-.83,3.76-1.34,6.73-4.15,8.38-7.93,4.34-9.96,2.27-14.86-4.37-22.74-.14-.17-.29-.4-.46-.65-.51-.78-1.14-1.75-2-1.61l-.11.04c-.51.27-2.95,5.9-2.97,5.96-.44,1.23-.88,2.66-.76,3.84.14,1.34,1.85,3.88,3.51,6.33.86,1.27,1.67,2.47,2.08,3.26l.1.18c.51.96.66,1.38.68,1.54-3.41,1.96-7.57,2.02-11.45.18-6.66-3.17-8.85-12.14-10.61-19.35-.39-1.61-.77-3.14-1.17-4.54.06-.25.62-.96.93-1.34.25-.31.46-.58.59-.78.65-1.05,1.34-2.33,1.68-3,.09-.17.19-.35.28-.53.49-.9,1.04-1.92,1.15-2.93l.1-.94-5.6,5.6-2.05-9.19-.11-.07c-.09-.06-.25-.13-.46-.09-.64.12-1.39,1.18-1.4,1.19-1.27,1.86-3.66,7.82-3.68,10.17,0,.46.09.91.19,1.36.07.35.15.68.17,1l-.41.15c-1.44.53-2.93,1.08-4.33,1.72l-.31.14c-5.44,2.47-12.21,5.54-15.63,10.65-1.37,2.04-3.89,7.89-4.26,10.23-.48,3.03.9,3.67,3.24,4.36,5.32,1.58,16.59.97,21.27-1.83,2.78-1.67,3.64-4.11,4.55-6.7.22-.62.44-1.26.7-1.89,1.09,5.27,3.89,13.81,10.81,17.37,2.21,1.14,4.57,1.71,6.92,1.71ZM33.63,68.58c.09-.06.17-.11.25-.17,2.4-1.68,5.2-3.12,7.92-4.51,2.74-1.4,5.57-2.86,8.04-4.57.31-.22.61-.5.91-.77.23-.22.45-.42.65-.56l2.11,8.77c-.48.47-3.29,1.78-4.2,2.11-4.94,1.79-10.34,1.96-16.51.51.09-.29.42-.52.85-.8Z"/>
|
||||||
|
<path class="cls-3" d="M106.79,87.85c3.39-.34,7.1-4.21,8.72-8.67,3.47,1.19,7.15,2.4,10.89,3.22l.33.07c2.85.63,5.89,1.23,6.85.8.48-.21.71-.84.9-1.35.04-.11.08-.21.12-.3.51-1.18,3.08-7.19,3.42-9.56.56-3.84-2.05-9.51-5.8-12.65-2.5-2.09-5.21-2.79-7.64-1.98-4.72,1.58-6.8,6.35-8.81,10.97-1.23,2.82-2.39,5.49-4.08,7.19-1.4,1.42-2.91,2.03-4.5,1.81-2.72-.37-6.25-2.79-7.12-5.45-3.05-9.37-4.66-28.07-7.84-47.56-.39-2.41-1.53-4.35-2.31-3.66-.89.79-5.65,6.21-6.03,7.42-.11.34-.27.57-.13.99,8.04,24.03,1.75,60.83,23.02,58.68ZM120.95,68.88c.71-1.13,1.64-1.57,2.85-1.36,2.43.43,5.04,3.37,5.7,4.44-1.33-.22-4.76-1.4-6.16-1.96l-.17-.07c-1.57-.61-2.06-.92-2.21-1.06Z"/>
|
||||||
|
<path class="cls-3" d="M96.14,94.72c-2.04-1.31-7.63-4.3-9.77-5.15-1.77-.7-1.99-.58-2.91.65l-.25.33c-.79,1.04-1.46,2.17-2.11,3.27-.43.72-.87,1.47-1.34,2.18-.47-.12-1.81-.82-3.02-1.44-2.21-1.14-4.97-2.57-6.47-2.78-.52-.07-1.04.17-1.33.63l-4.54,7.2c-.19.29-.24.65-.14.99.09.33.33.61.64.77l10.55,5.21c.18.09.37.13.57.13.44,0,.87-.22,1.11-.62l3.91-6.49c3.06.97,5.9,2.7,8.65,4.37l.09.05c.19.11.57.23.99-.14l5.67-7.49c.19-.26.27-.59.22-.9-.06-.32-.24-.6-.51-.77Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 17 KiB |
BIN
frontend/dist/logo_white.png
vendored
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
frontend/dist/simon_logo.png
vendored
Normal file
|
After Width: | Height: | Size: 90 KiB |
21
frontend/node_modules/.bin/vite
generated
vendored
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "$NODE_PATH" ]; then
|
||||||
|
export NODE_PATH="/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/vite@5.4.21/node_modules/vite/node_modules:/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/vite@5.4.21/node_modules:/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/node_modules"
|
||||||
|
else
|
||||||
|
export NODE_PATH="/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/vite@5.4.21/node_modules/vite/node_modules:/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/vite@5.4.21/node_modules:/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/node_modules:$NODE_PATH"
|
||||||
|
fi
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../vite/bin/vite.js" "$@"
|
||||||
|
fi
|
||||||
297
frontend/node_modules/.modules.yaml
generated
vendored
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
{
|
||||||
|
"hoistedDependencies": {
|
||||||
|
"@babel/helper-string-parser@7.27.1": {
|
||||||
|
"@babel/helper-string-parser": "private"
|
||||||
|
},
|
||||||
|
"@babel/helper-validator-identifier@7.28.5": {
|
||||||
|
"@babel/helper-validator-identifier": "private"
|
||||||
|
},
|
||||||
|
"@babel/parser@7.29.2": {
|
||||||
|
"@babel/parser": "private"
|
||||||
|
},
|
||||||
|
"@babel/types@7.29.0": {
|
||||||
|
"@babel/types": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/aix-ppc64@0.21.5": {
|
||||||
|
"@esbuild/aix-ppc64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/android-arm64@0.21.5": {
|
||||||
|
"@esbuild/android-arm64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/android-arm@0.21.5": {
|
||||||
|
"@esbuild/android-arm": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/android-x64@0.21.5": {
|
||||||
|
"@esbuild/android-x64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/darwin-arm64@0.21.5": {
|
||||||
|
"@esbuild/darwin-arm64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/darwin-x64@0.21.5": {
|
||||||
|
"@esbuild/darwin-x64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/freebsd-arm64@0.21.5": {
|
||||||
|
"@esbuild/freebsd-arm64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/freebsd-x64@0.21.5": {
|
||||||
|
"@esbuild/freebsd-x64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/linux-arm64@0.21.5": {
|
||||||
|
"@esbuild/linux-arm64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/linux-arm@0.21.5": {
|
||||||
|
"@esbuild/linux-arm": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/linux-ia32@0.21.5": {
|
||||||
|
"@esbuild/linux-ia32": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/linux-loong64@0.21.5": {
|
||||||
|
"@esbuild/linux-loong64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/linux-mips64el@0.21.5": {
|
||||||
|
"@esbuild/linux-mips64el": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/linux-ppc64@0.21.5": {
|
||||||
|
"@esbuild/linux-ppc64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/linux-riscv64@0.21.5": {
|
||||||
|
"@esbuild/linux-riscv64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/linux-s390x@0.21.5": {
|
||||||
|
"@esbuild/linux-s390x": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/linux-x64@0.21.5": {
|
||||||
|
"@esbuild/linux-x64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/netbsd-x64@0.21.5": {
|
||||||
|
"@esbuild/netbsd-x64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/openbsd-x64@0.21.5": {
|
||||||
|
"@esbuild/openbsd-x64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/sunos-x64@0.21.5": {
|
||||||
|
"@esbuild/sunos-x64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/win32-arm64@0.21.5": {
|
||||||
|
"@esbuild/win32-arm64": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/win32-ia32@0.21.5": {
|
||||||
|
"@esbuild/win32-ia32": "private"
|
||||||
|
},
|
||||||
|
"@esbuild/win32-x64@0.21.5": {
|
||||||
|
"@esbuild/win32-x64": "private"
|
||||||
|
},
|
||||||
|
"@jridgewell/sourcemap-codec@1.5.5": {
|
||||||
|
"@jridgewell/sourcemap-codec": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-android-arm-eabi@4.60.1": {
|
||||||
|
"@rollup/rollup-android-arm-eabi": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-android-arm64@4.60.1": {
|
||||||
|
"@rollup/rollup-android-arm64": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-darwin-arm64@4.60.1": {
|
||||||
|
"@rollup/rollup-darwin-arm64": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-darwin-x64@4.60.1": {
|
||||||
|
"@rollup/rollup-darwin-x64": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-freebsd-arm64@4.60.1": {
|
||||||
|
"@rollup/rollup-freebsd-arm64": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-freebsd-x64@4.60.1": {
|
||||||
|
"@rollup/rollup-freebsd-x64": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf@4.60.1": {
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf@4.60.1": {
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-linux-arm64-gnu@4.60.1": {
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-linux-arm64-musl@4.60.1": {
|
||||||
|
"@rollup/rollup-linux-arm64-musl": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-linux-loong64-gnu@4.60.1": {
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-linux-loong64-musl@4.60.1": {
|
||||||
|
"@rollup/rollup-linux-loong64-musl": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu@4.60.1": {
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-linux-ppc64-musl@4.60.1": {
|
||||||
|
"@rollup/rollup-linux-ppc64-musl": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu@4.60.1": {
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-linux-riscv64-musl@4.60.1": {
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-linux-s390x-gnu@4.60.1": {
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-linux-x64-gnu@4.60.1": {
|
||||||
|
"@rollup/rollup-linux-x64-gnu": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-linux-x64-musl@4.60.1": {
|
||||||
|
"@rollup/rollup-linux-x64-musl": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-openbsd-x64@4.60.1": {
|
||||||
|
"@rollup/rollup-openbsd-x64": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-openharmony-arm64@4.60.1": {
|
||||||
|
"@rollup/rollup-openharmony-arm64": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-win32-arm64-msvc@4.60.1": {
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-win32-ia32-msvc@4.60.1": {
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-win32-x64-gnu@4.60.1": {
|
||||||
|
"@rollup/rollup-win32-x64-gnu": "private"
|
||||||
|
},
|
||||||
|
"@rollup/rollup-win32-x64-msvc@4.60.1": {
|
||||||
|
"@rollup/rollup-win32-x64-msvc": "private"
|
||||||
|
},
|
||||||
|
"@types/estree@1.0.8": {
|
||||||
|
"@types/estree": "private"
|
||||||
|
},
|
||||||
|
"@vue/compiler-core@3.5.31": {
|
||||||
|
"@vue/compiler-core": "private"
|
||||||
|
},
|
||||||
|
"@vue/compiler-dom@3.5.31": {
|
||||||
|
"@vue/compiler-dom": "private"
|
||||||
|
},
|
||||||
|
"@vue/compiler-sfc@3.5.31": {
|
||||||
|
"@vue/compiler-sfc": "private"
|
||||||
|
},
|
||||||
|
"@vue/compiler-ssr@3.5.31": {
|
||||||
|
"@vue/compiler-ssr": "private"
|
||||||
|
},
|
||||||
|
"@vue/reactivity@3.5.31": {
|
||||||
|
"@vue/reactivity": "private"
|
||||||
|
},
|
||||||
|
"@vue/runtime-core@3.5.31": {
|
||||||
|
"@vue/runtime-core": "private"
|
||||||
|
},
|
||||||
|
"@vue/runtime-dom@3.5.31": {
|
||||||
|
"@vue/runtime-dom": "private"
|
||||||
|
},
|
||||||
|
"@vue/server-renderer@3.5.31(vue@3.5.31)": {
|
||||||
|
"@vue/server-renderer": "private"
|
||||||
|
},
|
||||||
|
"@vue/shared@3.5.31": {
|
||||||
|
"@vue/shared": "private"
|
||||||
|
},
|
||||||
|
"csstype@3.2.3": {
|
||||||
|
"csstype": "private"
|
||||||
|
},
|
||||||
|
"entities@7.0.1": {
|
||||||
|
"entities": "private"
|
||||||
|
},
|
||||||
|
"esbuild@0.21.5": {
|
||||||
|
"esbuild": "private"
|
||||||
|
},
|
||||||
|
"estree-walker@2.0.2": {
|
||||||
|
"estree-walker": "private"
|
||||||
|
},
|
||||||
|
"fsevents@2.3.3": {
|
||||||
|
"fsevents": "private"
|
||||||
|
},
|
||||||
|
"magic-string@0.30.21": {
|
||||||
|
"magic-string": "private"
|
||||||
|
},
|
||||||
|
"nanoid@3.3.11": {
|
||||||
|
"nanoid": "private"
|
||||||
|
},
|
||||||
|
"picocolors@1.1.1": {
|
||||||
|
"picocolors": "private"
|
||||||
|
},
|
||||||
|
"postcss@8.5.8": {
|
||||||
|
"postcss": "private"
|
||||||
|
},
|
||||||
|
"rollup@4.60.1": {
|
||||||
|
"rollup": "private"
|
||||||
|
},
|
||||||
|
"source-map-js@1.2.1": {
|
||||||
|
"source-map-js": "private"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hoistPattern": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"included": {
|
||||||
|
"dependencies": true,
|
||||||
|
"devDependencies": true,
|
||||||
|
"optionalDependencies": true
|
||||||
|
},
|
||||||
|
"injectedDeps": {},
|
||||||
|
"ignoredBuilds": [],
|
||||||
|
"layoutVersion": 5,
|
||||||
|
"nodeLinker": "isolated",
|
||||||
|
"packageManager": "pnpm@10.33.2",
|
||||||
|
"pendingBuilds": [],
|
||||||
|
"publicHoistPattern": [],
|
||||||
|
"prunedAt": "Fri, 22 May 2026 17:08:07 GMT",
|
||||||
|
"registries": {
|
||||||
|
"default": "https://registry.npmjs.org/",
|
||||||
|
"@jsr": "https://npm.jsr.io/"
|
||||||
|
},
|
||||||
|
"skipped": [
|
||||||
|
"@esbuild/aix-ppc64@0.21.5",
|
||||||
|
"@esbuild/android-arm64@0.21.5",
|
||||||
|
"@esbuild/android-arm@0.21.5",
|
||||||
|
"@esbuild/android-x64@0.21.5",
|
||||||
|
"@esbuild/darwin-x64@0.21.5",
|
||||||
|
"@esbuild/freebsd-arm64@0.21.5",
|
||||||
|
"@esbuild/freebsd-x64@0.21.5",
|
||||||
|
"@esbuild/linux-arm64@0.21.5",
|
||||||
|
"@esbuild/linux-arm@0.21.5",
|
||||||
|
"@esbuild/linux-ia32@0.21.5",
|
||||||
|
"@esbuild/linux-loong64@0.21.5",
|
||||||
|
"@esbuild/linux-mips64el@0.21.5",
|
||||||
|
"@esbuild/linux-ppc64@0.21.5",
|
||||||
|
"@esbuild/linux-riscv64@0.21.5",
|
||||||
|
"@esbuild/linux-s390x@0.21.5",
|
||||||
|
"@esbuild/linux-x64@0.21.5",
|
||||||
|
"@esbuild/netbsd-x64@0.21.5",
|
||||||
|
"@esbuild/openbsd-x64@0.21.5",
|
||||||
|
"@esbuild/sunos-x64@0.21.5",
|
||||||
|
"@esbuild/win32-arm64@0.21.5",
|
||||||
|
"@esbuild/win32-ia32@0.21.5",
|
||||||
|
"@esbuild/win32-x64@0.21.5",
|
||||||
|
"@rollup/rollup-android-arm-eabi@4.60.1",
|
||||||
|
"@rollup/rollup-android-arm64@4.60.1",
|
||||||
|
"@rollup/rollup-darwin-x64@4.60.1",
|
||||||
|
"@rollup/rollup-freebsd-arm64@4.60.1",
|
||||||
|
"@rollup/rollup-freebsd-x64@4.60.1",
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf@4.60.1",
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf@4.60.1",
|
||||||
|
"@rollup/rollup-linux-arm64-gnu@4.60.1",
|
||||||
|
"@rollup/rollup-linux-arm64-musl@4.60.1",
|
||||||
|
"@rollup/rollup-linux-loong64-gnu@4.60.1",
|
||||||
|
"@rollup/rollup-linux-loong64-musl@4.60.1",
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu@4.60.1",
|
||||||
|
"@rollup/rollup-linux-ppc64-musl@4.60.1",
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu@4.60.1",
|
||||||
|
"@rollup/rollup-linux-riscv64-musl@4.60.1",
|
||||||
|
"@rollup/rollup-linux-s390x-gnu@4.60.1",
|
||||||
|
"@rollup/rollup-linux-x64-gnu@4.60.1",
|
||||||
|
"@rollup/rollup-linux-x64-musl@4.60.1",
|
||||||
|
"@rollup/rollup-openbsd-x64@4.60.1",
|
||||||
|
"@rollup/rollup-openharmony-arm64@4.60.1",
|
||||||
|
"@rollup/rollup-win32-arm64-msvc@4.60.1",
|
||||||
|
"@rollup/rollup-win32-ia32-msvc@4.60.1",
|
||||||
|
"@rollup/rollup-win32-x64-gnu@4.60.1",
|
||||||
|
"@rollup/rollup-win32-x64-msvc@4.60.1"
|
||||||
|
],
|
||||||
|
"storeDir": "/Users/dianafif/Library/pnpm/store/v10",
|
||||||
|
"virtualStoreDir": ".pnpm",
|
||||||
|
"virtualStoreDirMaxLength": 120
|
||||||
|
}
|
||||||
27
frontend/node_modules/.pnpm-workspace-state-v1.json
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"lastValidatedTimestamp": 1779469687970,
|
||||||
|
"projects": {},
|
||||||
|
"pnpmfiles": [],
|
||||||
|
"settings": {
|
||||||
|
"autoInstallPeers": true,
|
||||||
|
"dedupeDirectDeps": false,
|
||||||
|
"dedupeInjectedDeps": true,
|
||||||
|
"dedupePeerDependents": true,
|
||||||
|
"dedupePeers": false,
|
||||||
|
"dev": true,
|
||||||
|
"excludeLinksFromLockfile": false,
|
||||||
|
"hoistPattern": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"hoistWorkspacePackages": true,
|
||||||
|
"injectWorkspacePackages": false,
|
||||||
|
"linkWorkspacePackages": false,
|
||||||
|
"nodeLinker": "isolated",
|
||||||
|
"optional": true,
|
||||||
|
"peersSuffixMaxLength": 1000,
|
||||||
|
"preferWorkspacePackages": false,
|
||||||
|
"production": true,
|
||||||
|
"publicHoistPattern": []
|
||||||
|
},
|
||||||
|
"filteredInstall": false
|
||||||
|
}
|
||||||
25
frontend/node_modules/.pnpm-workspace-state.json
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"lastValidatedTimestamp": 1776666613720,
|
||||||
|
"projects": {},
|
||||||
|
"pnpmfileExists": false,
|
||||||
|
"settings": {
|
||||||
|
"autoInstallPeers": true,
|
||||||
|
"dedupeDirectDeps": false,
|
||||||
|
"dedupeInjectedDeps": true,
|
||||||
|
"dedupePeerDependents": true,
|
||||||
|
"dev": true,
|
||||||
|
"excludeLinksFromLockfile": false,
|
||||||
|
"hoistPattern": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"hoistWorkspacePackages": true,
|
||||||
|
"injectWorkspacePackages": false,
|
||||||
|
"linkWorkspacePackages": false,
|
||||||
|
"nodeLinker": "isolated",
|
||||||
|
"optional": true,
|
||||||
|
"preferWorkspacePackages": false,
|
||||||
|
"production": true,
|
||||||
|
"publicHoistPattern": []
|
||||||
|
},
|
||||||
|
"filteredInstall": false
|
||||||
|
}
|
||||||
22
frontend/node_modules/.pnpm/@babel+helper-string-parser@7.27.1/node_modules/@babel/helper-string-parser/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
||||||
|
|
||||||
|
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.
|
||||||
19
frontend/node_modules/.pnpm/@babel+helper-string-parser@7.27.1/node_modules/@babel/helper-string-parser/README.md
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# @babel/helper-string-parser
|
||||||
|
|
||||||
|
> A utility package to parse strings
|
||||||
|
|
||||||
|
See our website [@babel/helper-string-parser](https://babeljs.io/docs/babel-helper-string-parser) for more information.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Using npm:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install --save @babel/helper-string-parser
|
||||||
|
```
|
||||||
|
|
||||||
|
or using yarn:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn add @babel/helper-string-parser
|
||||||
|
```
|
||||||
295
frontend/node_modules/.pnpm/@babel+helper-string-parser@7.27.1/node_modules/@babel/helper-string-parser/lib/index.js
generated
vendored
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.readCodePoint = readCodePoint;
|
||||||
|
exports.readInt = readInt;
|
||||||
|
exports.readStringContents = readStringContents;
|
||||||
|
var _isDigit = function isDigit(code) {
|
||||||
|
return code >= 48 && code <= 57;
|
||||||
|
};
|
||||||
|
const forbiddenNumericSeparatorSiblings = {
|
||||||
|
decBinOct: new Set([46, 66, 69, 79, 95, 98, 101, 111]),
|
||||||
|
hex: new Set([46, 88, 95, 120])
|
||||||
|
};
|
||||||
|
const isAllowedNumericSeparatorSibling = {
|
||||||
|
bin: ch => ch === 48 || ch === 49,
|
||||||
|
oct: ch => ch >= 48 && ch <= 55,
|
||||||
|
dec: ch => ch >= 48 && ch <= 57,
|
||||||
|
hex: ch => ch >= 48 && ch <= 57 || ch >= 65 && ch <= 70 || ch >= 97 && ch <= 102
|
||||||
|
};
|
||||||
|
function readStringContents(type, input, pos, lineStart, curLine, errors) {
|
||||||
|
const initialPos = pos;
|
||||||
|
const initialLineStart = lineStart;
|
||||||
|
const initialCurLine = curLine;
|
||||||
|
let out = "";
|
||||||
|
let firstInvalidLoc = null;
|
||||||
|
let chunkStart = pos;
|
||||||
|
const {
|
||||||
|
length
|
||||||
|
} = input;
|
||||||
|
for (;;) {
|
||||||
|
if (pos >= length) {
|
||||||
|
errors.unterminated(initialPos, initialLineStart, initialCurLine);
|
||||||
|
out += input.slice(chunkStart, pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const ch = input.charCodeAt(pos);
|
||||||
|
if (isStringEnd(type, ch, input, pos)) {
|
||||||
|
out += input.slice(chunkStart, pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ch === 92) {
|
||||||
|
out += input.slice(chunkStart, pos);
|
||||||
|
const res = readEscapedChar(input, pos, lineStart, curLine, type === "template", errors);
|
||||||
|
if (res.ch === null && !firstInvalidLoc) {
|
||||||
|
firstInvalidLoc = {
|
||||||
|
pos,
|
||||||
|
lineStart,
|
||||||
|
curLine
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
out += res.ch;
|
||||||
|
}
|
||||||
|
({
|
||||||
|
pos,
|
||||||
|
lineStart,
|
||||||
|
curLine
|
||||||
|
} = res);
|
||||||
|
chunkStart = pos;
|
||||||
|
} else if (ch === 8232 || ch === 8233) {
|
||||||
|
++pos;
|
||||||
|
++curLine;
|
||||||
|
lineStart = pos;
|
||||||
|
} else if (ch === 10 || ch === 13) {
|
||||||
|
if (type === "template") {
|
||||||
|
out += input.slice(chunkStart, pos) + "\n";
|
||||||
|
++pos;
|
||||||
|
if (ch === 13 && input.charCodeAt(pos) === 10) {
|
||||||
|
++pos;
|
||||||
|
}
|
||||||
|
++curLine;
|
||||||
|
chunkStart = lineStart = pos;
|
||||||
|
} else {
|
||||||
|
errors.unterminated(initialPos, initialLineStart, initialCurLine);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
++pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
pos,
|
||||||
|
str: out,
|
||||||
|
firstInvalidLoc,
|
||||||
|
lineStart,
|
||||||
|
curLine,
|
||||||
|
containsInvalid: !!firstInvalidLoc
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function isStringEnd(type, ch, input, pos) {
|
||||||
|
if (type === "template") {
|
||||||
|
return ch === 96 || ch === 36 && input.charCodeAt(pos + 1) === 123;
|
||||||
|
}
|
||||||
|
return ch === (type === "double" ? 34 : 39);
|
||||||
|
}
|
||||||
|
function readEscapedChar(input, pos, lineStart, curLine, inTemplate, errors) {
|
||||||
|
const throwOnInvalid = !inTemplate;
|
||||||
|
pos++;
|
||||||
|
const res = ch => ({
|
||||||
|
pos,
|
||||||
|
ch,
|
||||||
|
lineStart,
|
||||||
|
curLine
|
||||||
|
});
|
||||||
|
const ch = input.charCodeAt(pos++);
|
||||||
|
switch (ch) {
|
||||||
|
case 110:
|
||||||
|
return res("\n");
|
||||||
|
case 114:
|
||||||
|
return res("\r");
|
||||||
|
case 120:
|
||||||
|
{
|
||||||
|
let code;
|
||||||
|
({
|
||||||
|
code,
|
||||||
|
pos
|
||||||
|
} = readHexChar(input, pos, lineStart, curLine, 2, false, throwOnInvalid, errors));
|
||||||
|
return res(code === null ? null : String.fromCharCode(code));
|
||||||
|
}
|
||||||
|
case 117:
|
||||||
|
{
|
||||||
|
let code;
|
||||||
|
({
|
||||||
|
code,
|
||||||
|
pos
|
||||||
|
} = readCodePoint(input, pos, lineStart, curLine, throwOnInvalid, errors));
|
||||||
|
return res(code === null ? null : String.fromCodePoint(code));
|
||||||
|
}
|
||||||
|
case 116:
|
||||||
|
return res("\t");
|
||||||
|
case 98:
|
||||||
|
return res("\b");
|
||||||
|
case 118:
|
||||||
|
return res("\u000b");
|
||||||
|
case 102:
|
||||||
|
return res("\f");
|
||||||
|
case 13:
|
||||||
|
if (input.charCodeAt(pos) === 10) {
|
||||||
|
++pos;
|
||||||
|
}
|
||||||
|
case 10:
|
||||||
|
lineStart = pos;
|
||||||
|
++curLine;
|
||||||
|
case 8232:
|
||||||
|
case 8233:
|
||||||
|
return res("");
|
||||||
|
case 56:
|
||||||
|
case 57:
|
||||||
|
if (inTemplate) {
|
||||||
|
return res(null);
|
||||||
|
} else {
|
||||||
|
errors.strictNumericEscape(pos - 1, lineStart, curLine);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if (ch >= 48 && ch <= 55) {
|
||||||
|
const startPos = pos - 1;
|
||||||
|
const match = /^[0-7]+/.exec(input.slice(startPos, pos + 2));
|
||||||
|
let octalStr = match[0];
|
||||||
|
let octal = parseInt(octalStr, 8);
|
||||||
|
if (octal > 255) {
|
||||||
|
octalStr = octalStr.slice(0, -1);
|
||||||
|
octal = parseInt(octalStr, 8);
|
||||||
|
}
|
||||||
|
pos += octalStr.length - 1;
|
||||||
|
const next = input.charCodeAt(pos);
|
||||||
|
if (octalStr !== "0" || next === 56 || next === 57) {
|
||||||
|
if (inTemplate) {
|
||||||
|
return res(null);
|
||||||
|
} else {
|
||||||
|
errors.strictNumericEscape(startPos, lineStart, curLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res(String.fromCharCode(octal));
|
||||||
|
}
|
||||||
|
return res(String.fromCharCode(ch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function readHexChar(input, pos, lineStart, curLine, len, forceLen, throwOnInvalid, errors) {
|
||||||
|
const initialPos = pos;
|
||||||
|
let n;
|
||||||
|
({
|
||||||
|
n,
|
||||||
|
pos
|
||||||
|
} = readInt(input, pos, lineStart, curLine, 16, len, forceLen, false, errors, !throwOnInvalid));
|
||||||
|
if (n === null) {
|
||||||
|
if (throwOnInvalid) {
|
||||||
|
errors.invalidEscapeSequence(initialPos, lineStart, curLine);
|
||||||
|
} else {
|
||||||
|
pos = initialPos - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: n,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function readInt(input, pos, lineStart, curLine, radix, len, forceLen, allowNumSeparator, errors, bailOnError) {
|
||||||
|
const start = pos;
|
||||||
|
const forbiddenSiblings = radix === 16 ? forbiddenNumericSeparatorSiblings.hex : forbiddenNumericSeparatorSiblings.decBinOct;
|
||||||
|
const isAllowedSibling = radix === 16 ? isAllowedNumericSeparatorSibling.hex : radix === 10 ? isAllowedNumericSeparatorSibling.dec : radix === 8 ? isAllowedNumericSeparatorSibling.oct : isAllowedNumericSeparatorSibling.bin;
|
||||||
|
let invalid = false;
|
||||||
|
let total = 0;
|
||||||
|
for (let i = 0, e = len == null ? Infinity : len; i < e; ++i) {
|
||||||
|
const code = input.charCodeAt(pos);
|
||||||
|
let val;
|
||||||
|
if (code === 95 && allowNumSeparator !== "bail") {
|
||||||
|
const prev = input.charCodeAt(pos - 1);
|
||||||
|
const next = input.charCodeAt(pos + 1);
|
||||||
|
if (!allowNumSeparator) {
|
||||||
|
if (bailOnError) return {
|
||||||
|
n: null,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
errors.numericSeparatorInEscapeSequence(pos, lineStart, curLine);
|
||||||
|
} else if (Number.isNaN(next) || !isAllowedSibling(next) || forbiddenSiblings.has(prev) || forbiddenSiblings.has(next)) {
|
||||||
|
if (bailOnError) return {
|
||||||
|
n: null,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
errors.unexpectedNumericSeparator(pos, lineStart, curLine);
|
||||||
|
}
|
||||||
|
++pos;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (code >= 97) {
|
||||||
|
val = code - 97 + 10;
|
||||||
|
} else if (code >= 65) {
|
||||||
|
val = code - 65 + 10;
|
||||||
|
} else if (_isDigit(code)) {
|
||||||
|
val = code - 48;
|
||||||
|
} else {
|
||||||
|
val = Infinity;
|
||||||
|
}
|
||||||
|
if (val >= radix) {
|
||||||
|
if (val <= 9 && bailOnError) {
|
||||||
|
return {
|
||||||
|
n: null,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
} else if (val <= 9 && errors.invalidDigit(pos, lineStart, curLine, radix)) {
|
||||||
|
val = 0;
|
||||||
|
} else if (forceLen) {
|
||||||
|
val = 0;
|
||||||
|
invalid = true;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++pos;
|
||||||
|
total = total * radix + val;
|
||||||
|
}
|
||||||
|
if (pos === start || len != null && pos - start !== len || invalid) {
|
||||||
|
return {
|
||||||
|
n: null,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
n: total,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function readCodePoint(input, pos, lineStart, curLine, throwOnInvalid, errors) {
|
||||||
|
const ch = input.charCodeAt(pos);
|
||||||
|
let code;
|
||||||
|
if (ch === 123) {
|
||||||
|
++pos;
|
||||||
|
({
|
||||||
|
code,
|
||||||
|
pos
|
||||||
|
} = readHexChar(input, pos, lineStart, curLine, input.indexOf("}", pos) - pos, true, throwOnInvalid, errors));
|
||||||
|
++pos;
|
||||||
|
if (code !== null && code > 0x10ffff) {
|
||||||
|
if (throwOnInvalid) {
|
||||||
|
errors.invalidCodePoint(pos, lineStart, curLine);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
code: null,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
({
|
||||||
|
code,
|
||||||
|
pos
|
||||||
|
} = readHexChar(input, pos, lineStart, curLine, 4, false, throwOnInvalid, errors));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
pos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
frontend/node_modules/.pnpm/@babel+helper-string-parser@7.27.1/node_modules/@babel/helper-string-parser/lib/index.js.map
generated
vendored
Normal file
31
frontend/node_modules/.pnpm/@babel+helper-string-parser@7.27.1/node_modules/@babel/helper-string-parser/package.json
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "@babel/helper-string-parser",
|
||||||
|
"version": "7.27.1",
|
||||||
|
"description": "A utility package to parse strings",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/babel/babel.git",
|
||||||
|
"directory": "packages/babel-helper-string-parser"
|
||||||
|
},
|
||||||
|
"homepage": "https://babel.dev/docs/en/next/babel-helper-string-parser",
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"main": "./lib/index.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"charcodes": "^0.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"author": "The Babel Team (https://babel.dev/team)",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./lib/index.d.ts",
|
||||||
|
"default": "./lib/index.js"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
22
frontend/node_modules/.pnpm/@babel+helper-validator-identifier@7.28.5/node_modules/@babel/helper-validator-identifier/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
||||||
|
|
||||||
|
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.
|
||||||
19
frontend/node_modules/.pnpm/@babel+helper-validator-identifier@7.28.5/node_modules/@babel/helper-validator-identifier/README.md
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# @babel/helper-validator-identifier
|
||||||
|
|
||||||
|
> Validate identifier/keywords name
|
||||||
|
|
||||||
|
See our website [@babel/helper-validator-identifier](https://babeljs.io/docs/babel-helper-validator-identifier) for more information.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Using npm:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install --save @babel/helper-validator-identifier
|
||||||
|
```
|
||||||
|
|
||||||
|
or using yarn:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn add @babel/helper-validator-identifier
|
||||||
|
```
|
||||||
70
frontend/node_modules/.pnpm/@babel+helper-validator-identifier@7.28.5/node_modules/@babel/helper-validator-identifier/lib/identifier.js
generated
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.isIdentifierChar = isIdentifierChar;
|
||||||
|
exports.isIdentifierName = isIdentifierName;
|
||||||
|
exports.isIdentifierStart = isIdentifierStart;
|
||||||
|
let nonASCIIidentifierStartChars = "\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u037f\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u052f\u0531-\u0556\u0559\u0560-\u0588\u05d0-\u05ea\u05ef-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u0860-\u086a\u0870-\u0887\u0889-\u088f\u08a0-\u08c9\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u09fc\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0af9\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d\u0c58-\u0c5a\u0c5c\u0c5d\u0c60\u0c61\u0c80\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cdc-\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d04-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d54-\u0d56\u0d5f-\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e86-\u0e8a\u0e8c-\u0ea3\u0ea5\u0ea7-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f5\u13f8-\u13fd\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f8\u1700-\u1711\u171f-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1878\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191e\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4c\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1c80-\u1c8a\u1c90-\u1cba\u1cbd-\u1cbf\u1ce9-\u1cec\u1cee-\u1cf3\u1cf5\u1cf6\u1cfa\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2118-\u211d\u2124\u2126\u2128\u212a-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309b-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312f\u3131-\u318e\u31a0-\u31bf\u31f0-\u31ff\u3400-\u4dbf\u4e00-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua69d\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua7dc\ua7f1-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua8fd\ua8fe\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\ua9e0-\ua9e4\ua9e6-\ua9ef\ua9fa-\ua9fe\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa7e-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab5a\uab5c-\uab69\uab70-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc";
|
||||||
|
let nonASCIIidentifierChars = "\xb7\u0300-\u036f\u0387\u0483-\u0487\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u0669\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u06f0-\u06f9\u0711\u0730-\u074a\u07a6-\u07b0\u07c0-\u07c9\u07eb-\u07f3\u07fd\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u0897-\u089f\u08ca-\u08e1\u08e3-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u09e6-\u09ef\u09fe\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0ae6-\u0aef\u0afa-\u0aff\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b55-\u0b57\u0b62\u0b63\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0c00-\u0c04\u0c3c\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c66-\u0c6f\u0c81-\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0ce6-\u0cef\u0cf3\u0d00-\u0d03\u0d3b\u0d3c\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d66-\u0d6f\u0d81-\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0de6-\u0def\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0e50-\u0e59\u0eb1\u0eb4-\u0ebc\u0ec8-\u0ece\u0ed0-\u0ed9\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1040-\u1049\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u1369-\u1371\u1712-\u1715\u1732-\u1734\u1752\u1753\u1772\u1773\u17b4-\u17d3\u17dd\u17e0-\u17e9\u180b-\u180d\u180f-\u1819\u18a9\u1920-\u192b\u1930-\u193b\u1946-\u194f\u19d0-\u19da\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1ab0-\u1abd\u1abf-\u1add\u1ae0-\u1aeb\u1b00-\u1b04\u1b34-\u1b44\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1bad\u1bb0-\u1bb9\u1be6-\u1bf3\u1c24-\u1c37\u1c40-\u1c49\u1c50-\u1c59\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf4\u1cf7-\u1cf9\u1dc0-\u1dff\u200c\u200d\u203f\u2040\u2054\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\u30fb\ua620-\ua629\ua66f\ua674-\ua67d\ua69e\ua69f\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua82c\ua880\ua881\ua8b4-\ua8c5\ua8d0-\ua8d9\ua8e0-\ua8f1\ua8ff-\ua909\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\ua9d0-\ua9d9\ua9e5\ua9f0-\ua9f9\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa50-\uaa59\uaa7b-\uaa7d\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uaaeb-\uaaef\uaaf5\uaaf6\uabe3-\uabea\uabec\uabed\uabf0-\uabf9\ufb1e\ufe00-\ufe0f\ufe20-\ufe2f\ufe33\ufe34\ufe4d-\ufe4f\uff10-\uff19\uff3f\uff65";
|
||||||
|
const nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]");
|
||||||
|
const nonASCIIidentifier = new RegExp("[" + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "]");
|
||||||
|
nonASCIIidentifierStartChars = nonASCIIidentifierChars = null;
|
||||||
|
const astralIdentifierStartCodes = [0, 11, 2, 25, 2, 18, 2, 1, 2, 14, 3, 13, 35, 122, 70, 52, 268, 28, 4, 48, 48, 31, 14, 29, 6, 37, 11, 29, 3, 35, 5, 7, 2, 4, 43, 157, 19, 35, 5, 35, 5, 39, 9, 51, 13, 10, 2, 14, 2, 6, 2, 1, 2, 10, 2, 14, 2, 6, 2, 1, 4, 51, 13, 310, 10, 21, 11, 7, 25, 5, 2, 41, 2, 8, 70, 5, 3, 0, 2, 43, 2, 1, 4, 0, 3, 22, 11, 22, 10, 30, 66, 18, 2, 1, 11, 21, 11, 25, 7, 25, 39, 55, 7, 1, 65, 0, 16, 3, 2, 2, 2, 28, 43, 28, 4, 28, 36, 7, 2, 27, 28, 53, 11, 21, 11, 18, 14, 17, 111, 72, 56, 50, 14, 50, 14, 35, 39, 27, 10, 22, 251, 41, 7, 1, 17, 5, 57, 28, 11, 0, 9, 21, 43, 17, 47, 20, 28, 22, 13, 52, 58, 1, 3, 0, 14, 44, 33, 24, 27, 35, 30, 0, 3, 0, 9, 34, 4, 0, 13, 47, 15, 3, 22, 0, 2, 0, 36, 17, 2, 24, 20, 1, 64, 6, 2, 0, 2, 3, 2, 14, 2, 9, 8, 46, 39, 7, 3, 1, 3, 21, 2, 6, 2, 1, 2, 4, 4, 0, 19, 0, 13, 4, 31, 9, 2, 0, 3, 0, 2, 37, 2, 0, 26, 0, 2, 0, 45, 52, 19, 3, 21, 2, 31, 47, 21, 1, 2, 0, 185, 46, 42, 3, 37, 47, 21, 0, 60, 42, 14, 0, 72, 26, 38, 6, 186, 43, 117, 63, 32, 7, 3, 0, 3, 7, 2, 1, 2, 23, 16, 0, 2, 0, 95, 7, 3, 38, 17, 0, 2, 0, 29, 0, 11, 39, 8, 0, 22, 0, 12, 45, 20, 0, 19, 72, 200, 32, 32, 8, 2, 36, 18, 0, 50, 29, 113, 6, 2, 1, 2, 37, 22, 0, 26, 5, 2, 1, 2, 31, 15, 0, 24, 43, 261, 18, 16, 0, 2, 12, 2, 33, 125, 0, 80, 921, 103, 110, 18, 195, 2637, 96, 16, 1071, 18, 5, 26, 3994, 6, 582, 6842, 29, 1763, 568, 8, 30, 18, 78, 18, 29, 19, 47, 17, 3, 32, 20, 6, 18, 433, 44, 212, 63, 33, 24, 3, 24, 45, 74, 6, 0, 67, 12, 65, 1, 2, 0, 15, 4, 10, 7381, 42, 31, 98, 114, 8702, 3, 2, 6, 2, 1, 2, 290, 16, 0, 30, 2, 3, 0, 15, 3, 9, 395, 2309, 106, 6, 12, 4, 8, 8, 9, 5991, 84, 2, 70, 2, 1, 3, 0, 3, 1, 3, 3, 2, 11, 2, 0, 2, 6, 2, 64, 2, 3, 3, 7, 2, 6, 2, 27, 2, 3, 2, 4, 2, 0, 4, 6, 2, 339, 3, 24, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 7, 1845, 30, 7, 5, 262, 61, 147, 44, 11, 6, 17, 0, 322, 29, 19, 43, 485, 27, 229, 29, 3, 0, 208, 30, 2, 2, 2, 1, 2, 6, 3, 4, 10, 1, 225, 6, 2, 3, 2, 1, 2, 14, 2, 196, 60, 67, 8, 0, 1205, 3, 2, 26, 2, 1, 2, 0, 3, 0, 2, 9, 2, 3, 2, 0, 2, 0, 7, 0, 5, 0, 2, 0, 2, 0, 2, 2, 2, 1, 2, 0, 3, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 1, 2, 0, 3, 3, 2, 6, 2, 3, 2, 3, 2, 0, 2, 9, 2, 16, 6, 2, 2, 4, 2, 16, 4421, 42719, 33, 4381, 3, 5773, 3, 7472, 16, 621, 2467, 541, 1507, 4938, 6, 8489];
|
||||||
|
const astralIdentifierCodes = [509, 0, 227, 0, 150, 4, 294, 9, 1368, 2, 2, 1, 6, 3, 41, 2, 5, 0, 166, 1, 574, 3, 9, 9, 7, 9, 32, 4, 318, 1, 78, 5, 71, 10, 50, 3, 123, 2, 54, 14, 32, 10, 3, 1, 11, 3, 46, 10, 8, 0, 46, 9, 7, 2, 37, 13, 2, 9, 6, 1, 45, 0, 13, 2, 49, 13, 9, 3, 2, 11, 83, 11, 7, 0, 3, 0, 158, 11, 6, 9, 7, 3, 56, 1, 2, 6, 3, 1, 3, 2, 10, 0, 11, 1, 3, 6, 4, 4, 68, 8, 2, 0, 3, 0, 2, 3, 2, 4, 2, 0, 15, 1, 83, 17, 10, 9, 5, 0, 82, 19, 13, 9, 214, 6, 3, 8, 28, 1, 83, 16, 16, 9, 82, 12, 9, 9, 7, 19, 58, 14, 5, 9, 243, 14, 166, 9, 71, 5, 2, 1, 3, 3, 2, 0, 2, 1, 13, 9, 120, 6, 3, 6, 4, 0, 29, 9, 41, 6, 2, 3, 9, 0, 10, 10, 47, 15, 199, 7, 137, 9, 54, 7, 2, 7, 17, 9, 57, 21, 2, 13, 123, 5, 4, 0, 2, 1, 2, 6, 2, 0, 9, 9, 49, 4, 2, 1, 2, 4, 9, 9, 55, 9, 266, 3, 10, 1, 2, 0, 49, 6, 4, 4, 14, 10, 5350, 0, 7, 14, 11465, 27, 2343, 9, 87, 9, 39, 4, 60, 6, 26, 9, 535, 9, 470, 0, 2, 54, 8, 3, 82, 0, 12, 1, 19628, 1, 4178, 9, 519, 45, 3, 22, 543, 4, 4, 5, 9, 7, 3, 6, 31, 3, 149, 2, 1418, 49, 513, 54, 5, 49, 9, 0, 15, 0, 23, 4, 2, 14, 1361, 6, 2, 16, 3, 6, 2, 1, 2, 4, 101, 0, 161, 6, 10, 9, 357, 0, 62, 13, 499, 13, 245, 1, 2, 9, 233, 0, 3, 0, 8, 1, 6, 0, 475, 6, 110, 6, 6, 9, 4759, 9, 787719, 239];
|
||||||
|
function isInAstralSet(code, set) {
|
||||||
|
let pos = 0x10000;
|
||||||
|
for (let i = 0, length = set.length; i < length; i += 2) {
|
||||||
|
pos += set[i];
|
||||||
|
if (pos > code) return false;
|
||||||
|
pos += set[i + 1];
|
||||||
|
if (pos >= code) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function isIdentifierStart(code) {
|
||||||
|
if (code < 65) return code === 36;
|
||||||
|
if (code <= 90) return true;
|
||||||
|
if (code < 97) return code === 95;
|
||||||
|
if (code <= 122) return true;
|
||||||
|
if (code <= 0xffff) {
|
||||||
|
return code >= 0xaa && nonASCIIidentifierStart.test(String.fromCharCode(code));
|
||||||
|
}
|
||||||
|
return isInAstralSet(code, astralIdentifierStartCodes);
|
||||||
|
}
|
||||||
|
function isIdentifierChar(code) {
|
||||||
|
if (code < 48) return code === 36;
|
||||||
|
if (code < 58) return true;
|
||||||
|
if (code < 65) return false;
|
||||||
|
if (code <= 90) return true;
|
||||||
|
if (code < 97) return code === 95;
|
||||||
|
if (code <= 122) return true;
|
||||||
|
if (code <= 0xffff) {
|
||||||
|
return code >= 0xaa && nonASCIIidentifier.test(String.fromCharCode(code));
|
||||||
|
}
|
||||||
|
return isInAstralSet(code, astralIdentifierStartCodes) || isInAstralSet(code, astralIdentifierCodes);
|
||||||
|
}
|
||||||
|
function isIdentifierName(name) {
|
||||||
|
let isFirst = true;
|
||||||
|
for (let i = 0; i < name.length; i++) {
|
||||||
|
let cp = name.charCodeAt(i);
|
||||||
|
if ((cp & 0xfc00) === 0xd800 && i + 1 < name.length) {
|
||||||
|
const trail = name.charCodeAt(++i);
|
||||||
|
if ((trail & 0xfc00) === 0xdc00) {
|
||||||
|
cp = 0x10000 + ((cp & 0x3ff) << 10) + (trail & 0x3ff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isFirst) {
|
||||||
|
isFirst = false;
|
||||||
|
if (!isIdentifierStart(cp)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (!isIdentifierChar(cp)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !isFirst;
|
||||||
|
}
|
||||||
|
|
||||||
|
//# sourceMappingURL=identifier.js.map
|
||||||
57
frontend/node_modules/.pnpm/@babel+helper-validator-identifier@7.28.5/node_modules/@babel/helper-validator-identifier/lib/index.js
generated
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isIdentifierChar", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _identifier.isIdentifierChar;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isIdentifierName", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _identifier.isIdentifierName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isIdentifierStart", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _identifier.isIdentifierStart;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isKeyword", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _keyword.isKeyword;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isReservedWord", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _keyword.isReservedWord;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isStrictBindOnlyReservedWord", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _keyword.isStrictBindOnlyReservedWord;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isStrictBindReservedWord", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _keyword.isStrictBindReservedWord;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(exports, "isStrictReservedWord", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function () {
|
||||||
|
return _keyword.isStrictReservedWord;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var _identifier = require("./identifier.js");
|
||||||
|
var _keyword = require("./keyword.js");
|
||||||
|
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"names":["_identifier","require","_keyword"],"sources":["../src/index.ts"],"sourcesContent":["export {\n isIdentifierName,\n isIdentifierChar,\n isIdentifierStart,\n} from \"./identifier.ts\";\nexport {\n isReservedWord,\n isStrictBindOnlyReservedWord,\n isStrictBindReservedWord,\n isStrictReservedWord,\n isKeyword,\n} from \"./keyword.ts\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAAA,WAAA,GAAAC,OAAA;AAKA,IAAAC,QAAA,GAAAD,OAAA","ignoreList":[]}
|
||||||
35
frontend/node_modules/.pnpm/@babel+helper-validator-identifier@7.28.5/node_modules/@babel/helper-validator-identifier/lib/keyword.js
generated
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.isKeyword = isKeyword;
|
||||||
|
exports.isReservedWord = isReservedWord;
|
||||||
|
exports.isStrictBindOnlyReservedWord = isStrictBindOnlyReservedWord;
|
||||||
|
exports.isStrictBindReservedWord = isStrictBindReservedWord;
|
||||||
|
exports.isStrictReservedWord = isStrictReservedWord;
|
||||||
|
const reservedWords = {
|
||||||
|
keyword: ["break", "case", "catch", "continue", "debugger", "default", "do", "else", "finally", "for", "function", "if", "return", "switch", "throw", "try", "var", "const", "while", "with", "new", "this", "super", "class", "extends", "export", "import", "null", "true", "false", "in", "instanceof", "typeof", "void", "delete"],
|
||||||
|
strict: ["implements", "interface", "let", "package", "private", "protected", "public", "static", "yield"],
|
||||||
|
strictBind: ["eval", "arguments"]
|
||||||
|
};
|
||||||
|
const keywords = new Set(reservedWords.keyword);
|
||||||
|
const reservedWordsStrictSet = new Set(reservedWords.strict);
|
||||||
|
const reservedWordsStrictBindSet = new Set(reservedWords.strictBind);
|
||||||
|
function isReservedWord(word, inModule) {
|
||||||
|
return inModule && word === "await" || word === "enum";
|
||||||
|
}
|
||||||
|
function isStrictReservedWord(word, inModule) {
|
||||||
|
return isReservedWord(word, inModule) || reservedWordsStrictSet.has(word);
|
||||||
|
}
|
||||||
|
function isStrictBindOnlyReservedWord(word) {
|
||||||
|
return reservedWordsStrictBindSet.has(word);
|
||||||
|
}
|
||||||
|
function isStrictBindReservedWord(word, inModule) {
|
||||||
|
return isStrictReservedWord(word, inModule) || isStrictBindOnlyReservedWord(word);
|
||||||
|
}
|
||||||
|
function isKeyword(word) {
|
||||||
|
return keywords.has(word);
|
||||||
|
}
|
||||||
|
|
||||||
|
//# sourceMappingURL=keyword.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"names":["reservedWords","keyword","strict","strictBind","keywords","Set","reservedWordsStrictSet","reservedWordsStrictBindSet","isReservedWord","word","inModule","isStrictReservedWord","has","isStrictBindOnlyReservedWord","isStrictBindReservedWord","isKeyword"],"sources":["../src/keyword.ts"],"sourcesContent":["const reservedWords = {\n keyword: [\n \"break\",\n \"case\",\n \"catch\",\n \"continue\",\n \"debugger\",\n \"default\",\n \"do\",\n \"else\",\n \"finally\",\n \"for\",\n \"function\",\n \"if\",\n \"return\",\n \"switch\",\n \"throw\",\n \"try\",\n \"var\",\n \"const\",\n \"while\",\n \"with\",\n \"new\",\n \"this\",\n \"super\",\n \"class\",\n \"extends\",\n \"export\",\n \"import\",\n \"null\",\n \"true\",\n \"false\",\n \"in\",\n \"instanceof\",\n \"typeof\",\n \"void\",\n \"delete\",\n ],\n strict: [\n \"implements\",\n \"interface\",\n \"let\",\n \"package\",\n \"private\",\n \"protected\",\n \"public\",\n \"static\",\n \"yield\",\n ],\n strictBind: [\"eval\", \"arguments\"],\n};\nconst keywords = new Set(reservedWords.keyword);\nconst reservedWordsStrictSet = new Set(reservedWords.strict);\nconst reservedWordsStrictBindSet = new Set(reservedWords.strictBind);\n\n/**\n * Checks if word is a reserved word in non-strict mode\n */\nexport function isReservedWord(word: string, inModule: boolean): boolean {\n return (inModule && word === \"await\") || word === \"enum\";\n}\n\n/**\n * Checks if word is a reserved word in non-binding strict mode\n *\n * Includes non-strict reserved words\n */\nexport function isStrictReservedWord(word: string, inModule: boolean): boolean {\n return isReservedWord(word, inModule) || reservedWordsStrictSet.has(word);\n}\n\n/**\n * Checks if word is a reserved word in binding strict mode, but it is allowed as\n * a normal identifier.\n */\nexport function isStrictBindOnlyReservedWord(word: string): boolean {\n return reservedWordsStrictBindSet.has(word);\n}\n\n/**\n * Checks if word is a reserved word in binding strict mode\n *\n * Includes non-strict reserved words and non-binding strict reserved words\n */\nexport function isStrictBindReservedWord(\n word: string,\n inModule: boolean,\n): boolean {\n return (\n isStrictReservedWord(word, inModule) || isStrictBindOnlyReservedWord(word)\n );\n}\n\nexport function isKeyword(word: string): boolean {\n return keywords.has(word);\n}\n"],"mappings":";;;;;;;;;;AAAA,MAAMA,aAAa,GAAG;EACpBC,OAAO,EAAE,CACP,OAAO,EACP,MAAM,EACN,OAAO,EACP,UAAU,EACV,UAAU,EACV,SAAS,EACT,IAAI,EACJ,MAAM,EACN,SAAS,EACT,KAAK,EACL,UAAU,EACV,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,KAAK,EACL,KAAK,EACL,OAAO,EACP,OAAO,EACP,MAAM,EACN,KAAK,EACL,MAAM,EACN,OAAO,EACP,OAAO,EACP,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,MAAM,EACN,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,MAAM,EACN,QAAQ,CACT;EACDC,MAAM,EAAE,CACN,YAAY,EACZ,WAAW,EACX,KAAK,EACL,SAAS,EACT,SAAS,EACT,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,OAAO,CACR;EACDC,UAAU,EAAE,CAAC,MAAM,EAAE,WAAW;AAClC,CAAC;AACD,MAAMC,QAAQ,GAAG,IAAIC,GAAG,CAACL,aAAa,CAACC,OAAO,CAAC;AAC/C,MAAMK,sBAAsB,GAAG,IAAID,GAAG,CAACL,aAAa,CAACE,MAAM,CAAC;AAC5D,MAAMK,0BAA0B,GAAG,IAAIF,GAAG,CAACL,aAAa,CAACG,UAAU,CAAC;AAK7D,SAASK,cAAcA,CAACC,IAAY,EAAEC,QAAiB,EAAW;EACvE,OAAQA,QAAQ,IAAID,IAAI,KAAK,OAAO,IAAKA,IAAI,KAAK,MAAM;AAC1D;AAOO,SAASE,oBAAoBA,CAACF,IAAY,EAAEC,QAAiB,EAAW;EAC7E,OAAOF,cAAc,CAACC,IAAI,EAAEC,QAAQ,CAAC,IAAIJ,sBAAsB,CAACM,GAAG,CAACH,IAAI,CAAC;AAC3E;AAMO,SAASI,4BAA4BA,CAACJ,IAAY,EAAW;EAClE,OAAOF,0BAA0B,CAACK,GAAG,CAACH,IAAI,CAAC;AAC7C;AAOO,SAASK,wBAAwBA,CACtCL,IAAY,EACZC,QAAiB,EACR;EACT,OACEC,oBAAoB,CAACF,IAAI,EAAEC,QAAQ,CAAC,IAAIG,4BAA4B,CAACJ,IAAI,CAAC;AAE9E;AAEO,SAASM,SAASA,CAACN,IAAY,EAAW;EAC/C,OAAOL,QAAQ,CAACQ,GAAG,CAACH,IAAI,CAAC;AAC3B","ignoreList":[]}
|
||||||
31
frontend/node_modules/.pnpm/@babel+helper-validator-identifier@7.28.5/node_modules/@babel/helper-validator-identifier/package.json
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "@babel/helper-validator-identifier",
|
||||||
|
"version": "7.28.5",
|
||||||
|
"description": "Validate identifier/keywords name",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/babel/babel.git",
|
||||||
|
"directory": "packages/babel-helper-validator-identifier"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"main": "./lib/index.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./lib/index.d.ts",
|
||||||
|
"default": "./lib/index.js"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@unicode/unicode-17.0.0": "^1.6.10",
|
||||||
|
"charcodes": "^0.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"author": "The Babel Team (https://babel.dev/team)",
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
1073
frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/parser/CHANGELOG.md
generated
vendored
Normal file
19
frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/parser/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (C) 2012-2014 by various contributors (see AUTHORS)
|
||||||
|
|
||||||
|
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.
|
||||||
19
frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/parser/README.md
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# @babel/parser
|
||||||
|
|
||||||
|
> A JavaScript parser
|
||||||
|
|
||||||
|
See our website [@babel/parser](https://babeljs.io/docs/babel-parser) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20parser%22+is%3Aopen) associated with this package.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Using npm:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install --save-dev @babel/parser
|
||||||
|
```
|
||||||
|
|
||||||
|
or using yarn:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn add @babel/parser --dev
|
||||||
|
```
|
||||||
15
frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/parser/bin/babel-parser.js
generated
vendored
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/* eslint-disable no-var, unicorn/prefer-node-protocol */
|
||||||
|
|
||||||
|
var parser = require("..");
|
||||||
|
var fs = require("fs");
|
||||||
|
|
||||||
|
var filename = process.argv[2];
|
||||||
|
if (!filename) {
|
||||||
|
console.error("no filename specified");
|
||||||
|
} else {
|
||||||
|
var file = fs.readFileSync(filename, "utf8");
|
||||||
|
var ast = parser.parse(file);
|
||||||
|
|
||||||
|
console.log(JSON.stringify(ast, null, " "));
|
||||||
|
}
|
||||||
14582
frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/parser/lib/index.js
generated
vendored
Normal file
1
frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/parser/lib/index.js.map
generated
vendored
Normal file
21
frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/parser/node_modules/.bin/parser
generated
vendored
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "$NODE_PATH" ]; then
|
||||||
|
export NODE_PATH="/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/parser/bin/node_modules:/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/parser/node_modules:/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/node_modules:/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules:/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/node_modules"
|
||||||
|
else
|
||||||
|
export NODE_PATH="/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/parser/bin/node_modules:/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/parser/node_modules:/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/node_modules:/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules:/Users/dianafif/Software/shooting-event/frontend/node_modules/.pnpm/node_modules:$NODE_PATH"
|
||||||
|
fi
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../../bin/babel-parser.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../../bin/babel-parser.js" "$@"
|
||||||
|
fi
|
||||||
50
frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/parser/package.json
generated
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "@babel/parser",
|
||||||
|
"version": "7.29.2",
|
||||||
|
"description": "A JavaScript parser",
|
||||||
|
"author": "The Babel Team (https://babel.dev/team)",
|
||||||
|
"homepage": "https://babel.dev/docs/en/next/babel-parser",
|
||||||
|
"bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A+parser+%28babylon%29%22+is%3Aopen",
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"babel",
|
||||||
|
"javascript",
|
||||||
|
"parser",
|
||||||
|
"tc39",
|
||||||
|
"ecmascript",
|
||||||
|
"@babel/parser"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/babel/babel.git",
|
||||||
|
"directory": "packages/babel-parser"
|
||||||
|
},
|
||||||
|
"main": "./lib/index.js",
|
||||||
|
"types": "./typings/babel-parser.d.ts",
|
||||||
|
"files": [
|
||||||
|
"bin",
|
||||||
|
"lib",
|
||||||
|
"typings/babel-parser.d.ts",
|
||||||
|
"index.cjs"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
},
|
||||||
|
"# dependencies": "This package doesn't actually have runtime dependencies. @babel/types is only needed for type definitions.",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.29.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/code-frame": "^7.29.0",
|
||||||
|
"@babel/helper-check-duplicate-nodes": "^7.28.6",
|
||||||
|
"@babel/helper-fixtures": "^7.28.6",
|
||||||
|
"@babel/helper-string-parser": "^7.27.1",
|
||||||
|
"@babel/helper-validator-identifier": "^7.28.5",
|
||||||
|
"charcodes": "^0.2.0"
|
||||||
|
},
|
||||||
|
"bin": "./bin/babel-parser.js",
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
262
frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/parser/typings/babel-parser.d.ts
generated
vendored
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
// This file is auto-generated! Do not modify it directly.
|
||||||
|
// Run `yarn gulp bundle-dts` to re-generate it.
|
||||||
|
/* eslint-disable @typescript-eslint/consistent-type-imports, @typescript-eslint/no-redundant-type-constituents */
|
||||||
|
import { File, Expression } from '@babel/types';
|
||||||
|
|
||||||
|
declare class Position {
|
||||||
|
line: number;
|
||||||
|
column: number;
|
||||||
|
index: number;
|
||||||
|
constructor(line: number, col: number, index: number);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyntaxPlugin = "flow" | "typescript" | "jsx" | "pipelineOperator" | "placeholders";
|
||||||
|
type ParseErrorCode = "BABEL_PARSER_SYNTAX_ERROR" | "BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED";
|
||||||
|
interface ParseErrorSpecification<ErrorDetails> {
|
||||||
|
code: ParseErrorCode;
|
||||||
|
reasonCode: string;
|
||||||
|
syntaxPlugin?: SyntaxPlugin;
|
||||||
|
missingPlugin?: string | string[];
|
||||||
|
loc: Position;
|
||||||
|
details: ErrorDetails;
|
||||||
|
pos: number;
|
||||||
|
}
|
||||||
|
type ParseError$1<ErrorDetails> = SyntaxError & ParseErrorSpecification<ErrorDetails>;
|
||||||
|
|
||||||
|
type BABEL_8_BREAKING = false;
|
||||||
|
type IF_BABEL_7<V> = false extends BABEL_8_BREAKING ? V : never;
|
||||||
|
|
||||||
|
type Plugin$1 =
|
||||||
|
| "asyncDoExpressions"
|
||||||
|
| IF_BABEL_7<"asyncGenerators">
|
||||||
|
| IF_BABEL_7<"bigInt">
|
||||||
|
| IF_BABEL_7<"classPrivateMethods">
|
||||||
|
| IF_BABEL_7<"classPrivateProperties">
|
||||||
|
| IF_BABEL_7<"classProperties">
|
||||||
|
| IF_BABEL_7<"classStaticBlock">
|
||||||
|
| IF_BABEL_7<"decimal">
|
||||||
|
| "decorators-legacy"
|
||||||
|
| "deferredImportEvaluation"
|
||||||
|
| "decoratorAutoAccessors"
|
||||||
|
| "destructuringPrivate"
|
||||||
|
| IF_BABEL_7<"deprecatedImportAssert">
|
||||||
|
| "doExpressions"
|
||||||
|
| IF_BABEL_7<"dynamicImport">
|
||||||
|
| IF_BABEL_7<"explicitResourceManagement">
|
||||||
|
| "exportDefaultFrom"
|
||||||
|
| IF_BABEL_7<"exportNamespaceFrom">
|
||||||
|
| "flow"
|
||||||
|
| "flowComments"
|
||||||
|
| "functionBind"
|
||||||
|
| "functionSent"
|
||||||
|
| "importMeta"
|
||||||
|
| "jsx"
|
||||||
|
| IF_BABEL_7<"jsonStrings">
|
||||||
|
| IF_BABEL_7<"logicalAssignment">
|
||||||
|
| IF_BABEL_7<"importAssertions">
|
||||||
|
| IF_BABEL_7<"importReflection">
|
||||||
|
| "moduleBlocks"
|
||||||
|
| IF_BABEL_7<"moduleStringNames">
|
||||||
|
| IF_BABEL_7<"nullishCoalescingOperator">
|
||||||
|
| IF_BABEL_7<"numericSeparator">
|
||||||
|
| IF_BABEL_7<"objectRestSpread">
|
||||||
|
| IF_BABEL_7<"optionalCatchBinding">
|
||||||
|
| IF_BABEL_7<"optionalChaining">
|
||||||
|
| "partialApplication"
|
||||||
|
| "placeholders"
|
||||||
|
| IF_BABEL_7<"privateIn">
|
||||||
|
| IF_BABEL_7<"regexpUnicodeSets">
|
||||||
|
| "sourcePhaseImports"
|
||||||
|
| "throwExpressions"
|
||||||
|
| IF_BABEL_7<"topLevelAwait">
|
||||||
|
| "v8intrinsic"
|
||||||
|
| ParserPluginWithOptions[0];
|
||||||
|
|
||||||
|
type ParserPluginWithOptions =
|
||||||
|
| ["decorators", DecoratorsPluginOptions]
|
||||||
|
| ["discardBinding", { syntaxType: "void" }]
|
||||||
|
| ["estree", { classFeatures?: boolean }]
|
||||||
|
| IF_BABEL_7<["importAttributes", { deprecatedAssertSyntax: boolean }]>
|
||||||
|
| IF_BABEL_7<["moduleAttributes", { version: "may-2020" }]>
|
||||||
|
| ["optionalChainingAssign", { version: "2023-07" }]
|
||||||
|
| ["pipelineOperator", PipelineOperatorPluginOptions]
|
||||||
|
| ["recordAndTuple", RecordAndTuplePluginOptions]
|
||||||
|
| ["flow", FlowPluginOptions]
|
||||||
|
| ["typescript", TypeScriptPluginOptions];
|
||||||
|
|
||||||
|
type PluginConfig = Plugin$1 | ParserPluginWithOptions;
|
||||||
|
|
||||||
|
interface DecoratorsPluginOptions {
|
||||||
|
decoratorsBeforeExport?: boolean;
|
||||||
|
allowCallParenthesized?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PipelineOperatorPluginOptions {
|
||||||
|
proposal: BABEL_8_BREAKING extends false
|
||||||
|
? "minimal" | "fsharp" | "hack" | "smart"
|
||||||
|
: "fsharp" | "hack";
|
||||||
|
topicToken?: "%" | "#" | "@@" | "^^" | "^";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecordAndTuplePluginOptions {
|
||||||
|
syntaxType: "bar" | "hash";
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlowPluginOptions = BABEL_8_BREAKING extends true
|
||||||
|
? {
|
||||||
|
all?: boolean;
|
||||||
|
enums?: boolean;
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
all?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TypeScriptPluginOptions {
|
||||||
|
dts?: boolean;
|
||||||
|
disallowAmbiguousJSXLike?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Plugin = PluginConfig;
|
||||||
|
|
||||||
|
type SourceType = "script" | "commonjs" | "module" | "unambiguous";
|
||||||
|
interface Options {
|
||||||
|
/**
|
||||||
|
* By default, import and export declarations can only appear at a program's top level.
|
||||||
|
* Setting this option to true allows them anywhere where a statement is allowed.
|
||||||
|
*/
|
||||||
|
allowImportExportEverywhere?: boolean;
|
||||||
|
/**
|
||||||
|
* By default, await use is not allowed outside of an async function.
|
||||||
|
* Set this to true to accept such code.
|
||||||
|
*/
|
||||||
|
allowAwaitOutsideFunction?: boolean;
|
||||||
|
/**
|
||||||
|
* By default, a return statement at the top level raises an error.
|
||||||
|
* Set this to true to accept such code.
|
||||||
|
*/
|
||||||
|
allowReturnOutsideFunction?: boolean;
|
||||||
|
/**
|
||||||
|
* By default, new.target use is not allowed outside of a function or class.
|
||||||
|
* Set this to true to accept such code.
|
||||||
|
*/
|
||||||
|
allowNewTargetOutsideFunction?: boolean;
|
||||||
|
/**
|
||||||
|
* By default, super calls are not allowed outside of a method.
|
||||||
|
* Set this to true to accept such code.
|
||||||
|
*/
|
||||||
|
allowSuperOutsideMethod?: boolean;
|
||||||
|
/**
|
||||||
|
* By default, exported identifiers must refer to a declared variable.
|
||||||
|
* Set this to true to allow export statements to reference undeclared variables.
|
||||||
|
*/
|
||||||
|
allowUndeclaredExports?: boolean;
|
||||||
|
/**
|
||||||
|
* By default, yield use is not allowed outside of a generator function.
|
||||||
|
* Set this to true to accept such code.
|
||||||
|
*/
|
||||||
|
allowYieldOutsideFunction?: boolean;
|
||||||
|
/**
|
||||||
|
* By default, Babel parser JavaScript code according to Annex B syntax.
|
||||||
|
* Set this to `false` to disable such behavior.
|
||||||
|
*/
|
||||||
|
annexB?: boolean;
|
||||||
|
/**
|
||||||
|
* By default, Babel attaches comments to adjacent AST nodes.
|
||||||
|
* When this option is set to false, comments are not attached.
|
||||||
|
* It can provide up to 30% performance improvement when the input code has many comments.
|
||||||
|
* @babel/eslint-parser will set it for you.
|
||||||
|
* It is not recommended to use attachComment: false with Babel transform,
|
||||||
|
* as doing so removes all the comments in output code, and renders annotations such as
|
||||||
|
* /* istanbul ignore next *\/ nonfunctional.
|
||||||
|
*/
|
||||||
|
attachComment?: boolean;
|
||||||
|
/**
|
||||||
|
* By default, Babel always throws an error when it finds some invalid code.
|
||||||
|
* When this option is set to true, it will store the parsing error and
|
||||||
|
* try to continue parsing the invalid input file.
|
||||||
|
*/
|
||||||
|
errorRecovery?: boolean;
|
||||||
|
/**
|
||||||
|
* Indicate the mode the code should be parsed in.
|
||||||
|
* Can be one of "script", "commonjs", "module", or "unambiguous". Defaults to "script".
|
||||||
|
* "unambiguous" will make @babel/parser attempt to guess, based on the presence
|
||||||
|
* of ES6 import or export statements.
|
||||||
|
* Files with ES6 imports and exports are considered "module" and are otherwise "script".
|
||||||
|
*
|
||||||
|
* Use "commonjs" to parse code that is intended to be run in a CommonJS environment such as Node.js.
|
||||||
|
*/
|
||||||
|
sourceType?: SourceType;
|
||||||
|
/**
|
||||||
|
* Correlate output AST nodes with their source filename.
|
||||||
|
* Useful when generating code and source maps from the ASTs of multiple input files.
|
||||||
|
*/
|
||||||
|
sourceFilename?: string;
|
||||||
|
/**
|
||||||
|
* By default, all source indexes start from 0.
|
||||||
|
* You can provide a start index to alternatively start with.
|
||||||
|
* Useful for integration with other source tools.
|
||||||
|
*/
|
||||||
|
startIndex?: number;
|
||||||
|
/**
|
||||||
|
* By default, the first line of code parsed is treated as line 1.
|
||||||
|
* You can provide a line number to alternatively start with.
|
||||||
|
* Useful for integration with other source tools.
|
||||||
|
*/
|
||||||
|
startLine?: number;
|
||||||
|
/**
|
||||||
|
* By default, the parsed code is treated as if it starts from line 1, column 0.
|
||||||
|
* You can provide a column number to alternatively start with.
|
||||||
|
* Useful for integration with other source tools.
|
||||||
|
*/
|
||||||
|
startColumn?: number;
|
||||||
|
/**
|
||||||
|
* Array containing the plugins that you want to enable.
|
||||||
|
*/
|
||||||
|
plugins?: Plugin[];
|
||||||
|
/**
|
||||||
|
* Should the parser work in strict mode.
|
||||||
|
* Defaults to true if sourceType === 'module'. Otherwise, false.
|
||||||
|
*/
|
||||||
|
strictMode?: boolean;
|
||||||
|
/**
|
||||||
|
* Adds a ranges property to each node: [node.start, node.end]
|
||||||
|
*/
|
||||||
|
ranges?: boolean;
|
||||||
|
/**
|
||||||
|
* Adds all parsed tokens to a tokens property on the File node.
|
||||||
|
*/
|
||||||
|
tokens?: boolean;
|
||||||
|
/**
|
||||||
|
* By default, the parser adds information about parentheses by setting
|
||||||
|
* `extra.parenthesized` to `true` as needed.
|
||||||
|
* When this option is `true` the parser creates `ParenthesizedExpression`
|
||||||
|
* AST nodes instead of using the `extra` property.
|
||||||
|
*/
|
||||||
|
createParenthesizedExpressions?: boolean;
|
||||||
|
/**
|
||||||
|
* The default is false in Babel 7 and true in Babel 8
|
||||||
|
* Set this to true to parse it as an `ImportExpression` node.
|
||||||
|
* Otherwise `import(foo)` is parsed as `CallExpression(Import, [Identifier(foo)])`.
|
||||||
|
*/
|
||||||
|
createImportExpressions?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParserOptions = Partial<Options>;
|
||||||
|
type ParseError = ParseError$1<object>;
|
||||||
|
type ParseResult<Result extends File | Expression = File> = Result & {
|
||||||
|
comments: File["comments"];
|
||||||
|
errors: null | ParseError[];
|
||||||
|
tokens?: File["tokens"];
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Parse the provided code as an entire ECMAScript program.
|
||||||
|
*/
|
||||||
|
declare function parse(input: string, options?: ParserOptions): ParseResult<File>;
|
||||||
|
declare function parseExpression(input: string, options?: ParserOptions): ParseResult<Expression>;
|
||||||
|
|
||||||
|
declare const tokTypes: {
|
||||||
|
// todo(flow->ts) real token type
|
||||||
|
[name: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { DecoratorsPluginOptions, FlowPluginOptions, ParseError, ParseResult, ParserOptions, PluginConfig as ParserPlugin, ParserPluginWithOptions, PipelineOperatorPluginOptions, RecordAndTuplePluginOptions, TypeScriptPluginOptions, parse, parseExpression, tokTypes };
|
||||||
1
frontend/node_modules/.pnpm/@babel+parser@7.29.2/node_modules/@babel/types
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../@babel+types@7.29.0/node_modules/@babel/types
|
||||||
1
frontend/node_modules/.pnpm/@babel+types@7.29.0/node_modules/@babel/helper-string-parser
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../@babel+helper-string-parser@7.27.1/node_modules/@babel/helper-string-parser
|
||||||
1
frontend/node_modules/.pnpm/@babel+types@7.29.0/node_modules/@babel/helper-validator-identifier
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../@babel+helper-validator-identifier@7.28.5/node_modules/@babel/helper-validator-identifier
|
||||||
22
frontend/node_modules/.pnpm/@babel+types@7.29.0/node_modules/@babel/types/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
||||||
|
|
||||||
|
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.
|
||||||
19
frontend/node_modules/.pnpm/@babel+types@7.29.0/node_modules/@babel/types/README.md
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# @babel/types
|
||||||
|
|
||||||
|
> Babel Types is a Lodash-esque utility library for AST nodes
|
||||||
|
|
||||||
|
See our website [@babel/types](https://babeljs.io/docs/babel-types) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20types%22+is%3Aopen) associated with this package.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Using npm:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install --save-dev @babel/types
|
||||||
|
```
|
||||||
|
|
||||||
|
or using yarn:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn add @babel/types --dev
|
||||||
|
```
|
||||||
16
frontend/node_modules/.pnpm/@babel+types@7.29.0/node_modules/@babel/types/lib/asserts/assertNode.js
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.default = assertNode;
|
||||||
|
var _isNode = require("../validators/isNode.js");
|
||||||
|
function assertNode(node) {
|
||||||
|
if (!(0, _isNode.default)(node)) {
|
||||||
|
var _node$type;
|
||||||
|
const type = (_node$type = node == null ? void 0 : node.type) != null ? _node$type : JSON.stringify(node);
|
||||||
|
throw new TypeError(`Not a valid node of type "${type}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//# sourceMappingURL=assertNode.js.map
|
||||||
1
frontend/node_modules/.pnpm/@babel+types@7.29.0/node_modules/@babel/types/lib/asserts/assertNode.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"names":["_isNode","require","assertNode","node","isNode","_node$type","type","JSON","stringify","TypeError"],"sources":["../../src/asserts/assertNode.ts"],"sourcesContent":["import isNode from \"../validators/isNode.ts\";\nimport type * as t from \"../index.ts\";\n\nexport default function assertNode(node?: any): asserts node is t.Node {\n if (!isNode(node)) {\n const type = node?.type ?? JSON.stringify(node);\n throw new TypeError(`Not a valid node of type \"${type}\"`);\n }\n}\n"],"mappings":";;;;;;AAAA,IAAAA,OAAA,GAAAC,OAAA;AAGe,SAASC,UAAUA,CAACC,IAAU,EAA0B;EACrE,IAAI,CAAC,IAAAC,eAAM,EAACD,IAAI,CAAC,EAAE;IAAA,IAAAE,UAAA;IACjB,MAAMC,IAAI,IAAAD,UAAA,GAAGF,IAAI,oBAAJA,IAAI,CAAEG,IAAI,YAAAD,UAAA,GAAIE,IAAI,CAACC,SAAS,CAACL,IAAI,CAAC;IAC/C,MAAM,IAAIM,SAAS,CAAC,6BAA6BH,IAAI,GAAG,CAAC;EAC3D;AACF","ignoreList":[]}
|
||||||
1251
frontend/node_modules/.pnpm/@babel+types@7.29.0/node_modules/@babel/types/lib/asserts/generated/index.js
generated
vendored
Normal file
1
frontend/node_modules/.pnpm/@babel+types@7.29.0/node_modules/@babel/types/lib/asserts/generated/index.js.map
generated
vendored
Normal file
3
frontend/node_modules/.pnpm/@babel+types@7.29.0/node_modules/@babel/types/lib/ast-types/generated/index.js
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||