Skip to content

fix(setup): UI mode now re-packs .gtbundle after successful save#91

Open
BimaPangestu28 wants to merge 2 commits into
developfrom
fix/setup-ui-mode-repack-bundle
Open

fix(setup): UI mode now re-packs .gtbundle after successful save#91
BimaPangestu28 wants to merge 2 commits into
developfrom
fix/setup-ui-mode-repack-bundle

Conversation

@BimaPangestu28
Copy link
Copy Markdown
Member

Symptom

Bima reported `secret error: not-found` at runtime even after running `gtc setup` and seeing 'persisted N keys' for every provider. Extract of his `test.gtbundle` post-setup confirmed the bundle had no `state/`, `tenants/`, or `.greentic/` inside the squashfs — even though `/tmp/gtbundle-test/` (the extracted working dir) had everything correctly populated.

Root cause

`run_simple_setup` (greentic_setup.rs:267-296) calls `gtbundle::create_gtbundle` after `engine.execute` to write the configured bundle back to its `.gtbundle` archive. `run_ui_mode` (greentic_setup.rs:312-378) did the same engine work — answers, secrets, tenant config all written under `bundle_dir` — but never re-packed the extracted dir back to the user's input file.

Default `gtc setup` flow lands on UI mode (`cli.ui && !cli.no_ui && !cli.non_interactive`), so the typical user gets the broken path. The original `.gtbundle` stays unchanged, and the next `gtc start` sees none of the just-saved secrets.

Fix

  • Add `output_target: Option` to `UiState` and the `ui::launch` signature so the UI flow knows whether the user passed a `.gtbundle` archive (re-pack) or a directory (copy).
  • `run_ui_mode` now computes `setup_output_target(&bundle_path)` up front (same helper the simple-mode flow uses) and passes it through.
  • The `/api/execute` UI handler runs the existing `execute_setup` blocking task; on `result.success` it dispatches a follow-up `spawn_blocking` that calls `gtbundle::create_gtbundle` (Archive variant) or `copy_dir_recursive` (Directory variant) and appends a "Configured bundle written to: …" line to `result.stdout`. Re-pack failure flips `success` to `false` and surfaces the error in `stderr` so the UI shows it instead of silently lying.
  • Derive `Clone` on `SetupOutputTarget` since the value is now `Arc`-shared in `UiState` and consumed inside the spawn_blocking closure.

Verification

  • `bash ci/local_check.sh` — passed (fmt + clippy + 223 lib tests + doc + package-dry-run)
  • Manual round-trip on a fresh `.gtbundle`:
    • Before fix: post-setup squashfs had no `state/config/`, no `.greentic/dev/.dev.secrets.env`
    • After fix: same squashfs has `state/config//setup-answers.json` for every provider, plus the encrypted `.dev.secrets.env`, and `gtc start` boots without `secret error: not-found`

Related

`run_simple_setup` calls `gtbundle::create_gtbundle` after
`engine.execute` to write the configured bundle back to its `.gtbundle`
archive. `run_ui_mode` did the same engine work — answers, secrets,
tenant config all written under `bundle_dir` — but **never re-packed**
the extracted dir back to the user's input file. Result: every
`gtc setup` (default UI mode) on a `.gtbundle` left the original
archive untouched, and the next `gtc start` saw none of the
just-saved secrets and panicked with `secret error: not-found`.

Fix:
- Add `output_target: Option<SetupOutputTarget>` to `UiState` and the
  `ui::launch` signature so the UI flow knows whether the user passed
  a `.gtbundle` archive (re-pack) or a directory (copy).
- `run_ui_mode` now computes `setup_output_target(&bundle_path)` up
  front (same helper the simple-mode flow uses) and passes it through.
- The `/api/execute` UI handler runs the existing `execute_setup`
  blocking task; on `result.success` it dispatches a follow-up
  `spawn_blocking` that calls `gtbundle::create_gtbundle` (Archive)
  or `copy_dir_recursive` (Directory) and appends a "Configured
  bundle written to: …" line to the result. Re-pack failure flips
  `success` to false and surfaces the error in `stderr` so the UI
  can show it instead of silently lying about success.
- Derive `Clone` on `SetupOutputTarget` since the value is now stored
  in `UiState` (`Arc`-shared) and consumed inside the spawn_blocking
  closure.

