{"record":{"id":"77e53e2ee9ea9223","repo":"jamiepine/voicebox","slug":"cannot-modify-the-default-channel","errorCode":null,"errorMessage":"Cannot modify the default channel","messagePattern":"Cannot modify the default channel","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"warning","filePath":"backend/services/channels.py","lineNumber":121,"sourceCode":"        name=channel.name,\n        is_default=channel.is_default,\n        device_ids=data.device_ids,\n        created_at=channel.created_at,\n    )\n\n\nasync def update_channel(\n    channel_id: str,\n    data: AudioChannelUpdate,\n    db: Session,\n) -> Optional[AudioChannelResponse]:\n    \"\"\"Update an audio channel.\"\"\"\n    channel = db.query(DBAudioChannel).filter_by(id=channel_id).first()\n    if not channel:\n        return None\n    \n    if channel.is_default:\n        raise ValueError(\"Cannot modify the default channel\")\n    \n    # Update name if provided\n    if data.name is not None:\n        # Check if name already exists (excluding current channel)\n        existing = db.query(DBAudioChannel).filter(\n            DBAudioChannel.name == data.name,\n            DBAudioChannel.id != channel_id\n        ).first()\n        if existing:\n            raise ValueError(f\"Channel with name '{data.name}' already exists\")\n        channel.name = data.name\n    \n    # Update device mappings if provided\n    if data.device_ids is not None:\n        # Delete existing mappings\n        db.query(DBChannelDeviceMapping).filter_by(channel_id=channel_id).delete()\n        \n        # Add new mappings","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/services/channels.py#L103-L139","documentation":"Raised as ValueError (HTTP 400) by update_channel when the loaded channel has is_default=True. The default channel is immutable by design — its name and device mappings cannot be changed through update_channel. The guard fires after the channel is fetched but before any field mutation, so no partial write occurs.","triggerScenarios":"PUT /channels/{channel_id} targeting the channel whose is_default flag is True — typically the seeded 'Default' channel. Includes attempts that only change device_ids (the guard is unconditional on is_default, not gated on whether name is being changed).","commonSituations":"User selects the Default channel in an edit UI and saves; client auto-saves device-mapping changes without checking is_default; trying to rename the default channel.","solutions":["In the UI, disable editing for the channel with is_default=true (it's returned in AudioChannelResponse).","Route all desired device assignments through a non-default channel; create one if needed.","If you genuinely need to alter the default channel's devices, do so via direct DB seeding/migration, not this endpoint."],"exampleFix":"// before\nif (selected) await api.updateChannel(selected.id, patch);\n\n// after\nif (selected && !selected.is_default) {\n  await api.updateChannel(selected.id, patch);\n} else {\n  notify('The default channel cannot be modified');\n}","handlingStrategy":"validation","validationCode":"function isEditableChannel(ch) {\n  return Boolean(ch) && ch.is_default === false;\n}\nif (!isEditableChannel(channel)) { notify('The default channel cannot be modified'); return; }","typeGuard":"function isMutableChannel(ch) {\n  return Boolean(ch) && ch.is_default === false;\n}","tryCatchPattern":"try { await api.updateChannel(id, patch); }\ncatch (e) {\n  if (e.status === 400 && /default channel/.test(e.detail)) notify('Default channel is read-only');\n  else throw e;\n}","preventionTips":["Disable the edit form when is_default is true.","Don't include the default channel id in bulk device-mapping updates.","Alter the default channel only via seeding/migration, never this endpoint."],"tags":["api","channels","validation","business-rule","immutable"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}