diff --git a/.env.example b/.env.example index 8249555..44465e8 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/Makefile b/Makefile index 28e72cc..2e8ee55 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index b28ad37..a0ba69d 100644 --- a/README.md +++ b/README.md @@ -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 (опционально) diff --git a/backend/cmd/backfill-exif/main.go b/backend/cmd/backfill-exif/main.go index 3a6137a..9b47da1 100644 --- a/backend/cmd/backfill-exif/main.go +++ b/backend/cmd/backfill-exif/main.go @@ -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() { diff --git a/backend/cmd/migrate/main.go b/backend/cmd/migrate/main.go index 7576c91..94808e1 100644 --- a/backend/cmd/migrate/main.go +++ b/backend/cmd/migrate/main.go @@ -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() { diff --git a/backend/cmd/reconcile/main.go b/backend/cmd/reconcile/main.go index 04897ab..8ce9dfd 100644 --- a/backend/cmd/reconcile/main.go +++ b/backend/cmd/reconcile/main.go @@ -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/" diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 0a72ec5..efedea3 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -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 { diff --git a/backend/go.mod b/backend/go.mod index 4ca3177..d07f934 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,4 +1,4 @@ -module shared-album +module galo go 1.26.1 diff --git a/backend/internal/albums/handlers.go b/backend/internal/albums/handlers.go index 2b00705..f7cc41b 100644 --- a/backend/internal/albums/handlers.go +++ b/backend/internal/albums/handlers.go @@ -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 { diff --git a/backend/internal/albums/repository.go b/backend/internal/albums/repository.go index e22c98c..3b1ee18 100644 --- a/backend/internal/albums/repository.go +++ b/backend/internal/albums/repository.go @@ -3,7 +3,7 @@ package albums import ( "context" - "shared-album/internal/models" + "galo/internal/models" ) // Repository is the persistence contract for the albums domain. diff --git a/backend/internal/albums/service.go b/backend/internal/albums/service.go index 57ac51b..46883d0 100644 --- a/backend/internal/albums/service.go +++ b/backend/internal/albums/service.go @@ -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 { diff --git a/backend/internal/albums/sqlite_store.go b/backend/internal/albums/sqlite_store.go index c8bf624..22d1654 100644 --- a/backend/internal/albums/sqlite_store.go +++ b/backend/internal/albums/sqlite_store.go @@ -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. diff --git a/backend/internal/albums/store.go b/backend/internal/albums/store.go index 2ceaf5c..40539b2 100644 --- a/backend/internal/albums/store.go +++ b/backend/internal/albums/store.go @@ -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") diff --git a/backend/internal/app/app.go b/backend/internal/app/app.go index b775fc1..3379c4d 100644 --- a/backend/internal/app/app.go +++ b/backend/internal/app/app.go @@ -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 { diff --git a/backend/internal/app/health_test.go b/backend/internal/app/health_test.go index 4b934a5..b9588e4 100644 --- a/backend/internal/app/health_test.go +++ b/backend/internal/app/health_test.go @@ -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 { diff --git a/backend/internal/app/objects_test.go b/backend/internal/app/objects_test.go index fb94e8f..4455452 100644 --- a/backend/internal/app/objects_test.go +++ b/backend/internal/app/objects_test.go @@ -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) { diff --git a/backend/internal/app/share_flow_test.go b/backend/internal/app/share_flow_test.go index 8548ad9..5f17255 100644 --- a/backend/internal/app/share_flow_test.go +++ b/backend/internal/app/share_flow_test.go @@ -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 { diff --git a/backend/internal/auth/handlers.go b/backend/internal/auth/handlers.go index 352ec41..410704f 100644 --- a/backend/internal/auth/handlers.go +++ b/backend/internal/auth/handlers.go @@ -8,7 +8,7 @@ import ( "github.com/gin-gonic/gin" - "shared-album/internal/httpx" + "galo/internal/httpx" ) type Handler struct { diff --git a/backend/internal/auth/middleware.go b/backend/internal/auth/middleware.go index 8c865cb..066ce99 100644 --- a/backend/internal/auth/middleware.go +++ b/backend/internal/auth/middleware.go @@ -5,7 +5,7 @@ import ( "github.com/gin-gonic/gin" - "shared-album/internal/httpx" + "galo/internal/httpx" ) const ( diff --git a/backend/internal/auth/repository.go b/backend/internal/auth/repository.go index dcd75bc..5a5820a 100644 --- a/backend/internal/auth/repository.go +++ b/backend/internal/auth/repository.go @@ -3,7 +3,7 @@ package auth import ( "context" - "shared-album/internal/models" + "galo/internal/models" ) // Repository is the persistence contract for the auth domain. diff --git a/backend/internal/auth/service.go b/backend/internal/auth/service.go index 0453f79..bcd808d 100644 --- a/backend/internal/auth/service.go +++ b/backend/internal/auth/service.go @@ -6,8 +6,8 @@ import ( "strings" "time" - "shared-album/internal/httpx" - "shared-album/internal/models" + "galo/internal/httpx" + "galo/internal/models" ) type TokenPair struct { diff --git a/backend/internal/auth/sqlite_store.go b/backend/internal/auth/sqlite_store.go index 07b5312..b0022bb 100644 --- a/backend/internal/auth/sqlite_store.go +++ b/backend/internal/auth/sqlite_store.go @@ -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. diff --git a/backend/internal/auth/store.go b/backend/internal/auth/store.go index 054375c..c82db6d 100644 --- a/backend/internal/auth/store.go +++ b/backend/internal/auth/store.go @@ -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") diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index ef51c83..ca15eb9 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -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{ diff --git a/backend/internal/db/migrate_sqlite_test.go b/backend/internal/db/migrate_sqlite_test.go index c6387d9..3c0c48d 100644 --- a/backend/internal/db/migrate_sqlite_test.go +++ b/backend/internal/db/migrate_sqlite_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - assets "shared-album" + assets "galo" ) func TestMigrateSQLite(t *testing.T) { diff --git a/backend/internal/e2e/sqlite_test.go b/backend/internal/e2e/sqlite_test.go index 29e9046..91e2c0e 100644 --- a/backend/internal/e2e/sqlite_test.go +++ b/backend/internal/e2e/sqlite_test.go @@ -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 { diff --git a/backend/internal/members/handlers.go b/backend/internal/members/handlers.go index b8c47ac..db1308d 100644 --- a/backend/internal/members/handlers.go +++ b/backend/internal/members/handlers.go @@ -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 { diff --git a/backend/internal/members/repository.go b/backend/internal/members/repository.go index b700c99..69b95ec 100644 --- a/backend/internal/members/repository.go +++ b/backend/internal/members/repository.go @@ -3,7 +3,7 @@ package members import ( "context" - "shared-album/internal/models" + "galo/internal/models" ) // Repository is the persistence contract for the members domain. diff --git a/backend/internal/members/service.go b/backend/internal/members/service.go index 361725f..5d4cb92 100644 --- a/backend/internal/members/service.go +++ b/backend/internal/members/service.go @@ -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 { diff --git a/backend/internal/members/sqlite_store.go b/backend/internal/members/sqlite_store.go index 76771df..76e9709 100644 --- a/backend/internal/members/sqlite_store.go +++ b/backend/internal/members/sqlite_store.go @@ -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. diff --git a/backend/internal/members/store.go b/backend/internal/members/store.go index 2786476..970f06c 100644 --- a/backend/internal/members/store.go +++ b/backend/internal/members/store.go @@ -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") diff --git a/backend/internal/notifications/handlers.go b/backend/internal/notifications/handlers.go index 4a6c547..6021536 100644 --- a/backend/internal/notifications/handlers.go +++ b/backend/internal/notifications/handlers.go @@ -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 { diff --git a/backend/internal/notifications/repository.go b/backend/internal/notifications/repository.go index c69d9db..e4889ca 100644 --- a/backend/internal/notifications/repository.go +++ b/backend/internal/notifications/repository.go @@ -3,7 +3,7 @@ package notifications import ( "context" - "shared-album/internal/models" + "galo/internal/models" ) // Repository is the persistence contract for the notifications domain. diff --git a/backend/internal/notifications/service.go b/backend/internal/notifications/service.go index 1abf9b2..54af797 100644 --- a/backend/internal/notifications/service.go +++ b/backend/internal/notifications/service.go @@ -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 { diff --git a/backend/internal/notifications/sqlite_store.go b/backend/internal/notifications/sqlite_store.go index 061bdb9..4798bfb 100644 --- a/backend/internal/notifications/sqlite_store.go +++ b/backend/internal/notifications/sqlite_store.go @@ -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. diff --git a/backend/internal/notifications/store.go b/backend/internal/notifications/store.go index 9de08b1..e66bebc 100644 --- a/backend/internal/notifications/store.go +++ b/backend/internal/notifications/store.go @@ -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") diff --git a/backend/internal/photos/handlers.go b/backend/internal/photos/handlers.go index c207078..3a75acc 100644 --- a/backend/internal/photos/handlers.go +++ b/backend/internal/photos/handlers.go @@ -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 diff --git a/backend/internal/photos/helpers_test.go b/backend/internal/photos/helpers_test.go index 0659663..f68c825 100644 --- a/backend/internal/photos/helpers_test.go +++ b/backend/internal/photos/helpers_test.go @@ -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 { diff --git a/backend/internal/photos/objects_test.go b/backend/internal/photos/objects_test.go index 18fbf78..2d78409 100644 --- a/backend/internal/photos/objects_test.go +++ b/backend/internal/photos/objects_test.go @@ -8,7 +8,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" - "shared-album/internal/auth" + "galo/internal/auth" ) func TestObjectStateTransitions(t *testing.T) { diff --git a/backend/internal/photos/repository.go b/backend/internal/photos/repository.go index 48d56e2..f352dc2 100644 --- a/backend/internal/photos/repository.go +++ b/backend/internal/photos/repository.go @@ -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 diff --git a/backend/internal/photos/service.go b/backend/internal/photos/service.go index d5191d8..c128652 100644 --- a/backend/internal/photos/service.go +++ b/backend/internal/photos/service.go @@ -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 { diff --git a/backend/internal/photos/sqlite_helpers.go b/backend/internal/photos/sqlite_helpers.go index 02190fc..3797ffb 100644 --- a/backend/internal/photos/sqlite_helpers.go +++ b/backend/internal/photos/sqlite_helpers.go @@ -3,7 +3,7 @@ package photos import ( "time" - "shared-album/internal/sqliteutil" + "galo/internal/sqliteutil" ) func nullIfEmpty(s string) any { diff --git a/backend/internal/photos/sqlite_objects.go b/backend/internal/photos/sqlite_objects.go index 54b1722..9fa6da3 100644 --- a/backend/internal/photos/sqlite_objects.go +++ b/backend/internal/photos/sqlite_objects.go @@ -6,7 +6,7 @@ import ( "errors" "time" - "shared-album/internal/sqliteutil" + "galo/internal/sqliteutil" ) // CreateObject inserts a new object row in the pending state. diff --git a/backend/internal/photos/sqlite_store.go b/backend/internal/photos/sqlite_store.go index 1af97bd..c943fe1 100644 --- a/backend/internal/photos/sqlite_store.go +++ b/backend/internal/photos/sqlite_store.go @@ -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. diff --git a/backend/internal/photos/store.go b/backend/internal/photos/store.go index ff59950..66ff25b 100644 --- a/backend/internal/photos/store.go +++ b/backend/internal/photos/store.go @@ -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") diff --git a/backend/internal/photos/worker.go b/backend/internal/photos/worker.go index 9f1352d..80d76b0 100644 --- a/backend/internal/photos/worker.go +++ b/backend/internal/photos/worker.go @@ -7,7 +7,7 @@ import ( "log/slog" "time" - "shared-album/internal/auth" + "galo/internal/auth" ) const maxVariantAttempts = 5 diff --git a/backend/internal/photos/worker_test.go b/backend/internal/photos/worker_test.go index 3ba70e7..5b7f165 100644 --- a/backend/internal/photos/worker_test.go +++ b/backend/internal/photos/worker_test.go @@ -7,7 +7,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" - "shared-album/internal/storage" + "galo/internal/storage" ) func TestWorkerGeneratesVariantsAndCollects(t *testing.T) { diff --git a/backend/internal/reconcile/reconcile.go b/backend/internal/reconcile/reconcile.go index 2782edf..56f3bc6 100644 --- a/backend/internal/reconcile/reconcile.go +++ b/backend/internal/reconcile/reconcile.go @@ -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. diff --git a/backend/internal/reconcile/reconcile_test.go b/backend/internal/reconcile/reconcile_test.go index a06c9b8..b9c9fe5 100644 --- a/backend/internal/reconcile/reconcile_test.go +++ b/backend/internal/reconcile/reconcile_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "shared-album/internal/storage" + "galo/internal/storage" ) func TestClassify(t *testing.T) { diff --git a/backend/internal/repo/contract/contract.go b/backend/internal/repo/contract/contract.go index c1fa1c5..25cd9b8 100644 --- a/backend/internal/repo/contract/contract.go +++ b/backend/internal/repo/contract/contract.go @@ -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 diff --git a/backend/internal/repo/contract/postgres_test.go b/backend/internal/repo/contract/postgres_test.go index c8df098..652c2fa 100644 --- a/backend/internal/repo/contract/postgres_test.go +++ b/backend/internal/repo/contract/postgres_test.go @@ -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 diff --git a/backend/internal/repo/contract/sqlite_test.go b/backend/internal/repo/contract/sqlite_test.go index 0b52489..1873d69 100644 --- a/backend/internal/repo/contract/sqlite_test.go +++ b/backend/internal/repo/contract/sqlite_test.go @@ -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) { diff --git a/backend/internal/repo/repo_test.go b/backend/internal/repo/repo_test.go index df56c88..eafb44a 100644 --- a/backend/internal/repo/repo_test.go +++ b/backend/internal/repo/repo_test.go @@ -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 { diff --git a/backend/internal/settings/sqlite_store.go b/backend/internal/settings/sqlite_store.go index 61970d6..253658c 100644 --- a/backend/internal/settings/sqlite_store.go +++ b/backend/internal/settings/sqlite_store.go @@ -7,7 +7,7 @@ import ( "fmt" "time" - "shared-album/internal/sqliteutil" + "galo/internal/sqliteutil" ) // SQLiteStore is the SQLite implementation of Repository. diff --git a/backend/internal/sharelinks/handlers.go b/backend/internal/sharelinks/handlers.go index 7abfce4..7ddeda0 100644 --- a/backend/internal/sharelinks/handlers.go +++ b/backend/internal/sharelinks/handlers.go @@ -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 { diff --git a/backend/internal/sharelinks/repository.go b/backend/internal/sharelinks/repository.go index 54b7702..a752d52 100644 --- a/backend/internal/sharelinks/repository.go +++ b/backend/internal/sharelinks/repository.go @@ -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. diff --git a/backend/internal/sharelinks/service.go b/backend/internal/sharelinks/service.go index 13db847..7c00645 100644 --- a/backend/internal/sharelinks/service.go +++ b/backend/internal/sharelinks/service.go @@ -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 { diff --git a/backend/internal/sharelinks/sqlite_store.go b/backend/internal/sharelinks/sqlite_store.go index 15a04df..7c88262 100644 --- a/backend/internal/sharelinks/sqlite_store.go +++ b/backend/internal/sharelinks/sqlite_store.go @@ -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. diff --git a/backend/internal/sharelinks/store.go b/backend/internal/sharelinks/store.go index 244d5ee..b802e78 100644 --- a/backend/internal/sharelinks/store.go +++ b/backend/internal/sharelinks/store.go @@ -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") diff --git a/backend/internal/social/handlers.go b/backend/internal/social/handlers.go index 8520202..711d0fc 100644 --- a/backend/internal/social/handlers.go +++ b/backend/internal/social/handlers.go @@ -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 { diff --git a/backend/internal/social/repository.go b/backend/internal/social/repository.go index d1d48f7..853212f 100644 --- a/backend/internal/social/repository.go +++ b/backend/internal/social/repository.go @@ -3,7 +3,7 @@ package social import ( "context" - "shared-album/internal/models" + "galo/internal/models" ) // Repository is the persistence contract for the social domain. diff --git a/backend/internal/social/service.go b/backend/internal/social/service.go index 3e7a0cd..0d54e94 100644 --- a/backend/internal/social/service.go +++ b/backend/internal/social/service.go @@ -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 { diff --git a/backend/internal/social/sqlite_store.go b/backend/internal/social/sqlite_store.go index 26ba01f..5d40f01 100644 --- a/backend/internal/social/sqlite_store.go +++ b/backend/internal/social/sqlite_store.go @@ -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. diff --git a/backend/internal/social/store.go b/backend/internal/social/store.go index a2852bb..c43ee4d 100644 --- a/backend/internal/social/store.go +++ b/backend/internal/social/store.go @@ -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") diff --git a/backend/internal/storage/s3_test.go b/backend/internal/storage/s3_test.go index 6a7f777..95812db 100644 --- a/backend/internal/storage/s3_test.go +++ b/backend/internal/storage/s3_test.go @@ -13,7 +13,7 @@ import ( "testing" "time" - "shared-album/internal/config" + "galo/internal/config" ) func newTestS3(t *testing.T) (*S3, *Signer) { diff --git a/backend/internal/storage/storage.go b/backend/internal/storage/storage.go index 7303f7c..6e938bc 100644 --- a/backend/internal/storage/storage.go +++ b/backend/internal/storage/storage.go @@ -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, diff --git a/backend/internal/store/store.go b/backend/internal/store/store.go index 54eda95..a5b694f 100644 --- a/backend/internal/store/store.go +++ b/backend/internal/store/store.go @@ -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. diff --git a/backend/internal/store/store_test.go b/backend/internal/store/store_test.go index 6961d97..7726a07 100644 --- a/backend/internal/store/store_test.go +++ b/backend/internal/store/store_test.go @@ -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) { diff --git a/deploy/shared-album.service b/deploy/galo.service similarity index 60% rename from deploy/shared-album.service rename to deploy/galo.service index 2726c30..f482426 100644 --- a/deploy/shared-album.service +++ b/deploy/galo.service @@ -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 diff --git a/deploy/nginx-sse.conf b/deploy/nginx-sse.conf index 19e08ed..34b31a5 100644 --- a/deploy/nginx-sse.conf +++ b/deploy/nginx-sse.conf @@ -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; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 409c4db..e8ea332 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 3967b9f..8978ef4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "shared-album-frontend", + "name": "galo-frontend", "private": true, "version": "0.0.1", "type": "module", diff --git a/frontend/src/app.html b/frontend/src/app.html index e616e5c..06cf16b 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -3,7 +3,9 @@ - + + + + + + + + diff --git a/frontend/src/routes/(public)/+layout.svelte b/frontend/src/routes/(public)/+layout.svelte index a14569d..9e3f3b5 100644 --- a/frontend/src/routes/(public)/+layout.svelte +++ b/frontend/src/routes/(public)/+layout.svelte @@ -1,6 +1,6 @@ @@ -9,7 +9,7 @@
- + Galo diff --git a/frontend/static/apple-touch-icon.png b/frontend/static/apple-touch-icon.png new file mode 100644 index 0000000..7b8b393 Binary files /dev/null and b/frontend/static/apple-touch-icon.png differ diff --git a/frontend/static/favicon.png b/frontend/static/favicon.png new file mode 100644 index 0000000..e138800 Binary files /dev/null and b/frontend/static/favicon.png differ diff --git a/frontend/static/favicon.svg b/frontend/static/favicon.svg new file mode 100644 index 0000000..7edc042 --- /dev/null +++ b/frontend/static/favicon.svg @@ -0,0 +1,30 @@ + + + + + + diff --git a/openspec/changes/add-halo-branding/.openspec.yaml b/openspec/changes/add-halo-branding/.openspec.yaml new file mode 100644 index 0000000..a369219 --- /dev/null +++ b/openspec/changes/add-halo-branding/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-11 diff --git a/openspec/changes/add-halo-branding/design.md b/openspec/changes/add-halo-branding/design.md new file mode 100644 index 0000000..e3d940b --- /dev/null +++ b/openspec/changes/add-halo-branding/design.md @@ -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. diff --git a/openspec/changes/add-halo-branding/proposal.md b/openspec/changes/add-halo-branding/proposal.md new file mode 100644 index 0000000..34e74a6 --- /dev/null +++ b/openspec/changes/add-halo-branding/proposal.md @@ -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 + + +## 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. diff --git a/openspec/changes/add-halo-branding/specs/ui-branding/spec.md b/openspec/changes/add-halo-branding/specs/ui-branding/spec.md new file mode 100644 index 0000000..6b46336 --- /dev/null +++ b/openspec/changes/add-halo-branding/specs/ui-branding/spec.md @@ -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** эмблема и фавикон отображаются diff --git a/openspec/changes/add-halo-branding/tasks.md b/openspec/changes/add-halo-branding/tasks.md new file mode 100644 index 0000000..1b66661 --- /dev/null +++ b/openspec/changes/add-halo-branding/tasks.md @@ -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 diff --git a/openspec/changes/rename-shared-album-to-galo/.openspec.yaml b/openspec/changes/rename-shared-album-to-galo/.openspec.yaml new file mode 100644 index 0000000..8f39911 --- /dev/null +++ b/openspec/changes/rename-shared-album-to-galo/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +created: 2026-10-11 +skip_specs: true diff --git a/openspec/changes/rename-shared-album-to-galo/design.md b/openspec/changes/rename-shared-album-to-galo/design.md new file mode 100644 index 0000000..64384da --- /dev/null +++ b/openspec/changes/rename-shared-album-to-galo/design.md @@ -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. diff --git a/openspec/changes/rename-shared-album-to-galo/proposal.md b/openspec/changes/rename-shared-album-to-galo/proposal.md new file mode 100644 index 0000000..159de7f --- /dev/null +++ b/openspec/changes/rename-shared-album-to-galo/proposal.md @@ -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 + + +### Modified Capabilities + + +## 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. diff --git a/openspec/changes/rename-shared-album-to-galo/tasks.md b/openspec/changes/rename-shared-album-to-galo/tasks.md new file mode 100644 index 0000000..acb0e1b --- /dev/null +++ b/openspec/changes/rename-shared-album-to-galo/tasks.md @@ -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 diff --git a/project.md b/project.md index dd993d0..e875b4b 100644 --- a/project.md +++ b/project.md @@ -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. \ No newline at end of file