Add configuration files and update project documentation

- Introduced .editorconfig for consistent coding styles across the project.
- Added .golangci.yml for Go linting configuration.
- Updated AGENTS.md to clarify project structure and components.
- Enhanced CONTRIBUTING.md with Makefile usage for common tasks.
- Updated Dockerfiles to use Go 1.24 and improved build instructions.
- Refined README.md and deployment documentation for clarity.
- Added testing documentation in testing.md for backend and frontend tests.
- Introduced Makefile for streamlined development commands and tasks.
This commit is contained in:
2026-03-01 01:51:47 +03:00
parent 0466ff3087
commit 6529d7370e
92 changed files with 13411 additions and 8438 deletions

View File

@@ -11,6 +11,26 @@ import (
"go.etcd.io/bbolt"
)
const (
AUTH_ADMIN = "admin"
AUTH_MAP = "map"
AUTH_MARKERS = "markers"
AUTH_UPLOAD = "upload"
CharCleanupInterval = 10 * time.Second
CharStaleThreshold = 10 * time.Second
TileUpdateInterval = 30 * time.Minute
MaxZoomLevel = 5
GridSize = 100
SessionMaxAge = 7 * 24 * 3600 // 1 week in seconds
MultipartMaxMemory = 100 << 20 // 100 MB
MergeMaxMemory = 500 << 20 // 500 MB
ClientVersion = "4"
SSETickInterval = 5 * time.Second
SSETileChannelSize = 1000
SSEMergeChannelSize = 5
)
// App is the main application (map server) state.
type App struct {
gridStorage string
@@ -24,22 +44,29 @@ type App struct {
mergeUpdates Topic[Merge]
}
// GridStorage returns the grid storage path.
func (a *App) GridStorage() string {
return a.gridStorage
// NewApp creates an App with the given storage paths and database.
func NewApp(gridStorage, frontendRoot string, db *bbolt.DB) (*App, error) {
return &App{
gridStorage: gridStorage,
frontendRoot: frontendRoot,
db: db,
characters: make(map[string]Character),
}, nil
}
// GridUpdates returns the tile updates topic for MapService.
func (a *App) GridUpdates() *Topic[TileData] {
return &a.gridUpdates
}
// GridStorage returns the path to the grid storage directory.
func (a *App) GridStorage() string { return a.gridStorage }
// MergeUpdates returns the merge updates topic for MapService.
func (a *App) MergeUpdates() *Topic[Merge] {
return &a.mergeUpdates
}
// GridUpdates returns the tile update pub/sub topic.
func (a *App) GridUpdates() *Topic[TileData] { return &a.gridUpdates }
// GetCharacters returns a copy of all characters (for MapService).
// MergeUpdates returns the merge event pub/sub topic.
func (a *App) MergeUpdates() *Topic[Merge] { return &a.mergeUpdates }
// DB returns the underlying bbolt database.
func (a *App) DB() *bbolt.DB { return a.db }
// GetCharacters returns a copy of all characters.
func (a *App) GetCharacters() []Character {
a.chmu.RLock()
defer a.chmu.RUnlock()
@@ -50,128 +77,30 @@ func (a *App) GetCharacters() []Character {
return chars
}
// NewApp creates an App with the given storage paths and database.
// frontendRoot is the directory for the map SPA (e.g. "frontend").
func NewApp(gridStorage, frontendRoot string, db *bbolt.DB) (*App, error) {
return &App{
gridStorage: gridStorage,
frontendRoot: frontendRoot,
db: db,
characters: make(map[string]Character),
}, nil
// WithCharacters provides locked mutable access to the character map.
func (a *App) WithCharacters(fn func(chars map[string]Character)) {
a.chmu.Lock()
defer a.chmu.Unlock()
fn(a.characters)
}
type Session struct {
ID string
Username string
Auths Auths `json:"-"`
TempAdmin bool
}
type Character struct {
Name string `json:"name"`
ID int `json:"id"`
Map int `json:"map"`
Position Position `json:"position"`
Type string `json:"type"`
updated time.Time
}
type Marker struct {
Name string `json:"name"`
ID int `json:"id"`
GridID string `json:"gridID"`
Position Position `json:"position"`
Image string `json:"image"`
Hidden bool `json:"hidden"`
}
type FrontendMarker struct {
Name string `json:"name"`
ID int `json:"id"`
Map int `json:"map"`
Position Position `json:"position"`
Image string `json:"image"`
Hidden bool `json:"hidden"`
}
type MapInfo struct {
ID int
Name string
Hidden bool
Priority bool
}
type GridData struct {
ID string
Coord Coord
NextUpdate time.Time
Map int
}
type Coord struct {
X int `json:"x"`
Y int `json:"y"`
}
type Position struct {
X int `json:"x"`
Y int `json:"y"`
}
func (c Coord) Name() string {
return fmt.Sprintf("%d_%d", c.X, c.Y)
}
func (c Coord) Parent() Coord {
if c.X < 0 {
c.X--
}
if c.Y < 0 {
c.Y--
}
return Coord{
X: c.X / 2,
Y: c.Y / 2,
}
}
type Auths []string
func (a Auths) Has(auth string) bool {
for _, v := range a {
if v == auth {
return true
// CleanChars runs a background loop that removes stale character entries.
func (a *App) CleanChars() {
for range time.Tick(CharCleanupInterval) {
a.chmu.Lock()
for n, c := range a.characters {
if c.Updated.Before(time.Now().Add(-CharStaleThreshold)) {
delete(a.characters, n)
}
}
a.chmu.Unlock()
}
return false
}
const (
AUTH_ADMIN = "admin"
AUTH_MAP = "map"
AUTH_MARKERS = "markers"
AUTH_UPLOAD = "upload"
)
type User struct {
Pass []byte
Auths Auths
Tokens []string
// OAuth: provider -> subject (unique ID from provider)
OAuthLinks map[string]string `json:"oauth_links,omitempty"` // e.g. "google" -> "123456789"
}
type Page struct {
Title string `json:"title"`
}
// 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.
func (a *App) serveSPARoot(rw http.ResponseWriter, req *http.Request) {
// serveSPARoot serves the map SPA: static files from frontend, fallback to index.html.
func (a *App) ServeSPARoot(rw http.ResponseWriter, req *http.Request) {
path := req.URL.Path
// Redirect old /map/* URLs to flat routes
if path == "/map" || path == "/map/" {
http.Redirect(rw, req, "/", http.StatusFound)
return
@@ -200,7 +129,6 @@ func (a *App) serveSPARoot(rw http.ResponseWriter, req *http.Request) {
}
}
// File serving: path relative to frontend root (with baseURL /, files are at root)
filePath := strings.TrimPrefix(path, "/")
if filePath == "" {
filePath = "index.html"
@@ -210,14 +138,12 @@ func (a *App) serveSPARoot(rw http.ResponseWriter, req *http.Request) {
http.NotFound(rw, req)
return
}
// Try both root and map/ for backward compatibility with old builds
tryPaths := []string{filePath, filepath.Join("map", filePath)}
var f http.File
for _, p := range tryPaths {
var err error
f, err = http.Dir(a.frontendRoot).Open(p)
if err == nil {
filePath = p
break
}
}
@@ -234,28 +160,130 @@ func (a *App) serveSPARoot(rw http.ResponseWriter, req *http.Request) {
http.ServeContent(rw, req, stat.Name(), stat.ModTime(), f)
}
// CleanChars runs a background loop that removes stale character entries. Call once as a goroutine.
func (a *App) CleanChars() {
for range time.Tick(time.Second * 10) {
a.chmu.Lock()
for n, c := range a.characters {
if c.updated.Before(time.Now().Add(-10 * time.Second)) {
delete(a.characters, n)
}
}
a.chmu.Unlock()
// --- Domain types ---
// Session represents an authenticated user session.
type Session struct {
ID string
Username string
Auths Auths `json:"-"`
TempAdmin bool
}
// Character represents a game character on the map.
type Character struct {
Name string `json:"name"`
ID int `json:"id"`
Map int `json:"map"`
Position Position `json:"position"`
Type string `json:"type"`
Updated time.Time
}
// Marker represents a map marker stored per grid.
type Marker struct {
Name string `json:"name"`
ID int `json:"id"`
GridID string `json:"gridID"`
Position Position `json:"position"`
Image string `json:"image"`
Hidden bool `json:"hidden"`
}
// FrontendMarker is a marker with map-level coordinates for the frontend.
type FrontendMarker struct {
Name string `json:"name"`
ID int `json:"id"`
Map int `json:"map"`
Position Position `json:"position"`
Image string `json:"image"`
Hidden bool `json:"hidden"`
}
// MapInfo describes a map instance.
type MapInfo struct {
ID int
Name string
Hidden bool
Priority bool
}
// GridData represents a grid tile with its coordinates and map assignment.
type GridData struct {
ID string
Coord Coord
NextUpdate time.Time
Map int
}
// Coord represents a grid coordinate pair.
type Coord struct {
X int `json:"x"`
Y int `json:"y"`
}
// Position represents a pixel position within a grid.
type Position struct {
X int `json:"x"`
Y int `json:"y"`
}
// Name returns the string representation "X_Y" used as bucket keys.
func (c Coord) Name() string {
return fmt.Sprintf("%d_%d", c.X, c.Y)
}
// Parent returns the parent coordinate at the next zoom level.
func (c Coord) Parent() Coord {
if c.X < 0 {
c.X--
}
if c.Y < 0 {
c.Y--
}
return Coord{
X: c.X / 2,
Y: c.Y / 2,
}
}
// RegisterRoutes registers all HTTP handlers for the app.
func (a *App) RegisterRoutes() {
http.HandleFunc("/client/", a.client)
http.HandleFunc("/logout", a.redirectLogout)
// Auths is a list of permission strings (e.g. "admin", "map", "upload").
type Auths []string
http.HandleFunc("/map/api/", a.apiRouter)
http.HandleFunc("/map/updates", a.watchGridUpdates)
http.HandleFunc("/map/grids/", a.gridTile)
// SPA catch-all: must be last
http.HandleFunc("/", a.serveSPARoot)
// Has returns true if the list contains the given permission.
func (a Auths) Has(auth string) bool {
for _, v := range a {
if v == auth {
return true
}
}
return false
}
// User represents a stored user account.
type User struct {
Pass []byte
Auths Auths
Tokens []string
OAuthLinks map[string]string `json:"oauth_links,omitempty"`
}
// Page holds page metadata for rendering.
type Page struct {
Title string `json:"title"`
}
// Config holds the application config sent to the frontend.
type Config struct {
Title string `json:"title"`
Auths []string `json:"auths"`
}
// TileData represents a tile image entry in the database.
type TileData struct {
MapID int
Coord Coord
Zoom int
File string
Cache int64
}