Files
ten31-transcripts/Ten31Transcripts/Settings/AppSettings.swift
T
Grant Gilliam 11eb82178f Add agent instructions; extract signing/backend secrets from source
- Add AGENTS.md (canonical) + CLAUDE.md symlink + ROADMAP.md
- Move Apple Team ID from project.yml into a gitignored
  Config/Signing.xcconfig via configFiles; commit the .example template
- Replace hardcoded backend host in AppSettings with a neutral
  placeholder + SPARK_BACKEND_URL env-var fallback
- Scrub the Team ID, .local host, and raw LAN IP from README/docs
- Ignore Config/Signing.xcconfig and .env
2026-06-13 12:23:54 -05:00

152 lines
6.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import Combine
/// User-facing settings, persisted to `UserDefaults`.
///
/// Phase 0 scope: backend host + TLS-skip, output folder, and adapter toggles.
/// The adapter toggles persist but do nothing yet (adapters arrive in Phase 34).
@MainActor
final class AppSettings: ObservableObject {
/// Adapters the app will eventually run, in display order.
static let adapterKeys: [(key: String, label: String)] = [
("zoom", "Zoom"),
("teams", "Microsoft Teams"),
("signal", "Signal"),
("meet", "Google Meet"),
]
@Published var backendBaseURL: String {
didSet { defaults.set(backendBaseURL, forKey: Keys.backendBaseURL) }
}
@Published var skipTLSVerification: Bool {
didSet { defaults.set(skipTLSVerification, forKey: Keys.skipTLS) }
}
@Published var outputFolderPath: String {
didSet { defaults.set(outputFolderPath, forKey: Keys.outputFolder) }
}
@Published var adapterEnabled: [String: Bool] {
didSet { defaults.set(adapterEnabled, forKey: Keys.adapterEnabled) }
}
@Published var autoRecordOnDetection: Bool {
didSet { defaults.set(autoRecordOnDetection, forKey: Keys.autoRecord) }
}
/// The user's name, pre-seeded into the timeline for mic-VAD "self" spans.
@Published var selfName: String {
didSet { defaults.set(selfName, forKey: Keys.selfName) }
}
/// Auto-send a finished recording to the backend for transcription. Default
/// off while developing; flip on for hands-free transcripts.
@Published var autoSendOnStop: Bool {
didSet { defaults.set(autoSendOnStop, forKey: Keys.autoSend) }
}
/// After transcription, build the readable recap (topic sections + meeting
/// extras) via the gateway LLM and write transcript.md / recap.html. Best-effort.
@Published var recapEnabled: Bool {
didSet { defaults.set(recapEnabled, forKey: Keys.recapEnabled) }
}
/// Reconcile speaker labels after transcription: merge a person split across
/// chunks (same voiceprint) and name placeholder/initial labels from what the
/// conversation reveals (gateway LLM). Best-effort.
@Published var reconcileSpeakers: Bool {
didSet { defaults.set(reconcileSpeakers, forKey: Keys.reconcileSpeakers) }
}
/// Diarization chunk length (raw value of `ChunkMode`). `.auto` shrinks chunks on
/// large calls so a window is less likely to exceed Sortformer's ~4-speaker cap.
@Published var chunkMode: String {
didSet { defaults.set(chunkMode, forKey: Keys.chunkMode) }
}
/// Typed accessor for `chunkMode`.
var chunk: ChunkMode { ChunkMode(rawValue: chunkMode) ?? .auto }
/// User-editable recap templates (takeaways categories per meeting type).
@Published var recapTemplates: [RecapTemplate] {
didSet { persist(recapTemplates, forKey: Keys.recapTemplates) }
}
/// Id of the template used automatically for new recaps.
@Published var defaultTemplateId: String {
didSet { defaults.set(defaultTemplateId, forKey: Keys.defaultTemplate) }
}
/// The active default template (falls back to the first available / built-in).
var defaultTemplate: RecapTemplate {
recapTemplates.first { $0.id == defaultTemplateId } ?? recapTemplates.first ?? .internalMeeting
}
/// Output folder as a resolved file URL (expands a leading `~`).
var outputFolderURL: URL {
URL(fileURLWithPath: (outputFolderPath as NSString).expandingTildeInPath,
isDirectory: true)
}
private let defaults: UserDefaults
/// Neutral placeholder. The real (private LAN) backend host is never committed
/// it's entered in Settings (persisted to UserDefaults) or seeded from the
/// `SPARK_BACKEND_URL` env var for dev/CI/harness runs.
static let defaultBackendURL = "https://your-spark-backend.local"
init(defaults: UserDefaults = .standard) {
self.defaults = defaults
// Precedence: a value the user saved in Settings wins; else the env var
// (handy when launching from Xcode/terminal); else the placeholder.
self.backendBaseURL = defaults.string(forKey: Keys.backendBaseURL)
?? ProcessInfo.processInfo.environment["SPARK_BACKEND_URL"]
?? Self.defaultBackendURL
self.skipTLSVerification = defaults.object(forKey: Keys.skipTLS) as? Bool ?? true
self.outputFolderPath = defaults.string(forKey: Keys.outputFolder)
?? "~/Ten31Transcripts"
let stored = defaults.dictionary(forKey: Keys.adapterEnabled) as? [String: Bool]
self.adapterEnabled = stored ?? Dictionary(
uniqueKeysWithValues: Self.adapterKeys.map { ($0.key, true) }
)
self.autoRecordOnDetection = defaults.object(forKey: Keys.autoRecord) as? Bool ?? true
self.selfName = defaults.string(forKey: Keys.selfName) ?? "Me"
self.autoSendOnStop = defaults.object(forKey: Keys.autoSend) as? Bool ?? false
self.recapEnabled = defaults.object(forKey: Keys.recapEnabled) as? Bool ?? true
self.reconcileSpeakers = defaults.object(forKey: Keys.reconcileSpeakers) as? Bool ?? true
self.chunkMode = defaults.string(forKey: Keys.chunkMode) ?? ChunkMode.auto.rawValue
let loaded = (defaults.data(forKey: Keys.recapTemplates))
.flatMap { try? JSONDecoder().decode([RecapTemplate].self, from: $0) }
self.recapTemplates = (loaded?.isEmpty == false) ? loaded! : RecapTemplate.builtIns
self.defaultTemplateId = defaults.string(forKey: Keys.defaultTemplate)
?? RecapTemplate.builtIns.first!.id
}
private func persist<T: Encodable>(_ value: T, forKey key: String) {
if let data = try? JSONEncoder().encode(value) { defaults.set(data, forKey: key) }
}
private enum Keys {
static let backendBaseURL = "backendBaseURL"
static let skipTLS = "skipTLSVerification"
static let outputFolder = "outputFolderPath"
static let adapterEnabled = "adapterEnabled"
static let autoRecord = "autoRecordOnDetection"
static let selfName = "selfName"
static let autoSend = "autoSendOnStop"
static let recapEnabled = "recapEnabled"
static let reconcileSpeakers = "reconcileSpeakers"
static let chunkMode = "chunkMode"
static let recapTemplates = "recapTemplates"
static let defaultTemplate = "defaultTemplateId"
}
}