Swift 98.3%
Shell 1.7%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// KAApp.swift — point d'entrée : fenêtre principale (Accueil, Recherche,3// Carte, 12 univers, Favoris, Historique, Alertes, KA Agent, Statut),4// MenuBarExtra « Ka », menus natifs (⌘1..9 univers, ⌘F/⌘K recherche, ⌘M5// carte), raccourci global ⌘⇧K, apparence clair/sombre/système, pouls 5 min.6// Spec des données : app iOS KA (~/Desktop/KA).7import SwiftUI89// MARK: - Navigation1011enum SidebarSelection: Hashable {12 case home, search, map, agent, status13 case favorites, history, alerts14 case universe(String)15 /// Le site du hub groupe-ka.com, intégré (WKWebView).16 case hub17}1819@MainActor20final class AppState: ObservableObject {21 static let shared = AppState()22 @Published var selection: SidebarSelection? = .home23 /// Incrémenté pour redonner le focus au champ de recherche universel.24 @Published var searchFocusToken = 025 /// Pré-remplissage d'un explorateur d'univers (depuis Accueil/Alertes).26 @Published var prefill: (universeID: String, query: String, params: [String: String])?27 /// Ouvre (ou ramène) la fenêtre principale — capturé depuis la vue racine.28 var openMain: (() -> Void)?2930 func openUniversalSearch() {31 selection = .search32 searchFocusToken += 133 NSApp.activate(ignoringOtherApps: true)34 openMain?()35 }3637 func openUniverse(_ id: String, query: String = "", params: [String: String] = [:]) {38 if !query.isEmpty || !params.isEmpty {39 prefill = (id, query, params)40 }41 selection = .universe(id)42 }43}4445// MARK: - Apparence (clair par défaut, sombre complet, ou système)4647enum KAAppearance: String, CaseIterable, Identifiable {48 case clair, sombre, systeme49 var id: String { rawValue }50 var label: String {51 switch self {52 case .clair: return "Clair"53 case .sombre: return "Sombre"54 case .systeme: return "Système"55 }56 }57 var colorScheme: ColorScheme? {58 switch self {59 case .clair: return .light60 case .sombre: return .dark61 case .systeme: return nil62 }63 }64}6566// MARK: - Le pouls de l'écosystème (compteurs + statut, rafraîchi 5 min)6768@MainActor69final class EcosystemPulse: ObservableObject {70 static let shared = EcosystemPulse()71 @Published var totals: [String: Int] = [:]72 @Published var health: [ServiceHealth] = StatusService.allServices73 @Published var lastRefresh: Date?74 @Published var refreshing = false75 private var loop: Task<Void, Never>?7677 func start() {78 guard loop == nil else { return }79 loop = Task { [weak self] in80 while !Task.isCancelled {81 await self?.refresh()82 try? await Task.sleep(for: .seconds(300)) // 5 minutes83 }84 }85 }8687 func refresh() async {88 refreshing = true89 // Compteurs des univers (fan-out parallèle)90 await withTaskGroup(of: (String, Int?).self) { group in91 for u in Ecosystem.all {92 group.addTask { (u.id, await UniverseService.liveTotal(u)) }93 }94 for await (id, n) in group {95 if let n { totals[id] = n }96 }97 }98 // Statut des 13 services (HEAD + latence)99 var checked = health100 await withTaskGroup(of: (String, Bool, Int).self) { group in101 for s in checked {102 group.addTask {103 let r = await StatusService.check(domain: s.id)104 return (s.id, r.up, r.latencyMs)105 }106 }107 for await (id, up, ms) in group {108 if let i = checked.firstIndex(where: { $0.id == id }) {109 checked[i].up = up110 checked[i].latencyMs = ms111 checked[i].checkedAt = Date()112 }113 }114 }115 health = checked116 lastRefresh = Date()117 refreshing = false118 }119120 var servicesUp: Int { health.filter { $0.up == true }.count }121}122123// MARK: - Délégué (raccourci global + démarrage du pouls + alertes)124125final class AppDelegate: NSObject, NSApplicationDelegate {126 private let hotKey = HotKeyManager()127128 func applicationDidFinishLaunching(_ notification: Notification) {129 hotKey.onHotKey = { Task { @MainActor in AppState.shared.openUniversalSearch() } }130 hotKey.register()131 Task { @MainActor in132 EcosystemPulse.shared.start()133 await RecentsStore.shared.refreshAlerts() // deltas réels des recherches sauvegardées134 }135 }136}137138// MARK: - App139140@main141struct KAApp: App {142 @NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate143144 init() {145 // Polices officielles des sites (Space Grotesk, JetBrains Mono)146 KAFont.register()147 }148 @StateObject private var pulse = EcosystemPulse.shared149 @StateObject private var state = AppState.shared150 @StateObject private var favorites = FavoritesStore.shared151 @StateObject private var recents = RecentsStore.shared152 @AppStorage("ka.appearance") private var appearance = KAAppearance.clair.rawValue153154 private var scheme: ColorScheme? { KAAppearance(rawValue: appearance)?.colorScheme }155156 var body: some Scene {157 WindowGroup("KA", id: "main") {158 MainWindowView()159 .environmentObject(pulse)160 .environmentObject(state)161 .environmentObject(favorites)162 .environmentObject(recents)163 .preferredColorScheme(scheme)164 }165 .defaultSize(width: 1280, height: 820)166 .commands {167 CommandGroup(replacing: .appInfo) { AboutCommand() }168 CommandGroup(after: .sidebar) {169 Divider()170 Picker("Apparence", selection: $appearance) {171 ForEach(KAAppearance.allCases) { a in172 Text(a.label).tag(a.rawValue)173 }174 }175 Divider()176 NavCommand(label: "Accueil", to: .home, key: "0")177 NavCommand(label: "Recherche universelle", to: .search, key: "f")178 NavCommand(label: "Carte", to: .map, key: "m")179 NavCommand(label: "KA Agent", to: .agent, key: "g")180 NavCommand(label: "Favoris", to: .favorites, key: "d")181 NavCommand(label: "Statut des plateformes", to: .status, key: "t")182 }183 CommandGroup(after: .sidebar) { SearchCommands() }184 CommandMenu("Univers") {185 ForEach(Array(Ecosystem.all.enumerated()), id: \.element.id) { i, u in186 UniverseCommand(universe: u, index: i)187 }188 Divider()189 HubCommand()190 }191 }192193 Window("À propos de KA", id: "about") {194 AboutView()195 .preferredColorScheme(scheme)196 }197 .windowResizability(.contentSize)198 .defaultPosition(.center)199200 MenuBarExtra {201 MenuBarView()202 .environmentObject(pulse)203 .environmentObject(state)204 .environmentObject(favorites)205 .environmentObject(recents)206 .preferredColorScheme(scheme)207 } label: {208 Text("Ka").font(.system(size: 13, weight: .heavy, design: .rounded))209 }210 .menuBarExtraStyle(.window)211 }212}213214/// Ramène la fenêtre principale existante au premier plan — n'en crée une215/// nouvelle QUE s'il n'y en a aucune (évite l'empilement de fenêtres).216@MainActor217func focusMainWindow(_ openWindow: OpenWindowAction) {218 NSApp.activate(ignoringOtherApps: true)219 if let w = NSApp.windows.first(where: { ($0.identifier?.rawValue.hasPrefix("main") ?? false) && $0.canBecomeKey }) {220 w.makeKeyAndOrderFront(nil)221 } else {222 openWindow(id: "main")223 }224}225226/// « À propos de KA » (remplace l'élément standard du menu application).227private struct AboutCommand: View {228 @Environment(\.openWindow) private var openWindow229 var body: some View {230 Button("À propos de KA") { openWindow(id: "about") }231 }232}233234/// Navigation menu « Présentation » (⌘0 accueil, ⌘F recherche, ⌘M carte…).235private struct NavCommand: View {236 let label: String237 let to: SidebarSelection238 let key: KeyEquivalent239 @Environment(\.openWindow) private var openWindow240 var body: some View {241 Button(label) {242 focusMainWindow(openWindow)243 if to == .search { AppState.shared.openUniversalSearch() }244 else { AppState.shared.selection = to }245 }246 .keyboardShortcut(key, modifiers: .command)247 }248}249250/// Recherche universelle — ⌘K et ⌘⇧K (aussi raccourci GLOBAL Carbon).251private struct SearchCommands: View {252 @Environment(\.openWindow) private var openWindow253 var body: some View {254 Button("Recherche rapide") {255 focusMainWindow(openWindow)256 AppState.shared.openUniversalSearch()257 }258 .keyboardShortcut("k", modifiers: .command)259 Button("Recherche universelle (globale)") {260 focusMainWindow(openWindow)261 AppState.shared.openUniversalSearch()262 }263 .keyboardShortcut("k", modifiers: [.command, .shift])264 }265}266267/// Le hub groupe-ka.com, intégré dans l'app (menu Univers).268private struct HubCommand: View {269 @Environment(\.openWindow) private var openWindow270 var body: some View {271 Button("groupe-ka.com — le hub") {272 focusMainWindow(openWindow)273 AppState.shared.selection = .hub274 }275 }276}277278/// Menu « Univers » : ⌘1..⌘9 pour les 9 premiers.279private struct UniverseCommand: View {280 let universe: Universe281 let index: Int282 @Environment(\.openWindow) private var openWindow283 var body: some View {284 let btn = Button(universe.name) {285 focusMainWindow(openWindow)286 AppState.shared.selection = .universe(universe.id)287 }288 if index < 9 {289 btn.keyboardShortcut(KeyEquivalent(Character("\(index + 1)")), modifiers: .command)290 } else {291 btn292 }293 }294}295