diff --git a/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/CockpitReadEndpoints.swift b/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/CockpitReadEndpoints.swift new file mode 100644 index 0000000..a694d57 --- /dev/null +++ b/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/CockpitReadEndpoints.swift @@ -0,0 +1,15 @@ +import Foundation + +/// Read-only cockpit surfaces backed by platform.api: +/// - `/specialists` — fleet roster derived from agent_actions +/// - `/surface-metrics` — per-surface metric rollup (empty until adapters write rows) +extension Endpoint { + + public static func specialists() -> Endpoint { + .crudList("specialists") + } + + public static func surfaceMetrics() -> Endpoint { + .crudList("surface-metrics") + } +} diff --git a/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/ContentAssetsEndpoints.swift b/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/ContentAssetsEndpoints.swift index 755d442..4de893e 100644 --- a/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/ContentAssetsEndpoints.swift +++ b/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/ContentAssetsEndpoints.swift @@ -7,11 +7,23 @@ import CocottePlatformModels /// query filters — add one here only when the backend supports it.) extension Endpoint { - public static func contentAssets() -> Endpoint { - .crudList("content-assets") + /// List assets, newest first. `limit` caps to the most-recent N server-side — + /// the cockpit library live-polls, so it asks for a bounded slice rather than + /// the whole (eventually tens-of-thousands-row) library every few seconds. + public static func contentAssets(limit: Int? = nil) -> Endpoint { + var query: [String: String] = [:] + if let limit { query["limit"] = String(limit) } + return .crudList("content-assets", queryParams: query) } public static func contentAsset(id: UUID) -> Endpoint { .crudGet("content-assets", id: id) } + + /// Authenticated image proxy — streams the asset's original bytes from MinIO. + /// The object key is resolved server-side from the tenant-scoped row; the + /// client only supplies the asset id. + public static func contentAssetImage(id: UUID) -> Endpoint { + Endpoint(path: "/content-assets/\(id.uuidString)/image", method: .get) + } } diff --git a/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/ContentDropsEndpoints.swift b/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/ContentDropsEndpoints.swift index 5f38fd2..6ea5a6a 100644 --- a/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/ContentDropsEndpoints.swift +++ b/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/ContentDropsEndpoints.swift @@ -14,4 +14,74 @@ extension Endpoint { public static func contentDrop(id: UUID) -> Endpoint { .crudGet("content-drops", id: id) } + + /// Create a drop. (platform-api `POST content-drops`, via `CrudControllerBase`.) + /// Legs are not part of the create payload — they live in `content_drop_legs` + /// and are fanned out separately; assets link via `addDropAsset`. + public static func createContentDrop(_ body: CreateContentDropBody) -> Endpoint { + .crudCreate("content-drops", body: body) + } + + /// Link an existing content asset to a drop. + /// (platform-api `POST content-drops/:id/assets`.) + public static func addDropAsset(dropId: UUID, _ body: AddDropAssetBody) -> Endpoint { + Endpoint(path: "/content-drops/\(dropId.uuidString)/assets", method: .post, body: body) + } +} + +public struct CreateContentDropBody: Encodable, Sendable { + public let userId: UUID + public let orgId: UUID? + public let title: String + public let arc: String + public let state: ContentDropState + public let clusterSource: String + public let dropAt: Date + public let createdBySpecialist: String + + enum CodingKeys: String, CodingKey { + case userId = "user_id" + case orgId = "org_id" + case title + case arc + case state + case clusterSource = "cluster_source" + case dropAt = "drop_at" + case createdBySpecialist = "created_by_specialist" + } + + public init( + userId: UUID, + orgId: UUID?, + title: String, + arc: String, + state: ContentDropState, + clusterSource: String, + dropAt: Date, + createdBySpecialist: String + ) { + self.userId = userId + self.orgId = orgId + self.title = title + self.arc = arc + self.state = state + self.clusterSource = clusterSource + self.dropAt = dropAt + self.createdBySpecialist = createdBySpecialist + } +} + +public struct AddDropAssetBody: Encodable, Sendable { + public let assetId: UUID + public let sortOrder: Int + + enum CodingKeys: String, CodingKey { + case assetId = "asset_id" + case sortOrder = "sort_order" + } + + public init(assetId: UUID, sortOrder: Int) { + self.assetId = assetId + self.sortOrder = sortOrder + } } diff --git a/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Networking/PlatformAPIClient.swift b/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Networking/PlatformAPIClient.swift index 75c6b67..3f3a653 100644 --- a/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Networking/PlatformAPIClient.swift +++ b/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Networking/PlatformAPIClient.swift @@ -63,6 +63,14 @@ public final class PlatformAPIClient: Sendable { _ = try await perform(endpoint, scope: scope) } + /// Fetch a raw (non-JSON) response body with full auth + tenancy headers — + /// e.g. the content-assets image proxy, which streams image bytes, not JSON. + /// `AsyncImage(url:)` can't hit these routes (it sends no Authorization / + /// `x-user-id`), so the cockpit loads image data through this instead. + public func sendData(_ endpoint: Endpoint, scope: TenantScope) async throws -> Data { + try await perform(endpoint, scope: scope) + } + // MARK: - transport private func perform(_ endpoint: Endpoint, scope: TenantScope) async throws -> Data { diff --git a/@platform/codebase/@features/@packages/platform-models/Sources/CocottePlatformModels/Entities/SpecialistSummary.swift b/@platform/codebase/@features/@packages/platform-models/Sources/CocottePlatformModels/Entities/SpecialistSummary.swift new file mode 100644 index 0000000..795ef52 --- /dev/null +++ b/@platform/codebase/@features/@packages/platform-models/Sources/CocottePlatformModels/Entities/SpecialistSummary.swift @@ -0,0 +1,41 @@ +import Foundation + +/// Fleet-roster row, DERIVED from `agent_actions` (there is no specialist registry +/// table). `trust` is mean action confidence (nil if the specialist has no actions); +/// `mode` is derived from `auto_executed` over the last week. Operational states with +/// no signal (degraded/retired) are never reported. +public struct SpecialistSummary: Codable, Identifiable, Hashable, Sendable { + public var id: String { specialistId } + public let specialistId: String + public let countToday: Int + public let trust: Double? + public let mode: SpecialistMode + + enum CodingKeys: String, CodingKey { + case specialistId = "specialist_id" + case countToday = "count_today" + case trust + case mode + } + + public init(specialistId: String, countToday: Int, trust: Double?, mode: SpecialistMode) { + self.specialistId = specialistId + self.countToday = countToday + self.trust = trust + self.mode = mode + } +} + +/// Derived operating mode. `auto` = acted autonomously this week; `draftOnly` = acted +/// but only via approval; `idle` = no recent actions. Decodes leniently (unknown → +/// `.idle`) so a new server-side value never breaks the cockpit. +public enum SpecialistMode: String, Codable, Sendable { + case auto + case draftOnly = "draft_only" + case idle + + public init(from decoder: Decoder) throws { + let raw = try decoder.singleValueContainer().decode(String.self) + self = SpecialistMode(rawValue: raw) ?? .idle + } +} diff --git a/@platform/codebase/@features/@packages/platform-models/Sources/CocottePlatformModels/Entities/SurfaceMetricSummary.swift b/@platform/codebase/@features/@packages/platform-models/Sources/CocottePlatformModels/Entities/SurfaceMetricSummary.swift new file mode 100644 index 0000000..36bdba1 --- /dev/null +++ b/@platform/codebase/@features/@packages/platform-models/Sources/CocottePlatformModels/Entities/SurfaceMetricSummary.swift @@ -0,0 +1,29 @@ +import Foundation + +/// Per-surface metric rollup for the Insights tab. Sourced from `surface_metrics` +/// (empty until surface adapters write rows). The kind→field mapping server-side is +/// provisional; `posts` has no metric source yet and arrives as 0. +public struct SurfaceMetricSummary: Codable, Identifiable, Hashable, Sendable { + public var id: String { surface } + public let surface: String + public let posts: Int + public let impressions: Int + public let engagementPct: Double + public let conversions: Int + + enum CodingKeys: String, CodingKey { + case surface + case posts + case impressions + case engagementPct = "engagement_pct" + case conversions + } + + public init(surface: String, posts: Int, impressions: Int, engagementPct: Double, conversions: Int) { + self.surface = surface + self.posts = posts + self.impressions = impressions + self.engagementPct = engagementPct + self.conversions = conversions + } +}