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

@@ -1,7 +1,10 @@
package services
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"strconv"
"github.com/andyleap/hnh-map/internal/app"
@@ -10,20 +13,21 @@ import (
"golang.org/x/crypto/bcrypt"
)
// AdminService handles admin business logic (users, settings, maps, wipe).
// AdminService handles admin business logic (users, settings, maps, wipe, tile ops).
type AdminService struct {
st *store.Store
st *store.Store
mapSvc *MapService
}
// NewAdminService creates an AdminService.
func NewAdminService(st *store.Store) *AdminService {
return &AdminService{st: st}
// NewAdminService creates an AdminService with the given store and map service.
func NewAdminService(st *store.Store, mapSvc *MapService) *AdminService {
return &AdminService{st: st, mapSvc: mapSvc}
}
// ListUsers returns all usernames.
func (s *AdminService) ListUsers() ([]string, error) {
func (s *AdminService) ListUsers(ctx context.Context) ([]string, error) {
var list []string
err := s.st.View(func(tx *bbolt.Tx) error {
err := s.st.View(ctx, func(tx *bbolt.Tx) error {
return s.st.ForEachUser(tx, func(k, _ []byte) error {
list = append(list, string(k))
return nil
@@ -32,9 +36,9 @@ func (s *AdminService) ListUsers() ([]string, error) {
return list, err
}
// GetUser returns user auths by username.
func (s *AdminService) GetUser(username string) (auths app.Auths, found bool) {
s.st.View(func(tx *bbolt.Tx) error {
// GetUser returns a user's permissions by username.
func (s *AdminService) GetUser(ctx context.Context, username string) (auths app.Auths, found bool) {
s.st.View(ctx, func(tx *bbolt.Tx) error {
raw := s.st.GetUser(tx, username)
if raw == nil {
return nil
@@ -49,9 +53,9 @@ func (s *AdminService) GetUser(username string) (auths app.Auths, found bool) {
}
// CreateOrUpdateUser creates or updates a user.
// Returns (true, nil) when admin user was created and didn't exist before (temp admin bootstrap).
func (s *AdminService) CreateOrUpdateUser(username string, pass string, auths app.Auths) (adminCreated bool, err error) {
err = s.st.Update(func(tx *bbolt.Tx) error {
// Returns (true, nil) when admin user was created fresh (temp admin bootstrap).
func (s *AdminService) CreateOrUpdateUser(ctx context.Context, username string, pass string, auths app.Auths) (adminCreated bool, err error) {
err = s.st.Update(ctx, func(tx *bbolt.Tx) error {
existed := s.st.GetUser(tx, username) != nil
u := app.User{}
raw := s.st.GetUser(tx, username)
@@ -79,8 +83,8 @@ func (s *AdminService) CreateOrUpdateUser(username string, pass string, auths ap
}
// DeleteUser removes a user and their tokens.
func (s *AdminService) DeleteUser(username string) error {
return s.st.Update(func(tx *bbolt.Tx) error {
func (s *AdminService) DeleteUser(ctx context.Context, username string) error {
return s.st.Update(ctx, func(tx *bbolt.Tx) error {
uRaw := s.st.GetUser(tx, username)
if uRaw != nil {
var u app.User
@@ -93,9 +97,9 @@ func (s *AdminService) DeleteUser(username string) error {
})
}
// GetSettings returns prefix, defaultHide, title.
func (s *AdminService) GetSettings() (prefix string, defaultHide bool, title string, err error) {
err = s.st.View(func(tx *bbolt.Tx) error {
// GetSettings returns the current server settings.
func (s *AdminService) GetSettings(ctx context.Context) (prefix string, defaultHide bool, title string, err error) {
err = s.st.View(ctx, func(tx *bbolt.Tx) error {
if v := s.st.GetConfig(tx, "prefix"); v != nil {
prefix = string(v)
}
@@ -110,9 +114,9 @@ func (s *AdminService) GetSettings() (prefix string, defaultHide bool, title str
return prefix, defaultHide, title, err
}
// UpdateSettings updates config keys.
func (s *AdminService) UpdateSettings(prefix *string, defaultHide *bool, title *string) error {
return s.st.Update(func(tx *bbolt.Tx) error {
// UpdateSettings updates the specified server settings (nil fields are skipped).
func (s *AdminService) UpdateSettings(ctx context.Context, prefix *string, defaultHide *bool, title *string) error {
return s.st.Update(ctx, func(tx *bbolt.Tx) error {
if prefix != nil {
s.st.PutConfig(tx, "prefix", []byte(*prefix))
}
@@ -130,10 +134,10 @@ func (s *AdminService) UpdateSettings(prefix *string, defaultHide *bool, title *
})
}
// ListMaps returns all maps.
func (s *AdminService) ListMaps() ([]app.MapInfo, error) {
// ListMaps returns all maps for the admin panel.
func (s *AdminService) ListMaps(ctx context.Context) ([]app.MapInfo, error) {
var maps []app.MapInfo
err := s.st.View(func(tx *bbolt.Tx) error {
err := s.st.View(ctx, func(tx *bbolt.Tx) error {
return s.st.ForEachMap(tx, func(k, v []byte) error {
mi := app.MapInfo{}
json.Unmarshal(v, &mi)
@@ -148,9 +152,9 @@ func (s *AdminService) ListMaps() ([]app.MapInfo, error) {
}
// GetMap returns a map by ID.
func (s *AdminService) GetMap(id int) (*app.MapInfo, bool) {
func (s *AdminService) GetMap(ctx context.Context, id int) (*app.MapInfo, bool) {
var mi *app.MapInfo
s.st.View(func(tx *bbolt.Tx) error {
s.st.View(ctx, func(tx *bbolt.Tx) error {
raw := s.st.GetMap(tx, id)
if raw != nil {
mi = &app.MapInfo{}
@@ -162,9 +166,9 @@ func (s *AdminService) GetMap(id int) (*app.MapInfo, bool) {
return mi, mi != nil
}
// UpdateMap updates map name, hidden, priority.
func (s *AdminService) UpdateMap(id int, name string, hidden, priority bool) error {
return s.st.Update(func(tx *bbolt.Tx) error {
// UpdateMap updates a map's name, hidden, and priority fields.
func (s *AdminService) UpdateMap(ctx context.Context, id int, name string, hidden, priority bool) error {
return s.st.Update(ctx, func(tx *bbolt.Tx) error {
mi := app.MapInfo{}
raw := s.st.GetMap(tx, id)
if raw != nil {
@@ -179,10 +183,10 @@ func (s *AdminService) UpdateMap(id int, name string, hidden, priority bool) err
})
}
// ToggleMapHidden flips the hidden flag.
func (s *AdminService) ToggleMapHidden(id int) (*app.MapInfo, error) {
// ToggleMapHidden toggles the hidden flag of a map and returns the updated map.
func (s *AdminService) ToggleMapHidden(ctx context.Context, id int) (*app.MapInfo, error) {
var mi *app.MapInfo
err := s.st.Update(func(tx *bbolt.Tx) error {
err := s.st.Update(ctx, func(tx *bbolt.Tx) error {
raw := s.st.GetMap(tx, id)
mi = &app.MapInfo{}
if raw != nil {
@@ -196,9 +200,9 @@ func (s *AdminService) ToggleMapHidden(id int) (*app.MapInfo, error) {
return mi, err
}
// Wipe deletes grids, markers, tiles, maps buckets.
func (s *AdminService) Wipe() error {
return s.st.Update(func(tx *bbolt.Tx) error {
// Wipe deletes all grids, markers, tiles, and maps from the database.
func (s *AdminService) Wipe(ctx context.Context) error {
return s.st.Update(ctx, func(tx *bbolt.Tx) error {
for _, b := range [][]byte{
store.BucketGrids,
store.BucketMarkers,
@@ -214,3 +218,137 @@ func (s *AdminService) Wipe() error {
return nil
})
}
// WipeTile removes a tile at the given coordinates and rebuilds zoom levels.
func (s *AdminService) WipeTile(ctx context.Context, mapid, x, y int) error {
c := app.Coord{X: x, Y: y}
if err := s.st.Update(ctx, func(tx *bbolt.Tx) error {
grids := tx.Bucket(store.BucketGrids)
if grids == nil {
return nil
}
var ids [][]byte
err := grids.ForEach(func(k, v []byte) error {
g := app.GridData{}
if err := json.Unmarshal(v, &g); err != nil {
return err
}
if g.Coord == c && g.Map == mapid {
ids = append(ids, k)
}
return nil
})
if err != nil {
return err
}
for _, id := range ids {
grids.Delete(id)
}
return nil
}); err != nil {
return err
}
s.mapSvc.SaveTile(ctx, mapid, c, 0, "", -1)
zc := c
for z := 1; z <= app.MaxZoomLevel; z++ {
zc = zc.Parent()
s.mapSvc.UpdateZoomLevel(ctx, mapid, zc, z)
}
return nil
}
// SetCoords shifts all grid and tile coordinates by a delta.
func (s *AdminService) SetCoords(ctx context.Context, mapid, fx, fy, tx2, ty int) error {
fc := app.Coord{X: fx, Y: fy}
tc := app.Coord{X: tx2, Y: ty}
diff := app.Coord{X: tc.X - fc.X, Y: tc.Y - fc.Y}
var tds []*app.TileData
if err := s.st.Update(ctx, func(tx *bbolt.Tx) error {
grids := tx.Bucket(store.BucketGrids)
if grids == nil {
return nil
}
tiles := tx.Bucket(store.BucketTiles)
if tiles == nil {
return nil
}
mapZooms := tiles.Bucket([]byte(strconv.Itoa(mapid)))
if mapZooms == nil {
return nil
}
mapTiles := mapZooms.Bucket([]byte("0"))
if err := grids.ForEach(func(k, v []byte) error {
g := app.GridData{}
if err := json.Unmarshal(v, &g); err != nil {
return err
}
if g.Map == mapid {
g.Coord.X += diff.X
g.Coord.Y += diff.Y
raw, _ := json.Marshal(g)
grids.Put(k, raw)
}
return nil
}); err != nil {
return err
}
if err := mapTiles.ForEach(func(k, v []byte) error {
td := &app.TileData{}
if err := json.Unmarshal(v, td); err != nil {
return err
}
td.Coord.X += diff.X
td.Coord.Y += diff.Y
tds = append(tds, td)
return nil
}); err != nil {
return err
}
return tiles.DeleteBucket([]byte(strconv.Itoa(mapid)))
}); err != nil {
return err
}
ops := make([]TileOp, len(tds))
for i, td := range tds {
ops[i] = TileOp{MapID: td.MapID, X: td.Coord.X, Y: td.Coord.Y, File: td.File}
}
s.mapSvc.ProcessZoomLevels(ctx, ops)
return nil
}
// HideMarker marks a marker as hidden.
func (s *AdminService) HideMarker(ctx context.Context, markerID string) error {
return s.st.Update(ctx, func(tx *bbolt.Tx) error {
_, idB, err := s.st.CreateMarkersBuckets(tx)
if err != nil {
return err
}
grid := s.st.GetMarkersGridBucket(tx)
if grid == nil {
return fmt.Errorf("markers grid bucket not found")
}
key := idB.Get([]byte(markerID))
if key == nil {
slog.Warn("marker not found", "id", markerID)
return nil
}
raw := grid.Get(key)
if raw == nil {
return nil
}
m := app.Marker{}
json.Unmarshal(raw, &m)
m.Hidden = true
raw, _ = json.Marshal(m)
grid.Put(key, raw)
return nil
})
}
// RebuildZooms delegates to MapService.
func (s *AdminService) RebuildZooms(ctx context.Context) {
s.mapSvc.RebuildZooms(ctx)
}