Initial commit: CODEX_ADK (Svrnty Console) MVP v1.0.0

This is the initial commit for the CODEX_ADK project, a full-stack AI agent
management platform featuring:

BACKEND (ASP.NET Core 8.0):
- CQRS architecture with 6 commands and 7 queries
- 16 API endpoints (all working and tested)
- PostgreSQL database with 5 entities
- AES-256 encryption for API keys
- FluentValidation on all commands
- Rate limiting and CORS configured
- OpenAPI/Swagger documentation
- Docker Compose setup (PostgreSQL + Ollama)

FRONTEND (Flutter 3.x):
- Dark theme with Svrnty branding
- Collapsible sidebar navigation
- CQRS API client with Result<T> error handling
- Type-safe endpoints from OpenAPI schema
- Multi-platform support (Web, iOS, Android, macOS, Linux, Windows)

DOCUMENTATION:
- Comprehensive API reference
- Architecture documentation
- Development guidelines for Claude Code
- API integration guides
- context-claude.md project overview

Status: Backend ready (Grade A-), Frontend integration pending

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-26 18:32:38 -04:00
commit 3fae2fcbe1
248 changed files with 19504 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
/// Svrnty Console API Client Library
///
/// Type-safe CQRS API client for communicating with the backend.
///
/// This library provides:
/// - CQRS pattern support (queries, commands, paginated queries)
/// - Functional error handling with Result&lt;T&gt;
/// - Type-safe serialization via Serializable interface
/// - OpenAPI contract-driven development
///
/// ## Quick Start
///
/// ```dart
/// import 'package:console/api/api.dart';
///
/// // Create client
/// final client = CqrsApiClient(
/// config: ApiClientConfig.development,
/// );
///
/// // Execute query
/// final result = await client.checkHealth();
///
/// result.when(
/// success: (isHealthy) => print('API healthy: $isHealthy'),
/// error: (error) => print('Error: ${error.message}'),
/// );
///
/// // Clean up
/// client.dispose();
/// ```
///
/// ## CQRS Patterns
///
/// ### Queries (Read)
/// ```dart
/// final result = await client.executeQuery<UserDto>(
/// endpoint: 'users/123',
/// query: GetUserQuery(userId: '123'),
/// fromJson: UserDto.fromJson,
/// );
/// ```
///
/// ### Commands (Write)
/// ```dart
/// final result = await client.executeCommand(
/// endpoint: 'createUser',
/// command: CreateUserCommand(name: 'John', email: 'john@example.com'),
/// );
/// ```
///
/// ### Paginated Queries (Lists)
/// ```dart
/// final result = await client.executePaginatedQuery<UserDto>(
/// endpoint: 'users',
/// query: ListUsersQuery(),
/// itemFromJson: UserDto.fromJson,
/// page: 1,
/// pageSize: 20,
/// );
/// ```
///
/// See [README_API.md] for complete documentation.
library;
// Core exports
export 'client.dart' show CqrsApiClient, ApiClientConfig;
export 'types.dart'
show
// Result type
Result,
ApiSuccess,
ApiError,
// Error types
ApiErrorInfo,
ApiErrorType,
// Pagination
PaginatedResponse,
PageInfo,
FilterCriteria,
FilterOperator,
SortCriteria,
SortDirection,
// Serialization
Serializable,
// Queries (from schema)
HealthQuery;
// Endpoint extensions
export 'endpoints/health_endpoint.dart' show HealthEndpoint, performHealthCheck;
+401
View File
@@ -0,0 +1,401 @@
/// CQRS API Client for communicating with the backend
library;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'types.dart';
// =============================================================================
// API Client Configuration
// =============================================================================
/// Configuration for the API client
class ApiClientConfig {
final String baseUrl;
final Duration timeout;
final Map<String, String> defaultHeaders;
const ApiClientConfig({
required this.baseUrl,
this.timeout = const Duration(seconds: 30),
this.defaultHeaders = const {},
});
/// Default configuration for local development
static const ApiClientConfig development = ApiClientConfig(
baseUrl: 'http://localhost:5246',
timeout: Duration(seconds: 30),
defaultHeaders: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
}
// =============================================================================
// CQRS API Client
// =============================================================================
/// HTTP client for CQRS-based API
///
/// Provides methods for executing:
/// - Queries (read operations)
/// - Commands (write operations)
/// - Paginated queries (list operations with filtering/sorting)
class CqrsApiClient {
final ApiClientConfig config;
final http.Client _httpClient;
CqrsApiClient({
required this.config,
http.Client? httpClient,
}) : _httpClient = httpClient ?? http.Client();
/// Execute a query that returns a single value
///
/// Example:
/// ```dart
/// final result = await client.executeQuery<bool>(
/// 'health',
/// HealthQuery(),
/// (json) => json as bool,
/// );
/// ```
Future<Result<TResult>> executeQuery<TResult>({
required String endpoint,
required Serializable query,
required TResult Function(Object? json) fromJson,
}) async {
try {
final url = Uri.parse('${config.baseUrl}/api/query/$endpoint');
final body = _serializeQuery(query);
final response = await _httpClient
.post(
url,
headers: config.defaultHeaders,
body: body,
)
.timeout(config.timeout);
return _handleResponse<TResult>(response, fromJson);
} on TimeoutException catch (e) {
return ApiError<TResult>(
ApiErrorInfo(
message: 'Request timeout: ${e.message ?? "Operation took too long"}',
type: ApiErrorType.timeout,
),
);
} on SocketException catch (e) {
return ApiError<TResult>(
ApiErrorInfo(
message: 'Network error: ${e.message}',
type: ApiErrorType.network,
details: e.osError?.message,
),
);
} on http.ClientException catch (e) {
return ApiError<TResult>(
ApiErrorInfo(
message: 'HTTP client error: ${e.message}',
type: ApiErrorType.http,
),
);
} on FormatException catch (e) {
return ApiError<TResult>(
ApiErrorInfo(
message: 'JSON parsing error: ${e.message}',
type: ApiErrorType.serialization,
details: e.source.toString(),
),
);
} catch (e) {
return ApiError<TResult>(
ApiErrorInfo(
message: 'Unexpected error: $e',
type: ApiErrorType.unknown,
),
);
}
}
/// Execute a paginated query that returns a list of items
///
/// Example:
/// ```dart
/// final result = await client.executePaginatedQuery<VehicleItem>(
/// 'vehicles',
/// VehiclesQuery(),
/// (json) => VehicleItem.fromJson(json as Map<String, Object?>),
/// page: 1,
/// pageSize: 20,
/// filters: [FilterCriteria(field: 'status', operator: FilterOperator.equals, value: 'active')],
/// sorting: [SortCriteria(field: 'createdAt', direction: SortDirection.descending)],
/// );
/// ```
Future<Result<PaginatedResponse<TItem>>> executePaginatedQuery<TItem>({
required String endpoint,
required Serializable query,
required TItem Function(Map<String, Object?> json) itemFromJson,
int page = 1,
int pageSize = 20,
List<FilterCriteria>? filters,
List<SortCriteria>? sorting,
}) async {
try {
final url = Uri.parse('${config.baseUrl}/api/query/$endpoint');
// Build request body with query + pagination/filtering/sorting
final queryMap = _queryToMap(query);
final requestBody = <String, Object?>{
...queryMap,
'page': page,
'pageSize': pageSize,
if (filters != null && filters.isNotEmpty)
'filters': filters.map((f) => f.toJson()).toList(),
if (sorting != null && sorting.isNotEmpty)
'sorts': sorting.map((s) => s.toJson()).toList(),
};
final body = jsonEncode(requestBody);
final response = await _httpClient
.post(
url,
headers: config.defaultHeaders,
body: body,
)
.timeout(config.timeout);
return _handlePaginatedResponse<TItem>(response, itemFromJson);
} on TimeoutException catch (e) {
return ApiError<PaginatedResponse<TItem>>(
ApiErrorInfo(
message: 'Request timeout: ${e.message ?? "Operation took too long"}',
type: ApiErrorType.timeout,
),
);
} on SocketException catch (e) {
return ApiError<PaginatedResponse<TItem>>(
ApiErrorInfo(
message: 'Network error: ${e.message}',
type: ApiErrorType.network,
details: e.osError?.message,
),
);
} on http.ClientException catch (e) {
return ApiError<PaginatedResponse<TItem>>(
ApiErrorInfo(
message: 'HTTP client error: ${e.message}',
type: ApiErrorType.http,
),
);
} on FormatException catch (e) {
return ApiError<PaginatedResponse<TItem>>(
ApiErrorInfo(
message: 'JSON parsing error: ${e.message}',
type: ApiErrorType.serialization,
details: e.source.toString(),
),
);
} catch (e) {
return ApiError<PaginatedResponse<TItem>>(
ApiErrorInfo(
message: 'Unexpected error: $e',
type: ApiErrorType.unknown,
),
);
}
}
/// Execute a command (write operation)
///
/// Example:
/// ```dart
/// final result = await client.executeCommand(
/// 'createVehicle',
/// CreateVehicleCommand(name: 'Tesla Model 3'),
/// );
/// ```
Future<Result<void>> executeCommand({
required String endpoint,
required Serializable command,
}) async {
try {
final url = Uri.parse('${config.baseUrl}/api/command/$endpoint');
final body = _serializeQuery(command);
final response = await _httpClient
.post(
url,
headers: config.defaultHeaders,
body: body,
)
.timeout(config.timeout);
return _handleCommandResponse(response);
} on TimeoutException catch (e) {
return ApiError<void>(
ApiErrorInfo(
message: 'Request timeout: ${e.message ?? "Operation took too long"}',
type: ApiErrorType.timeout,
),
);
} on SocketException catch (e) {
return ApiError<void>(
ApiErrorInfo(
message: 'Network error: ${e.message}',
type: ApiErrorType.network,
details: e.osError?.message,
),
);
} on http.ClientException catch (e) {
return ApiError<void>(
ApiErrorInfo(
message: 'HTTP client error: ${e.message}',
type: ApiErrorType.http,
),
);
} on FormatException catch (e) {
return ApiError<void>(
ApiErrorInfo(
message: 'JSON parsing error: ${e.message}',
type: ApiErrorType.serialization,
details: e.source.toString(),
),
);
} catch (e) {
return ApiError<void>(
ApiErrorInfo(
message: 'Unexpected error: $e',
type: ApiErrorType.unknown,
),
);
}
}
// ---------------------------------------------------------------------------
// Private Helper Methods
// ---------------------------------------------------------------------------
/// Serialize query/command object to JSON string
String _serializeQuery(Serializable query) {
final jsonMap = query.toJson();
return jsonEncode(jsonMap);
}
/// Convert query object to Map for pagination requests
Map<String, Object?> _queryToMap(Serializable query) {
return query.toJson();
}
/// Handle HTTP response for single value queries
Result<TResult> _handleResponse<TResult>(
http.Response response,
TResult Function(Object? json) fromJson,
) {
if (response.statusCode >= 200 && response.statusCode < 300) {
try {
final json = jsonDecode(response.body);
final result = fromJson(json);
return ApiSuccess<TResult>(result);
} catch (e) {
return ApiError<TResult>(
ApiErrorInfo(
message: 'Failed to parse response',
statusCode: response.statusCode,
type: ApiErrorType.serialization,
details: e.toString(),
),
);
}
} else {
return ApiError<TResult>(
_parseErrorResponse(response),
);
}
}
/// Handle HTTP response for paginated queries
Result<PaginatedResponse<TItem>> _handlePaginatedResponse<TItem>(
http.Response response,
TItem Function(Map<String, Object?>) itemFromJson,
) {
if (response.statusCode >= 200 && response.statusCode < 300) {
try {
final json = jsonDecode(response.body) as Map<String, Object?>;
final paginatedResponse = PaginatedResponse<TItem>.fromJson(
json,
itemFromJson,
);
return ApiSuccess<PaginatedResponse<TItem>>(paginatedResponse);
} catch (e) {
return ApiError<PaginatedResponse<TItem>>(
ApiErrorInfo(
message: 'Failed to parse paginated response',
statusCode: response.statusCode,
type: ApiErrorType.serialization,
details: e.toString(),
),
);
}
} else {
return ApiError<PaginatedResponse<TItem>>(
_parseErrorResponse(response),
);
}
}
/// Handle HTTP response for commands
Result<void> _handleCommandResponse(http.Response response) {
if (response.statusCode >= 200 && response.statusCode < 300) {
return const ApiSuccess<void>(null);
} else {
return ApiError<void>(_parseErrorResponse(response));
}
}
/// Parse error information from HTTP response
ApiErrorInfo _parseErrorResponse(http.Response response) {
String message = 'Request failed with status ${response.statusCode}';
String? details;
try {
final json = jsonDecode(response.body) as Map<String, Object?>;
message = json['message'] as String? ?? message;
details = json['details'] as String? ?? json['error'] as String?;
} catch (_) {
// If JSON parsing fails, use response body as details
details = response.body;
}
return ApiErrorInfo(
message: message,
statusCode: response.statusCode,
type: _determineErrorType(response.statusCode),
details: details,
);
}
/// Determine error type based on HTTP status code
ApiErrorType _determineErrorType(int statusCode) {
if (statusCode >= 400 && statusCode < 500) {
if (statusCode == 422) {
return ApiErrorType.validation;
}
return ApiErrorType.http;
} else if (statusCode >= 500) {
return ApiErrorType.http;
}
return ApiErrorType.unknown;
}
/// Dispose of resources
void dispose() {
_httpClient.close();
}
}
@@ -0,0 +1,59 @@
/// Health check endpoint for API connectivity testing
library;
import '../client.dart';
import '../types.dart';
// =============================================================================
// Health Endpoint
// =============================================================================
/// Extension on CqrsApiClient for health check operations
extension HealthEndpoint on CqrsApiClient {
/// Check if the API is healthy and responding
///
/// Returns a `Result<bool>` where:
/// - Success: true if API is healthy
/// - Error: ApiError with details about the failure
///
/// Example:
/// ```dart
/// final client = CqrsApiClient(config: ApiClientConfig.development);
/// final result = await client.checkHealth();
///
/// result.when(
/// success: (isHealthy) => print('API is healthy: $isHealthy'),
/// error: (error) => print('Health check failed: ${error.message}'),
/// );
/// ```
Future<Result<bool>> checkHealth() async {
return executeQuery<bool>(
endpoint: 'health',
query: const HealthQuery(),
fromJson: (json) => json as bool,
);
}
}
/// Standalone health check function for convenience
///
/// Creates a temporary client instance to perform the health check.
/// Use this when you don't have a client instance readily available.
///
/// Example:
/// ```dart
/// final result = await performHealthCheck();
/// if (result.isSuccess && result.value) {
/// print('API is ready!');
/// }
/// ```
Future<Result<bool>> performHealthCheck({
ApiClientConfig config = ApiClientConfig.development,
}) async {
final client = CqrsApiClient(config: config);
try {
return await client.checkHealth();
} finally {
client.dispose();
}
}
+20
View File
@@ -0,0 +1,20 @@
/// OpenAPI code generation configuration
/// This file triggers the OpenAPI generator when build_runner executes
library;
import 'package:openapi_generator_annotations/openapi_generator_annotations.dart';
@Openapi(
additionalProperties: DioProperties(
pubName: 'console',
pubAuthor: 'Svrnty',
pubDescription: 'Svrnty Console API Client',
useEnumExtension: true,
enumUnknownDefaultCase: true,
),
inputSpec: InputSpec(path: 'api-schema.json'),
generatorName: Generator.dart,
outputDirectory: 'lib/api/generated',
skipSpecValidation: false,
)
class OpenapiConfig {}
+339
View File
@@ -0,0 +1,339 @@
/// Core types for API communication with CQRS backend
library;
// =============================================================================
// Serializable Interface
// =============================================================================
/// Interface for objects that can be serialized to/from JSON
/// All queries, commands, and DTOs must implement this interface
abstract interface class Serializable {
/// Convert this object to a JSON-serializable map
Map<String, Object?> toJson();
}
// =============================================================================
// Result Type - Functional Error Handling
// =============================================================================
/// Represents the result of an API operation that can either succeed or fail
sealed class Result<T> {
const Result();
/// The value for successful results
T get value;
/// The error for failed results
ApiErrorInfo get error;
/// Returns true if this result represents a success
bool get isSuccess => this is ApiSuccess<T>;
/// Returns true if this result represents an error
bool get isError => this is ApiError<T>;
/// Transform the success value if present
Result<R> map<R>(R Function(T value) transform) {
return switch (this) {
ApiSuccess<T>(value: final v) => ApiSuccess<R>(transform(v)),
ApiError<T>(: final error) => ApiError<R>(error),
};
}
/// Execute different actions based on result type
R when<R>({
required R Function(T value) success,
required R Function(ApiErrorInfo error) error,
}) {
return switch (this) {
ApiSuccess<T>(value: final v) => success(v),
ApiError<T>(error: final e) => error(e),
};
}
}
/// Represents a successful API result
final class ApiSuccess<T> extends Result<T> {
@override
final T value;
const ApiSuccess(this.value);
@override
ApiErrorInfo get error => throw StateError('Cannot get error from success result');
@override
String toString() => 'ApiSuccess($value)';
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ApiSuccess<T> &&
runtimeType == other.runtimeType &&
value == other.value;
@override
int get hashCode => value.hashCode;
}
/// Represents a failed API result
final class ApiError<T> extends Result<T> {
@override
final ApiErrorInfo error;
const ApiError(this.error);
@override
T get value => throw StateError('Cannot get value from error result');
@override
String toString() => 'ApiError($error)';
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ApiError<T> &&
runtimeType == other.runtimeType &&
error == other.error;
@override
int get hashCode => error.hashCode;
}
// =============================================================================
// Error Types
// =============================================================================
/// Information about an API error
class ApiErrorInfo {
final String message;
final int? statusCode;
final String? details;
final ApiErrorType type;
const ApiErrorInfo({
required this.message,
this.statusCode,
this.details,
required this.type,
});
@override
String toString() =>
'ApiError(type: $type, message: $message, status: $statusCode${details != null ? ', details: $details' : ''})';
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ApiErrorInfo &&
runtimeType == other.runtimeType &&
message == other.message &&
statusCode == other.statusCode &&
details == other.details &&
type == other.type;
@override
int get hashCode => Object.hash(message, statusCode, details, type);
}
/// Types of API errors
enum ApiErrorType {
/// Network connectivity issues
network,
/// HTTP protocol errors (4xx, 5xx)
http,
/// JSON parsing/serialization errors
serialization,
/// Request timeout
timeout,
/// Validation errors from backend
validation,
/// Unknown/unexpected errors
unknown,
}
// =============================================================================
// Paginated Response Types
// =============================================================================
/// Represents a paginated response from the API
class PaginatedResponse<T> {
final List<T> items;
final PageInfo pageInfo;
const PaginatedResponse({
required this.items,
required this.pageInfo,
});
factory PaginatedResponse.fromJson(
Map<String, Object?> json,
T Function(Map<String, Object?>) itemFromJson,
) {
final dataList = json['data'] as List<Object?>? ?? [];
final items = dataList
.whereType<Map<String, Object?>>()
.map(itemFromJson)
.toList();
return PaginatedResponse<T>(
items: items,
pageInfo: PageInfo.fromJson(json),
);
}
Map<String, Object?> toJson(Map<String, Object?> Function(T) itemToJson) => {
'data': items.map(itemToJson).toList(),
'page': pageInfo.page,
'pageSize': pageInfo.pageSize,
'totalItems': pageInfo.totalItems,
'totalPages': pageInfo.totalPages,
};
@override
String toString() => 'PaginatedResponse(items: ${items.length}, pageInfo: $pageInfo)';
}
/// Pagination metadata
class PageInfo {
final int page;
final int pageSize;
final int totalItems;
final int totalPages;
const PageInfo({
required this.page,
required this.pageSize,
required this.totalItems,
required this.totalPages,
});
factory PageInfo.fromJson(Map<String, Object?> json) {
final page = json['page'] as int? ?? 1;
final pageSize = json['pageSize'] as int? ?? 10;
final totalItems = json['totalItems'] as int? ?? 0;
final totalPages = json['totalPages'] as int? ?? 0;
return PageInfo(
page: page,
pageSize: pageSize,
totalItems: totalItems,
totalPages: totalPages,
);
}
Map<String, Object?> toJson() => {
'page': page,
'pageSize': pageSize,
'totalItems': totalItems,
'totalPages': totalPages,
};
@override
String toString() =>
'PageInfo(page: $page, pageSize: $pageSize, totalItems: $totalItems, totalPages: $totalPages)';
}
/// Filter criteria for paginated queries
class FilterCriteria {
final String field;
final FilterOperator operator;
final Object? value;
const FilterCriteria({
required this.field,
required this.operator,
required this.value,
});
Map<String, Object?> toJson() => {
'field': field,
'operator': operator.toServerString(),
'value': value,
};
}
/// Filter operators matching backend CQRS framework
enum FilterOperator {
equals,
notEquals,
contains,
startsWith,
endsWith,
greaterThan,
greaterThanOrEqual,
lessThan,
lessThanOrEqual,
isNull,
isNotNull;
String toServerString() => switch (this) {
FilterOperator.equals => 'Equal',
FilterOperator.notEquals => 'NotEqual',
FilterOperator.contains => 'Contains',
FilterOperator.startsWith => 'StartsWith',
FilterOperator.endsWith => 'EndsWith',
FilterOperator.greaterThan => 'GreaterThan',
FilterOperator.greaterThanOrEqual => 'GreaterThanOrEqual',
FilterOperator.lessThan => 'LessThan',
FilterOperator.lessThanOrEqual => 'LessThanOrEqual',
FilterOperator.isNull => 'IsNull',
FilterOperator.isNotNull => 'IsNotNull',
};
}
/// Sort criteria for paginated queries
class SortCriteria {
final String field;
final SortDirection direction;
const SortCriteria({
required this.field,
this.direction = SortDirection.ascending,
});
Map<String, Object?> toJson() => {
'field': field,
'direction': direction.toServerString(),
};
}
/// Sort direction
enum SortDirection {
ascending,
descending;
String toServerString() => switch (this) {
SortDirection.ascending => 'Ascending',
SortDirection.descending => 'Descending',
};
}
// =============================================================================
// Query/Command Models (Generated from OpenAPI Schema)
// =============================================================================
/// Health check query (matches backend HealthQuery record)
class HealthQuery implements Serializable {
const HealthQuery();
@override
Map<String, Object?> toJson() => {};
factory HealthQuery.fromJson(Map<String, Object?> json) => const HealthQuery();
@override
String toString() => 'HealthQuery()';
@override
bool operator ==(Object other) =>
identical(this, other) || other is HealthQuery && runtimeType == other.runtimeType;
@override
int get hashCode => 0;
}