fix(setup): reduce friction in installation procedure

- Add Language default ("jp") so missing field no longer produces empty
  string, and include language selector in setup wizard
- Add --setup flag to re-run wizard even when config.json exists,
  providing a recovery path for corrupted configs
- Auto-apply seed data on fresh databases so users who skip the wizard
  still get shops, events, and gacha
- Fix stale docs referencing non-existent init/setup.sh and
  schemas/patch-schema/ in docker/README, CONTRIBUTING, and README
This commit is contained in:
Houmgaor
2026-02-23 23:39:49 +01:00
parent 210cfa1fd1
commit bcdc4e0b7e
7 changed files with 67 additions and 39 deletions

View File

@@ -153,25 +153,18 @@ func TestYourFunction(t *testing.T) {
## Database Schema Changes
### Patch Schemas (Development)
Erupe uses an embedded auto-migrating schema system in `server/migrations/`.
When actively developing new features that require schema changes:
When adding schema changes:
1. Create a new file in `schemas/patch-schema/` with format: `NN_description.sql`
2. Increment the number from the last patch
3. Test the migration on a clean database
4. Document what the patch does in comments
1. Create a new file in `server/migrations/sql/` with format: `NNNN_description.sql` (e.g. `0002_add_new_table.sql`)
2. Increment the number from the last migration
3. Test the migration on both a fresh and existing database
4. Document what the migration does in SQL comments
**Important**: Patch schemas are temporary and may change during development.
Migrations run automatically on startup in order. Each runs in its own transaction and is tracked in the `schema_version` table.
### Update Schemas (Production)
For release-ready schema changes:
1. Consolidate patch schemas into update schemas
2. Create a new file in appropriate schema directory
3. Update schema version tracking
4. Test migration paths from previous versions
For seed/demo data (shops, events, gacha), add files to `server/migrations/seed/`. Seed data is applied automatically on fresh databases and can be re-applied via the setup wizard.
## Documentation Requirements

View File

@@ -197,14 +197,10 @@ Multiple channel servers can run simultaneously, organized by world types: Newbi
## Database Schemas
Erupe uses a structured schema system:
Erupe uses an embedded auto-migrating schema system. Migrations in [server/migrations/sql/](./server/migrations/sql/) are applied automatically on startup — no manual SQL steps needed.
- **Initialization Schema**: Bootstraps database to version 9.1.0
- **Update Schemas**: Production-ready updates for new releases
- **Patch Schemas**: Development updates (subject to change)
- **Seed Data**: Demo templates for shops, distributions, events, and gacha in [server/migrations/seed/](./server/migrations/seed/)
**Note**: Only use patch schemas if you're following active development. They get consolidated into update schemas on release.
- **Migrations**: Numbered SQL files (`0001_init.sql`, `0002_*.sql`, ...) tracked in a `schema_version` table
- **Seed Data**: Demo templates for shops, distributions, events, and gacha in [server/migrations/seed/](./server/migrations/seed/) — applied automatically on fresh databases
## Development
@@ -237,7 +233,7 @@ go test -v -race ./... # Check for race conditions (mandatory before merging
### Database schema errors
- Ensure all patch files are applied in order
- Schema migrations run automatically on startup — check the server logs for migration errors
- Check PostgreSQL logs for detailed error messages
- Verify database user has sufficient privileges

View File

@@ -342,6 +342,7 @@ func getOutboundIP4() (net.IP, error) {
// config.json (just database credentials) produces a fully working server.
func registerDefaults() {
// Top-level settings
viper.SetDefault("Language", "jp")
viper.SetDefault("BinPath", "bin")
viper.SetDefault("HideLoginNotice", true)
viper.SetDefault("LoginNotices", []string{

View File

@@ -19,7 +19,7 @@
docker compose up
```
The database is automatically initialized and patched on first start via `init/setup.sh`.
The database schema is automatically applied on first start via the embedded migration system.
pgAdmin is available at `http://localhost:5050` (default login: `user@pgadmin.com` / `password`).
@@ -45,18 +45,14 @@ To delete all persistent data, remove these directories after stopping:
## Updating
After pulling new changes:
After pulling new changes, rebuild and restart. Schema migrations are applied automatically on startup.
1. Check for new patch schemas in `schemas/patch-schema/` — apply them via pgAdmin or `psql` into the running database container.
2. Rebuild and restart:
```bash
docker compose down
docker compose build
docker compose up
```
```bash
docker compose down
docker compose build
docker compose up
```
## Troubleshooting
**Postgres won't populate on Windows**: `init/setup.sh` must use LF line endings, not CRLF. Open it in your editor and convert.
**Postgres won't start on Windows**: Ensure `docker/db-data/` doesn't contain stale data from a different PostgreSQL version. Delete it and restart to reinitialize.

24
main.go
View File

@@ -2,6 +2,7 @@ package main
import (
cfg "erupe-ce/config"
"flag"
"fmt"
"net"
"os"
@@ -71,6 +72,9 @@ func setupDiscordBot(config *cfg.Config, logger *zap.Logger) *discordbot.Discord
}
func main() {
runSetup := flag.Bool("setup", false, "Launch the setup wizard (even if config.json exists)")
flag.Parse()
var err error
var zapLogger *zap.Logger
@@ -79,6 +83,13 @@ func main() {
defer func() { _ = zapLogger.Sync() }()
logger := zapLogger.Named("main")
if *runSetup {
logger.Info("Launching setup wizard (--setup)")
if err := setup.Run(logger.Named("setup"), 8080); err != nil {
logger.Fatal("Setup wizard failed", zap.Error(err))
}
}
config, cfgErr := cfg.LoadConfig()
if cfgErr != nil {
if _, err := os.Stat("config.json"); os.IsNotExist(err) {
@@ -156,6 +167,7 @@ func main() {
logger.Info("Database: Started successfully")
// Run database migrations
verBefore, _ := migrations.Version(db)
applied, migErr := migrations.Migrate(db, logger.Named("migrations"))
if migErr != nil {
preventClose(config, fmt.Sprintf("Database migration failed: %s", migErr.Error()))
@@ -165,6 +177,18 @@ func main() {
logger.Info(fmt.Sprintf("Database: Applied %d migration(s), now at version %d", applied, ver))
}
// Auto-apply seed data on a fresh database so users who skip the wizard
// still get shops, events, and gacha. Seed files use ON CONFLICT DO NOTHING
// so this is safe to run even if data already exists.
if verBefore == 0 && applied > 0 {
seedApplied, seedErr := migrations.ApplySeedData(db, logger.Named("migrations"))
if seedErr != nil {
logger.Warn(fmt.Sprintf("Seed data failed: %s", seedErr.Error()))
} else if seedApplied > 0 {
logger.Info(fmt.Sprintf("Database: Applied %d seed data file(s)", seedApplied))
}
}
// Pre-compute all server IDs this instance will own, so we only
// delete our own rows (safe for multi-instance on the same DB).
var ownedServerIDs []string

View File

@@ -26,6 +26,7 @@ type FinishRequest struct {
DBPassword string `json:"dbPassword"`
DBName string `json:"dbName"`
Host string `json:"host"`
Language string `json:"language"`
ClientMode string `json:"clientMode"`
AutoCreateAccount bool `json:"autoCreateAccount"`
}
@@ -33,8 +34,13 @@ type FinishRequest struct {
// buildDefaultConfig produces a minimal config map with only user-provided values.
// All other settings are filled by Viper's registered defaults at load time.
func buildDefaultConfig(req FinishRequest) map[string]interface{} {
lang := req.Language
if lang == "" {
lang = "jp"
}
return map[string]interface{}{
"Host": req.Host,
"Language": lang,
"ClientMode": req.ClientMode,
"AutoCreateAccount": req.AutoCreateAccount,
"Database": map[string]interface{}{

View File

@@ -145,10 +145,20 @@ h1{font-size:1.75rem;margin-bottom:.5rem;color:#e94560;text-align:center}
</div>
<div style="font-size:.75rem;color:#666;margin-top:.3rem">Use 127.0.0.1 for local play, or auto-detect for LAN/internet play.</div>
</div>
<div class="field">
<label>Client Mode</label>
<select id="srv-client-mode"></select>
<div style="font-size:.75rem;color:#666;margin-top:.3rem">Must match your game client version. ZZ is the latest.</div>
<div class="field-row">
<div class="field">
<label>Client Mode</label>
<select id="srv-client-mode"></select>
<div style="font-size:.75rem;color:#666;margin-top:.3rem">Must match your game client version. ZZ is the latest.</div>
</div>
<div class="field field-sm">
<label>Language</label>
<select id="srv-language">
<option value="jp" selected>jp</option>
<option value="en">en</option>
</select>
<div style="font-size:.75rem;color:#666;margin-top:.3rem">Game text language.</div>
</div>
</div>
<label class="checkbox" style="margin-top:1rem"><input type="checkbox" id="srv-auto-create" checked> Auto-create accounts (recommended for private servers)</label>
<div class="actions">
@@ -339,6 +349,7 @@ function buildReview() {
['Database Password', masked],
['Database Name', document.getElementById('db-name').value],
['Server Host', document.getElementById('srv-host').value],
['Language', document.getElementById('srv-language').value],
['Client Mode', document.getElementById('srv-client-mode').value],
['Auto-create Accounts', document.getElementById('srv-auto-create').checked ? 'Yes' : 'No'],
];
@@ -362,6 +373,7 @@ async function finish() {
dbPassword: document.getElementById('db-password').value,
dbName: document.getElementById('db-name').value,
host: document.getElementById('srv-host').value,
language: document.getElementById('srv-language').value,
clientMode: document.getElementById('srv-client-mode').value,
autoCreateAccount: document.getElementById('srv-auto-create').checked,
})