pushframe 5.1.26__tar.gz → 5.1.28__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (430) hide show
  1. {pushframe-5.1.26 → pushframe-5.1.28}/CHANGELOG.md +53 -0
  2. {pushframe-5.1.26 → pushframe-5.1.28}/CLAUDE.md +3 -3
  3. {pushframe-5.1.26 → pushframe-5.1.28}/PKG-INFO +1 -1
  4. {pushframe-5.1.26 → pushframe-5.1.28}/docs/CLI.md +10 -1
  5. {pushframe-5.1.26 → pushframe-5.1.28}/docs/ERRORS.md +5 -0
  6. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/__init__.py +1 -1
  7. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/assetApi.py +2 -2
  8. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/frameApi.py +1 -1
  9. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/cli.py +155 -10
  10. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/schedule.py +5 -1
  11. pushframe-5.1.28/pushframe/utils/dt.py +27 -0
  12. {pushframe-5.1.26 → pushframe-5.1.28}/pyproject.toml +1 -1
  13. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_asset_partial.py +3 -3
  14. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_schedule.py +285 -0
  15. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_version.py +1 -1
  16. {pushframe-5.1.26 → pushframe-5.1.28}/uv.lock +1 -1
  17. pushframe-5.1.26/pushframe/utils/dt.py +0 -15
  18. {pushframe-5.1.26 → pushframe-5.1.28}/.README-orig.md +0 -0
  19. {pushframe-5.1.26 → pushframe-5.1.28}/.env.sample +0 -0
  20. {pushframe-5.1.26 → pushframe-5.1.28}/.freebuff/project-id +0 -0
  21. {pushframe-5.1.26 → pushframe-5.1.28}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  22. {pushframe-5.1.26 → pushframe-5.1.28}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  23. {pushframe-5.1.26 → pushframe-5.1.28}/.github/workflows/release.yml +0 -0
  24. {pushframe-5.1.26 → pushframe-5.1.28}/.github/workflows/tests.yml +0 -0
  25. {pushframe-5.1.26 → pushframe-5.1.28}/.gitignore +0 -0
  26. {pushframe-5.1.26 → pushframe-5.1.28}/.gsd/dispatch-isolation-sentinel.json +0 -0
  27. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/MILESTONES.md +0 -0
  28. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/PROJECT.md +0 -0
  29. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/REQUIREMENTS.md +0 -0
  30. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/RETROSPECTIVE.md +0 -0
  31. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/ROADMAP.md +0 -0
  32. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/STATE.md +0 -0
  33. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/WINDOWS.md +0 -0
  34. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/codebase/ARCHITECTURE.md +0 -0
  35. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/codebase/CONCERNS.md +0 -0
  36. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/codebase/CONVENTIONS.md +0 -0
  37. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/codebase/INTEGRATIONS.md +0 -0
  38. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/codebase/STACK.md +0 -0
  39. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/codebase/STRUCTURE.md +0 -0
  40. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/codebase/TESTING.md +0 -0
  41. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/config.json +0 -0
  42. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/debug/gsync-apply-budget-none-crash.md +0 -0
  43. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/debug/knowledge-base.md +0 -0
  44. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/debug/login-475-null-creds.md +0 -0
  45. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/debug/resolved/cli-status-verbose-logging.md +0 -0
  46. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/debug/resolved/inspect-asset-null-fields.md +0 -0
  47. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/debug/resolved/select-asset-401-unauthorized.md +0 -0
  48. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/debug/status-crash-401-trip.md +0 -0
  49. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-REQUIREMENTS.md +0 -0
  50. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-ROADMAP.md +0 -0
  51. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-01-PLAN.md +0 -0
  52. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-01-SUMMARY.md +0 -0
  53. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-02-PLAN.md +0 -0
  54. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-02-SUMMARY.md +0 -0
  55. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-CONTEXT.md +0 -0
  56. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-DISCUSSION-LOG.md +0 -0
  57. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-PATTERNS.md +0 -0
  58. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-RESEARCH.md +0 -0
  59. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-REVIEW.md +0 -0
  60. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-VERIFICATION.md +0 -0
  61. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-01-PLAN.md +0 -0
  62. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-01-SUMMARY.md +0 -0
  63. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-02-PLAN.md +0 -0
  64. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-02-SUMMARY.md +0 -0
  65. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-CONTEXT.md +0 -0
  66. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-DISCUSSION-LOG.md +0 -0
  67. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-LIVE-EVIDENCE.md +0 -0
  68. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-PATTERNS.md +0 -0
  69. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-RESEARCH.md +0 -0
  70. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-REVIEW.md +0 -0
  71. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-VERIFICATION.md +0 -0
  72. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-01-PLAN.md +0 -0
  73. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-01-SUMMARY.md +0 -0
  74. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-CONTEXT.md +0 -0
  75. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-DISCUSSION-LOG.md +0 -0
  76. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-PATTERNS.md +0 -0
  77. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-RESEARCH.md +0 -0
  78. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-REVIEW.md +0 -0
  79. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-VERIFICATION.md +0 -0
  80. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-ROADMAP.md +0 -0
  81. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/.gitkeep +0 -0
  82. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-01-PLAN.md +0 -0
  83. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-01-SUMMARY.md +0 -0
  84. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-02-PLAN.md +0 -0
  85. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-02-SUMMARY.md +0 -0
  86. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-03-PLAN.md +0 -0
  87. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-03-SUMMARY.md +0 -0
  88. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-PATTERNS.md +0 -0
  89. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-RESEARCH.md +0 -0
  90. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-REVIEW.md +0 -0
  91. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-SECURITY.md +0 -0
  92. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-UAT.md +0 -0
  93. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-VERIFICATION.md +0 -0
  94. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-REQUIREMENTS.md +0 -0
  95. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-ROADMAP.md +0 -0
  96. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-01-PLAN.md +0 -0
  97. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-01-SUMMARY.md +0 -0
  98. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-02-PLAN.md +0 -0
  99. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-02-SUMMARY.md +0 -0
  100. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-CONTEXT.md +0 -0
  101. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-DISCUSSION-LOG.md +0 -0
  102. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-PATTERNS.md +0 -0
  103. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-REVIEW.md +0 -0
  104. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-SECURITY.md +0 -0
  105. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-UAT.md +0 -0
  106. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-VERIFICATION.md +0 -0
  107. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-01-PLAN.md +0 -0
  108. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-01-SUMMARY.md +0 -0
  109. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-02-PLAN.md +0 -0
  110. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-02-SUMMARY.md +0 -0
  111. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-CONTEXT.md +0 -0
  112. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-DISCUSSION-LOG.md +0 -0
  113. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-PATTERNS.md +0 -0
  114. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-RESEARCH.md +0 -0
  115. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-REVIEW.md +0 -0
  116. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-SECURITY.md +0 -0
  117. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-VERIFICATION.md +0 -0
  118. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-01-PLAN.md +0 -0
  119. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-01-SUMMARY.md +0 -0
  120. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-02-PLAN.md +0 -0
  121. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-02-SUMMARY.md +0 -0
  122. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-03-PLAN.md +0 -0
  123. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-03-SUMMARY.md +0 -0
  124. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-CONTEXT.md +0 -0
  125. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-DISCUSSION-LOG.md +0 -0
  126. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-PATTERNS.md +0 -0
  127. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-REVIEW-FIX.md +0 -0
  128. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-REVIEW.md +0 -0
  129. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-SECURITY.md +0 -0
  130. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-VERIFICATION.md +0 -0
  131. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-01-PLAN.md +0 -0
  132. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-01-SUMMARY.md +0 -0
  133. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-02-PLAN.md +0 -0
  134. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-02-SUMMARY.md +0 -0
  135. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-03-PLAN.md +0 -0
  136. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-03-SUMMARY.md +0 -0
  137. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-04-PLAN.md +0 -0
  138. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-04-SUMMARY.md +0 -0
  139. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-CONTEXT.md +0 -0
  140. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-DISCUSSION-LOG.md +0 -0
  141. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-LIVE-FINDINGS.md +0 -0
  142. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-PATTERNS.md +0 -0
  143. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-RESEARCH.md +0 -0
  144. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-REVIEW-FIX.md +0 -0
  145. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-REVIEW.md +0 -0
  146. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-VERIFICATION.md +0 -0
  147. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/.gitkeep +0 -0
  148. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-01-PLAN.md +0 -0
  149. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-01-SUMMARY.md +0 -0
  150. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-02-PLAN.md +0 -0
  151. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-02-SUMMARY.md +0 -0
  152. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-CONTEXT.md +0 -0
  153. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-RESEARCH.md +0 -0
  154. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-REVIEW-FIX.md +0 -0
  155. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-REVIEW.md +0 -0
  156. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-VERIFICATION.md +0 -0
  157. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/.gitkeep +0 -0
  158. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-01-PLAN.md +0 -0
  159. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-01-SUMMARY.md +0 -0
  160. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-02-PLAN.md +0 -0
  161. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-02-SUMMARY.md +0 -0
  162. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-03-PLAN.md +0 -0
  163. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-03-SUMMARY.md +0 -0
  164. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-04-PLAN.md +0 -0
  165. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-04-SUMMARY.md +0 -0
  166. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-CONTEXT.md +0 -0
  167. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-DISCUSSION-LOG.md +0 -0
  168. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-LIVE-FINDINGS.md +0 -0
  169. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-PATTERNS.md +0 -0
  170. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-RESEARCH.md +0 -0
  171. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-SECURITY.md +0 -0
  172. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-UAT.md +0 -0
  173. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-VERIFICATION.md +0 -0
  174. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/COVERAGE.md +0 -0
  175. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260630-qs8-make-main-py-load-a-local-env-file-at-st/260630-qs8-PLAN.md +0 -0
  176. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260630-qs8-make-main-py-load-a-local-env-file-at-st/260630-qs8-SUMMARY.md +0 -0
  177. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260708-dt9-add-progress-feedback-to-aura-cli-sync-a/260708-dt9-PLAN.md +0 -0
  178. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260708-dt9-add-progress-feedback-to-aura-cli-sync-a/260708-dt9-SUMMARY.md +0 -0
  179. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260708-dt9-add-progress-feedback-to-aura-cli-sync-a/deferred-items.md +0 -0
  180. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260708-fyr-batch-refactor-of-sync-apply-write-path-/260708-fyr-PLAN.md +0 -0
  181. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260708-fyr-batch-refactor-of-sync-apply-write-path-/260708-fyr-SUMMARY.md +0 -0
  182. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/260708-fyr-batch-refactor-of-sync-apply-write-path-/260708-fyr-VERIFICATION.md +0 -0
  183. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v2.0-quick/README.md +0 -0
  184. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-REQUIREMENTS.md +0 -0
  185. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-ROADMAP.md +0 -0
  186. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-01-PLAN.md +0 -0
  187. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-01-SUMMARY.md +0 -0
  188. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-02-PLAN.md +0 -0
  189. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-02-SUMMARY.md +0 -0
  190. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-03-PLAN.md +0 -0
  191. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-03-SUMMARY.md +0 -0
  192. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-04-PLAN.md +0 -0
  193. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-04-SUMMARY.md +0 -0
  194. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-05-PLAN.md +0 -0
  195. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-05-SUMMARY.md +0 -0
  196. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-06-PLAN.md +0 -0
  197. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-06-SUMMARY.md +0 -0
  198. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-CONTEXT.md +0 -0
  199. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-DISCUSSION-LOG.md +0 -0
  200. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-LIVE-FINDINGS.md +0 -0
  201. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-PATTERNS.md +0 -0
  202. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-REVIEW.md +0 -0
  203. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-VERIFICATION.md +0 -0
  204. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-REQUIREMENTS.md +0 -0
  205. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-ROADMAP.md +0 -0
  206. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-01-PLAN.md +0 -0
  207. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-01-SUMMARY.md +0 -0
  208. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-02-PLAN.md +0 -0
  209. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-02-SUMMARY.md +0 -0
  210. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-03-PLAN.md +0 -0
  211. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-03-SUMMARY.md +0 -0
  212. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-CONTEXT.md +0 -0
  213. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-DECISION-RECORD.md +0 -0
  214. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-DISCUSSION-LOG.md +0 -0
  215. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-LIVE-FINDINGS.md +0 -0
  216. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/COVERAGE.md +0 -0
  217. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-01-PLAN.md +0 -0
  218. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-01-SUMMARY.md +0 -0
  219. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-02-PLAN.md +0 -0
  220. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-02-SUMMARY.md +0 -0
  221. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-CONTEXT.md +0 -0
  222. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-DISCUSS-CHECKPOINT.json +0 -0
  223. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-VERIFICATION.md +0 -0
  224. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/17-google-link-album-selection/COVERAGE.md +0 -0
  225. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-01-PLAN.md +0 -0
  226. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-01-SUMMARY.md +0 -0
  227. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-02-PLAN.md +0 -0
  228. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-02-SUMMARY.md +0 -0
  229. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-03-PLAN.md +0 -0
  230. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-03-SUMMARY.md +0 -0
  231. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-CONTEXT.md +0 -0
  232. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-RESEARCH.md +0 -0
  233. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-UAT.md +0 -0
  234. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-VERIFICATION.md +0 -0
  235. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/19-debt-closeout/19-01-PLAN.md +0 -0
  236. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/19-debt-closeout/19-01-SUMMARY.md +0 -0
  237. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/19-debt-closeout/19-02-PLAN.md +0 -0
  238. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/19-debt-closeout/19-02-SUMMARY.md +0 -0
  239. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/19-debt-closeout/19-CONTEXT.md +0 -0
  240. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/19-debt-closeout/19-RESEARCH.md +0 -0
  241. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v4.0-phases/19-debt-closeout/19-VERIFICATION.md +0 -0
  242. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-REQUIREMENTS.md +0 -0
  243. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-ROADMAP.md +0 -0
  244. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/20-pushframe-rename-migration-repo-switch/20-SUMMARY.md +0 -0
  245. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-01-PLAN.md +0 -0
  246. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-01-SUMMARY.md +0 -0
  247. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-02-PLAN.md +0 -0
  248. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-02-SUMMARY.md +0 -0
  249. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-CONTEXT.md +0 -0
  250. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-RESEARCH.md +0 -0
  251. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-VERIFICATION.md +0 -0
  252. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-01-PLAN.md +0 -0
  253. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-01-SUMMARY.md +0 -0
  254. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-02-PLAN.md +0 -0
  255. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-02-SUMMARY.md +0 -0
  256. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-CONTEXT.md +0 -0
  257. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-RESEARCH.md +0 -0
  258. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-VERIFICATION.md +0 -0
  259. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/OPERATOR-STEPS.md +0 -0
  260. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/23-config-wizard-precedence/23-01-PLAN.md +0 -0
  261. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/23-config-wizard-precedence/23-01-SUMMARY.md +0 -0
  262. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/23-config-wizard-precedence/23-CONTEXT.md +0 -0
  263. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/23-config-wizard-precedence/23-RESEARCH.md +0 -0
  264. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/23-config-wizard-precedence/23-VERIFICATION.md +0 -0
  265. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-01-PLAN.md +0 -0
  266. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-01-SUMMARY.md +0 -0
  267. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-CONTEXT.md +0 -0
  268. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-RESEARCH.md +0 -0
  269. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-VERIFICATION.md +0 -0
  270. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-01-PLAN.md +0 -0
  271. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-01-SUMMARY.md +0 -0
  272. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-CONTEXT.md +0 -0
  273. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-RESEARCH.md +0 -0
  274. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-VERIFICATION.md +0 -0
  275. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/ALBUM-ACCESS-V4-ADDENDUM.md +0 -0
  276. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/ALBUM-ACCESS.md +0 -0
  277. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/ARCHITECTURE.md +0 -0
  278. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/BROWSER-AUTOMATION.md +0 -0
  279. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/FEATURES.md +0 -0
  280. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/PITFALLS.md +0 -0
  281. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/STACK.md +0 -0
  282. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/research/SUMMARY.md +0 -0
  283. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/state.json +0 -0
  284. {pushframe-5.1.26 → pushframe-5.1.28}/.planning/todos/completed/2026-07-06-promote-debug-flag-to-a-global-cli-convention.md +0 -0
  285. {pushframe-5.1.26 → pushframe-5.1.28}/.python-version +0 -0
  286. {pushframe-5.1.26 → pushframe-5.1.28}/LICENSE +0 -0
  287. {pushframe-5.1.26 → pushframe-5.1.28}/README.md +0 -0
  288. {pushframe-5.1.26 → pushframe-5.1.28}/SECURITY.md +0 -0
  289. {pushframe-5.1.26 → pushframe-5.1.28}/VERIFICATION-REPORT.md +0 -0
  290. {pushframe-5.1.26 → pushframe-5.1.28}/docs/DEVELOPING.md +0 -0
  291. {pushframe-5.1.26 → pushframe-5.1.28}/docs/INTERNALS.md +0 -0
  292. {pushframe-5.1.26 → pushframe-5.1.28}/docs/agents/domain.md +0 -0
  293. {pushframe-5.1.26 → pushframe-5.1.28}/docs/agents/issue-tracker.md +0 -0
  294. {pushframe-5.1.26 → pushframe-5.1.28}/docs/agents/triage-labels.md +0 -0
  295. {pushframe-5.1.26 → pushframe-5.1.28}/docs/superpowers/specs/2026-07-09-write-rate-limiter-design.md +0 -0
  296. {pushframe-5.1.26 → pushframe-5.1.28}/hatch_build.py +0 -0
  297. {pushframe-5.1.26 → pushframe-5.1.28}/main.py +0 -0
  298. {pushframe-5.1.26 → pushframe-5.1.28}/packaging/control-template +0 -0
  299. {pushframe-5.1.26 → pushframe-5.1.28}/packaging/lintian-overrides +0 -0
  300. {pushframe-5.1.26 → pushframe-5.1.28}/packaging/postinst +0 -0
  301. {pushframe-5.1.26 → pushframe-5.1.28}/packaging/pushframe.copyright +0 -0
  302. {pushframe-5.1.26 → pushframe-5.1.28}/probes/.gitignore +0 -0
  303. {pushframe-5.1.26 → pushframe-5.1.28}/probes/README.md +0 -0
  304. {pushframe-5.1.26 → pushframe-5.1.28}/probes/browser_bootstrap.py +0 -0
  305. {pushframe-5.1.26 → pushframe-5.1.28}/probes/common.py +0 -0
  306. {pushframe-5.1.26 → pushframe-5.1.28}/probes/cookie_vault.py +0 -0
  307. {pushframe-5.1.26 → pushframe-5.1.28}/probes/fidelity_check.py +0 -0
  308. {pushframe-5.1.26 → pushframe-5.1.28}/probes/rpc_capture.py +0 -0
  309. {pushframe-5.1.26 → pushframe-5.1.28}/probes/shared_link_probe.py +0 -0
  310. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/__init__.py +0 -0
  311. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/accountApi.py +0 -0
  312. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/activityApi.py +0 -0
  313. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/baseApi.py +0 -0
  314. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/notificationApi.py +0 -0
  315. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/peopleApi.py +0 -0
  316. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/api/playlistApi.py +0 -0
  317. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/aura.py +0 -0
  318. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/aws/__init__.py +0 -0
  319. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/aws/awsclient.py +0 -0
  320. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/aws/s3client.py +0 -0
  321. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/aws/sqsclient.py +0 -0
  322. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/cache.py +0 -0
  323. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/client.py +0 -0
  324. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/config_store.py +0 -0
  325. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/doctor.py +0 -0
  326. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/exif.py +0 -0
  327. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/export.py +0 -0
  328. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/__init__.py +0 -0
  329. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/bootstrap.py +0 -0
  330. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/cache.py +0 -0
  331. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/client.py +0 -0
  332. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/enumerate.py +0 -0
  333. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/manifest.py +0 -0
  334. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/parsers.py +0 -0
  335. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/redaction.py +0 -0
  336. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/google/vault.py +0 -0
  337. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/gsync.py +0 -0
  338. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/migration.py +0 -0
  339. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/models/__init__.py +0 -0
  340. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/models/activity.py +0 -0
  341. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/models/asset.py +0 -0
  342. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/models/frame.py +0 -0
  343. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/models/meta.py +0 -0
  344. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/models/person.py +0 -0
  345. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/models/user.py +0 -0
  346. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/pairs.py +0 -0
  347. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/preflight.py +0 -0
  348. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/ratelimit.py +0 -0
  349. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/reconcile.py +0 -0
  350. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/report.py +0 -0
  351. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/session.py +0 -0
  352. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/sync.py +0 -0
  353. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/utils/io.py +0 -0
  354. {pushframe-5.1.26 → pushframe-5.1.28}/pushframe/utils/settings.py +0 -0
  355. {pushframe-5.1.26 → pushframe-5.1.28}/scripts/build-deb.sh +0 -0
  356. {pushframe-5.1.26 → pushframe-5.1.28}/scripts/make-apt-key.sh +0 -0
  357. {pushframe-5.1.26 → pushframe-5.1.28}/scripts/publish-apt-repo.sh +0 -0
  358. {pushframe-5.1.26 → pushframe-5.1.28}/scripts/release.sh +0 -0
  359. {pushframe-5.1.26 → pushframe-5.1.28}/scripts/test-apt-journey-container.sh +0 -0
  360. {pushframe-5.1.26 → pushframe-5.1.28}/scripts/test-config-journey-container.sh +0 -0
  361. {pushframe-5.1.26 → pushframe-5.1.28}/scripts/test-deb-container.sh +0 -0
  362. {pushframe-5.1.26 → pushframe-5.1.28}/scripts/test-install-journey-container.sh +0 -0
  363. {pushframe-5.1.26 → pushframe-5.1.28}/scripts/test-multipair-journey-container.sh +0 -0
  364. {pushframe-5.1.26 → pushframe-5.1.28}/tests/conftest.py +0 -0
  365. {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/asset_unprocessed.json +0 -0
  366. {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/assets_page1.json +0 -0
  367. {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/assets_page2.json +0 -0
  368. {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/assets_placeholders.json +0 -0
  369. {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/error_envelope.json +0 -0
  370. {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/frame_detail.json +0 -0
  371. {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/frames.json +0 -0
  372. {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/google_rpc_snackc_page.json +0 -0
  373. {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/google_share_page_sample.html +0 -0
  374. {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/login.json +0 -0
  375. {pushframe-5.1.26 → pushframe-5.1.28}/tests/fixtures/probe_af_initdata_sample.html +0 -0
  376. {pushframe-5.1.26 → pushframe-5.1.28}/tests/offline.py +0 -0
  377. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_asset_unprocessed.py +0 -0
  378. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_aura_logger.py +0 -0
  379. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_batch_update_contract.py +0 -0
  380. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_budget_identity.py +0 -0
  381. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_build_hook.py +0 -0
  382. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_apply.py +0 -0
  383. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_google_album.py +0 -0
  384. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_google_link.py +0 -0
  385. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_inspect.py +0 -0
  386. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_push_budget_geo.py +0 -0
  387. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_reconcile.py +0 -0
  388. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_status.py +0 -0
  389. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_status_google.py +0 -0
  390. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_cli_sync.py +0 -0
  391. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_client_rate_limit.py +0 -0
  392. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_config_wizard.py +0 -0
  393. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_docs_hint.py +0 -0
  394. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_doctor.py +0 -0
  395. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_execute_plan.py +0 -0
  396. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_execute_plan_budget_geo.py +0 -0
  397. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_fixtures_validity.py +0 -0
  398. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_google_cache.py +0 -0
  399. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_google_client.py +0 -0
  400. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_google_enumerate.py +0 -0
  401. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_google_manifest.py +0 -0
  402. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_google_parsers.py +0 -0
  403. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_google_vault.py +0 -0
  404. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_gsync_all.py +0 -0
  405. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_gsync_engine.py +0 -0
  406. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_gsync_execute.py +0 -0
  407. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_imports.py +0 -0
  408. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_logout.py +0 -0
  409. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_migration.py +0 -0
  410. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_network_guard.py +0 -0
  411. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_offline_read_path.py +0 -0
  412. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_pairs.py +0 -0
  413. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_preflight_sweep.py +0 -0
  414. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_preflight_vault.py +0 -0
  415. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_prep_upload_formats.py +0 -0
  416. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_probe_browser.py +0 -0
  417. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_probe_shared_link.py +0 -0
  418. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_ratelimit.py +0 -0
  419. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_read_path.py +0 -0
  420. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_reconcile.py +0 -0
  421. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_report.py +0 -0
  422. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_retry_401.py +0 -0
  423. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_session.py +0 -0
  424. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_settings_budget.py +0 -0
  425. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_settings_resolution.py +0 -0
  426. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_sync_engine.py +0 -0
  427. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_token_refresh_extension.py +0 -0
  428. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_write_endpoints_failloud.py +0 -0
  429. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_write_formats_live.py +0 -0
  430. {pushframe-5.1.26 → pushframe-5.1.28}/tests/test_write_throttling.py +0 -0
@@ -12,6 +12,59 @@ every channel.
12
12
  `pushframe` is an unofficial community CLI for Aura Frames digital photo
13
13
  frames; it is not affiliated with Aura Frames Inc.
14
14
 
15
+ ## [5.1.28] - 2026-10-04
16
+
17
+ ### Added
18
+
19
+ - Scheduled `push` jobs (cron or hand-written units feeding a "buffet"
20
+ directory) take `--scheduled`/`--report-tag` and write the same run
21
+ bookkeeping as scheduled `google-sync`/`sync`: the `=== run … ===`
22
+ header with the job tag, the `=== run end rc=<rc> elapsed=<s>s ===`
23
+ footer, then the 5 MiB rotation. `push` stays purely additive —
24
+ bookkeeping only, no removal guard involved.
25
+
26
+ ## [5.1.27] - 2026-10-03
27
+
28
+ ### Fixed
29
+
30
+ - `schedule add --sync-dir` units never ran: their ExecStart passed
31
+ `--scheduled`, which the `sync` parser never accepted — every tick died
32
+ with `unrecognized arguments: --scheduled` before any sync happened. The
33
+ parser now takes `--scheduled` and `--report-tag`.
34
+
35
+ ### Added
36
+
37
+ - `status` flags any installed scheduled unit whose ExecStart the current
38
+ binary's argparse refuses (`⚠ would FAIL to start: …` with the exact
39
+ reason) — hand-edited or version-drifted units otherwise die on every
40
+ tick with nothing but journalctl to explain why. The same contract is
41
+ enforced offline by a test over every ExecStart `schedule_add` can
42
+ generate, and on the host by a deliberate `pytest -m live` check over
43
+ the real installed units.
44
+ - Scheduled `sync` jobs (`--sync-dir`) write the same run bookkeeping as
45
+ scheduled `google-sync`: the `=== run … ===` header (with the job tag),
46
+ the `=== run end rc=<rc> elapsed=<s>s ===` footer, and the 5 MiB log
47
+ rotation — `<job>.log` alone answers which night synced what. Schedule
48
+ units now pass `--report-tag "<job>"`; `--report` (email) remains
49
+ google-sync-only.
50
+
51
+ ### Changed
52
+
53
+ - Replaced the deprecated `datetime.utcnow()` in `get_utc_now()` with the
54
+ deprecation-free naive-UTC equivalent
55
+ (`datetime.now(timezone.utc).replace(tzinfo=None)`) — same value, same
56
+ naive contract every caller relies on (`parse_aura_dt` diffs,
57
+ `WriteBudget` arithmetic and its persisted `updated_at`).
58
+ - Replaced Pydantic's deprecated `.dict()` with `.model_dump()` in the
59
+ API request-payload builders (`AssetApi.batch_update`,
60
+ `AssetApi.crop_asset`, `FrameApi.update_frame`) — Python 3.13-clean,
61
+ ready for Pydantic V3.
62
+ - Job log rotation keeps a bounded history instead of a single generation:
63
+ past 5 MiB the log slides through `<job>.log.1` … `<job>.log.28`
64
+ (default; `PUSHFRAME_JOB_LOG_KEEP` tunes it), the oldest generation
65
+ falling off — several weeks of consultable history, worst case
66
+ `keep × 5 MiB` per job.
67
+
15
68
  ## [5.1.26] - 2026-10-03
16
69
 
17
70
  ### Added
@@ -137,7 +137,7 @@ Python toolchain, so we know exactly what survives before building anything new.
137
137
  - Partial model pattern via custom `AllOptional` metaclass (`meta.py`): `class FramePartial(Frame, metaclass=AllOptional)`
138
138
  - Enums used for known string-valued discriminators: `ActivityType`, `ReactionType`, `Feature`
139
139
  - Pydantic `@validator` used for cross-field validation: `AssetPartialId.check_id_or_local_id`
140
- - `.dict(include={...})` used to build API request payloads from model instances
140
+ - `.model_dump(include={...})` used to build API request payloads from model instances
141
141
  ## Configuration
142
142
  - Credentials passed via env vars: `AURA_EMAIL`, `AURA_PASSWORD`
143
143
  - AWS pool IDs hardcoded as module constants (flagged as TODO to move to config)
@@ -172,7 +172,7 @@ Python toolchain, so we know exactly what survives before building anything new.
172
172
  ## Pattern Overview
173
173
  - `Aura` is the single public entry point; callers never instantiate API classes directly
174
174
  - Each `*Api` class is a thin wrapper over one REST resource domain with zero business logic of its own
175
- - Pydantic models serve as both validated DTOs and serialization targets (`.dict()` for request payloads, `**json_response` for hydration)
175
+ - Pydantic models serve as both validated DTOs and serialization targets (`.model_dump()` for request payloads, `**json_response` for hydration)
176
176
  - AWS auth uses Cognito anonymous identity pools — no long-lived AWS credentials needed
177
177
  - All HTTP is synchronous (`httpx.Client`); the codebase has acknowledged debt to migrate to async
178
178
  ## Layers
@@ -231,7 +231,7 @@ Python toolchain, so we know exactly what survives before building anything new.
231
231
  - Pattern: Constructor injection of `Client`; no interface/protocol defined
232
232
  - Purpose: Validate, hydrate, and serialise API response JSON
233
233
  - Examples: `auraframes/models/frame.py`, `auraframes/models/asset.py`
234
- - Pattern: `Model(**json_response.get('key'))` for hydration; `.dict(include={...})` for request payloads
234
+ - Pattern: `Model(**json_response.get('key'))` for hydration; `.model_dump(include={...})` for request payloads
235
235
  - Purpose: Generates a "partial" variant of any model (all fields become `Optional`) for PATCH-style updates
236
236
  - Examples: `auraframes/models/meta.py` — used by `FramePartial` in `auraframes/models/frame.py:93`
237
237
  - Pattern: `class FramePartial(Frame, metaclass=AllOptional): pass`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pushframe
3
- Version: 5.1.26
3
+ Version: 5.1.28
4
4
  Summary: Unofficial community CLI for Aura Frames digital photo frames (sync a local folder or a Google Photos album; not affiliated with Aura Frames Inc.)
5
5
  Project-URL: Homepage, https://github.com/coredmp95/pushframe
6
6
  Project-URL: Repository, https://github.com/coredmp95/pushframe
@@ -633,7 +633,7 @@ The second run reports `To upload: 0` — the photo is re-shown, never uploaded
633
633
  usage: pushframe push [-h] --frame FRAME [--apply] [--yes] [--limit LIMIT]
634
634
  [--batch-size BATCH_SIZE] [--chunk-delay CHUNK_DELAY]
635
635
  [--max-wait MAX_WAIT] [--no-wait] [--country COUNTRY]
636
- [--ignore-budget]
636
+ [--ignore-budget] [--scheduled] [--report-tag REPORT_TAG]
637
637
  dir
638
638
 
639
639
  positional arguments:
@@ -652,12 +652,21 @@ options:
652
652
  --no-wait Stop immediately instead of waiting when the write budget is exhausted
653
653
  --country COUNTRY Override the expected account country for the geo pre-flight guard
654
654
  --ignore-budget Escape hatch: bypass the write budget entirely for this run
655
+ --scheduled Timed-run bookkeeping: the job log opens with a timestamped
656
+ "=== run ... ===" header and closes with the rc/elapsed footer
657
+ --report-tag REPORT_TAG Job identity in the log header and the log-rotation target
655
658
  ```
656
659
 
657
660
  `push` is **structurally additive**: the removal list is forced empty, so it can never hide,
658
661
  remove, or re-show anything. It is the safe way to add photos from a supply directory without
659
662
  the frame being diffed to match it.
660
663
 
664
+ Cron-fed buffet frames: run `push ... --apply --yes --scheduled --report-tag <job>` from
665
+ cron or a hand-written unit — the job log (`~/.local/state/pushframe/<job>.log` for units)
666
+ then opens with a timestamped `=== run … ===` header, closes with `=== run end rc=<rc>
667
+ elapsed=<s>s ===`, and rotates past 5 MiB (`PUSHFRAME_JOB_LOG_KEEP` tunes the depth), like
668
+ every scheduled verb.
669
+
661
670
  ```bash
662
671
  pushframe push ./buffet --frame "Living Room"
663
672
  ```
@@ -104,6 +104,11 @@ chosen per job at install time with `schedule add … --report LEVEL`
104
104
  | `schedule add: --FLAG needs a value` / `unknown option "…"` / `… given twice` / `unexpected "…"` | The `schedule add` tail is malformed — a dangling flag, a typo in a flag name (they are listed), a duplicated flag, or a stray word | Read the message: it names the exact token and, for unknown options, lists the valid ones. Fixed in 5.1.22 — these used to crash with a raw traceback (pre-5.1.22) |
105
105
  | ``schedule list: unexpected "…" — `pushframe schedule list` takes no arguments`` / `schedule remove: unexpected "…" — usage: pushframe schedule remove <job>` | A stray extra word after the single argument these verbs take — exit 2, nothing listed or removed. Fixed in 5.1.25 — extras were ignored silently | `schedule list` takes no arguments; `schedule remove` takes exactly one job name |
106
106
  | A timer "fails" repeatedly | Read the job log | `~/.local/state/pushframe/<job>.log`; the unit ends on the first failure on purpose — the next tick is the retry |
107
+ | `=== run 2026-10-04T00:02:59+02:00 job=nightly pushframe=5.1.26 ===` opens a job log, `=== run end rc=0 elapsed=42s ===` closes it | Every scheduled run stamps its log with its identity (when, which job, which pushframe — 5.1.26) and its outcome (exit code + duration). A missing footer means the run died by exception: the traceback and systemd's `Failed` line carry that story | Read `<job>.log` one block per run; `rc=0` = healthy. No footer → the traceback above it and `journalctl --user -u pushframe-<job>` tell the rest |
108
+ | ` ~ <asset-id> (taken 2020-01-01) — re-show` / ` - <asset-id> (taken <date>)` lines under the plan | 5.1.26: a scheduled delta names WHICH frame assets it touches (the shape the interactive dry-run prints) — a delta is identifiable, not just countable | Check the ids against your expectations; `google-sync --pair <name>` without `--apply` shows the same plan interactively |
109
+ | `<job>.log.1` (and `.log.2` … `.log.28`) appearing next to a job log | 5.1.27: past 5 MiB the log slides one generation older at run end, the oldest falling off — the live file stays bounded, weeks of history stay readable. Generations beyond a lowered keep are swept | Nothing — that is the size bound working. `PUSHFRAME_JOB_LOG_KEEP` tunes the depth (a junk value falls back to the default; a rotation failure never fails a run) |
110
+ | A `--sync-dir` job that never ran, with `unrecognized arguments: --scheduled` in its log (pre-5.1.27) | The unit's ExecStart passed `--scheduled` before the `sync` parser accepted it — every tick died at argparse (exit 2) before any sync | Fixed in 5.1.27 — upgrade and the existing unit works as-is; re-add it (`schedule add --sync-dir …`) to also pick up `--report-tag` (named header + rotation) |
111
+ | `⚠ would FAIL to start: unrecognized arguments: --nope — re-add the job …` under a job line in `pushframe status` | The unit's ExecStart uses a flag the CURRENT binary's argparse refuses — the job dies on every tick before doing anything (hand-edited unit, or a unit installed by a mismatched pushframe version). 5.1.27 | Fix the unit or re-add the job; `pushframe --version` against the unit's flags tells you which way the drift goes |
107
112
 
108
113
  ## Anything else
109
114
 
@@ -1,4 +1,4 @@
1
1
  # pushframe — unofficial community CLI for Aura Frames (not affiliated).
2
2
  # Version single source for the CLI surface (REL-02); pyproject keeps the
3
3
  # packaging metadata version and tests/test_version.py enforces lockstep.
4
- __version__ = "5.1.26"
4
+ __version__ = "5.1.28"
@@ -69,7 +69,7 @@ class AssetApi(BaseApi):
69
69
 
70
70
  json_response = self._client.put(f'/assets/batch_update.json', data={
71
71
  "assets": [
72
- item.dict(
72
+ item.model_dump(
73
73
  include={
74
74
  'data_uti': True,
75
75
  'favorite': True,
@@ -179,7 +179,7 @@ class AssetApi(BaseApi):
179
179
  :param asset: Asset containing new rotation/rect data.
180
180
  :return: The asset with modified crop fields.
181
181
  """
182
- json_response = self._client.post(f'/assets/crop.json', data=asset.dict(
182
+ json_response = self._client.post(f'/assets/crop.json', data=asset.model_dump(
183
183
  include={
184
184
  'id': True,
185
185
  'local_identifier': True,
@@ -145,7 +145,7 @@ class FrameApi(BaseApi):
145
145
  :return: Returns the hydrated frame with changes.
146
146
  """
147
147
  json_response = self._client.put(f'/frames/{frame_id}.json',
148
- data={'frame': frame_partial.dict(exclude_unset=True)})
148
+ data={'frame': frame_partial.model_dump(exclude_unset=True)})
149
149
  return Frame(**json_response.get('frame'))
150
150
 
151
151
  def select_asset(self, frame_id: str, asset_partial_ids: AssetPartialId | list[AssetPartialId]) -> int:
@@ -103,6 +103,18 @@ def build_parser() -> argparse.ArgumentParser:
103
103
  sync_parser.add_argument('--frame', required=True, help='Frame name (substring) or id')
104
104
  sync_parser.add_argument('--apply', action='store_true', default=False, help='Execute the plan (upload + delete) instead of only printing it')
105
105
  sync_parser.add_argument('--yes', action='store_true', default=False, help='Skip the confirmation prompt (required for --apply when running non-interactively)')
106
+ # Scheduled sync-dir jobs (pushframe schedule add ... --sync-dir) run
107
+ # through this verb, so it needs the same bookkeeping flags as scheduled
108
+ # google-sync. (Regression: units passed --scheduled before this parser
109
+ # accepted it -- every sync-dir tick died 'unrecognized arguments'.)
110
+ sync_parser.add_argument('--scheduled', action='store_true', default=False,
111
+ help='Timed-run bookkeeping: the job log opens with a '
112
+ 'timestamped "=== run ... ===" header and closes with '
113
+ 'the rc/elapsed footer (used by pushframe schedule units)')
114
+ sync_parser.add_argument('--report-tag', default=None, dest='report_tag',
115
+ help='Job identity in the log header and the log-rotation '
116
+ 'target (schedule units pass the job name; sync jobs '
117
+ 'have no --report email)')
106
118
  # The three tiers of "no longer in the local directory" (D-02/D-03).
107
119
  # Hiding is the default because the frame has no photo-count limit, so a
108
120
  # mistaken sync should cost visibility, never photos. argparse enforces
@@ -136,6 +148,15 @@ def build_parser() -> argparse.ArgumentParser:
136
148
  push_parser.add_argument('--no-wait', action='store_true', default=False, help='Stop immediately instead of waiting when the write budget is exhausted')
137
149
  push_parser.add_argument('--country', default=None, help='Override the expected account country for the geo pre-flight guard (default from PUSHFRAME_COUNTRY, legacy AURA_COUNTRY)')
138
150
  push_parser.add_argument('--ignore-budget', action='store_true', default=False, dest='ignore_budget', help='Escape hatch: bypass the write budget entirely for this run')
151
+ # Scheduled push jobs (cron or hand-written units feeding a "buffet"
152
+ # directory) get the same log bookkeeping as scheduled sync/google-sync.
153
+ push_parser.add_argument('--scheduled', action='store_true', default=False,
154
+ help='Timed-run bookkeeping: the job log opens with a '
155
+ 'timestamped "=== run ... ===" header and closes with '
156
+ 'the rc/elapsed footer')
157
+ push_parser.add_argument('--report-tag', default=None, dest='report_tag',
158
+ help='Job identity in the log header and the log-rotation '
159
+ 'target (crons and units pass a job name)')
139
160
 
140
161
  # `reconcile` = data hygiene on EXISTING stuck placeholder rows (REL-05,
141
162
  # D-13) -- deliberately outside the sync/push loop. Report-only by
@@ -1076,6 +1097,7 @@ def _schedule_status_section() -> list[str]:
1076
1097
  for svc_path in services:
1077
1098
  job = svc_path.stem.replace('pushframe-', '')
1078
1099
  what = '(unreadable unit)'
1100
+ exec_line = ''
1079
1101
  try:
1080
1102
  unit_text = svc_path.read_text(encoding='utf-8')
1081
1103
  exec_line = next((l for l in unit_text.splitlines()
@@ -1085,10 +1107,64 @@ def _schedule_status_section() -> list[str]:
1085
1107
  pass
1086
1108
  when = next_fire.get(f'pushframe-{job}.timer', '(timer not enabled)')
1087
1109
  lines.append(f' {job}: {what} — next: {when}')
1110
+ if exec_line:
1111
+ try:
1112
+ problem = _unit_argv_parse_problem(exec_line)
1113
+ except Exception:
1114
+ problem = None # status degrades; it never fails here
1115
+ if problem:
1116
+ lines.append(f' ⚠ would FAIL to start: {problem} — '
1117
+ f're-add the job (`pushframe schedule add ...`) '
1118
+ f'or fix the unit')
1088
1119
  lines.append(' (detail: `pushframe schedule list`)')
1089
1120
  return lines
1090
1121
 
1091
1122
 
1123
+ def _unit_argv_parse_problem(exec_line: str) -> str | None:
1124
+ """Would this unit's ExecStart survive the CURRENT binary's argparse?
1125
+
1126
+ The tokens from the verb onward go through the real parser
1127
+ (parse_known_args): unrecognized flags, missing required arguments or
1128
+ invalid values surface here as a one-line reason — None when the
1129
+ command line is accepted verbatim. This is the guard against the
1130
+ sync-dir class of breakage (units installed passing a flag the parser
1131
+ of the day rejects die on EVERY tick — argparse exit 2, before any
1132
+ sync — with nothing but journalctl to explain why): hand-edited units
1133
+ and version drift (unit newer or older than the binary) are exactly
1134
+ what it surfaces. Never raises — status degrades, it does not fail.
1135
+ """
1136
+ import contextlib
1137
+ import io
1138
+ import shlex
1139
+ body = exec_line.removeprefix('ExecStart=')
1140
+ try:
1141
+ tokens = shlex.split(body)
1142
+ except ValueError:
1143
+ return 'ExecStart is not shell-quoted correctly'
1144
+ parser = build_parser()
1145
+ try:
1146
+ sub = next(a for a in parser._actions if a.dest == 'command')
1147
+ verbs = set(sub.choices)
1148
+ except Exception:
1149
+ return None # cannot introspect the parser — refuse to guess
1150
+ verb_idx = next((i for i, t in enumerate(tokens) if t in verbs), None)
1151
+ if verb_idx is None:
1152
+ return 'no pushframe command found in ExecStart'
1153
+ argv = tokens[verb_idx:]
1154
+ err = io.StringIO()
1155
+ try:
1156
+ with contextlib.redirect_stderr(err):
1157
+ _, extras = parser.parse_known_args(argv)
1158
+ except SystemExit:
1159
+ detail = err.getvalue().split('error:', 1)[-1]
1160
+ detail = ' '.join(detail.split())[:160]
1161
+ return f'argparse refused: {detail}' if detail \
1162
+ else 'argparse refused the command line'
1163
+ if extras:
1164
+ return f'unrecognized arguments: {" ".join(map(str, extras))[:160]}'
1165
+ return None
1166
+
1167
+
1092
1168
  def _frame_flag_needs_value(rest: list[str], flag: str) -> bool:
1093
1169
  """True when `flag` is present but dangling (last token, or followed by
1094
1170
  another --flag) — the ExecStart parser's guard against indexing past
@@ -2212,23 +2288,68 @@ def _scheduled_run_footer(rc: int, elapsed_s: float) -> None:
2212
2288
  print(f'=== run end rc={rc} elapsed={round(elapsed_s)}s ===')
2213
2289
 
2214
2290
 
2291
+ JOB_LOG_KEEP_GENERATIONS = 28
2292
+
2293
+
2294
+ def _job_log_keep() -> int:
2295
+ """Ops knob for the rotation history depth: PUSHFRAME_JOB_LOG_KEEP.
2296
+ A junk or <1 value falls back to the default — a retention typo must
2297
+ never fail a run that already succeeded (this function's only contract).
2298
+ """
2299
+ try:
2300
+ keep = int(os.getenv('PUSHFRAME_JOB_LOG_KEEP', ''))
2301
+ except ValueError:
2302
+ return JOB_LOG_KEEP_GENERATIONS
2303
+ return keep if keep >= 1 else JOB_LOG_KEEP_GENERATIONS
2304
+
2305
+
2215
2306
  def _rotate_job_log_if_large(report_tag: str | None, *,
2216
- limit_bytes: int = 5 * 1024 * 1024) -> bool:
2217
- """Bound a job's append log (systemd reopens it with O_APPEND every
2218
- start): past `limit_bytes`, the log slides to <job>.log.1 — one
2219
- previous generation kept. Called at run END: this process's still-open
2220
- stdout fd keeps appending to the renamed inode until exit, and the next
2221
- start opens a fresh <job>.log, so nothing is lost and no fd surgery is
2222
- needed. A rotation failure must never fail a run that already succeeded.
2307
+ limit_bytes: int = 5 * 1024 * 1024,
2308
+ keep: int | None = None) -> bool:
2309
+ """Bound a job's append log while keeping history consultable (systemd
2310
+ reopens the file with O_APPEND every start): past `limit_bytes`, the log
2311
+ slides one generation older — <job>.log.1 … <job>.log.<keep>, the oldest
2312
+ generation falling off the end — so the live file stays bounded AND the
2313
+ previous weeks remain readable. Generations are size-bounded, not
2314
+ time-bounded: a nightly job crossing the limit every night keeps ~4
2315
+ weeks (default 28); a quiet one keeps months. Worst case per job:
2316
+ keep × limit bytes. Called at run END: this process's still-open stdout
2317
+ fd keeps appending to the renamed inode until exit, and the next start
2318
+ opens a fresh <job>.log, so nothing is lost and no fd surgery is needed.
2319
+ A rotation failure must never fail a run that already succeeded.
2223
2320
  """
2224
2321
  if not report_tag:
2225
2322
  return False
2323
+ if keep is None:
2324
+ keep = _job_log_keep()
2325
+ if keep < 1:
2326
+ raise ValueError(f'keep={keep!r} must be >= 1')
2226
2327
  from pushframe.schedule import LOG_DIR
2227
2328
  log = LOG_DIR / f'{report_tag}.log'
2228
2329
  try:
2229
2330
  if not log.exists() or log.stat().st_size <= limit_bytes:
2230
2331
  return False
2332
+ # Slide every existing generation one slot older, oldest falling off
2333
+ # (os.replace overwrites the destination): .log.(keep-1) -> .log.keep,
2334
+ # ..., .log.1 -> .log.2, then the live log -> .log.1.
2335
+ for slot in range(keep - 1, 0, -1):
2336
+ src = log.parent / f'{log.name}.{slot}'
2337
+ if src.exists():
2338
+ os.replace(src, log.parent / f'{log.name}.{slot + 1}')
2231
2339
  os.replace(log, log.parent / (log.name + '.1'))
2340
+ # Generations beyond `keep` can only exist after an operator LOWERED
2341
+ # the retention (env or code); leftovers would linger as unbounded
2342
+ # silent tails, so sweep them — tolerantly, same run-success rule.
2343
+ try:
2344
+ prefix = log.name + '.'
2345
+ for stale in log.parent.iterdir():
2346
+ if not stale.name.startswith(prefix):
2347
+ continue
2348
+ suffix = stale.name[len(prefix):]
2349
+ if suffix.isdigit() and int(suffix) > keep:
2350
+ stale.unlink()
2351
+ except OSError:
2352
+ pass
2232
2353
  return True
2233
2354
  except OSError:
2234
2355
  return False
@@ -2398,16 +2519,40 @@ def _main(argv=None) -> int:
2398
2519
  return run_inspect(args.frame, debug=args.debug)
2399
2520
  if args.command == 'sync':
2400
2521
  removal_mode = 'hard_delete' if args.hard_delete else ('delete' if args.delete else 'hide')
2401
- return run_sync(args.dir, args.frame, apply=args.apply, yes=args.yes, debug=args.debug,
2402
- removal_mode=removal_mode)
2522
+ import time as _time
2523
+ if args.scheduled:
2524
+ _scheduled_run_header(args.report_tag)
2525
+ _t0 = _time.monotonic()
2526
+ rc = run_sync(args.dir, args.frame, apply=args.apply, yes=args.yes, debug=args.debug,
2527
+ removal_mode=removal_mode)
2528
+ if args.scheduled:
2529
+ # Same bookkeeping contract as scheduled google-sync: the job
2530
+ # log carries the run's identity (header above), end (rc +
2531
+ # elapsed), then the size bound — <job>.log alone answers which
2532
+ # night synced what.
2533
+ _scheduled_run_footer(rc, _time.monotonic() - _t0)
2534
+ _rotate_job_log_if_large(args.report_tag)
2535
+ return rc
2403
2536
  if args.command == 'push':
2404
- return run_sync(
2537
+ import time as _time
2538
+ if args.scheduled:
2539
+ _scheduled_run_header(args.report_tag)
2540
+ _t0 = _time.monotonic()
2541
+ rc = run_sync(
2405
2542
  args.dir, args.frame, apply=args.apply, yes=args.yes, debug=args.debug,
2406
2543
  no_delete=True, limit=args.limit, batch_size=args.batch_size,
2407
2544
  chunk_delay=args.chunk_delay, verb='push',
2408
2545
  max_wait=args.max_wait, no_wait=args.no_wait,
2409
2546
  country=args.country, ignore_budget=args.ignore_budget,
2410
2547
  )
2548
+ if args.scheduled:
2549
+ # Same bookkeeping contract as scheduled google-sync/sync: the
2550
+ # job log carries the run's identity (header above), end (rc +
2551
+ # elapsed), then the size bound. push is purely additive, so
2552
+ # there is no mass-hide guard to check — bookkeeping only.
2553
+ _scheduled_run_footer(rc, _time.monotonic() - _t0)
2554
+ _rotate_job_log_if_large(args.report_tag)
2555
+ return rc
2411
2556
  if args.command == 'reconcile':
2412
2557
  return run_reconcile(
2413
2558
  args.frame, remove=args.remove, yes=args.yes, mechanism=args.mechanism,
@@ -132,8 +132,12 @@ def _exec_start_for(pair: str | None = None, album: str | None = None,
132
132
  from pushframe import config_store
133
133
  parts = [_pushframe_command()]
134
134
  if sync_dir:
135
+ # --report-tag carries the job identity into the run's log header
136
+ # and its rotation (sync jobs have no --report email; the tag is
137
+ # what makes <job>.log self-describing).
135
138
  parts += ['sync', f'"{sync_dir}"', f'--frame "{frame}"',
136
- '--apply', '--yes', '--scheduled']
139
+ '--apply', '--yes', '--scheduled',
140
+ '--report-tag', f'"{job}"']
137
141
  else:
138
142
  spec = (config_store.load().get('pairs', {}) or {}).get(pair or '', {})
139
143
  album = spec.get('album', album)
@@ -0,0 +1,27 @@
1
+ from datetime import datetime, timezone
2
+
3
+ AURA_DT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
4
+
5
+
6
+ def parse_aura_dt(aura_dt_str: str):
7
+ return datetime.strptime(aura_dt_str, AURA_DT_FORMAT)
8
+
9
+
10
+ def get_utc_now():
11
+ """Naive UTC now -- the exact value `datetime.utcnow()` used to return.
12
+
13
+ Deliberately NAIVE, not `datetime.now(timezone.utc)`: every consumer
14
+ subtracts this from naive datetimes or stores it next to them --
15
+ `find_placeholders` diffs it against `parse_aura_dt` results (strptime
16
+ is naive) and `WriteBudget.acquire` does `(now - updated_at)` arithmetic
17
+ on a value persisted via isoformat round-trip. One aware datetime in
18
+ that pipeline raises `TypeError: can't subtract offset-naive and
19
+ offset-aware datetimes`. `datetime.now(timezone.utc).replace(tzinfo=None)`
20
+ is the deprecation-free equivalent Python recommends for the old
21
+ `utcnow()` semantics (no wall-clock read, immune to NTP/adjtime jumps).
22
+ """
23
+ return datetime.now(timezone.utc).replace(tzinfo=None)
24
+
25
+
26
+ def format_dt_to_aura(dt: datetime):
27
+ return dt.strftime(AURA_DT_FORMAT)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pushframe"
3
- version = "5.1.26"
3
+ version = "5.1.28"
4
4
  description = "Unofficial community CLI for Aura Frames digital photo frames (sync a local folder or a Google Photos album; not affiliated with Aura Frames Inc.)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -21,7 +21,7 @@ from pydantic import ValidationError
21
21
 
22
22
  from pushframe.models.asset import Asset, AssetPartial, AssetPartialId
23
23
 
24
- # The exact allowlist AssetApi.batch_update uses for its `.dict(include=...)`
24
+ # The exact allowlist AssetApi.batch_update uses for its `.model_dump(include=...)`
25
25
  # call (pushframe/api/assetApi.py:19-38) -- AssetPartial must serialize to
26
26
  # precisely this shape.
27
27
  BATCH_UPDATE_ALLOWLIST = {
@@ -51,7 +51,7 @@ def test_asset_partial_constructs_with_id_unset():
51
51
 
52
52
 
53
53
  def test_asset_partial_serializes_to_batch_update_payload_shape():
54
- """.dict(include=...) over batch_update's allowlist returns exactly
54
+ """.model_dump(include=...) over batch_update's allowlist returns exactly
55
55
  those keys, regardless of which fields were actually set."""
56
56
  partial = AssetPartial(
57
57
  local_identifier='local-id-1',
@@ -65,7 +65,7 @@ def test_asset_partial_serializes_to_batch_update_payload_shape():
65
65
  upload_priority=1,
66
66
  )
67
67
 
68
- payload = partial.dict(include=BATCH_UPDATE_ALLOWLIST)
68
+ payload = partial.model_dump(include=BATCH_UPDATE_ALLOWLIST)
69
69
 
70
70
  assert set(payload.keys()) == set(BATCH_UPDATE_ALLOWLIST.keys())
71
71
  assert payload['local_identifier'] == 'local-id-1'