feat(manager-core,dashboard): cascading app delete with styled confirmation modal
Deleting an app used to require zero scripts and zero domain claims —
practical for empty apps, painful for anything else. Add an opt-in
cascade so the operator can wipe an app in one click while keeping the
safe default for the no-flag case.
Backend: `DELETE /api/v1/admin/apps/{id}?force=true` runs a single
transaction that removes every script in the app (routes and execution
logs cascade via `script_id` FK), then deletes the app row (domains and
slug-history cascade off it). Without `?force=true` the handler still
returns the same `409 HasScripts { script_count }` payload it always did.
Frontend: a new `ConfirmModal.svelte` replaces the bare `window.confirm`
on this page. It's reusable — danger/neutral variants, optional
GitHub-style "type the slug to confirm" gate, ESC/backdrop cancel,
busy state, and a generic body slot — so future destructive actions can
adopt the same pattern instead of growing more browser dialogs. The app
delete confirmation now spells out exactly what disappears (script
count, domain claim list, "all routes & logs") and only enables the red
button once the slug is retyped. The domain-claim delete is also
wired through the modal so this page no longer uses `window.confirm`
anywhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,11 @@ pub trait AppRepository: Send + Sync {
|
||||
take_over_history: bool,
|
||||
) -> Result<App, ScriptRepositoryError>;
|
||||
async fn delete(&self, id: AppId) -> Result<(), ScriptRepositoryError>;
|
||||
/// Delete the app along with all its scripts (which in turn cascades
|
||||
/// routes and execution logs via their `script_id` FK). Domains and
|
||||
/// app-slug-history rows cascade off the app row itself. Runs in a
|
||||
/// single transaction so a partial delete cannot be observed.
|
||||
async fn delete_cascade(&self, id: AppId) -> Result<(), ScriptRepositoryError>;
|
||||
async fn count_scripts_in_app(&self, id: AppId) -> Result<i64, ScriptRepositoryError>;
|
||||
}
|
||||
|
||||
@@ -347,6 +352,25 @@ impl AppRepository for PostgresAppRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_cascade(&self, id: AppId) -> Result<(), ScriptRepositoryError> {
|
||||
let mut tx = self.pool.begin().await?;
|
||||
sqlx::query("DELETE FROM scripts WHERE app_id = $1")
|
||||
.bind(id.into_inner())
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
let res = sqlx::query("DELETE FROM apps WHERE id = $1")
|
||||
.bind(id.into_inner())
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
if res.rows_affected() == 0 {
|
||||
return Err(ScriptRepositoryError::Conflict(format!(
|
||||
"app {id} not found"
|
||||
)));
|
||||
}
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn count_scripts_in_app(&self, id: AppId) -> Result<i64, ScriptRepositoryError> {
|
||||
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM scripts WHERE app_id = $1")
|
||||
.bind(id.into_inner())
|
||||
|
||||
Reference in New Issue
Block a user