chore: drop the A6 rollback tombstones
A6 deployed cleanly and the provider keys are gone from the production `.env`, which makes the legacy collector inert regardless of any settings row. The two tombstones migration 029 pinned have no remaining job, and nothing in the codebase reads either key. Migration 030 deletes them and drops the Admin filter that hid them. Unlike 029's, its downgrade is meaningful — it restores both rows at their safe values, since going back past this revision means going back toward code that reads them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
"""Drop the A6 rollback tombstones
|
||||||
|
|
||||||
|
Revision ID: 030
|
||||||
|
Revises: 029
|
||||||
|
Create Date: 2026-08-07 00:00:00.000000
|
||||||
|
|
||||||
|
Migration ``029`` kept two SystemSetting rows alive as rollback tombstones,
|
||||||
|
pinned to the values a pre-A6 process needed to behave safely. A6 is deployed
|
||||||
|
and healthy, and the provider keys are gone from the production ``.env`` — which
|
||||||
|
makes the legacy collector inert regardless of any settings row — so the
|
||||||
|
tombstones have no remaining job.
|
||||||
|
|
||||||
|
Nothing in the current codebase reads either key.
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = "030"
|
||||||
|
down_revision: Union[str, None] = "029"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
# The safe values 029 pinned. Kept here so downgrade restores real protection
|
||||||
|
# rather than leaving a rolled-back process reading absent rows permissively.
|
||||||
|
_TOMBSTONES: dict[str, str] = {
|
||||||
|
"fundamental_data_sec_dolt_cutover_enabled": "true",
|
||||||
|
"job_fundamental_collector_enabled": "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
_settings = sa.table(
|
||||||
|
"system_settings",
|
||||||
|
sa.column("id", sa.Integer),
|
||||||
|
sa.column("key", sa.String),
|
||||||
|
sa.column("value", sa.Text),
|
||||||
|
sa.column("updated_at", sa.DateTime(timezone=True)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
for key in _TOMBSTONES:
|
||||||
|
row = conn.execute(
|
||||||
|
sa.select(_settings.c.value).where(_settings.c.key == key)
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
print(f"a6_tombstone_drop {key}: absent", flush=True)
|
||||||
|
continue
|
||||||
|
print(f"a6_tombstone_drop {key}: {row[0]!r}", flush=True)
|
||||||
|
conn.execute(sa.delete(_settings).where(_settings.c.key == key))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Restore the tombstones at their safe values.
|
||||||
|
|
||||||
|
Unlike 029's no-op downgrade, this one is meaningful: going back past this
|
||||||
|
revision implies going back toward code that still reads these keys.
|
||||||
|
"""
|
||||||
|
conn = op.get_bind()
|
||||||
|
now = sa.func.now()
|
||||||
|
for key, pinned in _TOMBSTONES.items():
|
||||||
|
exists = conn.execute(
|
||||||
|
sa.select(_settings.c.id).where(_settings.c.key == key)
|
||||||
|
).fetchone()
|
||||||
|
if exists is None:
|
||||||
|
conn.execute(
|
||||||
|
sa.insert(_settings).values(key=key, value=pinned, updated_at=now)
|
||||||
|
)
|
||||||
@@ -432,9 +432,10 @@ workstream B — Alpaca remains the price source throughout.
|
|||||||
remaining production action is flipping that switch on and observing it.
|
remaining production action is flipping that switch on and observing it.
|
||||||
- A6. **DONE 2026-08-07.** FMP/Finnhub/Alpha Vantage removed, along with the
|
- A6. **DONE 2026-08-07.** FMP/Finnhub/Alpha Vantage removed, along with the
|
||||||
weekly `fundamental_collector` job, the A5 cutover toggle (SEC+Dolt is now the
|
weekly `fundamental_collector` job, the A5 cutover toggle (SEC+Dolt is now the
|
||||||
unconditional path) and the parity report. Migration `029` tombstones the two
|
unconditional path) and the parity report. Migration `029` tombstoned the two
|
||||||
behavior-bearing settings rows for the rollback window; the archived parity
|
behavior-bearing settings rows for the rollback window and `030` dropped them
|
||||||
bundles stay as the A5 evidence trail.
|
once the deploy was confirmed healthy; the archived parity bundles stay as the
|
||||||
|
A5 evidence trail.
|
||||||
|
|
||||||
**Workstream B (independent, start when wanted):**
|
**Workstream B (independent, start when wanted):**
|
||||||
|
|
||||||
@@ -514,8 +515,8 @@ observed in production, so the legacy providers, their config/env keys, the week
|
|||||||
collector job and the parity report were all removed. Two consequences to carry:
|
collector job and the parity report were all removed. Two consequences to carry:
|
||||||
(1) `fundamental_data` now has no provider fallback — recovery is restore-from-backup;
|
(1) `fundamental_data` now has no provider fallback — recovery is restore-from-backup;
|
||||||
(2) disabling **SEC Fundamentals Import** stops the SEC fetch only, because the local
|
(2) disabling **SEC Fundamentals Import** stops the SEC fetch only, because the local
|
||||||
cache refresh was deliberately moved outside the job-enable check. Remaining
|
cache refresh was deliberately moved outside the job-enable check. No follow-ups
|
||||||
follow-up: delete the migration-029 tombstone rows once the rollback window closes.
|
remain: migration `030` dropped the tombstone rows after the deploy was verified.
|
||||||
|
|
||||||
**Known caveats to carry (documented in the findings report, not bugs to fix):**
|
**Known caveats to carry (documented in the findings report, not bugs to fix):**
|
||||||
- KLAC-class post-filing splits: P/E wrong until the next 10-Q; undetectable from
|
- KLAC-class post-filing splits: P/E wrong until the next 10-Q; undetectable from
|
||||||
|
|||||||
@@ -180,21 +180,6 @@ WHERE dimension = 'fundamental'
|
|||||||
GROUP BY dimension, is_stale;
|
GROUP BY dimension, is_stale;
|
||||||
```
|
```
|
||||||
|
|
||||||
### Retired settings rows (delete later)
|
|
||||||
|
|
||||||
Migration `029` pinned two SystemSetting rows as rollback tombstones rather than
|
|
||||||
deleting them, because migrations run before the service restarts and a
|
|
||||||
rolled-back pre-A6 process reads an absent `job_..._enabled` row as *enabled*:
|
|
||||||
|
|
||||||
| key | pinned value |
|
|
||||||
|---|---|
|
|
||||||
| `fundamental_data_sec_dolt_cutover_enabled` | `true` |
|
|
||||||
| `job_fundamental_collector_enabled` | `false` |
|
|
||||||
|
|
||||||
Neither controls anything now; Admin -> Settings hides both. Once the A6
|
|
||||||
rollback window has closed, delete the rows and the `MANAGED_SETTINGS` filter in
|
|
||||||
`frontend/src/components/admin/SettingsForm.tsx`.
|
|
||||||
|
|
||||||
## Failure and rollback
|
## Failure and rollback
|
||||||
|
|
||||||
- **There is no provider fallback any more, and no Admin switch that freezes the
|
- **There is no provider fallback any more, and no Admin switch that freezes the
|
||||||
|
|||||||
@@ -3,14 +3,6 @@ import { useSettings, useUpdateSetting } from '../../hooks/useAdmin';
|
|||||||
import { SkeletonTable } from '../ui/Skeleton';
|
import { SkeletonTable } from '../ui/Skeleton';
|
||||||
import type { SystemSetting } from '../../lib/types';
|
import type { SystemSetting } from '../../lib/types';
|
||||||
|
|
||||||
// Retired keys kept in the database as rollback tombstones (migration 029).
|
|
||||||
// They no longer control anything; hide them so nobody edits a dead switch.
|
|
||||||
// Delete both the rows and this filter once the A6 rollback window has closed.
|
|
||||||
const MANAGED_SETTINGS = new Set([
|
|
||||||
'fundamental_data_sec_dolt_cutover_enabled',
|
|
||||||
'job_fundamental_collector_enabled',
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function SettingsForm() {
|
export function SettingsForm() {
|
||||||
const { data: settings, isLoading, isError, error } = useSettings();
|
const { data: settings, isLoading, isError, error } = useSettings();
|
||||||
const updateSetting = useUpdateSetting();
|
const updateSetting = useUpdateSetting();
|
||||||
@@ -40,11 +32,10 @@ export function SettingsForm() {
|
|||||||
if (isLoading) return <SkeletonTable rows={4} cols={2} />;
|
if (isLoading) return <SkeletonTable rows={4} cols={2} />;
|
||||||
if (isError) return <p className="text-sm text-red-400">{(error as Error)?.message || 'Failed to load settings'}</p>;
|
if (isError) return <p className="text-sm text-red-400">{(error as Error)?.message || 'Failed to load settings'}</p>;
|
||||||
if (!settings || settings.length === 0) return <p className="text-sm text-gray-500">No settings found.</p>;
|
if (!settings || settings.length === 0) return <p className="text-sm text-gray-500">No settings found.</p>;
|
||||||
const visibleSettings = settings.filter((setting) => !MANAGED_SETTINGS.has(setting.key));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{visibleSettings.map((setting) => (
|
{settings.map((setting) => (
|
||||||
<div key={setting.key} className="glass p-4 flex flex-wrap items-center gap-3 glass-hover">
|
<div key={setting.key} className="glass p-4 flex flex-wrap items-center gap-3 glass-hover">
|
||||||
<label className="min-w-[140px] text-sm font-medium text-gray-300">{setting.key}</label>
|
<label className="min-w-[140px] text-sm font-medium text-gray-300">{setting.key}</label>
|
||||||
{setting.key === 'registration' ? (
|
{setting.key === 'registration' ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user