feat: rename service to galo, add halo branding and release build
- rename Go module, binary, systemd unit, PG_DATABASE default, frontend package and docs from shared-album to galo - add theme-aware halo emblem and favicon (new ui-branding capability) - add make build-release (versioned, stripped, static binary) and run the binary from the repo root
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@ PG_HOST=pg.localnet
|
||||
PG_PORT=5432
|
||||
PG_USER=album
|
||||
PG_PASSWORD=change_me
|
||||
PG_DATABASE=shared_album
|
||||
PG_DATABASE=galo
|
||||
PG_SSLMODE=disable
|
||||
|
||||
# Metadata backend: sqlite (default, embedded single instance) or postgres
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
.PHONY: build frontend backend run migrate reconcile reconcile-delete tidy test clean
|
||||
.PHONY: build build-release frontend backend run migrate reconcile reconcile-delete tidy test clean
|
||||
|
||||
# Release version stamped into the binary (-X main.version). Override with VERSION=...
|
||||
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
||||
|
||||
frontend:
|
||||
cd frontend && npm ci && npm run build
|
||||
@@ -10,12 +13,27 @@ backend:
|
||||
# -tags nodynamic forces gen2brain/webp to use its pure-Go transpiled path
|
||||
# (no purego/dlopen), keeping the binary fully static.
|
||||
cd backend && CGO_ENABLED=0 go build -tags nodynamic -trimpath -ldflags="-s -w" \
|
||||
-o ../bin/shared-album ./cmd/server
|
||||
-o ../bin/galo ./cmd/server
|
||||
|
||||
build: frontend backend
|
||||
|
||||
# Release build: full static, stripped and (if available) UPX-compressed binary
|
||||
# with the version embedded. Produces bin/galo and a copy at bin/galo-$(VERSION).
|
||||
build-release: frontend
|
||||
mkdir -p bin
|
||||
cd backend && CGO_ENABLED=0 go build -tags nodynamic -trimpath \
|
||||
-ldflags="-s -w -X main.version=$(VERSION)" -o ../bin/galo ./cmd/server
|
||||
strip --strip-all bin/galo
|
||||
@if command -v upx >/dev/null 2>&1; then \
|
||||
upx --best --quiet bin/galo && echo "upx: bin/galo compressed"; \
|
||||
else \
|
||||
echo "upx not found - skipping compression (install upx-ucl to enable)"; \
|
||||
fi
|
||||
cp bin/galo bin/galo-$(VERSION)
|
||||
@echo "release: bin/galo-$(VERSION)"
|
||||
|
||||
run: backend
|
||||
cd backend && ../bin/shared-album
|
||||
./bin/galo
|
||||
|
||||
migrate:
|
||||
cd backend && go run ./cmd/migrate
|
||||
|
||||
@@ -47,14 +47,14 @@ cp .env.example .env
|
||||
## Сборка
|
||||
|
||||
```bash
|
||||
make build # frontend -> backend/static -> go:embed -> bin/shared-album
|
||||
make build # frontend -> backend/static -> go:embed -> bin/galo
|
||||
```
|
||||
|
||||
Отдельные шаги:
|
||||
|
||||
```bash
|
||||
make frontend # npm ci && npm run build, копирует статику в backend/static
|
||||
make backend # CGO_ENABLED=0 go build -tags nodynamic -o bin/shared-album
|
||||
make backend # CGO_ENABLED=0 go build -tags nodynamic -o bin/galo
|
||||
```
|
||||
|
||||
> `-tags nodynamic` обязателен: он заставляет `gen2brain/webp` использовать
|
||||
@@ -64,7 +64,7 @@ make backend # CGO_ENABLED=0 go build -tags nodynamic -o bin/shared-album
|
||||
Проверка:
|
||||
|
||||
```bash
|
||||
file bin/shared-album # => statically linked
|
||||
file bin/galo # => statically linked
|
||||
```
|
||||
|
||||
## Миграции
|
||||
@@ -79,9 +79,9 @@ make migrate # go run ./cmd/migrate — применяет up-миграци
|
||||
## Локальный запуск
|
||||
|
||||
```bash
|
||||
make run # соберёт backend и запустит ./bin/shared-album
|
||||
make run # соберёт backend и запустит ./bin/galo
|
||||
# или напрямую:
|
||||
./bin/shared-album
|
||||
./bin/galo
|
||||
```
|
||||
|
||||
Сервер слушает `HTTP_ADDR` (по умолчанию `:8080`). Health-check:
|
||||
@@ -126,20 +126,20 @@ go run ./cmd/reconcile --delete # удалить сироты старше --m
|
||||
2. Создайте пользователя и каталог:
|
||||
|
||||
```bash
|
||||
sudo useradd --system --home /opt/shared-album album
|
||||
sudo mkdir -p /opt/shared-album
|
||||
sudo cp bin/shared-album /opt/shared-album/
|
||||
sudo cp .env /opt/shared-album/.env
|
||||
sudo chown -R album:album /opt/shared-album
|
||||
sudo useradd --system --home /opt/galo album
|
||||
sudo mkdir -p /opt/galo
|
||||
sudo cp bin/galo /opt/galo/
|
||||
sudo cp .env /opt/galo/.env
|
||||
sudo chown -R album:album /opt/galo
|
||||
```
|
||||
|
||||
3. Установите unit:
|
||||
|
||||
```bash
|
||||
sudo cp deploy/shared-album.service /etc/systemd/system/
|
||||
sudo cp deploy/galo.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now shared-album
|
||||
sudo journalctl -u shared-album -f
|
||||
sudo systemctl enable --now galo
|
||||
sudo journalctl -u galo -f
|
||||
```
|
||||
|
||||
## nginx (опционально)
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"shared-album/internal/config"
|
||||
"shared-album/internal/db"
|
||||
"shared-album/internal/settings"
|
||||
"shared-album/internal/storage"
|
||||
"shared-album/internal/store"
|
||||
"galo/internal/config"
|
||||
"galo/internal/db"
|
||||
"galo/internal/settings"
|
||||
"galo/internal/storage"
|
||||
"galo/internal/store"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
assets "shared-album"
|
||||
"shared-album/internal/config"
|
||||
"shared-album/internal/db"
|
||||
assets "galo"
|
||||
"galo/internal/config"
|
||||
"galo/internal/db"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -14,13 +14,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
assets "shared-album"
|
||||
"shared-album/internal/config"
|
||||
"shared-album/internal/db"
|
||||
"shared-album/internal/reconcile"
|
||||
"shared-album/internal/settings"
|
||||
"shared-album/internal/storage"
|
||||
"shared-album/internal/store"
|
||||
assets "galo"
|
||||
"galo/internal/config"
|
||||
"galo/internal/db"
|
||||
"galo/internal/reconcile"
|
||||
"galo/internal/settings"
|
||||
"galo/internal/storage"
|
||||
"galo/internal/store"
|
||||
)
|
||||
|
||||
const objectPrefix = "objects/"
|
||||
|
||||
@@ -13,23 +13,27 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
assets "shared-album"
|
||||
"shared-album/internal/app"
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/config"
|
||||
"shared-album/internal/db"
|
||||
"shared-album/internal/settings"
|
||||
"shared-album/internal/sse"
|
||||
"shared-album/internal/storage"
|
||||
"shared-album/internal/store"
|
||||
assets "galo"
|
||||
"galo/internal/app"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/config"
|
||||
"galo/internal/db"
|
||||
"galo/internal/settings"
|
||||
"galo/internal/sse"
|
||||
"galo/internal/storage"
|
||||
"galo/internal/store"
|
||||
)
|
||||
|
||||
// version is set at build time via -ldflags "-X main.version=..."
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
loadDotEnv(".env")
|
||||
loadDotEnv("../.env")
|
||||
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
slog.SetDefault(logger)
|
||||
slog.Info("starting galo", "version", version)
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
module shared-album
|
||||
module galo
|
||||
|
||||
go 1.26.1
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/httpx"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/httpx"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
|
||||
@@ -3,7 +3,7 @@ package albums
|
||||
import (
|
||||
"context"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"galo/internal/models"
|
||||
)
|
||||
|
||||
// Repository is the persistence contract for the albums domain.
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/httpx"
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/roles"
|
||||
"shared-album/internal/sse"
|
||||
"shared-album/internal/storage"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/httpx"
|
||||
"galo/internal/models"
|
||||
"galo/internal/roles"
|
||||
"galo/internal/sse"
|
||||
"galo/internal/storage"
|
||||
)
|
||||
|
||||
type MemberLookup interface {
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/sqliteutil"
|
||||
"galo/internal/models"
|
||||
"galo/internal/sqliteutil"
|
||||
)
|
||||
|
||||
// SQLiteStore is the SQLite implementation of Repository.
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shared-album/internal/httpx"
|
||||
"shared-album/internal/models"
|
||||
"galo/internal/httpx"
|
||||
"galo/internal/models"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("album not found")
|
||||
|
||||
+13
-13
@@ -10,19 +10,19 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
assets "shared-album"
|
||||
"shared-album/internal/albums"
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/config"
|
||||
"shared-album/internal/httpx"
|
||||
"shared-album/internal/members"
|
||||
"shared-album/internal/notifications"
|
||||
"shared-album/internal/photos"
|
||||
"shared-album/internal/sharelinks"
|
||||
"shared-album/internal/social"
|
||||
"shared-album/internal/sse"
|
||||
"shared-album/internal/storage"
|
||||
"shared-album/internal/store"
|
||||
assets "galo"
|
||||
"galo/internal/albums"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/config"
|
||||
"galo/internal/httpx"
|
||||
"galo/internal/members"
|
||||
"galo/internal/notifications"
|
||||
"galo/internal/photos"
|
||||
"galo/internal/sharelinks"
|
||||
"galo/internal/social"
|
||||
"galo/internal/sse"
|
||||
"galo/internal/storage"
|
||||
"galo/internal/store"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"shared-album/internal/config"
|
||||
"shared-album/internal/storage"
|
||||
"galo/internal/config"
|
||||
"galo/internal/storage"
|
||||
)
|
||||
|
||||
func mustSigner(t *testing.T) *storage.Signer {
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/config"
|
||||
"shared-album/internal/sse"
|
||||
"shared-album/internal/storage"
|
||||
"shared-album/internal/store"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/config"
|
||||
"galo/internal/sse"
|
||||
"galo/internal/storage"
|
||||
"galo/internal/store"
|
||||
)
|
||||
|
||||
func newTestServerWithFS(t *testing.T) (*storage.FS, *gin.Engine) {
|
||||
|
||||
@@ -18,16 +18,16 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shared-album/internal/albums"
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/config"
|
||||
"shared-album/internal/members"
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/photos"
|
||||
"shared-album/internal/sharelinks"
|
||||
"shared-album/internal/sse"
|
||||
"shared-album/internal/storage"
|
||||
"shared-album/internal/store"
|
||||
"galo/internal/albums"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/config"
|
||||
"galo/internal/members"
|
||||
"galo/internal/models"
|
||||
"galo/internal/photos"
|
||||
"galo/internal/sharelinks"
|
||||
"galo/internal/sse"
|
||||
"galo/internal/storage"
|
||||
"galo/internal/store"
|
||||
)
|
||||
|
||||
func integrationPool(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"shared-album/internal/httpx"
|
||||
"galo/internal/httpx"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"shared-album/internal/httpx"
|
||||
"galo/internal/httpx"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -3,7 +3,7 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"galo/internal/models"
|
||||
)
|
||||
|
||||
// Repository is the persistence contract for the auth domain.
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/httpx"
|
||||
"shared-album/internal/models"
|
||||
"galo/internal/httpx"
|
||||
"galo/internal/models"
|
||||
)
|
||||
|
||||
type TokenPair struct {
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/sqliteutil"
|
||||
"galo/internal/models"
|
||||
"galo/internal/sqliteutil"
|
||||
)
|
||||
|
||||
// SQLiteStore is the SQLite implementation of Repository.
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"galo/internal/models"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
@@ -131,7 +131,7 @@ func Load() (*Config, error) {
|
||||
Port: pgPort,
|
||||
User: getEnv("PG_USER", "album"),
|
||||
Password: getEnv("PG_PASSWORD", ""),
|
||||
Database: getEnv("PG_DATABASE", "shared_album"),
|
||||
Database: getEnv("PG_DATABASE", "galo"),
|
||||
SSLMode: getEnv("PG_SSLMODE", "disable"),
|
||||
},
|
||||
S3: S3{
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
assets "shared-album"
|
||||
assets "galo"
|
||||
)
|
||||
|
||||
func TestMigrateSQLite(t *testing.T) {
|
||||
|
||||
@@ -12,15 +12,15 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
assets "shared-album"
|
||||
"shared-album/internal/albums"
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/db"
|
||||
"shared-album/internal/photos"
|
||||
"shared-album/internal/settings"
|
||||
"shared-album/internal/sse"
|
||||
"shared-album/internal/storage"
|
||||
"shared-album/internal/store"
|
||||
assets "galo"
|
||||
"galo/internal/albums"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/db"
|
||||
"galo/internal/photos"
|
||||
"galo/internal/settings"
|
||||
"galo/internal/sse"
|
||||
"galo/internal/storage"
|
||||
"galo/internal/store"
|
||||
)
|
||||
|
||||
func makePNG(t *testing.T) []byte {
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/httpx"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/httpx"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
|
||||
@@ -3,7 +3,7 @@ package members
|
||||
import (
|
||||
"context"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"galo/internal/models"
|
||||
)
|
||||
|
||||
// Repository is the persistence contract for the members domain.
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/httpx"
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/roles"
|
||||
"shared-album/internal/sse"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/httpx"
|
||||
"galo/internal/models"
|
||||
"galo/internal/roles"
|
||||
"galo/internal/sse"
|
||||
)
|
||||
|
||||
type AlbumReader interface {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/sqliteutil"
|
||||
"galo/internal/models"
|
||||
"galo/internal/sqliteutil"
|
||||
)
|
||||
|
||||
// SQLiteStore is the SQLite implementation of Repository.
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"galo/internal/models"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/httpx"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/httpx"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
|
||||
@@ -3,7 +3,7 @@ package notifications
|
||||
import (
|
||||
"context"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"galo/internal/models"
|
||||
)
|
||||
|
||||
// Repository is the persistence contract for the notifications domain.
|
||||
|
||||
@@ -3,10 +3,10 @@ package notifications
|
||||
import (
|
||||
"context"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/httpx"
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/sse"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/httpx"
|
||||
"galo/internal/models"
|
||||
"galo/internal/sse"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/sqliteutil"
|
||||
"galo/internal/models"
|
||||
"galo/internal/sqliteutil"
|
||||
)
|
||||
|
||||
// SQLiteStore is the SQLite implementation of Repository.
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"galo/internal/models"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/httpx"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/httpx"
|
||||
)
|
||||
|
||||
const maxUploadSize = 100 << 20
|
||||
|
||||
@@ -12,12 +12,12 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/config"
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/roles"
|
||||
"shared-album/internal/sse"
|
||||
"shared-album/internal/storage"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/config"
|
||||
"galo/internal/models"
|
||||
"galo/internal/roles"
|
||||
"galo/internal/sse"
|
||||
"galo/internal/storage"
|
||||
)
|
||||
|
||||
func testPool(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"galo/internal/auth"
|
||||
)
|
||||
|
||||
func TestObjectStateTransitions(t *testing.T) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"galo/internal/models"
|
||||
)
|
||||
|
||||
// Repository is the persistence contract for the photos domain, including the
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/httpx"
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/roles"
|
||||
"shared-album/internal/sse"
|
||||
"shared-album/internal/storage"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/httpx"
|
||||
"galo/internal/models"
|
||||
"galo/internal/roles"
|
||||
"galo/internal/sse"
|
||||
"galo/internal/storage"
|
||||
)
|
||||
|
||||
type AlbumAccess interface {
|
||||
|
||||
@@ -3,7 +3,7 @@ package photos
|
||||
import (
|
||||
"time"
|
||||
|
||||
"shared-album/internal/sqliteutil"
|
||||
"galo/internal/sqliteutil"
|
||||
)
|
||||
|
||||
func nullIfEmpty(s string) any {
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/sqliteutil"
|
||||
"galo/internal/sqliteutil"
|
||||
)
|
||||
|
||||
// CreateObject inserts a new object row in the pending state.
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/sqliteutil"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/models"
|
||||
"galo/internal/sqliteutil"
|
||||
)
|
||||
|
||||
// SQLiteStore is the SQLite implementation of Repository.
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/models"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/models"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("photo not found")
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"galo/internal/auth"
|
||||
)
|
||||
|
||||
const maxVariantAttempts = 5
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shared-album/internal/storage"
|
||||
"galo/internal/storage"
|
||||
)
|
||||
|
||||
func TestWorkerGeneratesVariantsAndCollects(t *testing.T) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/storage"
|
||||
"galo/internal/storage"
|
||||
)
|
||||
|
||||
// Result holds the two kinds of divergence between storage and the journal.
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/storage"
|
||||
"galo/internal/storage"
|
||||
)
|
||||
|
||||
func TestClassify(t *testing.T) {
|
||||
|
||||
@@ -9,15 +9,15 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/albums"
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/members"
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/notifications"
|
||||
"shared-album/internal/photos"
|
||||
"shared-album/internal/settings"
|
||||
"shared-album/internal/sharelinks"
|
||||
"shared-album/internal/social"
|
||||
"galo/internal/albums"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/members"
|
||||
"galo/internal/models"
|
||||
"galo/internal/notifications"
|
||||
"galo/internal/photos"
|
||||
"galo/internal/settings"
|
||||
"galo/internal/sharelinks"
|
||||
"galo/internal/social"
|
||||
)
|
||||
|
||||
// Repos bundles one repository per domain plus a Reset hook that clears all
|
||||
|
||||
@@ -10,16 +10,16 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
assets "shared-album"
|
||||
"shared-album/internal/albums"
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/db"
|
||||
"shared-album/internal/members"
|
||||
"shared-album/internal/notifications"
|
||||
"shared-album/internal/photos"
|
||||
"shared-album/internal/settings"
|
||||
"shared-album/internal/sharelinks"
|
||||
"shared-album/internal/social"
|
||||
assets "galo"
|
||||
"galo/internal/albums"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/db"
|
||||
"galo/internal/members"
|
||||
"galo/internal/notifications"
|
||||
"galo/internal/photos"
|
||||
"galo/internal/settings"
|
||||
"galo/internal/sharelinks"
|
||||
"galo/internal/social"
|
||||
)
|
||||
|
||||
// TestPostgresContract runs the shared suite against PostgreSQL inside its own
|
||||
|
||||
@@ -6,16 +6,16 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
assets "shared-album"
|
||||
"shared-album/internal/albums"
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/db"
|
||||
"shared-album/internal/members"
|
||||
"shared-album/internal/notifications"
|
||||
"shared-album/internal/photos"
|
||||
"shared-album/internal/settings"
|
||||
"shared-album/internal/sharelinks"
|
||||
"shared-album/internal/social"
|
||||
assets "galo"
|
||||
"galo/internal/albums"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/db"
|
||||
"galo/internal/members"
|
||||
"galo/internal/notifications"
|
||||
"galo/internal/photos"
|
||||
"galo/internal/settings"
|
||||
"galo/internal/sharelinks"
|
||||
"galo/internal/social"
|
||||
)
|
||||
|
||||
func TestSQLiteContract(t *testing.T) {
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shared-album/internal/albums"
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/members"
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/photos"
|
||||
"shared-album/internal/sharelinks"
|
||||
"galo/internal/albums"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/members"
|
||||
"galo/internal/models"
|
||||
"galo/internal/photos"
|
||||
"galo/internal/sharelinks"
|
||||
)
|
||||
|
||||
func testPool(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/sqliteutil"
|
||||
"galo/internal/sqliteutil"
|
||||
)
|
||||
|
||||
// SQLiteStore is the SQLite implementation of Repository.
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/httpx"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/httpx"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
|
||||
@@ -3,7 +3,7 @@ package sharelinks
|
||||
import (
|
||||
"context"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"galo/internal/models"
|
||||
)
|
||||
|
||||
// Repository is the persistence contract for the share-links domain.
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/httpx"
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/roles"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/httpx"
|
||||
"galo/internal/models"
|
||||
"galo/internal/roles"
|
||||
)
|
||||
|
||||
type AlbumAccess interface {
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/sqliteutil"
|
||||
"galo/internal/models"
|
||||
"galo/internal/sqliteutil"
|
||||
)
|
||||
|
||||
// SQLiteStore is the SQLite implementation of Repository.
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"galo/internal/models"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/httpx"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/httpx"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
|
||||
@@ -3,7 +3,7 @@ package social
|
||||
import (
|
||||
"context"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"galo/internal/models"
|
||||
)
|
||||
|
||||
// Repository is the persistence contract for the social domain.
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/httpx"
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/roles"
|
||||
"shared-album/internal/sse"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/httpx"
|
||||
"galo/internal/models"
|
||||
"galo/internal/roles"
|
||||
"galo/internal/sse"
|
||||
)
|
||||
|
||||
type AlbumAccess interface {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"shared-album/internal/sqliteutil"
|
||||
"galo/internal/models"
|
||||
"galo/internal/sqliteutil"
|
||||
)
|
||||
|
||||
// SQLiteStore is the SQLite implementation of Repository.
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shared-album/internal/models"
|
||||
"galo/internal/models"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"shared-album/internal/config"
|
||||
"galo/internal/config"
|
||||
)
|
||||
|
||||
func newTestS3(t *testing.T) (*S3, *Signer) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/aws/smithy-go"
|
||||
|
||||
"shared-album/internal/config"
|
||||
"galo/internal/config"
|
||||
)
|
||||
|
||||
// ErrSignedURLInvalid is returned when an app-served object URL has a missing,
|
||||
|
||||
@@ -8,14 +8,14 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shared-album/internal/albums"
|
||||
"shared-album/internal/auth"
|
||||
"shared-album/internal/members"
|
||||
"shared-album/internal/notifications"
|
||||
"shared-album/internal/photos"
|
||||
"shared-album/internal/settings"
|
||||
"shared-album/internal/sharelinks"
|
||||
"shared-album/internal/social"
|
||||
"galo/internal/albums"
|
||||
"galo/internal/auth"
|
||||
"galo/internal/members"
|
||||
"galo/internal/notifications"
|
||||
"galo/internal/photos"
|
||||
"galo/internal/settings"
|
||||
"galo/internal/sharelinks"
|
||||
"galo/internal/social"
|
||||
)
|
||||
|
||||
// Repos bundles one repository per domain.
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
assets "shared-album"
|
||||
"shared-album/internal/db"
|
||||
assets "galo"
|
||||
"galo/internal/db"
|
||||
)
|
||||
|
||||
func assertComplete(t *testing.T, r Repos) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=Shared Album backend
|
||||
Description=Galo backend
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
@@ -7,9 +7,9 @@ Wants=network-online.target
|
||||
Type=simple
|
||||
User=album
|
||||
Group=album
|
||||
WorkingDirectory=/opt/shared-album
|
||||
EnvironmentFile=/opt/shared-album/.env
|
||||
ExecStart=/opt/shared-album/shared-album
|
||||
WorkingDirectory=/opt/galo
|
||||
EnvironmentFile=/opt/galo/.env
|
||||
ExecStart=/opt/galo/galo
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
@@ -18,7 +18,7 @@ NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/opt/shared-album
|
||||
ReadWritePaths=/opt/galo
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,7 +1,7 @@
|
||||
# Пример nginx-конфигурации перед Go-бэкендом.
|
||||
# Для SSE критично proxy_buffering off, иначе события буферизуются и не доходят.
|
||||
|
||||
upstream shared_album {
|
||||
upstream galo {
|
||||
server 127.0.0.1:8080;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ server {
|
||||
client_max_body_size 60m;
|
||||
|
||||
location /api/sse/ {
|
||||
proxy_pass http://shared_album;
|
||||
proxy_pass http://galo;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_buffering off; # иначе SSE буферизуется
|
||||
@@ -22,7 +22,7 @@ server {
|
||||
}
|
||||
|
||||
location /api/objects/ {
|
||||
proxy_pass http://shared_album;
|
||||
proxy_pass http://galo;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_buffering off; # стриминг объектов без буферизации
|
||||
@@ -30,7 +30,7 @@ server {
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://shared_album;
|
||||
proxy_pass http://galo;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
Generated
+68
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "shared-album-frontend",
|
||||
"name": "galo-frontend",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "shared-album-frontend",
|
||||
"name": "galo-frontend",
|
||||
"version": "0.0.1",
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
@@ -1102,6 +1102,72 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.11.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.2",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.4",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.2",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "shared-album-frontend",
|
||||
"name": "galo-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="alternate icon" href="%sveltekit.assets%/favicon.png" type="image/png" />
|
||||
<link rel="apple-touch-icon" href="%sveltekit.assets%/apple-touch-icon.png" />
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authApi } from '$lib/api';
|
||||
import Emblem from './Emblem.svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
import ThemeSwitcher from './ThemeSwitcher.svelte';
|
||||
import { auth } from '$lib/stores/auth.svelte';
|
||||
@@ -115,7 +116,7 @@
|
||||
<Icon name="menu" class="size-4" />
|
||||
</button>
|
||||
<a href="/" class="inline-flex items-center gap-1.5 truncate text-sm font-semibold">
|
||||
<Icon name="photo_camera" class="size-4" />
|
||||
<Emblem class="text-primary size-4" />
|
||||
Galo
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
label?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { label, class: className = 'size-5' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
viewBox="0 0 64 64"
|
||||
class={className}
|
||||
role={label ? 'img' : undefined}
|
||||
aria-label={label}
|
||||
aria-hidden={label ? undefined : 'true'}
|
||||
focusable="false"
|
||||
>
|
||||
<circle cx="32" cy="32" r="13" fill="currentColor" />
|
||||
<circle
|
||||
cx="32"
|
||||
cy="32"
|
||||
r="24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="6"
|
||||
stroke-opacity="0.45"
|
||||
/>
|
||||
</svg>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import ThemeSwitcher from '$lib/components/ThemeSwitcher.svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import Emblem from '$lib/components/Emblem.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
@@ -9,7 +9,7 @@
|
||||
<header class="border-border bg-chrome border-b">
|
||||
<div class="mx-auto flex max-w-6xl items-center justify-between px-4 py-3">
|
||||
<a href="/" class="inline-flex items-center gap-2 text-lg font-semibold">
|
||||
<Icon name="photo_camera" />
|
||||
<Emblem class="text-primary size-6" />
|
||||
Galo
|
||||
</a>
|
||||
<ThemeSwitcher />
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 840 B |
@@ -0,0 +1,30 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<style>
|
||||
.bg {
|
||||
fill: #ffffff;
|
||||
}
|
||||
.disc {
|
||||
fill: #2563eb;
|
||||
}
|
||||
.ring {
|
||||
fill: none;
|
||||
stroke: #2563eb;
|
||||
stroke-opacity: 0.45;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.bg {
|
||||
fill: #171717;
|
||||
}
|
||||
.disc {
|
||||
fill: #60a5fa;
|
||||
}
|
||||
.ring {
|
||||
stroke: #60a5fa;
|
||||
stroke-opacity: 0.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<rect class="bg" width="64" height="64" rx="14" />
|
||||
<circle class="disc" cx="32" cy="32" r="13" />
|
||||
<circle class="ring" cx="32" cy="32" r="24" stroke-width="6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 562 B |
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-11
|
||||
@@ -0,0 +1,49 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
See proposal.md - Why. Current state that shapes the approach:
|
||||
|
||||
- `frontend/src/app.html:6` already points at `%sveltekit.assets%/favicon.png`, but `frontend/static/` does not exist, so the request 404s.
|
||||
- The icon system (`ui-iconography`) requires self-contained inline vector icons with no external requests, and icons that inherit the theme color.
|
||||
- Theme colors live as CSS custom properties on `:root` per theme (`--primary` = blue light/dark, orange neutral), defined in `frontend/src/app.css`.
|
||||
- Static assets flow through `frontend/static/` -> `frontend/build/` -> `backend/static/` -> `go:embed` -> served by the static handler. No backend change is needed.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- One recognizable halo mark, legible down to 16px, used consistently as UI emblem and favicon.
|
||||
- Theme-awareness in both the UI emblem and the browser/touch icon.
|
||||
- Zero external requests for any brand asset.
|
||||
|
||||
**Non-Goals:**
|
||||
- Changing the wordmark, typography, or theme palette.
|
||||
- Open Graph / social preview images, PWA manifest icons, or app-store assets.
|
||||
- Any backend or API change.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1. Concept: pure halo (variant A)
|
||||
A sun disc with a separated outer ring. Alternatives: rays/glow (rejected - detail collapses at 16px) and an aperture hybrid (rejected - two meanings compete at small sizes). Geometry keeps a clear gap between disc and ring so the halo reads as a halo.
|
||||
|
||||
### D2. Two-tone color with monochrome fallback
|
||||
In the UI, the disc uses `--primary` and the ring uses `--primary` at reduced opacity; a single-color `currentColor` fallback keeps the mark usable where the two-tone treatment does not apply. Alternative: strictly monochrome (rejected - loses the "sun inside a halo" reading).
|
||||
|
||||
### D3. Inline SVG for UI, static files for favicon
|
||||
The UI emblem is an inline SVG component so it inherits `currentColor`/theme and satisfies the no-external-requests rule, mirroring the existing icon approach. The favicon cannot use CSS custom properties, so it is a standalone asset that carries its own theme rules.
|
||||
|
||||
### D4. Favicon formats
|
||||
Ship `favicon.svg` with an embedded `prefers-color-scheme` rule (dark and light variants), plus `favicon.png` and `apple-touch-icon.png` fallbacks. `app.html` references the SVG and keeps a PNG fallback. Alternative: `.ico` only (rejected - no theme awareness).
|
||||
|
||||
### D5. Placement
|
||||
Emblem in the `AppShell` header, the public/login layout, and the share-page header, always next to the "Galo" wordmark, where it is decorative (hidden from assistive tech).
|
||||
|
||||
### D6. Asset pipeline
|
||||
Files live in `frontend/static/`; the existing `make frontend` copy step carries them into `backend/static/` for embedding, so the backend serves them unchanged.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Mark illegible at 16px] -> Keep geometry minimal (thick-enough ring, clear gap) and verify a 16px render.
|
||||
- [SVG favicon not supported everywhere] -> Provide the PNG fallback in `app.html`.
|
||||
- [Favicon cannot use theme CSS variables] -> Embed `prefers-color-scheme` rules directly in the SVG.
|
||||
- [Asset bloat] -> Keep the SVG hand-written and small; PNGs limited to the needed sizes.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
The app references a favicon that does not exist (`frontend/src/app.html:6` points at `%sveltekit.assets%/favicon.png`, but there is no `frontend/static/`), so browsers get a 404. At the same time the product name **Galo** (= гало, the ring around the sun) has no visual mark — the brand is only text. This change gives the project a self-hosted halo emblem and a working favicon, consistent with the existing icon system that forbids external requests.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Introduce a **halo emblem**: a sun disc surrounded by a separated ring (variant A — pure ring + disc, legible at 16px).
|
||||
- Add an inline, theme-aware SVG emblem component used in the UI (header, login/public layout, share header); the sun disc uses `--primary`, the ring uses `--primary` at reduced opacity with a monochrome `currentColor` fallback.
|
||||
- Add static brand assets under `frontend/static/`: `favicon.svg` (theme-aware via `prefers-color-scheme`), `favicon.png` fallback, and `apple-touch-icon.png`.
|
||||
- Update `frontend/src/app.html` to reference `favicon.svg` with the PNG fallback.
|
||||
- Keep the "Galo" wordmark next to the emblem; the emblem is decorative there.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `ui-branding`: the halo emblem and favicon identity — a self-hosted, theme-aware visual mark that appears in the UI and as the browser/tab and touch icon.
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- None: the icon system (ui-iconography) is unchanged; the emblem follows its existing no-external-requests rule. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- **Frontend:** new `frontend/static/` assets (served through the existing `go:embed` pipeline after build), `frontend/src/app.html`, `frontend/src/lib/components/AppShell.svelte`, `frontend/src/routes/(public)/+layout.svelte`, share page header.
|
||||
- **Backend:** none (static assets are embedded and served by the existing static handler).
|
||||
- **Behavior:** the missing favicon 404 is replaced by a served, theme-aware icon; no API changes.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Единая визуальная айдентика приложения: гало-эмблема и фавикон, отдаваемые самим приложением, наследующие тему и читаемые в мелком размере.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Гало-эмблема
|
||||
Интерфейс SHALL отображать эмблему «гало» — диск солнца, окружённый отделённым от него кольцом — как встроенную векторную графику, наследующую цвет текущей темы.
|
||||
|
||||
#### Scenario: Отображение в шапке
|
||||
- **WHEN** открыт основной интерфейс
|
||||
- **THEN** в шапке рядом с надписью «Galo» показана гало-эмблема
|
||||
|
||||
#### Scenario: Смена темы
|
||||
- **WHEN** пользователь переключает тему (light/dark/neutral)
|
||||
- **THEN** цвет диска и кольца эмблемы меняется вместе с темой
|
||||
|
||||
#### Scenario: Мелкий размер
|
||||
- **WHEN** эмблема отрисована в размере порядка 16-24 px
|
||||
- **THEN** диск и отделённое кольцо остаются различимыми
|
||||
|
||||
### Requirement: Фавикон от приложения
|
||||
Приложение SHALL отдавать фавикон бренда со своего origin, а разметка страницы SHALL ссылаться на существующий файл.
|
||||
|
||||
#### Scenario: Запрос фавикона
|
||||
- **WHEN** браузер запрашивает фавикон
|
||||
- **THEN** приложение отвечает изображением SVG с собственным `Content-Type`
|
||||
|
||||
#### Scenario: Запасной формат
|
||||
- **WHEN** клиент не поддерживает SVG-фавикон
|
||||
- **THEN** разметка предоставляет PNG-запасной вариант
|
||||
|
||||
### Requirement: Фавикон под тему
|
||||
Фавикон SHALL учитывать предпочтение схемы оформления и в тёмной схеме SHALL использовать светлый вариант эмблемы на тёмном фоне.
|
||||
|
||||
#### Scenario: Тёмная схема
|
||||
- **WHEN** система сообщает о предпочтении тёмной схемы
|
||||
- **THEN** фавикон отображается в варианте для тёмной схемы
|
||||
|
||||
#### Scenario: Светлая схема
|
||||
- **WHEN** система сообщает о предпочтении светлой схемы
|
||||
- **THEN** фавикон отображается в варианте для светлой схемы
|
||||
|
||||
### Requirement: Доступность эмблемы
|
||||
Эмблема SHALL быть декоративной рядом с текстовым вордмарком и SHALL иметь доступное имя, когда используется как самостоятельный смысловой элемент.
|
||||
|
||||
#### Scenario: Эмблема рядом с вордмарком
|
||||
- **WHEN** эмблема сопровождает видимый текст «Galo»
|
||||
- **THEN** она скрыта от вспомогательных технологий
|
||||
|
||||
#### Scenario: Самостоятельная эмблема
|
||||
- **WHEN** эмблема является единственным содержимым управляющего элемента
|
||||
- **THEN** элемент имеет доступное имя
|
||||
|
||||
### Requirement: Автономность ассетов бренда
|
||||
Эмблема и фавикон SHALL загружаться с самого приложения и SHALL NOT требовать запросов к внешним источникам.
|
||||
|
||||
#### Scenario: Загрузка без внешней сети
|
||||
- **WHEN** страница загружается без доступа к внешней сети, кроме своего сервера
|
||||
- **THEN** эмблема и фавикон отображаются
|
||||
@@ -0,0 +1,26 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Brand assets
|
||||
|
||||
- [x] 1.1 Create `frontend/static/favicon.svg` with the halo mark (sun disc + separated ring) and embedded `prefers-color-scheme` dark/light variants, and verify it opens as valid SVG and renders legibly at 16px
|
||||
- [x] 1.2 Create `frontend/static/favicon.png` (fallback) and `frontend/static/apple-touch-icon.png` rendered from the same emblem, and verify both files exist at their intended pixel sizes (32x32 and 180x180)
|
||||
- [x] 1.3 Update `frontend/src/app.html` to reference `favicon.svg` with the PNG as fallback, and verify `make frontend` copies both into `frontend/build/`
|
||||
|
||||
## 2. UI emblem
|
||||
|
||||
- [x] 2.1 Add an inline SVG emblem component (disc in `--primary`, ring in `--primary` at reduced opacity, monochrome `currentColor` fallback), and verify it renders at 16-24px with the ring and disc still distinct
|
||||
- [x] 2.2 Verify the emblem changes color with the theme by switching light/dark/neutral and confirming the disc and ring follow the active theme
|
||||
|
||||
## 3. Placement
|
||||
|
||||
- [x] 3.1 Show the emblem in the `AppShell` header next to the "Galo" wordmark and verify it appears in the main interface
|
||||
- [x] 3.2 Show the emblem in the public/login layout and on the share page header and verify it appears on both
|
||||
|
||||
## 4. Accessibility
|
||||
|
||||
- [x] 4.1 Verify every decorative emblem (next to the wordmark) is hidden from assistive technologies and any standalone emblem element has an accessible name
|
||||
|
||||
## 5. Integration verification
|
||||
|
||||
- [x] 5.1 Run `make build` and verify the served favicon responds as an SVG image (`curl -I` shows the SVG content type) and that no brand asset request returns 404
|
||||
- [x] 5.2 Validate the change and verify `openspec validate add-halo-branding` passes
|
||||
@@ -0,0 +1,3 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-11
|
||||
skip_specs: true
|
||||
@@ -0,0 +1,56 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
See proposal.md - Why. Current state that shapes the approach:
|
||||
|
||||
- The Go module is `shared-album` (`backend/go.mod:1`), imported as `shared-album/internal/*` by ~63 files and as `assets "shared-album"` by 9 of them (module-root `backend` package holding `go:embed`).
|
||||
- The name also appears in the binary target (`Makefile`), the systemd unit and its `/opt/shared-album` paths, the nginx upstream, the frontend package name, and the docs.
|
||||
- The real deployment `.env` already uses `PG_DATABASE=galo` and `S3_BUCKET=galo-data`; only the code/sample defaults are stale.
|
||||
- Generated artifacts (`bin/`, `frontend/build/`, `frontend/.svelte-kit/`) are gitignored and pick up the new name from the build.
|
||||
- No user-facing string literal carries the old name: the UI already renders "Galo".
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- One consistent name `galo` across Go module, build target, deploy unit, config defaults, frontend package, and docs.
|
||||
- Preserve behavior except the intended DB-name default.
|
||||
- Keep the change verifiable by the existing build/test/validate commands.
|
||||
|
||||
**Non-Goals:**
|
||||
- Renaming the immutable archived change `openspec/changes/archive/2026-10-10-shared-album`.
|
||||
- Migrating existing database contents (the default is config-driven; live `.env` is already `galo`).
|
||||
- Any UI/branding change (already "Galo").
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1. Rename the Go module to `galo`
|
||||
Flat path, matching the current style (`module shared-album`, no domain). Alternatives: keep the module name (rejected - stays inconsistent) or adopt a namespaced path like `github.com/.../galo` (rejected - no hosting/import need, larger churn).
|
||||
|
||||
Impact: replace `shared-album` in `backend/go.mod` and every import/alias across the backend, then rely on the compiler (`go build ./...`) to prove completeness.
|
||||
|
||||
### D2. Change the metadata DB-name default to `galo`
|
||||
Update `config.go` default and `.env.example`. Rationale: aligns the code with the live `.env` (`PG_DATABASE=galo`). Alternative: leave the default (rejected - keeps the stale name). Trade-off: installs that omit `PG_DATABASE` will now target `galo`; deployments should set it explicitly.
|
||||
|
||||
### D3. Rename the deploy unit and install path
|
||||
`deploy/shared-album.service` -> `galo.service`, `/opt/shared-album` -> `/opt/galo`, and the nginx upstream `shared_album` -> `galo`. This is an operational change requiring re-install on the server; it stays a single unit with the same flags and `EnvironmentFile`.
|
||||
|
||||
### D4. Leave history intact
|
||||
The archived OpenSpec change keeps its original name - it records what happened, not what the project is called now.
|
||||
|
||||
### D5. Frontend package rename
|
||||
`shared-album-frontend` -> `galo-frontend` in `package.json`; regenerate `package-lock.json` via install so the lock name matches.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Missed or mistyped import path] -> Compiler-enforced; do the module/import replacement first, then `go build ./... && go test ./...`.
|
||||
- [Deploy breaks on path/unit rename] -> Treat as a documented migration step: stage `/opt/galo`, re-install the unit, disable the old one; keep `.env` authoritative.
|
||||
- [DB-name default surprises a fresh install] -> `.env.example` and docs state it; recommend keeping `PG_DATABASE` explicit in every `.env`.
|
||||
- [Lockfile drift after package rename] -> Regenerate the lock rather than hand-editing only its `name` fields.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Land the rename in one commit: backend module + imports, Makefile, `config.go`/`.env.example`, `deploy/*`, frontend package, docs.
|
||||
2. Verify: `cd backend && go build ./... && go test ./...`, `make build`, `openspec validate rename-shared-album-to-galo`.
|
||||
3. On the server: create `/opt/galo`, place `galo` binary and `.env`, install `galo.service`, `systemctl disable --now shared-album`, then `enable --now galo`.
|
||||
4. Rollback: revert the commit and re-enable the old unit; no data migration is involved.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
The product is named **galo** — the UI already shows "Galo" and the frontend persists state under `galo.*` keys — yet internal identifiers, build artifacts, the deploy unit, and the docs still carry the old working name `shared-album`. This mismatch confuses operators and readers, and the stale metadata default `shared_album` in `config.go` diverges from the actual deployment, whose `.env` already sets `PG_DATABASE=galo`.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Rename the Go module `shared-album` -> `galo` and update every internal import path (`shared-album/internal/...`, `assets "shared-album"`) across the backend (~63 files).
|
||||
- Rename the built binary `bin/shared-album` -> `bin/galo` in the Makefile.
|
||||
- Rename the systemd unit `deploy/shared-album.service` -> `deploy/galo.service` and its install path `/opt/shared-album` -> `/opt/galo`.
|
||||
- Change the metadata-layer DB name default `shared_album` -> `galo` in `config.go` and `.env.example`, aligning the default with the real deployment.
|
||||
- Rename the nginx upstream `shared_album` -> `galo`.
|
||||
- Rename the frontend package `shared-album-frontend` -> `galo-frontend` (package.json + regenerated lock).
|
||||
- **BREAKING** (operational): the deploy unit name and install path change, so an existing server needs the unit re-installed and the path re-created.
|
||||
- Update docs (`project.md`, including the §18 prompt log, and `README.md`) to the new name.
|
||||
- Leave immutable history untouched: the archived change `openspec/changes/archive/2026-10-10-shared-album` keeps its name.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
<!-- None: this is a pure identifier/refactor rename with no new behavior. -->
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- None: no spec-level behavior changes. The change's .openspec.yaml must set skip_specs: true. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- **Backend:** `backend/go.mod` module path + all `shared-album/internal/...` imports (~63 Go files); `internal/config/config.go` DB default.
|
||||
- **Build/deploy:** `Makefile`, `deploy/shared-album.service` (renamed), `deploy/nginx-sse.conf`, `.env.example`.
|
||||
- **Frontend:** `frontend/package.json` + `package-lock.json` (name only).
|
||||
- **Docs:** `project.md`, `README.md`.
|
||||
- **Behavior:** none at the API level; the only runtime-visible change is the default `PG_DATABASE` (affects fresh installs that omit the variable — the current `.env` already uses `galo`). Operational change: systemd unit/path rename.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Go module and imports
|
||||
|
||||
- [x] 1.1 Rename the module in `backend/go.mod` from `shared-album` to `galo` and verify `head -1 backend/go.mod` prints `module galo`
|
||||
- [x] 1.2 Replace every `shared-album/internal/...` import and the `assets "shared-album"` alias across the backend (~63 files) and verify `grep -rn '"shared-album' backend --include='*.go'` returns nothing
|
||||
- [x] 1.3 Build and test the backend after the rename and verify `cd backend && go build ./... && go test ./...` passes
|
||||
|
||||
## 2. Build target
|
||||
|
||||
- [x] 2.1 Rename the binary output in the Makefile (`bin/shared-album` -> `bin/galo`) in both the `backend` and `run` targets and verify `make backend && file bin/galo` reports a statically linked `galo` binary
|
||||
|
||||
## 3. Config defaults
|
||||
|
||||
- [x] 3.1 Change the `PG_DATABASE` default in `backend/internal/config/config.go` and in `.env.example` from `shared_album` to `galo` and verify `grep -rn shared_album backend/internal/config .env.example` returns nothing while `cd backend && go test ./internal/config/` passes
|
||||
|
||||
## 4. Deploy artifacts
|
||||
|
||||
- [x] 4.1 Rename `deploy/shared-album.service` to `deploy/galo.service` and update its `WorkingDirectory`, `EnvironmentFile`, `ExecStart`, and `ReadWritePaths` paths from `/opt/shared-album` to `/opt/galo`, then verify `grep -rn shared-album deploy/` returns nothing
|
||||
- [x] 4.2 Rename the nginx upstream `shared_album` to `galo` in `deploy/nginx-sse.conf` (upstream block and all `proxy_pass`) and verify `grep -rn 'shared_album\|shared-album' deploy/` returns nothing
|
||||
|
||||
## 5. Frontend package
|
||||
|
||||
- [x] 5.1 Rename the package to `galo-frontend` in `frontend/package.json` and regenerate `frontend/package-lock.json`, then verify `grep -n '"name"' frontend/package.json frontend/package-lock.json` shows `galo-frontend` everywhere
|
||||
|
||||
## 6. Documentation
|
||||
|
||||
- [x] 6.1 Update `README.md` and `project.md` (including the §18 step-by-step prompt log) to use `galo` / `/opt/galo` / `galo.service`, and verify `grep -rn shared-album README.md project.md` returns nothing
|
||||
- [x] 6.2 Confirm the archived change is left untouched and verify `git status --porcelain openspec/changes/archive/` prints nothing
|
||||
|
||||
## 7. Integration verification
|
||||
|
||||
- [x] 7.1 Run a repo-wide audit and verify `grep -rn 'shared-album\|shared_album\|shared-album-frontend' --exclude-dir=.git --exclude-dir=bin --exclude-dir=node_modules --exclude-dir=build --exclude-dir=.svelte-kit .` returns hits only under `openspec/changes/archive/2026-10-10-shared-album/` and this change's own directory `openspec/changes/rename-shared-album-to-galo/`
|
||||
- [x] 7.2 Run the full pipeline and verify `make build && make test` succeeds on the renamed codebase
|
||||
- [x] 7.3 Validate the change and verify `openspec validate rename-shared-album-to-galo` passes
|
||||
+17
-17
@@ -66,7 +66,7 @@
|
||||
### Сборка backend
|
||||
```bash
|
||||
cd backend
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o ../bin/shared-album ./cmd/server
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o ../bin/galo ./cmd/server
|
||||
```
|
||||
|
||||
### Сборка frontend
|
||||
@@ -84,10 +84,10 @@ cp -r build/* ../backend/static/
|
||||
3. Собрать backend (`go build`) — статика вшивается в бинарник
|
||||
4. Положить бинарник на сервер и запустить через systemd
|
||||
|
||||
### systemd unit `/etc/systemd/system/shared-album.service`
|
||||
### systemd unit `/etc/systemd/system/galo.service`
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Shared Album backend
|
||||
Description=Galo backend
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
@@ -95,9 +95,9 @@ Wants=network-online.target
|
||||
Type=simple
|
||||
User=album
|
||||
Group=album
|
||||
WorkingDirectory=/opt/shared-album
|
||||
EnvironmentFile=/opt/shared-album/.env
|
||||
ExecStart=/opt/shared-album/shared-album
|
||||
WorkingDirectory=/opt/galo
|
||||
EnvironmentFile=/opt/galo/.env
|
||||
ExecStart=/opt/galo/galo
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
@@ -106,7 +106,7 @@ NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/opt/shared-album
|
||||
ReadWritePaths=/opt/galo
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -115,8 +115,8 @@ WantedBy=multi-user.target
|
||||
Активация:
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now shared-album
|
||||
sudo journalctl -u shared-album -f
|
||||
sudo systemctl enable --now galo
|
||||
sudo journalctl -u galo -f
|
||||
```
|
||||
|
||||
### Прокси (опционально)
|
||||
@@ -141,7 +141,7 @@ PG_HOST=pg.localnet
|
||||
PG_PORT=5432
|
||||
PG_USER=album
|
||||
PG_PASSWORD=change_me
|
||||
PG_DATABASE=shared_album
|
||||
PG_DATABASE=galo
|
||||
PG_SSLMODE=disable
|
||||
|
||||
# Metadata backend
|
||||
@@ -480,7 +480,7 @@ X-Accel-Buffering: no
|
||||
## 13. Структура проекта
|
||||
|
||||
```
|
||||
shared-album/
|
||||
galo/
|
||||
├── backend/
|
||||
│ ├── cmd/server/main.go
|
||||
│ ├── internal/
|
||||
@@ -527,7 +527,7 @@ shared-album/
|
||||
│ ├── vite.config.ts
|
||||
│ └── package.json
|
||||
├── deploy/
|
||||
│ ├── shared-album.service # systemd unit
|
||||
│ ├── galo.service # systemd unit
|
||||
│ └── nginx-sse.conf # пример прокси для SSE
|
||||
├── tailwind.config.ts
|
||||
├── .env.example
|
||||
@@ -547,12 +547,12 @@ frontend:
|
||||
|
||||
backend:
|
||||
cd backend && CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" \
|
||||
-o ../bin/shared-album ./cmd/server
|
||||
-o ../bin/galo ./cmd/server
|
||||
|
||||
build: frontend backend
|
||||
|
||||
run: backend
|
||||
./bin/shared-album
|
||||
./bin/galo
|
||||
|
||||
migrate:
|
||||
cd backend && migrate -path migrations -database \
|
||||
@@ -605,7 +605,7 @@ migrate:
|
||||
## 18. Пошаговые промпты для итеративной генерации
|
||||
|
||||
### Шаг 1 — Скелет и подключения
|
||||
Создай структуру проекта shared-album: backend (Go + Gin) и frontend
|
||||
Создай структуру проекта galo: backend (Go + Gin) и frontend
|
||||
(SvelteKit 2 + Svelte 5 + adapter-static + Tailwind).
|
||||
Без Docker — нативная сборка Go-бинарника, статика вшивается через go:embed.
|
||||
Используй существующие pg.localnet и Garage на s3.localnet.
|
||||
@@ -614,7 +614,7 @@ config-пакет на Go, подключение к pg.localnet через pgx
|
||||
клиент S3 к Garage (AWS SDK v2, region=garage, UsePathStyle=true),
|
||||
health-check /api/health (проверяет БД и S3).
|
||||
Добавь Makefile: build (frontend → static → backend), run, migrate.
|
||||
Добавь systemd unit deploy/shared-album.service.
|
||||
Добавь systemd unit deploy/galo.service.
|
||||
|
||||
### Шаг 2 — Миграции
|
||||
Добавь golang-migrate и миграции для таблиц: users, albums, album_members,
|
||||
@@ -666,7 +666,7 @@ ThemeSwitcher.svelte — 4 кнопки с активным состоянием
|
||||
### Шаг 8 — Сборка и деплой без Docker
|
||||
Настрой Makefile: frontend собирается в статику, копируется в backend/static,
|
||||
backend собирается с go:embed. Финальный артефакт — один бинарник.
|
||||
Добавь systemd unit в deploy/shared-album.service с EnvironmentFile=.env
|
||||
Добавь systemd unit в deploy/galo.service с EnvironmentFile=.env
|
||||
и базовым харденингом (NoNewPrivileges, ProtectSystem, PrivateTmp).
|
||||
Добавь пример nginx-конфига с proxy_buffering off для SSE.
|
||||
Напиши README: как собрать, как положить бинарник, как запустить через systemd.
|
||||
Reference in New Issue
Block a user