Verified: a `.gtbundle` run through `gtc setup` now contains the
populated `state/config/<provider>/setup-answers.json` and
`.greentic/dev/.dev.secrets.env` after extraction, where it was empty
before this change.
BimaPangestu28 added a commit that referenced this pull request May 2, 2026
Carries the four PRs in the setup-roundtrip family up onto a stable
release tag so `cargo install greentic-setup` (and therefore the
`gtc setup` router target) picks up the fixes:

- #89/#90: squashfs perms (re-packed bundle now extracts as 0o755/0644
  instead of 0o000), `--non-interactive` no longer hangs on tunnel
  prompt, `emit-answers` template now includes `platform_setup.tunnel`.
- #91/#92: UI mode (default `gtc setup`) now re-packs the .gtbundle
  after engine.execute — answers + secrets + tenant config actually
  land inside the squashfs instead of being dropped on temp-dir
  cleanup.

Tag `v0.5.8` after merge to trigger crates.io publish via
release-binaries.yml.
BimaPangestu28 added a commit that referenced this pull request May 2, 2026
CI fmt check failed after the i18n nav-links work landed; running
`cargo fmt --all` reformats the affected blocks in
sanitize_nav_links / build_tooltip_obj. No semantic change — picked up
on the hotfix branch so CI on PR #92 (and the matching #91 sibling)
goes green.
BimaPangestu28 added a commit that referenced this pull request May 2, 2026
)

* fix(setup): UI mode now re-packs .gtbundle after successful save

`run_simple_setup` calls `gtbundle::create_gtbundle` after
`engine.execute` to write the configured bundle back to its `.gtbundle`
archive. `run_ui_mode` did the same engine work — answers, secrets,
tenant config all written under `bundle_dir` — but **never re-packed**
the extracted dir back to the user's input file. Result: every
`gtc setup` (default UI mode) on a `.gtbundle` left the original
archive untouched, and the next `gtc start` saw none of the
just-saved secrets and panicked with `secret error: not-found`.

Fix:
- Add `output_target: Option<SetupOutputTarget>` to `UiState` and the
  `ui::launch` signature so the UI flow knows whether the user passed
  a `.gtbundle` archive (re-pack) or a directory (copy).
- `run_ui_mode` now computes `setup_output_target(&bundle_path)` up
  front (same helper the simple-mode flow uses) and passes it through.
- The `/api/execute` UI handler runs the existing `execute_setup`
  blocking task; on `result.success` it dispatches a follow-up
  `spawn_blocking` that calls `gtbundle::create_gtbundle` (Archive)
  or `copy_dir_recursive` (Directory) and appends a "Configured
  bundle written to: …" line to the result. Re-pack failure flips
  `success` to false and surfaces the error in `stderr` so the UI
  can show it instead of silently lying about success.
- Derive `Clone` on `SetupOutputTarget` since the value is now stored
  in `UiState` (`Arc`-shared) and consumed inside the spawn_blocking
  closure.

Verified: a `.gtbundle` run through `gtc setup` now contains the
populated `state/config/<provider>/setup-answers.json` and
`.greentic/dev/.dev.secrets.env` after extraction, where it was empty
before this change.

* feat(setup): support QuestionKind::Table for repeating-row wizard answers

Phases 2–3 of the table-kind RFC (greenticai/greentic-types#112). Wizard
operators can now answer repeating-row questions row-by-row instead of
typing JSON arrays into a single textarea.

Changes:
- `setup_input::SetupQuestion` accepts `kind: table` plus `columns: [...]`,
  `min_rows`, `max_rows`. New `SetupTableColumn` struct mirrors the
  per-row schema (key/title/kind/required/help/placeholder/choices/default).
- `setup_to_formspec::convert::convert_table_question` bridges the
  setup.yaml shape into qa-spec's existing `QuestionType::List` +
  `ListSpec.fields`. Each column becomes a nested `QuestionSpec` keyed
  by its `key` field. Nested tables are not supported — column kind
  collapses to scalar.
- `qa::prompts::ask_list_question` implements the CLI prompt loop:
  "Add a row? [y/N]", per-column prompts, drop rows with empty
  required columns, enforce `min_items`/`max_items`. Returns a
  `Value::Array` of per-row JSON objects.
- `tenant_config::sync_nav_links_to_tenant_config` now accepts the
  native array shape (`nav_links`) produced by the table wizard, in
  addition to the existing `nav_links_json` string fallback. New helper
  `sanitize_nav_link_array` deduplicates the validation logic and adds
  i18n-keyed label support (locale-keyed object passes through).

Tests: 23 tenant_config (1 new — table-shape answer) + 22
setup_to_formspec (1 new — table → list bridge). 223 lib total.

Phase 4 (canary migration of `nav_links_json` to Table kind in
messaging-webchat-gui setup.yaml) lands in greenticai/greentic-messaging-providers
PR #149 alongside this.

* chore(release): bump to 0.5.8 to ship setup .gtbundle round-trip fixes

Carries the four PRs in the setup-roundtrip family up onto a stable
release tag so `cargo install greentic-setup` (and therefore the
`gtc setup` router target) picks up the fixes:

- #89/#90: squashfs perms (re-packed bundle now extracts as 0o755/0644
  instead of 0o000), `--non-interactive` no longer hangs on tunnel
  prompt, `emit-answers` template now includes `platform_setup.tunnel`.
