- Pending pledges surface in the card footer as '· N pending' tacked
on after the existing footer line (text-subdued). Quiet, semantic,
doesn't compete with the status pill or the avatar.
- The top-of-page 'You have pending pledges' callout was using the
amber DS::Alert warning variant. Pending isn't a warning — it's a
passive 'we're waiting on a sync' state. Switch to the info
variant so the visual weight matches the meaning.
The amber DS::Pill dot on the avatar collided visually with the
amber 'Behind' status pill on the same card — two amber signals in
the same eye-line. Move the indicator to a text-subdued clock icon
between the goal name and the status pill: quieter, semantically
clearer (clock = pending sync / time-bound), no tone collision with
the status palette. Same accessibility hook (aria-label + title).
Goals with open + unexpired pledges now carry a small amber DS::Pill
dot at the top-right of the avatar on the index card. Same primitive
+ position pattern as the beta gate dot on the sidebar nav, so the
'small marker' affordance reads consistently across the app.
Pledges are preloaded via the existing .includes(:open_pledges, ...) on
the index query, so the indicator is free at request time.
The earlier 'filter archived too' attempts kept toggling the archived
section based on chip state, which produced more confusion than value
(filter shows partial counts, archived hides on some chips, etc.).
Step back: archived stays in its own collapsed-by-default section,
always visible, never reacts to the chip / search filter. Render
the cards with filterable: false so they don't add a filter target
in the first place — no JS handling needed, and the active grid +
chips behave exactly like they did before this whole thread.
The KPI tile reads 'X of Y on track'. Y was every active goal minus
reached + paused, which included open-ended goals (no target_date).
But an open-ended goal has no required monthly pace to compare
against — by definition it can be neither on track nor behind. Counting
it in the denominator dragged the ratio down and never improved as the
user kept saving (the fraction stays stuck because the open-ended goal
is never a hit).
Exclude :no_target_date from tracked_total. Numerator unchanged. The
subline still surfaces 'N without a deadline' as informational so the
user knows those goals exist.
The card was the only place in Sure setting hover:bg-surface-hover on
a bg-container card; every other static card (settings, recurring
transactions, ai_prompts, etc.) stays still. The hover bg (gray-100)
landed almost exactly on top of the ring track (gray-200) and washed
out the pill fill, fighting the very signals the card was trying to
show. Swap to hover:shadow-sm — the lift matches the transaction-tabs
precedent already in Sure, the bg stays white, and ring track + status
pill keep their contrast.
The status pill on the goal card used a 10%-alpha fill (bg-warning/10,
bg-green-500/10). On the card's hover state (bg-surface-hover) the fill
blended into the new background and the pill lost its tint outline.
Extend DS::Pill with a green tone and an optional icon: param (renders
a Lucide icon in place of the dot) so the same primitive can carry both
the beta marker and the goal status badges. Map Goals::StatusPillComponent
to DS::Pill outline style — transparent fill + colored border + colored
text + glyph — which is immune to any change in the surrounding card bg.
One badge primitive, light-mode contrast already fixed (the color-mix
30% darkening on text), and the card hover state no longer washes out
the status.
Sure's convention for one-off custom classes (.combobox, .scrollbar,
.windows, .prose variants, .table-divider, .turbo-progress-bar) is to
define them directly in application.css. Topical files (privacy-mode,
date-picker-dark-mode, print-report) are reserved for coherent feature
namespaces with multiple rules. A single .goal-avatar rule belongs
with the other custom classes.
- Switch the goal_accounts → accounts FK from on_delete: :cascade to
:restrict. `Goal#must_have_at_least_one_linked_account` is enforced
at write time; the cascade let a raw DELETE silently orphan a Goal
whose only link pointed at the deleted account. Normal Rails
Account#destroy still cleans up via `dependent: :destroy`, but the
restrict guarantees the DB rejects any path that bypasses the
association.
- projection_payload: required_monthly is now monthly_target_amount&.to_f
so open-ended (no-target-date) goals serialize required_monthly: null
instead of 0, matching the absence of a required pace.
- index page + sidebar nav-rail dot now read the Beta label via
t("shared.beta") (and a new shared.beta locale key) instead of the
hardcoded "Beta" literal.
- _status_callout uses the view-helper t(...) instead of I18n.t(...)
for the status label so it follows the same convention as the rest
of the goals views.
- goal_projection_chart: read the computed style before stamping
position: relative so a stylesheet-defined position (fixed/sticky/
absolute) isn't clobbered.
- preview-deploy: add `set -euo pipefail` around the wrangler
container lookup so a curl/jq failure fails the job instead of
producing an empty CONTAINER_ID and silently skipping cleanup.
- Family#savings_inflow_windows wraps the current/prior 30d sums in a
single helper that memoizes the linked-account-id lookup. The KPI tile
on the goals index used to run the join+pluck twice per request.
- Replace two instance_variable_set pokes and one any_instance.stubs in
the goal/controller tests. Refetching the goal exercises the real
request lifecycle and stops the tests from leaning on implementation
details. The 'All caught up' assertion now relies on a real reached
state (target 1 vs the depository fixture's 5000 balance) rather than
stubbing :status.
- Add tests covering: hex format validation on Goal#color, AASM cache
reset (display_status reads the new state on the same instance after
pause!), negative pledge amount rejection, expire! no-op on already-
expired pledge, cancel! NotOpenError on non-open pledge, sweep job
idempotency on a second pass, and strong-params rejection of state /
family_id on goal create.
Native <dialog>.showModal() moves focus inside the dialog on open but
doesn't trap Tab / Shift+Tab, and focus restoration on close is
inconsistent across engines. Add three things to the dialog controller:
- Capture document.activeElement before showModal() so the trigger is
recoverable when the dialog closes (ESC, backdrop click, explicit
close button, programmatic close all route through the native close
event).
- Wrap Tab inside the dialog so a keyboard user can't tab out into the
scrim-covered page behind.
- Restore focus to the captured trigger on the close event. If the
trigger has been detached (Turbo morphed it out), skip silently
rather than throw.
Verified manually: opening the new-goal modal moves focus to the name
input; ESC restores focus to the "New goal" link; Tab wraps from the
last focusable back to the first.
- color_icon_picker: listeners were attached in initialize() with inline
arrows, so they had no removable reference and no disconnect() cleanup.
Every Turbo navigation that re-rendered the picker stacked another
listener on the same node and left the Pickr instance alive. Move
attachment to connect(), store bound references, and clean up in
disconnect() (including destroyAndRemove on the Pickr).
- goals_filter: replaceState fired on every keystroke. Debounce the URL
sync 200 ms so a typing burst collapses into a single update. The
visible filtering stays real-time. Clear the timer in disconnect()
so a pending sync doesn't fire on an unmounted controller.
* fix(ibkr): resolve weekend balance oscillations and improve data processing
Address issues where IBKR weekend/holiday data caused incorrect balance
calculations and improve the robustness of IBKR account processing.
- Fix historical balance oscillations by ignoring anomalous weekend rows
and filling gaps by carrying forward the last known trading day value.
- Normalize report dates to the last trading day to ensure consistency.
- Improve `HoldingsProcessor` to skip individual bad lots instead of
failing the entire group.
- Refactor `ActivitiesProcessor` to accumulate fee counts locally via
return values instead of using instance variables.
- Add support for accounting parentheses notation in `DataHelpers`.
- Memoize the account object in `IbkrAccount::Processor` to reduce
database queries.
- Update tests to reflect date normalization and improved precision
assertions.
* fix(ibkr): derive historical cash from materializer balances, not equity summary
Real IBKR Flex exports do not include a reliable cash/stock breakdown in
EquitySummaryByReportDateInBase — only the total is consistently present.
The previous implementation parsed the missing cash field as zero and wrote
cash_balance=0 for every historical date, causing negative and wildly
incorrect cash values throughout the account history.
Instead, read the materializer's already-computed cash_balance for each date
(derived from holdings via the reverse calculator) and use only IBKR's total
as an authoritative balance anchor. This is consistent with how present-day
balances are handled and requires no weekend/holiday filtering since IBKR does
not emit weekend rows and holiday totals are legitimate data points.
Also accept equity summary rows without an explicit currency field (some Flex
configurations omit it) and explicitly reject BASE_SUMMARY aggregate rows.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: simplify boolean coercion in import_commission_transaction
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ibkr): cover trailing weekend gap and align qty with valid lots
HistoricalBalancesSync: extend fill_gaps to account.current_anchor_date
so days after the last equity summary row (e.g. Saturday/Sunday when a
sync runs over the weekend) are also overridden rather than left with the
materializer's stale total=cash value.
HoldingsProcessor: replace separate quantity sum + weighted_cost_basis_for
with a single valid_lots method that computes both from the same set of
parseable lots. Previously a lot with a valid position but unparseable
cost_basis_price was excluded from the cost basis calculation but still
counted in quantity, producing inconsistent qty/cost_basis values.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* review: address PR feedback on ibkr fix branch
- Remove all "Fix N:" review-artifact comment labels
- Add Sentry.capture_message for silenced anchor repair failure so it surfaces in production monitoring
- Add Rails.logger.warn for zero/nil total rows skipped in HistoricalBalancesSync
- Document normalize_to_last_trading_day holiday limitation and why gap-fill covers it
- Rewrite two non-obvious comments to stand alone without the label prefix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: remove alignment padding in balance_rows hash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ibkr): address two P1 review findings
- Allow zero and negative equity summary totals through HistoricalBalancesSync
so fully-liquidated and margin accounts are not silently skipped (which would
cause fill_gaps to propagate a stale non-zero total forward).
- Remove normalize_to_last_trading_day from HoldingsProcessor: shifting weekend
report_dates to Friday caused Balance::SyncCache#get_holdings_value to find
no holdings on Saturday/Sunday (exact-date lookup), collapsing non_cash to
zero — reintroducing the very oscillation the fix was meant to prevent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(ibkr): add tests for historical balances sync and data helpers
- Add test case to verify non-cash balance calculation in historical balances sync
- Add test case to ensure rows with unparseable or nil totals are skipped
- Add new test file for IBKR data helpers
* fix(ibkr): prevent date range overflow during historical sync
Adjust the calculation of `last_date` in `HistoricalBalancesSync` to
ensure it does not exceed the current anchor date or today's date.
This prevents the sync process from attempting to fetch or process
future dates, which was causing oscillations in weekend data.
Also remove the conditional check for Sentry before capturing
error messages in the account processor.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Color picker had four hardcoded English strings ("Color", "Icon",
"Poor contrast, choose darker color or", "auto-adjust."). Move them
under `goals.color_picker.*` and call them through `t()`. CLAUDE.md
requires every user-facing string go through i18n.
- Status pill duplicated its visible label in `aria-label`, which makes
screen readers ignore the visible text. Drop the override so the
visible label is the accessible name.
- Goal: `display_status` and `projection_summary` memoize a value that
depends on the AASM state column. Without resetting them after a
transition the same instance keeps returning the pre-transition value.
Hook `after_all_transitions :reset_state_dependent_caches!` undoes the
memos so post-`archive!` / post-`pause!` reads see the new state.
- SweepExpiredGoalPledgesJob: the inner rescue covered per-pledge failures
but not cursor-phase failures (DB blip, OOM mid-batch). Add an outer
rescue that reports + re-raises so Sentry sees the failure and Sidekiq
retries the job.
- Add hex-format validation on Goal#color so submissions can't smuggle
arbitrary CSS into the style attribute on the avatar / picker preview.
The picker accepts custom hexes, so format validation (not inclusion)
is the right shape — anything not matching #RRGGBB is rejected at
the model boundary.
- Fix the on_delete in the down block of drop_goal_contributions to
match the original cascade. Restoring with restrict was a schema
drift that would have shifted referential behavior after a rollback.
* add missing Hungarian translations for newly extracted strings
Replace hard-coded UI strings with I18n lookups across controllers, models and views (breadcrumbs, dashboard, reports, settings, transactions, balance sheet, MFA status). Update models to use translations for category defaults, account/display names, classification group and period labels; remove a few hardcoded display_name methods. Add and update numerous locale files (English and extensive Hungarian translations, plus model/view/doorkeeper entries) to provide the required keys. These changes centralize copy for localization and prepare the app for Hungarian/English UI text.
* Pluralize account type labels; tidy Crypto model
Update English locale account type labels to use plural forms for consistency (Investment(s), Properties, Vehicles, Other Assets, Credit Cards, Loans, Other Liabilities). Also remove an extra blank line in app/models/crypto.rb to tidy up formatting.
* Back to singular
* fix(i18n): separate singular and group account labels
* Update _accountable_group.html.erb
* Use I18n plural names for account types
Change Accountable#display_name to look up pluralized account type names via I18n (accounts.types_plural.<underscored_class>) with a fallback to the legacy display logic. Add legacy_display_name helper to preserve previous behavior (singular for Depository and Crypto, pluralized otherwise). Add corresponding types_plural entries in English and Hungarian locale files for various account types.
---------
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
Co-authored-by: sure-admin <sure-admin@splashblot.com>
Goals-specific styling doesn't belong in the global stylesheet. Extract
the avatar tint + theme-aware text color into a topical goals.css and
@import it alongside the other feature CSS files.
Wraps the conditional + dot wiring into a single call so adding a new
beta nav entry doesn't require remembering to set `beta: true` by hand
or duplicating the `beta_features_enabled?` check. Naming mirrors the
existing `BetaGateable` concern.
The nav-item partial already supports a `beta: true` local that overlays
the DS::Pill dot on the icon, but the gating guide didn't show how to
wire a gated nav entry through it. Add a short "Gating the main nav"
section with the compact-array pattern, and mention the flag in the GA
removal checklist.
- Bind CSS `color-scheme` to Sure's `data-theme` attribute so the pill's
`light-dark()` resolves to the side that matches the active theme. In
the dark theme it was previously falling back to the light branch.
- Darken light-mode pill text 30% with black on top of the 700 stop so
the 10–11px uppercase label reads against the violet-50 background.
- Drop the Beta pill from the Goal detail page header. A single goal is
not the feature; the pill belongs on the feature index, not on each
record.
Pass beta: true on gated nav items so the nav_item partial renders a
violet dot-only pill in the top-right of the icon. The doc covers the
dot_only usage; the nav itself was never wired up before merge.
Add require_beta_features! to GoalsController and GoalPledgesController,
hide the Goals nav item for non-beta users, and tag index/show headers
with the Beta pill marker. Update controller tests to enable the
preference in setup and assert the redirect for users without access.
* feat: beta features toggle + Beta pill primitive
Adds the infrastructure for self-service beta opt-in. No call sites yet:
this PR is meant to land first so feature PRs (Goals, etc.) can ship
behind the gate incrementally.
User opts in via a single toggle at the bottom of Settings → Preferences.
The flag persists in the existing `users.preferences` JSONB column under
`beta_features_enabled` — same shape as `dashboard_two_column` and
`show_split_grouped`, so no migration is needed.
Controllers gate a beta feature by adding `before_action
:require_beta_features!` from the new `BetaGateable` concern (included in
ApplicationController). Views use the `beta_features_enabled?` helper to
hide / show nav items, banners, etc. Logged-out callers always return
false.
Ships `DS::BetaPill`, a small inline marker for tagging features as
Beta / Canary in nav, headers, and lists. Five tones (violet by default,
indigo, fuchsia, amber, gray) map to existing Sure color tokens — no raw
hex. Three styles (soft / filled / outline) and two sizes (sm / md) cover
the surfaces in the design handoff. The `dot_only:` mode renders just
the colored dot for use on a collapsed sidebar.
* review: rename to DS::Pill, fix CR/Codex nits, add tests
CodeRabbit + Codex review feedback:
- Rename DS::BetaPill → DS::Pill. The component was already generic in
shape (tones, styles, sizes); the name was misleading scope. "Beta"
becomes the default label (still i18n-driven). Goals' StatusPill can
later refactor onto this primitive without a third pill.
- Localize the default pill label via i18n (`ds.pill.default_label`)
instead of hard-coding English.
- Add role="img" to the dot-only span so the aria-label is consistently
exposed to assistive tech.
- Wrap the Preferences toggle row in <label for="…"> so the title and
description become an honest click target for the toggle (matches the
cursor-pointer affordance).
- Drop arbitrary Tailwind values (py-[3px], gap-[5px], tracking-[…]) in
favor of scale tokens. text-[10/11px] stays because the pill is
intentionally sub-12px (Sure's smallest scale token is text-xs / 12px)
to read as a marker, not a label.
- Add User#beta_features_enabled? predicate tests covering default-off,
explicit-true, and non-boolean truthy values.
Won't fix:
- Palette refs (`--color-violet-*` etc.). Sure has no semantic Beta/
Canary tokens; introducing them in this PR would be a design-system
change beyond the scope. The component centralizes palette use in one
`palette` method, matching the existing pattern in
Goals::StatusPillComponent.
* review: consistent title fallback in full-pill branch
* docs: how to gate a feature behind the beta toggle
* docs: unwrap doc lines to match existing style
* chore(preview): run Cloudflare PR previews on basic instances (#1831)
* fix(preview): use Rails health endpoint for container ping (#1823)
* fix(preview): use Rails health endpoint for container ping
* fix(preview): point container ping to localhost/up
---------
Co-authored-by: Sure Admin (bot) <sure-admin@splashblot.com>
When every active goal already hit its target, the "Goals on track"
tile read "0 of 2 · 2 reached" — logically correct but emotionally
upside-down. Reached goals aren't being tracked toward pace anymore;
they belong in the trophy column, not in the fraction.
- New `tracked_total` excludes reached and paused goals from the
denominator. Paused stops the pace clock on purpose; reached has
already cleared it.
- When `tracked_total` hits zero and at least one goal is reached, the
tile swaps to a celebratory empty state ("All caught up · N reached")
instead of trying to render a fraction with no denominator.
- Drop "reached" from the subline when the fraction is calculable. The
fraction is a needle, "N reached" is a trophy — surfacing them
together muddied the message. Reached only appears in the all-caught-
up empty state from here on.
Active-first / reached-last grid order already drops out of the
existing ACTIVE_STATUS_RANK sort (reached defaults to the lowest rank
so it naturally lands after behind / on_track / no_target_date /
paused).
Three issues raised on PR #1798 review:
- ProviderImportAdapter now memoizes account.goal_accounts.exists?
per-account so a bulk historical import on an unlinked account
short-circuits the reconciler instead of paying one SELECT per row.
Linked accounts still hit the per-row reconciler with no change.
- goal_projection_chart_controller.js reads Today / Projected /
Saved labels via Stimulus values fed from
goals.show.projection.* locale keys instead of inlining English.
- goal_test.rb now covers Goal#pace with real inflows, asserting
the 90-day window cutoff plus the Transaction.excluding_pending
and entries.excluded = false filters.
* Extract hardcoded strings to i18n
Replace numerous hardcoded English strings with I18n lookups (t / I18n.t) across controllers, views, helpers, and components, and convert model validation error messages to symbol keys. Added multiple locale files under config/locales for models and views. This centralizes user-facing notices/alerts, UI text, import/validation messages, and prepares the app for localization and easier translation maintenance.
* Update en.yml
* Update preview-cleanup.yml
* Revert "Update preview-cleanup.yml"
This reverts commit 1ba6d3c34c.
* test: align i18n assertions with translated messages
* Standardize balance error key and tweak locales
Replace SophtronAccount's :requires_balance error key with :no_balance and update related locale strings for sophtron, plaid, and simplefin accounts to use the new key and clearer copy. Also switch the QIF upload redirect notice to use a relative translation key (t('.qif_uploaded')), remove an unused SSO providers help line, and fix a trailing-newline/whitespace issue in the subscriptions locale. These changes standardize validation keys and improve translation consistency and messaging.
---------
Co-authored-by: KiloClaw <kiloclaw@openclaw.ai>
* feat: add Cloudflare Containers PR preview deployments
Add GitHub workflows to automatically deploy PRs to Cloudflare
Containers after tests pass, with automatic cleanup after 24 hours.
Components:
- workers/preview/: Cloudflare Worker entry point that routes
traffic to the Rails container
- preview-deploy.yml: Deploys PRs after CI passes, comments
preview URL on PR
- preview-cleanup.yml: Cleans up previews on PR close or after
24 hours via scheduled job
The container sleeps after 30 minutes of inactivity and wakes
automatically on the next request.
Required secrets:
- CLOUDFLARE_API_TOKEN
- CLOUDFLARE_ACCOUNT_ID
- CLOUDFLARE_WORKERS_SUBDOMAIN
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: use development environment with embedded PostgreSQL for previews
- Add preview-specific Dockerfile with PostgreSQL server included
- Add docker-entrypoint.sh to start PostgreSQL and run migrations
- Change RAILS_ENV from production to development
- Auto-generate SECRET_KEY_BASE and DATABASE_URL for self-contained previews
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* feat: add Redis to preview container
- Install redis-server in the preview Dockerfile
- Start Redis in the entrypoint before PostgreSQL
- Auto-configure REDIS_URL for Sidekiq background jobs
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: mark GitHub deployment inactive on manual PR cleanup
When using workflow_dispatch with a specific pr_number, the workflow
now also marks the associated GitHub deployment as inactive, mirroring
the behavior of the batch cleanup path.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: remove npm cache config that requires missing lockfile
The setup-node action's cache feature requires a package-lock.json
which doesn't exist in workers/preview/. Remove the cache configuration
to fix the workflow.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: only update deployment status when deployment ID exists
Add condition to check steps.deployment.outputs.result exists before
attempting to update deployment status. This prevents a JavaScript
syntax error when the deployment step fails and no ID is available.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: quote shell variables to fix SC2086 shellcheck warning
Quote the --var argument and GITHUB_OUTPUT redirection to prevent
word splitting issues.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: add permissions for deployment status operations
Add deployments: write permission to the cleanup workflow so the
GITHUB_TOKEN can list and update deployment statuses.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: specify build context for Dockerfile in wrangler config
Use object syntax for image config to set build context to repository
root, allowing the Dockerfile to reference files from both the root
(Gemfile, .ruby-version) and workers/preview/ (docker-entrypoint.sh).
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: run wrangler from repo root for correct build context
- Update workflow to run wrangler with --config flag from repo root
- Update wrangler.toml paths (main, image) to be relative to repo root
- Embed entrypoint script directly in Dockerfile using heredoc
- Remove separate docker-entrypoint.sh file
This ensures the Docker build context includes Gemfile, .ruby-version,
and other files at the repo root.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: move preview Dockerfile to repo root for correct build context
Wrangler resolves paths relative to the config file, not the current
directory. Moving Dockerfile.preview to repo root ensures:
- Build context is the repo root (where Gemfile, .ruby-version are)
- Path in wrangler.toml is ../../Dockerfile.preview (relative to config)
- Worker runs from workers/preview/ directory again
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: use find to locate pg_hba.conf instead of glob in redirection
Shell glob patterns don't work with redirection operators. Use find
to locate the actual pg_hba.conf path before writing to it.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix: enable workers_dev for preview deployments
Add workers_dev = true to make the preview worker accessible via
the workers.dev subdomain.
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* feat: enable observability for container logs
https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP
* fix preview container boot path
* fix: set preview container startup command explicitly
* fix: update preview worker compatibility date
* chore: expose preview container diagnostics
* fix: recover from stale preview container state
* fix: harden preview container startup paths
* chore: report preview startup stages
* fix: bypass stale container helper state during recovery
* fix: allow longer preview container startup
* fix: upgrade preview container runtime
* fix: use supported node version for preview deploy
* fix: use public container startup flow
* fix: simplify preview container startup
* chore: retain preview container diagnostic history
* fix: bypass systemctl redirect for postgres startup
* chore: probe rails readiness from inside preview container
* chore: capture rails process and port diagnostics
* chore: capture rails startup logs on preview timeout
* fix: align preview bind behavior with ipv6 startup model
* chore: capture preview socket state on rails timeout
* chore: capture rails wait state and child processes
* fix: launch preview with puma directly
* fix: run preview in production mode
* chore: probe preview app boot before puma
* fix: disable lookbook routes in production preview
* chore: capture ruby backtrace from hung boot probe
* fix: disable bootsnap in preview runtime
* fix: disable sidekiq web routes in production preview
* chore: trace hung preview boot probe with strace
* fix: json-escape preview telemetry payloads
* fix: pass preview telemetry env vars correctly
* chore: signal ruby child for preview boot backtrace
* fix: allow longer preview cold-start budget
* fix: skip sidekiq web requires in production preview
* chore: deploy hello world preview container
* fix(preview): restore rails image without redundant warmup
* feat(preview): seed demo dataset on boot
* ci(preview): require preview-cf label
* ci(preview): reuse pr workflow checks
* fix(preview): avoid clearing demo data in production boot
* fix(preview): tolerate already-running postgres on boot
* fix(preview): check demo user via psql during boot
* fix(preview): defer heavy demo seed until after boot
* fix(preview): move demo-user creation after rails boot
* fix(preview): fail fast on container lifecycle errors
* fix(preview): validate manual cleanup pr input
* fix(preview): parameterize preview pr number
* ci(preview): use setup-node v6
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: KiloClaw <kiloclaw@openclaw.ai>