feat(#94): implement spoke configuration UI

Implements the final piece of M7-Federation - the spoke configuration UI
that allows administrators to configure their local instance's federation
capabilities and settings.

Backend Changes:
- Add UpdateInstanceDto with validation for name, capabilities, and metadata
- Implement FederationService.updateInstanceConfiguration() method
- Add PATCH /api/v1/federation/instance endpoint to FederationController
- Add audit logging for configuration updates
- Add tests for updateInstanceConfiguration (5 new tests, all passing)

Frontend Changes:
- Create SpokeConfigurationForm component with PDA-friendly design
- Create /federation/settings page with configuration management
- Add regenerate keypair functionality with confirmation dialog
- Extend federation API client with updateInstanceConfiguration and regenerateInstanceKeys
- Add comprehensive tests (10 tests, all passing)

Design Decisions:
- Admin-only access via AdminGuard
- Never expose private key in API responses (security)
- PDA-friendly language throughout (no demanding terms)
- Clear visual hierarchy with read-only and editable fields
- Truncated public key with copy button for usability
- Confirmation dialog for destructive key regeneration

All tests passing:
- Backend: 13/13 federation service tests passing
- Frontend: 10/10 SpokeConfigurationForm tests passing
- TypeScript compilation: passing
- Linting: passing
- PDA-friendliness: verified

This completes M7-Federation. All federation features are now implemented.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-03 14:51:59 -06:00
parent 12abdfe81d
commit 0495f979a7
33 changed files with 1660 additions and 8 deletions

View File

@@ -104,6 +104,46 @@ export class FederationService {
return publicIdentity;
}
/**
* Update instance configuration
* Allows updating name, capabilities, and metadata
* Returns public identity only (no private key exposure)
*/
async updateInstanceConfiguration(updates: {
name?: string;
capabilities?: FederationCapabilities;
metadata?: Record<string, unknown>;
}): Promise<PublicInstanceIdentity> {
const instance = await this.getInstanceIdentity();
// Build update data object
const data: Prisma.InstanceUpdateInput = {};
if (updates.name !== undefined) {
data.name = updates.name;
}
if (updates.capabilities !== undefined) {
data.capabilities = updates.capabilities as Prisma.JsonObject;
}
if (updates.metadata !== undefined) {
data.metadata = updates.metadata as Prisma.JsonObject;
}
const updatedInstance = await this.prisma.instance.update({
where: { id: instance.id },
data,
});
this.logger.log(`Instance configuration updated: ${JSON.stringify(updates)}`);
// Return public identity only (security fix)
const identity = this.mapToInstanceIdentity(updatedInstance);
const { privateKey: _privateKey, ...publicIdentity } = identity;
return publicIdentity;
}
/**
* Create a new instance identity
*/