iOS: mehrere Server, TabBar, Uebersicht, Verlauf und Stammdaten

Die App konnte fuenf Dinge, die Web-Oberflaeche vierzehn. Alles darueber hinaus
- nachsehen was gestern passiert ist, einen Lagerort anlegen - zwang zurueck an
den Browser.

Mehrere Server: Statt einer Adresse und einem Token kennt die App jetzt
beliebig viele Profile, jedes mit eigenem Token im Keychain. Ein Wechsel ist
damit ein Tipp und meldet an keinem der Server ab. Jedes Profil hat einen frei
waehlbaren Namen ("Zuhause"), der oben in der App steht; ohne Angabe faellt er
auf den Rechnernamen zurueck. Eine bestehende Anmeldung wird beim ersten Start
in ein Profil ueberfuehrt - ohne das waere man nach dem Update abgemeldet.

Statt einer Startseite mit Kacheln gibt es vier Tabs: Start, Scannen, Listen
und - nur fuer Administratoren, wie im Web - Verwaltung. Neu darin sind eine
Uebersicht mit den Kennzahlen des Servers, die per Tipp in die passende Liste
fuehren, der Bewegungsverlauf (auch je Artikel, weil /movements danach filtern
kann) und die Stammdaten: Lagerorte, Einheiten, Gebinde, Kategorien und
Gruppen. Die fuenf teilen sich eine Ansicht und unterscheiden sich nur darin,
wie geladen und geschrieben wird.

Zwei Dinge, die beim Pruefen gegen einen echten Server auffielen: Der Server
schickt Zeitstempel in UTC, haengt hinter SQLite aber keine Zeitzone an. Naiv
gelesen haette die App jede Uhrzeit um die eigene Zeitverschiebung daneben
angezeigt, deshalb wird die nackte Form ausdruecklich als UTC gelesen. Und die
Anzeigeeinstellungen gehoeren dem Server, werden beim Wechsel also neu geladen.

Bewusst nicht dabei: Benutzer, Einstellungen, Branding und Import/Export. Die
bleiben vorerst im Web.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-23 18:24:07 +02:00
parent 794a81f58b
commit ff158a1e5f
13 changed files with 1601 additions and 47 deletions

View File

@@ -0,0 +1,72 @@
import Foundation
/// Ein hinterlegter Server. Die App kann beliebig viele davon kennen und
/// zwischen ihnen umschalten, ohne sich jedes Mal neu anzumelden.
///
/// Das Token liegt **nicht** hier, sondern im Keychain unter einem Konto, das
/// aus der `id` gebildet wird (siehe `Session.keychainAccount`). Diese Struktur
/// landet als JSON in den UserDefaults und darf deshalb nichts Geheimes
/// enthalten.
struct ServerProfile: Codable, Identifiable, Equatable {
let id: UUID
var name: String
var urlString: String
/// Zuletzt angemeldeter Benutzer - nur fuer die Anzeige und damit das
/// Anmeldefeld beim Wechsel schon ausgefuellt ist.
var username: String
/// Zuletzt bekannte Rolle. Entscheidet, ob der Verwaltungs-Tab erscheint,
/// bevor `me()` den Wert frisch bestaetigt hat.
var isAdmin: Bool
init(id: UUID = UUID(), name: String, urlString: String,
username: String = "", isAdmin: Bool = false) {
self.id = id
self.name = name
self.urlString = urlString
self.username = username
self.isAdmin = isAdmin
}
var url: URL? { Session.normalize(urlString) }
/// Ohne eigenen Namen ist der Rechnername die brauchbarste Bezeichnung:
/// aus "http://192.168.1.50:8080/" wird "192.168.1.50".
static func suggestedName(for raw: String) -> String {
guard let url = Session.normalize(raw), let host = url.host, !host.isEmpty else {
return "Server"
}
return host
}
}
/// Liest und schreibt die Profilliste. Bewusst getrennt von `Session`, damit
/// die Ablage fuer sich verstaendlich bleibt.
enum ProfileStore {
static let profilesKey = "server_profiles"
static let activeKey = "active_profile_id"
static func load() -> [ServerProfile] {
guard let data = UserDefaults.standard.data(forKey: profilesKey),
let profiles = try? JSONDecoder().decode([ServerProfile].self, from: data)
else { return [] }
return profiles
}
static func save(_ profiles: [ServerProfile]) {
guard let data = try? JSONEncoder().encode(profiles) else { return }
UserDefaults.standard.set(data, forKey: profilesKey)
}
static func loadActiveID() -> UUID? {
guard let raw = UserDefaults.standard.string(forKey: activeKey) else { return nil }
return UUID(uuidString: raw)
}
static func saveActiveID(_ id: UUID?) {
if let id {
UserDefaults.standard.set(id.uuidString, forKey: activeKey)
} else {
UserDefaults.standard.removeObject(forKey: activeKey)
}
}
}