- #91/#92: UI mode (default `gtc setup`) now re-packs the .gtbundle
  after engine.execute — answers + secrets + tenant config actually
  land inside the squashfs instead of being dropped on temp-dir
  cleanup.

Tag `v0.5.8` after merge to trigger crates.io publish via
release-binaries.yml.

* feat(setup-ui): render kind:table as add/remove-row table input

Phase 5 of the table-kind RFC. The web wizard now renders QuestionType::List
questions as a real table with one input per column, an Add row button,
and trash-icon row removal — instead of falling through to the empty
Back button stub.

Wire-up:
- QuestionInfo gains list_columns plus min_rows/max_rows.
- app.js: renderQuestion handles kind==='List', appendTableRow builds
  one tr of inputs, setupTableQuestions wires the Add button and seeds
  saved rows. restoreFormValues skips List, collectFormValues walks
  tbody rows and builds per-row JSON objects.
- style.css: minimal table styling.

End-to-end: operator sees a real table at gtc setup --ui, fills per-row,
submit posts { nav_links: [{label, url, external?}, ...] }, sync function
writes the array to tenants/<tenant>.json, SPA renderTopbarNav consumes.

* feat(setup-ui): multilingual cell + uniform input sizing for table columns

* feat(setup-ui): expand multilingual locale picker to full 65-entry SUPPORTED_LOCALES set

* feat(setup,ui): tooltip+num columns in nav_links table; nested tooltip emit

* chore: cargo fmt — apply to tenant_config.rs

CI fmt check failed after the i18n nav-links work landed; running
`cargo fmt --all` reformats the affected blocks in
sanitize_nav_links / build_tooltip_obj. No semantic change — picked up
on the hotfix branch so CI on PR #92 (and the matching #91 sibling)
goes green.

* fix(setup-ui): widen container + has-wide-form toggle for kind:table forms

* fix(setup): always prompt kind:List even when optional in normal (non-advanced) mode

* feat(setup-ui): row-level i18n picker, table hydration, legacy migration

- Replace per-cell "+ language" pickers with a single row-level toolbar
  that adds locale sub-rows to every multilingual cell at once and
  removes them symmetrically via chip ✕. Saves four clicks per row and
  keeps the pre-mount layout from cramming pickers into 7 columns.
- Pre-populate the nav_links table on wizard re-run by reading the
  bundle's persisted tenant.json::nav_links via a new
  read_existing_nav_links helper that flattens the on-disk nested
  tooltip {eyebrow,title,lede} shape back to the wizard's flat columns.
  saved_rows is sent through the new QuestionInfo field.
- Migrate legacy nav_links_json string answers to nav_links arrays in
  /api/scopes prefill, then drop the legacy key. Without this the
  ghost overrides the table edits the user just made on next sync.
- Fix collectFormValues silently dropping URL/Boolean/scalar columns:
  td.dataset.col (added for CSS column-width targeting) made the bare
  [data-col] selector match the parent <td> first; cell.value read
  back undefined and required-column rows got filtered out, leaving
  nav_links absent from the answers payload. Selector now scopes to
  input/select.
- Switch form-area listeners to event delegation so dynamically-added
  locale-row inputs participate in autosave.
- Per-column min-widths + container widening to 1380px for the
  7-column nav_links table; row-table__lang-row chips match brand.
- Verbose [hydrate] / [nav_links] logging for round-trip diagnosis.
- Bump 0.5.8 → 0.5.9.

* chore: cargo fmt + drop single-element legacy-key loop

CI fmt found two long lines in tenant_config.rs / ui/mod.rs; clippy
flagged the migration loop as single_element_loop. Open-code the
single legacy_key migration with const strings — when we add more
`_json` legacy answer shapes we can re-introduce the loop.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant