feat: Ipad app production readiness, Colony orchestration, Social posting (#44)
All checks were successful
Production Readiness / backend-contracts (push) Successful in 1m47s
Production Readiness / webos-typecheck (push) Successful in 1m50s
Production Readiness / ipad-parse (push) Successful in 1m34s

#38 Ipad app production readiness, Colony orchestration, Social posting

Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local>
Reviewed-on: #44
This commit was merged in pull request #44.
This commit is contained in:
2026-05-03 18:30:38 +05:30
parent 59d398abc3
commit eeb684b46c
86 changed files with 20349 additions and 1655 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,12 @@
import Foundation
enum AppStoreRefreshPolicy {
/// Native iPad surfaces refresh on initial view load, pull-to-refresh, and
/// explicit user mutations. View-local repeating timers are intentionally
/// avoided so AppStore can coalesce in-flight refreshes and hydrate mobile
/// edge state through one bulk request.
static let timerDrivenRefreshesEnabled = false
/// Match the WebOS bootstrap so Inventory, Dashboard, and shared summaries
/// are based on the same production property slice by default.
static let inventoryPropertyLimit = 100
@@ -9,8 +15,9 @@ enum AppStoreRefreshPolicy {
/// the operator's active task load on iPad surfaces.
static let canonicalTaskLimit = 50
/// iPad surfaces only render a small operator-focused timeline, so keep the
/// lead-event hydration set intentionally narrower than WebOS.
/// Lead timelines are hydrated through the mobile-edge bulk endpoint. Keep
/// the selected lead set bounded so every shared refresh remains one
/// predictable backend call rather than N per-lead calls.
static let leadTimelineHydrationLimit = 6
/// Fetch enough recent communication context for the visible iPad rails

View File

@@ -0,0 +1,220 @@
import CoreData
import Foundation
struct OfflineReplayRecord: Identifiable {
let id: String
let kind: String
let operation: String
let targetID: String?
let payload: Data
let queuedAt: Date
let attemptCount: Int
let lastAttemptAt: Date?
let lastError: String?
}
actor OfflineReplayStore {
static let shared = OfflineReplayStore()
private enum Schema {
static let entityName = "OfflineReplayItem"
}
private let container: NSPersistentContainer
private let context: NSManagedObjectContext
init() {
let model = Self.makeModel()
container = NSPersistentContainer(name: "VelocityOfflineReplay", managedObjectModel: model)
let applicationSupport = FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first ?? FileManager.default.temporaryDirectory
let directory = applicationSupport.appendingPathComponent("Velocity", isDirectory: true)
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let description = NSPersistentStoreDescription(
url: directory.appendingPathComponent("OfflineReplay.sqlite")
)
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = true
#if os(iOS)
description.setOption(
FileProtectionType.complete.rawValue as NSString,
forKey: NSPersistentStoreFileProtectionKey
)
#endif
container.persistentStoreDescriptions = [description]
var loadError: Error?
container.loadPersistentStores { _, error in
loadError = error
}
if let loadError {
assertionFailure("Velocity offline replay store failed to load: \(loadError.localizedDescription)")
}
context = container.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
func enqueue(kind: String, operation: String, targetID: String?, payload: Data) {
let context = context
context.performAndWait {
if let targetID {
let existing = Self.fetchManagedObjects(kind: kind, targetID: targetID, in: context)
existing.forEach(context.delete)
}
guard let entity = NSEntityDescription.entity(forEntityName: Schema.entityName, in: context) else {
return
}
let item = NSManagedObject(entity: entity, insertInto: context)
item.setValue(UUID().uuidString, forKey: "id")
item.setValue(kind, forKey: "kind")
item.setValue(operation, forKey: "operation")
item.setValue(targetID, forKey: "targetID")
item.setValue(payload, forKey: "payload")
item.setValue(Date(), forKey: "queuedAt")
item.setValue(0, forKey: "attemptCount")
item.setValue(nil, forKey: "lastAttemptAt")
item.setValue(nil, forKey: "lastError")
Self.saveIfNeeded(context)
}
}
func pendingRecords(limit: Int = 100) -> [OfflineReplayRecord] {
let context = context
var records: [OfflineReplayRecord] = []
context.performAndWait {
let request = NSFetchRequest<NSManagedObject>(entityName: Schema.entityName)
request.sortDescriptors = [
NSSortDescriptor(key: "queuedAt", ascending: true)
]
request.fetchLimit = limit
let items = (try? context.fetch(request)) ?? []
records = items.compactMap(Self.record(from:))
}
return records
}
func markCompleted(id: String) {
let context = context
context.performAndWait {
Self.fetchManagedObjects(id: id, in: context).forEach(context.delete)
Self.saveIfNeeded(context)
}
}
func markFailed(id: String, error: Error) {
let context = context
context.performAndWait {
for item in Self.fetchManagedObjects(id: id, in: context) {
let currentAttempts = item.value(forKey: "attemptCount") as? Int ?? 0
item.setValue(currentAttempts + 1, forKey: "attemptCount")
item.setValue(Date(), forKey: "lastAttemptAt")
item.setValue(error.localizedDescription, forKey: "lastError")
}
Self.saveIfNeeded(context)
}
}
func remove(kind: String, targetID: String) {
let context = context
context.performAndWait {
Self.fetchManagedObjects(kind: kind, targetID: targetID, in: context).forEach(context.delete)
Self.saveIfNeeded(context)
}
}
func reset() {
let context = context
context.performAndWait {
let request = NSFetchRequest<NSFetchRequestResult>(entityName: Schema.entityName)
let delete = NSBatchDeleteRequest(fetchRequest: request)
_ = try? context.execute(delete)
Self.saveIfNeeded(context)
}
}
private static func fetchManagedObjects(id: String, in context: NSManagedObjectContext) -> [NSManagedObject] {
let request = NSFetchRequest<NSManagedObject>(entityName: Schema.entityName)
request.predicate = NSPredicate(format: "id == %@", id)
request.fetchLimit = 1
return (try? context.fetch(request)) ?? []
}
private static func fetchManagedObjects(
kind: String,
targetID: String,
in context: NSManagedObjectContext
) -> [NSManagedObject] {
let request = NSFetchRequest<NSManagedObject>(entityName: Schema.entityName)
request.predicate = NSPredicate(format: "kind == %@ AND targetID == %@", kind, targetID)
return (try? context.fetch(request)) ?? []
}
private static func saveIfNeeded(_ context: NSManagedObjectContext) {
guard context.hasChanges else { return }
try? context.save()
}
private static func record(from object: NSManagedObject) -> OfflineReplayRecord? {
guard
let id = object.value(forKey: "id") as? String,
let kind = object.value(forKey: "kind") as? String,
let operation = object.value(forKey: "operation") as? String,
let payload = object.value(forKey: "payload") as? Data,
let queuedAt = object.value(forKey: "queuedAt") as? Date
else {
return nil
}
return OfflineReplayRecord(
id: id,
kind: kind,
operation: operation,
targetID: object.value(forKey: "targetID") as? String,
payload: payload,
queuedAt: queuedAt,
attemptCount: object.value(forKey: "attemptCount") as? Int ?? 0,
lastAttemptAt: object.value(forKey: "lastAttemptAt") as? Date,
lastError: object.value(forKey: "lastError") as? String
)
}
private static func makeModel() -> NSManagedObjectModel {
let model = NSManagedObjectModel()
let entity = NSEntityDescription()
entity.name = Schema.entityName
entity.managedObjectClassName = NSStringFromClass(NSManagedObject.self)
entity.properties = [
attribute("id", type: .stringAttributeType, optional: false),
attribute("kind", type: .stringAttributeType, optional: false),
attribute("operation", type: .stringAttributeType, optional: false),
attribute("targetID", type: .stringAttributeType, optional: true),
attribute("payload", type: .binaryDataAttributeType, optional: false),
attribute("queuedAt", type: .dateAttributeType, optional: false),
attribute("attemptCount", type: .integer64AttributeType, optional: false, defaultValue: 0),
attribute("lastAttemptAt", type: .dateAttributeType, optional: true),
attribute("lastError", type: .stringAttributeType, optional: true),
]
model.entities = [entity]
return model
}
private static func attribute(
_ name: String,
type: NSAttributeType,
optional: Bool,
defaultValue: Any? = nil
) -> NSAttributeDescription {
let attribute = NSAttributeDescription()
attribute.name = name
attribute.attributeType = type
attribute.isOptional = optional
attribute.defaultValue = defaultValue
return attribute
}
}