Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
124c6bd
plan-alm: convert to strict plan-only planner (execution skills self-…
tyaginidhi Jun 17, 2026
4d50c9e
Address Copilot review on #191
tyaginidhi Jun 17, 2026
2fd9aa6
Self-review fixes: PP-path activate step, manual steps[], EDM error rule
tyaginidhi Jun 17, 2026
298fcf0
Address Copilot second-pass review on #191
tyaginidhi Jun 17, 2026
9f66e35
Fix step-sync stage matching: marker "Deploy to {label}" vs plan step…
tyaginidhi Jun 17, 2026
16f3d32
Address Copilot third-pass: stable reconcile() nextStep contract
tyaginidhi Jun 17, 2026
be368a6
Harden stage matching: shared normalizeStageLabel for keys too
tyaginidhi Jun 17, 2026
776356d
Potential fix for pull request finding
tyaginidhi Jun 17, 2026
e59db35
Address Copilot fourth-pass on #191
tyaginidhi Jun 17, 2026
ed2ad59
EDM (enhanced/standard data-model) Power Pages site support in ALM di…
tyaginidhi Jun 17, 2026
c5db654
Site-referenced table discovery + dependency-aware solution splitting
tyaginidhi Jun 17, 2026
56b996f
Fix FFD bin-packing under-allocation that overflowed the schema-attr cap
tyaginidhi Jun 17, 2026
5e3d1ab
Wire --projectRoot into the remaining discover-site-components consumers
tyaginidhi Jun 17, 2026
696d41c
Enforce ALM plan refresh via PostToolUse reconcile backstop (auto-heal)
tyaginidhi Jun 17, 2026
e74e4b7
Surface reconcile failures in the hook + document failed[] contract
tyaginidhi Jun 17, 2026
a5e20e7
Fix stale "plan-alm Phases 6/7/8" reference in AGENTS.md
tyaginidhi Jun 17, 2026
dfb672a
Activate the PLAN_STATUS lifecycle: Approved -> In Execution -> Compl…
tyaginidhi Jun 17, 2026
3db879d
Surface COMPLETED_AT in the ALM plan footer
tyaginidhi Jun 17, 2026
f5ca2bb
Site-referenced table discovery + dependency-aware solution splitting
tyaginidhi Jun 17, 2026
be4a70d
Fix FFD bin-packing under-allocation that overflowed the schema-attr cap
tyaginidhi Jun 17, 2026
e55e06d
Wire --projectRoot into the remaining discover-site-components consumers
tyaginidhi Jun 17, 2026
bb6f7f7
Address #193 review: attr-cap overflow warning + componentCount proxy…
tyaginidhi Jun 18, 2026
6e8b081
Resolve merge conflicts with origin/users/nityagi/table-discovery-fix
Copilot Jun 18, 2026
91be9fd
Resolve merge conflicts with origin/users/nityagi/table-discovery-fix…
Copilot Jun 18, 2026
525405a
Merge main into branch: resolve conflicts after stacked PRs landed
Copilot Jun 19, 2026
673ae86
Fix incomplete Open Plugins resolution in the merge (#194)
tyaginidhi Jun 19, 2026
1f5ddd2
Address #194 review: DRY plan path + hook smoke test + completion-edg…
tyaginidhi Jun 22, 2026
f27128f
Address #194 Copilot review comments (3)
tyaginidhi Jun 22, 2026
af087e7
plan-alm: move PLAN_STATUS badge next to the "Generated" line
tyaginidhi Jun 22, 2026
dfb5023
Address #194 review: don't let Approved->In Execution promotion mask …
tyaginidhi Jun 22, 2026
c5bb5a0
Merge remote-tracking branch 'origin/main' into users/nityagi/plan-re…
tyaginidhi Jun 22, 2026
b018fa7
Sync legacy JSON mirrors to power-pages v2.5.0 after merging #203
tyaginidhi Jun 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"source": "./plugins/power-pages",
"description": "Power Pages development and management plugin for Claude Code and GitHub Copilot",
"category": "development",
"version": "2.4.0",
"version": "2.5.0",
"license": "MIT",
"tags": [
"power platform",
Expand Down
2 changes: 1 addition & 1 deletion marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"source": "./plugins/power-pages",
"description": "Power Pages development and management plugin for Claude Code and GitHub Copilot",
"category": "development",
"version": "2.4.0",
"version": "2.5.0",
"license": "MIT",
"tags": [
"power platform",
Expand Down
2 changes: 1 addition & 1 deletion plugins/power-pages/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "power-pages",
"version": "2.4.0",
"version": "2.5.0",
"description": "Create and deploy Power Pages sites using modern development approaches. Supports code sites (SPAs) with React, Angular, Vue, or Astro. Includes ALM orchestration (plan-alm) with a solution-splitting decision tree, per-solution pipelines, Azure Blob asset advisory, manifest schema v2 for multi-solution deployments, and force-link remediation for cross-host pipeline migrations.",
"author": {
"name": "Microsoft",
Expand Down
2 changes: 1 addition & 1 deletion plugins/power-pages/.plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "power-pages",
"version": "2.4.0",
"version": "2.5.0",
"description": "Create and deploy Power Pages sites using modern development approaches. Supports code sites (SPAs) with React, Angular, Vue, or Astro. Includes ALM orchestration (plan-alm) with a solution-splitting decision tree, per-solution pipelines, Azure Blob asset advisory, manifest schema v2 for multi-solution deployments, and force-link remediation for cross-host pipeline migrations.",
"author": {
"name": "Microsoft",
Expand Down
6 changes: 4 additions & 2 deletions plugins/power-pages/AGENTS.md

Large diffs are not rendered by default.

68 changes: 67 additions & 1 deletion plugins/power-pages/hooks/run-skill-posttool-validation.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
#!/usr/bin/env node

const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const {
getTrackedSkillFromToolInput,
getValidatorScript,
isAlmPlanSkill,
} = require('../scripts/lib/powerpages-hook-utils');
const { planDataPath } = require('../scripts/lib/alm-paths');

const DEBUG = process.env.DEBUG === '1' || process.env.DEBUG === 'true';

Expand Down Expand Up @@ -36,19 +39,82 @@ process.stdin.on('end', () => {
process.exit(0);
}

const cwd = input.cwd || process.cwd();

const validatorScript = getValidatorScript(skillName);
if (validatorScript) {
const validatorPath = path.join(__dirname, '..', validatorScript);
const result = spawnSync(process.execPath, [validatorPath], {
input: inputData,
encoding: 'utf8',
cwd: input.cwd || process.cwd(),
cwd,
});
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
validatorStatus = result.status ?? 0;
debug(`[power-pages hook] Validator exited with code ${validatorStatus}\n`);
}

// ALM plan reconcile backstop (auto-heal). The refresh-alm-plan-data.js calls
// in each SKILL.md are advisory — silently dropped on session fragmentation,
// manual execution, or oversight. After ANY ALM plan skill completes, reconcile
// the plan against the marker files: any marker newer than the plan (a skipped
// refresh) is ingested automatically. Best-effort and NON-blocking — it never
// changes the hook's exit code (the validator's status stands). Triggering on
// any ALM skill (not just the marker's writer) catches a skip that surfaces only
// when the NEXT ALM skill runs. Honors .alm-deferred + no-plan inside reconcile.
if (isAlmPlanSkill(skillName) && fs.existsSync(planDataPath(cwd))) {
try {
const refreshPath = path.join(__dirname, '..', 'scripts', 'lib', 'refresh-alm-plan-data.js');
const rec = spawnSync(process.execPath, [refreshPath, '--projectRoot', cwd, '--reconcile', '--render'], {
encoding: 'utf8',
cwd,
timeout: 20000,
});
// spawnSync surfaces a spawn/timeout failure on rec.error (e.g. ETIMEDOUT)
// and a non-zero / signalled exit on rec.status / rec.signal — none of which
// produce parseable stdout. Track those so a broken reconcile is reported
// rather than silently swallowed by the JSON.parse catch below.
const spawnFailed = !!rec.error || rec.status !== 0 || !!rec.signal;
let reconciled = [];
let failed = [];
let parsed = false;
try {
const out = JSON.parse((rec.stdout || '').trim());
reconciled = out.reconciled || [];
failed = out.failed || [];
parsed = true;
} catch { /* parsed stays false — surfaced in the spawnFailed/!parsed branch */ }
if (reconciled.length > 0) {
process.stdout.write(
`[power-pages] ALM plan was out of sync with ${reconciled.length} run marker(s) — refreshed automatically (${reconciled.join(', ')}).\n`,
);
}
// Failure reporting goes to STDERR (only on an actual failure, never on the
// happy path — the hook fires on every Skill use, so clean runs must stay
// quiet). A swallowed reconcile failure is exactly what makes a stale plan
// impossible to diagnose, so we forward the child's stderr verbatim — that's
// where refresh-alm-plan-data.js already writes its per-phase error detail,
// which is what makes the summary line below actionable.
if (failed.length > 0) {
process.stderr.write(
`[power-pages] ALM plan reconcile could not heal ${failed.length} phase(s): ${failed.map((f) => f.phase).join(', ')}. Details below.\n`,
);
if (rec.stderr) process.stderr.write(rec.stderr);
} else if (spawnFailed || !parsed) {
// The reconcile didn't even produce a parseable result (spawn error,
// timeout, non-zero exit, or garbled stdout). Non-blocking, but the user
// should still see why the auto-heal didn't run.
const why = rec.error ? rec.error.message : rec.signal ? `signal ${rec.signal}` : `exit ${rec.status}`;
process.stderr.write(`[power-pages] ALM plan reconcile did not complete (${why}). Details below.\n`);
if (rec.stderr) process.stderr.write(rec.stderr);
}
debug(`[power-pages hook] reconcile reconciled=${JSON.stringify(reconciled)} failed=${JSON.stringify(failed)} spawnFailed=${spawnFailed} parsed=${parsed}\n`);
} catch (e) {
// Best-effort — a reconcile failure must never break the skill or the hook.
debug(`[power-pages hook] reconcile error (ignored): ${e.message}\n`);
}
}
} catch (err) {
process.stderr.write(`[power-pages hook] Unexpected error: ${err.message}\n`);
validatorStatus = 0;
Expand Down
21 changes: 21 additions & 0 deletions plugins/power-pages/scripts/lib/alm-paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,31 @@ function ensureAlmDir(projectRoot) {
return dir;
}

// The rendered plan + its backing JSON live at the `docs/` ROOT (NOT under
// `docs/alm/`): `docs/alm-plan.html` and `docs/.alm-plan-data.json`. Centralized
// here so the PostToolUse hook, check-alm-plan.js, and refresh-alm-plan-data.js
// can't drift on the path.
const PLAN_DATA_FILE = '.alm-plan-data.json';
const PLAN_HTML_FILE = 'alm-plan.html';

function planDataPath(projectRoot) {
if (!projectRoot) throw new Error('planDataPath: projectRoot is required');
return path.join(projectRoot, 'docs', PLAN_DATA_FILE);
}

function planHtmlPath(projectRoot) {
if (!projectRoot) throw new Error('planHtmlPath: projectRoot is required');
return path.join(projectRoot, 'docs', PLAN_HTML_FILE);
}

module.exports = {
ALM_DIR,
FILE_NAMES,
PLAN_DATA_FILE,
PLAN_HTML_FILE,
almDir,
almPath,
ensureAlmDir,
planDataPath,
planHtmlPath,
};
41 changes: 37 additions & 4 deletions plugins/power-pages/scripts/lib/check-alm-plan.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
const fs = require('fs');
const path = require('path');
const helpers = require('./validation-helpers');
const { planDataPath, planHtmlPath } = require('./alm-paths');

// Heartbeat window — how recent `lastInvocationAt` must be for the plan to count
// as actively executing. 60 minutes is comfortably larger than the longest single
Expand Down Expand Up @@ -185,8 +186,8 @@ function readDeferralLocal(projectRoot) {

async function checkAlmPlan({ projectRoot, envUrl, token, solutionId, makeRequest, writeHeartbeat = true, now }) {
if (!projectRoot) throw new Error('--projectRoot is required');
const planPath = path.join(projectRoot, 'docs', '.alm-plan-data.json');
const htmlPath = path.join(projectRoot, 'docs', 'alm-plan.html');
const planPath = planDataPath(projectRoot);
const htmlPath = planHtmlPath(projectRoot);
const nowMs = (typeof now === 'number') ? now : Date.now();

// Deferral marker check — runs first, regardless of plan presence.
Expand Down Expand Up @@ -233,8 +234,40 @@ async function checkAlmPlan({ projectRoot, envUrl, token, solutionId, makeReques
// way each in-chain skill's Phase 0 call both observes the prior heartbeat
// (for its own decision) AND keeps the chain alive for the next skill.
const priorLastInvocationAt = planData.LAST_INVOCATION_AT || null;
const planStatus = planData.PLAN_STATUS || null;
const inExecution = computeInExecution(planStatus, priorLastInvocationAt, nowMs);
const priorPlanStatus = planData.PLAN_STATUS || null;

// Classify `inExecution` from the PRIOR on-disk status, BEFORE any promotion.
// An Approved plan is NOT yet an active orchestration — the user has just
// approved it and this is the first execution skill to reach its Phase 0 gate.
// If we promoted to "In Execution" first and then classified, computeInExecution
// would return status:"active" (the "first heartbeat" branch) — and callers that
// skip Phase 0 gates whenever inExecution.status === "active" (see deploy-pipeline
// SKILL.md Phase 0) would take the active shortcut and bypass the stale-plan
// confirmation that this helper computes further below. That would mask stale:true
// on the FIRST execution after approval — defeating the ALM freshness gate exactly
// when the source solution was modified between plan generation and approval. So
// the first Approved->In Execution entry reports inExecution:"not-running", keeping
// THIS caller on the normal Phase 0 path where it still honors stale:true. The
// promotion is persisted below, so the NEXT in-chain skill reads "In Execution"
// and correctly classifies as "active".
const inExecution = computeInExecution(priorPlanStatus, priorLastInvocationAt, nowMs);

// Promote Approved -> In Execution on the FIRST execution-skill Phase 0 entry and
// persist it (+ the first heartbeat) so the NEXT in-chain skill sees an active
// orchestration. plan-alm is plan-only: it leaves the plan "Approved" and the user
// runs the execution skills themselves, so the first execution skill to reach its
// Phase 0 gate is what actually starts execution — this is the transition that
// activates the heartbeat/active-chain machinery (nothing else sets it). The
// heartbeat write below then persists both the new status AND the first heartbeat
// in one atomic write. This promotion intentionally does NOT change `inExecution`
// for THIS caller (classified above from the prior status). Gated on `writeHeartbeat`
// so read-only callers (--no-heartbeat: audits, tests, and plan-alm's own deferral
// check) never mutate the plan's status.
let planStatus = priorPlanStatus;
if (writeHeartbeat && planStatus === 'Approved') {
planStatus = 'In Execution';
planData.PLAN_STATUS = 'In Execution';
}

if (writeHeartbeat && planStatus === 'In Execution') {
try {
Expand Down
33 changes: 33 additions & 0 deletions plugins/power-pages/scripts/lib/powerpages-hook-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,42 @@ function getValidatorScript(skillName) {
return TRACKED_SKILLS[skillName]?.validatorScript ?? null;
}

// Skills that write a `docs/alm/last-*.json` marker or otherwise consume the ALM
// plan. After any of these completes, the PostToolUse hook runs a plan reconcile
// (auto-heal) — so a refresh step skipped by ONE skill is caught when the NEXT
// ALM skill completes (covers manual/cross-session execution).
const ALM_PLAN_SKILLS = new Set([
'setup-solution',
'setup-pipeline',
'deploy-pipeline',
'export-solution',
'import-solution',
'configure-env-variables',
'activate-site',
'test-site',
'ensure-pipelines-host',
'force-link-environment',
]);

/**
* True when `value` (a raw skill name, `/skill`, or `power-pages:skill`) resolves
* to an ALM plan skill. Normalizes via `detectTrackedSkill`, so it also confirms
* the skill actually exists in this plugin.
* Accepts any value — non-strings (including null/undefined) resolve to false
* via detectTrackedSkill, so callers may pass an unvalidated skill name.
* @param {*} value
* @returns {boolean}
*/
function isAlmPlanSkill(value) {
const name = detectTrackedSkill(value);
return name != null && ALM_PLAN_SKILLS.has(name);
}

module.exports = {
TRACKED_SKILLS,
ALM_PLAN_SKILLS,
detectTrackedSkill,
getTrackedSkillFromToolInput,
getValidatorScript,
isAlmPlanSkill,
};
46 changes: 41 additions & 5 deletions plugins/power-pages/scripts/lib/refresh-alm-plan-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@
// finalize:
// - PLAN_STATUS = "Completed"
//
// After every phase's step-sync, a completion evaluator flips PLAN_STATUS to
// "Completed" (+ COMPLETED_AT) once all non-skip steps are completed and none
// failed — so the last execution skill terminates the plan automatically,
// without any skill needing to call the explicit "finalize" phase. The
// Approved -> In Execution promotion that starts the lifecycle lives in
// check-alm-plan.js (first execution-skill Phase 0).
//
// stdout JSON includes `nextStep: { name, skill: string | null } | null` (when ok:true) — the first
// still-pending checklist step and the slash command that runs it. Execution
// skills echo this so the user knows the next step to invoke (user-driven
Expand All @@ -49,7 +56,7 @@
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const { almPath } = require('./alm-paths');
const { almPath, planDataPath, planHtmlPath } = require('./alm-paths');

const PHASES = new Set([
'setup-solution',
Expand Down Expand Up @@ -527,6 +534,33 @@ function refreshFinalize(planData) {
return planData;
}

// Completion evaluator. Once every non-skipped checklist step is `completed`
// (and none `failed`), the plan has been fully executed — transition it to
// "Completed" + stamp COMPLETED_AT. Runs after each phase's step-sync (in both
// refresh() and reconcile()), so the LAST execution skill to finish flips the
// plan terminal automatically — no skill has to call `--phase finalize`
// explicitly (nothing did, so the lifecycle previously never completed). Only
// advances from a pre-terminal, non-draft state: "In Execution" (the normal
// case after check-alm-plan.js promoted it on the first execution skill) or
// "Approved" (defensive fallback if that promotion didn't run). Never regresses
// a Draft or an already-Completed plan, and a `failed` step blocks completion
// (e.g. a failed deploy must not look "done" just because later steps are
// pending-skipped).
function evaluatePlanCompletion(planData) {
if (!planData) return;
const status = planData.PLAN_STATUS;
if (status !== 'In Execution' && status !== 'Approved') return;
if (!Array.isArray(planData.steps)) return;
const live = planData.steps.filter((s) => s && s.skip !== true && typeof s.name === 'string');
if (live.length === 0) return;
const anyFailed = live.some((s) => s.status === 'failed');
const allCompleted = live.every((s) => s.status === 'completed');
if (!anyFailed && allCompleted) {
planData.PLAN_STATUS = 'Completed';
planData.COMPLETED_AT = new Date().toISOString();
}
}

// Refresh-phase helper: stamp `LAST_SYNC_AT` on planData so check-alm-plan.js's
// freshness check correctly accounts for source-solution modifications caused
// by the just-completed phase. Called by every phase that touches the source
Expand Down Expand Up @@ -1015,8 +1049,8 @@ function mtimeMs(filePath) {
// marker schema the refresh can't parse); the reconcile still heals the other phases.
function reconcile({ projectRoot, render, rendererPath }) {
if (!projectRoot) throw new Error('--projectRoot is required');
const dataPath = path.join(projectRoot, 'docs', '.alm-plan-data.json');
const htmlPath = path.join(projectRoot, 'docs', 'alm-plan.html');
const dataPath = planDataPath(projectRoot);
const htmlPath = planHtmlPath(projectRoot);

// Respect the project-level ALM opt-out. nextStep is null on these early paths
// (there's nothing to guide toward), kept on the return for a stable contract so
Expand Down Expand Up @@ -1085,6 +1119,7 @@ function reconcile({ projectRoot, render, rendererPath }) {
process.stderr.write(`[refresh-alm-plan-data] reconcile phase "${phase}" failed: ${e.message}\n`);
}
}
evaluatePlanCompletion(planData);
fs.writeFileSync(dataPath, JSON.stringify(planData, null, 2), 'utf8');

let rendered = false;
Expand Down Expand Up @@ -1116,8 +1151,8 @@ function refresh({ projectRoot, phase, render, rendererPath, stageName }) {
throw new Error('--phase must be one of: ' + [...PHASES].join(', '));
}

const dataPath = path.join(projectRoot, 'docs', '.alm-plan-data.json');
const htmlPath = path.join(projectRoot, 'docs', 'alm-plan.html');
const dataPath = planDataPath(projectRoot);
const htmlPath = planHtmlPath(projectRoot);

if (!fs.existsSync(dataPath)) {
return {
Expand All @@ -1135,6 +1170,7 @@ function refresh({ projectRoot, phase, render, rendererPath, stageName }) {
}

applyRefresh(planData, phase, projectRoot, stageName);
evaluatePlanCompletion(planData);
fs.writeFileSync(dataPath, JSON.stringify(planData, null, 2), 'utf8');

let rendered = false;
Expand Down
19 changes: 18 additions & 1 deletion plugins/power-pages/scripts/tests/alm-paths.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const fs = require('fs');
const path = require('path');
const os = require('os');

const { almDir, almPath, ensureAlmDir, FILE_NAMES, ALM_DIR } = require('../lib/alm-paths');
const { almDir, almPath, ensureAlmDir, FILE_NAMES, ALM_DIR, planDataPath, planHtmlPath } = require('../lib/alm-paths');

function makeTmp(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'alm-paths-'));
Expand Down Expand Up @@ -73,6 +73,23 @@ test('ensureAlmDir is idempotent when docs/alm/ already exists', (t) => {
assert.equal(fs.readFileSync(path.join(root, 'docs', 'alm', 'sentinel.txt'), 'utf8'), 'pre-existing');
});

test('planDataPath / planHtmlPath resolve to the docs/ ROOT, not docs/alm/', () => {
const root = path.join(path.sep, 'tmp', 'project');
// The rendered plan + backing JSON intentionally live at docs/ (NOT docs/alm/).
assert.equal(planDataPath(root), path.join(root, 'docs', '.alm-plan-data.json'));
assert.equal(planHtmlPath(root), path.join(root, 'docs', 'alm-plan.html'));
// Guard the dotfile + non-alm-subdir invariant the helpers exist to centralize.
assert.ok(planDataPath(root).endsWith(path.join('docs', '.alm-plan-data.json')));
assert.ok(!planDataPath(root).includes(path.join('docs', 'alm', '')), 'plan data must NOT be under docs/alm/');
});

test('planDataPath / planHtmlPath throw when projectRoot is missing', () => {
assert.throws(() => planDataPath(undefined), /projectRoot is required/);
assert.throws(() => planDataPath(''), /projectRoot is required/);
assert.throws(() => planHtmlPath(undefined), /projectRoot is required/);
assert.throws(() => planHtmlPath(''), /projectRoot is required/);
});

test('every FILE_NAMES entry resolves via almPath without error', () => {
const root = path.join(path.sep, 'tmp', 'project');
for (const key of Object.keys(FILE_NAMES)) {
Expand Down
Loading
Loading