WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Conversation

@lucasmrod
Copy link
Member

@lucasmrod lucasmrod commented Dec 26, 2025

Resolves #35455

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.

  • Input data is properly validated, SELECT * is avoided, SQL injection is prevented (using placeholders for values in statements)

Testing

  • Added/updated automated tests
  • QA'd all new/changed functionality manually

Summary by CodeRabbit

  • New Features

    • Introduced scheduled software updates for iOS/iPadOS managed devices with time-window based installation scheduling that considers device timezone
    • Added timezone tracking for managed iOS/iPadOS hosts to enable timezone-aware update scheduling
  • Improvements

    • Enhanced software update scheduling system with timezone and time-window awareness for eligible devices

✏️ Tip: You can customize this high-level summary in your review settings.

@codecov
Copy link

codecov bot commented Dec 26, 2025

Codecov Report

❌ Patch coverage is 19.10828% with 254 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.80%. Comparing base (1751673) to head (b7e4e10).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
server/service/apple_mdm.go 11.95% 234 Missing and 9 partials ⚠️
cmd/fleet/serve.go 0.00% 9 Missing ⚠️
...ontend/pages/hosts/details/cards/Vitals/Vitals.tsx 60.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #37704      +/-   ##
==========================================
- Coverage   65.88%   65.80%   -0.08%     
==========================================
  Files        2361     2361              
  Lines      187298   187594     +296     
  Branches     7974     7873     -101     
