Keep using Photos.app like you always do. Attic quietly backs up your originals and edits to an S3 bucket you control. One-way, append-only.
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(backup): Stabilize progress bar and show failure details

Disable terminal echo/canonical mode during backup so keypresses don't
inject newlines that push the dashboard down. Show failed asset filenames
and error messages in the completion summary with a retry hint.

+29
+29
Sources/AtticCLI/TerminalRenderer.swift
··· 12 12 private var startTime: Date? 13 13 private var lastRenderTime: Date? 14 14 private let spinner: PreparationSpinner? 15 + private var originalTermios: termios? 15 16 16 17 init(spinner: PreparationSpinner? = nil) { 17 18 self.spinner = spinner 18 19 } 19 20 21 + /// Disable stdin echo and canonical mode so keypresses don't disrupt the dashboard. 22 + private func disableInputEcho() { 23 + var raw = termios() 24 + tcgetattr(STDIN_FILENO, &raw) 25 + originalTermios = raw 26 + raw.c_lflag &= ~UInt(ECHO | ICANON) 27 + tcsetattr(STDIN_FILENO, TCSANOW, &raw) 28 + } 29 + 30 + /// Restore the original terminal settings. 31 + private func restoreInputEcho() { 32 + guard var original = originalTermios else { return } 33 + tcsetattr(STDIN_FILENO, TCSANOW, &original) 34 + originalTermios = nil 35 + } 36 + 20 37 private struct RenderState { 21 38 var total: Int = 0 22 39 var photos: Int = 0 ··· 34 51 var pauseReason: String = "" 35 52 var pauseStarted: Date? 36 53 var totalPauseDuration: TimeInterval = 0 54 + var failedAssets: [(filename: String, message: String)] = [] 37 55 } 38 56 39 57 // MARK: - BackupProgressDelegate 40 58 41 59 func backupStarted(pending: Int, photos: Int, videos: Int) { 42 60 spinner?.stop() 61 + disableInputEcho() 43 62 lock.withLock { 44 63 state.total = pending 45 64 state.photos = photos ··· 71 90 lock.withLock { 72 91 state.failed += 1 73 92 state.currentFile = "\(filename) — \(message)" 93 + state.failedAssets.append((filename: filename, message: message)) 74 94 } 75 95 render() 76 96 } ··· 175 195 } 176 196 177 197 private func renderFinal() { 198 + restoreInputEcho() 199 + 178 200 let s: RenderState = lock.withLock { state } 179 201 let elapsed = lock.withLock { startTime.map { Date().timeIntervalSince($0) } ?? 0 } 180 202 ··· 190 212 print(" Uploaded: \(s.uploaded) (\(formatBytes(s.totalBytes)))") 191 213 if s.failed > 0 { 192 214 print(" Failed: \(s.failed)") 215 + print("") 216 + print("Failed assets:") 217 + for failure in s.failedAssets { 218 + print(" ✗ \(failure.filename): \(failure.message)") 219 + } 220 + print("") 221 + print("Tip: Run `attic backup` again to retry failed assets.") 193 222 } 194 223 195 224 fflush(stdout)