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:
2026-02-28 23:53:20 +03:00
parent 5ffa10f8b7
commit 0466ff3087
12 changed files with 1203 additions and 1198 deletions

13
Dockerfile.dev Normal file
View 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"]

View File

@@ -1,11 +1,11 @@
# Development: backend (Go) on 8080, frontend (Nuxt dev) on 3000 with proxy to backend.
# Open http://localhost:3000/map/ — /map/api, /map/updates, /map/grids are proxied to backend.
# Development: backend API on 3080 + frontend Nuxt dev server on 3000.
# Open http://localhost:3000/ for app development with live-reload.
services:
backend:
build:
context: .
dockerfile: Dockerfile
dockerfile: Dockerfile.dev
ports:
- "3080:3080"
volumes:
@@ -26,5 +26,6 @@ services:
- /app/node_modules
environment:
- NUXT_PUBLIC_API_BASE=/map/api
- HOST=0.0.0.0
depends_on:
- backend

View File

@@ -35,10 +35,13 @@ npm run dev
docker compose -f docker-compose.dev.yml up
```
- Фронт: порт **3000** (Nuxt dev-сервер).
- Бэкенд: порт **3080** (чтобы не конфликтовать с другими сервисами на 8080).
Dev-композ поднимает два сервиса:
Откройте http://localhost:3000/. Запросы к `/map/api`, `/map/updates`, `/map/grids` проксируются на бэкенд (host `backend`, порт 3080).
- `backend` — Go API на порту `3080` (без сборки/раздачи фронтенд-статики в dev-режиме).
- `frontend` — Nuxt dev-сервер на порту `3000` с live-reload; запросы к `/map/api`, `/map/updates`, `/map/grids` проксируются на бэкенд.
Используйте [http://localhost:3000/](http://localhost:3000/) как основной URL для разработки интерфейса.
Порт `3080` предназначен для API и backend-эндпоинтов; корень `/` может возвращать `404` в dev-режиме — это ожидаемо.
### Сборка образа и prod-композ

View File

@@ -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

View File

@@ -1,5 +1,5 @@
<template>
<div class="relative h-full w-full" @click="mapLogic.closeContextMenus()">
<div class="relative h-full w-full" @click="(e: MouseEvent) => e.button === 0 && mapLogic.closeContextMenus()">
<div
v-if="mapsLoaded && maps.length === 0"
class="absolute inset-0 z-[500] flex flex-col items-center justify-center gap-4 bg-base-200/90 p-6"
@@ -95,6 +95,8 @@ let markers: UniqueList<InstanceType<typeof Marker>> | null = null
let characters: UniqueList<InstanceType<typeof Character>> | null = null
let markersHidden = false
let autoMode = false
let mapContainer: HTMLElement | null = null
let contextMenuHandler: ((ev: MouseEvent) => void) | null = null
function toLatLng(x: number, y: number) {
return map!.unproject([x, y], HnHMaxZoom)
@@ -187,7 +189,8 @@ onMounted(async () => {
const config = (await api.getConfig().catch(() => ({}))) as { title?: string; auths?: string[] }
if (config?.title) document.title = config.title
if (config?.auths) auths.value = config.auths
const user = await api.me().catch(() => null)
auths.value = (user as { auths?: string[] } | null)?.auths ?? config?.auths ?? []
map = L.map(mapRef.value, {
minZoom: HnHMinZoom,
@@ -257,13 +260,25 @@ onMounted(async () => {
const markerIconPath = baseURL.endsWith('/') ? baseURL : baseURL + '/'
L.Icon.Default.imagePath = markerIconPath
map.on('contextmenu', (mev: L.LeafletMouseEvent) => {
if (auths.value.includes('admin')) {
const point = map!.project(mev.latlng, 6)
// Document-level capture so we get contextmenu before any map layer or iframe can swallow it
mapContainer = map.getContainer()
contextMenuHandler = (ev: MouseEvent) => {
const target = ev.target as Node
if (!mapContainer?.contains(target)) return
const isAdmin = auths.value.includes('admin')
if (import.meta.dev) console.log('[MapView contextmenu]', { isAdmin, auths: auths.value })
if (isAdmin) {
ev.preventDefault()
ev.stopPropagation()
const rect = mapContainer.getBoundingClientRect()
const containerPoint = L.point(ev.clientX - rect.left, ev.clientY - rect.top)
const latlng = map!.containerPointToLatLng(containerPoint)
const point = map!.project(latlng, 6)
const coords = { x: Math.floor(point.x / TileSize), y: Math.floor(point.y / TileSize) }
mapLogic.openTileContextMenu(mev.originalEvent.clientX, mev.originalEvent.clientY, coords)
mapLogic.openTileContextMenu(ev.clientX, ev.clientY, coords)
}
})
}
document.addEventListener('contextmenu', contextMenuHandler, true)
const updatesPath = `${backendBase}/updates`
const updatesUrl = import.meta.client ? `${window.location.origin}${updatesPath}` : updatesPath
@@ -353,6 +368,8 @@ onMounted(async () => {
marker.setClickCallback(() => map!.setView(marker.marker!.getLatLng(), HnHMaxZoom))
marker.setContextMenu((mev: L.LeafletMouseEvent) => {
if (auths.value.includes('admin')) {
mev.originalEvent.preventDefault()
mev.originalEvent.stopPropagation()
mapLogic.openMarkerContextMenu(mev.originalEvent.clientX, mev.originalEvent.clientY, marker.id, marker.name)
}
})
@@ -473,6 +490,9 @@ onBeforeUnmount(() => {
if (import.meta.client) {
window.removeEventListener('keydown', onKeydown)
}
if (contextMenuHandler) {
document.removeEventListener('contextmenu', contextMenuHandler, true)
}
if (intervalId) clearInterval(intervalId)
if (source) source.close()
if (map) map.remove()

View File

@@ -1,8 +1,9 @@
<template>
<!-- Context menu (tile) -->
<!-- Context menu (tile) Teleport so it is not clipped by map/overflow -->
<Teleport to="body">
<div
v-show="contextMenu.tile.show"
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"
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
@@ -23,7 +24,7 @@
<!-- Context menu (marker) -->
<div
v-show="contextMenu.marker.show"
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"
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
@@ -34,6 +35,7 @@
Hide marker {{ contextMenu.marker.data?.name }}
</button>
</div>
</Teleport>
</template>
<script setup lang="ts">

View File

@@ -101,6 +101,7 @@ export function useMapLogic() {
}
function openTileContextMenu(clientX: number, clientY: number, coords: { x: number; y: number }) {
closeContextMenus()
contextMenu.tile.show = true
contextMenu.tile.x = clientX
contextMenu.tile.y = clientY
@@ -108,6 +109,7 @@ export function useMapLogic() {
}
function openMarkerContextMenu(clientX: number, clientY: number, id: number, name: string) {
closeContextMenus()
contextMenu.marker.show = true
contextMenu.marker.x = clientX
contextMenu.marker.y = clientY

View File

@@ -43,9 +43,9 @@ export default defineNuxtConfig({
// Dev: proxy /map API, SSE and grids to Go backend (e.g. docker compose -f docker-compose.dev.yml)
nitro: {
devProxy: {
'/map/api': { target: 'http://backend:3080', changeOrigin: true },
'/map/updates': { target: 'http://backend:3080', changeOrigin: true },
'/map/grids': { target: 'http://backend:3080', changeOrigin: true },
'/map/api': { target: 'http://backend:3080/map/api', changeOrigin: true },
'/map/updates': { target: 'http://backend:3080/map/updates', changeOrigin: true },
'/map/grids': { target: 'http://backend:3080/map/grids', changeOrigin: true },
},
},

View File

@@ -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>