added shared support

This commit is contained in:
Mark Gregory
2026-10-06 08:55:19 +01:00
parent 211115c9bc
commit 6bb62ebf69
4 changed files with 155 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
# SonarDesk iOS + Team — Phase 1
Added the non-destructive shared workspace domain foundation.
## Added
- `WorkspaceKind`: personal/team
- `WorkspaceRole`: owner/admin/developer/viewer
- `SonarWorkspace`
- `WorkspaceMember`
- `WorkspaceStore`
- iCloud Drive persistence for workspace metadata
- Explicit credential boundary: SonarQube tokens stay device-local in Keychain
## Next in Xcode
1. Add a SonarDesk iOS app target (`net.absdev.SonarDesk.iOS`).
2. Enable the existing iCloud container `iCloud.net.absdev.SonarDesk` plus CloudKit.
3. Share API/models/workspace files with both targets.
4. Add an iOS-specific app entry point and `NavigationSplitView`.
5. Implement Dashboard, Projects and Project Detail.
6. Introduce CloudKit private/shared workspace repository and migrate the workspace JSON records.
No existing macOS source was modified in Phase 1; the new synchronized-folder files are automatically visible to the current Xcode project.
@@ -0,0 +1,32 @@
# SonarDesk shared workspace foundation
This is the first compatibility-safe step toward macOS + iOS + Team.
## Storage boundaries
- iCloud Drive currently remains the persistence mechanism for existing macOS data.
- Workspace metadata is stored in `Data/workspaces.json` so current releases can adopt the model without a destructive migration.
- SonarQube credentials remain in Keychain and are **not** written to iCloud Drive.
- `ThisDeviceOnly` remains the default credential policy. An iPhone therefore authenticates independently.
## Next migration
Move workspace/team records to CloudKit private/shared databases:
- Personal workspace -> private database
- Team workspace -> CKShare/shared database
- Team membership and roles -> shared records
- Reports -> iCloud Drive assets/CloudKit asset metadata
The existing JSON workspace model is intentionally Codable so it can be migrated to CloudKit records without changing UI-facing domain types.
## iOS target
The iOS target should share:
- Models/Models.swift
- Services/SonarQubeClient.swift
- Services/KeychainService.swift
- Shared/Workspaces/*
macOS-only UI and exporters stay out of the iOS target. The first iOS screens are Dashboard, Projects and Project Detail.
@@ -0,0 +1,49 @@
import Foundation
enum WorkspaceKind: String, Codable, CaseIterable, Sendable {
case personal
case team
}
enum WorkspaceRole: String, Codable, CaseIterable, Sendable {
case owner
case admin
case developer
case viewer
}
struct SonarWorkspace: Codable, Identifiable, Hashable, Sendable {
var id: UUID
var name: String
var kind: WorkspaceKind
var role: WorkspaceRole
var serverIDs: [UUID]
var createdAt: Date
var updatedAt: Date
init(id: UUID = UUID(), name: String, kind: WorkspaceKind, role: WorkspaceRole,
serverIDs: [UUID] = [], createdAt: Date = Date(), updatedAt: Date = Date()) {
self.id = id
self.name = name
self.kind = kind
self.role = role
self.serverIDs = serverIDs
self.createdAt = createdAt
self.updatedAt = updatedAt
}
static let personal = SonarWorkspace(
id: UUID(uuidString: "00000000-0000-0000-0000-000000000001")!,
name: "Personal",
kind: .personal,
role: .owner
)
}
struct WorkspaceMember: Codable, Identifiable, Hashable, Sendable {
var id: UUID
var workspaceID: UUID
var displayName: String
var role: WorkspaceRole
var joinedAt: Date
}
@@ -0,0 +1,52 @@
import Foundation
import Combine
@MainActor
final class WorkspaceStore: ObservableObject {
@Published private(set) var workspaces: [SonarWorkspace] = [.personal]
@Published var selectedWorkspaceID: UUID = SonarWorkspace.personal.id
private let driveStore = ICloudDriveStore.shared
private let fileName = "workspaces.json"
var selectedWorkspace: SonarWorkspace {
workspaces.first(where: { $0.id == selectedWorkspaceID }) ?? .personal
}
init() {
load()
}
func load() {
do {
guard let saved = try driveStore.read([SonarWorkspace].self, fileName: fileName), !saved.isEmpty else {
persist()
return
}
workspaces = saved
if !workspaces.contains(where: { $0.id == selectedWorkspaceID }) {
selectedWorkspaceID = workspaces.first?.id ?? SonarWorkspace.personal.id
}
} catch {
// Keep Personal usable if iCloud is temporarily unavailable.
workspaces = [.personal]
}
}
func createTeam(name: String) {
let clean = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !clean.isEmpty else { return }
let team = SonarWorkspace(name: clean, kind: .team, role: .owner)
workspaces.append(team)
selectedWorkspaceID = team.id
persist()
}
func select(_ workspace: SonarWorkspace) {
selectedWorkspaceID = workspace.id
}
private func persist() {
try? driveStore.write(workspaces, fileName: fileName)
}
}