==========================================
+ Hits       123394   123449      +55     
- Misses      52623    52858     +235     
- Partials    11281    11287       +6     
Flag Coverage Δ
backend 67.64% <18.44%> (-0.09%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@lucasmrod
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 26, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 26, 2025

Walkthrough

This PR implements scheduled software updates for iOS/iPadOS managed devices. The changes add timezone tracking to hosts, integrate a VPP installer dependency into the MDM service, and introduce logic to determine when eligible software should be automatically installed within configured maintenance windows.

Changes

Cohort / File(s) Summary
Host Timezone Support
server/fleet/hosts.go, server/datastore/mysql/hosts.go, server/datastore/mysql/labels.go, server/mdm/apple/commander.go
Added TimeZone field to Host struct and updated SQL queries to select/update timezone. Extended DeviceInformation command to request timezone data from devices.
Scheduled Update Logic
server/service/apple_mdm.go, server/service/apple_mdm_test.go
Introduced handleScheduledUpdates method and timezone utility functions (getCurrentLocalTimeInHostTimeZone, isTimezoneInWindow, parseHHMM). Added VPP installer and premium flag fields to MDMAppleCheckinAndCommandService. Propagates timezone from device responses and schedules VPP app installations based on time windows and update eligibility.
Software Auto-Update Scheduling API
server/fleet/datastore.go, server/datastore/mysql/software_titles.go, server/mock/datastore_mock.go
Modified ListSoftwareAutoUpdateSchedules to accept a source parameter, filtering schedules by software source. Updated all call sites and mock implementations.
Service Initialization
cmd/fleet/serve.go, server/service/testing_utils.go
Updated NewMDMAppleCheckinAndCommandService constructor calls to pass VPP installer instance (extracted via type assertion) and premium license flag.
Host Software Installation Options
server/fleet/software_installer.go
Added ForScheduledUpdates field to HostSoftwareInstallOptions and updated IsFleetInitiated logic to treat scheduled updates as fleet-initiated.
Test Data & Frontend
cmd/fleetctl/fleetctl/testdata/expectedHostDetailResponseJson.json, cmd/fleetctl/fleetctl/testdata/expectedHostDetailResponseYaml.yml, cmd/fleetctl/fleetctl/testdata/expectedListHostsJson.json, cmd/fleetctl/fleetctl/testdata/expectedListHostsMDM.json, cmd/fleetctl/fleetctl/testdata/expectedListHostsYaml.yml, frontend/pages/hosts/details/cards/Vitals/Vitals.tsx, frontend/utilities/constants.tsx
Added timezone field to host API response fixtures (JSON and YAML). Extended Vitals card to conditionally render timezone for iOS/iPadOS hosts. Added "timezone" to HOST_VITALS_DATA constant.

Sequence Diagram(s)

sequenceDiagram
    participant Device as iOS/iPadOS Device
    participant Fleet as Fleet MDM Service
    participant DB as Datastore
    participant VPP as VPP Installer
    
    Device->>Fleet: Check-in (InstalledApplicationList)
    activate Fleet
    Fleet->>Device: Send DeviceInformation (with TimeZone query)
    deactivate Fleet
    
    Device->>Fleet: Device response + TimeZone + installed apps
    activate Fleet
    Fleet->>Fleet: Update host.TimeZone from response
    
    Note over Fleet: Premium check: isPremium == true?
    alt Premium License
        Fleet->>DB: Get installed software for host
        DB-->>Fleet: Installed apps list
        
        Fleet->>DB: ListSoftwareAutoUpdateSchedules(teamID, source)
        DB-->>Fleet: Auto-update schedules
        
        loop For each schedule
            Note over Fleet: Check eligibility:<br/>- Newer version available?<br/>- Host in labels?<br/>- App currently installed?
            alt Eligible for update
                Fleet->>Fleet: Check if current time<br/>in maintenance window<br/>(using timezone)
                alt Time in window
                    Fleet->>VPP: InstallVPPAppPostValidation(host, app)
                    activate VPP
                    VPP-->>Fleet: Command UUID
                    deactivate VPP
                end
            end
        end
    end
    
    Fleet->>DB: UpdateHost with new software state
    deactivate Fleet
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • jahzielv
  • MagnusHJensen

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is incomplete. While it resolves the correct issue and confirms changes file addition and data validation, critical checklist items for testing are unchecked, indicating automated tests and manual QA were not performed. Complete the testing checklist by adding/updating automated tests and confirming manual QA of the new scheduled updates functionality before merge.
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly and specifically summarizes the main change: adding scheduled updates functionality for iOS/iPadOS managed devices.
Linked Issues check ✅ Passed The pull request implements all key requirements from issue #35455: ingests TimeZone via DeviceInformation [server/mdm/apple/commander.go, server/service/apple_mdm.go], stores it in the host model [server/fleet/hosts.go, database updates], determines software eligibility [handleScheduledUpdates logic], checks maintenance windows with timezone support [isTimezoneInWindow helper], and triggers VPP app installations [vppInstaller integration].
Out of Scope Changes check ✅ Passed All changes are within scope. The PR modifies the MDM check-in flow, adds timezone support, implements scheduled update logic, and updates related database schemas and frontend displays—all directly supporting the scheduled updates feature for iOS/iPadOS devices.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 35455-schedule-updates

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
server/fleet/software_installer.go (1)

1009-1015: Clarify IsFleetInitiated comment now that ForScheduledUpdates is supported

The implementation now classifies non‑self‑service scheduled updates as fleet‑initiated in addition to policy‑triggered installs. That behavior looks correct, but the comment above IsFleetInitiated still only mentions policies; consider updating it to also call out scheduled updates so future readers don’t assume policies are the sole trigger.

Also applies to: 1018-1022

server/service/apple_mdm_test.go (1)

6253-6258: Clarify test intent and consider adding a couple more cases

This smoke test is fine, but it only exercises a single happy-path window/timezone. To make the intent clearer and broaden coverage a bit, consider:

  • Adding a short comment noting that "00:00"–"23:59" is used so the result is deterministic regardless of current time.
  • Optionally expanding to a small table-driven test that also checks:
    • A window that crosses midnight (e.g. "23:00""01:00") to exercise that branch.
    • An invalid timezone or malformed window string to assert error behavior.

This would better lock in isTimezoneInWindow’s expected semantics without much extra code.

server/fleet/datastore.go (1)

593-599: Interface change for ListSoftwareAutoUpdateSchedules looks correct

Adding the source string parameter aligns with the new per-source/per-platform scheduling tests and keeps the variadic filter semantics intact. Consider documenting (or typing) the expected source values ("ios_apps", "ipados_apps", etc.) near this method to reduce the chance of call‑site typos, but the current shape is fine.

server/datastore/mysql/software_titles_test.go (1)

2596-2791: testUpdateAutoUpdateConfig thoroughly covers per‑source schedules; minor robustness nits

The test does a good job exercising:

  • Validation of malformed time strings for UpdateSoftwareTitleAutoUpdateConfig.
  • Enabling/disabling auto‑update and verifying persisted config via SoftwareTitleByID.
  • Separation of schedules per source ("ipados_apps" vs "ios_apps") using the new ListSoftwareAutoUpdateSchedules(ctx, teamID, source, filter...) API.
  • Filtering by Enabled flag through SoftwareAutoUpdateScheduleFilter.

Two optional improvements to make the test less brittle:

  • Instead of relying on titles[0], titles[1], titles[2] to infer which title is which, derive titleID/title2ID/title3ID based on titles[i].Source (and/or Name). That will keep the test stable if the default ordering in ListSoftwareTitles ever changes.
  • Similarly, if ListSoftwareAutoUpdateSchedules doesn’t already guarantee ordering, consider asserting via a small map {TitleID: cfg} rather than positional indexes, to decouple the expectations from internal ORDER BY choices.

These are test‑robustness tweaks only; the current behavior is logically sound.

cmd/fleet/serve.go (1)

1308-1316: LGTM — VPP installer integration follows established patterns.

The type assertion and constructor call correctly wire up the VPP installer and premium flag for the MDM check-in service, enabling scheduled updates functionality. This matches the existing pattern at line 1032.

Optional: Consider defensive type assertion for better error diagnostics.

While the single-value type assertion matches the existing pattern at line 1032, using the two-value form would provide a more informative error message instead of a panic if the interface is not implemented:

🔎 Proposed defensive type assertion
-		vppInstaller := svc.(fleet.AppleMDMVPPInstaller)
+		vppInstaller, ok := svc.(fleet.AppleMDMVPPInstaller)
+		if !ok {
+			initFatal(errors.New("service does not implement AppleMDMVPPInstaller interface"), "initialize MDM checkin service")
+		}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1751673 and 568b134.

📒 Files selected for processing (22)
  • changes/35455-schedule-updates
  • cmd/fleet/serve.go
  • cmd/fleetctl/fleetctl/testdata/expectedHostDetailResponseJson.json
  • cmd/fleetctl/fleetctl/testdata/expectedHostDetailResponseYaml.yml
  • cmd/fleetctl/fleetctl/testdata/expectedListHostsJson.json
  • cmd/fleetctl/fleetctl/testdata/expectedListHostsMDM.json
  • cmd/fleetctl/fleetctl/testdata/expectedListHostsYaml.yml
  • frontend/pages/hosts/details/cards/Vitals/Vitals.tsx
  • frontend/utilities/constants.tsx
  • server/datastore/mysql/hosts.go
  • server/datastore/mysql/labels.go
  • server/datastore/mysql/software_titles.go
  • server/datastore/mysql/software_titles_test.go
  • server/fleet/apple_mdm.go
  • server/fleet/datastore.go
  • server/fleet/hosts.go
  • server/fleet/software_installer.go
  • server/mdm/apple/commander.go
  • server/mock/datastore_mock.go
  • server/service/apple_mdm.go
  • server/service/apple_mdm_test.go
  • server/service/testing_utils.go
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

⚙️ CodeRabbit configuration file

When reviewing SQL queries that are added or modified, ensure that appropriate filtering criteria are applied—especially when a query is intended to return data for a specific entity (e.g., a single host). Check for missing WHERE clauses or incorrect filtering that could lead to incorrect or non-deterministic results (e.g., returning the first row instead of the correct one). Flag any queries that may return unintended results due to lack of precise scoping.

Files:

  • server/datastore/mysql/labels.go
  • server/mdm/apple/commander.go
  • server/datastore/mysql/software_titles.go
  • server/fleet/apple_mdm.go
  • server/service/apple_mdm_test.go
  • server/fleet/hosts.go
  • server/fleet/datastore.go
  • server/service/testing_utils.go
  • cmd/fleet/serve.go
  • server/fleet/software_installer.go
  • server/datastore/mysql/hosts.go
  • server/mock/datastore_mock.go
  • server/service/apple_mdm.go
  • server/datastore/mysql/software_titles_test.go
🧠 Learnings (2)
📚 Learning: 2025-10-03T18:16:11.482Z
Learnt from: MagnusHJensen
Repo: fleetdm/fleet PR: 33805
File: server/service/integration_mdm_test.go:1248-1251
Timestamp: 2025-10-03T18:16:11.482Z
Learning: In server/service/integration_mdm_test.go, the helper createAppleMobileHostThenEnrollMDM(platform string) is exclusively for iOS/iPadOS hosts (mobile). Do not flag macOS model/behavior issues based on changes within this helper; macOS provisioning uses different helpers such as createHostThenEnrollMDM.

Applied to files:

  • server/fleet/apple_mdm.go
  • server/service/apple_mdm_test.go
  • server/service/testing_utils.go
  • cmd/fleet/serve.go
  • server/service/apple_mdm.go
📚 Learning: 2025-08-08T08:32:31.529Z
Learnt from: getvictor
Repo: fleetdm/fleet PR: 31695
File: server/datastore/mysql/apple_mdm_test.go:132-132
Timestamp: 2025-08-08T08:32:31.529Z
Learning: Datastore.NewMDMWindowsConfigProfile signature is: NewMDMWindowsConfigProfile(ctx context.Context, cp fleet.MDMWindowsConfigProfile, usesFleetVars []string) (*fleet.MDMWindowsConfigProfile, error). Passing nil for usesFleetVars in tests denotes “no Fleet variables referenced” and is used consistently across the repo.

Applied to files:

  • cmd/fleet/serve.go
  • server/service/apple_mdm.go
🧬 Code graph analysis (7)
server/datastore/mysql/software_titles.go (1)
server/fleet/software.go (2)
  • SoftwareAutoUpdateScheduleFilter (273-275)
  • SoftwareAutoUpdateSchedule (267-271)
server/fleet/datastore.go (1)
server/fleet/software.go (2)
  • SoftwareAutoUpdateScheduleFilter (273-275)
  • SoftwareAutoUpdateSchedule (267-271)
cmd/fleet/serve.go (2)
server/fleet/apple_mdm.go (1)
  • AppleMDMVPPInstaller (1110-1117)
server/service/apple_mdm.go (1)
  • NewMDMAppleCheckinAndCommandService (3340-3359)
frontend/pages/hosts/details/cards/Vitals/Vitals.tsx (1)
frontend/utilities/constants.tsx (1)
  • DEFAULT_EMPTY_CELL_VALUE (404-404)
server/mock/datastore_mock.go (1)
server/fleet/software.go (2)
  • SoftwareAutoUpdateScheduleFilter (273-275)
  • SoftwareAutoUpdateSchedule (267-271)
server/service/apple_mdm.go (1)
server/fleet/software.go (4)
  • Software (49-115)
  • Software (117-119)
  • SoftwareAutoUpdateSchedule (267-271)
  • SoftwareTitle (278-328)
server/datastore/mysql/software_titles_test.go (4)
server/fleet/vpp.go (3)
  • VPPApp (71-90)
  • VPPAppTeam (21-58)
  • VPPAppID (9-14)
server/fleet/mdm.go (1)
  • IOSPlatform (1107-1107)
server/fleet/software.go (2)
  • SoftwareAutoUpdateConfig (255-265)
  • SoftwareAutoUpdateScheduleFilter (273-275)
server/ptr/ptr.go (2)
  • String (10-12)
  • Bool (25-27)
🔇 Additional comments (18)
server/mdm/apple/commander.go (1)

386-404: Adding TimeZone to DeviceInformation queries looks good

Including TimeZone in the DeviceInformation Queries array cleanly supports populating the host timezone without impacting existing fields or the command structure.

server/datastore/mysql/hosts.go (1)

692-745: Timezone wiring through host read/write paths is consistent

Selecting h.timezone in Host, ListHosts, LoadHostByNodeKey, and HostByIdentifier, and persisting it via the new timezone = ? column in UpdateHost, keeps the new field available across the main host read/write flows. The SQL still scopes by host id (no broader WHERE changes), so this is a straightforward schema extension rather than a behavioral change in host selection.

Also applies to: 986-1037, 2663-2707, 3198-3241, 5275-5361

cmd/fleetctl/fleetctl/testdata/expectedHostDetailResponseYaml.yml (1)

102-102: Host detail YAML correctly includes timezone

The added timezone: null field aligns with the extended host schema and keeps the expected detail output in sync with backend changes.

cmd/fleetctl/fleetctl/testdata/expectedListHostsYaml.yml (1)

68-68: List-hosts YAML fixtures updated to expose timezone

Both host specs now include timezone: null, matching the new host field while preserving the existing structure and ordering of the YAML.

Also applies to: 127-127

changes/35455-schedule-updates (1)

1-1: Changelog entry matches the implemented feature

The note succinctly captures the new iOS/iPadOS scheduled updates functionality and fits the style of other changes/ entries.

server/datastore/mysql/labels.go (1)

759-759: LGTM!

The addition of h.timezone to the SELECT statement properly extends the host data returned by ListHostsInLabel. The query maintains appropriate filtering by label_id and team.

cmd/fleetctl/fleetctl/testdata/expectedHostDetailResponseJson.json (1)

63-63: LGTM!

The test data correctly reflects the new timezone field added to the host schema.

cmd/fleetctl/fleetctl/testdata/expectedListHostsMDM.json (1)

60-60: LGTM!

The test data correctly reflects the new timezone field for both host entries.

Also applies to: 139-139

frontend/pages/hosts/details/cards/Vitals/Vitals.tsx (2)

243-257: LGTM!

The renderTimezone function properly gates the timezone display to iOS/iPadOS hosts and checks for data presence before rendering.


526-526: LGTM!

The timezone rendering is correctly integrated into the Vitals card layout.

cmd/fleetctl/fleetctl/testdata/expectedListHostsJson.json (1)

59-59: LGTM!

The test data correctly reflects the new timezone field for both host entries.

Also applies to: 138-138

server/fleet/apple_mdm.go (1)

1115-1115: LGTM!

The comment clarifies the return value of the InstallVPPAppPostValidation method, improving the interface documentation.

frontend/utilities/constants.tsx (1)

444-444: LGTM!

The timezone field is correctly added to the HOST_VITALS_DATA constant, ensuring it's properly tracked as part of host vitals.

server/datastore/mysql/software_titles_test.go (1)

29-50: New UpdateAutoUpdateConfig test is properly wired into the main suite

Including {"UpdateAutoUpdateConfig", testUpdateAutoUpdateConfig} in the cases slice ensures the new auto‑update behavior is exercised alongside existing software title tests. No issues here.

server/datastore/mysql/software_titles.go (1)

866-879: LGTM - Query correctly scoped by team and source.

The changes properly filter schedules by both team_id and source using parameterized placeholders. The INNER JOIN with software_titles ensures only schedules for valid titles matching the specified source (e.g., ios_apps, ipados_apps) are returned, which aligns with the per-platform auto-update scheduling requirements.

server/service/testing_utils.go (1)

477-478: LGTM - Test utility updated for VPP installer dependency.

The type assertion and updated constructor call correctly wire the VPP installer and premium flag for MDM command processing in tests. While the unchecked type assertion could panic, this is acceptable in test code since it would immediately surface any interface mismatch during test execution.

server/mock/datastore_mock.go (2)

458-458: Signature update for ListSoftwareAutoUpdateSchedulesFunc looks correct

Adding the source string parameter here matches the evolved datastore interface and will let tests exercise source-specific auto-update behavior via the mock.


5762-5766: Mock method correctly forwards new source argument

The DataStore.ListSoftwareAutoUpdateSchedules method now takes source and passes it through to ListSoftwareAutoUpdateSchedulesFunc while preserving the existing lock/invocation-flag pattern, which is consistent with the rest of this mock.

Copy link
Contributor

@sgress454 sgress454 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! It'd be nice if we could just use InstallSoftwareTitle(). It'd need to be adjusted to accept the new ForScheduledUpdates install option, and I guess it'd do a lot of redundant work, but in the long run we probably don't want to be re-implementing the logic for "check if VPP app is right for host and if so, install it" multiple times. It's not something I'd touch in this pass at any rate.

}

