Skip to content

Commit 0c60033

Browse files
authored
Merge branch 'main' into feat/cors-policy-management-issue-1000
2 parents a41dbcb + 8b322a1 commit 0c60033

167 files changed

Lines changed: 34890 additions & 3961 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Disaster Recovery Automation
2+
3+
# Automated disaster-recovery routine:
4+
# 1. Runs a full DR drill (backup → verify → restore + monitor health check)
5+
# 2. Creates a DR backup (with pre-check)
6+
# 3. Captures and uploads DR status + backup artefacts
7+
#
8+
# Wired to a schedule so backups/status are taken on a routine cadence
9+
# independent of human action, plus a manual dispatch for on-demand runs.
10+
11+
on:
12+
schedule:
13+
# Daily at 03:17 UTC
14+
- cron: '17 3 * * *'
15+
workflow_dispatch:
16+
17+
env:
18+
NODE_VERSION: '20'
19+
20+
jobs:
21+
dr-routine:
22+
name: DR Backup + Status
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Checkout code
26+
uses: actions/checkout@v7
27+
with:
28+
fetch-depth: 0
29+
30+
- name: Setup Node.js
31+
uses: actions/setup-node@v7
32+
with:
33+
node-version: ${{ env.NODE_VERSION }}
34+
cache: 'npm'
35+
36+
- name: Install dependencies
37+
run: npm ci --legacy-peer-deps
38+
39+
- name: Run DR drill (backup → verify → restore + health)
40+
run: node scripts/dr-test.js
41+
continue-on-error: true
42+
43+
- name: Create DR backup (with pre-check)
44+
run: ./scripts/dr-backup.sh --pre-check --region "${DR_REGION:-us-east-1}" --env "${DR_ENVIRONMENT:-production}"
45+
env:
46+
DR_REGION: us-east-1
47+
DR_ENVIRONMENT: production
48+
49+
- name: Capture DR status (JSON)
50+
run: |
51+
STATUS_FILE="dr-status-${{ github.run_id }}.json"
52+
./scripts/dr-status.sh --json > "$STATUS_FILE" 2>&1 || true
53+
echo "STATUS_FILE=$STATUS_FILE" >> "$GITHUB_ENV"
54+
id: status
55+
56+
- name: Upload DR backup artefact
57+
if: always()
58+
uses: actions/upload-artifact@v7
59+
with:
60+
name: dr-backups-${{ github.run_id }}
61+
path: |
62+
.dr-backups/*.tar.gz
63+
.dr-recovery-log.jsonl
64+
${{ env.STATUS_FILE }}
65+
66+
- name: Notify on degraded/critical DR health
67+
if: always()
68+
run: |
69+
if ! ./scripts/dr-status.sh --short; then
70+
echo "::warning::DR system is in a degraded/critical state — review the DR status artefact."
71+
else
72+
echo "DR system is healthy."
73+
fi

app/screens/BatchOperationsScreen.tsx

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,299 @@ import {
2525
exportBatchResultToCsv as exportCsv,
2626
} from '../stores/batchStore';
2727
import { colors, spacing, typography, borderRadius } from '../../src/utils/constants';
28+
import {
29+
useAtomicBatch,
30+
validateAtomicBatch,
31+
type AtomicBatchItem,
32+
type AtomicBatchReport,
33+
type AtomicBatchStatus,
34+
} from '../services/atomicBatchService';
35+
36+
// ── Atomic status colour map ──────────────────────────────────────────────
37+
38+
const ATOMIC_STATUS_COLORS: Record<AtomicBatchStatus, string> = {
39+
idle: colors.textSecondary,
40+
validating: colors.warning,
41+
snapshotting: colors.warning,
42+
executing: colors.primary,
43+
committing: colors.primary,
44+
rolling_back: colors.error,
45+
committed: colors.success,
46+
rolled_back: colors.error,
47+
failed: colors.error,
48+
};
49+
50+
// ── AtomicExecutionPanel ──────────────────────────────────────────────────
51+
52+
interface AtomicExecutionPanelProps {
53+
operationType: BatchOperationType;
54+
subscriptionIds: string[];
55+
}
56+
57+
const AtomicExecutionPanel: React.FC<AtomicExecutionPanelProps> = ({
58+
operationType,
59+
subscriptionIds,
60+
}) => {
61+
const { runAtomic, isBusy } = useAtomicBatch();
62+
const [report, setReport] = React.useState<AtomicBatchReport | null>(null);
63+
const [status, setStatus] = React.useState<AtomicBatchStatus>('idle');
64+
const [validationErrors, setValidationErrors] = React.useState<string[]>([]);
65+
66+
const items: AtomicBatchItem[] = subscriptionIds.map((sid, idx) => ({
67+
id: `item_${idx}`,
68+
subscriptionId: sid,
69+
operation: operationType,
70+
payload: {},
71+
}));
72+
73+
const handleRunAtomic = React.useCallback(async () => {
74+
setValidationErrors([]);
75+
const validation = validateAtomicBatch(items);
76+
if (!validation.valid) {
77+
setValidationErrors(validation.errors);
78+
return;
79+
}
80+
setStatus('executing');
81+
try {
82+
const result = await runAtomic(`batch_${Date.now()}`, items, {
83+
failFast: true,
84+
concurrency: 1,
85+
timeoutPerItemMs: 10_000,
86+
});
87+
setReport(result);
88+
setStatus(result.status);
89+
} catch {
90+
setStatus('failed');
91+
}
92+
}, [items, runAtomic]);
93+
94+
const handleReset = () => {
95+
setReport(null);
96+
setStatus('idle');
97+
setValidationErrors([]);
98+
};
99+
100+
const statusColor = ATOMIC_STATUS_COLORS[status];
101+
102+
return (
103+
<View style={atomicStyles.panel}>
104+
<View style={atomicStyles.panelHeader}>
105+
<Text style={atomicStyles.panelTitle}>⚛ Atomic Execution</Text>
106+
<View style={[atomicStyles.statusBadge, { backgroundColor: statusColor + '22', borderColor: statusColor }]}>
107+
<Text style={[atomicStyles.statusText, { color: statusColor }]}>
108+
{status.toUpperCase().replace('_', ' ')}
109+
</Text>
110+
</View>
111+
</View>
112+
113+
<Text style={atomicStyles.description}>
114+
Atomic mode executes all {subscriptionIds.length} item
115+
{subscriptionIds.length !== 1 ? 's' : ''} as a single unit. Any failure
116+
will automatically roll back all previously applied changes.
117+
</Text>
118+
119+
{validationErrors.length > 0 && (
120+
<View style={atomicStyles.errorBox}>
121+
{validationErrors.map((e, i) => (
122+
<Text key={i} style={atomicStyles.errorText}>{e}</Text>
123+
))}
124+
</View>
125+
)}
126+
127+
{report && (
128+
<View style={atomicStyles.reportBox}>
129+
<View style={atomicStyles.reportRow}>
130+
<Text style={atomicStyles.reportLabel}>Total items</Text>
131+
<Text style={atomicStyles.reportValue}>{report.totalItems}</Text>
132+
</View>
133+
<View style={atomicStyles.reportRow}>
134+
<Text style={atomicStyles.reportLabel}>Succeeded</Text>
135+
<Text style={[atomicStyles.reportValue, { color: colors.success }]}>
136+
{report.succeededItems}
137+
</Text>
138+
</View>
139+
<View style={atomicStyles.reportRow}>
140+
<Text style={atomicStyles.reportLabel}>Failed</Text>
141+
<Text style={[atomicStyles.reportValue, { color: colors.error }]}>
142+
{report.failedItems}
143+
</Text>
144+
</View>
145+
{report.rolledBackItems > 0 && (
146+
<View style={atomicStyles.reportRow}>
147+
<Text style={atomicStyles.reportLabel}>Rolled back</Text>
148+
<Text style={[atomicStyles.reportValue, { color: colors.warning }]}>
149+
{report.rolledBackItems}
150+
</Text>
151+
</View>
152+
)}
153+
{report.durationMs !== undefined && (
154+
<View style={atomicStyles.reportRow}>
155+
<Text style={atomicStyles.reportLabel}>Duration</Text>
156+
<Text style={atomicStyles.reportValue}>
157+
{report.durationMs < 1000
158+
? `${report.durationMs} ms`
159+
: `${(report.durationMs / 1000).toFixed(2)} s`}
160+
</Text>
161+
</View>
162+
)}
163+
{report.rollbackReason && (
164+
<View style={atomicStyles.rollbackBox}>
165+
<Text style={atomicStyles.rollbackLabel}>Rollback reason:</Text>
166+
<Text style={atomicStyles.rollbackReason}>{report.rollbackReason}</Text>
167+
</View>
168+
)}
169+
<Text style={atomicStyles.idempotencyKey}>
170+
Key: {report.idempotencyKey}
171+
</Text>
172+
</View>
173+
)}
174+
175+
<View style={atomicStyles.buttonRow}>
176+
{status === 'idle' || status === 'failed' || status === 'committed' || status === 'rolled_back' ? (
177+
<>
178+
<TouchableOpacity
179+
style={[
180+
atomicStyles.runButton,
181+
(isBusy || subscriptionIds.length === 0) && atomicStyles.disabledButton,
182+
]}
183+
onPress={handleRunAtomic}
184+
disabled={isBusy || subscriptionIds.length === 0}>
185+
<Text style={atomicStyles.runButtonText}>
186+
{status === 'idle' ? '▶ Run Atomically' : '↺ Re-run'}
187+
</Text>
188+
</TouchableOpacity>
189+
{report && (
190+
<TouchableOpacity style={atomicStyles.resetButton} onPress={handleReset}>
191+
<Text style={atomicStyles.resetButtonText}>Reset</Text>
192+
</TouchableOpacity>
193+
)}
194+
</>
195+
) : (
196+
<ActivityIndicator color={colors.primary} />
197+
)}
198+
</View>
199+
</View>
200+
);
201+
};
202+
203+
// ── Styles for AtomicExecutionPanel ───────────────────────────────────────
204+
205+
const atomicStyles = StyleSheet.create({
206+
panel: {
207+
margin: spacing.md,
208+
padding: spacing.md,
209+
backgroundColor: colors.surface,
210+
borderRadius: borderRadius.md,
211+
borderWidth: 1,
212+
borderColor: colors.primary + '44',
213+
},
214+
panelHeader: {
215+
flexDirection: 'row',
216+
alignItems: 'center',
217+
justifyContent: 'space-between',
218+
marginBottom: spacing.sm,
219+
},
220+
panelTitle: {
221+
...typography.h3,
222+
color: colors.text,
223+
fontWeight: '700',
224+
},
225+
statusBadge: {
226+
paddingHorizontal: spacing.sm,
227+
paddingVertical: 2,
228+
borderRadius: borderRadius.round,
229+
borderWidth: 1,
230+
},
231+
statusText: {
232+
...typography.small,
233+
fontWeight: '700',
234+
letterSpacing: 0.5,
235+
},
236+
description: {
237+
...typography.body,
238+
color: colors.textSecondary,
239+
marginBottom: spacing.sm,
240+
},
241+
errorBox: {
242+
backgroundColor: colors.error + '18',
243+
padding: spacing.sm,
244+
borderRadius: borderRadius.sm,
245+
marginBottom: spacing.sm,
246+
},
247+
errorText: {
248+
...typography.caption,
249+
color: colors.error,
250+
},
251+
reportBox: {
252+
backgroundColor: colors.surfaceVariant,
253+
padding: spacing.sm,
254+
borderRadius: borderRadius.sm,
255+
marginBottom: spacing.sm,
256+
gap: spacing.xs,
257+
},
258+
reportRow: {
259+
flexDirection: 'row',
260+
justifyContent: 'space-between',
261+
},
262+
reportLabel: {
263+
...typography.caption,
264+
color: colors.textSecondary,
265+
},
266+
reportValue: {
267+
...typography.caption,
268+
color: colors.text,
269+
fontWeight: '600',
270+
},
271+
rollbackBox: {
272+
marginTop: spacing.xs,
273+
},
274+
rollbackLabel: {
275+
...typography.small,
276+
color: colors.error,
277+
fontWeight: '600',
278+
},
279+
rollbackReason: {
280+
...typography.small,
281+
color: colors.textSecondary,
282+
},
283+
idempotencyKey: {
284+
...typography.small,
285+
color: colors.textSecondary,
286+
marginTop: spacing.xs,
287+
fontFamily: 'monospace',
288+
},
289+
buttonRow: {
290+
flexDirection: 'row',
291+
gap: spacing.sm,
292+
marginTop: spacing.sm,
293+
},
294+
runButton: {
295+
flex: 1,
296+
backgroundColor: colors.primary,
297+
paddingVertical: spacing.sm,
298+
borderRadius: borderRadius.md,
299+
alignItems: 'center',
300+
},
301+
runButtonText: {
302+
...typography.button,
303+
color: colors.onPrimary,
304+
},
305+
resetButton: {
306+
paddingHorizontal: spacing.md,
307+
paddingVertical: spacing.sm,
308+
borderWidth: 1,
309+
borderColor: colors.border,
310+
borderRadius: borderRadius.md,
311+
alignItems: 'center',
312+
},
313+
resetButtonText: {
314+
...typography.button,
315+
color: colors.text,
316+
},
317+
disabledButton: {
318+
opacity: 0.45,
319+
},
320+
});
28321

29322
// ════════════════════════════════════════════════════════════════
30323
// Constants
@@ -876,6 +1169,20 @@ export const BatchOperationsScreen: React.FC = () => {
8761169
{renderResults()}
8771170
{renderAnalytics()}
8781171

1172+
{/* Issue #919 — Atomic Execution Panel */}
1173+
<AtomicExecutionPanel
1174+
operationType={draft.operationType}
1175+
subscriptionIds={
1176+
draft.csvContent
1177+
? draft.csvContent
1178+
.split('\n')
1179+
.map((l) => l.trim().split(',')[0])
1180+
.filter(Boolean)
1181+
.slice(0, 100)
1182+
: []
1183+
}
1184+
/>
1185+
8791186
<View style={styles.bottomPad} />
8801187
</ScrollView>
8811188

0 commit comments

Comments
 (0)