Refactor Docker setup and enhance development environment
- Updated docker-compose.dev.yml to use Dockerfile.dev for backend builds and added HOST environment variable for frontend. - Introduced Dockerfile.dev for streamlined backend development with Go. - Enhanced development documentation to reflect changes in local setup and API proxying. - Removed outdated frontend Dockerfile and adjusted frontend configuration for improved development experience.
This commit is contained in:
13
Dockerfile.dev
Normal file
13
Dockerfile.dev
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM golang:1.21-alpine
|
||||||
|
|
||||||
|
WORKDIR /hnh-map
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN go build -o hnh-map ./cmd/hnh-map
|
||||||
|
|
||||||
|
EXPOSE 3080
|
||||||
|
|
||||||
|
CMD ["/hnh-map/hnh-map", "-grids=/map"]
|
||||||
@@ -1,30 +1,31 @@
|
|||||||
# Development: backend (Go) on 8080, frontend (Nuxt dev) on 3000 with proxy to backend.
|
# Development: backend API on 3080 + frontend Nuxt dev server on 3000.
|
||||||
# Open http://localhost:3000/map/ — /map/api, /map/updates, /map/grids are proxied to backend.
|
# Open http://localhost:3000/ for app development with live-reload.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile.dev
|
||||||
ports:
|
ports:
|
||||||
- "3080:3080"
|
- "3080:3080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./grids:/map
|
- ./grids:/map
|
||||||
environment:
|
environment:
|
||||||
- HNHMAP_PORT=3080
|
- HNHMAP_PORT=3080
|
||||||
- HNHMAP_BOOTSTRAP_PASSWORD=admin
|
- HNHMAP_BOOTSTRAP_PASSWORD=admin
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: node:20-alpine
|
image: node:20-alpine
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
command: sh -c "npm ci && npm run dev"
|
command: sh -c "npm ci && npm run dev"
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend-nuxt:/app
|
- ./frontend-nuxt:/app
|
||||||
# Prevent overwriting node_modules from host
|
# Prevent overwriting node_modules from host
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
environment:
|
environment:
|
||||||
- NUXT_PUBLIC_API_BASE=/map/api
|
- NUXT_PUBLIC_API_BASE=/map/api
|
||||||
depends_on:
|
- HOST=0.0.0.0
|
||||||
- backend
|
depends_on:
|
||||||
|
- backend
|
||||||
|
|||||||
@@ -1,50 +1,53 @@
|
|||||||
# Разработка
|
# Разработка
|
||||||
|
|
||||||
## Локальный запуск
|
## Локальный запуск
|
||||||
|
|
||||||
### Бэкенд (Go)
|
### Бэкенд (Go)
|
||||||
|
|
||||||
Из корня репозитория:
|
Из корня репозитория:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build -o hnh-map ./cmd/hnh-map
|
go build -o hnh-map ./cmd/hnh-map
|
||||||
./hnh-map -grids=./grids -port=8080
|
./hnh-map -grids=./grids -port=8080
|
||||||
```
|
```
|
||||||
|
|
||||||
Или без сборки:
|
Или без сборки:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go run ./cmd/hnh-map -grids=./grids -port=8080
|
go run ./cmd/hnh-map -grids=./grids -port=8080
|
||||||
```
|
```
|
||||||
|
|
||||||
Сервер будет отдавать статику из каталога `frontend/` (нужно предварительно собрать фронт, см. ниже).
|
Сервер будет отдавать статику из каталога `frontend/` (нужно предварительно собрать фронт, см. ниже).
|
||||||
|
|
||||||
### Фронтенд (Nuxt)
|
### Фронтенд (Nuxt)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend-nuxt
|
cd frontend-nuxt
|
||||||
npm install
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
В dev-режиме приложение доступно по корню (например `http://localhost:3000/`). Бэкенд должен быть доступен; при необходимости настройте прокси в `nuxt.config.ts` (например на `http://localhost:8080`).
|
В dev-режиме приложение доступно по корню (например `http://localhost:3000/`). Бэкенд должен быть доступен; при необходимости настройте прокси в `nuxt.config.ts` (например на `http://localhost:8080`).
|
||||||
|
|
||||||
### Docker Compose (разработка)
|
### Docker Compose (разработка)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.dev.yml up
|
docker compose -f docker-compose.dev.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
- Фронт: порт **3000** (Nuxt dev-сервер).
|
Dev-композ поднимает два сервиса:
|
||||||
- Бэкенд: порт **3080** (чтобы не конфликтовать с другими сервисами на 8080).
|
|
||||||
|
- `backend` — Go API на порту `3080` (без сборки/раздачи фронтенд-статики в dev-режиме).
|
||||||
Откройте http://localhost:3000/. Запросы к `/map/api`, `/map/updates`, `/map/grids` проксируются на бэкенд (host `backend`, порт 3080).
|
- `frontend` — Nuxt dev-сервер на порту `3000` с live-reload; запросы к `/map/api`, `/map/updates`, `/map/grids` проксируются на бэкенд.
|
||||||
|
|
||||||
### Сборка образа и prod-композ
|
Используйте [http://localhost:3000/](http://localhost:3000/) как основной URL для разработки интерфейса.
|
||||||
|
Порт `3080` предназначен для API и backend-эндпоинтов; корень `/` может возвращать `404` в dev-режиме — это ожидаемо.
|
||||||
```bash
|
|
||||||
docker build -t hnh-map .
|
### Сборка образа и prod-композ
|
||||||
docker compose -f docker-compose.prod.yml up -d
|
|
||||||
```
|
```bash
|
||||||
|
docker build -t hnh-map .
|
||||||
В prod фронт собран в образ и отдаётся бэкендом из каталога `frontend/`; порт 8080.
|
docker compose -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
В prod фронт собран в образ и отдаётся бэкендом из каталога `frontend/`; порт 8080.
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# Use Node 20+ for build (required by Tailwind/PostCSS toolchain)
|
|
||||||
FROM node:20-alpine AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package.json package-lock.json* ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN npm run generate
|
|
||||||
|
|
||||||
# Output: .output/public is the static site root (for Go http.Dir("frontend"))
|
|
||||||
FROM alpine:3.19
|
|
||||||
RUN apk add --no-cache bash
|
|
||||||
COPY --from=builder /app/.output/public /frontend
|
|
||||||
# Optional: when integrating with main Dockerfile, copy /frontend into the image
|
|
||||||
@@ -1,66 +1,66 @@
|
|||||||
# HnH Map – Nuxt 3 frontend
|
# HnH Map – Nuxt 3 frontend
|
||||||
|
|
||||||
Nuxt 3 + Tailwind + DaisyUI frontend for the HnH map. Served by the Go backend under `/map/`.
|
Nuxt 3 + Tailwind + DaisyUI frontend for the HnH map. Served by the Go backend under `/map/`.
|
||||||
|
|
||||||
In dev mode the app is available at the path with baseURL **`/map/`** (e.g. `http://localhost:3000/map/`). The Go backend must be reachable (directly or via the dev proxy in `nuxt.config.ts`).
|
In dev mode the app is available at the path with baseURL **`/map/`** (e.g. `http://localhost:3000/map/`). The Go backend must be reachable (directly or via the dev proxy in `nuxt.config.ts`).
|
||||||
|
|
||||||
## Project structure
|
## Project structure
|
||||||
|
|
||||||
- **pages/** — route pages (e.g. map view, profile, login)
|
- **pages/** — route pages (e.g. map view, profile, login)
|
||||||
- **components/** — Vue components
|
- **components/** — Vue components
|
||||||
- **composables/** — shared composition functions
|
- **composables/** — shared composition functions
|
||||||
- **layouts/** — layout components
|
- **layouts/** — layout components
|
||||||
- **server/** — Nitro server (if used)
|
- **server/** — Nitro server (if used)
|
||||||
- **plugins/** — Nuxt plugins
|
- **plugins/** — Nuxt plugins
|
||||||
- **public/gfx/** — static assets (sprites, terrain, etc.)
|
- **public/gfx/** — static assets (sprites, terrain, etc.)
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- **Node.js 20+** (required for build; `engines` in package.json). Use `nvm use` if you have `.nvmrc`, or build via Docker (see below).
|
- **Node.js 20+** (required for build; `engines` in package.json). Use `nvm use` if you have `.nvmrc`, or build via Docker (see below).
|
||||||
- npm
|
- npm
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Then open the app at the path shown (e.g. `http://localhost:3000/map/`). Ensure the Go backend is running and proxying or serving this app if needed.
|
Then open the app at the path shown (e.g. `http://localhost:3000/map/`). Ensure the Go backend is running and proxying or serving this app if needed.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
Static export (for Go `http.Dir`):
|
Static export (for Go `http.Dir`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run generate
|
npm run generate
|
||||||
```
|
```
|
||||||
|
|
||||||
Output is in `.output/public`. To serve from the existing `frontend` directory, copy contents to `../frontend` after generate, or set `nitro.output.dir` in `nuxt.config.ts` and build from the repo root.
|
Output is in `.output/public`. To serve from the existing `frontend` directory, copy contents to `../frontend` after generate, or set `nitro.output.dir` in `nuxt.config.ts` and build from the repo root.
|
||||||
|
|
||||||
## Build with Docker (Node 20)
|
## Build with Docker (Node 20)
|
||||||
|
|
||||||
Build requires Node 20+. If your host has an older version (e.g. Node 18), build the frontend in Docker:
|
Build requires Node 20+. If your host has an older version (e.g. Node 18), build the frontend in Docker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t frontend-nuxt .
|
docker build -t frontend-nuxt .
|
||||||
docker create --name fn frontend-nuxt
|
docker create --name fn frontend-nuxt
|
||||||
docker cp fn:/frontend ./output-public
|
docker cp fn:/frontend ./output-public
|
||||||
docker rm fn
|
docker rm fn
|
||||||
# Copy output-public/* into repo frontend/ and run Go server
|
# Copy output-public/* into repo frontend/ and run Go server
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cutover from Vue 2 frontend
|
## Cutover from Vue 2 frontend
|
||||||
|
|
||||||
1. Build this app (`npm run generate`).
|
1. Build this app (`npm run generate`).
|
||||||
2. Copy `.output/public/*` into the repo’s `frontend` directory (or point Go at the Nuxt output directory).
|
2. Copy `.output/public/*` into the repo’s `frontend` directory (or point Go at the Nuxt output directory).
|
||||||
3. Restart the Go server. The same `/map/` routes and API remain.
|
3. Restart the Go server. The same `/map/` routes and API remain.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,63 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Context menu (tile) -->
|
<!-- Context menu (tile) — Teleport so it is not clipped by map/overflow -->
|
||||||
<div
|
<Teleport to="body">
|
||||||
v-show="contextMenu.tile.show"
|
<div
|
||||||
class="fixed z-[1000] bg-base-100/95 backdrop-blur-xl shadow-xl rounded-lg border border-base-300 py-1 min-w-[180px] transition-opacity duration-150"
|
v-show="contextMenu.tile.show"
|
||||||
:style="{ left: contextMenu.tile.x + 'px', top: contextMenu.tile.y + 'px' }"
|
class="fixed z-[9999] bg-base-100/95 backdrop-blur-xl shadow-xl rounded-lg border border-base-300 py-1 min-w-[180px] transition-opacity duration-150"
|
||||||
>
|
:style="{ left: contextMenu.tile.x + 'px', top: contextMenu.tile.y + 'px' }"
|
||||||
<button
|
>
|
||||||
type="button"
|
<button
|
||||||
class="btn btn-ghost btn-sm w-full justify-start hover:bg-base-200 transition-colors"
|
type="button"
|
||||||
@click="onWipeTile(contextMenu.tile.data?.coords)"
|
class="btn btn-ghost btn-sm w-full justify-start hover:bg-base-200 transition-colors"
|
||||||
>
|
@click="onWipeTile(contextMenu.tile.data?.coords)"
|
||||||
Wipe tile {{ contextMenu.tile.data?.coords?.x }}, {{ contextMenu.tile.data?.coords?.y }}
|
>
|
||||||
</button>
|
Wipe tile {{ contextMenu.tile.data?.coords?.x }}, {{ contextMenu.tile.data?.coords?.y }}
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<button
|
||||||
class="btn btn-ghost btn-sm w-full justify-start hover:bg-base-200 transition-colors"
|
type="button"
|
||||||
@click="onRewriteCoords(contextMenu.tile.data?.coords)"
|
class="btn btn-ghost btn-sm w-full justify-start hover:bg-base-200 transition-colors"
|
||||||
>
|
@click="onRewriteCoords(contextMenu.tile.data?.coords)"
|
||||||
Rewrite tile coords
|
>
|
||||||
</button>
|
Rewrite tile coords
|
||||||
</div>
|
</button>
|
||||||
<!-- Context menu (marker) -->
|
</div>
|
||||||
<div
|
<!-- Context menu (marker) -->
|
||||||
v-show="contextMenu.marker.show"
|
<div
|
||||||
class="fixed z-[1000] bg-base-100/95 backdrop-blur-xl shadow-xl rounded-lg border border-base-300 py-1 min-w-[180px] transition-opacity duration-150"
|
v-show="contextMenu.marker.show"
|
||||||
:style="{ left: contextMenu.marker.x + 'px', top: contextMenu.marker.y + 'px' }"
|
class="fixed z-[9999] bg-base-100/95 backdrop-blur-xl shadow-xl rounded-lg border border-base-300 py-1 min-w-[180px] transition-opacity duration-150"
|
||||||
>
|
:style="{ left: contextMenu.marker.x + 'px', top: contextMenu.marker.y + 'px' }"
|
||||||
<button
|
>
|
||||||
type="button"
|
<button
|
||||||
class="btn btn-ghost btn-sm w-full justify-start hover:bg-base-200 transition-colors"
|
type="button"
|
||||||
@click="onHideMarker(contextMenu.marker.data?.id)"
|
class="btn btn-ghost btn-sm w-full justify-start hover:bg-base-200 transition-colors"
|
||||||
>
|
@click="onHideMarker(contextMenu.marker.data?.id)"
|
||||||
Hide marker {{ contextMenu.marker.data?.name }}
|
>
|
||||||
</button>
|
Hide marker {{ contextMenu.marker.data?.name }}
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</div>
|
||||||
|
</Teleport>
|
||||||
<script setup lang="ts">
|
</template>
|
||||||
import type { ContextMenuState } from '~/composables/useMapLogic'
|
|
||||||
|
<script setup lang="ts">
|
||||||
defineProps<{
|
import type { ContextMenuState } from '~/composables/useMapLogic'
|
||||||
contextMenu: ContextMenuState
|
|
||||||
}>()
|
defineProps<{
|
||||||
|
contextMenu: ContextMenuState
|
||||||
const emit = defineEmits<{
|
}>()
|
||||||
wipeTile: [coords: { x: number; y: number } | undefined]
|
|
||||||
rewriteCoords: [coords: { x: number; y: number } | undefined]
|
const emit = defineEmits<{
|
||||||
hideMarker: [id: number | undefined]
|
wipeTile: [coords: { x: number; y: number } | undefined]
|
||||||
}>()
|
rewriteCoords: [coords: { x: number; y: number } | undefined]
|
||||||
|
hideMarker: [id: number | undefined]
|
||||||
function onWipeTile(coords: { x: number; y: number } | undefined) {
|
}>()
|
||||||
if (coords) emit('wipeTile', coords)
|
|
||||||
}
|
function onWipeTile(coords: { x: number; y: number } | undefined) {
|
||||||
|
if (coords) emit('wipeTile', coords)
|
||||||
function onRewriteCoords(coords: { x: number; y: number } | undefined) {
|
}
|
||||||
if (coords) emit('rewriteCoords', coords)
|
|
||||||
}
|
function onRewriteCoords(coords: { x: number; y: number } | undefined) {
|
||||||
|
if (coords) emit('rewriteCoords', coords)
|
||||||
function onHideMarker(id: number | undefined) {
|
}
|
||||||
if (id != null) emit('hideMarker', id)
|
|
||||||
}
|
function onHideMarker(id: number | undefined) {
|
||||||
</script>
|
if (id != null) emit('hideMarker', id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export function useMapLogic() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openTileContextMenu(clientX: number, clientY: number, coords: { x: number; y: number }) {
|
function openTileContextMenu(clientX: number, clientY: number, coords: { x: number; y: number }) {
|
||||||
|
closeContextMenus()
|
||||||
contextMenu.tile.show = true
|
contextMenu.tile.show = true
|
||||||
contextMenu.tile.x = clientX
|
contextMenu.tile.x = clientX
|
||||||
contextMenu.tile.y = clientY
|
contextMenu.tile.y = clientY
|
||||||
@@ -108,6 +109,7 @@ export function useMapLogic() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openMarkerContextMenu(clientX: number, clientY: number, id: number, name: string) {
|
function openMarkerContextMenu(clientX: number, clientY: number, id: number, name: string) {
|
||||||
|
closeContextMenus()
|
||||||
contextMenu.marker.show = true
|
contextMenu.marker.show = true
|
||||||
contextMenu.marker.x = clientX
|
contextMenu.marker.x = clientX
|
||||||
contextMenu.marker.y = clientY
|
contextMenu.marker.y = clientY
|
||||||
|
|||||||
@@ -1,128 +1,128 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-screen flex flex-col bg-base-100 overflow-hidden">
|
<div class="h-screen flex flex-col bg-base-100 overflow-hidden">
|
||||||
<header class="navbar bg-base-100/80 backdrop-blur-xl border-b border-base-300/50 px-4 gap-2 shrink-0">
|
<header class="navbar bg-base-100/80 backdrop-blur-xl border-b border-base-300/50 px-4 gap-2 shrink-0">
|
||||||
<NuxtLink to="/" class="text-lg font-semibold hover:opacity-80 transition-all duration-200">{{ title }}</NuxtLink>
|
<NuxtLink to="/" class="text-lg font-semibold hover:opacity-80 transition-all duration-200">{{ title }}</NuxtLink>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="!isLogin"
|
v-if="!isLogin"
|
||||||
to="/"
|
to="/"
|
||||||
class="btn btn-ghost btn-sm gap-1.5 transition-all duration-200 hover:scale-105"
|
class="btn btn-ghost btn-sm gap-1.5 transition-all duration-200 hover:scale-105"
|
||||||
:class="route.path === '/' ? 'btn-primary' : ''"
|
:class="route.path === '/' ? 'btn-primary' : ''"
|
||||||
>
|
>
|
||||||
<icons-icon-map />
|
<icons-icon-map />
|
||||||
Map
|
Map
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="!isLogin"
|
v-if="!isLogin"
|
||||||
to="/profile"
|
to="/profile"
|
||||||
class="btn btn-ghost btn-sm gap-1.5 transition-all duration-200 hover:scale-105"
|
class="btn btn-ghost btn-sm gap-1.5 transition-all duration-200 hover:scale-105"
|
||||||
:class="route.path === '/profile' ? 'btn-primary' : ''"
|
:class="route.path === '/profile' ? 'btn-primary' : ''"
|
||||||
>
|
>
|
||||||
<icons-icon-user />
|
<icons-icon-user />
|
||||||
Profile
|
Profile
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="!isLogin && isAdmin"
|
v-if="!isLogin && isAdmin"
|
||||||
to="/admin"
|
to="/admin"
|
||||||
class="btn btn-ghost btn-sm gap-1.5 transition-all duration-200 hover:scale-105"
|
class="btn btn-ghost btn-sm gap-1.5 transition-all duration-200 hover:scale-105"
|
||||||
:class="route.path.startsWith('/admin') ? 'btn-primary' : ''"
|
:class="route.path.startsWith('/admin') ? 'btn-primary' : ''"
|
||||||
>
|
>
|
||||||
<icons-icon-shield />
|
<icons-icon-shield />
|
||||||
Admin
|
Admin
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<button
|
<button
|
||||||
v-if="!isLogin && me"
|
v-if="!isLogin && me"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost btn-sm btn-error btn-outline gap-1.5 transition-all duration-200 hover:scale-105"
|
class="btn btn-ghost btn-sm btn-error btn-outline gap-1.5 transition-all duration-200 hover:scale-105"
|
||||||
@click="doLogout"
|
@click="doLogout"
|
||||||
>
|
>
|
||||||
<icons-icon-logout />
|
<icons-icon-logout />
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
<label class="swap swap-rotate btn btn-ghost btn-sm transition-all duration-200 hover:scale-105">
|
<label class="swap swap-rotate btn btn-ghost btn-sm transition-all duration-200 hover:scale-105">
|
||||||
<input type="checkbox" v-model="dark" @change="toggleTheme" />
|
<input type="checkbox" v-model="dark" @change="toggleTheme" />
|
||||||
<span class="swap-off"><icons-icon-sun /></span>
|
<span class="swap-off"><icons-icon-sun /></span>
|
||||||
<span class="swap-on"><icons-icon-moon /></span>
|
<span class="swap-on"><icons-icon-moon /></span>
|
||||||
</label>
|
</label>
|
||||||
<span v-if="live" class="badge badge-success badge-sm">Live</span>
|
<span v-if="live" class="badge badge-success badge-sm">Live</span>
|
||||||
</header>
|
</header>
|
||||||
<main class="flex-1 min-h-0 overflow-y-auto relative">
|
<main class="flex-1 min-h-0 overflow-y-auto relative">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const THEME_KEY = 'hnh-map-theme'
|
const THEME_KEY = 'hnh-map-theme'
|
||||||
|
|
||||||
function getInitialDark(): boolean {
|
function getInitialDark(): boolean {
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
const stored = localStorage.getItem(THEME_KEY)
|
const stored = localStorage.getItem(THEME_KEY)
|
||||||
if (stored === 'dark') return true
|
if (stored === 'dark') return true
|
||||||
if (stored === 'light') return false
|
if (stored === 'light') return false
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = ref('HnH Map')
|
const title = ref('HnH Map')
|
||||||
const dark = ref(false)
|
const dark = ref(false)
|
||||||
const live = ref(false)
|
const live = ref(false)
|
||||||
const me = ref<{ username?: string; auths?: string[] } | null>(null)
|
const me = ref<{ username?: string; auths?: string[] } | null>(null)
|
||||||
|
|
||||||
const { isLoginPath } = useAppPaths()
|
const { isLoginPath } = useAppPaths()
|
||||||
const isLogin = computed(() => isLoginPath(route.path))
|
const isLogin = computed(() => isLoginPath(route.path))
|
||||||
const isAdmin = computed(() => !!me.value?.auths?.includes('admin'))
|
const isAdmin = computed(() => !!me.value?.auths?.includes('admin'))
|
||||||
|
|
||||||
async function loadMe() {
|
async function loadMe() {
|
||||||
if (isLogin.value) return
|
if (isLogin.value) return
|
||||||
try {
|
try {
|
||||||
me.value = await useMapApi().me()
|
me.value = await useMapApi().me()
|
||||||
} catch {
|
} catch {
|
||||||
me.value = null
|
me.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
if (isLogin.value) return
|
if (isLogin.value) return
|
||||||
try {
|
try {
|
||||||
const config = await useMapApi().getConfig()
|
const config = await useMapApi().getConfig()
|
||||||
if (config?.title) title.value = config.title
|
if (config?.title) title.value = config.title
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
dark.value = getInitialDark()
|
dark.value = getInitialDark()
|
||||||
const html = document.documentElement
|
const html = document.documentElement
|
||||||
html.setAttribute('data-theme', dark.value ? 'dark' : 'light')
|
html.setAttribute('data-theme', dark.value ? 'dark' : 'light')
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.path,
|
() => route.path,
|
||||||
(path) => {
|
(path) => {
|
||||||
if (!isLoginPath(path)) loadMe().then(loadConfig)
|
if (!isLoginPath(path)) loadMe().then(loadConfig)
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
const html = document.documentElement
|
const html = document.documentElement
|
||||||
if (dark.value) {
|
if (dark.value) {
|
||||||
html.setAttribute('data-theme', 'dark')
|
html.setAttribute('data-theme', 'dark')
|
||||||
localStorage.setItem(THEME_KEY, 'dark')
|
localStorage.setItem(THEME_KEY, 'dark')
|
||||||
} else {
|
} else {
|
||||||
html.setAttribute('data-theme', 'light')
|
html.setAttribute('data-theme', 'light')
|
||||||
localStorage.setItem(THEME_KEY, 'light')
|
localStorage.setItem(THEME_KEY, 'light')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doLogout() {
|
async function doLogout() {
|
||||||
await useMapApi().logout()
|
await useMapApi().logout()
|
||||||
await router.push('/login')
|
await router.push('/login')
|
||||||
me.value = null
|
me.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ setLive: (v: boolean) => { live.value = v } })
|
defineExpose({ setLive: (v: boolean) => { live.value = v } })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,54 +1,54 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
import { viteUriGuard } from './vite/vite-uri-guard'
|
import { viteUriGuard } from './vite/vite-uri-guard'
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2024-11-01',
|
compatibilityDate: '2024-11-01',
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
baseURL: '/',
|
baseURL: '/',
|
||||||
pageTransition: { name: 'page', mode: 'out-in' },
|
pageTransition: { name: 'page', mode: 'out-in' },
|
||||||
head: {
|
head: {
|
||||||
title: 'HnH Map',
|
title: 'HnH Map',
|
||||||
meta: [{ charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }],
|
meta: [{ charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }],
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
||||||
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },
|
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },
|
||||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap' },
|
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
apiBase: '/map/api',
|
apiBase: '/map/api',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
modules: ['@nuxtjs/tailwindcss'],
|
modules: ['@nuxtjs/tailwindcss'],
|
||||||
tailwindcss: {
|
tailwindcss: {
|
||||||
cssPath: '~/assets/css/app.css',
|
cssPath: '~/assets/css/app.css',
|
||||||
},
|
},
|
||||||
css: ['~/assets/css/app.css', 'leaflet/dist/leaflet.css', '~/assets/css/leaflet-overrides.css'],
|
css: ['~/assets/css/app.css', 'leaflet/dist/leaflet.css', '~/assets/css/leaflet-overrides.css'],
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [viteUriGuard()],
|
plugins: [viteUriGuard()],
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: ['leaflet'],
|
include: ['leaflet'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Dev: proxy /map API, SSE and grids to Go backend (e.g. docker compose -f docker-compose.dev.yml)
|
// Dev: proxy /map API, SSE and grids to Go backend (e.g. docker compose -f docker-compose.dev.yml)
|
||||||
nitro: {
|
nitro: {
|
||||||
devProxy: {
|
devProxy: {
|
||||||
'/map/api': { target: 'http://backend:3080', changeOrigin: true },
|
'/map/api': { target: 'http://backend:3080/map/api', changeOrigin: true },
|
||||||
'/map/updates': { target: 'http://backend:3080', changeOrigin: true },
|
'/map/updates': { target: 'http://backend:3080/map/updates', changeOrigin: true },
|
||||||
'/map/grids': { target: 'http://backend:3080', changeOrigin: true },
|
'/map/grids': { target: 'http://backend:3080/map/grids', changeOrigin: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// For cutover: set nitro.preset to 'static' and optionally copy .output/public to ../frontend
|
// For cutover: set nitro.preset to 'static' and optionally copy .output/public to ../frontend
|
||||||
// nitro: { output: { dir: '../frontend' } },
|
// nitro: { output: { dir: '../frontend' } },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.4.0/dist/leaflet.css"
|
|
||||||
integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA=="
|
|
||||||
crossorigin=""/>
|
|
||||||
<title></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
<strong>We're sorry but map-frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,261 +1,261 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.etcd.io/bbolt"
|
"go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// App is the main application (map server) state.
|
// App is the main application (map server) state.
|
||||||
type App struct {
|
type App struct {
|
||||||
gridStorage string
|
gridStorage string
|
||||||
frontendRoot string
|
frontendRoot string
|
||||||
db *bbolt.DB
|
db *bbolt.DB
|
||||||
|
|
||||||
characters map[string]Character
|
characters map[string]Character
|
||||||
chmu sync.RWMutex
|
chmu sync.RWMutex
|
||||||
|
|
||||||
gridUpdates Topic[TileData]
|
gridUpdates Topic[TileData]
|
||||||
mergeUpdates Topic[Merge]
|
mergeUpdates Topic[Merge]
|
||||||
}
|
}
|
||||||
|
|
||||||
// GridStorage returns the grid storage path.
|
// GridStorage returns the grid storage path.
|
||||||
func (a *App) GridStorage() string {
|
func (a *App) GridStorage() string {
|
||||||
return a.gridStorage
|
return a.gridStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
// GridUpdates returns the tile updates topic for MapService.
|
// GridUpdates returns the tile updates topic for MapService.
|
||||||
func (a *App) GridUpdates() *Topic[TileData] {
|
func (a *App) GridUpdates() *Topic[TileData] {
|
||||||
return &a.gridUpdates
|
return &a.gridUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeUpdates returns the merge updates topic for MapService.
|
// MergeUpdates returns the merge updates topic for MapService.
|
||||||
func (a *App) MergeUpdates() *Topic[Merge] {
|
func (a *App) MergeUpdates() *Topic[Merge] {
|
||||||
return &a.mergeUpdates
|
return &a.mergeUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCharacters returns a copy of all characters (for MapService).
|
// GetCharacters returns a copy of all characters (for MapService).
|
||||||
func (a *App) GetCharacters() []Character {
|
func (a *App) GetCharacters() []Character {
|
||||||
a.chmu.RLock()
|
a.chmu.RLock()
|
||||||
defer a.chmu.RUnlock()
|
defer a.chmu.RUnlock()
|
||||||
chars := make([]Character, 0, len(a.characters))
|
chars := make([]Character, 0, len(a.characters))
|
||||||
for _, v := range a.characters {
|
for _, v := range a.characters {
|
||||||
chars = append(chars, v)
|
chars = append(chars, v)
|
||||||
}
|
}
|
||||||
return chars
|
return chars
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp creates an App with the given storage paths and database.
|
// NewApp creates an App with the given storage paths and database.
|
||||||
// frontendRoot is the directory for the map SPA (e.g. "frontend").
|
// frontendRoot is the directory for the map SPA (e.g. "frontend").
|
||||||
func NewApp(gridStorage, frontendRoot string, db *bbolt.DB) (*App, error) {
|
func NewApp(gridStorage, frontendRoot string, db *bbolt.DB) (*App, error) {
|
||||||
return &App{
|
return &App{
|
||||||
gridStorage: gridStorage,
|
gridStorage: gridStorage,
|
||||||
frontendRoot: frontendRoot,
|
frontendRoot: frontendRoot,
|
||||||
db: db,
|
db: db,
|
||||||
characters: make(map[string]Character),
|
characters: make(map[string]Character),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
ID string
|
ID string
|
||||||
Username string
|
Username string
|
||||||
Auths Auths `json:"-"`
|
Auths Auths `json:"-"`
|
||||||
TempAdmin bool
|
TempAdmin bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Character struct {
|
type Character struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Map int `json:"map"`
|
Map int `json:"map"`
|
||||||
Position Position `json:"position"`
|
Position Position `json:"position"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
updated time.Time
|
updated time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type Marker struct {
|
type Marker struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
GridID string `json:"gridID"`
|
GridID string `json:"gridID"`
|
||||||
Position Position `json:"position"`
|
Position Position `json:"position"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
Hidden bool `json:"hidden"`
|
Hidden bool `json:"hidden"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FrontendMarker struct {
|
type FrontendMarker struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Map int `json:"map"`
|
Map int `json:"map"`
|
||||||
Position Position `json:"position"`
|
Position Position `json:"position"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
Hidden bool `json:"hidden"`
|
Hidden bool `json:"hidden"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MapInfo struct {
|
type MapInfo struct {
|
||||||
ID int
|
ID int
|
||||||
Name string
|
Name string
|
||||||
Hidden bool
|
Hidden bool
|
||||||
Priority bool
|
Priority bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type GridData struct {
|
type GridData struct {
|
||||||
ID string
|
ID string
|
||||||
Coord Coord
|
Coord Coord
|
||||||
NextUpdate time.Time
|
NextUpdate time.Time
|
||||||
Map int
|
Map int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Coord struct {
|
type Coord struct {
|
||||||
X int `json:"x"`
|
X int `json:"x"`
|
||||||
Y int `json:"y"`
|
Y int `json:"y"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Position struct {
|
type Position struct {
|
||||||
X int `json:"x"`
|
X int `json:"x"`
|
||||||
Y int `json:"y"`
|
Y int `json:"y"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Coord) Name() string {
|
func (c Coord) Name() string {
|
||||||
return fmt.Sprintf("%d_%d", c.X, c.Y)
|
return fmt.Sprintf("%d_%d", c.X, c.Y)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Coord) Parent() Coord {
|
func (c Coord) Parent() Coord {
|
||||||
if c.X < 0 {
|
if c.X < 0 {
|
||||||
c.X--
|
c.X--
|
||||||
}
|
}
|
||||||
if c.Y < 0 {
|
if c.Y < 0 {
|
||||||
c.Y--
|
c.Y--
|
||||||
}
|
}
|
||||||
return Coord{
|
return Coord{
|
||||||
X: c.X / 2,
|
X: c.X / 2,
|
||||||
Y: c.Y / 2,
|
Y: c.Y / 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Auths []string
|
type Auths []string
|
||||||
|
|
||||||
func (a Auths) Has(auth string) bool {
|
func (a Auths) Has(auth string) bool {
|
||||||
for _, v := range a {
|
for _, v := range a {
|
||||||
if v == auth {
|
if v == auth {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AUTH_ADMIN = "admin"
|
AUTH_ADMIN = "admin"
|
||||||
AUTH_MAP = "map"
|
AUTH_MAP = "map"
|
||||||
AUTH_MARKERS = "markers"
|
AUTH_MARKERS = "markers"
|
||||||
AUTH_UPLOAD = "upload"
|
AUTH_UPLOAD = "upload"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Pass []byte
|
Pass []byte
|
||||||
Auths Auths
|
Auths Auths
|
||||||
Tokens []string
|
Tokens []string
|
||||||
// OAuth: provider -> subject (unique ID from provider)
|
// OAuth: provider -> subject (unique ID from provider)
|
||||||
OAuthLinks map[string]string `json:"oauth_links,omitempty"` // e.g. "google" -> "123456789"
|
OAuthLinks map[string]string `json:"oauth_links,omitempty"` // e.g. "google" -> "123456789"
|
||||||
}
|
}
|
||||||
|
|
||||||
type Page struct {
|
type Page struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveSPARoot serves the map SPA from root: static files from frontend, fallback to index.html for client-side routes.
|
// serveSPARoot serves the map SPA from root: static files from frontend, fallback to index.html for client-side routes.
|
||||||
// Handles redirects from old /map/* URLs for backward compatibility.
|
// Handles redirects from old /map/* URLs for backward compatibility.
|
||||||
func (a *App) serveSPARoot(rw http.ResponseWriter, req *http.Request) {
|
func (a *App) serveSPARoot(rw http.ResponseWriter, req *http.Request) {
|
||||||
path := req.URL.Path
|
path := req.URL.Path
|
||||||
|
|
||||||
// Redirect old /map/* URLs to flat routes
|
// Redirect old /map/* URLs to flat routes
|
||||||
if path == "/map" || path == "/map/" {
|
if path == "/map" || path == "/map/" {
|
||||||
http.Redirect(rw, req, "/", http.StatusFound)
|
http.Redirect(rw, req, "/", http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(path, "/map/") {
|
if strings.HasPrefix(path, "/map/") {
|
||||||
rest := path[len("/map/"):]
|
rest := path[len("/map/"):]
|
||||||
switch {
|
switch {
|
||||||
case rest == "login":
|
case rest == "login":
|
||||||
http.Redirect(rw, req, "/login", http.StatusFound)
|
http.Redirect(rw, req, "/login", http.StatusFound)
|
||||||
return
|
return
|
||||||
case rest == "profile":
|
case rest == "profile":
|
||||||
http.Redirect(rw, req, "/profile", http.StatusFound)
|
http.Redirect(rw, req, "/profile", http.StatusFound)
|
||||||
return
|
return
|
||||||
case rest == "admin" || strings.HasPrefix(rest, "admin/"):
|
case rest == "admin" || strings.HasPrefix(rest, "admin/"):
|
||||||
http.Redirect(rw, req, "/"+rest, http.StatusFound)
|
http.Redirect(rw, req, "/"+rest, http.StatusFound)
|
||||||
return
|
return
|
||||||
case rest == "setup":
|
case rest == "setup":
|
||||||
http.Redirect(rw, req, "/setup", http.StatusFound)
|
http.Redirect(rw, req, "/setup", http.StatusFound)
|
||||||
return
|
return
|
||||||
case strings.HasPrefix(rest, "character/"):
|
case strings.HasPrefix(rest, "character/"):
|
||||||
http.Redirect(rw, req, "/"+rest, http.StatusFound)
|
http.Redirect(rw, req, "/"+rest, http.StatusFound)
|
||||||
return
|
return
|
||||||
case strings.HasPrefix(rest, "grid/"):
|
case strings.HasPrefix(rest, "grid/"):
|
||||||
http.Redirect(rw, req, "/"+rest, http.StatusFound)
|
http.Redirect(rw, req, "/"+rest, http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// File serving: path relative to frontend root (with baseURL /, files are at root)
|
// File serving: path relative to frontend root (with baseURL /, files are at root)
|
||||||
filePath := strings.TrimPrefix(path, "/")
|
filePath := strings.TrimPrefix(path, "/")
|
||||||
if filePath == "" {
|
if filePath == "" {
|
||||||
filePath = "index.html"
|
filePath = "index.html"
|
||||||
}
|
}
|
||||||
filePath = filepath.Clean(filePath)
|
filePath = filepath.Clean(filePath)
|
||||||
if filePath == "." || filePath == ".." || strings.HasPrefix(filePath, "..") {
|
if filePath == "." || filePath == ".." || strings.HasPrefix(filePath, "..") {
|
||||||
http.NotFound(rw, req)
|
http.NotFound(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Try both root and map/ for backward compatibility with old builds
|
// Try both root and map/ for backward compatibility with old builds
|
||||||
tryPaths := []string{filePath, filepath.Join("map", filePath)}
|
tryPaths := []string{filePath, filepath.Join("map", filePath)}
|
||||||
var f http.File
|
var f http.File
|
||||||
for _, p := range tryPaths {
|
for _, p := range tryPaths {
|
||||||
var err error
|
var err error
|
||||||
f, err = http.Dir(a.frontendRoot).Open(p)
|
f, err = http.Dir(a.frontendRoot).Open(p)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
filePath = p
|
filePath = p
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if f == nil {
|
if f == nil {
|
||||||
http.ServeFile(rw, req, filepath.Join(a.frontendRoot, "index.html"))
|
http.ServeFile(rw, req, filepath.Join(a.frontendRoot, "index.html"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
stat, err := f.Stat()
|
stat, err := f.Stat()
|
||||||
if err != nil || stat.IsDir() {
|
if err != nil || stat.IsDir() {
|
||||||
http.ServeFile(rw, req, filepath.Join(a.frontendRoot, "index.html"))
|
http.ServeFile(rw, req, filepath.Join(a.frontendRoot, "index.html"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.ServeContent(rw, req, stat.Name(), stat.ModTime(), f)
|
http.ServeContent(rw, req, stat.Name(), stat.ModTime(), f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanChars runs a background loop that removes stale character entries. Call once as a goroutine.
|
// CleanChars runs a background loop that removes stale character entries. Call once as a goroutine.
|
||||||
func (a *App) CleanChars() {
|
func (a *App) CleanChars() {
|
||||||
for range time.Tick(time.Second * 10) {
|
for range time.Tick(time.Second * 10) {
|
||||||
a.chmu.Lock()
|
a.chmu.Lock()
|
||||||
for n, c := range a.characters {
|
for n, c := range a.characters {
|
||||||
if c.updated.Before(time.Now().Add(-10 * time.Second)) {
|
if c.updated.Before(time.Now().Add(-10 * time.Second)) {
|
||||||
delete(a.characters, n)
|
delete(a.characters, n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.chmu.Unlock()
|
a.chmu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterRoutes registers all HTTP handlers for the app.
|
// RegisterRoutes registers all HTTP handlers for the app.
|
||||||
func (a *App) RegisterRoutes() {
|
func (a *App) RegisterRoutes() {
|
||||||
http.HandleFunc("/client/", a.client)
|
http.HandleFunc("/client/", a.client)
|
||||||
http.HandleFunc("/logout", a.redirectLogout)
|
http.HandleFunc("/logout", a.redirectLogout)
|
||||||
|
|
||||||
http.HandleFunc("/map/api/", a.apiRouter)
|
http.HandleFunc("/map/api/", a.apiRouter)
|
||||||
http.HandleFunc("/map/updates", a.watchGridUpdates)
|
http.HandleFunc("/map/updates", a.watchGridUpdates)
|
||||||
http.HandleFunc("/map/grids/", a.gridTile)
|
http.HandleFunc("/map/grids/", a.gridTile)
|
||||||
|
|
||||||
// SPA catch-all: must be last
|
// SPA catch-all: must be last
|
||||||
http.HandleFunc("/", a.serveSPARoot)
|
http.HandleFunc("/", a.serveSPARoot)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user