func (ds *Datastore) ListSoftwareAutoUpdateSchedules(ctx context.Context, teamID uint, optionalFilter ...fleet.SoftwareAutoUpdateScheduleFilter) ([]fleet.SoftwareAutoUpdateSchedule, error) {
func (ds *Datastore) ListSoftwareAutoUpdateSchedules(ctx context.Context, teamID uint, source string, optionalFilter ...fleet.SoftwareAutoUpdateScheduleFilter) ([]fleet.SoftwareAutoUpdateSchedule, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also make source a filter in SoftwareAutoUpdateScheduleFilter. The idea there is that without any filters, you can list all the schedules for a team. Making it optional is a little more work though (we would need a token for the JOIN clause) so nbd either way.

Comment on lines +4094 to +4099
// 3. Filter out software that already has a pending installation (update).
adamIDsPendingInstallForHost, err := svc.ds.MapAdamIDsPendingInstall(ctx, host.ID)
if err != nil {
return ctxerr.Wrap(ctx, err, "get Adam IDs pending install for host")
}
var softwaresWithinUpdateScheduleToInstall []*fleet.SoftwareTitle
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely out of scope for this story, but seems like InstallVPPAppPostValidation should either be idempotent by default or take something like ignorePending as an option, so that callers don't have to do this check themselves.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS/iPad VPP auto-updates: push app scheduled app update to device

3 participants