$npx -y skills add microsoft/power-platform-skills --skill preview-offline-scopeUse when the user wants to estimate the download size + sync frequency cost of an offline profile BEFORE pushing to users. Read-only. Wraps verify-offline-profile.js with per-table row-count estimates.
| 1 | **Shared instructions: [shared-instructions.md](${CLAUDE_SKILL_DIR}/../../shared/shared-instructions.md)** — read first. |
| 2 | |
| 3 | # Preview Offline Scope |
| 4 | |
| 5 | Read-only diagnostic. Tells you "if you pushed this profile to users right now, here's what their devices would download and how often it would re-sync." No mutations. |
| 6 | |
| 7 | Useful before: |
| 8 | - `/setup-offline-profile` for a final sanity check |
| 9 | - `/assign-offline-profile` (so users don't get surprised by data caps) |
| 10 | - `/edit-offline-profile` to gauge impact of a column-list change |
| 11 | |
| 12 | ## Workflow |
| 13 | |
| 14 | 1. Verify project + locate profile → 2. Run verify (drift check) → 3. Per-table row counts → 4. Cache-size estimate → 5. Report |
| 15 | |
| 16 | --- |
| 17 | |
| 18 | ### Step 1 — Verify project + locate profile |
| 19 | |
| 20 | Same as `/edit-offline-profile` Step 1. Read profileId from `offline-profile.json` or `$ARGUMENTS --profile-id`. Do not read profile metadata from `power.config.json`; it is owned by `npx power-apps init`. |
| 21 | |
| 22 | ```bash |
| 23 | test -f power.config.json |
| 24 | node "${CLAUDE_SKILL_DIR}/../../scripts/resolve-environment.js" "$(node -e \"console.log(require('./power.config.json').environmentId)\")" |
| 25 | ``` |
| 26 | |
| 27 | ### Step 2 — Run verify |
| 28 | |
| 29 | ```bash |
| 30 | node "${CLAUDE_SKILL_DIR}/../../scripts/verify-offline-profile.js" <envUrl> \ |
| 31 | --project-root "$(pwd)" |
| 32 | ``` |
| 33 | |
| 34 | If `status: drift`, surface the drift list verbatim. The estimate that follows is still meaningful but flag that the live profile diverges from `offline-profile.json` — recommend re-running `/setup-offline-profile` or `/edit-offline-profile` to reconcile. |
| 35 | |
| 36 | ### Step 3 — Per-table row counts (with scope-applied filter) |
| 37 | |
| 38 | For each table in the profile, run a `count` query that applies the same filter the runtime would use: |
| 39 | |
| 40 | | `recorddistributioncriteria` | `recordsownedby*` flag | Effective filter | |
| 41 | |---|---|---| |
| 42 | | 1 (All records) | n/a | `?$count=true&$top=0` (whole table) | |
| 43 | | 2 + me | recordsownedbyme=true | `?$count=true&$top=0&$filter=_ownerid_value eq <current-user-id>` | |
| 44 | | 2 + team | recordsownedbymyteam=true | Approx: `_ownerid_value` in (current user's team-owned IDs); for estimate, use team count from `teamroles_association` | |
| 45 | | 2 + bu | recordsownedbymybusinessunit=true | `?$count=true&$top=0&$filter=_owningbusinessunit_value eq <current-bu-id>` | |
| 46 | | 0 (Related only) | n/a | Can't estimate independently — depends on parents. Report `~depends on parent counts`. | |
| 47 | |
| 48 | For current-user/current-BU filters, resolve identity only inside this skill by calling Dataverse `WhoAmI` through `scripts/dataverse-request.js` against the resolved `<envUrl>`. If that call fails, report the affected scope as `unknown — current user/BU unavailable` instead of blocking the whole preview. Do not expect `scripts/resolve-environment.js` or `auth.config.json.environment` to provide `UserId` / `BusinessUnitId`. |
| 49 | |
| 50 | For each table: |
| 51 | |
| 52 | ```bash |
| 53 | node "${CLAUDE_SKILL_DIR}/../../scripts/dataverse-request.js" <envUrl> GET \ |
| 54 | "<entitysetname>?\$count=true&\$top=0&<scope-filter>" |
| 55 | ``` |
| 56 | |
| 57 | Cap at 5000 (Dataverse non-aggregate count cap). When the result is exactly 5000, prefix with `≥` in the report. |
| 58 | |
| 59 | ### Step 4 — Cache-size estimate |
| 60 | |
| 61 | Rough byte-per-row heuristics (configurable; replace with measured values once we have a real sync benchmark): |
| 62 | |
| 63 | | Column type | Bytes | |
| 64 | |---|---| |
| 65 | | Uniqueidentifier | 40 | |
| 66 | | String (avg 80 chars) | 100 | |
| 67 | | Integer / Decimal | 12 | |
| 68 | | DateTime | 28 | |
| 69 | | Boolean / State / Status | 4 | |
| 70 | | Picklist (option value only) | 8 | |
| 71 | | Memo (avg 500 chars) | 600 | |
| 72 | | Lookup (FK only) | 40 | |
| 73 | | Image (URL + thumbnail metadata only — see note below for full-image bytes) | 200 | |
| 74 | | File (URL + name metadata) | 200 | |
| 75 | |
| 76 | For each table, compute: `estimatedBytesPerRow = sum(bytesForEach column in selectedcolumns)`. Then `tableTotalBytes = rows × estimatedBytesPerRow`. |
| 77 | |
| 78 | Per-table sync overhead: each `syncintervalinminutes` interval triggers a delta query → assume ~20% of total rows touched on a typical day. `dailyTransferBytes = (totalBytes × 0.20 × intervalsPerDay)`. |
| 79 | |
| 80 | ### Step 5 — Report |
| 81 | |
| 82 | ``` |
| 83 | ═════════════════════════════════════════════════════════════ |
| 84 | Offline Scope Preview — <profileName> |
| 85 | ═════════════════════════════════════════════════════════════ |
| 86 | |
| 87 | Drift status: ok | drift (see verify output) |
| 88 | |
| 89 | Per-table breakdown: |
| 90 | |
| 91 | | Table | Scope | Rows | Cols | Est. bytes | Sync (min) | |
| 92 | |--------------------|----------------|----------|------|------------|------------| |
| 93 | | chnl_region | All records | 12 | 7 | 4 KB | 60 | |
| 94 | | chnl_product | All records | 312 | 13 | 280 KB | 60 | |
| 95 | | chnl_rmprofile | Org+me | 1 | 10 | 1 KB | 10 | |
| 96 | | chnl_store | Org+me | 84 | 17 | 112 KB | |