mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
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:
@@ -153,25 +153,18 @@ func TestYourFunction(t *testing.T) {
|
|||||||
|
|
||||||
## Database Schema Changes
|
## 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`
|
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 patch
|
2. Increment the number from the last migration
|
||||||
3. Test the migration on a clean database
|
3. Test the migration on both a fresh and existing database
|
||||||
4. Document what the patch does in comments
|
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 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.
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
## Documentation Requirements
|
## Documentation Requirements
|
||||||
|
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -197,14 +197,10 @@ Multiple channel servers can run simultaneously, organized by world types: Newbi
|
|||||||
|
|
||||||
## Database Schemas
|
## 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
|
- **Migrations**: Numbered SQL files (`0001_init.sql`, `0002_*.sql`, ...) tracked in a `schema_version` table
|
||||||
- **Update Schemas**: Production-ready updates for new releases
|
- **Seed Data**: Demo templates for shops, distributions, events, and gacha in [server/migrations/seed/](./server/migrations/seed/) — applied automatically on fresh databases
|
||||||
- **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.
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -237,7 +233,7 @@ go test -v -race ./... # Check for race conditions (mandatory before merging
|
|||||||
|
|
||||||
### Database schema errors
|
### 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
|
- Check PostgreSQL logs for detailed error messages
|
||||||
- Verify database user has sufficient privileges
|
- Verify database user has sufficient privileges
|
||||||
|
|
||||||
|
|||||||
@@ -342,6 +342,7 @@ func getOutboundIP4() (net.IP, error) {
|
|||||||
// config.json (just database credentials) produces a fully working server.
|
// config.json (just database credentials) produces a fully working server.
|
||||||
func registerDefaults() {
|
func registerDefaults() {
|
||||||
// Top-level settings
|
// Top-level settings
|
||||||
|
viper.SetDefault("Language", "jp")
|
||||||
viper.SetDefault("BinPath", "bin")
|
viper.SetDefault("BinPath", "bin")
|
||||||
viper.SetDefault("HideLoginNotice", true)
|
viper.SetDefault("HideLoginNotice", true)
|
||||||
viper.SetDefault("LoginNotices", []string{
|
viper.SetDefault("LoginNotices", []string{
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
docker compose up
|
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`).
|
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
|
## 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.
|
```bash
|
||||||
|
docker compose down
|
||||||
2. Rebuild and restart:
|
docker compose build
|
||||||
|
docker compose up
|
||||||
```bash
|
```
|
||||||
docker compose down
|
|
||||||
docker compose build
|
|
||||||
docker compose up
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
## 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
24
main.go
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
cfg "erupe-ce/config"
|
cfg "erupe-ce/config"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
@@ -71,6 +72,9 @@ func setupDiscordBot(config *cfg.Config, logger *zap.Logger) *discordbot.Discord
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
runSetup := flag.Bool("setup", false, "Launch the setup wizard (even if config.json exists)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
var zapLogger *zap.Logger
|
var zapLogger *zap.Logger
|
||||||
@@ -79,6 +83,13 @@ func main() {
|
|||||||
defer func() { _ = zapLogger.Sync() }()
|
defer func() { _ = zapLogger.Sync() }()
|
||||||
logger := zapLogger.Named("main")
|
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()
|
config, cfgErr := cfg.LoadConfig()
|
||||||
if cfgErr != nil {
|
if cfgErr != nil {
|
||||||
if _, err := os.Stat("config.json"); os.IsNotExist(err) {
|
if _, err := os.Stat("config.json"); os.IsNotExist(err) {
|
||||||
@@ -156,6 +167,7 @@ func main() {
|
|||||||
logger.Info("Database: Started successfully")
|
logger.Info("Database: Started successfully")
|
||||||
|
|
||||||
// Run database migrations
|
// Run database migrations
|
||||||
|
verBefore, _ := migrations.Version(db)
|
||||||
applied, migErr := migrations.Migrate(db, logger.Named("migrations"))
|
applied, migErr := migrations.Migrate(db, logger.Named("migrations"))
|
||||||
if migErr != nil {
|
if migErr != nil {
|
||||||
preventClose(config, fmt.Sprintf("Database migration failed: %s", migErr.Error()))
|
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))
|
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
|
// Pre-compute all server IDs this instance will own, so we only
|
||||||
// delete our own rows (safe for multi-instance on the same DB).
|
// delete our own rows (safe for multi-instance on the same DB).
|
||||||
var ownedServerIDs []string
|
var ownedServerIDs []string
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type FinishRequest struct {
|
|||||||
DBPassword string `json:"dbPassword"`
|
DBPassword string `json:"dbPassword"`
|
||||||
DBName string `json:"dbName"`
|
DBName string `json:"dbName"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
|
Language string `json:"language"`
|
||||||
ClientMode string `json:"clientMode"`
|
ClientMode string `json:"clientMode"`
|
||||||
AutoCreateAccount bool `json:"autoCreateAccount"`
|
AutoCreateAccount bool `json:"autoCreateAccount"`
|
||||||
}
|
}
|
||||||
@@ -33,8 +34,13 @@ type FinishRequest struct {
|
|||||||
// buildDefaultConfig produces a minimal config map with only user-provided values.
|
// buildDefaultConfig produces a minimal config map with only user-provided values.
|
||||||
// All other settings are filled by Viper's registered defaults at load time.
|
// All other settings are filled by Viper's registered defaults at load time.
|
||||||
func buildDefaultConfig(req FinishRequest) map[string]interface{} {
|
func buildDefaultConfig(req FinishRequest) map[string]interface{} {
|
||||||
|
lang := req.Language
|
||||||
|
if lang == "" {
|
||||||
|
lang = "jp"
|
||||||
|
}
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"Host": req.Host,
|
"Host": req.Host,
|
||||||
|
"Language": lang,
|
||||||
"ClientMode": req.ClientMode,
|
"ClientMode": req.ClientMode,
|
||||||
"AutoCreateAccount": req.AutoCreateAccount,
|
"AutoCreateAccount": req.AutoCreateAccount,
|
||||||
"Database": map[string]interface{}{
|
"Database": map[string]interface{}{
|
||||||
|
|||||||
@@ -145,11 +145,21 @@ h1{font-size:1.75rem;margin-bottom:.5rem;color:#e94560;text-align:center}
|
|||||||
</div>
|
</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 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>
|
||||||
|
<div class="field-row">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Client Mode</label>
|
<label>Client Mode</label>
|
||||||
<select id="srv-client-mode"></select>
|
<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 style="font-size:.75rem;color:#666;margin-top:.3rem">Must match your game client version. ZZ is the latest.</div>
|
||||||
</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>
|
<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">
|
<div class="actions">
|
||||||
<button class="btn btn-secondary" onclick="goToStep(2)">Back</button>
|
<button class="btn btn-secondary" onclick="goToStep(2)">Back</button>
|
||||||
@@ -339,6 +349,7 @@ function buildReview() {
|
|||||||
['Database Password', masked],
|
['Database Password', masked],
|
||||||
['Database Name', document.getElementById('db-name').value],
|
['Database Name', document.getElementById('db-name').value],
|
||||||
['Server Host', document.getElementById('srv-host').value],
|
['Server Host', document.getElementById('srv-host').value],
|
||||||
|
['Language', document.getElementById('srv-language').value],
|
||||||
['Client Mode', document.getElementById('srv-client-mode').value],
|
['Client Mode', document.getElementById('srv-client-mode').value],
|
||||||
['Auto-create Accounts', document.getElementById('srv-auto-create').checked ? 'Yes' : 'No'],
|
['Auto-create Accounts', document.getElementById('srv-auto-create').checked ? 'Yes' : 'No'],
|
||||||
];
|
];
|
||||||
@@ -362,6 +373,7 @@ async function finish() {
|
|||||||
dbPassword: document.getElementById('db-password').value,
|
dbPassword: document.getElementById('db-password').value,
|
||||||
dbName: document.getElementById('db-name').value,
|
dbName: document.getElementById('db-name').value,
|
||||||
host: document.getElementById('srv-host').value,
|
host: document.getElementById('srv-host').value,
|
||||||
|
language: document.getElementById('srv-language').value,
|
||||||
clientMode: document.getElementById('srv-client-mode').value,
|
clientMode: document.getElementById('srv-client-mode').value,
|
||||||
autoCreateAccount: document.getElementById('srv-auto-create').checked,
|
autoCreateAccount: document.getElementById('srv-auto-create').checked,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user