feat(apps): stepped creation wizard, branch previews, and app-creation fixes
This session (frontend focus):
- Rebuild /apps/new as a 4-step wizard (Basics → Configure → Trigger → Review):
WizardRail, SourceKindPicker card grid, AppManifest review, per-step validation,
ConfirmDialog-based unsaved-changes guard.
- Extract lib/workload/sourceForms.ts (single source of truth for source_config)
+ {Image,Compose,Static,Dockerfile}SourceForm + StaticDiscoveryWizard; fold the
/apps/[id] edit form onto the same components (removes the duplication). Add
vitest + sourceForms unit tests.
- Branch preview environments UI: /chain is_preview/preview_branch + a Preview
environments panel on /apps/[id] (per-branch URLs, ConfirmDialog teardown, armed
state); RegistryImagePicker on the registry trigger and the image source.
- Fixes: image-inspect 404 -> admin-gated POST /api/discovery/image/inspect;
conflict-panel blur flicker; friendly localized discovery errors; CPU/Memory
label hints; dashboard + /apps "Total workloads" count only source_kind workloads
(drop stale trigger_kind gate); NPM cert/access-list name cache; EntityPicker
empty-list guard.
- Update CLAUDE.md frontend conventions + add a Build & Test section.
Also captures pre-existing in-progress platform work (not from this session):
workload notifications, Prometheus metrics export, store lockfile, health probes,
backup hardening, and related store/webhook/scheduler changes.
This commit is contained in:
+32
-1
@@ -43,6 +43,7 @@ import (
|
||||
// itself with internal/workload/plugin. Adding a new Source or Trigger
|
||||
// is a matter of dropping a new package and adding it to this list.
|
||||
_ "github.com/alexei/tinyforge/internal/workload/plugin/source/compose"
|
||||
_ "github.com/alexei/tinyforge/internal/workload/plugin/source/dockerfile"
|
||||
_ "github.com/alexei/tinyforge/internal/workload/plugin/source/image"
|
||||
_ "github.com/alexei/tinyforge/internal/workload/plugin/source/static"
|
||||
_ "github.com/alexei/tinyforge/internal/workload/plugin/trigger/git"
|
||||
@@ -62,6 +63,20 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Acquire single-instance lockfile BEFORE opening the DB. SQLite +
|
||||
// SetMaxOpenConns(1) does not protect against two Tinyforge processes
|
||||
// sharing a data directory; without this guard a misconfigured
|
||||
// systemd unit, container restart race, or `tinyforge` shell typo can
|
||||
// silently double-fire schedulers, double-poll registries, and
|
||||
// corrupt `extra_json` RMW. The lockfile is a PID file under
|
||||
// $DATA_DIR/tinyforge.lock — collisions with dead PIDs are reclaimed.
|
||||
releaseLock, err := store.AcquireLockfile(dataDir)
|
||||
if err != nil {
|
||||
slog.Error("could not acquire data-dir lock", "data_dir", dataDir, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer releaseLock()
|
||||
|
||||
// Open database.
|
||||
dbPath := filepath.Join(dataDir, "tinyforge.db")
|
||||
db, err := store.New(dbPath)
|
||||
@@ -78,6 +93,21 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// One-shot migration: rewrite every legacy unprefixed-hex secret
|
||||
// in the DB into the new tf1: envelope form. Idempotent (gated by
|
||||
// schema_versions version 2). Lets the rest of the codebase treat
|
||||
// envelope-presence as a stable invariant for future key rotations.
|
||||
// Failures here are logged but non-fatal: a partial migration just
|
||||
// means some columns keep working through Decrypt's legacy
|
||||
// fallback until the next manual save re-encrypts them.
|
||||
if err := db.MigrateSecretsToEnvelope(store.EnvelopeMigrator{
|
||||
HasEnvelope: crypto.HasEnvelope,
|
||||
Decrypt: func(v string) (string, error) { return crypto.Decrypt(encKey, v) },
|
||||
Encrypt: func(v string) (string, error) { return crypto.Encrypt(encKey, v) },
|
||||
}); err != nil {
|
||||
slog.Warn("secrets envelope migration", "error", err)
|
||||
}
|
||||
|
||||
// Import seed config on first launch (idempotent).
|
||||
seedPath := envOrDefault("SEED_FILE", "./tinyforge.yaml")
|
||||
if err := config.ImportSeed(db, seedPath); err != nil {
|
||||
@@ -197,7 +227,8 @@ func main() {
|
||||
switch {
|
||||
case r.Deployed:
|
||||
deployed++
|
||||
case r.Reason == webhook.ReasonBindingDisabled, r.Reason == webhook.ReasonNoMatch:
|
||||
case r.Reason == webhook.ReasonBindingDisabled, r.Reason == webhook.ReasonNoMatch,
|
||||
r.Reason == webhook.ReasonPreviewNoop:
|
||||
// not a failure — silent
|
||||
default:
|
||||
errored++
|
||||
|
||||
Reference in New Issue
Block a user