2026-06-07 00:03:19 -07:00
|
|
|
import SwiftUI
|
|
|
|
|
import CocotteCockpitKit
|
2026-06-07 23:00:29 -07:00
|
|
|
import CocottePlatformModels
|
|
|
|
|
import CocottePlatformAPIClient
|
2026-06-07 00:03:19 -07:00
|
|
|
|
|
|
|
|
// Stream 2 — the iOS cockpit. TabView shell (navigation lives here, not the Kit):
|
|
|
|
|
// Drops · Assets · Fleet · Activity · Insights, reusing the shared model + views.
|
|
|
|
|
|
|
|
|
|
@main
|
|
|
|
|
struct CocotteCockpitiOSApp: App {
|
|
|
|
|
var body: some Scene {
|
|
|
|
|
WindowGroup { RootView() }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct RootView: View {
|
2026-06-07 00:06:22 -07:00
|
|
|
@State private var model = RootView.makeModel()
|
2026-06-07 00:03:19 -07:00
|
|
|
@State private var theme: Theme = .dark
|
2026-06-07 00:06:22 -07:00
|
|
|
|
2026-06-07 23:00:29 -07:00
|
|
|
/// Build the data source. With `--api-base <host-root>` + `--user-id <uuid>`
|
|
|
|
|
/// (and optional `--token <jwt>`, `--org-id <uuid>`) the cockpit talks to the live
|
|
|
|
|
/// platform.api via the shared client; without them, the mock dataset.
|
|
|
|
|
/// `--api-base` is the host root (e.g. `http://black:3060`) — the client adds `/api/v1`.
|
2026-06-07 00:06:22 -07:00
|
|
|
private static func makeModel() -> CockpitModel {
|
|
|
|
|
let args = ProcessInfo.processInfo.arguments
|
|
|
|
|
func value(_ flag: String) -> String? {
|
|
|
|
|
guard let i = args.firstIndex(of: flag), i + 1 < args.count else { return nil }
|
|
|
|
|
return args[i + 1]
|
|
|
|
|
}
|
2026-06-07 23:00:29 -07:00
|
|
|
guard let base = value("--api-base"), let url = URL(string: base),
|
|
|
|
|
let userIdStr = value("--user-id"), let userId = UUID(uuidString: userIdStr) else {
|
2026-06-07 00:06:22 -07:00
|
|
|
return CockpitModel()
|
|
|
|
|
}
|
2026-06-07 23:00:29 -07:00
|
|
|
let scope = TenantScope(userId: userId, orgId: value("--org-id").flatMap { UUID(uuidString: $0) })
|
|
|
|
|
let auth = InMemoryAuthProvider(token: value("--token"))
|
|
|
|
|
return CockpitModel(api: LiveCockpitAPI(baseURL: url, auth: auth, scope: scope))
|
2026-06-07 00:06:22 -07:00
|
|
|
}
|
2026-06-07 00:03:19 -07:00
|
|
|
@State private var selection: Int = {
|
|
|
|
|
let args = ProcessInfo.processInfo.arguments
|
|
|
|
|
if let i = args.firstIndex(of: "--tab"), i + 1 < args.count, let n = Int(args[i + 1]) { return n }
|
|
|
|
|
return 0
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
private var tokens: Tokens { Tokens.make(theme, .regular) }
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
TabView(selection: $selection) {
|
|
|
|
|
DropsTab(model: model, theme: $theme)
|
|
|
|
|
.tabItem { Label("Drops", systemImage: "tray.full") }.tag(0)
|
|
|
|
|
AssetsTab(model: model)
|
|
|
|
|
.tabItem { Label("Assets", systemImage: "photo.on.rectangle") }.tag(1)
|
|
|
|
|
FleetTab(model: model)
|
|
|
|
|
.tabItem { Label("Fleet", systemImage: "person.3") }.tag(2)
|
2026-06-28 17:43:06 -04:00
|
|
|
ProspectorTab()
|
|
|
|
|
.tabItem { Label("Prospector", systemImage: "person.2") }.tag(3)
|
2026-06-07 00:03:19 -07:00
|
|
|
ActivityTab(model: model)
|
2026-06-28 17:43:06 -04:00
|
|
|
.tabItem { Label("Activity", systemImage: "bolt.horizontal") }.tag(4)
|
2026-06-07 00:03:19 -07:00
|
|
|
InsightsTab(model: model)
|
2026-06-28 17:43:06 -04:00
|
|
|
.tabItem { Label("Insights", systemImage: "chart.bar") }.tag(5)
|
2026-06-07 00:03:19 -07:00
|
|
|
}
|
|
|
|
|
.environment(\.tokens, tokens)
|
|
|
|
|
.preferredColorScheme(theme == .dark ? .dark : .light)
|
|
|
|
|
.tint(tokens.accent)
|
2026-06-08 03:30:19 -07:00
|
|
|
.task { await model.autoRefresh() }
|
2026-06-07 00:03:19 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct DropsTab: View {
|
|
|
|
|
var model: CockpitModel
|
|
|
|
|
@Binding var theme: Theme
|
|
|
|
|
@State private var path: [UUID] = []
|
|
|
|
|
@State private var composing = ProcessInfo.processInfo.arguments.contains("--composer")
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
NavigationStack(path: $path) {
|
|
|
|
|
ContentDropsView(model: model, onSelectDrop: { path.append($0.id) })
|
|
|
|
|
.navigationTitle("CocotteAI · social")
|
|
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
|
|
.toolbar {
|
|
|
|
|
ToolbarItem(placement: .topBarLeading) {
|
|
|
|
|
Button { theme = (theme == .dark ? .light : .dark) } label: {
|
|
|
|
|
Image(systemName: "circle.lefthalf.filled")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
|
|
|
Button { composing = true } label: { Image(systemName: "plus") }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.navigationDestination(for: UUID.self) { id in
|
|
|
|
|
if let drop = model.drop(id) {
|
|
|
|
|
DropDetailView(drop: drop)
|
|
|
|
|
.navigationTitle(drop.displayTitle)
|
|
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.sheet(isPresented: $composing) {
|
|
|
|
|
ComposerView(model: model, onClose: { composing = false })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct AssetsTab: View {
|
|
|
|
|
var model: CockpitModel
|
|
|
|
|
var body: some View {
|
|
|
|
|
NavigationStack {
|
|
|
|
|
AssetLibraryView(model: model)
|
|
|
|
|
.navigationTitle("Assets")
|
|
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct FleetTab: View {
|
|
|
|
|
var model: CockpitModel
|
|
|
|
|
@State private var path: [UUID] = []
|
|
|
|
|
var body: some View {
|
|
|
|
|
NavigationStack(path: $path) {
|
|
|
|
|
FleetListView(model: model, onSelect: { path.append($0.id) })
|
|
|
|
|
.navigationTitle("Fleet · content")
|
|
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
|
|
.navigationDestination(for: UUID.self) { id in
|
|
|
|
|
if let s = model.specialist(id) {
|
|
|
|
|
SpecialistDetailView(specialist: s)
|
|
|
|
|
.navigationTitle(s.displayName)
|
|
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct ActivityTab: View {
|
|
|
|
|
var model: CockpitModel
|
|
|
|
|
var body: some View {
|
|
|
|
|
NavigationStack {
|
|
|
|
|
ActivityView(model: model)
|
|
|
|
|
.navigationTitle("Activity")
|
|
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct InsightsTab: View {
|
|
|
|
|
var model: CockpitModel
|
|
|
|
|
var body: some View {
|
|
|
|
|
NavigationStack {
|
|
|
|
|
AnalyticsView(model: model)
|
|
|
|
|
.navigationTitle("Insights")
|
|
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-28 17:43:06 -04:00
|
|
|
|
|
|
|
|
private struct ProspectorTab: View {
|
|
|
|
|
var body: some View {
|
|
|
|
|
NavigationStack {
|
|
|
|
|
ProspectorView()
|
|
|
|
|
.navigationTitle("Prospector")
|
|
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|