// Auteur : Simon-Pierre Boucher — contact@spboucher.ai // KAApp.swift — point d'entrée : fenêtre principale (Accueil, Recherche, // Carte, 12 univers, Favoris, Historique, Alertes, KA Agent, Statut), // MenuBarExtra « Ka », menus natifs (⌘1..9 univers, ⌘F/⌘K recherche, ⌘M // carte), raccourci global ⌘⇧K, apparence clair/sombre/système, pouls 5 min. // Spec des données : app iOS KA (~/Desktop/KA). import SwiftUI // MARK: - Navigation enum SidebarSelection: Hashable { case home, search, map, agent, status case favorites, history, alerts case universe(String) /// Le site du hub groupe-ka.com, intégré (WKWebView). case hub } @MainActor final class AppState: ObservableObject { static let shared = AppState() @Published var selection: SidebarSelection? = .home /// Incrémenté pour redonner le focus au champ de recherche universel. @Published var searchFocusToken = 0 /// Pré-remplissage d'un explorateur d'univers (depuis Accueil/Alertes). @Published var prefill: (universeID: String, query: String, params: [String: String])? /// Ouvre (ou ramène) la fenêtre principale — capturé depuis la vue racine. var openMain: (() -> Void)? func openUniversalSearch() { selection = .search searchFocusToken += 1 NSApp.activate(ignoringOtherApps: true) openMain?() } func openUniverse(_ id: String, query: String = "", params: [String: String] = [:]) { if !query.isEmpty || !params.isEmpty { prefill = (id, query, params) } selection = .universe(id) } } // MARK: - Apparence (clair par défaut, sombre complet, ou système) enum KAAppearance: String, CaseIterable, Identifiable { case clair, sombre, systeme var id: String { rawValue } var label: String { switch self { case .clair: return "Clair" case .sombre: return "Sombre" case .systeme: return "Système" } } var colorScheme: ColorScheme? { switch self { case .clair: return .light case .sombre: return .dark case .systeme: return nil } } } // MARK: - Le pouls de l'écosystème (compteurs + statut, rafraîchi 5 min) @MainActor final class EcosystemPulse: ObservableObject { static let shared = EcosystemPulse() @Published var totals: [String: Int] = [:] @Published var health: [ServiceHealth] = StatusService.allServices @Published var lastRefresh: Date? @Published var refreshing = false private var loop: Task? func start() { guard loop == nil else { return } loop = Task { [weak self] in while !Task.isCancelled { await self?.refresh() try? await Task.sleep(for: .seconds(300)) // 5 minutes } } } func refresh() async { refreshing = true // Compteurs des univers (fan-out parallèle) await withTaskGroup(of: (String, Int?).self) { group in for u in Ecosystem.all { group.addTask { (u.id, await UniverseService.liveTotal(u)) } } for await (id, n) in group { if let n { totals[id] = n } } } // Statut des 13 services (HEAD + latence) var checked = health await withTaskGroup(of: (String, Bool, Int).self) { group in for s in checked { group.addTask { let r = await StatusService.check(domain: s.id) return (s.id, r.up, r.latencyMs) } } for await (id, up, ms) in group { if let i = checked.firstIndex(where: { $0.id == id }) { checked[i].up = up checked[i].latencyMs = ms checked[i].checkedAt = Date() } } } health = checked lastRefresh = Date() refreshing = false } var servicesUp: Int { health.filter { $0.up == true }.count } } // MARK: - Délégué (raccourci global + démarrage du pouls + alertes) final class AppDelegate: NSObject, NSApplicationDelegate { private let hotKey = HotKeyManager() func applicationDidFinishLaunching(_ notification: Notification) { hotKey.onHotKey = { Task { @MainActor in AppState.shared.openUniversalSearch() } } hotKey.register() Task { @MainActor in EcosystemPulse.shared.start() await RecentsStore.shared.refreshAlerts() // deltas réels des recherches sauvegardées } } } // MARK: - App @main struct KAApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate init() { // Polices officielles des sites (Space Grotesk, JetBrains Mono) KAFont.register() } @StateObject private var pulse = EcosystemPulse.shared @StateObject private var state = AppState.shared @StateObject private var favorites = FavoritesStore.shared @StateObject private var recents = RecentsStore.shared @AppStorage("ka.appearance") private var appearance = KAAppearance.clair.rawValue private var scheme: ColorScheme? { KAAppearance(rawValue: appearance)?.colorScheme } var body: some Scene { WindowGroup("KA", id: "main") { MainWindowView() .environmentObject(pulse) .environmentObject(state) .environmentObject(favorites) .environmentObject(recents) .preferredColorScheme(scheme) } .defaultSize(width: 1280, height: 820) .commands { CommandGroup(replacing: .appInfo) { AboutCommand() } CommandGroup(after: .sidebar) { Divider() Picker("Apparence", selection: $appearance) { ForEach(KAAppearance.allCases) { a in Text(a.label).tag(a.rawValue) } } Divider() NavCommand(label: "Accueil", to: .home, key: "0") NavCommand(label: "Recherche universelle", to: .search, key: "f") NavCommand(label: "Carte", to: .map, key: "m") NavCommand(label: "KA Agent", to: .agent, key: "g") NavCommand(label: "Favoris", to: .favorites, key: "d") NavCommand(label: "Statut des plateformes", to: .status, key: "t") } CommandGroup(after: .sidebar) { SearchCommands() } CommandMenu("Univers") { ForEach(Array(Ecosystem.all.enumerated()), id: \.element.id) { i, u in UniverseCommand(universe: u, index: i) } Divider() HubCommand() } } Window("À propos de KA", id: "about") { AboutView() .preferredColorScheme(scheme) } .windowResizability(.contentSize) .defaultPosition(.center) MenuBarExtra { MenuBarView() .environmentObject(pulse) .environmentObject(state) .environmentObject(favorites) .environmentObject(recents) .preferredColorScheme(scheme) } label: { Text("Ka").font(.system(size: 13, weight: .heavy, design: .rounded)) } .menuBarExtraStyle(.window) } } /// Ramène la fenêtre principale existante au premier plan — n'en crée une /// nouvelle QUE s'il n'y en a aucune (évite l'empilement de fenêtres). @MainActor func focusMainWindow(_ openWindow: OpenWindowAction) { NSApp.activate(ignoringOtherApps: true) if let w = NSApp.windows.first(where: { ($0.identifier?.rawValue.hasPrefix("main") ?? false) && $0.canBecomeKey }) { w.makeKeyAndOrderFront(nil) } else { openWindow(id: "main") } } /// « À propos de KA » (remplace l'élément standard du menu application). private struct AboutCommand: View { @Environment(\.openWindow) private var openWindow var body: some View { Button("À propos de KA") { openWindow(id: "about") } } } /// Navigation menu « Présentation » (⌘0 accueil, ⌘F recherche, ⌘M carte…). private struct NavCommand: View { let label: String let to: SidebarSelection let key: KeyEquivalent @Environment(\.openWindow) private var openWindow var body: some View { Button(label) { focusMainWindow(openWindow) if to == .search { AppState.shared.openUniversalSearch() } else { AppState.shared.selection = to } } .keyboardShortcut(key, modifiers: .command) } } /// Recherche universelle — ⌘K et ⌘⇧K (aussi raccourci GLOBAL Carbon). private struct SearchCommands: View { @Environment(\.openWindow) private var openWindow var body: some View { Button("Recherche rapide") { focusMainWindow(openWindow) AppState.shared.openUniversalSearch() } .keyboardShortcut("k", modifiers: .command) Button("Recherche universelle (globale)") { focusMainWindow(openWindow) AppState.shared.openUniversalSearch() } .keyboardShortcut("k", modifiers: [.command, .shift]) } } /// Le hub groupe-ka.com, intégré dans l'app (menu Univers). private struct HubCommand: View { @Environment(\.openWindow) private var openWindow var body: some View { Button("groupe-ka.com — le hub") { focusMainWindow(openWindow) AppState.shared.selection = .hub } } } /// Menu « Univers » : ⌘1..⌘9 pour les 9 premiers. private struct UniverseCommand: View { let universe: Universe let index: Int @Environment(\.openWindow) private var openWindow var body: some View { let btn = Button(universe.name) { focusMainWindow(openWindow) AppState.shared.selection = .universe(universe.id) } if index < 9 { btn.keyboardShortcut(KeyEquivalent(Character("\(index + 1)")), modifiers: .command) } else { btn } } }