{"record":{"id":"7e0939bc2c9d59c1","repo":"jamiepine/voicebox","slug":"cannot-delete-the-default-channel","errorCode":null,"errorMessage":"Cannot delete the default channel","messagePattern":"Cannot delete the default channel","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"warning","filePath":"backend/services/channels.py","lineNumber":173,"sourceCode":"    device_ids = [m.device_id for m in device_mappings]\n    \n    return AudioChannelResponse(\n        id=channel.id,\n        name=channel.name,\n        is_default=channel.is_default,\n        device_ids=device_ids,\n        created_at=channel.created_at,\n    )\n\n\nasync def delete_channel(channel_id: str, db: Session) -> bool:\n    \"\"\"Delete an audio channel.\"\"\"\n    channel = db.query(DBAudioChannel).filter_by(id=channel_id).first()\n    if not channel:\n        return False\n    \n    if channel.is_default:\n        raise ValueError(\"Cannot delete the default channel\")\n    \n    # Delete device mappings\n    db.query(DBChannelDeviceMapping).filter_by(channel_id=channel_id).delete()\n    \n    # Delete profile-channel mappings\n    db.query(DBProfileChannelMapping).filter_by(channel_id=channel_id).delete()\n    \n    # Delete channel\n    db.delete(channel)\n    db.commit()\n    \n    return True\n\n\nasync def get_channel_voices(channel_id: str, db: Session) -> List[str]:\n    \"\"\"Get list of profile IDs assigned to a channel.\"\"\"\n    mappings = db.query(DBProfileChannelMapping).filter_by(\n        channel_id=channel_id","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/services/channels.py#L155-L191","documentation":"Raised as ValueError (HTTP 400) by delete_channel when the channel's is_default=True. The default channel is protected from deletion so an audio output always exists. The guard runs after the lookup but before any DELETE of mappings, so no partial cleanup occurs on rejection. A non-existent channel returns False (surfaced as 404 by the route), not this error.","triggerScenarios":"DELETE /channels/{channel_id} where channel_id is the default channel (is_default=True).","commonSituations":"User clicks delete on the Default channel; client offers a delete affordance on every channel without filtering is_default; bulk-delete loop includes the default id.","solutions":["Hide/disable the delete action for the channel with is_default=true in the UI.","In bulk operations, filter out default channel ids before issuing DELETEs.","If a different default is needed, seed/migrate a new default rather than deleting this one."],"exampleFix":"// before\nfor (const id of selectedIds) await api.deleteChannel(id);\n\n// after\nfor (const id of selectedIds) {\n  const ch = channels.find(c => c.id === id);\n  if (ch?.is_default) continue;\n  await api.deleteChannel(id);\n}","handlingStrategy":"validation","validationCode":"function isDeletableChannel(ch) {\n  return Boolean(ch) && ch.is_default === false;\n}\nconst targets = selected.filter(isDeletableChannel);\nif (!targets.length) { notify('Select a non-default channel to delete'); return; }","typeGuard":"function isRemovableChannel(ch) {\n  return Boolean(ch) && ch.is_default === false;\n}","tryCatchPattern":"try { await api.deleteChannel(id); }\ncatch (e) {\n  if (e.status === 400 && /default channel/.test(e.detail)) notify('Default channel cannot be deleted');\n  else if (e.status === 404) { dropChannel(id); }\n  else throw e;\n}","preventionTips":["Hide/disable delete for is_default channels.","Filter default ids out of bulk delete sets.","Seed a new default via migration if a different default is required."],"tags":["api","channels","validation","business-rule","immutable"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}