diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index b280998..b9a75b7 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -41,7 +41,7 @@ { "name": "azure-storage", "description": "Azure Storage canvas — browse blobs and containers, bulk transfer with live progress, filter with aggregate statistics, mint and open SAS links, preview blobs, and plan account to account copies, all agent-drivable on one live surface.", - "version": "0.3.0", + "version": "0.3.2", "source": "canvases/azure-storage" }, { diff --git a/canvases/azure-storage/.github/plugin/plugin.json b/canvases/azure-storage/.github/plugin/plugin.json index ec4b210..3c3e2ed 100644 --- a/canvases/azure-storage/.github/plugin/plugin.json +++ b/canvases/azure-storage/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "azure-storage", - "version": "0.3.0", + "version": "0.3.2", "description": "Azure Storage canvas — browse blobs and containers, bulk transfer with live progress, filter with aggregate statistics, mint and open SAS links, preview blobs, and plan account to account copies, all agent-drivable on one live surface.", "extensions": { "com.github.copilot": { diff --git a/canvases/azure-storage/README.md b/canvases/azure-storage/README.md index a3abe1d..888a993 100644 --- a/canvases/azure-storage/README.md +++ b/canvases/azure-storage/README.md @@ -1,59 +1,59 @@ -# Azure Storage canvas - -Browse Azure Storage and operate it on one live surface that you and Copilot share. - -## Install - -In GitHub Copilot, open **Customize → Plugins → marketplace gear**, add -`microsoft/azure-dev-tools` (marketplace ID `azure-dev-tools`), and install -`azure-storage` when listed. The full plugin includes the canvas and -its opening skill. - -For canvas-only installation, add the [Azure Storage canvas URL](https://github.com/microsoft/azure-dev-tools/tree/azure-storage-latest/canvases/azure-storage/com.github.copilot/extensions/azure-storage) -under **Customize → Canvases → +**. This does not install the opening skill; -open **Azure Storage** from your installed canvases. - -The [latest customer README](https://github.com/microsoft/azure-dev-tools/blob/azure-storage-latest/canvases/azure-storage/README.md) -contains the same setup instructions. Use these paths only when published; -if neither is available, report that rather than substituting a source folder. - -## Quickstart - -1. With the full plugin installed, ask Copilot: **"Open Azure Storage canvas."** - For a canvas-only installation, open **Azure Storage** from installed canvases. -2. Click **Select subscriptions** in **Storage Explorer**, choose the - subscriptions you intend to browse, and apply the selection. -3. Expand a subscription and storage account in the tree, then open a blob - container to see its blobs. -4. Use **Filter** to inspect statistics, or type **"What is taking up the most - space?"** in the canvas question box. Browsing and filtering do not modify data. - -## What it does - -- Browse storage accounts, blob containers, and blobs with sizes, tiers, and timestamps. -- Filter a container and compute aggregate statistics (totals, size histogram, by type, by tier, by extension, top largest). -- Bulk download selected or filtered blobs with a live progress bar and a per file pass or fail summary. -- Preview a blob inline and ask Copilot to explain the file or the statistics on the canvas. -- Open a container SAS link with no sign in, list its blobs, and download them read only. -- Plan an account to account copy, detect name conflicts, and get the exact `azcopy` command without moving any data. - -## How it is driven - -Copilot can operate the same controls you see. Ask it to open an account, open a container, filter blobs, or download a set, and it drives the canvas live while you watch and can take over at any time. - -## Requirements - -Live mode uses the Azure CLI and your existing `az login`. The canvas reads storage with your own sign in and never starts a sign in for you. Every mutating action runs only from an explicit action on the canvas. - -## Troubleshooting - -- **Canvas unavailable:** check that the plugin is enabled, reload extensions, - fully quit and reopen GitHub Copilot, then retry in a fresh chat. -- **No accounts or access denied:** use **Refresh subscriptions**, confirm the - intended subscription selection, and check your Azure CLI sign-in and storage - permissions. The canvas does not sign in or grant access for you. -- **SAS link rejected:** check its expiry and read/list permissions. Anyone - holding a valid SAS can use its granted access; do not paste it into public - issues or logs. -- **Transfer plan only:** account-to-account copies stop at an `azcopy` - command. Install AzCopy separately to run that command yourself. +# Azure Storage canvas + +Browse Azure Storage and operate it on one live surface that you and Copilot share. + +## Install + +In GitHub Copilot, open **Customize → Plugins → marketplace gear**, add +`microsoft/azure-dev-tools` (marketplace ID `azure-dev-tools`), and install +`azure-storage` when listed. The full plugin includes the canvas and +its opening skill. + +For canvas-only installation, add the [Azure Storage canvas URL](https://github.com/microsoft/azure-dev-tools/tree/azure-storage-latest/canvases/azure-storage/com.github.copilot/extensions/azure-storage) +under **Customize → Canvases → +**. This does not install the opening skill; +open **Azure Storage** from your installed canvases. + +The [latest customer README](https://github.com/microsoft/azure-dev-tools/blob/azure-storage-latest/canvases/azure-storage/README.md) +contains the same setup instructions. Use these paths only when published; +if neither is available, report that rather than substituting a source folder. + +## Quickstart + +1. With the full plugin installed, ask Copilot: **"Open Azure Storage canvas."** + For a canvas-only installation, open **Azure Storage** from installed canvases. +2. Click **Select subscriptions** in **Storage Explorer**, choose the + subscriptions you intend to browse, and apply the selection. +3. Expand a subscription and storage account in the tree, then open a blob + container to see its blobs. +4. Use **Filter** to inspect statistics, or type **"What is taking up the most + space?"** in the canvas question box. Browsing and filtering do not modify data. + +## What it does + +- Browse storage accounts, blob containers, and blobs with sizes, tiers, and timestamps. +- Filter a container and compute aggregate statistics (totals, size histogram, by type, by tier, by extension, top largest). +- Bulk download selected or filtered blobs with a live progress bar and a per file pass or fail summary. +- Preview a blob inline and ask Copilot to explain the file or the statistics on the canvas. +- Open a container SAS link with no sign in, list its blobs, and download them read only. +- Plan an account to account copy, detect name conflicts, and get the exact `azcopy` command without moving any data. + +## How it is driven + +Copilot can operate the same controls you see. Ask it to open an account, open a container, filter blobs, or download a set, and it drives the canvas live while you watch and can take over at any time. + +## Requirements + +Live mode uses the Azure CLI and your existing `az login`. The canvas reads storage with your own sign in and never starts a sign in for you. Every mutating action runs only from an explicit action on the canvas. + +## Troubleshooting + +- **Canvas unavailable:** check that the plugin is enabled, reload extensions, + fully quit and reopen GitHub Copilot, then retry in a fresh chat. +- **No accounts or access denied:** use **Refresh subscriptions**, confirm the + intended subscription selection, and check your Azure CLI sign-in and storage + permissions. The canvas does not sign in or grant access for you. +- **SAS link rejected:** check its expiry and read/list permissions. Anyone + holding a valid SAS can use its granted access; do not paste it into public + issues or logs. +- **Transfer plan only:** account-to-account copies stop at an `azcopy` + command. Install AzCopy separately to run that command yourself. diff --git a/canvases/azure-storage/SHA256SUMS b/canvases/azure-storage/SHA256SUMS index b964375..2d16ce9 100644 --- a/canvases/azure-storage/SHA256SUMS +++ b/canvases/azure-storage/SHA256SUMS @@ -1,14 +1,14 @@ -0dd923a312795d762d9853a0e0f715d1aed49e281604b635cfe4ada995131c12 skills/azure-storage/SKILL.md -1352b0a2e44313483df3622b0e8df403038a1e54995137ca3cf1ca8ddfcf6274 com.github.copilot/extensions/azure-storage/extension.mjs -2821b05b200f512e61b7901e6154aab559f38ecb3a1d1e740788f3790c2c685a checksums.json -469f95fb802bf3811935ee552234b42ec1f5bf14dfa9fafafc8ff512bdfd3789 com.github.copilot/extensions/azure-storage/studio-package.json -4bf8866501ff3a135f4d5c1cb4b7f28a54c431ec54061bffdb1dcfca878ed2c5 package.json -5fb6a9018aecd7aede2a8937f78d81867bf00c5550f30ab071d158b3852ccaca assets/preview.png -6553c36dad97992722dd5d1320889ec09038847c31f2a04cf8756930f725c169 com.github.copilot/extensions/azure-storage/sdk/queue.mjs -88d23b0a1a226385486aade505b086c7bf524ef0bd5b031bac4df0351e8a20f6 com.github.copilot/extensions/azure-storage/sdk/file.mjs +3a262e33c0e21381bd1a4f51edb8ffaf6d823d3a1516e4ef510ca3aff95da9b0 .github/plugin/plugin.json a813071223c79d70046ffde6cd6de1d52eecf7b7b3ce174a3e1bcb2c2ebb283d THIRD_PARTY_NOTICES.txt -afff90d7859f3bf801926ca5907c897f36166a437c1948ac4aa6d32bf8381e30 release.json -c8b84a02d3954378dff3b3a90e3444c71cda4ec09b6037f1d9a4388e0fc5ac8a .github/plugin/plugin.json -cf4b43961f200d9d73f1158aa28b628863274d33e66f53457afa7f6a16562202 com.github.copilot/extensions/azure-storage/sdk/tables.mjs +5fb6a9018aecd7aede2a8937f78d81867bf00c5550f30ab071d158b3852ccaca assets/preview.png +122366309ceaa49cae3a1fad71798502bb0587e31ad50e1746ad8af8e7608a77 checksums.json +45fcd18ae09de9b5542e7a113556482105da42419b1c603fcc69ace3be1a55b4 com.github.copilot/extensions/azure-storage/extension.mjs f7d102239138f46038f9fbbc576d6a34e2f03370f27c282dc7b59dbe470f791d com.github.copilot/extensions/azure-storage/sdk/blob.mjs +88d23b0a1a226385486aade505b086c7bf524ef0bd5b031bac4df0351e8a20f6 com.github.copilot/extensions/azure-storage/sdk/file.mjs fa0526b9488b0e797bfa79044b632edb53483e9e2dc12fe672d093bfb9ace670 com.github.copilot/extensions/azure-storage/sdk/identity.mjs +6553c36dad97992722dd5d1320889ec09038847c31f2a04cf8756930f725c169 com.github.copilot/extensions/azure-storage/sdk/queue.mjs +cf4b43961f200d9d73f1158aa28b628863274d33e66f53457afa7f6a16562202 com.github.copilot/extensions/azure-storage/sdk/tables.mjs +5520dc6cc7535a61cae0fe0c5764746837e96ecc5a431b360deca6d2534d0f3f com.github.copilot/extensions/azure-storage/studio-package.json +be4affe6bd79ceb663389d03dc47fc1e55a8223d7075b370dc0f78853825faaf package.json +e534abc17ed846674eeb89b13b1bf1da5efa3aad6e35fbc0fa9454b12eec118b release.json +27a2fce06a96abfd4a76405a78ff80b3be2cacf50f9dd4bde49649cfea84942f skills/azure-storage/SKILL.md diff --git a/canvases/azure-storage/checksums.json b/canvases/azure-storage/checksums.json index ed59a51..d6af1d2 100644 --- a/canvases/azure-storage/checksums.json +++ b/canvases/azure-storage/checksums.json @@ -1,15 +1,15 @@ { - ".github/plugin/plugin.json": "c8b84a02d3954378dff3b3a90e3444c71cda4ec09b6037f1d9a4388e0fc5ac8a", + ".github/plugin/plugin.json": "3a262e33c0e21381bd1a4f51edb8ffaf6d823d3a1516e4ef510ca3aff95da9b0", "assets/preview.png": "5fb6a9018aecd7aede2a8937f78d81867bf00c5550f30ab071d158b3852ccaca", - "com.github.copilot/extensions/azure-storage/extension.mjs": "1352b0a2e44313483df3622b0e8df403038a1e54995137ca3cf1ca8ddfcf6274", + "com.github.copilot/extensions/azure-storage/extension.mjs": "45fcd18ae09de9b5542e7a113556482105da42419b1c603fcc69ace3be1a55b4", "com.github.copilot/extensions/azure-storage/sdk/blob.mjs": "f7d102239138f46038f9fbbc576d6a34e2f03370f27c282dc7b59dbe470f791d", "com.github.copilot/extensions/azure-storage/sdk/file.mjs": "88d23b0a1a226385486aade505b086c7bf524ef0bd5b031bac4df0351e8a20f6", "com.github.copilot/extensions/azure-storage/sdk/identity.mjs": "fa0526b9488b0e797bfa79044b632edb53483e9e2dc12fe672d093bfb9ace670", "com.github.copilot/extensions/azure-storage/sdk/queue.mjs": "6553c36dad97992722dd5d1320889ec09038847c31f2a04cf8756930f725c169", "com.github.copilot/extensions/azure-storage/sdk/tables.mjs": "cf4b43961f200d9d73f1158aa28b628863274d33e66f53457afa7f6a16562202", - "com.github.copilot/extensions/azure-storage/studio-package.json": "469f95fb802bf3811935ee552234b42ec1f5bf14dfa9fafafc8ff512bdfd3789", - "package.json": "4bf8866501ff3a135f4d5c1cb4b7f28a54c431ec54061bffdb1dcfca878ed2c5", - "release.json": "afff90d7859f3bf801926ca5907c897f36166a437c1948ac4aa6d32bf8381e30", - "skills/azure-storage/SKILL.md": "0dd923a312795d762d9853a0e0f715d1aed49e281604b635cfe4ada995131c12", + "com.github.copilot/extensions/azure-storage/studio-package.json": "5520dc6cc7535a61cae0fe0c5764746837e96ecc5a431b360deca6d2534d0f3f", + "package.json": "be4affe6bd79ceb663389d03dc47fc1e55a8223d7075b370dc0f78853825faaf", + "release.json": "e534abc17ed846674eeb89b13b1bf1da5efa3aad6e35fbc0fa9454b12eec118b", + "skills/azure-storage/SKILL.md": "27a2fce06a96abfd4a76405a78ff80b3be2cacf50f9dd4bde49649cfea84942f", "THIRD_PARTY_NOTICES.txt": "a813071223c79d70046ffde6cd6de1d52eecf7b7b3ce174a3e1bcb2c2ebb283d" } diff --git a/canvases/azure-storage/com.github.copilot/extensions/azure-storage/extension.mjs b/canvases/azure-storage/com.github.copilot/extensions/azure-storage/extension.mjs index af2a766..8751cae 100644 --- a/canvases/azure-storage/com.github.copilot/extensions/azure-storage/extension.mjs +++ b/canvases/azure-storage/com.github.copilot/extensions/azure-storage/extension.mjs @@ -1,12 +1,265 @@ import { createRequire as __canvasCreateRequire } from "node:module"; const require = __canvasCreateRequire(import.meta.url); -import{createCanvas,joinSession}from"@github/copilot-sdk/extension";import os2 from"node:os";import path2 from"node:path";import fs from"node:fs/promises";import{spawn as spawn3}from"node:child_process";import http from"node:http";import{randomBytes}from"node:crypto";var HOST_SESSION_VARIABLE=/^(?:SESSION_ID|(?:COPILOT|AGENCY)_\w*SESSION\w*)$/i;function inheritedCliEnvironment(source=process.env){let env={};for(let[key,value]of Object.entries(source))typeof value=="string"&&!HOST_SESSION_VARIABLE.test(key)&&(env[key]=value);return env}function scrubHostSessionEnv(target=process.env){let removed=[];for(let key of Object.keys(target))HOST_SESSION_VARIABLE.test(key)&&(delete target[key],removed.push(key));return removed}function classifyAuthError(message="",code=""){let text=`${message} ${code}`,aadCode=/\bAADSTS(\d{5,7})\b/.exec(text)?.[1]||null;return aadCode==="901001"||/invalid[_ ]request|client_session/i.test(text)?{needsAuth:!1,kind:"invalid-request",aadCode}:/\b403\b|AuthorizationFailed|\bForbidden\b|does not have authorization/i.test(text)?{needsAuth:!1,kind:"permission",aadCode}:/\b429\b|TooManyRequests|throttl/i.test(text)?{needsAuth:!1,kind:"throttled",aadCode}:/\b401\b|CredentialUnavailable|DefaultAzureCredential|az login|not (?:logged|signed) in|AADSTS(?:50076|50079|50078|50158|65001|700082|70043|50173)|\bcredential\b/i.test(text)?{needsAuth:!0,kind:"needs-auth",aadCode}:aadCode||/authenticat/i.test(text)?{needsAuth:!0,kind:"needs-auth",aadCode}:{needsAuth:!1,kind:"other",aadCode}}function cspHeader(nonce){return["default-src 'none'",`script-src 'nonce-${nonce}'`,"style-src 'unsafe-inline'","img-src 'self' data:","connect-src 'self'","font-src 'self'","base-uri 'none'","form-action 'none'","frame-ancestors 'none'"].join("; ")}var MAX_BODY=100*1024*1024;function isLoopbackHost(hostHeader){if(!hostHeader)return!1;let h=String(hostHeader).toLowerCase(),host=h.startsWith("[")?h.slice(0,h.indexOf("]")+1):h.split(":")[0];return host==="127.0.0.1"||host==="localhost"||host==="[::1]"||host==="::1"}function sameOriginLoopback(req){if(!isLoopbackHost(req.headers.host))return!1;let origin=req.headers.origin;if(origin)try{if(!isLoopbackHost(new URL(origin).host))return!1}catch{return!1}return!0}function createCanvasServer({html,routes={},media=null,initialState={},port=0}={}){let state={activityLog:[],...initialState},clients=new Set;function broadcast(){let data=`data: ${JSON.stringify(state)} +var __defProp=Object.defineProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})};import{createCanvas,joinSession}from"@github/copilot-sdk/extension";import os2 from"node:os";import path2 from"node:path";import fs from"node:fs/promises";import{spawn as spawn3}from"node:child_process";import http from"node:http";import{randomBytes}from"node:crypto";var HOST_SESSION_VARIABLE=/^(?:SESSION_ID|(?:COPILOT|AGENCY)_\w*SESSION\w*)$/i;function inheritedCliEnvironment(source=process.env){let env={};for(let[key,value]of Object.entries(source))typeof value=="string"&&!HOST_SESSION_VARIABLE.test(key)&&(env[key]=value);return env}function scrubHostSessionEnv(target=process.env){let removed=[];for(let key of Object.keys(target))target===process.env&&key.toUpperCase()==="SESSION_ID"||HOST_SESSION_VARIABLE.test(key)&&(delete target[key],removed.push(key));return removed}function humanErrorSummary(message,needsAuth=!1,status=""){let text=String(message||"");return/ContainerAlreadyExists|QueueAlreadyExists|TableAlreadyExists|ShareAlreadyExists|ResourceAlreadyExists|already exists/i.test(text)?"That resource already exists. Choose a different name.":/IncorrectConfiguration|AADSTS901001|invalid.*client_session/i.test(text)?"Azure CLI authentication is misconfigured. Check the tenant login in Azure CLI, then retry.":needsAuth||/AADSTS500|please run.*az login|interaction_required|expired.*token/i.test(text)?"Azure access needs sign-in. Use Sign in only if you want to authenticate, then retry.":String(status)==="403"||/HTTP 403|AuthorizationFailed|Forbidden|PermissionMismatch|not authorized/i.test(text)?"Azure denied access. Check your resource permissions, then retry.":String(status)==="429"||/HTTP 429|TooManyRequests/i.test(text)?"Azure is throttling requests. Wait briefly, then retry.":/ETIMEDOUT|ENOTFOUND|ECONNRESET|fetch failed|timed? ?out/i.test(text)?"Azure could not be reached. Check your network connection, then retry.":text.length>240||/[\r\n]|Command failed|cmd\.exe|Traceback|az account get-access-token|ARM (GET|POST)|HTTP \d{3}/i.test(text)?"Azure could not complete the request. Retry, or open the Debug log for diagnostics.":text}function classifyAuthError(message="",code=""){let text=`${message} ${code}`,aadCode=/\bAADSTS(\d{5,7})\b/.exec(text)?.[1]||null;return aadCode==="901001"||/invalid[_ ]request|client_session/i.test(text)?{needsAuth:!1,kind:"invalid-request",aadCode}:/\b403\b|AuthorizationFailed|\bForbidden\b|does not have authorization/i.test(text)?{needsAuth:!1,kind:"permission",aadCode}:/\b429\b|TooManyRequests|throttl/i.test(text)?{needsAuth:!1,kind:"throttled",aadCode}:/\b401\b|CredentialUnavailable|DefaultAzureCredential|az login|not (?:logged|signed) in|AADSTS(?:50076|50079|50078|50158|65001|700082|70043|50173)|\bcredential\b/i.test(text)?{needsAuth:!0,kind:"needs-auth",aadCode}:aadCode||/authenticat/i.test(text)?{needsAuth:!0,kind:"needs-auth",aadCode}:{needsAuth:!1,kind:"other",aadCode}}function cspHeader(nonce){return["default-src 'none'",`script-src 'nonce-${nonce}'`,"style-src 'unsafe-inline'","img-src 'self' data:","connect-src 'self'","font-src 'self'","base-uri 'none'","form-action 'none'","frame-ancestors 'none'"].join("; ")}var MAX_BODY=100*1024*1024;function isLoopbackHost(hostHeader){if(!hostHeader)return!1;let h=String(hostHeader).toLowerCase(),host=h.startsWith("[")?h.slice(0,h.indexOf("]")+1):h.split(":")[0];return host==="127.0.0.1"||host==="localhost"||host==="[::1]"||host==="::1"}function sameOriginLoopback(req){if(!isLoopbackHost(req.headers.host))return!1;let origin=req.headers.origin;if(origin)try{if(!isLoopbackHost(new URL(origin).host))return!1}catch{return!1}return!0}function createCanvasServer({html,routes={},media=null,initialState={},port=0}={}){let state={activityLog:[],s_entitiesRevision:0,...initialState},clients=new Set;function broadcast(entitiesChanged){let update={...state};entitiesChanged||(delete update.s_entities,update.s_entitiesUnchanged=!0);let data=`data: ${JSON.stringify(update)} -`;for(let res of clients)try{res.write(data)}catch{}}function setState(patch){state={...state,...patch},broadcast()}function getState(){return state}function log(msg){let line=`[${new Date().toLocaleTimeString()}] ${msg}`,activityLog=[...state.activityLog||[],line].slice(-200);setState({activityLog})}let ctx={getState,setState,log},server2=http.createServer(async(req,res)=>{if(req.method==="GET"&&req.url==="/events"){res.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive"}),res.write(`data: ${JSON.stringify(state)} +`;for(let res of clients)try{res.write(data)}catch{}}function setState(patch){let entitiesChanged=Object.hasOwn(patch,"s_entities");state={...state,...patch,s_entitiesRevision:(state.s_entitiesRevision||0)+Number(entitiesChanged)},broadcast(entitiesChanged)}function getState(){return state}function log(msg){let line=`[${new Date().toLocaleTimeString()}] ${msg}`,activityLog=[...state.activityLog||[],line].slice(-200);setState({activityLog})}let ctx={getState,setState,log},server2=http.createServer(async(req,res)=>{if(req.method==="GET"&&req.url==="/events"){res.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive"}),res.write(`data: ${JSON.stringify(state)} -`),clients.add(res),req.on("close",()=>clients.delete(res));return}if(req.method==="GET"&&(req.url==="/"||req.url.startsWith("/?"))){let nonce=randomBytes(16).toString("base64");res.writeHead(200,{"Content-Type":"text/html; charset=utf-8","Content-Security-Policy":cspHeader(nonce),"X-Content-Type-Options":"nosniff","Referrer-Policy":"no-referrer"}),res.end(typeof html=="function"?html(nonce):String(html||""));return}if(req.method==="GET"&&media&&req.url!=="/"&&!req.url.startsWith("/?")){let u=new URL(req.url,"http://127.0.0.1"),out=null;try{out=await media(u.pathname,u.searchParams,ctx)}catch(err){res.writeHead(500,{"Content-Type":"text/plain"}),res.end(String(err?.message||err));return}if(out&&out.buffer){res.writeHead(200,{"Content-Type":out.contentType||"application/octet-stream","Cache-Control":"no-store"}),res.end(out.buffer);return}}if(req.method==="POST"){if(!sameOriginLoopback(req)){res.writeHead(403,{"Content-Type":"application/json"}),res.end(JSON.stringify({ok:!1,error:"cross-origin request blocked"}));return}let path3=req.url.split("?")[0],handler=routes[path3];if(!handler){res.writeHead(404,{"Content-Type":"application/json"}),res.end(JSON.stringify({ok:!1,error:"no route "+path3}));return}let body="",tooLarge=!1;req.on("data",c=>{tooLarge||(body+=c,body.length>MAX_BODY&&(tooLarge=!0,res.writeHead(413,{"Content-Type":"application/json"}),res.end(JSON.stringify({ok:!1,error:"request body too large"})),req.destroy()))}),req.on("end",async()=>{if(tooLarge)return;let payload;try{payload=body?JSON.parse(body):{}}catch{res.writeHead(400,{"Content-Type":"application/json"}),res.end(JSON.stringify({ok:!1,error:"malformed JSON body"}));return}try{let out=await handler(payload,ctx);res.writeHead(200,{"Content-Type":"application/json"}),res.end(JSON.stringify(out??{ok:!0}))}catch(err){res.writeHead(500,{"Content-Type":"application/json"}),res.end(JSON.stringify({ok:!1,error:String(err?.message||err)}))}});return}res.writeHead(404),res.end()});return new Promise(resolve=>{server2.listen(port,"127.0.0.1",()=>{let port2=server2.address().port;resolve({port:port2,url:`http://127.0.0.1:${port2}`,getState,setState,log,close:()=>{for(let res of clients)try{res.end()}catch{}clients.clear(),server2.close()}})})})}async function safe(fn){try{return{ok:!0,data:await fn()}}catch(err){let msg=String(err?.message||err),code=String(err?.code||err?.statusCode||""),{needsAuth,kind}=classifyAuthError(msg,code);return{ok:!1,error:msg,needsAuth,authKind:kind}}}import{AzureCliCredential}from"./sdk/identity.mjs";import{BlobServiceClient,StorageSharedKeyCredential,ContainerClient,generateBlobSASQueryParameters,BlobSASPermissions,ContainerSASPermissions,SASProtocol}from"./sdk/blob.mjs";import{QueueServiceClient,StorageSharedKeyCredential as QueueSharedKey}from"./sdk/queue.mjs";import{ShareServiceClient,StorageSharedKeyCredential as FileSharedKey}from"./sdk/file.mjs";import{TableServiceClient,TableClient,AzureNamedKeyCredential}from"./sdk/tables.mjs";import{spawn,spawnSync}from"node:child_process";import{existsSync}from"node:fs";var ARM="https://management.azure.com",API="2023-01-01";scrubHostSessionEnv();var _cred=null;function credential(){return _cred||=new AzureCliCredential}var _tokens=new Map;async function tokenFor(scope){let now=Date.now(),hit=_tokens.get(scope);if(hit?.token&&hit.exp-12e4>now)return hit.token;if(hit?.inflight)return hit.inflight;let inflight=credential().getToken(scope).then(t=>{if(!t?.token)throw new Error(`Could not acquire a token for ${scope}. Run \`az login\`.`);return _tokens.set(scope,{token:t.token,exp:t.expiresOnTimestamp||now+33e5}),t.token}).catch(e=>{throw _tokens.delete(scope),e});return _tokens.set(scope,{...hit||{},inflight}),inflight}var sharedCredential={getToken:async(scopes,options)=>{if(options?.claims)return credential().getToken(scopes,options);let scope=Array.isArray(scopes)?scopes[0]:scopes;return{token:await tokenFor(scope),expiresOnTimestamp:_tokens.get(scope)?.exp??Date.now()+33e5}}},_warmed=null;function warmup(){return _warmed||(_warmed=Promise.allSettled([tokenFor("https://management.azure.com/.default"),tokenFor("https://storage.azure.com/.default")]),_warmed)}function resetTokenCache(){_tokens.clear(),_cred=null,_warmed=null}function azMissingMsg(e){let m=String(e&&e.message||e);return/ENOENT/i.test(m)?"Azure CLI (az) was not found on PATH. Install it from aka.ms/azure-cli, then try again, or run `az login` manually.":"Could not start az login: "+m}var _signInInflight=null;function signIn(opts={}){return _signInInflight||(_signInInflight=_doSignIn(opts).finally(()=>{_signInInflight=null}),_signInInflight)}function _doSignIn({timeoutMs=18e4,_spawn=spawn}={}){return new Promise(resolve=>{let isWin=process.platform==="win32",env={...inheritedCliEnvironment(),AZURE_CORE_LOGIN_EXPERIENCE_V2:"off"},child;try{child=_spawn("az",["login","--only-show-errors"],{env,windowsHide:!0,shell:isWin,stdio:["ignore","ignore","pipe"]})}catch(e){resolve({ok:!1,error:azMissingMsg(e)});return}let err="";child.stderr&&child.stderr.on("data",d=>{err+=String(d)});let timer=setTimeout(()=>{try{child.kill()}catch{}resolve({ok:!1,error:"Sign-in timed out after 3 minutes. Finish az login in the browser, then click Refresh."})},timeoutMs);child.on("error",e=>{clearTimeout(timer),resolve({ok:!1,error:azMissingMsg(e)})}),child.on("close",code=>{if(clearTimeout(timer),code===0){resetTokenCache(),resolve({ok:!0});return}resolve({ok:!1,error:err.trim()||"az login exited with code "+code})})})}var SE_RESOURCE_TYPE={blob:"Azure.BlobContainer",file:"Azure.FileShare",queue:"Azure.Queue",table:"Azure.Table"};function storageExplorerDeepLink({account,kind,item}={}){let m=account?accountMeta(account):null;if(!m||!m.subscriptionId||!m.resourceGroup)return null;let accountId=`/subscriptions/${m.subscriptionId}/resourceGroups/${m.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${m.name}`,link="storageexplorer://v=1&accountid="+encodeURIComponent(accountId)+"&subscriptionid="+encodeURIComponent(m.subscriptionId),rt=kind?SE_RESOURCE_TYPE[kind]:null;return rt&&item&&(link+="&resourcetype="+encodeURIComponent(rt)+"&resourcename="+encodeURIComponent(item)),link}function storageExplorerExe(){if(process.platform!=="win32")return null;let candidates=[process.env.LOCALAPPDATA&&process.env.LOCALAPPDATA+"\\Programs\\Microsoft Azure Storage Explorer\\StorageExplorer.exe",process.env.ProgramFiles&&process.env.ProgramFiles+"\\Microsoft Azure Storage Explorer\\StorageExplorer.exe",process.env["ProgramFiles(x86)"]&&process.env["ProgramFiles(x86)"]+"\\Microsoft Azure Storage Explorer\\StorageExplorer.exe"].filter(Boolean);for(let p of candidates)try{if(existsSync(p))return p}catch{}return null}var SE_DOWNLOAD="https://aka.ms/storageexplorer",_seInstalled;function storageExplorerInstalled(){if(_seInstalled!==void 0)return _seInstalled;if(storageExplorerExe())return _seInstalled=!0;if(process.platform==="win32"){for(let key of["HKCU\\Software\\Classes\\storageexplorer","HKCR\\storageexplorer"])try{let r=spawnSync("reg",["query",key],{stdio:"ignore",windowsHide:!0});if(r&&r.status===0)return _seInstalled=!0}catch{}return _seInstalled=!1}return _seInstalled=null}function openBrowser(url){let opts={detached:!0,stdio:"ignore"},file,args;process.platform==="win32"?(file=process.env.ComSpec||"cmd.exe",args=["/c","start","",url]):process.platform==="darwin"?(file="open",args=[url]):(file="xdg-open",args=[url]);try{let c=spawn(file,args,opts);c.on("error",()=>{}),c.unref&&c.unref()}catch{}}function seMissingMsg(e){let m=String(e&&e.message||e);return/ENOENT/i.test(m)?"Azure Storage Explorer isn't installed. Get the free desktop app at "+SE_DOWNLOAD+".":"Could not launch Azure Storage Explorer: "+m}function openInStorageExplorer({account,kind,item}={}){return new Promise(resolve=>{if(storageExplorerInstalled()===!1){openBrowser(SE_DOWNLOAD),resolve({ok:!1,notInstalled:!0,install:SE_DOWNLOAD});return}let link=storageExplorerDeepLink({account,kind,item}),exe=storageExplorerExe(),opts={detached:!0,stdio:"ignore"},file,args;if(exe)file=exe,args=link?[link]:[];else if(process.platform==="win32"){if(!link){openBrowser(SE_DOWNLOAD),resolve({ok:!1,notInstalled:!0,install:SE_DOWNLOAD});return}file=process.env.ComSpec||"cmd.exe",args=["/c","start","",link]}else process.platform==="darwin"?(file="open",args=link?[link]:["-a","Microsoft Azure Storage Explorer"]):(file="StorageExplorer",args=link?[link]:[]);let onFail=e=>{if(e&&/ENOENT/i.test(String(e.message||e))){openBrowser(SE_DOWNLOAD),resolve({ok:!1,notInstalled:!0,install:SE_DOWNLOAD});return}resolve({ok:!1,error:seMissingMsg(e)})},child;try{child=spawn(file,args,opts)}catch(e){onFail(e);return}child.on("error",onFail),child.on("spawn",()=>{try{child.unref()}catch{}resolve({ok:!0,deepLink:!!link})})})}async function armToken(){return tokenFor("https://management.azure.com/.default")}async function armPost(path3){let res=await fetch(`${ARM}${path3}`,{method:"POST",headers:{Authorization:`Bearer ${await armToken()}`}});if(!res.ok){let e=new Error(`ARM POST ${path3} \u2192 HTTP ${res.status}`);throw e.statusCode=res.status,e}return res.json()}async function armGetAll(path3){let token=await armToken(),url=path3.startsWith("http")?path3:`${ARM}${path3}`,items=[];for(;url;){let res=await fetch(url,{headers:{Authorization:`Bearer ${token}`}});if(!res.ok){let e=new Error(`ARM GET ${url} \u2192 HTTP ${res.status}`);throw e.statusCode=res.status,e}let j=await res.json();if(Array.isArray(j.value))for(let v of j.value)items.push(v);url=j.nextLink||j["@odata.nextLink"]||null}return items}async function listSubscriptions(){return(await armGetAll("/subscriptions?api-version=2022-12-01")).map(s=>({subscriptionId:s.subscriptionId,displayName:s.displayName}))}var accountIndex=new Map;function rgFromId(id){let m=/\/resourceGroups\/([^/]+)\//i.exec(id||"");return m?m[1]:""}async function mapLimit(items,limit,fn){let results=[],i=0,workers=Array.from({length:Math.min(limit,items.length)},async()=>{for(;i{let accts;try{accts=await armGetAll(`/subscriptions/${sub.subscriptionId}/providers/Microsoft.Storage/storageAccounts?api-version=${API}`)}catch(e){errors.push({subscriptionName:sub.displayName,subscriptionId:sub.subscriptionId,error:String(e?.message||e)});return}for(let a of accts){let acct={name:a.name,subscriptionId:sub.subscriptionId,subscriptionName:sub.displayName,resourceGroup:rgFromId(a.id),location:a.location||"",sku:a.sku?.name||"",kind:a.kind||""};accountIndex.set(a.name,acct),out.push(acct)}}),out.sort((x,y)=>x.name.localeCompare(y.name)),{accounts:out,errors}}function accountMeta(account){return accountIndex.get(account)||null}async function fetchAccountKey(account){let m=accountIndex.get(account);if(!m)throw new Error(`account '${account}' not indexed`);let key=(await armPost(`/subscriptions/${m.subscriptionId}/resourceGroups/${m.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${account}/listKeys?api-version=${API}`))?.keys?.[0]?.value;if(!key)throw new Error("listKeys returned no key");return key}var svcCache=new Map;function accountUrl(account){return`https://${account}.blob.core.windows.net`}async function probe(svc){await svc.listContainers().byPage({maxPageSize:1})[Symbol.asyncIterator]().next()}function isAuthError(e){let s=e?.statusCode??e?.response?.status;if(s===401||s===403)return!0;let txt=`${e?.code||e?.name||""} ${e?.message||""}`;return/Authoriz|Authentic|KeyBasedAuthenticationNotPermitted|InvalidAuthenticationInfo|NoAuthenticationInformation/i.test(txt)}async function buildEntry(account){try{let key=await fetchAccountKey(account);return{svc:new BlobServiceClient(accountUrl(account),new StorageSharedKeyCredential(account,key)),mode:"sharedKey",key}}catch{return{svc:new BlobServiceClient(accountUrl(account),sharedCredential),mode:"aad",key:null}}}async function withBlob(account,fn){let entry=svcCache.get(account);entry||(entry=await buildEntry(account),svcCache.set(account,entry));try{return await fn(entry.svc)}catch(e){if(entry.mode==="sharedKey"&&isAuthError(e)){let aad={svc:new BlobServiceClient(accountUrl(account),sharedCredential),mode:"aad",key:null};return svcCache.set(account,aad),await fn(aad.svc)}throw e}}async function resolveBlobService(account){if(svcCache.has(account))return svcCache.get(account);let keyErr=null,aadErr=null;try{let key=await fetchAccountKey(account),cred=new StorageSharedKeyCredential(account,key),svc=new BlobServiceClient(accountUrl(account),cred);await probe(svc);let entry={svc,mode:"sharedKey",key};return svcCache.set(account,entry),entry}catch(err){keyErr=err}try{let svc=new BlobServiceClient(accountUrl(account),sharedCredential);await probe(svc);let entry={svc,mode:"aad",key:null};return svcCache.set(account,entry),entry}catch(err){aadErr=err}throw new Error(`Could not access '${account}': key fallback ${keyErr?.code||keyErr?.message||"n/a"}; AAD ${aadErr?.code||aadErr?.message||"n/a"}.`)}async function listContainers(account){return withBlob(account,async svc=>{let out=[];for await(let c of svc.listContainers())out.push({name:c.name,lastModified:c.properties?.lastModified||null});return out})}function mapBlob(b){return{name:b.name,size:b.properties?.contentLength??0,lastModified:b.properties?.lastModified?new Date(b.properties.lastModified).toISOString():null,contentType:b.properties?.contentType||"",accessTier:b.properties?.accessTier||"",blobType:b.properties?.blobType||"",tags:b.tags||null}}async function collectBlobs(cc,max){let out=[],truncated=!1;try{for await(let b of cc.listBlobsFlat({includeTags:!0})){if(out.length>=max){truncated=!0;break}out.push(mapBlob(b))}}catch{out.length=0,truncated=!1;for await(let b of cc.listBlobsFlat()){if(out.length>=max){truncated=!0;break}out.push(mapBlob(b))}}return out.truncated=truncated,out.limit=max,out}async function listBlobs(account,container,{max=5e3}={}){return withBlob(account,svc=>collectBlobs(svc.getContainerClient(container),max))}async function createContainer(account,container){let{svc}=await resolveBlobService(account);return await svc.getContainerClient(container).createIfNotExists(),{ok:!0,container}}async function deleteContainer(account,container){let{svc}=await resolveBlobService(account);return await svc.getContainerClient(container).delete(),{ok:!0}}async function deleteBlob(account,container,name){let{svc}=await resolveBlobService(account);return await svc.getContainerClient(container).getBlockBlobClient(name).delete(),{ok:!0}}async function setBlobTier(account,container,name,tier){let{svc}=await resolveBlobService(account);return await svc.getContainerClient(container).getBlockBlobClient(name).setAccessTier(tier),{ok:!0}}async function uploadBlob(account,container,name,buffer,contentType){let{svc}=await resolveBlobService(account),cc=svc.getContainerClient(container);return await cc.createIfNotExists(),await cc.getBlockBlobClient(name).uploadData(buffer,contentType?{blobHTTPHeaders:{blobContentType:contentType}}:void 0),{ok:!0,name,size:buffer.length}}async function downloadBlob(account,container,name,{maxBytes=null}={}){let{svc}=await resolveBlobService(account),bc=svc.getContainerClient(container).getBlockBlobClient(name);if(Number.isFinite(maxBytes)&&maxBytes>0){let resp=await bc.download(0,maxBytes);return await streamToBuffer(resp.readableStreamBody)}return await bc.downloadToBuffer()}async function blobSize(account,container,name){let{svc}=await resolveBlobService(account),props=await svc.getContainerClient(container).getBlockBlobClient(name).getProperties();return Number(props.contentLength)||0}async function streamToBuffer(stream){let chunks=[];for await(let chunk of stream)chunks.push(Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk));return Buffer.concat(chunks)}async function generateBlobSas(account,container,name,{minutes=60,permissions="r"}={}){let entry=await resolveBlobService(account),perms=BlobSASPermissions.parse(permissions||"r"),startsOn=new Date(Date.now()-5*6e4),expiresOn=new Date(Date.now()+Math.max(1,minutes)*6e4),common={containerName:container,blobName:name,permissions:perms,startsOn,expiresOn,protocol:SASProtocol.Https},sas,mode;if(entry.mode==="sharedKey"&&entry.key){let cred=new StorageSharedKeyCredential(account,entry.key);sas=generateBlobSASQueryParameters(common,cred).toString(),mode="account-key"}else{let udk=await entry.svc.getUserDelegationKey(startsOn,expiresOn);sas=generateBlobSASQueryParameters(common,udk,account).toString(),mode="user-delegation"}let encName=name.split("/").map(encodeURIComponent).join("/");return{url:`${accountUrl(account)}/${container}/${encName}?${sas}`,expiresOn:expiresOn.toISOString(),permissions:permissions||"r",mode}}async function generateContainerSas(account,container,{minutes=1440,permissions="rl"}={}){let entry=await resolveBlobService(account),perms=ContainerSASPermissions.parse(permissions||"rl"),startsOn=new Date(Date.now()-5*6e4),expiresOn=new Date(Date.now()+Math.max(1,minutes)*6e4),common={containerName:container,permissions:perms,startsOn,expiresOn,protocol:SASProtocol.Https},sas,mode;if(entry.mode==="sharedKey"&&entry.key){let cred=new StorageSharedKeyCredential(account,entry.key);sas=generateBlobSASQueryParameters(common,cred).toString(),mode="account-key"}else{let udk=await entry.svc.getUserDelegationKey(startsOn,expiresOn);sas=generateBlobSASQueryParameters(common,udk,account).toString(),mode="user-delegation"}return{url:`${accountUrl(account)}/${container}?${sas}`,expiresOn:expiresOn.toISOString(),permissions:permissions||"rl",mode}}async function listSasBlobs(sasUrl,{max=5e3}={}){let cc=new ContainerClient(sasUrl);return collectBlobs(cc,max)}async function downloadSasBlob(sasUrl,name){return await new ContainerClient(sasUrl).getBlockBlobClient(name).downloadToBuffer()}var queueCache=new Map;function queueUrl(account){return`https://${account}.queue.core.windows.net`}async function resolveQueueService(account){if(queueCache.has(account))return queueCache.get(account);let keyErr=null;try{let key=await fetchAccountKey(account),svc=new QueueServiceClient(queueUrl(account),new QueueSharedKey(account,key));await svc.listQueues().byPage({maxPageSize:1})[Symbol.asyncIterator]().next();let e={svc,mode:"sharedKey"};return queueCache.set(account,e),e}catch(err){keyErr=err}try{let svc=new QueueServiceClient(queueUrl(account),sharedCredential);await svc.listQueues().byPage({maxPageSize:1})[Symbol.asyncIterator]().next();let e={svc,mode:"aad"};return queueCache.set(account,e),e}catch(err){throw new Error(`Could not access queues for '${account}': key ${keyErr?.code||keyErr?.message||"n/a"}; AAD ${err?.code||err?.message||"n/a"}.`)}}async function listQueues(account){let{svc}=await resolveQueueService(account),out=[];for await(let q of svc.listQueues()){let count=null;try{count=(await svc.getQueueClient(q.name).getProperties()).approximateMessagesCount??null}catch{}out.push({name:q.name,approximateMessagesCount:count})}return out}async function peekQueueMessages(account,queue,{max=32}={}){let{svc}=await resolveQueueService(account);return((await svc.getQueueClient(queue).peekMessages({numberOfMessages:Math.min(max,32)})).peekedMessageItems||[]).map(m=>({messageId:m.messageId,messageText:m.messageText,insertedOn:m.insertedOn?new Date(m.insertedOn).toISOString():null,dequeueCount:m.dequeueCount??0}))}async function createQueue(account,queue){let{svc}=await resolveQueueService(account);return await svc.getQueueClient(queue).create(),{ok:!0}}async function deleteQueue(account,queue){let{svc}=await resolveQueueService(account);return await svc.getQueueClient(queue).delete(),{ok:!0}}async function sendQueueMessage(account,queue,text){let{svc}=await resolveQueueService(account);return{ok:!0,messageId:(await svc.getQueueClient(queue).sendMessage(String(text??""))).messageId}}async function clearQueueMessages(account,queue){let{svc}=await resolveQueueService(account);return await svc.getQueueClient(queue).clearMessages(),{ok:!0}}async function withReceivedMessage(account,queue,messageId,act){let{svc}=await resolveQueueService(account),qc=svc.getQueueClient(queue),msgs=(await qc.receiveMessages({numberOfMessages:32,visibilityTimeout:30})).receivedMessageItems||[],target=msgs.find(m=>m.messageId===messageId);for(let m of msgs)if(m.messageId!==messageId)try{await qc.updateMessage(m.messageId,m.popReceipt,void 0,0)}catch{}if(!target)throw new Error("That message isn't among the first 32 in the queue, so it can't be changed directly.");return await act(qc,target),{ok:!0}}async function updateQueueMessage(account,queue,messageId,text){return withReceivedMessage(account,queue,messageId,(qc,target)=>qc.updateMessage(messageId,target.popReceipt,String(text??""),0))}async function deleteQueueMessage(account,queue,messageId){return withReceivedMessage(account,queue,messageId,(qc,target)=>qc.deleteMessage(messageId,target.popReceipt))}var tableCache=new Map;function tableUrl(account){return`https://${account}.table.core.windows.net`}async function resolveTableService(account){if(tableCache.has(account))return tableCache.get(account);let keyErr=null;try{let key=await fetchAccountKey(account),cred=new AzureNamedKeyCredential(account,key),svc=new TableServiceClient(tableUrl(account),cred);await svc.listTables().byPage({maxPageSize:1})[Symbol.asyncIterator]().next();let e={svc,mode:"sharedKey",cred};return tableCache.set(account,e),e}catch(err){keyErr=err}try{let cred=sharedCredential,svc=new TableServiceClient(tableUrl(account),cred);await svc.listTables().byPage({maxPageSize:1})[Symbol.asyncIterator]().next();let e={svc,mode:"aad",cred};return tableCache.set(account,e),e}catch(err){throw new Error(`Could not access tables for '${account}': key ${keyErr?.code||keyErr?.message||"n/a"}; AAD ${err?.code||err?.message||"n/a"}.`)}}async function listTables(account){let{svc}=await resolveTableService(account),out=[];for await(let t of svc.listTables())out.push({name:t.name});return out}async function queryTableEntities(account,table,{max=100}={}){let{cred}=await resolveTableService(account),tc=new TableClient(tableUrl(account),table,cred),rows=[],cols=new Set(["partitionKey","rowKey","timestamp"]);for await(let ent of tc.listEntities()){let row={};for(let k of Object.keys(ent)){if(k.startsWith("odata.")||k==="etag")continue;cols.add(k);let v=ent[k];row[k]=v&&typeof v=="object"&&typeof v.toISOString=="function"?v.toISOString():v}if(rows.push(row),rows.length>=max)break}return{columns:Array.from(cols),rows}}async function createTable(account,table){let{svc}=await resolveTableService(account);return await svc.createTable(table),{ok:!0}}async function deleteTable(account,table){let{svc}=await resolveTableService(account);return await svc.deleteTable(table),{ok:!0}}async function upsertEntity(account,table,entity){if(!entity||!entity.partitionKey||!entity.rowKey)throw new Error("entity requires partitionKey and rowKey");let{cred}=await resolveTableService(account);return await new TableClient(tableUrl(account),table,cred).upsertEntity(entity,"Merge"),{ok:!0}}async function deleteEntity(account,table,partitionKey,rowKey){let{cred}=await resolveTableService(account);return await new TableClient(tableUrl(account),table,cred).deleteEntity(partitionKey,rowKey),{ok:!0}}var shareCache=new Map;function fileUrl(account){return`https://${account}.file.core.windows.net`}async function resolveShareService(account){if(shareCache.has(account))return shareCache.get(account);let keyErr=null;try{let key=await fetchAccountKey(account),svc=new ShareServiceClient(fileUrl(account),new FileSharedKey(account,key));await svc.listShares().byPage({maxPageSize:1})[Symbol.asyncIterator]().next();let e={svc,mode:"sharedKey"};return shareCache.set(account,e),e}catch(err){keyErr=err}try{let svc=new ShareServiceClient(fileUrl(account),sharedCredential,{fileRequestIntent:"backup"});await svc.listShares().byPage({maxPageSize:1})[Symbol.asyncIterator]().next();let e={svc,mode:"aad"};return shareCache.set(account,e),e}catch(err){throw new Error(`Could not access file shares for '${account}': key ${keyErr?.code||keyErr?.message||"n/a"}; AAD ${err?.code||err?.message||"n/a"}.`)}}async function listShares(account){let{svc}=await resolveShareService(account),out=[];for await(let s of svc.listShares())out.push({name:s.name,quotaGiB:s.properties?.quota??null});return out}async function listShareItems(account,share,dir=""){let{svc}=await resolveShareService(account),dc=svc.getShareClient(share).getDirectoryClient(dir||""),out=[];for await(let item of dc.listFilesAndDirectories())out.push({name:item.name,kind:item.kind,size:item.kind==="file"?item.properties?.contentLength??0:null});return out}async function createShare(account,share){let{svc}=await resolveShareService(account);return await svc.getShareClient(share).create(),{ok:!0}}async function deleteShare(account,share){let{svc}=await resolveShareService(account);return await svc.getShareClient(share).delete(),{ok:!0}}async function createShareDirectory(account,share,dir){let{svc}=await resolveShareService(account);return await svc.getShareClient(share).getDirectoryClient(dir).create(),{ok:!0}}async function uploadShareFile(account,share,name,buffer){let{svc}=await resolveShareService(account),fc=svc.getShareClient(share).rootDirectoryClient.getFileClient(name);return await fc.create(buffer.length),buffer.length&&await fc.uploadData(buffer),{ok:!0,size:buffer.length}}async function downloadShareFile(account,share,name){let{svc}=await resolveShareService(account);return await svc.getShareClient(share).rootDirectoryClient.getFileClient(name).downloadToBuffer()}async function deleteShareItem(account,share,name,kind){let{svc}=await resolveShareService(account),sc=svc.getShareClient(share);return kind==="directory"?await sc.getDirectoryClient(name).delete():await sc.rootDirectoryClient.getFileClient(name).delete(),{ok:!0}}function fmtBytes(n){if(n=Number(n)||0,n<1024)return`${n} B`;let u=["KB","MB","GB","TB","PB"],i=-1;do n/=1024,i++;while(n>=1024&&i=10?0:1)} ${u[i]}`}function globToRe(glob){let body=String(glob).replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*").replace(/\?/g,".");return new RegExp("^"+body+"$","i")}function typeFamily(contentType,name){let ct=String(contentType||"").toLowerCase(),ext=String(name||"").toLowerCase().split(".").pop();return/^image\//.test(ct)||/^(png|jpe?g|gif|webp|bmp|svg|tiff?)$/.test(ext)?"image":/^video\//.test(ct)||/^(mp4|mov|avi|mkv|webm)$/.test(ext)?"video":/^audio\//.test(ct)||/^(mp3|wav|flac|ogg|m4a)$/.test(ext)?"audio":/json|xml|csv|text\/|javascript|yaml/.test(ct)||/^(json|xml|csv|txt|log|md|yaml|yml|js|ts|html?|css)$/.test(ext)?"text/data":/pdf|word|excel|powerpoint|officedocument|msword/.test(ct)||/^(pdf|docx?|xlsx?|pptx?)$/.test(ext)?"document":/zip|gzip|tar|compress|x-7z|rar/.test(ct)||/^(zip|gz|tar|7z|rar|bz2)$/.test(ext)?"archive":/octet-stream/.test(ct)||/^(bin|dat|iso|vhd|img)$/.test(ext)?"binary":ct.split("/")[0]||"other"}function applyBlobFilter(blobs,c={}){let list=Array.isArray(blobs)?blobs:[],nowMs=c.now?Date.parse(c.now):Date.now(),re=c.namePattern&&String(c.namePattern).trim()?globToRe(String(c.namePattern).trim()):null,prefix=c.prefix!=null&&String(c.prefix)!==""?String(c.prefix).toLowerCase():null,suffix=c.suffix!=null&&String(c.suffix)!==""?String(c.suffix).toLowerCase():null,contains=c.contains!=null&&String(c.contains)!==""?String(c.contains).toLowerCase():null,min=c.minSize!=null&&c.minSize!==""?Number(c.minSize):null,max=c.maxSize!=null&&c.maxSize!==""?Number(c.maxSize):null,older=c.olderThanDays!=null&&c.olderThanDays!==""?Number(c.olderThanDays):null,newer=c.newerThanDays!=null&&c.newerThanDays!==""?Number(c.newerThanDays):null,type=c.type&&String(c.type).trim()&&c.type!=="all"?String(c.type).trim():null,tiers=normList(c.tiers).map(t=>t.toLowerCase()),blobTypes=normList(c.blobTypes).map(t=>t.toLowerCase()),tagKeys=normList(c.tagKeys),tags=c.tags&&typeof c.tags=="object"?c.tags:null,DAY=864e5;return list.filter(b=>{let nm=String(b.name||""),nml=nm.toLowerCase();if(re&&!re.test(nm)||prefix&&!nml.startsWith(prefix)||suffix&&!nml.endsWith(suffix)||contains&&nml.indexOf(contains)===-1)return!1;let sz=Number(b.size)||0;if(min!=null&&szmax)return!1;if((older!=null||newer!=null)&&b.lastModified){let ageMs=nowMs-Date.parse(b.lastModified);if(older!=null&&!(ageMs>older*DAY)||newer!=null&&!(ageMs<=newer*DAY))return!1}else if(older!=null||newer!=null)return!1;if(type&&typeFamily(b.contentType,b.name)!==type||tiers.length&&tiers.indexOf(String(b.accessTier||"").toLowerCase())===-1||blobTypes.length&&blobTypes.indexOf(String(b.blobType||"").toLowerCase())===-1)return!1;if(tagKeys.length){let bt=b.tags||{};for(let k of tagKeys)if(!(k in bt))return!1}if(tags){let bt=b.tags||{};for(let k of Object.keys(tags))if(String(bt[k])!==String(tags[k]))return!1}return!0})}function normList(v){return v==null||v===""?[]:Array.isArray(v)?v.map(x=>String(x).trim()).filter(Boolean):String(v).split(",").map(x=>x.trim()).filter(Boolean)}function computeBlobStats(blobs){let list=Array.isArray(blobs)?blobs:[],n=list.length,sizes=list.map(b=>Number(b.size)||0),total=sizes.reduce((a,s)=>a+s,0),max=n?Math.max(...sizes):0,avg=n?Math.round(total/n):0,buckets=[{label:"0\u20131 KB",lo:0,hi:1024},{label:"1 KB\u20131 MB",lo:1024,hi:1048576},{label:"1\u2013100 MB",lo:1048576,hi:104857600},{label:"100 MB\u20131 GB",lo:104857600,hi:1073741824},{label:">1 GB",lo:1073741824,hi:1/0}].map(bk=>({label:bk.label,n:sizes.filter(s=>s>=bk.lo&&s({label,n:v.n,bytes:v.bytes})).sort((a,b)=>b.bytes-a.bytes),byTierMap=new Map;for(let b of list){let tier=String(b.accessTier||"").trim()||"\u2014",cur=byTierMap.get(tier)||{n:0,bytes:0};cur.n++,cur.bytes+=Number(b.size)||0,byTierMap.set(tier,cur)}let byTier=[...byTierMap.entries()].map(([label,v])=>({label,n:v.n,bytes:v.bytes})).sort((a,b)=>b.bytes-a.bytes),hasTier=byTier.some(t=>t.label!=="\u2014"),byExtMap=new Map;for(let b of list){let parts=String(b.name||"").toLowerCase().split("."),ext=parts.length>1?"."+parts.pop():"(none)",cur=byExtMap.get(ext)||{n:0,bytes:0};cur.n++,cur.bytes+=Number(b.size)||0,byExtMap.set(ext,cur)}let byExt=[...byExtMap.entries()].map(([label,v])=>({label,n:v.n,bytes:v.bytes})).sort((a,b)=>b.bytes-a.bytes).slice(0,8),topLargest=[...list].sort((a,b)=>(Number(b.size)||0)-(Number(a.size)||0)).slice(0,10).map(b=>({name:b.name,size:Number(b.size)||0,sizeh:fmtBytes(b.size),lastModified:b.lastModified||null}));return{count:n,totalBytes:total,totalh:fmtBytes(total),avgBytes:avg,avgh:fmtBytes(avg),maxBytes:max,maxh:fmtBytes(max),buckets,byType,byTier:hasTier?byTier:[],byExt,topLargest}}function filterAndStat(blobs,criteria={}){let filtered=applyBlobFilter(blobs,criteria);return{filtered,stats:computeBlobStats(filtered),criteria}}var OVERWRITE={overwrite:"true",skip:"false","if-newer":"ifSourceNewer"};function resolutionFlag(resolution){return OVERWRITE[resolution]||"true"}function planTransfer(sourceBlobs,destBlobs,resolution="overwrite"){let src=Array.isArray(sourceBlobs)?sourceBlobs:[],dstByName=new Map((Array.isArray(destBlobs)?destBlobs:[]).map(b=>[b.name,b])),conflicts=[],newFiles=[];for(let b of src)dstByName.has(b.name)?conflicts.push(b):newFiles.push(b);let toTransfer;if(resolution==="skip")toTransfer=newFiles.slice();else if(resolution==="if-newer"){let newerConflicts=conflicts.filter(b=>{let d=dstByName.get(b.name),s=Date.parse(b.lastModified||""),dm=Date.parse(d?.lastModified||"");return Number.isFinite(s)&&Number.isFinite(dm)?s>dm:!0});toTransfer=newFiles.concat(newerConflicts)}else toTransfer=src.slice();let bytes=toTransfer.reduce((a,b)=>a+(Number(b.size)||0),0),skipped=src.length-toTransfer.length;return{resolution,sourceCount:src.length,destCount:dstByName.size,conflictCount:conflicts.length,newCount:newFiles.length,transferCount:toTransfer.length,skippedCount:skipped,transferBytes:bytes,transferBytesh:fmtBytes(bytes),conflicts:conflicts.slice(0,100).map(b=>({name:b.name,size:Number(b.size)||0,sizeh:fmtBytes(b.size),lastModified:b.lastModified||null})),toTransfer:toTransfer.map(b=>b.name)}}function azcopyCommand({sourceUrl,destUrl,resolution="overwrite",includeNames=null,allSource=!1}={}){let q=s=>`"${String(s||"").replace(/[\\"$`]/g,"\\$&")}"`,flags=[`--overwrite=${resolutionFlag(resolution)}`];if(allSource)flags.push("--recursive");else if(Array.isArray(includeNames)){if(!includeNames.length)return{command:null,needsLogin:!1,empty:!0};flags.push(`--include-path ${q(includeNames.join(";"))}`),flags.push("--recursive")}else flags.push("--recursive");let needsLogin=!/[?&]sig=/.test(String(sourceUrl||""))||!/[?&]sig=/.test(String(destUrl||""));return{command:`azcopy copy ${q(sourceUrl)} ${q(destUrl)} ${flags.join(" ")}`,needsLogin}}function sniffText(bytes){let buf=bytes instanceof Uint8Array?bytes:new Uint8Array(bytes||[]);if(!buf.length)return{isText:!0,text:""};let control=0;for(let i=0;i13&&c<32||c===127)&&control++}if(control/buf.length>.1)return{isText:!1,text:null};try{let text=new TextDecoder("utf-8",{fatal:!1}).decode(buf);return(text.match(/\uFFFD/g)||[]).length/Math.max(text.length,1)>.1?{isText:!1,text:null}:{isText:!0,text}}catch{return{isText:!1,text:null}}}var MAGIC=[{off:0,hex:"67 6C 54 46",format:"glTF binary (GLB) 3D model",category:"3D model"},{off:0,hex:"89 50 4E 47 0D 0A 1A 0A",format:"PNG image",category:"image"},{off:0,hex:"FF D8 FF",format:"JPEG image",category:"image"},{off:0,hex:"47 49 46 38",format:"GIF image",category:"image"},{off:0,hex:"42 4D",format:"BMP image",category:"image"},{off:0,hex:"49 49 2A 00",format:"TIFF image (little-endian)",category:"image"},{off:0,hex:"4D 4D 00 2A",format:"TIFF image (big-endian)",category:"image"},{off:0,hex:"00 00 01 00",format:"ICO icon",category:"image"},{off:0,hex:"25 50 44 46",format:"PDF document",category:"document"},{off:0,hex:"D0 CF 11 E0 A1 B1 1A E1",format:"Legacy MS Office (OLE2: .doc/.xls/.ppt)",category:"document"},{off:0,hex:"50 4B 03 04",format:"ZIP archive (or OOXML .docx/.xlsx/.pptx, JAR, APK)",category:"archive"},{off:0,hex:"50 4B 05 06",format:"ZIP archive (empty)",category:"archive"},{off:0,hex:"1F 8B",format:"gzip archive",category:"archive"},{off:0,hex:"42 5A 68",format:"bzip2 archive",category:"archive"},{off:0,hex:"37 7A BC AF 27 1C",format:"7-Zip archive",category:"archive"},{off:0,hex:"52 61 72 21 1A 07",format:"RAR archive",category:"archive"},{off:0,hex:"FD 37 7A 58 5A 00",format:"XZ archive",category:"archive"},{off:257,hex:"75 73 74 61 72",format:"TAR archive",category:"archive"},{off:0,hex:"7F 45 4C 46",format:"ELF executable / shared object",category:"executable"},{off:0,hex:"4D 5A",format:"Windows PE executable (.exe/.dll)",category:"executable"},{off:0,hex:"CA FE BA BE",format:"Java class file",category:"executable"},{off:0,hex:"00 61 73 6D",format:"WebAssembly (WASM) module",category:"executable"},{off:0,hex:"4F 67 67 53",format:"Ogg media",category:"audio"},{off:0,hex:"49 44 33",format:"MP3 audio (ID3)",category:"audio"},{off:0,hex:"66 4C 61 43",format:"FLAC audio",category:"audio"},{off:0,hex:"53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 00",format:"SQLite database",category:"database"},{off:0,hex:"50 41 52 31",format:"Apache Parquet",category:"data"},{off:0,hex:"4F 62 6A 01",format:"Apache Avro",category:"data"}],EXT_FORMAT={glb:["glTF binary (GLB) 3D model","3D model"],gltf:["glTF 3D model","3D model"],obj:["Wavefront OBJ 3D model","3D model"],fbx:["Autodesk FBX 3D model","3D model"],stl:["STL 3D model","3D model"],usdz:["USDZ 3D scene","3D model"],ply:["PLY 3D model","3D model"],png:["PNG image","image"],jpg:["JPEG image","image"],jpeg:["JPEG image","image"],gif:["GIF image","image"],webp:["WebP image","image"],bmp:["BMP image","image"],tiff:["TIFF image","image"],ico:["ICO icon","image"],heic:["HEIC image","image"],avif:["AVIF image","image"],svg:["SVG vector image","image"],psd:["Photoshop document","image"],pdf:["PDF document","document"],docx:["Word document (OOXML)","document"],xlsx:["Excel workbook (OOXML)","document"],pptx:["PowerPoint deck (OOXML)","document"],doc:["Legacy Word document","document"],xls:["Legacy Excel workbook","document"],ppt:["Legacy PowerPoint deck","document"],zip:["ZIP archive","archive"],gz:["gzip archive","archive"],tgz:["gzip TAR archive","archive"],tar:["TAR archive","archive"],rar:["RAR archive","archive"],"7z":["7-Zip archive","archive"],bz2:["bzip2 archive","archive"],xz:["XZ archive","archive"],zst:["Zstandard archive","archive"],mp3:["MP3 audio","audio"],wav:["WAV audio","audio"],flac:["FLAC audio","audio"],ogg:["Ogg audio","audio"],m4a:["AAC audio","audio"],mp4:["MP4 video","video"],mov:["QuickTime video","video"],avi:["AVI video","video"],mkv:["Matroska video","video"],webm:["WebM video","video"],exe:["Windows executable","executable"],dll:["Windows library","executable"],so:["Shared object","executable"],wasm:["WebAssembly module","executable"],parquet:["Apache Parquet","data"],avro:["Apache Avro","data"],orc:["Apache ORC","data"],db:["SQLite database","database"],sqlite:["SQLite database","database"],bin:["Raw binary","binary"],dat:["Raw data","binary"]};function hexHead(buf,n=16){let out=[];for(let i=0;i{let sig=hex.split(" ").map(h=>parseInt(h,16));if(off+sig.length>buf.length)return!1;for(let i=0;ibuf.length)return{version,truncated:!0};json=new TextDecoder("utf-8").decode(buf.subarray(dstart,dend));break}off=dend}if(json==null)return{version,truncated:!0};let g;try{g=JSON.parse(json)}catch{return{version,truncated:!0}}let n=a=>Array.isArray(a)?a.length:0,accessors=g.accessors||[],vertices=0,triangles=0;for(let mesh of g.meshes||[])for(let prim of mesh.primitives||[]){let posIdx=prim.attributes&&prim.attributes.POSITION;posIdx!=null&&accessors[posIdx]&&(vertices+=accessors[posIdx].count||0),prim.indices!=null&&accessors[prim.indices]&&(triangles+=Math.floor((accessors[prim.indices].count||0)/3))}return{version,truncated:!1,meshes:n(g.meshes),materials:n(g.materials),textures:n(g.textures),images:n(g.images),animations:n(g.animations),nodes:n(g.nodes),accessors:n(g.accessors),vertices,triangles,generator:g.asset&&g.asset.generator||null,gltfVersion:g.asset&&g.asset.version||null}}function previewLang(name){let m=/\.([a-z0-9]+)$/i.exec(String(name||"")),ext=m?m[1].toLowerCase():"";return{json:"json",ndjson:"json",js:"javascript",mjs:"javascript",ts:"typescript",csv:"csv",tsv:"csv",xml:"xml",html:"html",htm:"html",md:"markdown",yml:"yaml",yaml:"yaml",log:"log",txt:"text",ini:"ini",conf:"ini",sql:"sql",sh:"shell",py:"python",cs:"csharp"}[ext]||ext||"text"}function parseSasUrl(rawUrl){let u;try{u=new URL(String(rawUrl||"").trim())}catch{return{ok:!1,error:"That doesn't look like a URL."}}if(u.protocol!=="https:"){let emulator=/^(127\.0\.0\.1|localhost|\[::1\])$/i.test(u.hostname);if(!(u.protocol==="http:"&&emulator))return{ok:!1,error:"SAS URL must use HTTPS \u2014 a SAS signature is a bearer credential and must not travel over plain HTTP."}}let q=u.searchParams;if(!q.get("sig"))return{ok:!1,error:"No SAS signature (sig=) in the URL \u2014 that's a plain URL, not a SAS."};let host=u.hostname,accountMatch=/^([^.]+)\.blob\./.exec(host),account=accountMatch?accountMatch[1]:host,segs=u.pathname.replace(/^\/+/,"").split("/"),container=segs[0]||"",blobPath=segs.slice(1).join("/"),perms=q.get("sp")||"",expiry=q.get("se")||"",resource=q.get("sr")||(blobPath?"b":container?"c":""),now=Date.now(),expMs=expiry?Date.parse(expiry):NaN,expired=Number.isFinite(expMs)&&expMs{let done=!1,finish=value=>{done||(done=!0,resolve(value))};execFile("azcopy",["--version"],{timeout:4e3,windowsHide:!0},(err,stdout)=>{if(err){let missing=err.code==="ENOENT";finish({installed:!1,version:null,reason:missing?"notFound":"error",hint:missing?`AzCopy isn't on your PATH. Install it from ${AZCOPY_INSTALL_URL}, then re-open the transfer plan.`:`AzCopy was found but couldn't be queried (${String(err.message||err).slice(0,120)}).`,installUrl:AZCOPY_INSTALL_URL});return}let version=(String(stdout||"").match(/\d+\.\d+\.\d+/)||[])[0]||null;finish({installed:!0,version,reason:"ok",hint:null,installUrl:AZCOPY_INSTALL_URL})}).on("error",()=>finish({installed:!1,version:null,reason:"notFound",hint:`AzCopy isn't on your PATH. Install it from ${AZCOPY_INSTALL_URL}, then re-open the transfer plan.`,installUrl:AZCOPY_INSTALL_URL}))})),azcopyProbe}import{execFile as execFile2,spawn as spawn2,spawnSync as spawnSync2}from"node:child_process";import{createHash}from"node:crypto";import{accessSync,constants,readFileSync}from"node:fs";import{cp,lstat,mkdir,mkdtemp,readFile,readdir,rename,rm,stat,writeFile}from"node:fs/promises";import os from"node:os";import path from"node:path";import{fileURLToPath}from"node:url";function resolveStudioRoot(moduleUrl){let directory=path.dirname(fileURLToPath(moduleUrl)),candidates=path.basename(directory)==="src"?[directory,path.dirname(directory)]:[directory];for(let candidate of candidates)for(let metadata of["studio-package.json","package.json"])try{return accessSync(path.join(candidate,metadata)),candidate}catch(error){if(error.code!=="ENOENT")throw error}throw new Error(`Missing canvas package metadata for ${moduleUrl}`)}function resolveStudioBuildInfo(moduleUrl,fallbackVersion="unknown"){let version=fallbackVersion,revision="unknown";try{let manifest;try{manifest=readFileSync(new URL("./studio-package.json",moduleUrl),"utf8")}catch(error){if(error.code!=="ENOENT")throw error;manifest=readFileSync(path.join(resolveStudioRoot(moduleUrl),"package.json"),"utf8")}version=JSON.parse(manifest).version||fallbackVersion}catch{}try{revision=createHash("sha256").update(readFileSync(new URL(moduleUrl))).digest("hex").slice(0,10)}catch{}return{version,revision}}var ICONS={vscode:'',github:'',azure:''},COMMAND_BUTTONS=` +`),clients.add(res),req.on("close",()=>clients.delete(res));return}if(req.method==="GET"&&(req.url==="/"||req.url.startsWith("/?"))){let nonce=randomBytes(16).toString("base64");res.writeHead(200,{"Content-Type":"text/html; charset=utf-8","Content-Security-Policy":cspHeader(nonce),"X-Content-Type-Options":"nosniff","Referrer-Policy":"no-referrer"}),res.end(typeof html=="function"?html(nonce):String(html||""));return}if(req.method==="GET"&&media&&req.url!=="/"&&!req.url.startsWith("/?")){let u=new URL(req.url,"http://127.0.0.1"),out=null;try{out=await media(u.pathname,u.searchParams,ctx)}catch(err){res.writeHead(500,{"Content-Type":"text/plain"}),res.end(String(err?.message||err));return}if(out&&out.buffer){res.writeHead(200,{"Content-Type":out.contentType||"application/octet-stream","Cache-Control":"no-store"}),res.end(out.buffer);return}}if(req.method==="POST"){if(!sameOriginLoopback(req)){res.writeHead(403,{"Content-Type":"application/json"}),res.end(JSON.stringify({ok:!1,error:"cross-origin request blocked"}));return}let path3=req.url.split("?")[0],handler=routes[path3];if(!handler){res.writeHead(404,{"Content-Type":"application/json"}),res.end(JSON.stringify({ok:!1,error:"no route "+path3}));return}let body="",tooLarge=!1;req.on("data",c=>{tooLarge||(body+=c,body.length>MAX_BODY&&(tooLarge=!0,res.writeHead(413,{"Content-Type":"application/json"}),res.end(JSON.stringify({ok:!1,error:"request body too large"})),req.destroy()))}),req.on("end",async()=>{if(tooLarge)return;let payload;try{payload=body?JSON.parse(body):{}}catch{res.writeHead(400,{"Content-Type":"application/json"}),res.end(JSON.stringify({ok:!1,error:"malformed JSON body"}));return}try{let out=await handler(payload,ctx);res.writeHead(200,{"Content-Type":"application/json"}),res.end(JSON.stringify(out??{ok:!0}))}catch(err){res.writeHead(500,{"Content-Type":"application/json"}),res.end(JSON.stringify({ok:!1,error:String(err?.message||err)}))}});return}res.writeHead(404),res.end()});return new Promise(resolve=>{server2.listen(port,"127.0.0.1",()=>{let port2=server2.address().port;resolve({port:port2,url:`http://127.0.0.1:${port2}`,getState,setState,log,close:()=>{for(let res of clients)try{res.end()}catch{}clients.clear(),server2.close()}})})})}async function safe(fn){try{return{ok:!0,data:await fn()}}catch(err){let msg=String(err?.message||err),code=String(err?.code||err?.statusCode||""),{needsAuth,kind}=classifyAuthError(msg,code);return{ok:!1,error:msg,needsAuth,authKind:kind}}}var azure_exports={};__export(azure_exports,{accountMeta:()=>accountMeta,blobSize:()=>blobSize,clearQueueMessages:()=>clearQueueMessages,collectBlobs:()=>collectBlobs,createContainer:()=>createContainer,createQueue:()=>createQueue,createShare:()=>createShare,createShareDirectory:()=>createShareDirectory,createTable:()=>createTable,credential:()=>credential,deleteBlob:()=>deleteBlob,deleteContainer:()=>deleteContainer,deleteEntity:()=>deleteEntity,deleteQueue:()=>deleteQueue,deleteQueueMessage:()=>deleteQueueMessage,deleteShare:()=>deleteShare,deleteShareItem:()=>deleteShareItem,deleteTable:()=>deleteTable,discoveryFailure:()=>discoveryFailure,downloadBlob:()=>downloadBlob,downloadSasBlob:()=>downloadSasBlob,downloadShareFile:()=>downloadShareFile,ensureAccountMeta:()=>ensureAccountMeta,generateBlobSas:()=>generateBlobSas,generateContainerSas:()=>generateContainerSas,getAccountConfig:()=>getAccountConfig,listBlobDirectory:()=>listBlobDirectory,listBlobs:()=>listBlobs,listContainers:()=>listContainers,listQueues:()=>listQueues,listSasBlobs:()=>listSasBlobs,listShareItems:()=>listShareItems,listShares:()=>listShares,listStorageAccounts:()=>listStorageAccounts,listStorageAccountsForSubs:()=>listStorageAccountsForSubs,listStoragePage:()=>listStoragePage,listSubscriptions:()=>listSubscriptions,listTables:()=>listTables,listTenantSubscriptions:()=>listTenantSubscriptions,listTenants:()=>listTenants,openInStorageExplorer:()=>openInStorageExplorer,peekQueueMessages:()=>peekQueueMessages,queryTableEntities:()=>queryTableEntities,registerAccountContext:()=>registerAccountContext,requireNewResource:()=>requireNewResource,resetTokenCache:()=>resetTokenCache,resolveBlobService:()=>resolveBlobService,resolveQueueService:()=>resolveQueueService,resolveShareService:()=>resolveShareService,resolveTableService:()=>resolveTableService,sendQueueMessage:()=>sendQueueMessage,setBlobTier:()=>setBlobTier,signIn:()=>signIn,storageCredential:()=>storageCredential,storageExplorerDeepLink:()=>storageExplorerDeepLink,storageExplorerInstalled:()=>storageExplorerInstalled,storagePage:()=>storagePage,tokenFor:()=>tokenFor,updateQueueMessage:()=>updateQueueMessage,uploadBlob:()=>uploadBlob,uploadShareFile:()=>uploadShareFile,upsertEntity:()=>upsertEntity,warmup:()=>warmup});import{execFile,spawn,spawnSync}from"node:child_process";import{createHash}from"node:crypto";import{accessSync,constants,readFileSync}from"node:fs";import{cp,lstat,mkdir,mkdtemp,readFile,readdir,rename,rm,stat,writeFile}from"node:fs/promises";import os from"node:os";import path from"node:path";import{fileURLToPath}from"node:url";function resolveStudioRoot(moduleUrl){let directory=path.dirname(fileURLToPath(moduleUrl)),candidates=path.basename(directory)==="src"?[directory,path.dirname(directory)]:[directory];for(let candidate of candidates)for(let metadata of["studio-package.json","package.json"])try{return accessSync(path.join(candidate,metadata)),candidate}catch(error){if(error.code!=="ENOENT")throw error}throw new Error(`Missing canvas package metadata for ${moduleUrl}`)}function resolveStudioBuildInfo(moduleUrl,fallbackVersion="unknown"){let version=fallbackVersion,revision="unknown";try{let manifest;try{manifest=readFileSync(new URL("./studio-package.json",moduleUrl),"utf8")}catch(error){if(error.code!=="ENOENT")throw error;manifest=readFileSync(path.join(resolveStudioRoot(moduleUrl),"package.json"),"utf8")}version=JSON.parse(manifest).version||fallbackVersion}catch{}try{revision=createHash("sha256").update(readFileSync(new URL(moduleUrl))).digest("hex").slice(0,10)}catch{}return{version,revision}}var ICONS={vscode:'',github:'',azure:''},COMMAND_BUTTONS=` - `;var BUILD_INFO=resolveStudioBuildInfo(new URL("./extension.mjs",import.meta.url));var CSS=` + `;function canExecuteCommand(file,osName=os.platform()){try{return accessSync(file,osName==="win32"?constants.F_OK:constants.X_OK),!0}catch{return!1}}var WINDOWS_COMMAND_META_CHARACTERS=/([()\][%!^"`<>&|;, *?])/g;function escapeWindowsCommandMetaCharacters(value){return value.replace(WINDOWS_COMMAND_META_CHARACTERS,"^$1")}function assertWindowsCommandValue(value){let text=String(value);if(/[\r\n]/.test(text))throw new Error("Command arguments contain unsupported Windows command characters.");return text}function escapeWindowsCommand(value){return escapeWindowsCommandMetaCharacters(assertWindowsCommandValue(value))}function quoteWindowsCommandArgument(value){let text=assertWindowsCommandValue(value);return text=text.replace(/(?=(\\+?)?)\1"/g,'$1$1\\"'),text=text.replace(/(?=(\\+?)?)\1$/,"$1$1"),escapeWindowsCommandMetaCharacters(`"${text}"`)}function windowsCommandShellLine(command,args=[]){return`"${[escapeWindowsCommand(command),...args.map(quoteWindowsCommandArgument)].join(" ")}"`}function spawnSpecForLocated(located,args,env,osName){return osName==="win32"&&/\.(?:cmd|bat)$/i.test(located.path)?{file:env.ComSpec||env.COMSPEC||path.win32.join(env.SystemRoot||env.SYSTEMROOT||"C:\\Windows","System32","cmd.exe"),args:["/d","/s","/c",windowsCommandShellLine(located.path,args)],env,located,windowsVerbatimArguments:!0}:{file:located.path,args:[...args],env,located,windowsVerbatimArguments:!1}}var AZURE_CLI_ENV_KEYS=["HOME","USER","LOGNAME","TMPDIR","LANG","LC_ALL","SHELL","USERPROFILE","HOMEDRIVE","HOMEPATH","SystemRoot","SYSTEMROOT","TEMP","TMP","ComSpec","COMSPEC","PATHEXT","AZURE_CONFIG_DIR","XDG_CONFIG_HOME","HTTP_PROXY","HTTPS_PROXY","ALL_PROXY","NO_PROXY","http_proxy","https_proxy","all_proxy","no_proxy","REQUESTS_CA_BUNDLE","CURL_CA_BUNDLE","SSL_CERT_FILE","SSL_CERT_DIR","AZURE_CLI_DISABLE_CONNECTION_VERIFICATION"];function sourcePath(source){return source.PATH||source.Path||source.path||""}function locateAzureCli(source=process.env,osName=os.platform(),exists=candidate=>canExecuteCommand(candidate,osName)){let paths=osName==="win32"?path.win32:path.posix,inherited=sourcePath(source).split(paths.delimiter).filter(Boolean),home=source.HOME||source.USERPROFILE||os.homedir(),known=osName==="win32"?[paths.join(source.ProgramFiles||"C:\\Program Files","Microsoft SDKs","Azure","CLI2","wbin"),paths.join(source["ProgramFiles(x86)"]||"C:\\Program Files (x86)","Microsoft SDKs","Azure","CLI2","wbin")]:["/opt/homebrew/bin","/usr/local/bin","/opt/local/bin",paths.join(home,".local","bin"),paths.join(home,"bin"),"/usr/bin","/bin","/snap/bin"],directories=[...new Set([...inherited,...known])],searched=[];if(source.AZURE_CLI_PATH&&(searched.push(source.AZURE_CLI_PATH),exists(source.AZURE_CLI_PATH)))return{found:!0,path:source.AZURE_CLI_PATH,directory:paths.dirname(source.AZURE_CLI_PATH),searched};for(let directory of directories)for(let name of osName==="win32"?["az.cmd","az.exe","az"]:["az"]){let candidate=paths.join(directory,name);if(searched.push(candidate),exists(candidate))return{found:!0,path:candidate,directory,searched}}return{found:!1,path:"",directory:"",searched}}var AzureCliNotFoundError=class extends Error{constructor(searched=[]){super("Azure CLI executable was not found. Install Azure CLI or set AZURE_CLI_PATH to its absolute path, then retry."),this.name="AzureCliNotFoundError",this.code="AZURE_CLI_NOT_FOUND",this.searched=searched}};function azureCliChildEnv(source=process.env,located=locateAzureCli(source),osName=os.platform()){let paths=osName==="win32"?path.win32:path.posix,env={},copied=new Set;for(let key of AZURE_CLI_ENV_KEYS){let identity=osName==="win32"?key.toLowerCase():key;source[key]!=null&&!copied.has(identity)&&(env[key]=source[key],copied.add(identity))}let inherited=sourcePath(source).split(paths.delimiter).filter(Boolean),fallbacks=osName==="win32"?[paths.join(source.SystemRoot||source.SYSTEMROOT||"C:\\Windows","System32"),source.SystemRoot||source.SYSTEMROOT||"C:\\Windows"]:["/opt/homebrew/bin","/usr/local/bin","/usr/bin","/bin"];return env.PATH=[...new Set([...located.found?[located.directory]:[],...inherited,...fallbacks])].join(paths.delimiter),osName==="win32"&&!env.ComSpec&&!env.COMSPEC&&(env.ComSpec=paths.join(source.SystemRoot||source.SYSTEMROOT||"C:\\Windows","System32","cmd.exe")),env}function azureCliSpawnSpec(args,{env=process.env,osName=os.platform(),locate=locateAzureCli}={}){assertFixtureAuthenticationDenied("az");let located=locate(env,osName);if(!located.found)throw new AzureCliNotFoundError(located.searched);let childEnv=azureCliChildEnv(env,located,osName);return spawnSpecForLocated(located,args,childEnv,osName)}async function runAzureCliJson(args,subscription,{timeout=3e4,maxBuffer=1024*1024,env=process.env,execute=execFileText,locate=locateAzureCli,osName=os.platform(),signal}={}){let full=subscription?[...args,"--subscription",subscription]:[...args],withFlag=full.includes("--only-show-errors")?full:[...full,"--only-show-errors"],{stdout}=await runAzureCliText(withFlag,{env,maxBuffer,timeout,execute,locate,osName,...signal?{signal}:{}}),text=stdout.trim();return text?JSON.parse(text):null}async function runAzureCliText(args,{env=process.env,execute=execFileText,locate=locateAzureCli,osName=os.platform(),...options}={}){let command=azureCliSpawnSpec(args,{env,locate,osName});return execute(command.file,command.args,{maxBuffer:1024*1024,...options,env:command.env,windowsVerbatimArguments:command.windowsVerbatimArguments})}var fixtureAuthCommands=[];function assertFixtureAuthenticationDenied(command,args=[]){if(process.env.FUNCTION_STUDIO_TEST_MODE!=="1")return;let name=path.win32.basename(String(command)).replace(/\.(?:cmd|bat|exe)$/i,"").toLowerCase(),gitRemote=name==="git"&&args.some(arg=>["clone","fetch","pull","push","ls-remote","credential"].includes(arg)),packageInstall=["npm","npx","pip","pip3","uv"].includes(name)&&args.some(arg=>["install","exec","add","run"].includes(arg))||/^python(?:\d+(?:\.\d+)?)?$/.test(name)&&args.includes("pip")&&args.includes("install");if(!(!gitRemote&&!packageInstall&&!["az","azd","gh","azureauth","security","codesign","npx"].includes(name)))throw fixtureAuthCommands.push(name),new Error(`Live authentication command '${name}' is disabled in Hosted Skills fixture mode. Inject a fake service instead.`)}function execFileText(file,args,options={}){return new Promise((resolve,reject)=>{assertFixtureAuthenticationDenied(file,args),execFile(file,args,{maxBuffer:1024*1024,...options},(error,stdout,stderr)=>{if(error){error.stdout=stdout,error.stderr=stderr,reject(error);return}resolve({stdout,stderr})})})}import{BlobServiceClient,StorageSharedKeyCredential,ContainerClient,generateBlobSASQueryParameters,BlobSASPermissions,ContainerSASPermissions,SASProtocol}from"./sdk/blob.mjs";import{QueueServiceClient,StorageSharedKeyCredential as QueueSharedKey}from"./sdk/queue.mjs";import{ShareServiceClient,StorageSharedKeyCredential as FileSharedKey}from"./sdk/file.mjs";import{TableServiceClient,TableClient,AzureNamedKeyCredential}from"./sdk/tables.mjs";import{spawn as spawn2,spawnSync as spawnSync2}from"node:child_process";import{existsSync}from"node:fs";var ARM="https://management.azure.com",API="2023-01-01";scrubHostSessionEnv();var _credentials=new Map;function credential(tenantId=""){return _credentials.has(tenantId)||_credentials.set(tenantId,{async getToken(scopes,options){if(options?.claims)throw new Error("Azure CLI cannot satisfy this claims challenge. Sign in to the account's tenant and retry.");let args=["account","get-access-token","--scope",...Array.isArray(scopes)?scopes:[scopes],"--output","json"];tenantId&&args.push("--tenant",tenantId);let result=await runAzureCliJson(args,void 0,{env:inheritedCliEnvironment()}),expiresOnTimestamp=result?.expires_on?Number(result.expires_on)*1e3:Date.parse(result?.expiresOn);if(!result?.accessToken||!Number.isFinite(expiresOnTimestamp))throw new Error("Azure CLI returned an invalid access token response. Run `az login` and retry.");return{token:result.accessToken,expiresOnTimestamp}}}),_credentials.get(tenantId)}var _tokens=new Map;async function tokenFor(scope,tenantId="",acquire=credential){let key=JSON.stringify([tenantId,scope]),now=Date.now(),hit=_tokens.get(key);if(hit?.token&&hit.exp-12e4>now)return hit.token;if(hit?.inflight)return hit.inflight;let inflight=acquire(tenantId).getToken(scope).then(t=>{if(!t?.token)throw new Error(`Could not acquire a token for ${scope}. Run \`az login\`.`);return _tokens.set(key,{token:t.token,exp:t.expiresOnTimestamp||now+33e5}),t.token}).catch(e=>{throw _tokens.delete(key),e});return _tokens.set(key,{...hit||{},inflight}),inflight}function storageCredential(account){if(!accountMeta(account)&&!knownAccount(account))throw new Error(`Storage account '${account}' has no known subscription or tenant context. Select its subscription before accessing storage.`);return{getToken:async(scopes,options)=>{let{tenantId}=await ensureAccountMeta(account);if(options?.claims)return credential(tenantId).getToken(scopes,options);let scope=Array.isArray(scopes)?scopes[0]:scopes;return{token:await tokenFor(scope,tenantId),expiresOnTimestamp:_tokens.get(JSON.stringify([tenantId,scope]))?.exp??Date.now()+33e5}}}}var _warmed=null;function warmup(){return _warmed||(_warmed=Promise.allSettled([tokenFor("https://management.azure.com/.default"),tokenFor("https://storage.azure.com/.default")]),_warmed)}function resetTokenCache(){_tokens.clear(),_credentials.clear(),svcCache.clear(),queueCache.clear(),tableCache.clear(),shareCache.clear(),_warmed=null}function azMissingMsg(e){let m=String(e&&e.message||e);return/ENOENT/i.test(m)?"Azure CLI (az) was not found on PATH. Install it from aka.ms/azure-cli, then try again, or run `az login` manually.":"Could not start az login: "+m}var _signInInflight=null;function signIn(opts={}){return _signInInflight||(_signInInflight=_doSignIn(opts).finally(()=>{_signInInflight=null}),_signInInflight)}function _doSignIn({tenantId,timeoutMs=18e4,_spawn=spawn2}={}){return new Promise(resolve=>{let isWin=process.platform==="win32",env={...inheritedCliEnvironment(),AZURE_CORE_LOGIN_EXPERIENCE_V2:"off"},child;try{child=_spawn("az",["login","--only-show-errors",...tenantId?["--tenant",tenantId]:[]],{env,windowsHide:!0,shell:isWin,stdio:["ignore","ignore","pipe"]})}catch(e){resolve({ok:!1,error:azMissingMsg(e)});return}let err="";child.stderr&&child.stderr.on("data",d=>{err+=String(d)});let timer=setTimeout(()=>{try{child.kill()}catch{}resolve({ok:!1,error:"Sign-in timed out after 3 minutes. Finish az login in the browser, then click Refresh."})},timeoutMs);child.on("error",e=>{clearTimeout(timer),resolve({ok:!1,error:azMissingMsg(e)})}),child.on("close",code=>{if(clearTimeout(timer),code===0){resetTokenCache(),resolve({ok:!0});return}resolve({ok:!1,error:err.trim()||"az login exited with code "+code})})})}var SE_RESOURCE_TYPE={blob:"Azure.BlobContainer",file:"Azure.FileShare",queue:"Azure.Queue",table:"Azure.Table"};function storageExplorerDeepLink({account,kind,item}={}){let m=account?accountMeta(account):null;if(!m||!m.subscriptionId||!m.resourceGroup)return null;let accountId=`/subscriptions/${m.subscriptionId}/resourceGroups/${m.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${m.name}`,link="storageexplorer://v=1&accountid="+encodeURIComponent(accountId)+"&subscriptionid="+encodeURIComponent(m.subscriptionId),rt=kind?SE_RESOURCE_TYPE[kind]:null;return rt&&item&&(link+="&resourcetype="+encodeURIComponent(rt)+"&resourcename="+encodeURIComponent(item)),link}function storageExplorerExe(){if(process.platform!=="win32")return null;let candidates=[process.env.LOCALAPPDATA&&process.env.LOCALAPPDATA+"\\Programs\\Microsoft Azure Storage Explorer\\StorageExplorer.exe",process.env.ProgramFiles&&process.env.ProgramFiles+"\\Microsoft Azure Storage Explorer\\StorageExplorer.exe",process.env["ProgramFiles(x86)"]&&process.env["ProgramFiles(x86)"]+"\\Microsoft Azure Storage Explorer\\StorageExplorer.exe"].filter(Boolean);for(let p of candidates)try{if(existsSync(p))return p}catch{}return null}var SE_DOWNLOAD="https://aka.ms/storageexplorer",_seInstalled;function storageExplorerInstalled(){if(_seInstalled!==void 0)return _seInstalled;if(storageExplorerExe())return _seInstalled=!0;if(process.platform==="win32"){for(let key of["HKCU\\Software\\Classes\\storageexplorer","HKCR\\storageexplorer"])try{let r=spawnSync2("reg",["query",key],{stdio:"ignore",windowsHide:!0});if(r&&r.status===0)return _seInstalled=!0}catch{}return _seInstalled=!1}return _seInstalled=null}function openBrowser(url){let opts={detached:!0,stdio:"ignore"},file,args;process.platform==="win32"?(file=process.env.ComSpec||"cmd.exe",args=["/c","start","",url]):process.platform==="darwin"?(file="open",args=[url]):(file="xdg-open",args=[url]);try{let c=spawn2(file,args,opts);c.on("error",()=>{}),c.unref&&c.unref()}catch{}}function seMissingMsg(e){let m=String(e&&e.message||e);return/ENOENT/i.test(m)?"Azure Storage Explorer isn't installed. Get the free desktop app at "+SE_DOWNLOAD+".":"Could not launch Azure Storage Explorer: "+m}function openInStorageExplorer({account,kind,item}={}){return new Promise(resolve=>{if(storageExplorerInstalled()===!1){openBrowser(SE_DOWNLOAD),resolve({ok:!1,notInstalled:!0,install:SE_DOWNLOAD});return}let link=storageExplorerDeepLink({account,kind,item}),exe=storageExplorerExe(),opts={detached:!0,stdio:"ignore"},file,args;if(exe)file=exe,args=link?[link]:[];else if(process.platform==="win32"){if(!link){openBrowser(SE_DOWNLOAD),resolve({ok:!1,notInstalled:!0,install:SE_DOWNLOAD});return}file=process.env.ComSpec||"cmd.exe",args=["/c","start","",link]}else process.platform==="darwin"?(file="open",args=link?[link]:["-a","Microsoft Azure Storage Explorer"]):(file="StorageExplorer",args=link?[link]:[]);let onFail=e=>{if(e&&/ENOENT/i.test(String(e.message||e))){openBrowser(SE_DOWNLOAD),resolve({ok:!1,notInstalled:!0,install:SE_DOWNLOAD});return}resolve({ok:!1,error:seMissingMsg(e)})},child;try{child=spawn2(file,args,opts)}catch(e){onFail(e);return}child.on("error",onFail),child.on("spawn",()=>{try{child.unref()}catch{}resolve({ok:!0,deepLink:!!link})})})}async function armToken(tenantId){return tokenFor("https://management.azure.com/.default",tenantId)}async function armGet(path3,tenantId){let res=await fetch(`${ARM}${path3}`,{headers:{Authorization:`Bearer ${await armToken(tenantId)}`}});if(!res.ok){let e=new Error(`ARM GET ${path3} \u2192 HTTP ${res.status}`);throw e.statusCode=res.status,e}return res.json()}async function armPost(path3,tenantId){let res=await fetch(`${ARM}${path3}`,{method:"POST",headers:{Authorization:`Bearer ${await armToken(tenantId)}`}});if(!res.ok){let e=new Error(`ARM POST ${path3} \u2192 HTTP ${res.status}`);throw e.statusCode=res.status,e}return res.json()}async function armGetAll(path3,tenantId){let token=await armToken(tenantId),url=path3.startsWith("http")?path3:`${ARM}${path3}`,items=[];for(;url;){let res=await fetch(url,{headers:{Authorization:`Bearer ${token}`}});if(!res.ok){let e=new Error(`ARM GET ${url} \u2192 HTTP ${res.status}`);throw e.statusCode=res.status,e}let j=await res.json();if(Array.isArray(j.value))for(let v of j.value)items.push(v);url=j.nextLink||j["@odata.nextLink"]||null}return items}async function listSubscriptions({runCli=runAzureCliJson}={}){let value=await runCli(["account","list","--all","--output","json"],void 0,{env:inheritedCliEnvironment()});if(!Array.isArray(value))throw new Error("Azure CLI returned an invalid subscription list.");if(value.some(s=>s.id&&!s.tenantId))throw new Error("Azure CLI returned a subscription without a tenant. Refresh your Azure CLI login.");return value.filter(s=>s.id).map(s=>({subscriptionId:s.id,displayName:s.name,tenantId:s.tenantId,state:s.state}))}async function listTenants({read=armGetAll}={}){return(await read("/tenants?api-version=2022-12-01")).map(t=>({tenantId:t.tenantId,displayName:t.displayName||t.defaultDomain||"Unnamed directory",defaultDomain:t.defaultDomain||"",current:!0}))}async function listTenantSubscriptions(tenantId,{read=armGetAll}={}){if(!tenantId)throw new Error("A tenant is required.");return(await read("/subscriptions?api-version=2022-12-01",tenantId)).map(s=>({subscriptionId:s.subscriptionId,displayName:s.displayName,tenantId,state:s.state}))}function discoveryFailure(error){let text=String(error?.message||error),auth=classifyAuthError(text),status=error?.statusCode||/\bHTTP (\d{3})\b/.exec(text)?.[1];return{error:text,needsAuth:auth.needsAuth,message:humanErrorSummary(text,auth.needsAuth,status)}}var accountIndex=new Map,accountContexts=new Set,accountResolutions=new Map;function registerAccountContext(getContext){return accountContexts.add(getContext),()=>accountContexts.delete(getContext)}function knownAccount(account){for(let getContext of[...accountContexts].reverse()){let{accounts=[],subscriptions=[]}=getContext(),metadata=accounts.find(a=>a.name===account);if(metadata)return{metadata,subscription:subscriptions.find(s=>s.subscriptionId===metadata.subscriptionId)}}return null}function completeAccount(metadata){return!!(metadata?.tenantId&&metadata.subscriptionId&&metadata.resourceGroup)}function rgFromId(id){let m=/\/resourceGroups\/([^/]+)\//i.exec(id||"");return m?m[1]:""}async function mapLimit(items,limit,fn){let results=[],i=0,workers=Array.from({length:Math.min(limit,items.length)},async()=>{for(;i`${e.subscriptionName}: ${e.error}`).join("; "));return accounts}async function listStorageAccountsForSubs(subs,{concurrency=16,readAccounts=armGetAll}={}){let out=[],errors=[];return await mapLimit(subs,concurrency,async sub=>{let accts;try{if(!sub.tenantId)throw new Error("Subscription tenant is unknown. Refresh subscriptions.");accts=await readAccounts(`/subscriptions/${sub.subscriptionId}/providers/Microsoft.Storage/storageAccounts?api-version=${API}`,sub.tenantId)}catch(e){errors.push({subscriptionName:sub.displayName||sub.subscriptionId,subscriptionId:sub.subscriptionId,tenantId:sub.tenantId,...discoveryFailure(e)});return}for(let a of accts){let acct={name:a.name,subscriptionId:sub.subscriptionId,subscriptionName:sub.displayName,tenantId:sub.tenantId,resourceGroup:rgFromId(a.id),location:a.location||"",sku:a.sku?.name||"",kind:a.kind||""};accountIndex.set(a.name,acct),out.push(acct)}}),out.sort((x,y)=>x.name.localeCompare(y.name)),{accounts:out,errors}}function accountMeta(account){let cached=accountIndex.get(account);if(completeAccount(cached))return cached;let known=knownAccount(account);if(!known)return cached||null;let metadata={...known.metadata,tenantId:known.metadata.tenantId||known.subscription?.tenantId,resourceGroup:known.metadata.resourceGroup||rgFromId(known.metadata.id)};return completeAccount(metadata)?(accountIndex.set(account,metadata),metadata):cached||null}async function ensureAccountMeta(account,{readAccounts=armGetAll}={}){let cached=accountMeta(account);if(completeAccount(cached))return cached;if(accountResolutions.has(account))return accountResolutions.get(account);let known=knownAccount(account),subscriptionId=known?.metadata.subscriptionId,tenantId=known?.metadata.tenantId||known?.subscription?.tenantId;if(!subscriptionId||!tenantId)throw new Error(`Storage account '${account}' has no known subscription or tenant context. Select its subscription before accessing storage.`);let pending=(async()=>{let result=await listStorageAccountsForSubs([{...known.subscription,subscriptionId,tenantId}],{readAccounts});if(result.errors.length)throw new Error(`Could not resolve storage account '${account}': ${result.errors[0].error}`);let resolved=result.accounts.find(a=>a.name===account);if(!completeAccount(resolved))throw new Error(`Storage account '${account}' was not returned with complete metadata in subscription '${subscriptionId}'. Refresh that subscription's accounts.`);return resolved})();accountResolutions.set(account,pending);try{return await pending}finally{accountResolutions.get(account)===pending&&accountResolutions.delete(account)}}async function getAccountConfig(account){try{let m=await ensureAccountMeta(account),j=await armGet(`/subscriptions/${m.subscriptionId}/resourceGroups/${m.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${account}?api-version=${API}`,m.tenantId),p=j.properties||{};return{ok:!0,exists:!0,allowSharedKeyAccess:p.allowSharedKeyAccess!==!1,publicNetworkAccess:p.publicNetworkAccess||"Enabled",defaultAction:p.networkAcls?.defaultAction||"Allow",provisioningState:p.provisioningState||"",sku:j.sku?.name||"",location:j.location||""}}catch(err){return err.statusCode===404?{ok:!0,exists:!1}:{ok:!1,error:String(err?.message||err)}}}async function fetchAccountKey(account){let m=await ensureAccountMeta(account),key=(await armPost(`/subscriptions/${m.subscriptionId}/resourceGroups/${m.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${account}/listKeys?api-version=${API}`,m.tenantId))?.keys?.[0]?.value;if(!key)throw new Error("listKeys returned no key");return key}var svcCache=new Map;function accountUrl(account){return`https://${account}.blob.core.windows.net`}async function probe(svc){await svc.listContainers().byPage({maxPageSize:1})[Symbol.asyncIterator]().next()}function isAuthError(e){let s=e?.statusCode??e?.response?.status;if(s===401||s===403)return!0;let txt=`${e?.code||e?.name||""} ${e?.message||""}`;return/Authoriz|Authentic|KeyBasedAuthenticationNotPermitted|InvalidAuthenticationInfo|NoAuthenticationInformation/i.test(txt)}async function buildEntry(account){await ensureAccountMeta(account);try{let key=await fetchAccountKey(account);return{svc:new BlobServiceClient(accountUrl(account),new StorageSharedKeyCredential(account,key)),mode:"sharedKey",key}}catch{return{svc:new BlobServiceClient(accountUrl(account),storageCredential(account)),mode:"aad",key:null}}}async function withBlob(account,fn){let entry=svcCache.get(account);entry||(entry=await buildEntry(account),svcCache.set(account,entry));try{return await fn(entry.svc)}catch(e){if(entry.mode==="sharedKey"&&isAuthError(e)){let aad={svc:new BlobServiceClient(accountUrl(account),storageCredential(account)),mode:"aad",key:null};return svcCache.set(account,aad),await fn(aad.svc)}throw e}}async function resolveBlobService(account){if(await ensureAccountMeta(account),svcCache.has(account))return svcCache.get(account);let keyErr=null,aadErr=null;try{let key=await fetchAccountKey(account),cred=new StorageSharedKeyCredential(account,key),svc=new BlobServiceClient(accountUrl(account),cred);await probe(svc);let entry={svc,mode:"sharedKey",key};return svcCache.set(account,entry),entry}catch(err){keyErr=err}try{let svc=new BlobServiceClient(accountUrl(account),storageCredential(account));await probe(svc);let entry={svc,mode:"aad",key:null};return svcCache.set(account,entry),entry}catch(err){aadErr=err}throw new Error(`Could not access '${account}': key fallback ${keyErr?.code||keyErr?.message||"n/a"}; AAD ${aadErr?.code||aadErr?.message||"n/a"}.`)}async function listContainers(account){return withBlob(account,async svc=>{let out=[];for await(let c of svc.listContainers())out.push({name:c.name,lastModified:c.properties?.lastModified||null});return out})}function mapBlob(b){return{name:b.name,size:b.properties?.contentLength??0,lastModified:b.properties?.lastModified?new Date(b.properties.lastModified).toISOString():null,contentType:b.properties?.contentType||"",accessTier:b.properties?.accessTier||"",blobType:b.properties?.blobType||"",tags:b.tags||null}}async function collectBlobs(cc,max,{prefix,onProgress,requireTags=!1}={}){let out=[],truncated=!1,read=async includeTags=>{for await(let b of cc.listBlobsFlat({prefix,includeTags})){if(out.length>=max){truncated=!0;break}out.push(mapBlob(b)),out.length%1e3===0&&onProgress?.(out.length)}onProgress?.(out.length)};try{await read(!0)}catch(error){if(error?.statusCode!==403||requireTags)throw error;out.length=0,truncated=!1,onProgress?.(0),await read(!1)}return out.truncated=truncated,out.limit=max,out}async function listBlobs(account,container,{max=5e3,complete=!1,...options}={}){return withBlob(account,svc=>collectBlobs(svc.getContainerClient(container),complete?1/0:max,options))}async function listBlobDirectory(account,container,{prefix="",max=5e3,list}={}){let collect=async(cc,includeTags)=>{let blobs=[],prefixes=[],truncated=!1;for await(let item of cc.listBlobsByHierarchy("/",{prefix,includeTags})){if(blobs.length+prefixes.length>=max){truncated=!0;break}item.kind==="prefix"?prefixes.push(item.name):blobs.push(mapBlob(item))}return{blobs,prefixes,truncated,limit:max}},read=async cc=>{try{return await collect(cc,!0)}catch(error){if(error?.statusCode!==403)throw error;return collect(cc,!1)}};return list?read({listBlobsByHierarchy:list}):withBlob(account,svc=>read(svc.getContainerClient(container)))}async function storagePage(iterator,continuationToken,maxPageSize=100){let result=await iterator.byPage({continuationToken:continuationToken||void 0,maxPageSize}).next();return result.done?{page:[],nextToken:null}:{page:result.value,nextToken:result.value.continuationToken||null}}async function listStoragePage({account,kind,resource,dir="",prefix="",token,sasUrl,resolve,tableClient}){let read=iterator=>storagePage(iterator,token);if(kind==="blob"||kind==="sas"){let fromClient=async svc=>{let cc=kind==="sas"?svc:resource?svc.getContainerClient(resource):null;if(!cc){let{page,nextToken}=await read(svc.listContainers());return{items:(page.containerItems||[]).map(c=>({name:c.name,lastModified:c.properties?.lastModified||null})),nextToken}}let result;try{result=await read(kind==="sas"?cc.listBlobsFlat({includeTags:!0}):cc.listBlobsByHierarchy("/",{prefix,includeTags:!0}))}catch(error){if(error?.statusCode!==403)throw error;result=await read(kind==="sas"?cc.listBlobsFlat():cc.listBlobsByHierarchy("/",{prefix}))}return{items:(result.page.segment?.blobItems||[]).map(mapBlob),prefixes:(result.page.segment?.blobPrefixes||[]).map(p=>p.name),nextToken:result.nextToken}};return kind==="sas"?fromClient(new ContainerClient(sasUrl)):resolve?fromClient((await resolve(account)).svc):withBlob(account,fromClient)}if(kind==="file"){let{svc}=await(resolve||resolveShareService)(account),{page,nextToken}=await read(resource?svc.getShareClient(resource).getDirectoryClient(dir).listFilesAndDirectories({includeTimestamps:!0}):svc.listShares());return{items:resource?[...(page.segment?.directoryItems||[]).map(x=>({name:x.name,kind:"directory",size:null,lastModified:null})),...(page.segment?.fileItems||[]).map(x=>({name:x.name,kind:"file",size:x.properties?.contentLength??0,lastModified:x.properties?.lastModified?new Date(x.properties.lastModified).toISOString():null}))]:(page.shareItems||[]).map(x=>({name:x.name,quotaGiB:x.properties?.quota??null})),nextToken}}if(kind==="queue"){let{svc}=await(resolve||resolveQueueService)(account),{page,nextToken}=await read(svc.listQueues());return{items:(page.queueItems||[]).map(x=>({name:x.name,approximateMessagesCount:null})),nextToken}}if(kind==="table"){let{svc,cred}=await(resolve||resolveTableService)(account),{page,nextToken}=await read(resource?(tableClient||new TableClient(tableUrl(account),resource,cred)).listEntities():svc.listTables());if(!resource)return{items:page.map(x=>({name:x.name})),nextToken};let columns=new Set(["partitionKey","rowKey","timestamp"]);return{entities:{rows:page.map(ent=>{let row={};for(let[key,value]of Object.entries(ent))key.startsWith("odata.")||key==="etag"||(columns.add(key),row[key]=value&&typeof value.toISOString=="function"?value.toISOString():value);return row}),columns:[...columns]},nextToken}}throw new Error("This storage view does not support continuation pages.")}async function createContainer(account,container,{resolve=resolveBlobService}={}){let{svc}=await resolve(account);return await svc.getContainerClient(container).create(),{ok:!0,container}}async function deleteContainer(account,container){let{svc}=await resolveBlobService(account);return await svc.getContainerClient(container).delete(),{ok:!0}}async function deleteBlob(account,container,name){let{svc}=await resolveBlobService(account);return await svc.getContainerClient(container).getBlockBlobClient(name).delete(),{ok:!0}}async function setBlobTier(account,container,name,tier){let{svc}=await resolveBlobService(account);return await svc.getContainerClient(container).getBlockBlobClient(name).setAccessTier(tier),{ok:!0}}async function uploadBlob(account,container,name,buffer,contentType){let{svc}=await resolveBlobService(account),cc=svc.getContainerClient(container);return await cc.createIfNotExists(),await cc.getBlockBlobClient(name).uploadData(buffer,contentType?{blobHTTPHeaders:{blobContentType:contentType}}:void 0),{ok:!0,name,size:buffer.length}}async function downloadBlob(account,container,name,{maxBytes=null}={}){let{svc}=await resolveBlobService(account),bc=svc.getContainerClient(container).getBlockBlobClient(name);if(Number.isFinite(maxBytes)&&maxBytes>0){let resp=await bc.download(0,maxBytes);return await streamToBuffer(resp.readableStreamBody)}return await bc.downloadToBuffer()}async function blobSize(account,container,name){let{svc}=await resolveBlobService(account),props=await svc.getContainerClient(container).getBlockBlobClient(name).getProperties();return Number(props.contentLength)||0}async function streamToBuffer(stream){let chunks=[];for await(let chunk of stream)chunks.push(Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk));return Buffer.concat(chunks)}async function generateBlobSas(account,container,name,{minutes=60,permissions="r"}={}){let entry=await resolveBlobService(account),perms=BlobSASPermissions.parse(permissions||"r"),startsOn=new Date(Date.now()-5*6e4),expiresOn=new Date(Date.now()+Math.max(1,minutes)*6e4),common={containerName:container,blobName:name,permissions:perms,startsOn,expiresOn,protocol:SASProtocol.Https},sas,mode;if(entry.mode==="sharedKey"&&entry.key){let cred=new StorageSharedKeyCredential(account,entry.key);sas=generateBlobSASQueryParameters(common,cred).toString(),mode="account-key"}else{let udk=await entry.svc.getUserDelegationKey(startsOn,expiresOn);sas=generateBlobSASQueryParameters(common,udk,account).toString(),mode="user-delegation"}let encName=name.split("/").map(encodeURIComponent).join("/");return{url:`${accountUrl(account)}/${container}/${encName}?${sas}`,expiresOn:expiresOn.toISOString(),permissions:permissions||"r",mode}}async function generateContainerSas(account,container,{minutes=1440,permissions="rl"}={}){let entry=await resolveBlobService(account),perms=ContainerSASPermissions.parse(permissions||"rl"),startsOn=new Date(Date.now()-5*6e4),expiresOn=new Date(Date.now()+Math.max(1,minutes)*6e4),common={containerName:container,permissions:perms,startsOn,expiresOn,protocol:SASProtocol.Https},sas,mode;if(entry.mode==="sharedKey"&&entry.key){let cred=new StorageSharedKeyCredential(account,entry.key);sas=generateBlobSASQueryParameters(common,cred).toString(),mode="account-key"}else{let udk=await entry.svc.getUserDelegationKey(startsOn,expiresOn);sas=generateBlobSASQueryParameters(common,udk,account).toString(),mode="user-delegation"}return{url:`${accountUrl(account)}/${container}?${sas}`,expiresOn:expiresOn.toISOString(),permissions:permissions||"rl",mode}}async function listSasBlobs(sasUrl,{max=5e3,complete=!1,...options}={}){let cc=new ContainerClient(sasUrl);return collectBlobs(cc,complete?1/0:max,options)}async function downloadSasBlob(sasUrl,name){return await new ContainerClient(sasUrl).getBlockBlobClient(name).downloadToBuffer()}var queueCache=new Map;function queueUrl(account){return`https://${account}.queue.core.windows.net`}async function resolveQueueService(account){if(await ensureAccountMeta(account),queueCache.has(account))return queueCache.get(account);let keyErr=null;try{let key=await fetchAccountKey(account),svc=new QueueServiceClient(queueUrl(account),new QueueSharedKey(account,key));await svc.listQueues().byPage({maxPageSize:1})[Symbol.asyncIterator]().next();let e={svc,mode:"sharedKey"};return queueCache.set(account,e),e}catch(err){keyErr=err}try{let svc=new QueueServiceClient(queueUrl(account),storageCredential(account));await svc.listQueues().byPage({maxPageSize:1})[Symbol.asyncIterator]().next();let e={svc,mode:"aad"};return queueCache.set(account,e),e}catch(err){throw new Error(`Could not access queues for '${account}': key ${keyErr?.code||keyErr?.message||"n/a"}; AAD ${err?.code||err?.message||"n/a"}.`)}}async function listQueues(account){let{svc}=await resolveQueueService(account),out=[];for await(let q of svc.listQueues()){let count=null;try{count=(await svc.getQueueClient(q.name).getProperties()).approximateMessagesCount??null}catch{}out.push({name:q.name,approximateMessagesCount:count})}return out}async function peekQueueMessages(account,queue,{max=32}={}){let{svc}=await resolveQueueService(account);return((await svc.getQueueClient(queue).peekMessages({numberOfMessages:Math.min(max,32)})).peekedMessageItems||[]).map(m=>({messageId:m.messageId,messageText:m.messageText,insertedOn:m.insertedOn?new Date(m.insertedOn).toISOString():null,dequeueCount:m.dequeueCount??0}))}function requireNewResource(items,name,kind){if(items.some(item=>item.name.toLowerCase()===name.toLowerCase()))throw new Error(`A ${kind} named "${name}" already exists. Choose a different name.`)}async function createQueue(account,queue){requireNewResource(await listQueues(account),queue,"queue");let{svc}=await resolveQueueService(account);return await svc.getQueueClient(queue).create(),{ok:!0}}async function deleteQueue(account,queue){let{svc}=await resolveQueueService(account);return await svc.getQueueClient(queue).delete(),{ok:!0}}async function sendQueueMessage(account,queue,text){let{svc}=await resolveQueueService(account);return{ok:!0,messageId:(await svc.getQueueClient(queue).sendMessage(String(text??""))).messageId}}async function clearQueueMessages(account,queue){let{svc}=await resolveQueueService(account);return await svc.getQueueClient(queue).clearMessages(),{ok:!0}}async function withReceivedMessage(account,queue,messageId,act){let{svc}=await resolveQueueService(account),qc=svc.getQueueClient(queue),msgs=(await qc.receiveMessages({numberOfMessages:32,visibilityTimeout:30})).receivedMessageItems||[],target=msgs.find(m=>m.messageId===messageId);for(let m of msgs)if(m.messageId!==messageId)try{await qc.updateMessage(m.messageId,m.popReceipt,void 0,0)}catch{}if(!target)throw new Error("That message isn't among the first 32 in the queue, so it can't be changed directly.");return await act(qc,target),{ok:!0}}async function updateQueueMessage(account,queue,messageId,text){return withReceivedMessage(account,queue,messageId,(qc,target)=>qc.updateMessage(messageId,target.popReceipt,String(text??""),0))}async function deleteQueueMessage(account,queue,messageId){return withReceivedMessage(account,queue,messageId,(qc,target)=>qc.deleteMessage(messageId,target.popReceipt))}var tableCache=new Map;function tableUrl(account){return`https://${account}.table.core.windows.net`}async function resolveTableService(account){if(await ensureAccountMeta(account),tableCache.has(account))return tableCache.get(account);let keyErr=null;try{let key=await fetchAccountKey(account),cred=new AzureNamedKeyCredential(account,key),svc=new TableServiceClient(tableUrl(account),cred);await svc.listTables().byPage({maxPageSize:1})[Symbol.asyncIterator]().next();let e={svc,mode:"sharedKey",cred};return tableCache.set(account,e),e}catch(err){keyErr=err}try{let cred=storageCredential(account),svc=new TableServiceClient(tableUrl(account),cred);await svc.listTables().byPage({maxPageSize:1})[Symbol.asyncIterator]().next();let e={svc,mode:"aad",cred};return tableCache.set(account,e),e}catch(err){throw new Error(`Could not access tables for '${account}': key ${keyErr?.code||keyErr?.message||"n/a"}; AAD ${err?.code||err?.message||"n/a"}.`)}}async function listTables(account){let{svc}=await resolveTableService(account),out=[];for await(let t of svc.listTables())out.push({name:t.name});return out}async function queryTableEntities(account,table,{max=100}={}){let{cred}=await resolveTableService(account),tc=new TableClient(tableUrl(account),table,cred),rows=[],cols=new Set(["partitionKey","rowKey","timestamp"]);for await(let ent of tc.listEntities()){let row={};for(let k of Object.keys(ent)){if(k.startsWith("odata.")||k==="etag")continue;cols.add(k);let v=ent[k];row[k]=v&&typeof v=="object"&&typeof v.toISOString=="function"?v.toISOString():v}if(rows.push(row),rows.length>=max)break}return{columns:Array.from(cols),rows}}async function createTable(account,table){requireNewResource(await listTables(account),table,"table");let{svc}=await resolveTableService(account);return await svc.createTable(table),{ok:!0}}async function deleteTable(account,table){let{svc}=await resolveTableService(account);return await svc.deleteTable(table),{ok:!0}}async function upsertEntity(account,table,entity){if(!entity||!entity.partitionKey||!entity.rowKey)throw new Error("entity requires partitionKey and rowKey");let{cred}=await resolveTableService(account);return await new TableClient(tableUrl(account),table,cred).upsertEntity(entity,"Merge"),{ok:!0}}async function deleteEntity(account,table,partitionKey,rowKey){let{cred}=await resolveTableService(account);return await new TableClient(tableUrl(account),table,cred).deleteEntity(partitionKey,rowKey),{ok:!0}}var shareCache=new Map;function fileUrl(account){return`https://${account}.file.core.windows.net`}async function resolveShareService(account){if(await ensureAccountMeta(account),shareCache.has(account))return shareCache.get(account);let keyErr=null;try{let key=await fetchAccountKey(account),svc=new ShareServiceClient(fileUrl(account),new FileSharedKey(account,key));await svc.listShares().byPage({maxPageSize:1})[Symbol.asyncIterator]().next();let e={svc,mode:"sharedKey"};return shareCache.set(account,e),e}catch(err){keyErr=err}try{let svc=new ShareServiceClient(fileUrl(account),storageCredential(account),{fileRequestIntent:"backup"});await svc.listShares().byPage({maxPageSize:1})[Symbol.asyncIterator]().next();let e={svc,mode:"aad"};return shareCache.set(account,e),e}catch(err){throw new Error(`Could not access file shares for '${account}': key ${keyErr?.code||keyErr?.message||"n/a"}; AAD ${err?.code||err?.message||"n/a"}.`)}}async function listShares(account){let{svc}=await resolveShareService(account),out=[];for await(let s of svc.listShares())out.push({name:s.name,quotaGiB:s.properties?.quota??null});return out}async function listShareItems(account,share,dir="",{resolve=resolveShareService}={}){let{svc}=await resolve(account),dc=svc.getShareClient(share).getDirectoryClient(dir||""),out=[];for await(let item of dc.listFilesAndDirectories({includeTimestamps:!0}))out.push({name:item.name,kind:item.kind,size:item.kind==="file"?item.properties?.contentLength??0:null,lastModified:item.kind==="file"&&item.properties?.lastModified?new Date(item.properties.lastModified).toISOString():null});return out}async function createShare(account,share){requireNewResource(await listShares(account),share,"file share");let{svc}=await resolveShareService(account);return await svc.getShareClient(share).create(),{ok:!0}}async function deleteShare(account,share){let{svc}=await resolveShareService(account);return await svc.getShareClient(share).delete(),{ok:!0}}async function createShareDirectory(account,share,dir){let{svc}=await resolveShareService(account);return await svc.getShareClient(share).getDirectoryClient(dir).create(),{ok:!0}}async function uploadShareFile(account,share,name,buffer,dir="",{resolve=resolveShareService}={}){let{svc}=await resolve(account),fc=svc.getShareClient(share).getDirectoryClient(dir).getFileClient(name);return await fc.create(buffer.length),buffer.length&&await fc.uploadData(buffer),{ok:!0,size:buffer.length}}async function downloadShareFile(account,share,name,dir="",{resolve=resolveShareService}={}){let{svc}=await resolve(account);return await svc.getShareClient(share).getDirectoryClient(dir).getFileClient(name).downloadToBuffer()}async function deleteShareItem(account,share,name,kind,dir="",{resolve=resolveShareService}={}){let{svc}=await resolve(account),sc=svc.getShareClient(share);return kind==="directory"?await sc.getDirectoryClient(dir?`${dir}/${name}`:name).delete():await sc.getDirectoryClient(dir).getFileClient(name).delete(),{ok:!0}}function fmtBytes(n){if(n=Number(n)||0,n<1024)return`${n} B`;let u=["KB","MB","GB","TB","PB"],i=-1;do n/=1024,i++;while(n>=1024&&i=10?0:1)} ${u[i]}`}function globToRe(glob){let body=String(glob).replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*").replace(/\?/g,".");return new RegExp("^"+body+"$","i")}function typeFamily(contentType,name){let ct=String(contentType||"").toLowerCase(),ext=String(name||"").toLowerCase().split(".").pop();return/^image\//.test(ct)||/^(png|jpe?g|gif|webp|bmp|svg|tiff?)$/.test(ext)?"image":/^video\//.test(ct)||/^(mp4|mov|avi|mkv|webm)$/.test(ext)?"video":/^audio\//.test(ct)||/^(mp3|wav|flac|ogg|m4a)$/.test(ext)?"audio":/json|xml|csv|text\/|javascript|yaml/.test(ct)||/^(json|xml|csv|txt|log|md|yaml|yml|js|ts|html?|css)$/.test(ext)?"text/data":/pdf|word|excel|powerpoint|officedocument|msword/.test(ct)||/^(pdf|docx?|xlsx?|pptx?)$/.test(ext)?"document":/zip|gzip|tar|compress|x-7z|rar/.test(ct)||/^(zip|gz|tar|7z|rar|bz2)$/.test(ext)?"archive":/octet-stream/.test(ct)||/^(bin|dat|iso|vhd|img)$/.test(ext)?"binary":ct.split("/")[0]||"other"}function applyBlobFilter(blobs,c={}){let list=Array.isArray(blobs)?blobs:[],nowMs=c.now?Date.parse(c.now):Date.now(),re=c.namePattern&&String(c.namePattern).trim()?globToRe(String(c.namePattern).trim()):null,prefix=c.prefix!=null&&String(c.prefix)!==""?String(c.prefix).toLowerCase():null,suffix=c.suffix!=null&&String(c.suffix)!==""?String(c.suffix).toLowerCase():null,contains=c.contains!=null&&String(c.contains)!==""?String(c.contains).toLowerCase():null,min=c.minSize!=null&&c.minSize!==""?Number(c.minSize):null,max=c.maxSize!=null&&c.maxSize!==""?Number(c.maxSize):null,older=c.olderThanDays!=null&&c.olderThanDays!==""?Number(c.olderThanDays):null,newer=c.newerThanDays!=null&&c.newerThanDays!==""?Number(c.newerThanDays):null,type=c.type&&String(c.type).trim()&&c.type!=="all"?String(c.type).trim():null,tiers=normList(c.tiers).map(t=>t.toLowerCase()),blobTypes=normList(c.blobTypes).map(t=>t.toLowerCase()),tagKeys=normList(c.tagKeys),tags=c.tags&&typeof c.tags=="object"?c.tags:null,DAY=864e5;return list.filter(b=>{let nm=String(b.name||""),nml=nm.toLowerCase();if(re&&!re.test(nm)||prefix&&!nml.startsWith(prefix)||suffix&&!nml.endsWith(suffix)||contains&&nml.indexOf(contains)===-1)return!1;let sz=Number(b.size)||0;if(min!=null&&szmax)return!1;if((older!=null||newer!=null)&&b.lastModified){let ageMs=nowMs-Date.parse(b.lastModified);if(older!=null&&!(ageMs>older*DAY)||newer!=null&&!(ageMs<=newer*DAY))return!1}else if(older!=null||newer!=null)return!1;if(type&&typeFamily(b.contentType,b.name)!==type||tiers.length&&tiers.indexOf(String(b.accessTier||"").toLowerCase())===-1||blobTypes.length&&blobTypes.indexOf(String(b.blobType||"").toLowerCase())===-1)return!1;if(tagKeys.length){let bt=b.tags||{};for(let k of tagKeys)if(!(k in bt))return!1}if(tags){let bt=b.tags||{};for(let k of Object.keys(tags))if(String(bt[k])!==String(tags[k]))return!1}return!0})}function normList(v){return v==null||v===""?[]:Array.isArray(v)?v.map(x=>String(x).trim()).filter(Boolean):String(v).split(",").map(x=>x.trim()).filter(Boolean)}function computeBlobStats(blobs){let list=Array.isArray(blobs)?blobs:[],n=list.length,sizes=list.map(b=>Number(b.size)||0),total=sizes.reduce((a,s)=>a+s,0),max=n?Math.max(...sizes):0,avg=n?Math.round(total/n):0,buckets=[{label:"0\u20131 KB",lo:0,hi:1024},{label:"1 KB\u20131 MB",lo:1024,hi:1048576},{label:"1\u2013100 MB",lo:1048576,hi:104857600},{label:"100 MB\u20131 GB",lo:104857600,hi:1073741824},{label:">1 GB",lo:1073741824,hi:1/0}].map(bk=>({label:bk.label,n:sizes.filter(s=>s>=bk.lo&&s({label,n:v.n,bytes:v.bytes})).sort((a,b)=>b.bytes-a.bytes),byTierMap=new Map;for(let b of list){let tier=String(b.accessTier||"").trim()||"\u2014",cur=byTierMap.get(tier)||{n:0,bytes:0};cur.n++,cur.bytes+=Number(b.size)||0,byTierMap.set(tier,cur)}let byTier=[...byTierMap.entries()].map(([label,v])=>({label,n:v.n,bytes:v.bytes})).sort((a,b)=>b.bytes-a.bytes),hasTier=byTier.some(t=>t.label!=="\u2014"),byExtMap=new Map;for(let b of list){let parts=String(b.name||"").toLowerCase().split("."),ext=parts.length>1?"."+parts.pop():"(none)",cur=byExtMap.get(ext)||{n:0,bytes:0};cur.n++,cur.bytes+=Number(b.size)||0,byExtMap.set(ext,cur)}let byExt=[...byExtMap.entries()].map(([label,v])=>({label,n:v.n,bytes:v.bytes})).sort((a,b)=>b.bytes-a.bytes).slice(0,8),topLargest=[...list].sort((a,b)=>(Number(b.size)||0)-(Number(a.size)||0)).slice(0,10).map(b=>({name:b.name,size:Number(b.size)||0,sizeh:fmtBytes(b.size),lastModified:b.lastModified||null}));return{count:n,totalBytes:total,totalh:fmtBytes(total),avgBytes:avg,avgh:fmtBytes(avg),maxBytes:max,maxh:fmtBytes(max),buckets,byType,byTier:hasTier?byTier:[],byExt,topLargest}}function filterAndStat(blobs,criteria={}){let filtered=applyBlobFilter(blobs,criteria);return{filtered,stats:computeBlobStats(filtered),criteria}}var OVERWRITE={overwrite:"true",skip:"false","if-newer":"ifSourceNewer"};function resolutionFlag(resolution){return OVERWRITE[resolution]||"true"}function planTransfer(sourceBlobs,destBlobs,resolution="overwrite"){let src=Array.isArray(sourceBlobs)?sourceBlobs:[],dstByName=new Map((Array.isArray(destBlobs)?destBlobs:[]).map(b=>[b.name,b])),conflicts=[],newFiles=[];for(let b of src)dstByName.has(b.name)?conflicts.push(b):newFiles.push(b);let toTransfer;if(resolution==="skip")toTransfer=newFiles.slice();else if(resolution==="if-newer"){let newerConflicts=conflicts.filter(b=>{let d=dstByName.get(b.name),s=Date.parse(b.lastModified||""),dm=Date.parse(d?.lastModified||"");return Number.isFinite(s)&&Number.isFinite(dm)?s>dm:!0});toTransfer=newFiles.concat(newerConflicts)}else toTransfer=src.slice();let bytes=toTransfer.reduce((a,b)=>a+(Number(b.size)||0),0),skipped=src.length-toTransfer.length;return{resolution,sourceCount:src.length,destCount:dstByName.size,conflictCount:conflicts.length,newCount:newFiles.length,transferCount:toTransfer.length,skippedCount:skipped,transferBytes:bytes,transferBytesh:fmtBytes(bytes),conflicts:conflicts.slice(0,100).map(b=>({name:b.name,size:Number(b.size)||0,sizeh:fmtBytes(b.size),lastModified:b.lastModified||null})),toTransfer:toTransfer.map(b=>b.name)}}function azcopyCommand({sourceUrl,destUrl,resolution="overwrite",includeNames=null,allSource=!1}={}){let q=s=>`"${String(s||"").replace(/[\\"$`]/g,"\\$&")}"`,flags=[`--overwrite=${resolutionFlag(resolution)}`];if(allSource)flags.push("--recursive");else if(Array.isArray(includeNames)){if(!includeNames.length)return{command:null,needsLogin:!1,empty:!0};flags.push(`--include-path ${q(includeNames.join(";"))}`),flags.push("--recursive")}else flags.push("--recursive");let needsLogin=!/[?&]sig=/.test(String(sourceUrl||""))||!/[?&]sig=/.test(String(destUrl||""));return{command:`azcopy copy ${q(sourceUrl)} ${q(destUrl)} ${flags.join(" ")}`,needsLogin}}function sniffText(bytes){let buf=bytes instanceof Uint8Array?bytes:new Uint8Array(bytes||[]);if(!buf.length)return{isText:!0,text:""};let control=0;for(let i=0;i13&&c<32||c===127)&&control++}if(control/buf.length>.1)return{isText:!1,text:null};try{let text=new TextDecoder("utf-8",{fatal:!1}).decode(buf);return(text.match(/\uFFFD/g)||[]).length/Math.max(text.length,1)>.1?{isText:!1,text:null}:{isText:!0,text}}catch{return{isText:!1,text:null}}}var MAGIC=[{off:0,hex:"67 6C 54 46",format:"glTF binary (GLB) 3D model",category:"3D model"},{off:0,hex:"89 50 4E 47 0D 0A 1A 0A",format:"PNG image",category:"image"},{off:0,hex:"FF D8 FF",format:"JPEG image",category:"image"},{off:0,hex:"47 49 46 38",format:"GIF image",category:"image"},{off:0,hex:"42 4D",format:"BMP image",category:"image"},{off:0,hex:"49 49 2A 00",format:"TIFF image (little-endian)",category:"image"},{off:0,hex:"4D 4D 00 2A",format:"TIFF image (big-endian)",category:"image"},{off:0,hex:"00 00 01 00",format:"ICO icon",category:"image"},{off:0,hex:"25 50 44 46",format:"PDF document",category:"document"},{off:0,hex:"D0 CF 11 E0 A1 B1 1A E1",format:"Legacy MS Office (OLE2: .doc/.xls/.ppt)",category:"document"},{off:0,hex:"50 4B 03 04",format:"ZIP archive (or OOXML .docx/.xlsx/.pptx, JAR, APK)",category:"archive"},{off:0,hex:"50 4B 05 06",format:"ZIP archive (empty)",category:"archive"},{off:0,hex:"1F 8B",format:"gzip archive",category:"archive"},{off:0,hex:"42 5A 68",format:"bzip2 archive",category:"archive"},{off:0,hex:"37 7A BC AF 27 1C",format:"7-Zip archive",category:"archive"},{off:0,hex:"52 61 72 21 1A 07",format:"RAR archive",category:"archive"},{off:0,hex:"FD 37 7A 58 5A 00",format:"XZ archive",category:"archive"},{off:257,hex:"75 73 74 61 72",format:"TAR archive",category:"archive"},{off:0,hex:"7F 45 4C 46",format:"ELF executable / shared object",category:"executable"},{off:0,hex:"4D 5A",format:"Windows PE executable (.exe/.dll)",category:"executable"},{off:0,hex:"CA FE BA BE",format:"Java class file",category:"executable"},{off:0,hex:"00 61 73 6D",format:"WebAssembly (WASM) module",category:"executable"},{off:0,hex:"4F 67 67 53",format:"Ogg media",category:"audio"},{off:0,hex:"49 44 33",format:"MP3 audio (ID3)",category:"audio"},{off:0,hex:"66 4C 61 43",format:"FLAC audio",category:"audio"},{off:0,hex:"53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 00",format:"SQLite database",category:"database"},{off:0,hex:"50 41 52 31",format:"Apache Parquet",category:"data"},{off:0,hex:"4F 62 6A 01",format:"Apache Avro",category:"data"}],EXT_FORMAT={glb:["glTF binary (GLB) 3D model","3D model"],gltf:["glTF 3D model","3D model"],obj:["Wavefront OBJ 3D model","3D model"],fbx:["Autodesk FBX 3D model","3D model"],stl:["STL 3D model","3D model"],usdz:["USDZ 3D scene","3D model"],ply:["PLY 3D model","3D model"],png:["PNG image","image"],jpg:["JPEG image","image"],jpeg:["JPEG image","image"],gif:["GIF image","image"],webp:["WebP image","image"],bmp:["BMP image","image"],tiff:["TIFF image","image"],ico:["ICO icon","image"],heic:["HEIC image","image"],avif:["AVIF image","image"],svg:["SVG vector image","image"],psd:["Photoshop document","image"],pdf:["PDF document","document"],docx:["Word document (OOXML)","document"],xlsx:["Excel workbook (OOXML)","document"],pptx:["PowerPoint deck (OOXML)","document"],doc:["Legacy Word document","document"],xls:["Legacy Excel workbook","document"],ppt:["Legacy PowerPoint deck","document"],zip:["ZIP archive","archive"],gz:["gzip archive","archive"],tgz:["gzip TAR archive","archive"],tar:["TAR archive","archive"],rar:["RAR archive","archive"],"7z":["7-Zip archive","archive"],bz2:["bzip2 archive","archive"],xz:["XZ archive","archive"],zst:["Zstandard archive","archive"],mp3:["MP3 audio","audio"],wav:["WAV audio","audio"],flac:["FLAC audio","audio"],ogg:["Ogg audio","audio"],m4a:["AAC audio","audio"],mp4:["MP4 video","video"],mov:["QuickTime video","video"],avi:["AVI video","video"],mkv:["Matroska video","video"],webm:["WebM video","video"],exe:["Windows executable","executable"],dll:["Windows library","executable"],so:["Shared object","executable"],wasm:["WebAssembly module","executable"],parquet:["Apache Parquet","data"],avro:["Apache Avro","data"],orc:["Apache ORC","data"],db:["SQLite database","database"],sqlite:["SQLite database","database"],bin:["Raw binary","binary"],dat:["Raw data","binary"]};function hexHead(buf,n=16){let out=[];for(let i=0;i{let sig=hex.split(" ").map(h=>parseInt(h,16));if(off+sig.length>buf.length)return!1;for(let i=0;ibuf.length)return{version,truncated:!0};json=new TextDecoder("utf-8").decode(buf.subarray(dstart,dend));break}off=dend}if(json==null)return{version,truncated:!0};let g;try{g=JSON.parse(json)}catch{return{version,truncated:!0}}let n=a=>Array.isArray(a)?a.length:0,accessors=g.accessors||[],vertices=0,triangles=0;for(let mesh of g.meshes||[])for(let prim of mesh.primitives||[]){let posIdx=prim.attributes&&prim.attributes.POSITION;posIdx!=null&&accessors[posIdx]&&(vertices+=accessors[posIdx].count||0),prim.indices!=null&&accessors[prim.indices]&&(triangles+=Math.floor((accessors[prim.indices].count||0)/3))}return{version,truncated:!1,meshes:n(g.meshes),materials:n(g.materials),textures:n(g.textures),images:n(g.images),animations:n(g.animations),nodes:n(g.nodes),accessors:n(g.accessors),vertices,triangles,generator:g.asset&&g.asset.generator||null,gltfVersion:g.asset&&g.asset.version||null}}function previewLang(name){let m=/\.([a-z0-9]+)$/i.exec(String(name||"")),ext=m?m[1].toLowerCase():"";return{json:"json",ndjson:"json",js:"javascript",mjs:"javascript",ts:"typescript",csv:"csv",tsv:"csv",xml:"xml",html:"html",htm:"html",md:"markdown",yml:"yaml",yaml:"yaml",log:"log",txt:"text",ini:"ini",conf:"ini",sql:"sql",sh:"shell",py:"python",cs:"csharp"}[ext]||ext||"text"}function parseSasUrl(rawUrl){let u;try{u=new URL(String(rawUrl||"").trim())}catch{return{ok:!1,error:"That doesn't look like a URL."}}if(u.protocol!=="https:"){let emulator=/^(127\.0\.0\.1|localhost|\[::1\])$/i.test(u.hostname);if(!(u.protocol==="http:"&&emulator))return{ok:!1,error:"SAS URL must use HTTPS \u2014 a SAS signature is a bearer credential and must not travel over plain HTTP."}}let q=u.searchParams;if(!q.get("sig"))return{ok:!1,error:"No SAS signature (sig=) in the URL \u2014 that's a plain URL, not a SAS."};let host=u.hostname,accountMatch=/^([^.]+)\.blob\./.exec(host),account=accountMatch?accountMatch[1]:host,segs=u.pathname.replace(/^\/+/,"").split("/"),container=segs[0]||"",blobPath=segs.slice(1).join("/"),perms=q.get("sp")||"",expiry=q.get("se")||"",resource=q.get("sr")||(blobPath?"b":container?"c":""),now=Date.now(),expMs=expiry?Date.parse(expiry):NaN,expired=Number.isFinite(expMs)&&expMs{let done=!1,finish=value=>{done||(done=!0,resolve(value))};execFile2("azcopy",["--version"],{timeout:4e3,windowsHide:!0},(err,stdout)=>{if(err){let missing=err.code==="ENOENT";finish({installed:!1,version:null,reason:missing?"notFound":"error",hint:missing?`AzCopy isn't on your PATH. Install it from ${AZCOPY_INSTALL_URL}, then re-open the transfer plan.`:`AzCopy was found but couldn't be queried (${String(err.message||err).slice(0,120)}).`,installUrl:AZCOPY_INSTALL_URL});return}let version=(String(stdout||"").match(/\d+\.\d+\.\d+/)||[])[0]||null;finish({installed:!0,version,reason:"ok",hint:null,installUrl:AZCOPY_INSTALL_URL})}).on("error",()=>finish({installed:!1,version:null,reason:"notFound",hint:`AzCopy isn't on your PATH. Install it from ${AZCOPY_INSTALL_URL}, then re-open the transfer plan.`,installUrl:AZCOPY_INSTALL_URL}))})),azcopyProbe}import"node:fs";var BUILD_INFO=resolveStudioBuildInfo(new URL("./extension.mjs",import.meta.url));import"node:fs";var TENANT_ICON=` + + + + + + +`,AZURE_BRAND_ICON=` + + + + + + + + + + + + + + + + + + + + + + + +`,RESOURCE_ICONS={account:` + + + + + + + + + + + + + +`,blob:` + + + + + + + + + + + + + + + + + +`,file:`\r + \r + \r + \r + \r + \r + \r + \r + \r + \r + \r + \r + \r + \r + \r + \r + \r + \r + \r + \r + \r + \r + \r + \r +\r +`,queue:` + + + + + + + + + + + + + + + + + + +`,table:` + + + + + + + + + + + + + + + + + + + + + + + +`,sub:` + + + + + + + + + + + + + +`};var UI_ICONS={add:` + + `,clear:` + + `,check:` + + `,warning:` + + `,insight:` + + `,sort:` + + `,select:` + + `,send:` + + `,stop:` + + `,busy:` + + `,refresh:` + + `,download:` + + `,upload:` + + `,trash:` + + `,link:` + + `,edit:` + + `,folder:` + + `,file:` + + `,back:` + + `,forward:` + + `,down:` + + `,first:` + + `,last:` + + `,collapse:` + + `,expand:` + + `,pin:` + + `,open:` + + `,move:` + + `,transfer:` + + `,plan:` + + `,filter:` + + `,archive:` + + `,list:` + + `,lock:` + + `,policy:` + + `,terminal:` + + `,copy:` + + `,explorer:` + + + + + + + + + + + + + + + + + `,search:` + + + `,more:` + + + + `,read:` + + + `,info:` + + + `,statistics:` + + + + `};function uiIcon(name,options={}){let icon=UI_ICONS[name];if(!icon)throw new Error("Unknown Storage icon: "+name);let size=options.size||16,attributes=' data-icon="'+name+'" width="'+size+'" height="'+size+'"'+(options.className?' class="'+options.className+'"':"")+(options.style?' style="'+options.style+'"':"");return icon.replace(""); + background-image:url("data:image/svg+xml,${encodeURIComponent(uiIcon("down",{size:12}).replace("currentColor","#a6a6a6"))}"); background-repeat:no-repeat;background-position:right 11px center} input:hover,select:hover{border-color:var(--stroke-2);background:var(--surface)} input:focus,select:focus{border-color:var(--accent);box-shadow:var(--glow);background:var(--surface)} input::placeholder{color:var(--text-3)} -.list{border:1px solid var(--stroke);border-radius:10px;overflow:hidden;max-height:360px;overflow-y:auto;background:rgba(7,11,22,.5)} +.list{border:1px solid var(--stroke);border-radius:10px;overflow:hidden;max-height:360px;overflow-y:auto;background:color-mix(in srgb,var(--text) 50%,transparent)} .row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:9px 13px;border-bottom:1px solid var(--stroke);cursor:pointer;transition:.1s} .row:last-child{border-bottom:none} .row:hover{background:var(--surface-2)} @@ -302,8 +549,8 @@ input::placeholder{color:var(--text-3)} .row .meta{color:var(--text-3);font-size:11px} .muted{color:var(--text-2)} .empty{color:var(--text-3);font-size:12.5px;padding:16px 4px;text-align:center} -.xempty{display:flex;flex-direction:column;align-items:center;text-align:center;gap:10px;padding:26px 20px;background:linear-gradient(180deg,rgba(124,58,237,.08),rgba(124,58,237,0));border:1px solid var(--stroke);border-radius:14px} -.xempty .xe-ic{width:46px;height:46px;border-radius:13px;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#7c3aed,#a78bfa);box-shadow:0 5px 16px rgba(124,58,237,.42)} +.xempty{display:flex;flex-direction:column;align-items:center;text-align:center;gap:10px;padding:26px 20px;background:linear-gradient(180deg,color-mix(in srgb,var(--accent) 8%,transparent),color-mix(in srgb,var(--accent) 0%,transparent));border:1px solid var(--stroke);border-radius:14px} +.xempty .xe-ic{width:46px;height:46px;border-radius:13px;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,var(--accent),var(--accent));box-shadow:0 5px 16px color-mix(in srgb,var(--accent) 42%,transparent)} .xempty .xe-ic svg{width:23px;height:23px} .xempty .xe-t{font-size:14px;font-weight:650;color:var(--text)} .xempty .xe-s{font-size:12px;color:var(--text-3);line-height:1.55;max-width:400px} @@ -315,42 +562,43 @@ input::placeholder{color:var(--text-3)} .bar{height:9px;border-radius:999px;background:var(--surface);border:1px solid var(--stroke-2);overflow:hidden;flex:1;min-width:160px} .bar>i{display:block;height:100%;width:0;background:var(--brand);transition:width .25s} .pill{display:inline-block;font-size:10.5px;font-weight:700;border-radius:999px;padding:2px 10px} -.pill.ok{background:rgba(108,203,95,.16);color:var(--ok)} -.pill.bad{background:rgba(241,112,123,.16);color:var(--bad)} -.pill.warn{background:rgba(242,198,97,.16);color:var(--warn)} +.pill.ok{background:color-mix(in srgb,var(--ok) 16%,transparent);color:var(--ok)} +.pill.bad{background:color-mix(in srgb,var(--bad) 16%,transparent);color:var(--bad)} +.pill.warn{background:color-mix(in srgb,var(--bad) 16%,transparent);color:var(--warn)} .sas-banner{background:var(--surface);border:1px solid var(--stroke-2);border-radius:12px;padding:12px 14px} .sas-tbl th{text-align:left;font-size:11px;color:var(--muted);font-weight:600;padding:4px 8px;border-bottom:1px solid var(--stroke-2)} .sas-tbl td{padding:5px 8px;border-bottom:1px solid var(--stroke);font-size:12.5px;vertical-align:middle} .sas-tbl td.fname{max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -.sas-tbl tr:hover td{background:rgba(255,255,255,.02)} +.sas-tbl tr:hover td{background:color-mix(in srgb,var(--text) 2%,transparent)} /* SAS connected view */ -.sasconn{background:linear-gradient(160deg,rgba(71,158,245,.06),rgba(98,171,245,.02) 60%,transparent);border:1px solid var(--stroke-2);border-radius:14px;padding:14px 16px;box-shadow:inset 0 1px 0 rgba(255,255,255,.03),0 8px 24px -16px rgba(0,0,0,.7)} +.sasconn{background:linear-gradient(160deg,color-mix(in srgb,var(--accent) 6%,transparent),color-mix(in srgb,var(--accent) 2%,transparent) 60%,transparent);border:1px solid var(--stroke-2);border-radius:14px;padding:14px 16px;box-shadow:inset 0 1px 0 color-mix(in srgb,var(--text) 3%,transparent),0 8px 24px -16px rgba(0,0,0,.7)} .sashead{display:flex;align-items:center;gap:13px} -.sasavatar{width:40px;height:40px;border-radius:11px;flex:none;display:grid;place-items:center;background:var(--grad);box-shadow:0 4px 14px -3px rgba(15,108,189,.6)} +.sasavatar{width:40px;height:40px;border-radius:11px;flex:none;display:grid;place-items:center;background:var(--grad);box-shadow:0 4px 14px -3px color-mix(in srgb,var(--accent) 60%,transparent)} +.sasavatar .svi{width:20px;height:20px} .sasid{flex:1;min-width:0} .sastitle{display:flex;align-items:center;gap:8px;font-size:16.5px;line-height:1.2;overflow:hidden} .sastitle b{font-weight:700;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .sastitle .chev{color:var(--text-3);font-weight:400;flex:none} .sassub{font-size:11.5px;color:var(--text-3);margin-top:3px} .sasmeta{display:flex;align-items:center;gap:8px;flex:none} -.constatus{display:inline-flex;align-items:center;gap:7px;font-size:11.5px;font-weight:600;color:var(--ok);background:rgba(108,203,95,.12);border:1px solid rgba(108,203,95,.38);border-radius:999px;padding:4px 12px 4px 9px} -.livedot{width:8px;height:8px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 0 rgba(108,203,95,.5);animation:lpulse 2s infinite;flex:none} -@keyframes lpulse{0%{box-shadow:0 0 0 0 rgba(108,203,95,.45)}70%{box-shadow:0 0 0 6px rgba(108,203,95,0)}100%{box-shadow:0 0 0 0 rgba(108,203,95,0)}} +.constatus{display:inline-flex;align-items:center;gap:7px;font-size:11.5px;font-weight:600;color:var(--ok);background:color-mix(in srgb,var(--ok) 12%,transparent);border:1px solid color-mix(in srgb,var(--ok) 38%,transparent);border-radius:999px;padding:4px 12px 4px 9px} +.livedot{width:8px;height:8px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 0 color-mix(in srgb,var(--ok) 50%,transparent);animation:lpulse 2s infinite;flex:none} +@keyframes lpulse{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--ok) 45%,transparent)}70%{box-shadow:0 0 0 6px color-mix(in srgb,var(--ok) 0%,transparent)}100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--ok) 0%,transparent)}} .permrow{display:flex;align-items:center;gap:14px;margin-top:13px;padding-top:13px;border-top:1px solid var(--stroke)} .plabel{font-size:10px;font-weight:700;letter-spacing:.9px;text-transform:uppercase;color:var(--text-3);flex:none;width:78px} .caps{display:flex;gap:8px;flex-wrap:wrap} .cap{display:inline-flex;align-items:center;gap:7px;font-size:12px;font-weight:600;border-radius:9px;padding:6px 12px;border:1px solid var(--stroke);transition:.14s} .cap .ci{width:15px;height:15px;display:inline-flex;flex:none} .cap .lk{width:12px;height:12px;display:inline-flex;margin-left:1px;opacity:.7} -.cap.on{background:linear-gradient(180deg,rgba(108,203,95,.16),rgba(108,203,95,.07));border-color:rgba(108,203,95,.5);color:#8fd782;box-shadow:inset 0 1px 0 rgba(255,255,255,.05)} -.cap.on .ci{color:#8fd782} -.cap.off{background:rgba(255,255,255,.015);color:var(--text-3);border-color:var(--stroke)} +.cap.on{background:linear-gradient(180deg,color-mix(in srgb,var(--ok) 16%,transparent),color-mix(in srgb,var(--ok) 7%,transparent));border-color:color-mix(in srgb,var(--ok) 50%,transparent);color:var(--ok);box-shadow:inset 0 1px 0 color-mix(in srgb,var(--text) 5%,transparent)} +.cap.on .ci{color:var(--ok)} +.cap.off{background:color-mix(in srgb,var(--text) 1.5%,transparent);color:var(--text-3);border-color:var(--stroke)} .cap.off .ci{color:var(--text-3);opacity:.75} -.expchip{font-size:11px;font-weight:600;border-radius:999px;padding:4px 12px;border:1px solid var(--stroke-2);color:var(--text-2);background:rgba(255,255,255,.02)} -.expchip.warn{background:rgba(242,198,97,.12);border-color:rgba(242,198,97,.4);color:var(--warn)} -.expchip.bad{background:rgba(241,112,123,.14);border-color:rgba(241,112,123,.4);color:var(--bad)} +.expchip{font-size:11px;font-weight:600;border-radius:999px;padding:4px 12px;border:1px solid var(--stroke-2);color:var(--text-2);background:color-mix(in srgb,var(--text) 2%,transparent)} +.expchip.warn{background:color-mix(in srgb,var(--bad) 12%,transparent);border-color:color-mix(in srgb,var(--bad) 40%,transparent);color:var(--warn)} +.expchip.bad{background:color-mix(in srgb,var(--bad) 14%,transparent);border-color:color-mix(in srgb,var(--bad) 40%,transparent);color:var(--bad)} .sasm{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:13px} -.sasm .c{background:rgba(255,255,255,.025);border:1px solid var(--stroke);border-radius:12px;padding:11px 13px} +.sasm .c{background:color-mix(in srgb,var(--text) 2.5%,transparent);border:1px solid var(--stroke);border-radius:12px;padding:11px 13px} .sasm .c b{display:block;font-size:18px;font-weight:700;line-height:1.1;color:var(--text)} .sasm .c span{display:block;margin-top:3px;font-size:10px;color:var(--text-3);text-transform:uppercase;letter-spacing:.6px;font-weight:600} .sasact{width:40px;height:34px;padding:0;font-size:12px;line-height:0;display:inline-flex;align-items:center;justify-content:center;color:var(--text);background:var(--surface-2);border:1px solid var(--stroke-2);border-radius:9px;transition:.12s} @@ -359,23 +607,23 @@ input::placeholder{color:var(--text-3)} .sas-tbl th.acth,.sas-tbl td.acth{text-align:right;padding-right:8px} .acthdr,.rowacts{display:inline-grid;grid-auto-flow:column;grid-auto-columns:40px;gap:12px;justify-content:end;justify-items:center;align-items:center} .acthdr span{font-size:10px;color:var(--text-3);font-weight:700;text-transform:uppercase;letter-spacing:.4px} -.tierpill{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:600;color:var(--text-2);background:rgba(255,255,255,.03);border:1px solid var(--stroke-2);border-radius:999px;padding:2px 10px} +.tierpill{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:600;color:var(--text-2);background:color-mix(in srgb,var(--text) 3%,transparent);border:1px solid var(--stroke-2);border-radius:999px;padding:2px 10px} .tdot{width:7px;height:7px;border-radius:50%;flex:none} -.dlmsg{display:flex;align-items:center;gap:10px;flex-wrap:nowrap;margin-top:12px;padding:10px 12px;background:rgba(108,203,95,.08);border:1px solid rgba(108,203,95,.28);border-radius:10px;font-size:12.5px} +.dlmsg{display:flex;align-items:center;gap:10px;flex-wrap:nowrap;margin-top:12px;padding:10px 12px;background:color-mix(in srgb,var(--ok) 8%,transparent);border:1px solid color-mix(in srgb,var(--ok) 28%,transparent);border-radius:10px;font-size:12.5px} .dlmsg .ok{color:var(--ok);font-weight:600;flex:none} .dlpath{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11.5px;color:var(--text-2);background:rgba(0,0,0,.3);padding:4px 8px;border-radius:6px;flex:1 1 auto;min-width:60px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .dlacts{display:inline-flex;gap:8px;flex:none} .dlacts button{padding:5px 11px;font-size:12px;white-space:nowrap} #sasDlAll{display:inline-flex;align-items:center;gap:6px} -#sasDlAll svg{width:13px;height:13px;margin:0} +#sasDlAll svg{width:16px;height:16px;margin:0} .xradio{display:inline-flex;align-items:center;gap:5px;font-size:12.5px;color:var(--text);cursor:pointer} .xradio input{margin:0} .xstat{display:inline-flex;flex-direction:column;min-width:70px} .xstat b{font-size:19px;font-weight:700;line-height:1.1;color:var(--text)} -.xstat b.warn{color:var(--warn,#e5a83c)} +.xstat b.warn{color:var(--warn,var(--bad))} .xstat span{font-size:11px;color:var(--muted);margin-top:2px} .mv{display:inline-flex;color:var(--muted)} -.pill.warn{background:rgba(229,168,60,.14);border-color:rgba(229,168,60,.4);color:#e5a83c} +.pill.warn{background:color-mix(in srgb,var(--bad) 14%,transparent);border-color:color-mix(in srgb,var(--bad) 40%,transparent);color:var(--bad)} .xcmd{background:var(--code-bg);border:1px solid var(--stroke-2);border-radius:8px;padding:10px 12px;margin-top:6px; font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--code-text);white-space:pre-wrap;word-break:break-all;overflow-x:auto} @@ -388,15 +636,15 @@ input::placeholder{color:var(--text-3)} .factions{display:flex;gap:10px;align-items:center;margin-top:13px;padding-top:12px;border-top:1px solid var(--stroke);flex-wrap:wrap} .factions .grow{flex:1} .factions button{display:inline-flex;align-items:center;gap:5px;padding:8px 15px;font-size:12.5px} -.hicon{width:26px;height:26px;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;background:var(--grad);box-shadow:0 2px 8px rgba(15,108,189,.45)} -.hicon.g-sas{background:linear-gradient(135deg,#0891b2,#22d3ee);box-shadow:0 2px 8px rgba(8,145,178,.45)} -.hicon.g-xfer{background:linear-gradient(135deg,#7c3aed,#a78bfa);box-shadow:0 2px 8px rgba(124,58,237,.45)} -.hicon.g-log{background:linear-gradient(135deg,#0d9488,#2dd4bf);box-shadow:0 2px 8px rgba(13,148,136,.45)} -.hicon.g-ask{background:linear-gradient(135deg,#8b5cf6,#22d3ee);box-shadow:0 2px 8px rgba(139,92,246,.45)} +.hicon{width:26px;height:26px;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;background:var(--grad);box-shadow:0 2px 8px color-mix(in srgb,var(--accent) 45%,transparent)} +.hicon.g-sas{background:linear-gradient(135deg,var(--accent),var(--accent));box-shadow:0 2px 8px color-mix(in srgb,var(--accent) 45%,transparent)} +.hicon.g-xfer{background:linear-gradient(135deg,var(--accent),var(--accent));box-shadow:0 2px 8px color-mix(in srgb,var(--accent) 45%,transparent)} +.hicon.g-log{background:linear-gradient(135deg,var(--ok),var(--ok));box-shadow:0 2px 8px color-mix(in srgb,var(--ok) 45%,transparent)} +.hicon.g-ask{background:linear-gradient(135deg,var(--accent),var(--accent));box-shadow:0 2px 8px color-mix(in srgb,var(--accent) 45%,transparent)} .askrow{display:flex;gap:10px;align-items:stretch} .askbar{flex:1;display:flex;align-items:center;gap:9px;background:var(--surface-2);border:1px solid var(--stroke-2);border-radius:11px;padding:2px 14px;transition:border-color .15s,box-shadow .15s;animation:askglow 3.8s ease-in-out infinite} .askbar:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);animation:none} -@keyframes askglow{0%,100%{box-shadow:0 0 0 0 rgba(71,158,245,0)}50%{box-shadow:0 0 0 3px rgba(71,158,245,.10)}} +@keyframes askglow{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--accent) 0%,transparent)}50%{box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 10%,transparent)}} .askbar .ask-mag{color:var(--accent);flex:none;display:flex} .askbar input{flex:1;background:transparent;border:none;outline:none;color:var(--text);font-size:13.5px;padding:10px 0;min-width:0} .askbar input::placeholder{color:var(--text-3);transition:opacity .3s ease} @@ -407,18 +655,18 @@ input::placeholder{color:var(--text-3)} .askchip{display:inline-flex;align-items:center;gap:7px;padding:7px 12px;border-radius:999px;background:var(--surface-2);border:1px solid var(--stroke-2);color:var(--text-2);font-size:12px;font-weight:500;cursor:pointer;transition:border-color .14s,background .14s,color .14s,transform .1s} .askchip:hover{border-color:var(--accent);background:var(--surface-3);color:var(--text);transform:translateY(-1px)} .askchip:active{transform:translateY(0)} -.askchip>svg{color:#a78bfa;flex:none} +.askchip>svg{color:var(--accent);flex:none} .askout{margin-top:15px;display:flex;flex-direction:column;gap:13px} .ask-q{display:flex;justify-content:flex-end} -.ask-qtext{max-width:82%;background:linear-gradient(135deg,var(--brand),#2b88d8);color:#fff;padding:9px 14px;border-radius:14px 14px 4px 14px;font-size:13px;line-height:1.5;box-shadow:0 2px 10px rgba(15,108,189,.32);word-break:break-word} +.ask-qtext{max-width:82%;background:linear-gradient(135deg,var(--brand),var(--accent));color:var(--color-white,#fff);padding:9px 14px;border-radius:14px 14px 4px 14px;font-size:13px;line-height:1.5;box-shadow:0 2px 10px color-mix(in srgb,var(--accent) 32%,transparent);word-break:break-word} .ask-a{display:flex;gap:10px;align-items:flex-start} -.ask-av{width:30px;height:30px;flex:none;border-radius:9px;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#8b5cf6,#22d3ee);box-shadow:0 2px 8px rgba(139,92,246,.4)} +.ask-av{width:30px;height:30px;flex:none;border-radius:9px;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,var(--accent),var(--accent));box-shadow:0 2px 8px color-mix(in srgb,var(--accent) 40%,transparent)} .ask-abody{flex:1;min-width:0;background:var(--surface-2);border:1px solid var(--stroke);border-radius:4px 14px 14px 14px;padding:13px 15px;animation:askpop .22s ease} @keyframes askpop{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}} .ask-atext{font-size:13px;line-height:1.6;color:var(--text);white-space:pre-wrap;word-break:break-word} -.ask-abody code,.insight-box code{background:var(--surface-3);border:1px solid var(--stroke);border-radius:5px;padding:1px 6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;color:#8fd0ff;white-space:normal;word-break:break-word} +.ask-abody code,.insight-box code{background:var(--surface-3);border:1px solid var(--stroke);border-radius:5px;padding:1px 6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;color:var(--accent);white-space:normal;word-break:break-word} .ask-abody strong,.insight-box strong{color:var(--text);font-weight:600} -.ask-abody .stat{background:var(--accent-soft);border-color:rgba(71,158,245,.3);margin:12px 10px 2px 0} +.ask-abody .stat{background:var(--accent-soft);border-color:color-mix(in srgb,var(--accent) 30%,transparent);margin:12px 10px 2px 0} .ask-abody .hbar{font-size:12px;margin:6px 0} .ask-abody table{width:100%;border-collapse:collapse;margin-top:12px;font-size:12.5px} .ask-abody th{text-align:left;color:var(--text-3);font-weight:600;font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;padding:6px 10px;border-bottom:1px solid var(--stroke-2)} @@ -447,13 +695,13 @@ input::placeholder{color:var(--text-3)} .insight-head{display:flex;align-items:center;gap:8px;font-weight:640;font-size:12.5px;margin-bottom:6px} .insight-head .ix{margin-left:auto} .insight-sug{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px;padding-top:10px;border-top:1px solid var(--stroke-2)} -.insight-why{margin-top:10px;font-size:11.5px;color:var(--text-2);line-height:1.5;background:rgba(255,255,255,.03);border:1px solid var(--stroke);border-radius:8px;padding:8px 10px} -.sugcount{font-size:11px;color:var(--accent);background:var(--accent-soft);border:1px solid rgba(71,158,245,.35);border-radius:999px;padding:2px 9px;font-variant-numeric:tabular-nums} +.insight-why{margin-top:10px;font-size:11.5px;color:var(--text-2);line-height:1.5;background:color-mix(in srgb,var(--text) 3%,transparent);border:1px solid var(--stroke);border-radius:8px;padding:8px 10px} +.sugcount{font-size:11px;color:var(--accent);background:var(--accent-soft);border:1px solid color-mix(in srgb,var(--accent) 35%,transparent);border-radius:999px;padding:2px 9px;font-variant-numeric:tabular-nums} .ix{background:transparent;border:none;color:var(--text-3);font-size:12px;padding:2px 6px;cursor:pointer;border-radius:6px} .ix:hover{background:var(--surface-2);color:var(--text)} .rowact{background:var(--surface-2);border:1px solid var(--stroke-2);border-radius:6px;padding:2px 8px;font-size:11px;color:var(--text-2)} .rowact:hover{background:var(--surface-3);color:var(--text)} -.rowact.del:hover{color:var(--bad,#f1707b);border-color:var(--bad,#f1707b)} +.rowact.del:hover{color:var(--bad,var(--bad));border-color:var(--bad,var(--bad))} .stat{display:inline-flex;flex-direction:column;padding:12px 18px;border:1px solid var(--stroke-2);border-radius:10px; background:var(--grad-soft);margin:0 10px 10px 0;min-width:96px} @@ -469,36 +717,36 @@ input::placeholder{color:var(--text-3)} @media(max-width:720px){.metrics{grid-template-columns:repeat(2,1fr)}} .hbar{display:flex;align-items:center;gap:9px;margin:3px 0;font-size:11.5px} .hbar .track{flex:1;height:8px;background:var(--surface);border-radius:999px;overflow:hidden} -.hbar .track>i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),#8b5cf6);border-radius:999px} +.hbar .track>i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--accent));border-radius:999px} .chart{max-width:680px} .sect{color:var(--text-3);font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.7px;margin:16px 0 8px} .brow{display:grid;grid-template-columns:108px 1fr;align-items:center;gap:12px;margin:6px 0} .brow .blabel{color:var(--text-2);font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -.bwrap{position:relative;height:24px;background:rgba(255,255,255,.05);border-radius:7px;overflow:hidden} +.bwrap{position:relative;height:24px;background:color-mix(in srgb,var(--text) 5%,transparent);border-radius:7px;overflow:hidden} .bwrap .bfill{position:absolute;left:0;top:0;bottom:0;border-radius:7px;min-width:3px;box-shadow:0 1px 6px rgba(0,0,0,.25) inset;transition:width .35s cubic-bezier(.4,0,.2,1)} -.bwrap .bval{position:absolute;right:6px;top:50%;transform:translateY(-50%);line-height:16px;padding:1px 7px;border-radius:6px;background:rgba(15,18,24,.55);font-size:11px;color:#fff;white-space:nowrap;font-variant-numeric:tabular-nums;text-shadow:0 1px 2px rgba(0,0,0,.9)} +.bwrap .bval{position:absolute;right:6px;top:50%;transform:translateY(-50%);line-height:16px;padding:1px 7px;border-radius:6px;background:color-mix(in srgb,var(--text) 55%,transparent);font-size:11px;color:var(--color-white,#fff);white-space:nowrap;font-variant-numeric:tabular-nums;text-shadow:0 1px 2px rgba(0,0,0,.9)} .bwrap .bval b{font-weight:700} .bwrap .bval .bmut{color:var(--text-2)} -.lrow{position:relative;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 12px;border-radius:9px;overflow:hidden;margin:5px 0;background:rgba(255,255,255,.04);border:1px solid var(--stroke)} -.lrow .lfill{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,rgba(56,189,248,.30),rgba(139,92,246,.10));border-right:1px solid rgba(56,189,248,.45)} +.lrow{position:relative;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 12px;border-radius:9px;overflow:hidden;margin:5px 0;background:color-mix(in srgb,var(--text) 4%,transparent);border:1px solid var(--stroke)} +.lrow .lfill{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,color-mix(in srgb,var(--accent) 30%,transparent),color-mix(in srgb,var(--accent) 10%,transparent));border-right:1px solid color-mix(in srgb,var(--accent) 45%,transparent)} .lrow .lname{position:relative;font-size:12.5px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .lrow .lsize{position:relative;font-size:12px;color:var(--text-2);white-space:nowrap;font-variant-numeric:tabular-nums;flex:none} table{width:100%;border-collapse:collapse;font-size:12.5px} th,td{text-align:left;padding:7px 9px;border-bottom:1px solid var(--stroke)} -th{color:var(--text-3);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.5px} +th{color:var(--text-3);font-weight:600;font-size:11px;text-transform:none} -.err{background:rgba(241,112,123,.1);border:1px solid rgba(241,112,123,.5);color:#f1a3aa;border-radius:10px;padding:10px 14px;margin:14px 22px 0;font-size:12.5px;display:flex;align-items:flex-start;gap:10px} +.err{background:color-mix(in srgb,var(--bad) 10%,transparent);border:1px solid color-mix(in srgb,var(--bad) 50%,transparent);color:var(--bad);border-radius:10px;padding:10px 14px;margin:14px 22px 0;font-size:12.5px;display:flex;align-items:flex-start;gap:10px} .err .etext{flex:1} -.err .eclose{cursor:pointer;background:transparent;border:none;color:#f1a3aa;font-size:16px;line-height:1;padding:0 2px;opacity:.8;flex:none} +.err .eclose{cursor:pointer;background:transparent;border:none;color:var(--bad);font-size:16px;line-height:1;padding:0 2px;opacity:.8;flex:none} .err .eclose:hover{opacity:1} .err .eact{display:flex;align-items:center;gap:8px;flex:none} -.err .ebtn{cursor:pointer;background:#0f6cbd;border:none;color:#fff;font-size:12px;font-weight:600;border-radius:7px;padding:5px 12px;white-space:nowrap} -.err .ebtn:hover{background:#1277c9} -.err.err-auth{background:rgba(71,158,245,.1);border-color:rgba(71,158,245,.45);color:#bcddff} -.err.err-auth .eclose{color:#bcddff} -.log{font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;color:var(--text-2);white-space:pre-wrap;max-height:170px;overflow-y:auto;background:rgba(7,11,22,.5);border:1px solid var(--stroke);border-radius:10px;padding:10px 12px} -.mono{font-family:ui-monospace,monospace;font-size:11px;color:var(--text-3);white-space:pre-wrap;word-break:break-all;background:rgba(7,11,22,.5);border:1px solid var(--stroke);border-radius:8px;padding:8px 10px;margin-top:6px} -.spin{width:13px;height:13px;border:2px solid var(--stroke-2);border-top-color:var(--cyan);border-radius:50%;display:inline-block;animation:sp .7s linear infinite;vertical-align:middle} +.err .ebtn{cursor:pointer;background:var(--accent);border:none;color:var(--color-white,#fff);font-size:12px;font-weight:600;border-radius:7px;padding:5px 12px;white-space:nowrap} +.err .ebtn:hover{background:var(--accent)} +.err.err-auth{background:color-mix(in srgb,var(--accent) 10%,transparent);border-color:color-mix(in srgb,var(--accent) 45%,transparent);color:var(--accent)} +.err.err-auth .eclose{color:var(--accent)} +.log{font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;color:var(--text-2);white-space:pre-wrap;max-height:170px;overflow-y:auto;background:color-mix(in srgb,var(--text) 50%,transparent);border:1px solid var(--stroke);border-radius:10px;padding:10px 12px} +.mono{font-family:ui-monospace,monospace;font-size:11px;color:var(--text-3);white-space:pre-wrap;word-break:break-all;background:color-mix(in srgb,var(--text) 50%,transparent);border:1px solid var(--stroke);border-radius:8px;padding:8px 10px;margin-top:6px} +.spin{width:13px;height:13px;display:inline-block;animation:sp .7s linear infinite;vertical-align:middle} @keyframes sp{to{transform:rotate(360deg)}} /* subscription scope bar + chip */ @@ -516,7 +764,7 @@ th{color:var(--text-3);font-weight:600;font-size:11px;text-transform:uppercase;l .tree-cta{text-align:center;padding:26px 14px;color:var(--text-2)} .tree-cta .ic{font-size:26px;margin-bottom:8px} .tree-cta p{margin:0 0 14px;font-size:12.5px;line-height:1.5} -.tree-cta button{background:var(--brand);color:#fff;border:none;border-radius:8px;padding:9px 16px;font-weight:600;font-size:12.5px;cursor:pointer} +.tree-cta button{background:var(--brand);color:var(--color-white,#fff);border:none;border-radius:8px;padding:9px 16px;font-weight:600;font-size:12.5px;cursor:pointer} .tree-cta button:hover{background:var(--brand-hover)} /* subscription picker modal */ @@ -548,8 +796,8 @@ th{color:var(--text-3);font-weight:600;font-size:11px;text-transform:uppercase;l .subrow .nm{font-size:12.5px;font-weight:600;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .subrow .id{font-size:10.5px;color:var(--text-3);font-family:ui-monospace,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .dlg-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:13px 20px;border-top:1px solid var(--stroke)} -button.danger{border-color:rgba(241,112,123,.5);color:#f1a3aa;background:var(--surface-2)} -button.danger:hover{background:rgba(241,112,123,.14);color:#f5c2c7} +button.danger{border-color:color-mix(in srgb,var(--bad) 50%,transparent);color:var(--bad);background:var(--surface-2)} +button.danger:hover{background:color-mix(in srgb,var(--bad) 14%,transparent);color:var(--bad)} .addbtn,.delbtn{background:none;border:none;color:var(--text-3);cursor:pointer;font-size:12px;line-height:1;padding:2px 5px;flex:none;border-radius:6px} .addbtn{margin-left:auto} .tmeta+.addbtn,.tmeta+.delbtn{margin-left:6px} @@ -557,26 +805,25 @@ button.danger:hover{background:rgba(241,112,123,.14);color:#f5c2c7} .delbtn:hover{color:var(--bad);background:var(--surface-2)} .trow .delbtn{opacity:0;margin-left:6px} .trow:hover .delbtn{opacity:1} -.pinbtn,.hidebtn{background:none;border:none;color:var(--text-3);cursor:pointer;font-size:12px;line-height:1;padding:2px 5px;flex:none;border-radius:6px;opacity:0;margin-left:4px} -.trow:hover .pinbtn,.trow:hover .hidebtn{opacity:1} +.pinbtn{background:none;border:none;color:var(--text-3);cursor:pointer;font-size:12px;line-height:1;padding:2px 5px;flex:none;border-radius:6px;opacity:0;margin-left:4px} +.trow:hover .pinbtn{opacity:1} .pinbtn.on{opacity:1;color:var(--azure)} .pinbtn:hover{color:var(--azure);background:var(--surface-2)} -.hidebtn:hover{color:var(--bad);background:var(--surface-2)} -.pinhdr .ti,.hidhdr .ti{color:var(--azure)} +.pinhdr .ti{color:var(--azure)} #dataBody{position:relative} -.dropmask{position:absolute;inset:0;display:none;align-items:center;justify-content:center;background:rgba(71,158,245,.10);border:2px dashed var(--accent);border-radius:12px;z-index:6;pointer-events:none} +.dropmask{position:absolute;inset:0;display:none;align-items:center;justify-content:center;background:color-mix(in srgb,var(--accent) 10%,transparent);border:2px dashed var(--accent);border-radius:12px;z-index:6;pointer-events:none} #dataBody.dragging .dropmask{display:flex} .dropmsg{display:flex;flex-direction:column;align-items:center;gap:8px;color:var(--accent);font-weight:600;font-size:14px;text-align:center} .dropic{font-size:30px;line-height:1} .vtool{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid var(--stroke)} .rowact{background:none;border:1px solid var(--stroke-2);color:var(--text-2);border-radius:7px;padding:2px 9px;font-size:11px;cursor:pointer;flex:none} .rowact:hover{border-color:var(--accent);color:var(--text)} -.rowact.del:hover{border-color:var(--bad);color:#f1a3aa} +.rowact.del:hover{border-color:var(--bad);color:var(--bad)} .dti{display:inline-flex;align-items:center}.dti .svi{width:17px;height:17px} .vtool .grow{margin-left:auto} .browsetools button{padding:7px 13px;font-size:12px} -.browsetools .primary{background:var(--brand);color:#fff;border:1px solid transparent;font-weight:600} -.browsetools .primary:hover{background:var(--brand-hover);color:#fff} +.browsetools .primary{background:var(--brand);color:var(--color-white,#fff);border:1px solid transparent;font-weight:600} +.browsetools .primary:hover{background:var(--brand-hover);color:var(--color-white,#fff)} .browsetools input[type=file]{min-width:230px;font-size:12px;color:var(--text-2)} .browsetools input[type=file]::file-selector-button{background:var(--surface-3);color:var(--text);border:1px solid var(--stroke-2);border-radius:7px;padding:6px 12px;margin-right:9px;font-size:12px;font-weight:560;cursor:pointer} .browsetools input[type=file]::file-selector-button:hover{border-color:var(--accent)} @@ -615,12 +862,12 @@ button.danger:hover{background:rgba(241,112,123,.14);color:#f5c2c7} .blobnav{margin:16px 0 18px} .blobtab-back{display:inline-flex;align-items:center;gap:5px;background:var(--surface-2);border:1px solid var(--stroke-2);color:var(--text-2);font-size:11.5px;font-weight:600;cursor:pointer;padding:6px 12px;border-radius:8px;margin-bottom:10px} .blobtab-back:hover{color:var(--text);border-color:var(--accent);background:var(--surface-3)} -.blobtabgroup{display:flex;gap:5px;padding:5px;background:var(--surface-2);border:1px solid var(--stroke);border-radius:13px;box-shadow:inset 0 1px 0 rgba(255,255,255,.02)} +.blobtabgroup{display:flex;gap:5px;padding:5px;background:var(--surface-2);border:1px solid var(--stroke);border-radius:13px;box-shadow:inset 0 1px 0 color-mix(in srgb,var(--text) 2%,transparent)} .blobtab{flex:1;position:relative;display:inline-flex;align-items:center;justify-content:center;gap:9px;background:transparent;border:0;padding:12px 10px;color:var(--text-2);font-size:13.5px;font-weight:600;cursor:pointer;border-radius:9px;transition:background .15s,color .15s,box-shadow .15s} .blobtab:hover{background:var(--surface-3);color:var(--text)} -.blobtab.active{background:linear-gradient(180deg,#4aa2f7,#3d87dd);color:#fff;box-shadow:0 2px 9px rgba(61,135,221,.4)} -.blobtab .tbi{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:8px;background:rgba(255,255,255,.05);flex:none} -.blobtab.active .tbi{background:rgba(255,255,255,.18)} +.blobtab.active{background:linear-gradient(180deg,var(--accent),var(--accent));color:var(--color-white,#fff);box-shadow:0 2px 9px color-mix(in srgb,var(--accent) 40%,transparent)} +.blobtab .tbi{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:8px;background:color-mix(in srgb,var(--text) 5%,transparent);flex:none} +.blobtab.active .tbi{background:color-mix(in srgb,var(--text) 18%,transparent)} .xdestmode{margin:6px auto 9px;display:flex;width:fit-content;max-width:100%;flex:none;justify-content:flex-start;padding:0;border:1px solid var(--stroke-2);border-radius:8px;overflow:hidden} .xdestpick{display:flex;align-items:center;gap:8px} .xdestpick select{flex:1;min-width:0} @@ -632,7 +879,7 @@ button.danger:hover{background:rgba(241,112,123,.14);color:#f5c2c7} .xlbl-row{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:24px} .xcol .xdestmode{margin:0} .seg-sm label{padding:3px 11px;font-size:10.5px;font-weight:600} -.xarrow{align-self:end;width:34px;height:34px;margin:0 auto 3px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--accent);background:var(--accent-soft);border:1px solid rgba(71,158,245,.32);box-shadow:0 2px 8px rgba(71,158,245,.18)} +.xarrow{align-self:end;width:34px;height:34px;margin:0 auto 3px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--accent);background:var(--accent-soft);border:1px solid color-mix(in srgb,var(--accent) 32%,transparent);box-shadow:0 2px 8px color-mix(in srgb,var(--accent) 18%,transparent)} .xfoot{display:flex;align-items:center;gap:14px;margin-top:15px;padding-top:14px;border-top:1px solid var(--stroke);flex-wrap:wrap} .xfoot .grow{flex:1;min-width:8px} .xconf{display:flex;align-items:center;gap:11px;min-width:0} @@ -661,15 +908,6 @@ button.danger:hover{background:rgba(241,112,123,.14);color:#f5c2c7} .selx-pop::-webkit-scrollbar{width:10px} .selx-pop::-webkit-scrollbar-thumb{background:var(--stroke-2);border-radius:6px;border:2px solid var(--surface-2)} .xdestpick .selx{flex:1;min-width:0} -.confseg{display:grid;grid-template-columns:repeat(3,1fr);gap:9px} -.conf-opt{position:relative;display:flex;align-items:center;justify-content:center;gap:9px;padding:10px 12px;border:1px solid var(--stroke-2);border-radius:11px;background:var(--surface-2);cursor:pointer;transition:border-color .15s,background .15s,box-shadow .15s} -.conf-opt:hover{border-color:var(--stroke-2);background:var(--surface-3)} -.conf-opt input{position:absolute;opacity:0;pointer-events:none} -.conf-ic{width:30px;height:30px;flex:none;border-radius:8px;display:flex;align-items:center;justify-content:center;background:var(--surface-3);color:var(--text-3);transition:.15s} -.conf-t{font-size:12.5px;font-weight:600;color:var(--text-2);transition:color .15s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -.conf-opt:has(input:checked) .conf-t{color:var(--text)} -.conf-opt:has(input:checked){border-color:var(--accent);background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--accent)} -.conf-opt:has(input:checked) .conf-ic{background:var(--accent);color:#fff;box-shadow:0 2px 7px rgba(71,158,245,.35)} .blobtab .svi{width:16px;height:16px;flex:none} .hubpanel{margin-top:12px} .hubshare{padding:8px 2px;display:flex;flex-direction:column;align-items:center;text-align:center} @@ -679,9 +917,9 @@ button.danger:hover{background:rgba(241,112,123,.14);color:#f5c2c7} .svc-item .fname{display:flex;align-items:center;gap:8px} .svc-item .fname .svi{width:15px;height:15px;flex:none} .imeta{color:var(--text-3);font-size:11px;margin-right:8px} -.dlsaved{display:flex;flex-direction:column;align-items:stretch;gap:10px;margin-top:12px;padding:13px 14px;border:1px solid rgba(64,190,120,.4);background:linear-gradient(180deg,rgba(52,150,95,.14),rgba(40,120,80,.08));border-radius:12px} +.dlsaved{display:flex;flex-direction:column;align-items:stretch;gap:10px;margin-top:12px;padding:13px 14px;border:1px solid color-mix(in srgb,var(--ok) 40%,transparent);background:linear-gradient(180deg,color-mix(in srgb,var(--ok) 14%,transparent),color-mix(in srgb,var(--ok) 8%,transparent));border-radius:12px} .dlsaved .dls-top{display:flex;align-items:center;gap:10px} -.dlsaved .dls-ic{width:30px;height:30px;flex:none;display:flex;align-items:center;justify-content:center;border-radius:8px;background:rgba(64,190,120,.18);font-size:16px} +.dlsaved .dls-ic{width:30px;height:30px;flex:none;display:flex;align-items:center;justify-content:center;border-radius:8px;background:color-mix(in srgb,var(--ok) 18%,transparent);font-size:16px} .dlsaved .dls-lead{font-size:13px;font-weight:640;color:var(--text);line-height:1.25} .dlsaved .dls-path{display:block;font-size:11.5px;color:var(--text-2);background:var(--bg);border:1px solid var(--stroke-2);border-radius:8px;padding:7px 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace} .dlsaved .dls-acts{display:flex;gap:8px} @@ -697,7 +935,7 @@ button.danger:hover{background:rgba(241,112,123,.14);color:#f5c2c7} .xf-bar i{display:block;height:100%;background:var(--grad);border-radius:999px;transition:width .25s} .xf-meta{display:flex;align-items:center;gap:8px;margin-top:8px;font-size:11px;color:var(--text-2)} .xf-fails{margin-top:10px;display:flex;flex-direction:column;gap:5px;max-height:130px;overflow:auto} -.xf-fail{font-size:11px;background:rgba(200,70,70,.1);border:1px solid rgba(200,70,70,.28);border-radius:8px;padding:6px 8px} +.xf-fail{font-size:11px;background:color-mix(in srgb,var(--bad) 10%,transparent);border:1px solid color-mix(in srgb,var(--bad) 28%,transparent);border-radius:8px;padding:6px 8px} .xf-fail b{display:block;color:var(--text);word-break:break-all} .xf-fail span{color:var(--text-3)} .xfer .dlsaved{margin-top:11px} @@ -714,7 +952,7 @@ button.danger:hover{background:rgba(241,112,123,.14);color:#f5c2c7} .entrow input,.entrow select{background:var(--surface);border:1px solid var(--stroke-2);border-radius:8px;padding:7px 9px;color:var(--text);font-size:12px;outline:none;min-width:0} .entrow input:focus,.entrow select:focus{border-color:var(--accent);box-shadow:var(--glow)} .entrow .epx{padding:5px 0;text-align:center} -.dlg-err{color:#f1a3aa;font-size:11.5px} +.dlg-err{color:var(--bad);font-size:11.5px} .sgfield{margin-bottom:16px} .sgfield .sglabel{display:block;font-size:11px;color:var(--text-2);font-weight:600;text-transform:uppercase;letter-spacing:.4px;margin-bottom:9px} .sgperms{display:grid;grid-template-columns:repeat(auto-fit,minmax(118px,1fr));gap:9px} @@ -723,44 +961,270 @@ button.danger:hover{background:rgba(241,112,123,.14);color:#f5c2c7} .sgperm input{position:absolute;left:12px;top:13px;width:15px;height:15px;accent-color:var(--accent)} .sgperm .sgpttl{font-size:12.5px;font-weight:620;color:var(--text)} .sgperm .sgpdesc{font-size:10.5px;color:var(--text-3)} -.sgperm:has(input:checked){border-color:var(--accent);background:rgba(71,158,245,.1)} +.sgperm:has(input:checked){border-color:var(--accent);background:color-mix(in srgb,var(--accent) 10%,transparent)} .sgexp{display:flex;gap:8px;flex-wrap:wrap} .sgchip{font-size:12px;font-weight:600;padding:7px 14px;border-radius:999px;border:1px solid var(--stroke-2);background:var(--surface-2);color:var(--text-2);cursor:pointer;transition:.13s} .sgchip:hover{border-color:var(--accent);color:var(--text)} -.sgchip.active{background:var(--grad);border-color:transparent;color:#fff} -.sgresult{margin-top:4px;padding:13px 14px;border:1px solid rgba(64,190,120,.4);background:linear-gradient(180deg,rgba(52,150,95,.14),rgba(40,120,80,.07));border-radius:12px} +.sgchip.active{background:var(--grad);border-color:transparent;color:var(--color-white,#fff)} +.sgresult{margin-top:4px;padding:13px 14px;border:1px solid color-mix(in srgb,var(--ok) 40%,transparent);background:linear-gradient(180deg,color-mix(in srgb,var(--ok) 14%,transparent),color-mix(in srgb,var(--ok) 7%,transparent));border-radius:12px} .sgresult .sgok{font-size:12px;font-weight:600;color:var(--ok);margin-bottom:9px;display:flex;align-items:center;gap:7px;flex-wrap:wrap} .sgresult .sgbadge{font-size:10px;font-weight:700;color:var(--text-2);background:var(--surface-2);border:1px solid var(--stroke-2);border-radius:999px;padding:2px 9px;text-transform:none;letter-spacing:0} .sgurl{width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--stroke-2);border-radius:9px;padding:9px 11px;color:var(--text);font:11.5px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;outline:none;resize:vertical;min-height:56px;word-break:break-all} -.footer-meta{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin:22px auto 0;padding:12px 2px 4px;border-top:1px solid var(--stroke);font-size:11.5px;color:var(--text-3)} -.build-stamp{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;letter-spacing:.01em} -.build-stamp .bs-rev{color:var(--text-2);opacity:.85} -.feedback-link{display:inline-flex;align-items:center;gap:6px;color:var(--accent);text-decoration:none;font-weight:600;font-family:inherit;border-radius:6px;padding:3px 6px} +.build-stamp{display:inline-flex;align-items:center;gap:8px;min-width:0;overflow-wrap:anywhere;font-family:inherit;font-size:11px;color:var(--text-3)} +.feedback-link{display:inline-flex;align-items:center;gap:6px;color:var(--accent);text-decoration:none;font-weight:400;font-family:inherit;font-size:12px;border-radius:6px;padding:3px 6px;white-space:nowrap} .feedback-link:hover{text-decoration:underline} .feedback-link:focus-visible{outline:2px solid var(--color-focus-outline,var(--accent));outline-offset:2px} +/* All UI surfaces derive from the host's documented semantic contract. */ +:root:root{ + --bg-2:color-mix(in srgb,var(--bg) 97%,var(--text)); + --panel:var(--bg);--surface:color-mix(in srgb,var(--bg) 96%,var(--text)); + --surface-2:color-mix(in srgb,var(--bg) 92%,var(--text)); + --surface-3:color-mix(in srgb,var(--bg) 86%,var(--text)); + --stroke-2:color-mix(in srgb,var(--stroke) 75%,var(--text)); + --text-1:var(--text);--text-2:color-mix(in srgb,var(--text) 85%,var(--bg)); + --accent:var(--true-color-blue,var(--azure));--accent-2:var(--accent); + --cyan:var(--accent);--brand:var(--accent); + --brand-hover:color-mix(in srgb,var(--accent) 85%,var(--text)); + --brand-press:color-mix(in srgb,var(--accent) 75%,var(--text)); + --accent-soft:var(--true-color-blue-muted,color-mix(in srgb,var(--accent) 12%,var(--bg))); + --sel:color-mix(in srgb,var(--accent) 16%,var(--bg)); + --on-accent:var(--color-white,#fff);--code-bg:var(--bg-2);--code-text:var(--text); + --bad:var(--true-color-red,#cf222e);--warn:var(--text-2);--ok:var(--accent); + --grad:linear-gradient(135deg,var(--accent),var(--brand-hover));--grad-soft:var(--accent-soft); + --glow:0 0 0 2px var(--color-focus-outline,var(--accent)); +} +body{font-family:var(--font-sans,-apple-system,"Segoe UI",sans-serif)} +button,input,select,textarea{font-family:inherit} +button:focus-visible,.card-head:focus-visible{outline:2px solid var(--color-focus-outline,var(--accent));outline-offset:2px} +.iconbtn,.railbtn{padding:0;line-height:1;flex:none} +.ti,.dti,.wt-ic,.cx-i{display:inline-flex;align-items:center;justify-content:center} +.railbtn .svi{width:22px;height:22px} +.dti .svi{width:22px;height:22px} +.wt-ic .svi{width:28px;height:28px} +.cx-i .svi{width:16px;height:16px} +.svcbar .svi{width:18px;height:18px} +.card-head h2{min-width:0;flex-wrap:wrap;font-weight:var(--font-weight-semibold,600)} +.list,.log,.mono,.dlpath,.sasm .c{background:var(--bg-2)} +.hicon,.hicon.g-sas,.hicon.g-xfer,.hicon.g-log,.hicon.g-ask,.ask-av,.xempty .xe-ic{background:var(--grad)} +.err,.xerr{color:var(--bad);background:var(--true-color-red-muted,color-mix(in srgb,var(--bad) 10%,var(--bg)));border-color:var(--bad)} +.err .eclose,.xerr-ic,.xm-warn .xm-v{color:var(--bad)} +.xerr.xwarn{color:var(--warn);background:color-mix(in srgb,var(--warn) 10%,var(--bg));border-color:var(--warn)} +.xwarn .xerr-ic{color:var(--warn)} +.err .etext{min-width:0;overflow-wrap:anywhere} +.subscription-errors{margin-top:8px;color:var(--text-2)} +.subscription-errors summary{cursor:pointer;color:var(--bad)} +.subscription-error{margin:8px 0} +.err.err-auth,.err.err-auth .eclose{color:var(--accent)} +.err.err-auth{background:var(--accent-soft);border-color:var(--accent)} +.chip.warn,.pill.warn,.xchip-warn{color:var(--warn);background:var(--surface-2);border-color:var(--stroke-2)} +.askchip>svg,.ask-abody code,.insight-box code,.xep-dst .xep-ic{color:var(--accent)} +[hidden]{display:none!important} +body{height:100vh;display:flex;flex-direction:column;overflow:hidden} +.appbar,#err{flex:none} +.ctx{flex:none;margin:0;padding:7px 20px;min-height:32px;box-sizing:border-box;border-left:0;border-bottom:1px solid var(--stroke)} +.ti>svg{width:15px;height:15px;display:block;flex:none} +.shell{flex:1;height:auto;min-height:0} +.main{padding-bottom:18px} +.debug-panel{flex:none;border-top:1px solid var(--stroke);background:var(--bg);min-height:0} +.debug-panel>summary{padding:5px 16px;font-size:12px;color:var(--text-2);cursor:pointer;user-select:none} +.debug-panel .log{margin:0;padding:8px 16px;border:0;border-top:1px solid var(--stroke);border-radius:0;max-height:min(24vh,200px);overflow:auto;overflow-wrap:anywhere;font-family:var(--font-mono,monospace)} +.diagnostics-link{padding:0 4px;min-height:24px;border:1px solid transparent;background:transparent;color:var(--text-2)} +.diagnostics-link:hover,.diagnostics-link:focus-visible{border-color:var(--stroke-2)} +.cx-sep{opacity:1;color:var(--text-2)} +.tw{display:grid;place-items:center;width:16px;height:16px;color:var(--text-2)} +.tw svg{width:14px;height:14px;pointer-events:none} +.tchildren{margin-left:12px;padding-left:8px} +.side.collapsed #visibilityBtn{display:none} +.tree-failure{color:var(--text-2);font-style:normal} +.tree-failure details{margin-top:4px} +.tree-failure pre{white-space:pre-wrap;overflow-wrap:anywhere;font:11px/1.4 var(--font-mono,monospace);max-height:120px;overflow:auto} +.visibility-copy{padding:0 16px;font-size:12px;color:var(--text-2)} +.visibility-row{display:flex;align-items:center;gap:6px;padding:5px 8px;font-size:12px;user-select:none} +.visibility-row.sub{padding-left:26px} +.visibility-row{white-space:nowrap;min-width:0;height:28px;box-sizing:border-box} +.visibility-row input{flex:none} +.visibility-label{overflow:hidden;text-overflow:ellipsis;min-width:0;flex:1} +.visibility-domain{color:var(--text-2);flex:0 1 42%;min-width:0;overflow:hidden;text-overflow:ellipsis} +.dialog .dlg-list{overflow:auto;min-height:0} +.xdestpick select,.selx-btn{min-height:28px;padding:4px 8px} +button,.trow,.card-head,.resource-head,.seg{user-select:none;-webkit-user-select:none} +button{min-height:28px;padding:4px 9px;border-radius:5px;font-size:12px;font-weight:400} +button svg,button .svi{width:16px;height:16px} +.iconbtn{width:28px;height:28px} +.side-head{min-height:42px;padding:6px 8px;gap:6px} +.side-head .t{font-size:13px} +.side.collapsed #refreshBtn{display:none} +.trow{height:30px;min-height:30px;box-sizing:border-box;padding:1px 5px;border-radius:4px;gap:5px} +.trow:focus-visible{outline:2px solid var(--color-focus-outline,var(--accent));outline-offset:-2px} +.tree-hint{font-size:11px;color:var(--text-2);padding:4px 6px 10px} +.tree-failure{white-space:normal;overflow-wrap:anywhere;color:var(--text-2)} +.resource-pane{min-width:0} +.resource-head{padding:4px 0;border-bottom:1px solid var(--stroke)} +.resource-head h2{display:flex;align-items:center;gap:6px;margin:0;font-size:14px;font-weight:600} +.resource-body{padding:4px 0} +.resource-actions{margin-top:10px} +.resource-search{display:block;width:100%;box-sizing:border-box;min-height:28px;margin:0 0 8px;padding:4px 8px;border:1px solid var(--stroke-2);border-radius:5px;background:var(--surface);color:var(--text)} +.visibility-tools{padding:0 16px 8px;display:flex;gap:6px;flex-wrap:wrap} +.visibility-tools input{width:100%;box-sizing:border-box;min-height:28px;padding:4px 8px;background:var(--surface);color:var(--text);border:1px solid var(--stroke-2);border-radius:5px} +.pinhdr .ti{font-size:17px;width:18px;height:18px} +.pinbtn{font-size:17px;min-width:26px;min-height:26px;padding:2px} +.pinbtn:focus-visible,.trow:focus-within .pinbtn{opacity:1} +.pinbtn svg,.pinhdr .ti svg{width:18px;height:18px;display:block} +.pinbtn.on svg,.pinhdr .ti svg{fill:currentColor} +.resource-toolbar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:8px 0} +.resource-toolbar .resource-search{flex:1;min-width:140px;width:auto;margin:0} +.resource-pane .vtool{gap:6px;justify-content:flex-start;margin:8px 0;padding:0;border:0} +.vtool input:not([type="file"]){box-sizing:border-box;height:28px;min-height:28px;min-width:140px;padding:4px 8px;font:inherit;font-size:12px} +.resource-pane button.primary,.resource-toolbar button,.browsetools button,.vtool button,.blobtab-back,.backlink{background:var(--surface);color:var(--text-2);border:1px solid var(--stroke-2);box-shadow:none;font-weight:400;border-radius:4px;min-height:28px;padding:4px 8px} +.resource-pane button.primary:hover,.resource-toolbar button:hover,.browsetools button:hover,.vtool button:hover{background:var(--surface-2);color:var(--text)} +.resource-pane button svg,.vtool button svg{width:16px;height:16px;vertical-align:middle;flex:none} +.blobnav{margin:8px 0} +.resource-path{display:flex;align-items:center;gap:6px;min-width:0;max-width:100%;margin:0;font-size:12px} +.resource-path .path-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.resource-path{flex-wrap:nowrap;padding:0;min-height:28px} +.path-link{background:transparent;border:1px solid transparent;color:var(--text);padding:3px 5px;font:inherit;cursor:pointer;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.path-link:hover,.path-link:focus-visible{border-color:var(--stroke-2);background:var(--surface-2)} +.blobtabgroup{display:flex;justify-content:flex-start;padding:0;gap:4px;border:0;border-radius:0;background:transparent;box-shadow:none} +.blobtab{flex:none;min-height:28px;font-size:12px;font-weight:400;padding:4px 8px;gap:5px;border-radius:4px;border:1px solid transparent} +.blobtab .tbi{width:16px;height:16px;background:transparent;border-radius:0} +.blobtab.active{background:var(--surface-2);color:var(--text);border-color:var(--stroke-2);box-shadow:none} +.blobtab.active .tbi{background:transparent} +.blob-check{width:14px;height:14px;margin:0 6px 0 0;flex:none} +button.resource-delete{display:inline-grid;place-items:center;width:28px;height:28px;padding:0;border:1px solid transparent;background:transparent;color:var(--text-2);box-shadow:none} +button.resource-delete:hover,button.resource-delete:focus-visible{border-color:var(--stroke-2);background:var(--surface-2);color:var(--bad)} +.agent-note{margin-top:12px;padding:10px;border-left:2px solid var(--accent);background:var(--surface-2)} +.agent-note h3{font-size:12px;margin:0 0 8px} +.card-head{padding:8px 10px} +.card-sub{margin:4px 0 12px} +.factions button,.browsetools button,.sasrow button{min-height:28px;padding:4px 9px;font-size:12px;font-weight:400} +button.primary,.factions button,.sasrow button{background:var(--surface);border:1px solid var(--stroke-2);color:var(--text-2);box-shadow:none;font-weight:400} +button.primary:hover,.factions button:hover,.sasrow button:hover{background:var(--surface-2);color:var(--text)} +.row .fname{display:flex;align-items:center;gap:6px;min-width:0} +.row .fname>svg{width:16px;height:16px;flex:none} +.row .meta{display:inline-flex;align-items:center;gap:4px} +.vtool .resource-search{flex:1;min-width:140px;width:auto;margin:0} +.resource-pane .vtool button.resource-delete{min-width:28px;min-height:28px;padding:0;border-color:transparent;background:transparent} +.resource-pane .vtool button.resource-delete:hover,.resource-pane .vtool button.resource-delete:focus-visible{border-color:var(--stroke-2);background:var(--surface-2)} +.resource-delete[data-blobsas]:hover,.resource-delete[data-blobsas]:focus-visible,.resource-delete[data-shdl]:hover,.resource-delete[data-shdl]:focus-visible{color:var(--text)} +.secta{flex-wrap:wrap} +.secta-tx{flex:1 1 220px} +.secta-btn{box-sizing:border-box;max-width:100%;min-width:0;min-height:28px;padding:4px 8px;border-radius:4px;font-weight:400;background:var(--surface);color:var(--text-2);overflow:hidden} +.sgperm .sgpttl{font-weight:400} +.svcbar{padding:3px;gap:3px;border-radius:5px} +.seg{padding:4px 6px;min-height:28px;border-radius:4px;font-weight:400} +.card-head .cw{width:24px;height:24px} +.card-head .cw svg{width:18px;height:18px} +.hicon{width:20px;height:20px;border-radius:4px;box-shadow:none;color:var(--color-white,#fff)} +.dti .svi,.svcbar .svi{width:16px;height:16px} +.resource-context{display:flex;flex-direction:column;align-items:stretch;gap:4px;margin:4px 0} +.resource-context .resource-path{flex:none;min-width:0} +.resource-context .search{flex:none;min-width:0;width:100%} +.resource-context .search input{box-sizing:border-box;width:100%} +.resource-path .backlink,.resource-path .path-link{display:inline-flex;align-items:center;gap:4px;background:transparent;border:1px solid transparent;color:var(--text);font:inherit;min-height:28px;padding:3px 5px;border-radius:4px;box-shadow:none} +.resource-path .backlink:hover,.resource-path .backlink:focus-visible,.resource-path .path-link:hover,.resource-path .path-link:focus-visible{border-color:var(--stroke-2);background:var(--surface-2)} +.resource-path>button{flex:none;white-space:nowrap} +.resource-path>.path-current{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;display:block} +.path-overflow{position:relative;flex:none} +.path-overflow summary{display:grid;place-items:center;width:28px;height:28px;cursor:pointer;border:1px solid var(--stroke-2);border-radius:4px;list-style:none} +.path-overflow summary::-webkit-details-marker{display:none} +.path-overflow-menu{position:absolute;left:0;top:100%;z-index:30;min-width:160px;max-width:min(360px,calc(100vw - 30px));max-height:240px;overflow:auto;padding:4px;border:1px solid var(--stroke-2);border-radius:4px;background:var(--panel);box-shadow:var(--shadow)} +.resource-path .path-overflow-menu button{display:block;width:100%;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.vtool button,.resource-toolbar button,.side-toolbar button{display:inline-flex;align-items:center;justify-content:center;gap:4px;box-sizing:border-box;min-height:28px;padding:4px 8px;font:inherit;font-size:12px;background:var(--surface);border:1px solid var(--stroke-2);color:var(--text-2);border-radius:4px;box-shadow:none} +.resource-pane .vtool button:disabled{opacity:.45;cursor:default} +.mainwrap{display:flex;flex-direction:column;height:100%;min-height:100%;gap:8px} +.mainwrap>.card{flex-shrink:0} +.resource-pane{display:flex;flex-direction:column;flex:1 0 440px;min-height:0} +.resource-body{display:flex;flex-direction:column;flex:1;min-height:0} +#dataBody{display:flex;flex-direction:column;flex:1;min-height:0} +.resource-list{flex:1;min-height:240px;height:0;overflow:auto;border:1px solid var(--stroke);border-radius:5px} +#svcBar,.resource-context,.resource-toolbar,.browsetools{flex-shrink:0} +.resource-list .list{border:0;border-radius:0;margin:0;max-height:none;overflow:visible;background:transparent} +.resource-list .row{display:grid;grid-template-columns:minmax(0,1fr) minmax(70px,auto);align-items:center;min-height:40px;font-size:12px} +.resource-list .fname{overflow:hidden} +.resource-list .fname>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.row-check-space{width:20px;flex:none} +.resource-list-header{display:flex;justify-content:space-between;gap:12px;padding:6px 9px;border-bottom:1px solid var(--stroke);font-size:11px;color:var(--text-3);position:sticky;top:0;background:var(--panel);z-index:1} +.resource-list .row,#dataBody:not(.table-view) tbody td,.sas-tbl td{border-bottom:0} +.storage-pager{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:8px 0;flex:none;font-size:12px;color:var(--text-3)} +.storage-pager .page-note{margin-left:auto;font-size:11px} +.entity-viewport{position:relative;overflow:auto;min-height:240px;flex:1;contain:strict;overflow-anchor:none;border:1px solid var(--stroke);border-radius:5px} +.entity-spacer{position:relative} +.entity-window{position:absolute;left:0;top:0} +.entity-vrow{position:absolute;display:flex;height:40px} +.entity-vcell{box-sizing:border-box;width:160px;height:40px;padding:9px;font-size:12px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;border-right:1px solid var(--stroke);border-bottom:1px solid var(--stroke)} +.entity-vhead{position:absolute;display:flex;height:36px;background:var(--panel);z-index:2} +.entity-vhead .entity-vcell{height:36px;color:var(--text-3);background:var(--panel);text-transform:none} +th,.xtbl th,.ask-abody th,.resource-list-header{text-transform:none;letter-spacing:normal} +.storage-pager{display:flex;align-items:center;gap:8px} +.page-count{margin-right:auto;color:var(--text-3)} +.page-buttons{display:flex;align-items:center;gap:5px} +.page-buttons button{display:inline-flex;align-items:center;justify-content:center;min-width:28px;height:28px;padding:4px} +.page-buttons button[hidden]{display:none} +.page-buttons svg{width:16px;height:16px} +.entity-vhead .entity-vcell,.resource-list-header{font-size:11px;font-weight:400;color:var(--text-3);background:var(--panel)} +.resource-list-header{height:36px;box-sizing:border-box;align-items:center} +.resource-list table{width:100%;border-collapse:collapse;background:transparent} +.resource-list th{height:36px;box-sizing:border-box;padding:6px 9px;font-size:11px;font-weight:400;background:var(--panel);position:sticky;top:0;z-index:1} +.resource-list td{height:40px;box-sizing:border-box;padding:9px;font-size:12px;color:var(--text);background:transparent;vertical-align:middle} +.resource-list tbody tr:hover{background:var(--surface-2)} +.resource-list .row.sel,.resource-list tr:has(input:checked),.entity-vrow[aria-selected="true"]{background:var(--surface-2);box-shadow:inset 2px 0 0 var(--accent)} +.entity-vrow:hover{background:var(--surface-2)} +.resource-list .message-id{font:inherit;color:var(--text);white-space:nowrap} +.resource-list td .resource-delete{vertical-align:middle} +.resource-context{margin:4px 0;gap:4px} +.resource-path .backlink{width:28px;height:28px;padding:5px;box-sizing:border-box} +.resource-path .backlink svg{width:16px;height:16px} +.resource-path .backlink:disabled{opacity:.45;cursor:default} +.page-buttons button[aria-busy="true"] svg{animation:sp .8s linear infinite} +@media(prefers-reduced-motion:reduce){.page-buttons button[aria-busy="true"] svg{animation:none}} +.entity-viewport+.storage-pager{flex:none} +.entity-vrow:focus-visible{outline:2px solid var(--color-focus-outline,var(--accent));outline-offset:-2px} +.resource-list-count{padding:4px 9px;color:var(--text-3);font-size:11px} +.resource-list.contents .resource-list-header,.resource-list.contents .row{display:grid;grid-template-columns:minmax(120px,1fr) 80px 150px;gap:8px;padding-left:9px;padding-right:9px} +.resource-list.contents .item-size{text-align:right} +.resource-list.contents .item-date{white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.resource-list.contents .item-actions{display:flex;justify-content:center} +.svcbar,.card .seg:has(label){display:flex;padding:0;border:0;border-bottom:1px solid var(--stroke);border-radius:0;background:transparent;box-shadow:none} +.svcbar>.seg,.card .seg label{position:relative;flex:1;display:inline-flex;align-items:center;justify-content:center;gap:6px;margin:0;min-height:32px;padding:5px 8px;border:0;border-radius:0;border-right:1px solid var(--stroke);background:transparent;color:var(--text-2);font:inherit;font-size:12px;box-shadow:none;white-space:nowrap} +.svcbar>.seg:last-child,.card .seg label:last-child{border-right:0} +.svcbar>.seg:hover,.card .seg label:hover{background:var(--surface-2);color:var(--text)} +.svcbar>.seg.act,.card .seg label:has(input:checked){background:transparent;color:var(--text);font-weight:400;box-shadow:inset 0 -2px 0 var(--accent)} +.svcbar>.seg:focus-visible,.card .seg label:has(input:focus-visible){outline:2px solid var(--color-focus-outline,var(--accent));outline-offset:-4px} +.seg.act .segn,.segn{background:transparent;color:var(--text-3);border:0;font-weight:400} +.card .field>label,.card .xlbl,.card .xsrc-lbl,.card .xpolicy,.card .xm-l,.card .plabel,.card .sasm .c span,.card .acthdr span,.card .askchip-lbl,.card .metric .mk,.card .sect,.sgfield .sglabel,.entprops-head span{font-family:inherit;text-transform:none;letter-spacing:normal;font-weight:400;font-size:12px} +.card .sasrow input{font-family:inherit} +.xradio{display:flex;flex-wrap:wrap;gap:8px 20px;margin:6px 0 10px} +.xradio label{display:inline-flex;align-items:center;gap:6px;cursor:pointer;font:inherit;color:var(--text-2)} +.xradio input{position:static;opacity:1;pointer-events:auto;width:16px;height:16px;margin:0;accent-color:var(--accent);flex:none} +.xradio input:focus-visible{outline:2px solid var(--color-focus-outline,var(--accent));outline-offset:2px} +@media(max-width:600px){.resource-list.contents .resource-list-header,.resource-list.contents .row{grid-template-columns:minmax(100px,1fr) 65px 115px;gap:6px}.resource-list.contents{overflow:auto}.appbar{flex-wrap:wrap;gap:6px;padding:8px}.appbar h1{font-size:14px}.appbar .build-stamp{flex-direction:column;align-items:flex-start;gap:0;font-size:10px}.appbar .feedback-link{margin-left:auto;padding:3px;font-size:11px}} +@media(max-width:600px){ + .shell{grid-template-columns:minmax(160px,40%) minmax(0,1fr)} + .main{padding:12px 10px 40px} + .card-head{padding:10px;gap:6px} + .card-body{padding:10px} + .svcbar{flex-wrap:wrap} + .ctx{display:flex} +} `,BODY=`
- +

Azure Storage

-
+
- starting\u2026 +
-
- -
-

Ask about your storage

\u25BE
-
-
-
- - -
- -
-
-
-
-
- -
-

Storage data

\u25BE
-
+
+ +
-
-

Filter & statistics

No container selected\u25BE
+
+

${uiIcon("statistics",{size:15})} Filter & statistics

${uiIcon("down")}
@@ -814,10 +1261,10 @@ button.danger:hover{background:rgba(241,112,123,.14);color:#f5c2c7}
- - - - + + + +
@@ -828,14 +1275,19 @@ button.danger:hover{background:rgba(241,112,123,.14);color:#f5c2c7}
-
-

Instant SAS

\u25BE
+
+

${uiIcon("link",{size:16})} Share

${uiIcon("down")}
+

Create a time-limited SAS link for the open blob container. Choose permissions and expiration before creating the link.

+
+ +
+

${uiIcon("link",{size:16})} Instant SAS

${uiIcon("down")}
- +
Paste a container SAS URL for an Explorer-style read view: list, filter, inspect sizes/tiers and download, all from the SAS alone. No Azure sign-in required.
@@ -844,82 +1296,74 @@ button.danger:hover{background:rgba(241,112,123,.14);color:#f5c2c7}
-
-

Transfer planning

\u25BE
+
+

${uiIcon("transfer",{size:15})} Transfer

${uiIcon("down")}
-

Preview an AzCopy copy from the open container to another container or SAS URL: see new vs. conflicting files, choose how conflicts resolve, and get the exact azcopy command. Nothing moves until you run it yourself.

+

Preview an AzCopy copy between containers using loaded accounts or container SAS URLs. Review conflicts and get the exact azcopy command. Nothing moves until you run it yourself.

-
+
+
+
/
+ +
-
+
/
- +
-
- - - +
+ + +
- + - +
-
-

Activity log

0 entries\u25BE
-
-
-
+
+ Debug log +
+
+ -
+
-
-
-

Select subscriptions

-

Choose one or more subscriptions. Storage accounts load from your selection.

-
- -
- -
-
-
- 0 selected -
- - -
-
+

Visible directories and subscriptions

+

Choose what appears in the Explorer. This does not change Azure access or sign you in.

+
+
+

Confirm

- +
-

Add entity

PartitionKey and RowKey identify the entity. Add any number of typed properties.

+

Add entity

PartitionKey and RowKey identify the entity. Add any number of typed properties.

-
Properties
+
Properties
@@ -928,14 +1372,14 @@ button.danger:hover{background:rgba(241,112,123,.14);color:#f5c2c7}
-

\u{1F517} Create SAS link

A secure, time-limited URL. No Azure login needed to use it.

+

Create SAS link

A secure, time-limited URL. No Azure login needed to use it.

What can the link do?
- - - - + + + +
Expires in @@ -952,6 +1396,10 @@ button.danger:hover{background:rgba(241,112,123,.14);color:#f5c2c7}
`,CLIENT=` +${humanErrorSummary.toString()} +const UI_ICONS=${JSON.stringify(UI_ICONS)}; +${uiIcon.toString()} +var RESOURCE_ICONS=${JSON.stringify(RESOURCE_ICONS)}; const $=function(id){return document.getElementById(id)}; function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c]})} function mdInline(s){ @@ -964,52 +1412,43 @@ function mdInline(s){ return t; } function fmtBytes(n){n=Number(n)||0;if(n<1024)return n+' B';var u=['KB','MB','GB','TB','PB'],i=-1;do{n/=1024;i++}while(n>=1024&&i=10?0:1)+' '+u[i]} -var bridge={send:function(route,payload){return fetch(route,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload||{})}).then(function(r){return r.json()}).catch(function(){return{ok:false}})}}; -var state={};var selected=new Set(); +var bridge={send:function(route,payload){return fetch(route,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload||{})}).then(function(r){if(!r.ok)throw new Error('Canvas request failed: HTTP '+r.status);return r.json()}).catch(function(error){state.s_error=error.message;renderError();return{ok:false,error:error.message}})}}; +var state={};var selected=new Set();var selectedFiles=new Set(); var openSubs={}; -var openAccts={}; -var openCat={}; var treeQuery=''; function ascLoad(k){try{return JSON.parse(localStorage.getItem(k)||'null')||{}}catch(e){return{}}} +var hiddenTenants={},hiddenSubscriptions={}; var pinnedAccts=ascLoad('asc_pinned'); -var hiddenAccts=ascLoad('asc_hidden'); -var showHidden=false; -function saveAcctPrefs(){try{localStorage.setItem('asc_pinned',JSON.stringify(pinnedAccts));localStorage.setItem('asc_hidden',JSON.stringify(hiddenAccts))}catch(e){}try{bridge.send('/save-prefs',{pinned:pinnedAccts,hidden:hiddenAccts})}catch(e){}} -var activeSvc=null;var svcListMode=false;var lastAcct='';var lastItemKey='';var lastPrevName=''; -var blobTab='browse'; +function saveAcctPrefs(){try{localStorage.setItem('asc_pinned',JSON.stringify(pinnedAccts))}catch(e){}try{bridge.send('/save-prefs',{pinned:pinnedAccts})}catch(e){}} +var activeSvc=null;var svcListMode=false;var lastAcct='';var lastItemKey='';var lastResourceKey='';var lastPrevName=''; +var selectedRecords=new Set(),lastRecordsScope='',sasSelected=new Set(),sasSelectionScope='',sasDownloading=false; +var queueDrafts={}; +function entityKey(row){return JSON.stringify([row.partitionKey,row.rowKey])} var xSourceCont=''; -var xDestMode='pick';var xDestAcct='';var xDestCont='';var xDestReq={}; -var pickOpen=false;var pickSel=new Set();var pickQuery=''; +var xSourceMode='pick';var xDestMode='pick';var xDestAcct='';var xDestCont='';var xDestReq={}; +var resourceQueries={}; var sasFilterText='';var sasSort='name'; var es=new EventSource('/events'); -es.onmessage=function(ev){try{state=JSON.parse(ev.data)}catch(e){return}render()}; -(function(){bridge.send('/get-prefs',{}).then(function(r){if(r&&r.ok&&r.prefs){if(r.prefs.pinned)pinnedAccts=r.prefs.pinned;if(r.prefs.hidden)hiddenAccts=r.prefs.hidden;try{localStorage.setItem('asc_pinned',JSON.stringify(pinnedAccts));localStorage.setItem('asc_hidden',JSON.stringify(hiddenAccts))}catch(e){}if(typeof renderTree==='function')renderTree()}})})(); +es.onmessage=function(ev){try{var next=JSON.parse(ev.data);if(next.s_entitiesUnchanged)next.s_entities=state.s_entities;state=next}catch(e){return}render()}; +(function(){bridge.send('/get-prefs',{}).then(function(r){if(r&&r.ok&&r.prefs){if(r.prefs.pinned)pinnedAccts=r.prefs.pinned;hiddenTenants=r.prefs.hiddenTenants||{};hiddenSubscriptions=r.prefs.hiddenSubscriptions||{};try{localStorage.setItem('asc_pinned',JSON.stringify(pinnedAccts))}catch(e){}if(typeof renderTree==='function')renderTree()}})})(); -var lastStatCont=''; -function render(){renderCtx();renderError();renderStatus();renderSeBtn();renderScope();renderTree();renderCrumb();renderData();renderBatch();resetFilterOnSwitch();renderStatsScope();renderStats();renderInsight();renderSas();renderTransferSrc();renderXDest();renderTransfer();renderAskChips();renderAsk();renderLog();if(pickOpen)renderPicker()} -function renderCtx(){ - var el=$('ctxBar');if(!el)return; - if(!state.s_account){el.className='ctx';el.innerHTML='No storage account open';return} - var svc=activeSvc||'blob'; - var showItem=state.s_item&&state.s_kind===svc&&!svcListMode; - var kindName={blob:'Container',file:'File share',queue:'Queue',table:'Table'}[svc]||'Item'; - var svcName={blob:'Blob containers',file:'File shares',queue:'Queues',table:'Tables'}[svc]||'Data'; - var sep='\u203A'; - var sideEl=$('side');var collapsed=sideEl&&sideEl.classList.contains('collapsed'); - var h=''; - if(collapsed){ - var acct=(state.s_accounts||[]).filter(function(x){return x.name===state.s_account})[0]||{}; - var sub=acct.subscriptionName||''; - if(sub)h+=''+svgIcon('sub')+''+esc(sub)+''+sep; - } - h+=''+svgIcon('account')+''+esc(state.s_account)+''; - if(showItem){h+=sep+''+svgIcon(svc)+''+esc(state.s_item)+''} - else{h+=sep+''+svgIcon(svc)+''+esc(svcName)+''} - el.className='ctx on';el.innerHTML=h; -} +var lastFilterScope=null,lastFilterCriteria=null; +function render(){renderError();renderStatus();renderSeBtn();renderTree();renderData(true);renderBatch();syncAppliedFilter();renderStats();renderInsight();renderSas();renderTransferSrc();renderXDest();renderTransfer();renderAsk();renderLog();updateStoragePagers()} function renderTransferSrc(){ + $('xsrcpick').hidden=xSourceMode!=='pick';$('xsrcpaste').hidden=xSourceMode!=='sas'; + $('xfiltered').disabled=xSourceMode==='sas'; + if(xSourceMode==='sas')$('xfiltered').checked=false; + Array.prototype.forEach.call(document.getElementsByName('xsrcmode'),function(r){r.checked=r.value===xSourceMode;r.onchange=function(){if(r.checked){xSourceMode=r.value;renderTransferSrc();renderXDest()}}}); + if(xSourceMode==='sas')return; var sel=$('xsrcsel'),banner=$('xsrc');var acct=state.s_account; + var sourceAccount=$('xsrcacct'),accts=(state.s_accounts||[]).map(function(x){return x.name}).sort(); + if(sourceAccount){ + sourceAccount.innerHTML=accts.map(function(n){return ''}).join('')||''; + sourceAccount.disabled=!accts.length;sourceAccount.value=acct||''; + sourceAccount.onchange=function(){xSourceCont='';bridge.send('/account',{account:sourceAccount.value})}; + selxEnhance(sourceAccount); + } var tree=(state.s_tree||{})[acct]||{};var conts=tree.containers||[]; var names=conts.map(function(c){return c.name}); if(sel){ @@ -1018,15 +1457,15 @@ function renderTransferSrc(){ else{ if(!xSourceCont||names.indexOf(xSourceCont)<0){xSourceCont=(state.s_container&&names.indexOf(state.s_container)>=0)?state.s_container:names[0]} sel.disabled=false; - sel.innerHTML=names.map(function(n){return ''}).join(''); + sel.innerHTML=names.map(function(n){return ''}).join(''); sel.value=xSourceCont; } - sel.onchange=function(){xSourceCont=sel.value;renderTransferSrc()}; + sel.onchange=function(){xSourceCont=sel.value;renderTransferSrc();renderXDest()}; selxEnhance(sel); } if(banner){ - if(!acct){banner.style.display='';banner.className='xsrc off';banner.innerHTML=''+CONTAINER_IC+'Open a storage account in the Explorer to choose a source container.';return} - if(!names.length){banner.style.display='';banner.className='xsrc off';banner.innerHTML=''+CONTAINER_IC+'This account has no blob containers to transfer from.';return} + if(!acct){banner.style.display='';banner.className='xsrc off';banner.innerHTML=''+svgIcon('blob')+'Open a storage account in the Explorer to choose a source container.';return} + if(!names.length){banner.style.display='';banner.className='xsrc off';banner.innerHTML=''+svgIcon('blob')+'This account has no blob containers to transfer from.';return} banner.style.display='none';banner.innerHTML=''; } } @@ -1037,7 +1476,8 @@ function renderXDest(){ if(paste)paste.style.display=mode==='paste'?'':'none'; Array.prototype.forEach.call(document.getElementsByName('xdestmode'),function(r){r.checked=(r.value===mode);r.onchange=function(){if(r.checked){xDestMode=r.value;renderXDest()}}}); if(mode!=='pick')return; - var accts=(state.s_accounts||[]).map(function(x){return x.name}); + var accts=state.s_account?(state.s_accounts||[]).map(function(x){return x.name}):[]; + if(!state.s_account){xDestAcct='';xDestCont=''} var aSel=$('xdestAcct'),cSel=$('xdestCont'); if(aSel){ if(!accts.length){aSel.innerHTML='';aSel.disabled=true} @@ -1058,7 +1498,7 @@ function renderXDest(){ if(!xDestReq[xDestAcct]){xDestReq[xDestAcct]=true;bridge.send('/tree-containers',{account:xDestAcct})} } else{ - var names=conts.map(function(c){return c.name}).filter(function(n){return !(xDestAcct===state.s_account&&n===xSourceCont)}); + var names=conts.map(function(c){return c.name}).filter(function(n){return !(xSourceMode==='pick'&&xDestAcct===state.s_account&&n===xSourceCont)}); if(!names.length){cSel.innerHTML='';cSel.disabled=true;xDestCont=''} else{ if(!xDestCont||names.indexOf(xDestCont)<0)xDestCont=names[0]; @@ -1071,8 +1511,6 @@ function renderXDest(){ selxEnhance(cSel); } } -var SELX_CARET=''; -var SELX_CHECK=''; var selxOpen=null; function selxCloseAll(){if(selxOpen){selxOpen.host.classList.remove('open');selxOpen.pop.classList.remove('show');selxOpen=null}} function selxPlace(w){ @@ -1087,7 +1525,7 @@ function selxRefresh(sel){ var w=sel.__selx;if(!w)return; var cur=sel.options[sel.selectedIndex]; w.btn.disabled=sel.disabled;w.lbl.textContent=cur?cur.textContent:''; - var html='';for(var i=0;i'+SELX_CHECK+''+esc(o.textContent)+'
'} + var html='';for(var i=0;i'+uiIcon('check',{size:14})+''+esc(o.textContent)+'
'} w.pop.innerHTML=html; if(selxOpen===w)selxPlace(w); } @@ -1097,7 +1535,7 @@ function selxEnhance(sel){ sel.parentNode.insertBefore(host,sel);host.appendChild(sel);sel.classList.add('selx-native'); var btn=document.createElement('button');btn.type='button';btn.className='selx-btn'; var lbl=document.createElement('span');lbl.className='selx-txt'; - btn.appendChild(lbl);btn.insertAdjacentHTML('beforeend',SELX_CARET);host.appendChild(btn); + btn.appendChild(lbl);btn.insertAdjacentHTML('beforeend',uiIcon('down',{size:14,className:'selx-car'}));host.appendChild(btn); var pop=document.createElement('div');pop.className='selx-pop';document.body.appendChild(pop); var w={host:host,btn:btn,lbl:lbl,pop:pop,sel:sel};sel.__selx=w; btn.onclick=function(e){e.stopPropagation();if(sel.disabled)return;selxToggle(w)}; @@ -1107,16 +1545,22 @@ function selxEnhance(sel){ document.addEventListener('click',function(e){if(selxOpen&&selxOpen.pop.contains(e.target))return;selxCloseAll()}); window.addEventListener('resize',function(){if(selxOpen)selxPlace(selxOpen)}); window.addEventListener('scroll',function(e){if(!selxOpen)return;if(selxOpen.pop.contains(e.target))return;var r=selxOpen.btn.getBoundingClientRect();if(r.bottom<4||r.top>window.innerHeight-4){selxCloseAll();return}selxPlace(selxOpen)},true); -function renderStatsScope(){var el=$('statsScope');if(!el)return;if(containerOpen()){el.className='scopechip on';el.innerHTML=''+esc(state.s_account)+' / '+esc(state.s_container)+''}else{el.className='scopechip off';el.textContent='No container selected'}} -function resetFilterOnSwitch(){var c=(state.s_kind==='blob'?state.s_container:'')||'';if(c===lastStatCont)return;lastStatCont=c;['fName','fMin','fMax','fOlder','fNewer','fTag'].forEach(function(id){var e=$(id);if(e)e.value=''});['fType','fTier','fBlobType'].forEach(function(id){var e=$(id);if(e)e.selectedIndex=0});} +function syncAppliedFilter(){ + var scope=JSON.stringify([state.s_account,state.s_kind,state.s_container,state.s_blobPrefix||'']); + var criteria=state.s_kind==='blob'?state.s_criteria:null; + var signature=JSON.stringify(criteria||{}); + if(scope===lastFilterScope&&signature===lastFilterCriteria)return; + lastFilterScope=scope;lastFilterCriteria=signature; + syncFilterForm(criteria); +} function renderStatus(){ var c=$('statusChip');if(!c)return;var a=state.s_accounts||[]; - if(state.s_loading&&!a.length){c.className='chip';c.innerHTML=' loading\u2026';return} - if(state.s_signingIn){c.className='chip';c.innerHTML=' signing in\u2026';return} + if((state.s_accountsLoading||state.s_subsLoading)&&!a.length){c.className='chip';c.innerHTML=''+uiIcon('busy',{size:13,className:'spin'})+' loading\u2026';return} + if(a.length){c.className='chip';c.textContent=a.length+(a.length===1?' loaded account':' loaded accounts');c.title='Accounts returned across loaded subscriptions, including hidden Explorer scopes.';return} + if(state.s_signingIn){c.className='chip';c.innerHTML=''+uiIcon('busy',{size:13,className:'spin'})+' signing in\u2026';return} if(state.s_needsAuth){c.className='chip warn';c.textContent='not signed in';return} if(state.s_error){c.className='chip warn';c.textContent='error';return} - if(a.length){c.className='chip';c.textContent=a.length+(a.length===1?' account':' accounts');return} c.className='chip';c.textContent='no accounts'; } // The ASE hand-off is a quiet escape hatch, not a front door: muted/icon-only @@ -1143,16 +1587,13 @@ function renderSeBtn(){ } } -// The official Azure Storage Explorer product mark, as an inline SVG at any size. -function aseIcon(sz){var s=sz||16;return '';} - // A contextual hand-off strip, shown only where the canvas reaches its own limits // (nested folders, whole-folder or bulk upload, access tiers, metadata, snapshots). // The button label flips to "Get" when we know the desktop app is not installed. function seCtaHtml(title,msg){ var absent=state.s_seInstalled===false; var lab=absent?'Get Azure Storage Explorer':'Open in Azure Storage Explorer'; - return '
'+aseIcon(22)+'
'+esc(title)+''+esc(msg)+'
'; + return '
'+uiIcon('explorer',{size:22})+'
'+esc(title)+''+esc(msg)+'
'; } // Launch the desktop app (or its download page when absent). Shared by the @@ -1171,180 +1612,235 @@ function wireSeCta(el){ Array.prototype.forEach.call(el.querySelectorAll('[data-seopen]'),function(btn){btn.onclick=function(e){e.stopPropagation();openSE()}}); } -function renderScope(){ - var lab=$('scopeLabel');if(!lab)return; - var sel=state.s_selectedSubs||[]; - if(!sel.length){lab.textContent='Select subscriptions';lab.title='';return} - var base=sel.length+(sel.length===1?' subscription':' subscriptions'); - var accts=state.s_accounts||[]; - var withAcct={};accts.forEach(function(a){if(a.subscriptionName)withAcct[a.subscriptionName]=1}); - var n=Object.keys(withAcct).length; - if(!state.s_loading&&accts.length&&n'+n+' with accounts'; - lab.title=base+' selected, '+n+' contain storage accounts'; - } else {lab.textContent=base;lab.title=base;} +function renderError(){ + var e=$('err');if(!e)return; + if(state.s_signingIn){e.innerHTML='
'+uiIcon('busy',{size:13,className:'spin'})+' Opening az login\u2026 complete the sign-in in your browser window.
';return} + if(!state.s_error){e.innerHTML='';return} + var authBtn=state.s_needsAuth?'':''; + var failures=state.s_subscriptionErrors||[]; + var summary=failures.length?failures.length+' subscription(s) could not be read. Check the affected subscriptions in the Explorer.':humanErrorSummary(state.s_error,state.s_needsAuth); + var context=state.s_account&&!failures.length?state.s_account+': ':''; + e.innerHTML='
'+esc(context+summary)+'
'+authBtn+'
'; + wireDiagnostics(e); + var s=$('errSignin');if(s)s.onclick=function(){doSignin()}; + var b=$('errClose');if(b)b.onclick=function(){state.s_error=null;state.s_needsAuth=false;renderError();bridge.send('/dismiss-error',{})}; } -function renderError(){var e=$('err');if(!e)return;if(state.s_signingIn){e.innerHTML='
Opening az login\u2026 complete the sign-in in your browser window.
';return}if(state.s_error){var authBtn=state.s_needsAuth?'':'';var extra=state.s_needsAuth?'. Sign in to continue.':'';e.innerHTML='
\u26A0 '+esc(state.s_error)+extra+''+authBtn+'
';var s=$('errSignin');if(s)s.onclick=function(){doSignin()};var b=$('errClose');if(b)b.onclick=function(){state.s_error=null;state.s_needsAuth=false;renderError();bridge.send('/dismiss-error',{})}}else e.innerHTML=''} function doSignin(){state.s_signingIn=true;renderError();var p=bridge.send('/sign-in',{});if(p&&p.then)p.then(function(r){state.s_signingIn=false;if(r&&r.ok===false)renderError()})} function svgIcon(kind){ - var col={account:'#4db8ff',blob:'#4fc3f0',file:'#f2c661',queue:'#b18cf5',table:'#f0885a',sub:'#f2c661'}[kind]||'#9db1d6'; - var g={ - account:'', - sub:'', - blob:'', - file:'', - queue:'', - table:'' - }; - return ''+(g[kind]||g.blob)+''; + if(kind==='tenant')return ${JSON.stringify(TENANT_ICON)}; + return ''; } -function catNode(account,kind,label,items,loading){ - var key=account+'|'+kind; - var open=!!openCat[key]; - var ea=esc(account); - var h='
'; - h+='
\u25B6'+svgIcon(kind)+''+label+''+(items&&items.length?''+items.length+'':'')+'
'; - if(open){ - h+='
'; - if(loading){h+='
loading\u2026
'} - else if(!items||!items.length){h+='
None
'} - else{items.forEach(function(it){ - var isSel=(state.s_account===account&&state.s_kind===kind&&state.s_item===it.name); - var meta=''; - if(kind==='queue'&&it.approximateMessagesCount!=null)meta=''+it.approximateMessagesCount+''; - if(kind==='file'&&it.quotaGiB!=null)meta=''+it.quotaGiB+' GiB'; - h+='
'+svgIcon(kind)+''+esc(it.name)+''+meta+'
'; - })} - h+='
'; +function resourcePath(kind,item){ + var path='
'; + if(kind==='blob'){ + path+=''; + var prefix=state.s_blobPrefix||'',offset=0; + prefix.split('/').slice(0,-1).forEach(function(part){offset+=part.length+1;path+=''}); + return path+'
'; } - h+='
'; - return h; + if(kind!=='file')return path+''+esc(item)+'
'; + path+=''; + var parts=(state.s_fileDir||'').split('/').filter(Boolean); + parts.forEach(function(part,index){path+=''}); + return path+'
'; +} +function searchControl(id,label,value){ + return ''; +} +function fitResourcePath(){ + var path=$('dataBody').querySelector('.resource-path');if(!path)return; + var width=path.clientWidth;if(path.dataset.fitWidth===String(width))return;path.dataset.fitWidth=String(width); + var old=path.querySelector('.path-overflow');if(old)old.remove(); + var children=Array.prototype.slice.call(path.children);if(!children.length)return; + children.forEach(function(el){el.hidden=false;el.classList.remove('path-current')}); + var current=children[children.length-1];current.classList.add('path-current'); + current.title=current.getAttribute('data-blob-path')||current.getAttribute('data-file-path')||current.textContent.trim(); + var ancestors=children.filter(function(el){return el.tagName==='BUTTON'&&el!==current&&!el.classList.contains('backlink')}); + if(!ancestors.length)return; + var overflow=document.createElement('details');overflow.className='path-overflow';overflow.hidden=true; + overflow.innerHTML=''+uiIcon('more')+'
'; + var back=path.querySelector('.backlink');path.insertBefore(overflow,back?back.nextSibling:path.firstChild); + var menu=overflow.querySelector('.path-overflow-menu'); + ancestors.forEach(function(button){ + if(path.scrollWidth<=path.clientWidth&¤t.clientWidth>=Math.min(120,current.scrollWidth))return; + overflow.hidden=false;button.hidden=true; + var separator=button.previousElementSibling;if(separator&&separator.tagName==='SPAN')separator.hidden=true; + var choice=button.cloneNode(true);choice.hidden=false;choice.removeAttribute('id');choice.removeAttribute('data-blob-path');choice.removeAttribute('data-file-path'); + choice.title=button.getAttribute('data-blob-path')||button.getAttribute('data-file-path')||button.textContent.trim(); + choice.onclick=function(){overflow.open=false;button.click()};menu.appendChild(choice); + }); +} +new ResizeObserver(fitResourcePath).observe($('dataBody')); +function resourceList(rows,count,contents){ + var header=contents?'NameSizeLast modified':'Name'; + return '
'+header+'
'+rows+'
'; } - function acctNode(x){ - var ao=!!openAccts[x.name]; - var t=(state.s_tree||{})[x.name]||{}; - var ld=t.loading||{}; var pinned=!!pinnedAccts[x.name]; var h='
'; - h+='
\u25B6'+svgIcon('account')+''+esc(x.name)+''+(x.location?''+esc(x.location)+'':'')+'
'; - if(ao){ - h+='
'; - h+=catNode(x.name,'blob','Blob Containers',t.containers,!!ld.blob&&!(t.containers&&t.containers.length)); - h+=catNode(x.name,'file','File Shares',t.shares,!!ld.file); - h+=catNode(x.name,'queue','Queues',t.queues,!!ld.queue); - h+=catNode(x.name,'table','Tables',t.tables,!!ld.table); - h+='
'; - } + h+='
'+svgIcon('account')+''+esc(x.name)+''+(x.location?''+esc(x.location)+'':'')+'
'; h+='
'; return h; } +var openTenants={}; +var lastTreeKey=''; +function treeRenderKey(){ + function failure(f){return f?[f.message||f.error,!!f.needsAuth]:null} + return JSON.stringify([treeQuery,pinnedAccts,hiddenTenants,hiddenSubscriptions,openTenants,openSubs, + state.s_account,!!state.s_accountsLoading,!!state.s_subsLoading,state.s_tenantLoading,state.s_tenantInventoryError, + (state.s_accounts||[]).map(function(a){return[a.name,a.subscriptionId,a.subscriptionName,a.tenantId,a.location]}), + (state.s_subscriptions||[]).map(function(s){return[s.subscriptionId,s.tenantId,s.displayName]}), + (state.s_tenants||[]).map(function(t){return[t.tenantId,t.displayName,t.defaultDomain,t.current]}), + state.s_selectedSubs||[],Object.keys(state.s_tenantErrors||{}).sort().map(function(t){return[t,failure(state.s_tenantErrors[t])]}), + (state.s_subscriptionErrors||[]).map(function(e){return[e.subscriptionId,failure(e)]})]); +} function renderTree(){ var el=$('tree');var a=state.s_accounts||[]; + var key=treeRenderKey();if(key===lastTreeKey)return;lastTreeKey=key; + var scroller=el.closest('.side-body'),scrollTop=scroller.scrollTop,focused=document.activeElement,focusKey=null; + if(el.contains(focused)){['data-pin','data-acct','data-sub','data-tenant','data-signin-tenant'].some(function(attr){var node=focused.closest('['+attr+']');if(node){focusKey=[attr,node.getAttribute(attr)];return true}return false})} var sel=state.s_selectedSubs||[]; - if(!sel.length&&!a.length){ - if(state.s_loading){el.innerHTML='
Loading storage accounts\u2026
';return} - var loadingSubs=state.s_subsLoading&&!(state.s_subscriptions||[]).length; - el.innerHTML='
\u{1F511}

'+(loadingSubs?'Loading your subscriptions\u2026':'Pick which subscriptions to load your storage accounts from.')+'

'+(loadingSubs?'':'')+'
'; - var b=$('ctaPick');if(b)b.onclick=openPicker; - return; - } - if(state.s_loading&&!a.length){el.innerHTML='
Loading storage accounts\u2026
';return} - if(!a.length){el.innerHTML='
No storage accounts in the selected subscriptions.
';return} + var subs=state.s_subscriptions||[]; + if(!subs.length&&!(state.s_tenants||[]).length){el.innerHTML='
'+(state.s_subsLoading?'Loading tenants and subscriptions\u2026':'No subscriptions available. Use Refresh to read your Azure CLI accounts.')+'
';return} var q=treeQuery.toLowerCase(); - function matchAcct(x){return !q||x.name.toLowerCase().indexOf(q)>=0||(x.subscriptionName||'').toLowerCase().indexOf(q)>=0} + function matchAcct(x){return !q||x.name.toLowerCase().indexOf(q)>=0||(x.subscriptionName||'').toLowerCase().indexOf(q)>=0||(x.tenantId||'').toLowerCase().indexOf(q)>=0} var html=''; - var pins=a.filter(function(x){return pinnedAccts[x.name]&&!hiddenAccts[x.name]&&matchAcct(x)}); + if(state.s_tenantInventoryError)html+=failureHtml({message:'Current directory inventory unavailable; showing cached directories.',error:state.s_tenantInventoryError},''); + function scopeVisible(x){return !hiddenTenants[x.tenantId]&&!hiddenSubscriptions[x.subscriptionId]} + var pins=a.filter(function(x){return scopeVisible(x)&&pinnedAccts[x.name]&&matchAcct(x)}); if(pins.length){ - html+='
\u2605Pinned'+pins.length+'
'; + html+='
'+uiIcon('pin')+'Pinned'+pins.length+'
'; pins.forEach(function(x){html+=acctNode(x)}); html+='
'; } - var groups={};var order=[]; - a.forEach(function(x){var s=x.subscriptionName||'(no subscription)';if(!groups[s]){groups[s]=[];order.push(s)}groups[s].push(x)}); - order.sort(); - order.forEach(function(sub){ - var accts=groups[sub].filter(function(x){return !hiddenAccts[x.name]&&!pinnedAccts[x.name]&&matchAcct(x)}); - if(!accts.length)return; - var so=q?true:!!openSubs[sub]; - html+='
'; - html+='
\u25B6\u{1F511}'+esc(sub)+''+accts.length+'
'; - if(so){ - html+='
'; - accts.forEach(function(x){html+=acctNode(x)}); - html+='
'; - } - html+='
'; + var tenants={};(state.s_tenants||[]).forEach(function(t){tenants[t.tenantId]=[]}); + subs.forEach(function(s){var t=s.tenantId||'Unknown tenant';(tenants[t]||(tenants[t]=[])).push(s)}); + Object.keys(tenants).sort().forEach(function(t){ + if(hiddenTenants[t])return; + var info=(state.s_tenants||[]).find(function(x){return x.tenantId===t})||{displayName:'Cached directory'}; + var shown=tenants[t].filter(function(s){return !hiddenSubscriptions[s.subscriptionId]&&(!q||(info.displayName||'').toLowerCase().indexOf(q)>=0||t.toLowerCase().indexOf(q)>=0||(s.displayName||'').toLowerCase().indexOf(q)>=0||s.subscriptionId.toLowerCase().indexOf(q)>=0||a.some(function(x){return x.subscriptionId===s.subscriptionId&&matchAcct(x)}))}); + if(q&&!shown.length&&(info.displayName||'').toLowerCase().indexOf(q)<0)return; + var to=!!q||(openTenants[t]===undefined?tenants[t].length>0:openTenants[t]); + html+='
'+uiIcon('forward')+''+svgIcon('tenant')+''+esc(info.displayName)+''+shown.length+'
'; + if(to){html+='
'; + if(info.current===false)html+='
Cached directory; current access is unverified.
'; + if(state.s_tenantLoading===t)html+='
Loading subscriptions\u2026
'; + else if(!shown.length&&!((state.s_tenantErrors||{})[t]))html+='
No subscriptions available.
'; + var tenantFailure=(state.s_tenantErrors||{})[t];if(tenantFailure)html+=failureHtml(tenantFailure,t); + shown.sort(function(x,y){return (x.displayName||'').localeCompare(y.displayName||'')}).forEach(function(s){ + var selected=sel.indexOf(s.subscriptionId)>=0; + var accts=a.filter(function(x){return x.subscriptionId===s.subscriptionId&&!pinnedAccts[x.name]&&matchAcct(x)}); + var so=!!q||(openSubs[s.subscriptionId]===undefined?selected:openSubs[s.subscriptionId]); + var failure=(state.s_subscriptionErrors||[]).find(function(e){return e.subscriptionId===s.subscriptionId}); + html+='
'+uiIcon('forward')+''+svgIcon('sub')+''+esc(s.displayName||s.subscriptionId)+''+(selected?(state.s_accountsLoading?uiIcon('busy'):failure?uiIcon('warning'):accts.length):'')+'
'; + if(so){html+='
'; + if(selected&&state.s_accountsLoading)html+='
'+uiIcon('busy',{size:13,className:'spin'})+' Loading accounts\u2026
'; + else if(failure)html+=failureHtml(failure,s.tenantId); + else if(selected&&!accts.length)html+='
Azure returned no storage accounts in this subscription.
'; + else accts.forEach(function(x){html+=acctNode(x)}); + html+='
'; + }html+='
'; + });html+='
'; + }html+='
'; }); - var hid=a.filter(function(x){return hiddenAccts[x.name]&&matchAcct(x)}); - if(hid.length){ - html+='
\u25B6\u2298Hidden'+hid.length+'
'; - if(showHidden){ - html+='
'; - hid.forEach(function(x){html+='
'+svgIcon('account')+''+esc(x.name)+'
'}); - html+='
'; - } - html+='
'; - } el.innerHTML=html||'
No matches.
'; - Array.prototype.forEach.call(el.querySelectorAll('[data-sub]'),function(r){r.onclick=function(){var s=r.getAttribute('data-sub');openSubs[s]=!openSubs[s];renderTree()}}); - Array.prototype.forEach.call(el.querySelectorAll('[data-acct]'),function(r){if(r.getAttribute('data-cat')||r.getAttribute('data-item'))return;r.onclick=function(e){var n=r.getAttribute('data-acct');var open=!!openAccts[n];var onCaret=e.target.classList&&e.target.classList.contains('tw');if(onCaret){openAccts[n]=!open;renderTree();if(openAccts[n])bridge.send('/tree-containers',{account:n});return}openAccts[n]=true;renderTree();bridge.send('/account',{account:n})}}); + wireDiagnostics(el); + Array.prototype.forEach.call(el.querySelectorAll('[data-signin-tenant]'),function(b){b.onclick=function(){bridge.send('/sign-in',{tenantId:b.getAttribute('data-signin-tenant')})}}); + Array.prototype.forEach.call(el.querySelectorAll('[data-tenant]'),function(r){r.onclick=function(){var t=r.getAttribute('data-tenant');openTenants[t]=r.getAttribute('aria-expanded')!=='true';if(openTenants[t]&&!tenants[t].length){bridge.send('/tenant-subscriptions',{tenantId:t})}renderTree()}}); + Array.prototype.forEach.call(el.querySelectorAll('[data-sub]'),function(r){ + var id=r.getAttribute('data-sub'); + r.onclick=function(){ + openSubs[id]=sel.indexOf(id)<0||r.getAttribute('aria-expanded')!=='true'; + if(openSubs[id]&&sel.indexOf(id)<0){ + var ids=(state.s_selectedSubs||[]).concat(id); + state.s_selectedSubs=ids;state.s_loading=true;state.s_accountsLoading=true; + bridge.send('/select-subscriptions',{subscriptionIds:ids}); + } + renderTree(); + }; + }); + Array.prototype.forEach.call(el.querySelectorAll('.trow[role="button"]'),function(r){r.onkeydown=function(e){if(e.target===r&&(e.key==='Enter'||e.key===' ')){e.preventDefault();r.click()}}}); + Array.prototype.forEach.call(el.querySelectorAll('[data-acct]'),function(r){r.onclick=function(){bridge.send('/account',{account:r.getAttribute('data-acct')})}}); Array.prototype.forEach.call(el.querySelectorAll('[data-pin]'),function(r){r.onclick=function(e){e.stopPropagation();var n=r.getAttribute('data-pin');if(pinnedAccts[n])delete pinnedAccts[n];else pinnedAccts[n]=1;saveAcctPrefs();renderTree()}}); - Array.prototype.forEach.call(el.querySelectorAll('[data-hide]'),function(r){r.onclick=function(e){e.stopPropagation();var n=r.getAttribute('data-hide');hiddenAccts[n]=1;delete pinnedAccts[n];saveAcctPrefs();renderTree()}}); - Array.prototype.forEach.call(el.querySelectorAll('[data-unhide]'),function(r){r.onclick=function(e){e.stopPropagation();var n=r.getAttribute('data-unhide');delete hiddenAccts[n];saveAcctPrefs();renderTree()}}); - var th=el.querySelector('[data-togglehidden]');if(th)th.onclick=function(){showHidden=!showHidden;renderTree()}; - Array.prototype.forEach.call(el.querySelectorAll('[data-cat]'),function(r){r.onclick=function(e){e.stopPropagation();var kind=r.getAttribute('data-cat');var acct=r.getAttribute('data-acct');var key=acct+'|'+kind;openCat[key]=!openCat[key];if(openCat[key]){if(kind==='file')bridge.send('/list-shares',{account:acct});else if(kind==='queue')bridge.send('/list-queues',{account:acct});else if(kind==='table')bridge.send('/list-tables',{account:acct})}renderTree()}}); - Array.prototype.forEach.call(el.querySelectorAll('[data-add]'),function(r){r.onclick=function(e){e.stopPropagation();createInCategory(r.getAttribute('data-add'),r.getAttribute('data-acct'))}}); - Array.prototype.forEach.call(el.querySelectorAll('[data-del]'),function(r){r.onclick=function(e){e.stopPropagation();deleteItem(r.getAttribute('data-del'),r.getAttribute('data-delname'),r.getAttribute('data-acct'))}}); - Array.prototype.forEach.call(el.querySelectorAll('[data-item]'),function(r){r.onclick=function(e){e.stopPropagation();var kind=r.getAttribute('data-kind');var name=r.getAttribute('data-item');var acct=r.getAttribute('data-acct');if(kind==='blob')bridge.send('/container',{container:name,account:acct});else if(kind==='queue')bridge.send('/open-queue',{queue:name,account:acct});else if(kind==='table')bridge.send('/open-table',{table:name,account:acct});else if(kind==='file')bridge.send('/open-share',{share:name,account:acct})}}); + Array.prototype.forEach.call(el.querySelectorAll('.trow'),function(r){if(!r.onclick)return;r.setAttribute('role','button');r.tabIndex=0;r.onkeydown=function(e){if(e.target===r&&(e.key==='Enter'||e.key===' ')){e.preventDefault();r.click()}}}); + if(focusKey){var replacement=el.querySelector('['+focusKey[0]+'="'+CSS.escape(focusKey[1])+'"]');if(replacement)replacement.focus({preventScroll:true})} + scroller.scrollTop=scrollTop; } -function renderCrumb(){ - var el=$('crumb');if(!el)return;el.style.display='none'; +function failureHtml(f,t){return '
'+esc(humanErrorSummary(f.message||f.error,f.needsAuth))+' '+(f.needsAuth?'':'')+'
'} +function wireDiagnostics(root){Array.prototype.forEach.call(root.querySelectorAll('[data-debug-log]'),function(b){b.onclick=function(){var panel=$('debugPanel');panel.open=true;renderLog();panel.querySelector('summary').focus()}})} +$('visibilityBtn').onclick=function(){ + var tenants=state.s_tenants||[],subs=state.s_subscriptions||[]; + $('visibilityList').innerHTML=tenants.map(function(t){return '
'+subs.filter(function(s){return s.tenantId===t.tenantId}).map(function(s){return ''}).join('')+'
'}).join(''); + $('visibilitySearch').value=''; + Array.prototype.forEach.call($('visibilityList').querySelectorAll('[data-visible-tenant]'),function(c){ + var children=c.closest('.visibility-group').querySelectorAll('[data-visible-sub]'); + if(!c.checked)Array.prototype.forEach.call(children,function(child){child.checked=false}); + c.onchange=function(){if(!c.checked)Array.prototype.forEach.call(children,function(child){child.checked=false})}; + }); + Array.prototype.forEach.call($('visibilityList').querySelectorAll('[data-visible-sub]'),function(c){c.onchange=function(){if(c.checked)c.closest('.visibility-group').querySelector('[data-visible-tenant]').checked=true}}); + $('visibilityOverlay').classList.add('open'); +}; +$('visibilitySearch').oninput=function(){ + var q=this.value.trim().toLowerCase(); + Array.prototype.forEach.call($('visibilityList').querySelectorAll('.visibility-group'),function(g){ + var parent=g.querySelector('.visibility-row'),tenantMatch=(parent.textContent+' '+g.getAttribute('data-directory')).toLowerCase().indexOf(q)>=0,shown=false; + Array.prototype.forEach.call(g.querySelectorAll('.visibility-row.sub'),function(row){var show=!q||tenantMatch||(row.textContent+' '+row.querySelector('input').getAttribute('data-visible-sub')).toLowerCase().indexOf(q)>=0;row.hidden=!show;if(show)shown=true}); + g.hidden=!!q&&!tenantMatch&&!shown; + }); +}; +function bulkVisibility(checked){ + Array.prototype.forEach.call($('visibilityList').querySelectorAll('.visibility-group'),function(g){ + if(g.hidden)return; + var hasHiddenChildren=Array.prototype.some.call(g.querySelectorAll('.visibility-row.sub'),function(row){return row.hidden}); + Array.prototype.forEach.call(g.querySelectorAll('.visibility-row'),function(row){ + if(row.hidden||(!row.classList.contains('sub')&&hasHiddenChildren))return; + row.querySelector('input').checked=checked; + if(checked&&row.classList.contains('sub'))g.querySelector('[data-visible-tenant]').checked=true; + }); + }); } +$('visibilitySelectAll').onclick=function(){bulkVisibility(true)}; +$('visibilityClearAll').onclick=function(){bulkVisibility(false)}; +$('visibilityClose').onclick=function(){$('visibilityOverlay').classList.remove('open')}; +$('visibilityApply').onclick=function(){ + Array.prototype.forEach.call($('visibilityList').querySelectorAll('[data-visible-sub]:checked'),function(c){c.closest('.visibility-group').querySelector('[data-visible-tenant]').checked=true}); + hiddenTenants=Object.assign({},hiddenTenants);hiddenSubscriptions=Object.assign({},hiddenSubscriptions); + Array.prototype.forEach.call($('visibilityList').querySelectorAll('[data-visible-tenant]'),function(c){var id=c.getAttribute('data-visible-tenant');if(!c.checked)hiddenTenants[id]=1;else delete hiddenTenants[id]}); + Array.prototype.forEach.call($('visibilityList').querySelectorAll('[data-visible-sub]'),function(c){var id=c.getAttribute('data-visible-sub');if(!c.checked)hiddenSubscriptions[id]=1;else delete hiddenSubscriptions[id]}); + bridge.send('/save-prefs',{pinned:pinnedAccts,hiddenTenants:hiddenTenants,hiddenSubscriptions:hiddenSubscriptions}); + $('visibilityOverlay').classList.remove('open');renderTree(); +}; + var DATA_KINDS=[ - {k:'blob',label:'Blob Containers',desc:'Unstructured object storage for files, images, logs and backups. Upload, download, filter and delete blobs.'}, - {k:'file',label:'File Shares',desc:'Fully-managed SMB file shares you can mount. Browse folders, upload and download files.'}, - {k:'queue',label:'Queues',desc:'Durable message queues for decoupled apps. Send, peek and clear messages.'}, - {k:'table',label:'Tables',desc:'Schemaless NoSQL key/value store. Add, edit and delete entities.'} + {k:'blob',label:'Blob Containers'}, + {k:'file',label:'File Shares'}, + {k:'queue',label:'Queues'}, + {k:'table',label:'Tables'} ]; -function dataTitleFor(k){return k==='blob'?'Blob container':k==='file'?'File share':k==='queue'?'Queue':k==='table'?'Table':'Storage data'} +function dataTitleFor(k){return k==='blob'?'Blob container':k==='file'?'File share':k==='queue'?'Queue':k==='table'?'Table':'Resource contents'} function openCategoryFor(kind){ var acct=state.s_account;if(!acct)return; var accObj=(state.s_accounts||[]).filter(function(x){return x.name===acct})[0]; - if(accObj&&accObj.subscriptionName)openSubs[accObj.subscriptionName]=true; - openAccts[acct]=true; - openCat[acct+'|'+kind]=true; - if(kind==='file')bridge.send('/list-shares',{account:acct}); - else if(kind==='queue')bridge.send('/list-queues',{account:acct}); - else if(kind==='table')bridge.send('/list-tables',{account:acct}); - renderTree(); -} - -function renderWelcome(el){ - var acct=state.s_account; - var lead; - if(acct){lead='
'+svgIcon('account')+'

'+esc(acct)+'

Storage account is open. Choose a data service below to jump straight to it, or pick an item from the Explorer to view and manage its contents.

'} - else{lead='
'+svgIcon('account')+'

Azure Storage

Select a storage account from the Explorer on the left to browse and manage its blob containers, file shares, queues and tables, all with full create, read, update and delete.

'} - var tiles=DATA_KINDS.map(function(d){ - var act=acct?' act':''; - return '
'+svgIcon(d.k)+'

'+d.label+'

'+d.desc+'

'+(acct?'
Open in Explorer \u2192
':'')+'
' - }).join(''); - el.innerHTML=lead+'
'+tiles+'
'; - if(acct){Array.prototype.forEach.call(el.querySelectorAll('[data-tile]'),function(r){r.onclick=function(){openCategoryFor(r.getAttribute('data-tile'))}})} + if(accObj)openSubs[accObj.subscriptionId]=true; + activeSvc=kind;svcListMode=true; + $('dataCard').classList.remove('min'); + ensureList(kind); + renderTree();renderData(); } function uploadFiles(fl){ if(!fl||!fl.length)return; + var target={account:state.s_account,container:state.s_container,prefix:state.s_blobPrefix||''}; var items=[];var pending=fl.length; Array.prototype.forEach.call(fl,function(file){ var rd=new FileReader(); - rd.onload=function(){var b64=String(rd.result).split(',')[1]||'';items.push({name:file.name,contentBase64:b64,contentType:file.type});if(--pending===0)bridge.send('/upload-many',{items:items})}; - rd.onerror=function(){if(--pending===0&&items.length)bridge.send('/upload-many',{items:items})}; + rd.onload=function(){var b64=String(rd.result).split(',')[1]||'';items.push({name:file.name,contentBase64:b64,contentType:file.type});if(--pending===0)bridge.send('/upload-many',Object.assign({items:items},target))}; + rd.onerror=function(){state.s_error='Could not read an upload file: '+file.name;renderError();if(--pending===0&&items.length)bridge.send('/upload-many',Object.assign({items:items},target))}; rd.readAsDataURL(file); }); } @@ -1357,123 +1853,347 @@ function wireDrop(el){ } function renderBlobData(el,cnt){ - var b=state.s_blobs||[];cnt.textContent=b.length?b.length:''; + var b=state.s_blobs||[];cnt.textContent=''; var names={};b.forEach(function(x){names[x.name]=1});selected.forEach(function(n){if(!names[n])selected.delete(n)}); - var h='
'; - if(state.s_blobsMore){h+='
Showing the first '+b.length+' blobs \u2014 this container has more. Statistics and filters below reflect only this capped sample.
'} - if(!b.length){h+='
No blobs in this container yet. Drag files here or use Upload to add them.
'+seCtaHtml('Adding a whole folder of files?','This view uploads loose files (drag a selection, or pick them). For whole folders with nested directory structure and very large bulk uploads, open the free desktop app.')} - else{h+='
'+b.map(function(x){var sel=selected.has(x.name)?' sel':'';var d=x.lastModified?new Date(x.lastModified).toLocaleDateString():'';return ''}).join('')+'
';h+='
Select blobs then Download, drag files here or upload to add new ones, or \u{1F517} SAS link to share a secure time-limited URL.
'+seCtaHtml('Need more than browse, upload and download?','Nested folders, whole-folder upload, access tiers, metadata, snapshots and property editing live in the free Azure Storage Explorer desktop app.')} - h+='
\u2B06
Drop files to upload to '+esc(state.s_container||'this container')+'
'; + var h=resourcePath('blob',state.s_container)+'
'; + var folders=state.s_blobPrefixes||[]; + if(state.s_blobsMore){h+='
More items are available. Use Next below. Search, selection and statistics apply to this page.
'} + var rows=folders.map(function(prefix){return '
'+uiIcon('folder')+'
'}).join(''); + rows+=b.map(function(x){var sel=selected.has(x.name)?' sel':'';var d=x.lastModified?new Date(x.lastModified).toLocaleString():'';return '
'+uiIcon('file')+''+esc(x.name)+''+fmtBytes(x.size)+''+esc(d)+'
'}).join(''); + h+=resourceList(rows||'
'+(state.s_loading?'Loading items\u2026':state.s_error?'Could not load items. Review the error above.':'No items in this directory.')+'
',b.length+folders.length,true); + h+='
'+uiIcon('upload')+'
Drop files to upload to '+esc(state.s_container||'this container')+'
'; el.innerHTML=h; wireDrop(el); $('dlBtn').onclick=function(){if(selected.size)bridge.send('/download-many',{blobs:Array.from(selected)})}; - $('selAll').onclick=function(){(state.s_blobs||[]).forEach(function(x){selected.add(x.name)});renderData()}; - $('selNone').onclick=function(){selected.clear();renderData()}; - $('upBtn').onclick=function(){uploadFiles($('upFiles').files)}; - Array.prototype.forEach.call(el.querySelectorAll('[data-blob]'),function(r){var toggle=function(){var n=r.getAttribute('data-blob');if(selected.has(n))selected.delete(n);else selected.add(n);renderData()};r.onclick=toggle;r.onkeydown=function(e){if(e.key===' '||e.key==='Spacebar'||e.key==='Enter'){e.preventDefault();toggle()}}}); - Array.prototype.forEach.call(el.querySelectorAll('[data-blobsas]'),function(bt){bt.onclick=function(e){e.stopPropagation();openSasGen(bt.getAttribute('data-blobsas'))}}); - Array.prototype.forEach.call(el.querySelectorAll('[data-blobdel]'),function(bt){bt.onclick=function(e){e.stopPropagation();var n=bt.getAttribute('data-blobdel');askConfirm({title:'Delete blob',msg:'Delete blob "'+n+'" from container "'+state.s_container+'"?',okLabel:'Delete',danger:true}).then(function(ok){if(ok)bridge.send('/blob-delete',{container:state.s_container,name:n})})}}); + $('selAll').onclick=function(){Array.prototype.forEach.call(el.querySelectorAll('[data-blob]:not([hidden])'),function(row){selected.add(row.getAttribute('data-blob'))});renderData()}; + $('selNone').onclick=function(){Array.prototype.forEach.call(el.querySelectorAll('[data-blob]:not([hidden])'),function(row){selected.delete(row.getAttribute('data-blob'))});renderData()}; + $('upBtn').onclick=function(){$('upFiles').click()}; + $('upFiles').onchange=function(){uploadFiles(this.files)}; + Array.prototype.forEach.call(el.querySelectorAll('[data-blob]'),function(r){ + function toggle(){var n=r.getAttribute('data-blob');if(selected.has(n))selected.delete(n);else selected.add(n);renderData();var row=Array.prototype.find.call(el.querySelectorAll('[data-blob]'),function(x){return x.getAttribute('data-blob')===n});if(row)row.querySelector('input').focus()} + r.onclick=function(e){if(e.target.closest('button'))return;toggle()}; + r.querySelector('input').onclick=function(e){e.stopPropagation();toggle()}; + }); + $('blobSas').onclick=function(){if(selected.size===1)openSasGen(Array.from(selected)[0])}; + $('deleteSelected').onclick=function(){deleteSelectedItems('blob',selected)}; + Array.prototype.forEach.call(el.querySelectorAll('[data-blob-folder],[data-blob-path]'),function(button){button.onclick=function(){bridge.send('/container',{account:state.s_account,container:state.s_container,prefix:button.getAttribute('data-blob-folder')||button.getAttribute('data-blob-path')||''})}}); wireSeCta(el); } +function deleteSelectedItems(kind,selection){ + var payload={account:state.s_account,kind:kind,resource:state.s_item,dir:kind==='file'?state.s_fileDir||'':'',prefix:kind==='blob'?state.s_blobPrefix||'':'',names:Array.from(selection)}; + askConfirm({title:'Delete selected items',msg:'Permanently delete '+payload.names.length+' selected item(s) from "'+payload.resource+(payload.dir?'/'+payload.dir:'')+'"? Directories must be empty. This cannot be undone.',okLabel:'Delete',danger:true}).then(function(ok){if(ok)bridge.send('/delete-selected',payload)}); +} var SEGS=[{k:'blob',label:'Blobs'},{k:'file',label:'File shares'},{k:'queue',label:'Queues'},{k:'table',label:'Tables'}]; function svcPlural(k){return k==='blob'?'blob containers':k==='file'?'file shares':k==='queue'?'queues':'tables'} -function svcTagline(k){return k==='blob'?'Object storage for files, images, logs and backups.':k==='file'?'Mountable SMB file shares you can map as a drive.':k==='queue'?'Durable messages passed between app components.':'Schemaless NoSQL key/value records.'} function svcListOf(k){var t=(state.s_tree||{})[state.s_account]||{};return k==='blob'?t.containers:k==='file'?t.shares:k==='queue'?t.queues:t.tables} function svcCount(k){var a=svcListOf(k);return a?a.length:null} -function ensureList(k){if(svcListOf(k)!=null)return;var a=state.s_account;if(!a)return;if(k==='blob')bridge.send('/account',{account:a});else if(k==='file')bridge.send('/list-shares',{account:a});else if(k==='queue')bridge.send('/list-queues',{account:a});else bridge.send('/list-tables',{account:a})} -function renderSwitcher(){return '
'+SEGS.map(function(s){var n=svcCount(s.k);var badge=n!=null?''+n+'':'';return ''}).join('')+'
'} -function wireSwitcher(root){Array.prototype.forEach.call(root.querySelectorAll('[data-seg]'),function(b){b.onclick=function(){var kd=b.getAttribute('data-seg');activeSvc=kd;svcListMode=true;ensureList(kd);renderData()}})} -function itemMetaHtml(k,it){if(k==='queue'&&it.approximateMessagesCount!=null)return ''+it.approximateMessagesCount+' msgs';if(k==='file'&&it.quotaGiB!=null)return ''+it.quotaGiB+' GiB';return ''} -function openSvcItem(k,name){activeSvc=k;svcListMode=false;if(k==='blob')blobTab='browse';var a=state.s_account;if(k==='blob')bridge.send('/container',{container:name,account:a});else if(k==='queue')bridge.send('/open-queue',{queue:name,account:a});else if(k==='table')bridge.send('/open-table',{table:name,account:a});else bridge.send('/open-share',{share:name,account:a})} -function blobTabsHtml(){ - var tabs=[ - ['browse','Browse',''], - ['filter','Filter & stats',''], - ['share','Share',''] - ]; - return '
'+tabs.map(function(t){return ''}).join('')+'
'; +function ensureList(k,paged){var a=state.s_account;if(!a)return;if(svcListOf(k)!=null){if(paged&&state.s_pages&&!state.s_pages[storagePageKey({account:a,kind:k})])bridge.send('/storage-page',{account:a,kind:k,direction:'refresh'});return}if(k==='blob')bridge.send('/tree-containers',{account:a});else if(k==='file')bridge.send('/list-shares',{account:a});else if(k==='queue')bridge.send('/list-queues',{account:a});else bridge.send('/list-tables',{account:a})} +function renderSwitcher(){return '
'+SEGS.map(function(s){var n=svcCount(s.k);var badge=n!=null?''+n+'':'';return ''}).join('')+'
'} +var serviceClients=new Map(),restoreServiceClient=null; +function wireSwitcher(root){Array.prototype.forEach.call(root.querySelectorAll('[data-seg]'),function(b,index){b.onclick=async function(){ + var kd=b.getAttribute('data-seg');if(kd===activeSvc)return; + saveServiceClient(); + activeSvc=kd;var saved=serviceClients.get(state.s_account+'|'+kd);svcListMode=saved?saved.listMode:true; + if(!saved)renderData(); + restoreServiceClient=saved?Object.assign({account:state.s_account,kind:kd},saved):null; + var response=await bridge.send('/activate-service',{account:state.s_account,kind:kd});if(!response.ok)return; + ensureList(kd,true);renderData(); +};b.onkeydown=function(e){var target=e.key==='Home'?0:e.key==='End'?SEGS.length-1:e.key==='ArrowRight'?(index+1)%SEGS.length:e.key==='ArrowLeft'?(index+SEGS.length-1)%SEGS.length:null;if(target===null)return;e.preventDefault();var key=SEGS[target].k;root.querySelector('[data-seg="'+key+'"]').click();root.querySelector('[data-seg="'+key+'"]').focus()}})} +function saveServiceClient(account){ + if(!state.s_account||!activeSvc)return; + var view=$('dataBody').querySelector('.resource-list,.entity-viewport'); + serviceClients.set((account||state.s_account)+'|'+activeSvc,{listMode:svcListMode,selected:Array.from(selected),files:Array.from(selectedFiles),records:Array.from(selectedRecords),top:view?view.scrollTop:0,left:view?view.scrollLeft:0}); } -function wireBlobTabs(root){Array.prototype.forEach.call(root.querySelectorAll('[data-blobtab]'),function(b){b.onclick=function(){blobTab=b.getAttribute('data-blobtab');renderData()}});var bk=root.querySelector('[data-blobback]');if(bk)bk.onclick=function(){svcListMode=true;renderData()}} -function applyBlobTab(){ - var body=$('dataBody'),hf=$('hubFilter'),hs=$('hubShare'); - if(!hf||!hs)return; +function itemMetaHtml(k,it){if(k==='queue'&&it.approximateMessagesCount!=null)return ''+it.approximateMessagesCount+' msgs';if(k==='file'&&it.quotaGiB!=null)return ''+it.quotaGiB+' GiB';return ''} +function openSvcItem(k,name){activeSvc=k;svcListMode=false;var a=state.s_account;if(k==='blob')bridge.send('/container',{container:name,account:a});else if(k==='queue')bridge.send('/open-queue',{queue:name,account:a});else if(k==='table')bridge.send('/open-table',{table:name,account:a});else bridge.send('/open-share',{share:name,account:a})} +function applyResourceCards(){ var isBlobItem=state.s_item&&state.s_kind==='blob'&&activeSvc==='blob'&&!svcListMode; - if(!isBlobItem){if(body)body.style.display='';hf.style.display='none';hs.style.display='none';return} - if(body)body.style.display=blobTab==='browse'?'':'none'; - hf.style.display=blobTab==='filter'?'':'none'; - hs.style.display=blobTab==='share'?'':'none'; + ['stats','share'].forEach(function(kind){var card=document.querySelector('[data-card="'+kind+'"]');if(card)card.hidden=!isBlobItem}); } function renderServiceList(el){ var k=activeSvc,a=state.s_account,arr=svcListOf(k),plural=svcPlural(k); + if(arr&&state.s_pages&&!state.s_pages[storagePageKey({account:a,kind:k})])arr=arr.slice(0,100); + var queryKey=a+'|'+k,query=resourceQueries[queryKey]||''; var newLabel=k==='blob'?'New container':k==='file'?'New share':k==='queue'?'New queue':'New table'; - var h='
'+svgIcon(k)+''+svcTagline(k)+'
'; - if(arr==null){h+='
loading '+plural+'\u2026
';el.innerHTML=h;var nb0=$('svcNew');if(nb0)nb0.onclick=function(){createInCategory(k,a)};return} - if(!arr.length){h+='
No '+plural+' in this account yet. Use '+newLabel+' to create one.
'} - else{h+='
'+arr.map(function(it){return '
'+svgIcon(k)+' '+esc(it.name)+''+itemMetaHtml(k,it)+'
'}).join('')+'
'} + var h='
'+esc(plural.charAt(0).toUpperCase()+plural.slice(1))+'
'+searchControl('resourceSearch','Search '+plural,query)+'
'+(arr!=null?'':'')+'
'; + if(arr==null){h+=state.s_error?'
Could not load '+plural+'. Review the error above.
':'
'+uiIcon('busy',{size:13,className:'spin'})+' loading '+plural+'\u2026
';el.innerHTML=h;var nb0=$('svcNew');if(nb0)nb0.onclick=function(){createInCategory(k,a)};var retry=$('svcRetry');if(retry)retry.onclick=function(){state.s_error=null;ensureList(k);renderData()};return} + if(!arr.length){h+=resourceList('
No '+plural+' in this account yet.
',0)} + else{h+=resourceList(arr.map(function(it){return '
'+svgIcon(k)+' '+esc(it.name)+''+itemMetaHtml(k,it)+'
'}).join(''),arr.length)+''} el.innerHTML=h; + function filterResources(){var q=$('resourceSearch').value.toLowerCase(),visible=0;resourceQueries[queryKey]=$('resourceSearch').value;Array.prototype.forEach.call(el.querySelectorAll('[data-open]'),function(row){row.hidden=row.getAttribute('data-open').toLowerCase().indexOf(q)<0;if(!row.hidden)visible++});var empty=$('resourceNoMatch');if(empty)empty.hidden=visible>0} + $('resourceSearch').oninput=filterResources;filterResources(); $('svcNew').onclick=function(){createInCategory(k,a)}; - Array.prototype.forEach.call(el.querySelectorAll('[data-open]'),function(r){r.onclick=function(){openSvcItem(k,r.getAttribute('data-open'))}}); - Array.prototype.forEach.call(el.querySelectorAll('[data-itemdel]'),function(b){b.onclick=function(e){e.stopPropagation();deleteItem(k,b.getAttribute('data-itemdel'),a)}}); + Array.prototype.forEach.call(el.querySelectorAll('[data-open]'),function(r){r.tabIndex=0;r.setAttribute('aria-label','Open '+r.getAttribute('data-open'));r.onclick=function(event){if(event.target.closest('button,input,a,select,textarea,label'))return;openSvcItem(k,r.getAttribute('data-open'))};r.onkeydown=function(event){if(event.target===r&&(event.key==='Enter'||event.key===' ')){event.preventDefault();openSvcItem(k,r.getAttribute('data-open'))}}}); + if($('svcDelete'))$('svcDelete').onclick=function(){if(selectedRecords.size===1)deleteItem(k,Array.from(selectedRecords)[0],a)}; + Array.prototype.forEach.call(el.querySelectorAll('[data-resource-select]'),function(input){input.onchange=function(){var name=input.dataset.resourceSelect;if(input.checked)selectedRecords.add(name);else selectedRecords.delete(name);$('svcDelete').disabled=selectedRecords.size!==1}}); +} +function storageTarget(){ + return {account:state.s_account,kind:activeSvc,resource:svcListMode?undefined:state.s_item,prefix:!svcListMode&&activeSvc==='blob'?state.s_blobPrefix||'':'',dir:!svcListMode&&activeSvc==='file'?state.s_fileDir||'':''}; +} +function storagePageKey(t){return JSON.stringify([t.account||'',t.kind,t.resource||'',t.dir||t.prefix||''])} +function wireStoragePager(el,target){ + if(!target.account)return; + var key=storagePageKey(target),page=(state.s_pages||{})[key]; + var queuePeek=target.kind==='queue'&&target.resource;if(queuePeek)page={index:0,cachedPageCount:1}; + var loading=state.s_pageLoading===key; + var count=target.kind==='sas'?(state.s_sasBlobs||[]).length:target.kind==='table'&&target.resource?(state.s_entities&&state.s_entities.rows||[]).length:el.querySelectorAll('.resource-list .row,tbody tr').length; + function arrow(name,disabled){return ''} + el.insertAdjacentHTML('beforeend',''); + el.querySelector('.storage-pager').dataset.pageKey=key; + if(queuePeek)el.querySelector('.storage-pager').dataset.queuePeek='true'; + Array.prototype.forEach.call(el.querySelectorAll('[data-page-dir]'),function(button){button.onclick=function(){if(queuePeek){if(button.dataset.pageDir==='refresh'&&!state.s_loading)bridge.send('/open-queue',{account:target.account,queue:target.resource});return}var busy=state.s_pageLoading===key;bridge.send('/storage-page',Object.assign({},target,{direction:busy&&button.dataset.pageDir==='refresh'?'cancel':button.dataset.pageDir}))}}); + updateStoragePagers(); +} +function updateStoragePagers(){ + Array.prototype.forEach.call(document.querySelectorAll('.storage-pager'),function(nav){ + var key=nav.dataset.pageKey,peek=nav.dataset.queuePeek==='true',page=peek?{index:0,cachedPageCount:1}:(state.s_pages||{})[key];if(!page)return; + var loading=peek?!!state.s_loading:state.s_pageLoading===key; + nav.querySelector('[data-page-dir="previous"]').disabled=loading||!page.index; + nav.querySelector('[data-page-dir="next"]').disabled=loading||!page.nextToken&&!(page.index+1
'); + function filter(){var value=$('contentSearch').value,shown=0;resourceQueries[key]=value;Array.prototype.forEach.call(rows,function(row){row.hidden=row.textContent.toLowerCase().indexOf(value.toLowerCase())<0;if(!row.hidden)shown++});$('contentNoMatch').hidden=!value||shown>0} + $('contentSearch').oninput=filter;filter(); + } + if(state.s_loading)Array.prototype.forEach.call(el.querySelectorAll('.vtool button'),function(button){button.disabled=true}); + fitResourcePath(); + } + wireStoragePager(el,storageTarget()); + Array.prototype.forEach.call(el.querySelectorAll('[data-directory],[data-file-item]'),function(row){ + var link=row.querySelector('[data-blob-folder],[data-file-folder]');if(!link)return; + row.style.cursor='pointer'; + row.onclick=function(event){if(event.target.closest('button,input,a,select,textarea,label'))return;link.click()}; + }); + if(el.querySelector('.entity-viewport')){ + var context=document.createElement('div');context.className='resource-context'; + context.innerHTML=resourcePath('table',state.s_item)+searchControl('entitySearch','Search items',resourceQueries[state.s_account+'|table|'+state.s_item]||''); + el.insertBefore(context,el.firstChild); + var queryKey=state.s_account+'|table|'+state.s_item; + mountEntityViewport(el.querySelector('.entity-viewport'),state.s_entities,resourceQueries[queryKey]||''); + if(oldViewport&&oldResource===lastResourceKey){var viewport=el.querySelector('.entity-viewport');viewport.scrollLeft=oldEntityLeft;if(previousScope===lastItemKey)viewport.scrollTop=oldEntityTop} + $('entitySearch').oninput=function(){resourceQueries[queryKey]=this.value;mountEntityViewport(el.querySelector('.entity-viewport'),state.s_entities,this.value)}; + } + if(restoreServiceClient&&restoreServiceClient.account===state.s_account&&restoreServiceClient.kind===state.s_kind){ + var saved=restoreServiceClient;restoreServiceClient=null; + var view=el.querySelector('.resource-list,.entity-viewport');if(view){view.scrollTop=saved.top;view.scrollLeft=saved.left} + Array.prototype.forEach.call(el.querySelectorAll('[data-blob] input[type="checkbox"]'),function(input){input.checked=selected.has(input.closest('[data-blob]').getAttribute('data-blob'))}); + Array.prototype.forEach.call(el.querySelectorAll('[data-file-select]'),function(input){input.checked=selectedFiles.has(input.getAttribute('data-file-select'))}); + } + var back=$('svcBack');if(back)back.onclick=function(){ + if(activeSvc==='blob'&&state.s_blobPrefix){var parts=state.s_blobPrefix.split('/');parts.splice(-2);bridge.send('/container',{account:state.s_account,container:state.s_item,prefix:parts.length?parts.join('/')+'/':''});return} + if(activeSvc==='file'&&state.s_fileDir){bridge.send('/open-share',{account:state.s_account,share:state.s_item,dir:state.s_fileDir.split('/').slice(0,-1).join('/')});return} + svcListMode=true;selectedRecords.clear();ensureList(activeSvc);renderData(); + }; + wireCommandIcons(el); + fitResourcePath(); + lastDataSignature=dataSignature(); +} +function wireCommandIcons(el){ + var icons={svcNew:'add',selAll:'select',shAll:'select',selNone:'clear',shNone:'clear',shMkdir:'folder',qSend:'send',qClear:'clear',qDel:'trash',tAdd:'add',tDel:'trash'}; + Array.prototype.forEach.call(el.querySelectorAll('.vtool button,.resource-toolbar button'),function(button){ + var icon=icons[button.id];if(!icon||button.querySelector('svg'))return; + button.innerHTML=uiIcon(icon)+' '+esc(button.textContent.trim().replace(/^[+\uFF0B]\\s*/,''))+''; + }); +} +function mountEntityViewport(view,e,query){ + var renderStarted=performance.now(); + if(view.__dispose)view.__dispose(); + var cols=e.columns,rows=e.rows.map(function(row,i){return {row:row,index:i}}).filter(function(x){return !query||Object.values(x.row).join(' ').toLowerCase().includes(query.toLowerCase())}); + var width=160,totalCols=cols.length+1,rowHeight=40,headerHeight=36,frame=0,lastRange=''; + function columnLeft(index){return index?40+(index-1)*width:0} + view.innerHTML='
'+(!rows.length?'
No matching items.
':''); + view.setAttribute('role','table');view.setAttribute('aria-label','Table entities');view.setAttribute('aria-colcount',totalCols);view.setAttribute('aria-rowcount',rows.length+1);view.tabIndex=0; + var layer=view.querySelector('.entity-window'); + var head=document.createElement('div'),headCells=new Map(),visibleRows=new Map(); + head.className='entity-vhead';head.setAttribute('role','row');head.setAttribute('aria-rowindex','1');layer.append(head); + function paintCells(parent,cells,firstCol,lastCol,item){ + var recycled=[]; + cells.forEach(function(cell,column){if(column=lastCol){recycled.push(cell);cells.delete(column)}}); + for(var c=firstCol;c=lastRow){entry.node.remove();visibleRows.delete(index)}}); + for(var r=firstRow;rSelect a storage account.

';return} if(acct!==lastAcct){lastAcct=acct;activeSvc='blob';svcListMode=true;lastItemKey='';ensureList('file');ensureList('queue');ensureList('table')} - var itemKey=(state.s_kind||'')+'|'+(state.s_item||''); - if(itemKey!==lastItemKey){lastItemKey=itemKey;if(state.s_item){activeSvc=state.s_kind;svcListMode=false}} + var page=(state.s_pages||{})[storagePageKey({account:acct,kind:state.s_kind,resource:state.s_item,prefix:state.s_kind==='blob'?state.s_blobPrefix||'':'',dir:state.s_kind==='file'?state.s_fileDir||'':''})]; + var resourceKey=acct+'|'+(state.s_kind||'')+'|'+(state.s_item||'')+'|'+(state.s_kind==='file'?state.s_fileDir||'':state.s_blobPrefix||''),itemKey=resourceKey+'|'+(page?page.index:0); + if(itemKey!==lastItemKey){lastItemKey=itemKey;selected.clear();selectedFiles.clear();selectedRecords.clear()} + if(restoreServiceClient){selected=new Set(restoreServiceClient.selected);selectedFiles=new Set(restoreServiceClient.files);selectedRecords=new Set(restoreServiceClient.records||[])} + if(resourceKey!==lastResourceKey){lastResourceKey=resourceKey;if(state.s_item){activeSvc=state.s_kind;svcListMode=restoreServiceClient?restoreServiceClient.listMode:false}} if(!activeSvc)activeSvc='blob'; var k=state.s_kind,item=state.s_item; var showItem=item&&k===activeSvc&&!svcListMode; - if(showItem){icEl.innerHTML=svgIcon(k);tEl.innerHTML=''+esc(acct)+'\u203A'+esc(item)+'';} - else{icEl.innerHTML=svgIcon('account');tEl.textContent=esc(acct);} - if(bar){bar.style.display='';var bh=renderSwitcher();if(showItem){if(k==='blob'){bh+=blobTabsHtml()}else{bh+='
'+svgIcon(k)+' '+esc(item)+'
'}}bar.innerHTML=bh;wireSwitcher(bar);if(showItem){var bk=$('svcBack');if(bk)bk.onclick=function(){svcListMode=true;renderData()};if(k==='blob')wireBlobTabs(bar)}} + var recordsTarget=storageTarget(),recordsPage=(state.s_pages||{})[storagePageKey(recordsTarget)]; + var recordsScope=storagePageKey(recordsTarget)+'|'+(recordsPage?recordsPage.index:0); + if(lastRecordsScope!==recordsScope&&!restoreServiceClient)selectedRecords.clear();lastRecordsScope=recordsScope; + el.classList.toggle('table-view',!!showItem&&k==='table'); + if(showItem){icEl.innerHTML=svgIcon(k);tEl.textContent=acct;} + else{icEl.innerHTML=svgIcon('account');tEl.textContent=acct;} + if(bar){bar.style.display='';bar.innerHTML=renderSwitcher();wireSwitcher(bar)} if(!showItem){renderServiceList(el);return} - if(state.s_loading){el.innerHTML='
loading\u2026
';return} if(k==='blob'){renderBlobData(el,cnt);return} if(k==='queue'){ var m=state.s_messages||[]; - var h='
'; - if(!m.length){h+='
No messages to peek (queue empty or peek returned none).
'} - else{h+=''+m.map(function(x){var d=x.insertedOn?new Date(x.insertedOn).toLocaleString():'';var txt=x.messageText||'';return ''}).join('')+'
Message IDDequeue #InsertedText
'+esc(x.messageId)+''+x.dequeueCount+''+esc(d)+''+esc(txt.slice(0,400))+'
Peek shows up to 32 messages. Editing or deleting one briefly receives the batch (Azure has no random-access), so dequeue counts may rise.
'} + selectedRecords.forEach(function(key){if(!m.some(function(message){return message.messageId===key}))selectedRecords.delete(key)}); + var draftKey=JSON.stringify([acct,item]); + var h='
'; + if(!m.length){h+='
'+(state.s_loading?'Loading messages\u2026':state.s_error?'Could not load messages. Review the error above.':'No messages returned by peek.')+'
'} + else{h+='
'+m.map(function(x){var d=x.insertedOn?new Date(x.insertedOn).toLocaleString():'';var txt=x.messageText||'';return ''}).join('')+'
Message IDDequeue countInsertedText
'+esc(x.messageId)+''+x.dequeueCount+''+esc(d)+''+esc(txt.slice(0,400))+'
'} el.innerHTML=h; - $('qSend').onclick=function(){var t=$('qMsg').value;if(!t)return;bridge.send('/queue-send',{queue:item,text:t})}; + $('qMsg').oninput=function(){queueDrafts[draftKey]=this.value}; + $('qSend').onclick=function(){var t=$('qMsg').value;if(!t)return;bridge.send('/queue-send',{queue:item,text:t}).then(function(result){if(result&&result.ok&&queueDrafts[draftKey]===t){delete queueDrafts[draftKey];if(state.s_account===acct&&state.s_item===item&&$('qMsg'))$('qMsg').value=''}})}; $('qMsg').onkeydown=function(e){if(e.key==='Enter')$('qSend').click()}; $('qClear').onclick=function(){askConfirm({title:'Clear messages',msg:'Delete ALL messages in queue "'+item+'"?',okLabel:'Clear',danger:true}).then(function(ok){if(ok)bridge.send('/queue-clear',{queue:item})})}; $('qDel').onclick=function(){deleteItem('queue',item)}; - Array.prototype.forEach.call(el.querySelectorAll('[data-qedit]'),function(b){b.onclick=function(){var id=b.getAttribute('data-qedit');var cur=b.getAttribute('data-qtext');askConfirm({title:'Edit message',msg:'Update the text of this queue message:',input:true,rows:3,value:cur,okLabel:'Save'}).then(function(t){if(t===null)return;bridge.send('/queue-msg-update',{queue:item,messageId:id,text:t})})}}); - Array.prototype.forEach.call(el.querySelectorAll('[data-qdel]'),function(b){b.onclick=function(){var id=b.getAttribute('data-qdel');askConfirm({title:'Delete message',msg:'Delete this message from queue "'+item+'"?',okLabel:'Delete',danger:true}).then(function(ok){if(ok)bridge.send('/queue-msg-delete',{queue:item,messageId:id})})}}); + function pickedMessage(){return selectedRecords.size===1?m.find(function(x){return selectedRecords.has(x.messageId)}):null} + $('qEdit').onclick=function(){var message=pickedMessage();if(!message)return;askConfirm({title:'Edit message',msg:'Update this message. Azure must briefly receive the batch to edit it; dequeue counts may rise.',input:true,rows:3,value:message.messageText||'',okLabel:'Save'}).then(function(t){if(t!==null)bridge.send('/queue-msg-update',{queue:item,messageId:message.messageId,text:t})})}; + $('qDelete').onclick=function(){var message=pickedMessage();if(!message)return;askConfirm({title:'Delete message',msg:'Delete this message from queue "'+item+'"? Azure must briefly receive the batch; dequeue counts may rise.',okLabel:'Delete',danger:true}).then(function(ok){if(ok)bridge.send('/queue-msg-delete',{queue:item,messageId:message.messageId})})}; + Array.prototype.forEach.call(el.querySelectorAll('[data-message-select]'),function(input){input.onchange=function(){if(input.checked)selectedRecords.add(input.dataset.messageSelect);else selectedRecords.delete(input.dataset.messageSelect);$('qEdit').disabled=$('qDelete').disabled=!pickedMessage()}}); } else if(k==='table'){ var e=state.s_entities; - var h='
'; - if(!e||!e.rows.length){h+='
No entities yet. Use Add entity to create one.
'} + selectedRecords.forEach(function(key){if(!e||!e.rows.some(function(row){return entityKey(row)===key}))selectedRecords.delete(key)}); + var h='
'; + if(!e||!e.rows.length){h+='
'+(state.s_loading?'Loading entities\u2026':state.s_error?'Could not load entities. Review the error above.':'No entities yet. Use Add entity to create one.')+'
'} else{ - var cols=e.columns; - var head=cols.map(function(c){return ''+esc(c)+''}).join('')+''; - var body=e.rows.map(function(row,ri){return ''+cols.map(function(c){var v=row[c];return ''+esc(v==null?'':String(v)).slice(0,300)+''}).join('')+' '}).join(''); - h+='
'+head+''+body+'

First '+e.rows.length+' entities.

'; + h+='
'; } el.innerHTML=h; $('tAdd').onclick=function(){openEntityForm(null)}; $('tDel').onclick=function(){deleteItem('table',item)}; - Array.prototype.forEach.call(el.querySelectorAll('[data-entedit]'),function(b){b.onclick=function(){var ri=parseInt(b.getAttribute('data-entedit'),10);var row=(state.s_entities&&state.s_entities.rows)?state.s_entities.rows[ri]:null;if(row)openEntityForm(row)}}); - Array.prototype.forEach.call(el.querySelectorAll('[data-entpk]'),function(b){b.onclick=function(){var pk=b.getAttribute('data-entpk'),rk=b.getAttribute('data-entrk');askConfirm({title:'Delete entity',msg:'Delete entity ('+pk+', '+rk+')?',okLabel:'Delete',danger:true}).then(function(ok){if(ok)bridge.send('/entity-delete',{table:item,partitionKey:pk,rowKey:rk})})}}); + function pickedEntity(){return selectedRecords.size===1&&e?e.rows.find(function(row){return selectedRecords.has(entityKey(row))}):null} + $('tEdit').onclick=function(){var row=pickedEntity();if(row)openEntityForm(row)}; + $('tDelete').onclick=function(){var row=pickedEntity();if(!row)return;askConfirm({title:'Delete entity',msg:'Delete entity ('+row.partitionKey+', '+row.rowKey+')?',okLabel:'Delete',danger:true}).then(function(ok){if(ok)bridge.send('/entity-delete',{table:item,partitionKey:row.partitionKey,rowKey:row.rowKey})})}; } else if(k==='file'){ var f=state.s_files||[]; - var h='
'; - if(!f.length){h+='
Empty share. No files or folders yet. Use Upload or New folder to add content.
'} - else{h+='
'+f.map(function(x){var ic=x.kind==='directory'?'\u{1F4C2}':'\u{1F4C4}';var meta=x.kind==='file'?fmtBytes(x.size):'folder';var acts=(x.kind==='file'?' ':'')+'';return '
'+ic+' '+esc(x.name)+''+esc(meta)+'   '+acts+'
'}).join('')+'
'} + selectedFiles.forEach(function(name){if(!f.some(function(x){return x.name===name}))selectedFiles.delete(name)}); + var dir=state.s_fileDir||''; + var h=resourcePath('file',item)+'
'; + if(!f.length){h+=resourceList('
'+(state.s_loading?'Loading items\u2026':state.s_error?'Could not load items. Review the error above.':'No items in this directory.')+'
',0,true)} + else{h+=resourceList(f.slice().sort(function(a,b){return Number(b.kind==='directory')-Number(a.kind==='directory')}).map(function(x){var ic=uiIcon(x.kind==='directory'?'folder':'file');var size=x.kind==='file'?fmtBytes(x.size):'',date=x.kind==='file'&&x.lastModified?new Date(x.lastModified).toLocaleString():'';return '
'+ic+(x.kind==='directory'?'':''+esc(x.name)+'')+''+esc(size)+''+esc(date)+'
'}).join(''),f.length,true)} el.innerHTML=h; - $('shUp').onclick=function(){var fl=$('shFile').files;if(!fl||!fl.length)return;var file=fl[0];var rd=new FileReader();rd.onload=function(){var b64=String(rd.result).split(',')[1]||'';bridge.send('/share-upload',{share:item,name:file.name,contentBase64:b64})};rd.readAsDataURL(file)}; - $('shMkdir').onclick=function(){askConfirm({title:'New folder',msg:'Directory name to create in "'+item+'":',input:true,okLabel:'Create'}).then(function(d){d=(d||'').trim();if(d)bridge.send('/share-mkdir',{share:item,dir:d})})}; - $('shDel').onclick=function(){deleteItem('file',item)}; - Array.prototype.forEach.call(el.querySelectorAll('[data-shdl]'),function(b){b.onclick=function(){bridge.send('/share-download',{share:item,name:b.getAttribute('data-shdl')})}}); - Array.prototype.forEach.call(el.querySelectorAll('[data-shdel]'),function(b){b.onclick=function(){var n=b.getAttribute('data-shdel'),kd=b.getAttribute('data-shkind');askConfirm({title:'Delete '+kd,msg:'Delete '+kd+' "'+n+'"?',okLabel:'Delete',danger:true}).then(function(ok){if(ok)bridge.send('/share-delete-item',{share:item,name:n,kind:kd})})}}); + $('shUp').onclick=function(){$('shFile').click()}; + $('shFile').onchange=function(){var fl=this.files;if(!fl||!fl.length)return;var file=fl[0];var rd=new FileReader();rd.onload=function(){var b64=String(rd.result).split(',')[1]||'';bridge.send('/share-upload',{account:acct,share:item,name:file.name,contentBase64:b64,dir:dir})};rd.onerror=function(){state.s_error='Could not read an upload file: '+file.name;renderError()};rd.readAsDataURL(file)}; + $('shMkdir').onclick=function(){askConfirm({title:'New folder',msg:'Directory name to create in "'+item+(dir?'/'+dir:'')+'":',input:true,okLabel:'Create'}).then(function(d){d=(d||'').trim();if(d)bridge.send('/share-mkdir',{account:acct,share:item,dir:dir,name:d})})}; + $('shDelete').onclick=function(){deleteSelectedItems('file',selectedFiles)}; + $('shDownload').onclick=async function(){var names=Array.from(selectedFiles);for(var x of f){if(x.kind==='file'&&names.indexOf(x.name)>=0){var r=await bridge.send('/share-download',{account:acct,share:item,name:x.name,dir:dir});if(!r.ok)break}}}; + function selectShown(on){Array.prototype.forEach.call(el.querySelectorAll('[data-file-item]:not([hidden])'),function(row){if(on)selectedFiles.add(row.getAttribute('data-file-item'));else selectedFiles.delete(row.getAttribute('data-file-item'))});renderData()} + $('shAll').onclick=function(){selectShown(true)};$('shNone').onclick=function(){selectShown(false)}; + Array.prototype.forEach.call(el.querySelectorAll('[data-file-select]'),function(c){c.onchange=function(){var name=c.getAttribute('data-file-select');if(c.checked)selectedFiles.add(name);else selectedFiles.delete(name);renderData();var next=Array.prototype.find.call(el.querySelectorAll('[data-file-select]'),function(x){return x.getAttribute('data-file-select')===name});if(next)next.focus()}}); + Array.prototype.forEach.call(el.querySelectorAll('[data-file-folder]'),function(b){b.onclick=function(){bridge.send('/open-share',{share:item,account:acct,dir:[dir,b.getAttribute('data-file-folder')].filter(Boolean).join('/')})}}); + Array.prototype.forEach.call(el.querySelectorAll('[data-file-path]'),function(b){b.onclick=function(){bridge.send('/open-share',{share:item,account:acct,dir:b.getAttribute('data-file-path')})}}); } } var xferTimer=null; @@ -1486,15 +2206,15 @@ function renderBatch(){ var opn={upload:{g:'Uploading',d:'Upload',n:'file'},download:{g:'Downloading',d:'Download',n:'file'},archive:{g:'Moving to Archive',d:'Archive',n:'blob'},'delete':{g:'Deleting',d:'Delete',n:'blob'}}; var o=opn[s.op]||opn.download; var title=s.running?o.g+'\u2026':(s.failed.length?o.d+' finished with issues':o.d+' complete'); - var h='
'+(s.running?' ':'\u2713 ')+esc(title)+'
'; + var h='
'+(s.running?''+uiIcon('busy',{size:13,className:'spin'})+' ':uiIcon('check'))+esc(title)+'
'; h+='
'; h+='
'+s.done+' of '+s.total+' '+o.n+(s.total===1?'':'s')+''+(s.failed.length?''+s.failed.length+' failed'+(s.ok?''+s.ok+' done':''):(!s.running?'Completed':''))+'
'; if(s.failed.length){h+='
'+s.failed.slice(0,4).map(function(f){return '
'+esc(f.name)+''+esc(f.error)+'
'}).join('')+(s.failed.length>4?'
+'+(s.failed.length-4)+' more
':'')+'
'} - if(s.op==='download'&&!s.running&&s.ok>0&&s.dir){h+='
\u{1F4C1}'+s.ok+' file'+(s.ok>1?'s':'')+' saved to your PC
'+esc(shortPath(s.dir))+'
'} + if(s.op==='download'&&!s.running&&s.ok>0&&s.dir){h+='
'+s.ok+' file'+(s.ok>1?'s':'')+' saved to your PC
'+esc(shortPath(s.dir))+'
'} el.innerHTML=h;el.style.display=''; var cx=$('xferClose');if(cx)cx.onclick=dismissXfer; - var of=$('openDlFolder');if(of)of.onclick=function(){if(xferTimer){clearTimeout(xferTimer);xferTimer=null}of.disabled=true;of.textContent='Opening\u2026';bridge.send('/open-folder',{dir:s.dir}).then(function(r){if(r&&r.ok){of.textContent='\u2713 Opened';setTimeout(function(){if($('openDlFolder')){of.disabled=false;of.textContent='\u{1F4C2} Open folder'}},1600)}else{of.textContent='Couldn\u2019t open. Copy the path instead.';of.disabled=false}})}; - var cp=$('copyDlPath');if(cp)cp.onclick=function(){if(xferTimer){clearTimeout(xferTimer);xferTimer=null}var t=s.dir;try{navigator.clipboard.writeText(t)}catch(e){}cp.textContent='\u2713 Copied';setTimeout(function(){if($('copyDlPath'))cp.textContent='\u{1F4CB} Copy path'},1400)}; + var of=$('openDlFolder');if(of)of.onclick=function(){if(xferTimer){clearTimeout(xferTimer);xferTimer=null}of.disabled=true;of.textContent='Opening\u2026';bridge.send('/open-folder',{dir:s.dir}).then(function(r){if(r&&r.ok){of.textContent='Opened';setTimeout(function(){if($('openDlFolder')){of.disabled=false;of.textContent='Open folder'}},1600)}else{of.textContent='Couldn\u2019t open. Copy the path instead.';of.disabled=false}})}; + var cp=$('copyDlPath');if(cp)cp.onclick=function(){if(xferTimer){clearTimeout(xferTimer);xferTimer=null}var t=s.dir;try{navigator.clipboard.writeText(t)}catch(e){}cp.textContent='Copied';setTimeout(function(){if($('copyDlPath'))cp.textContent='Copy path'},1400)}; if(xferTimer){clearTimeout(xferTimer);xferTimer=null} if(!s.running&&!s.failed.length){xferTimer=setTimeout(dismissXfer,(s.op==='download'&&s.dir)?9000:4500)} } @@ -1503,8 +2223,8 @@ function renderStats(){ if(!s){el.innerHTML=containerOpen()?'
Set optional criteria above and click Filter to see live totals, a size histogram, a by-type breakdown and the largest blobs.
':'
'+cubeIc()+'Open a container first. Pick one from the tree on the left, then Filter its blobs.
';return} var maxb=Math.max.apply(null,s.buckets.map(function(x){return x.n}).concat([1])); var h=''; - if(containerOpen())h+='
Statistics for '+esc(state.s_container)+' \xB7 '+s.count+' blob'+(s.count===1?'':'s')+' matched in '+esc(state.s_account)+'
'; - h+='
'+metricCard('matched',String(s.count),'#479ef5')+metricCard('total',s.totalh,'#8b5cf6')+metricCard('avg',s.avgh,'#22d3ee')+metricCard('largest',s.maxh,'#f59e0b')+'
'; + if(containerOpen())h+='
Statistics for loaded blobs in '+esc(state.s_container+(state.s_blobPrefix?'/'+state.s_blobPrefix:''))+' \xB7 '+s.count+' blob'+(s.count===1?'':'s')+' matched in '+esc(state.s_account)+''+(state.s_blobsMore?' (incomplete directory listing)':'')+'
'; + h+='
'+metricCard('matched',String(s.count),'var(--accent)')+metricCard('total',s.totalh,'var(--text-2)')+metricCard('avg',s.avgh,'var(--accent)')+metricCard('largest',s.maxh,'var(--warn)')+'
'; h+='
'; h+='
Size distribution
'+s.buckets.map(function(bk){return brkBar(bk.label,bk.n/maxb,''+bk.n+'',gradFor('size',bk.label))}).join(''); h+=brkSection('By type',s.byType,'type'); @@ -1526,8 +2246,8 @@ function visualHtml(v){ function renderInsight(){ var el=$('insight');if(!el)return;var ins=state.s_insight; if(!ins){el.innerHTML='';return} - if(ins.pending){el.innerHTML='
Copilot is interpreting the statistics\u2026
';return} - var h='
\u{1F9E0} Copilot insight
'; + if(ins.pending){el.innerHTML='
'+uiIcon('busy',{size:13,className:'spin'})+' Copilot is interpreting the statistics\u2026
';return} + var h='
'+uiIcon('insight')+' Copilot insight
'; h+='
'+mdInline(ins.answer)+'
'; h+=visualHtml(ins.visual); if(ins.suggestedFilter&&Object.keys(ins.suggestedFilter).length){ @@ -1546,7 +2266,7 @@ function renderInsight(){ h+='
'; el.innerHTML=h; if($('insightX'))$('insightX').onclick=function(){bridge.send('/dismiss-insight',{})}; - if($('applySug'))$('applySug').onclick=function(){var f=state.s_insight.suggestedFilter||{};syncFilterForm(f);bridge.send('/filter',f);var sb=$('applySug');if(sb){sb.disabled=true;sb.textContent='\u2713 Applied'}}; + if($('applySug'))$('applySug').onclick=function(){var f=state.s_insight.suggestedFilter||{};syncFilterForm(f);bridge.send('/filter',f);var sb=$('applySug');if(sb){sb.disabled=true;sb.innerHTML=uiIcon('check')+' Applied'}}; } function globOk(pat,name){pat=(''+pat).toLowerCase();name=(''+name).toLowerCase(); if(pat.indexOf('*')===-1)return pat===name; @@ -1575,92 +2295,79 @@ function countMatch(f){f=f||{};var b=state.s_blobs||[];if(!b.length)return 0; function metricCard(label,val,color){var v=String(val==null?'':val);var m=v.match(/^([d.,]+)s*(.*)$/);var main=m?m[1]:v;var unit=(m&&m[2])?' '+esc(m[2])+'':'';return '
'+esc(label)+'
'+esc(main)+unit+'
'} function gradFor(kind,label){ if(kind==='tier'){var t=String(label||'').toLowerCase(); - if(t.indexOf('hot')>=0)return 'linear-gradient(90deg,#f59e0b,#fcd34d)'; - if(t.indexOf('archive')>=0)return 'linear-gradient(90deg,#7c3aed,#a78bfa)'; - if(t.indexOf('cool')>=0)return 'linear-gradient(90deg,#2563eb,#60a5fa)'; - if(t.indexOf('cold')>=0)return 'linear-gradient(90deg,#0891b2,#22d3ee)'; - return 'linear-gradient(90deg,#4b5563,#9ca3af)';} - if(kind==='type')return 'linear-gradient(90deg,#8b5cf6,#c084fc)'; - if(kind==='ext')return 'linear-gradient(90deg,#0ea5e9,#38bdf8)'; - return 'linear-gradient(90deg,#3b82f6,#22d3ee)'; + if(t.indexOf('hot')>=0)return 'linear-gradient(90deg,var(--warn),var(--text-3))'; + if(t.indexOf('archive')>=0)return 'linear-gradient(90deg,var(--text-3),var(--surface-3))';} + return 'linear-gradient(90deg,var(--accent),var(--accent-soft))'; } function brkBar(label,frac,valHtml,grad){var f=frac||0;var w=(f>0)?Math.max(3,Math.min(100,Math.round(100*f))):0;return '
'+esc(label)+'
'+valHtml+'
'} function brkSection(title,items,kind){if(!items||!items.length)return '';var maxb=Math.max.apply(null,items.map(function(x){return Number(x.bytes)||0}).concat([1]));return '
'+esc(title)+'
'+items.map(function(t){var lbl=(t.label==null||t.label===''||t.label==='\u2014')?'(untiered)':String(t.label);return brkBar(lbl,(Number(t.bytes)||0)/maxb,''+t.n+' \xB7 '+esc(fmtBytes(t.bytes))+'',gradFor(kind,lbl))}).join('')} -function sasPerm(on,label){return ''+(on?'\u2713 ':'\u2715 ')+label+''} -var SAS_CHECK=''; -var SAS_LOCK=''; -var IC_OPEN=''; -var IC_LINK=''; -var IC_DL=''; -function permChip(on,label){return ''+(on?SAS_CHECK:SAS_LOCK)+label+''} -var CAP_READ=''; -var CAP_LIST=''; -var CAP_WRITE=''; -var CAP_DELETE=''; -var CAP_LOCK=''; -function capChip(on,label,ic){return ''+ic+''+label+(on?'':''+CAP_LOCK+'')+''} -function tierColor(t){var k=String(t||'').toLowerCase();if(k==='hot')return '#e0a13c';if(k==='cool')return '#4b9df0';if(k==='cold')return '#42c5c5';if(k==='archive')return '#a06cf0';return '#8a93a6'} +function sasPerm(on,label){return ''+uiIcon(on?'check':'clear')+label+''} +function permChip(on,label){return ''+(on?uiIcon('check',{className:'pi'}):uiIcon('lock',{className:'pi'}))+label+''} +function capChip(on,label,ic){return ''+ic+''+label+(on?'':''+uiIcon('lock')+'')+''} +function tierColor(t){var k=String(t||'').toLowerCase();if(k==='hot')return 'var(--warn)';if(k==='cool'||k==='cold')return 'var(--accent)';return 'var(--text-3)'} function tierChip(t){if(!t)return '\xB7';var c=tierColor(t);return ''+esc(t)+''} function expChipHtml(info){ if(!info.expiresOn)return 'no expiry'; var t=Date.parse(info.expiresOn);var d=String(info.expiresOn).slice(0,10); - if(info.expired||(isFinite(t)&&t\u26A0 expired '+esc(d)+''; + if(info.expired||(isFinite(t)&&t'+uiIcon('warning')+' expired '+esc(d)+''; if(isFinite(t)){var days=Math.round((t-Date.now())/86400000);if(days<=7)return 'expires in '+days+'d ('+esc(d)+')'} return 'expires '+esc(d)+''; } function renderSas(){ var el=$('sas');var info=state.s_sasInfo;var blobs=state.s_sasBlobs||[]; - if(!info){el.innerHTML='
\u{1F511} No SAS connected yet. Paste a container SAS URL above and click Connect.
';return} + var sasPage=info&&(state.s_pages||{})[storagePageKey({account:info.account,kind:'sas',resource:info.container})]; + var selectionScope=JSON.stringify([info&&info.account,info&&info.container,sasPage&&sasPage.index]);if(selectionScope!==sasSelectionScope){sasSelected.clear();sasSelectionScope=selectionScope} + sasSelected.forEach(function(name){if(!blobs.some(function(blob){return blob.name===name}))sasSelected.delete(name)}); + if(!info){el.innerHTML='
No SAS connected. Paste a container SAS URL above to connect.
';return} var st=state.s_sasStats; var h='
'; - h+='
'; - h+='
'+esc(info.account)+'\u203A'+cubeIc()+''+esc(info.container)+'
Shared Access Signature \xB7 read-only connection
'; + h+='
'+svgIcon('account')+'
'; + h+='
'+esc(info.account)+''+uiIcon('forward',{className:'chev'})+cubeIc()+''+esc(info.container)+'
Shared Access Signature \xB7 read-only connection
'; h+='
Connected'+expChipHtml(info)+'
'; - h+='
Permissions
'+capChip(info.canRead,'Read',CAP_READ)+capChip(info.canList,'List',CAP_LIST)+capChip(info.canWrite,'Write',CAP_WRITE)+capChip(info.canDelete,'Delete',CAP_DELETE)+'
'; + h+='
Permissions
'+capChip(info.canRead,'Read',uiIcon('read'))+capChip(info.canList,'List',uiIcon('list'))+capChip(info.canWrite,'Write',uiIcon('edit'))+capChip(info.canDelete,'Delete',uiIcon('trash'))+'
'; if(st){h+='
'+st.count+'blobs
'+esc(st.totalh)+'total size
'+esc(st.maxh)+'largest
'} h+='
'; - if(!blobs.length){h+='
No blobs (or this SAS lacks list permission).
';el.innerHTML=h;return} + if(!blobs.length){h+='
No blobs on this page.
';el.innerHTML=h;wireStoragePager(el,{account:info.account,kind:'sas',resource:info.container});return} var ft=sasFilterText.toLowerCase(); var shown=blobs.filter(function(x){return !ft||String(x.name).toLowerCase().indexOf(ft)>-1}); if(sasSort==='size')shown=shown.slice().sort(function(a,b){return (Number(b.size)||0)-(Number(a.size)||0)}); else shown=shown.slice().sort(function(a,b){return String(a.name).localeCompare(String(b.name))}); - var urlMap={};shown.forEach(function(x){urlMap[x.name]=x.url||''}); - h+='
'; - h+=''+shown.length+' of '+blobs.length+' shown'; - var hasVS=!!info.canRead; - var actHead=hasVS?'
ViewShareSave
':'
Save
'; - h+=''; + h+='
'; + h+='
NameSizeModifiedTier'+actHead+'
'; h+=shown.map(function(x){ var mod=x.lastModified?String(x.lastModified).slice(0,10):'\xB7'; var tier=tierChip(x.accessTier); - var acts='
'; - if(x.url){acts+=''} - acts+='
'; - return '' - }).join('')+'
NameSizeModifiedTier
'+esc(x.name)+''+esc(fmtBytes(x.size))+''+esc(mod)+''+tier+''+acts+'
'; + return ''+esc(x.name)+''+esc(fmtBytes(x.size))+''+esc(mod)+''+tier+'' + }).join('')+'
'; h+=''; el.innerHTML=h; + wireStoragePager(el,{account:info.account,kind:'sas',resource:info.container}); var fi=$('sasFilter');if(fi){fi.oninput=function(){sasFilterText=fi.value;var pos=fi.selectionStart;renderSas();var nf=$('sasFilter');if(nf){nf.focus();try{nf.setSelectionRange(pos,pos)}catch(e){}}}} if($('sasSortName'))$('sasSortName').onclick=function(){sasSort='name';renderSas()}; if($('sasSortSize'))$('sasSortSize').onclick=function(){sasSort='size';renderSas()}; - function sasShowDl(dir,label){var m=$('sasDlMsg');if(!m)return;if(!dir){m.style.display='flex';m.innerHTML='\u2713 '+esc(label)+'';return}m.style.display='flex';m.innerHTML='\u2713 '+esc(label)+''+esc(dir)+'';var od=$('sasOpenDir');if(od)od.onclick=function(){od.textContent='Opening\u2026';bridge.send('/open-folder',{dir:dir}).then(function(r){od.textContent=(r&&r.ok)?'\u2713 Opened':'Couldn\u2019t open';setTimeout(function(){if($('sasOpenDir'))od.textContent='Open folder'},1600)})};var cd=$('sasCopyDir');if(cd)cd.onclick=function(){try{navigator.clipboard.writeText(dir)}catch(e){}cd.textContent='\u2713 Copied';setTimeout(function(){if($('sasCopyDir'))cd.textContent='Copy path'},1400)}} - if($('sasDlAll'))$('sasDlAll').onclick=function(){var b=$('sasDlAll');b.disabled=true;var o=b.innerHTML;b.innerHTML='Saving\u2026';var n=shown.length,done=0,dir=null;shown.forEach(function(x){bridge.send('/sas-download',{name:x.name}).then(function(r){done++;if(r&&r.dir)dir=r.dir;if(done===n){b.disabled=false;b.innerHTML=o;sasShowDl(dir,'Saved '+n+' file'+(n===1?'':'s')+' to')}})})}; - Array.prototype.forEach.call(el.querySelectorAll('[data-sasdl]'),function(btn){btn.onclick=function(){var nm=btn.getAttribute('data-sasdl');bridge.send('/sas-download',{name:nm}).then(function(r){if(r&&r.ok){sasShowDl(r.dir,'Saved '+nm+' to')}else{sasShowDl(null,'Download failed'+(r&&r.error?': '+r.error:''))}})}}); - Array.prototype.forEach.call(el.querySelectorAll('[data-sasopen]'),function(btn){btn.onclick=function(){var u=urlMap[btn.getAttribute('data-sasopen')];if(u){try{window.open(u,'_blank')}catch(e){}}}}); - Array.prototype.forEach.call(el.querySelectorAll('[data-sascopy]'),function(btn){btn.onclick=function(){var u=urlMap[btn.getAttribute('data-sascopy')];if(!u)return;try{navigator.clipboard.writeText(u)}catch(e){}var o=btn.innerHTML;btn.innerHTML=SAS_CHECK;setTimeout(function(){btn.innerHTML=o},1300)}}); + function sasShowDl(dir,label){var m=$('sasDlMsg');if(!m)return;m.style.display='flex';m.innerHTML=''+uiIcon('check')+' '+esc(label)+''+(dir?''+esc(dir)+'':'');var od=$('sasOpenDir');if(od)od.onclick=function(){bridge.send('/open-folder',{dir:dir}).then(function(r){if(!r||!r.ok){state.s_error='Could not open download folder: '+(r&&r.error||dir);renderError()}})};var cd=$('sasCopyDir');if(cd)cd.onclick=function(){navigator.clipboard.writeText(dir).catch(function(error){state.s_error='Could not copy path: '+error.message;renderError()})}} + function updateSasSelection(){var one=sasSelected.size===1&&blobs.find(function(blob){return sasSelected.has(blob.name)});$('sasOpen').disabled=$('sasCopy').disabled=!(one&&one.url);$('sasDlAll').disabled=sasDownloading||!sasSelected.size} + Array.prototype.forEach.call(el.querySelectorAll('[data-sas-select]'),function(input){input.onchange=function(){if(input.checked)sasSelected.add(input.dataset.sasSelect);else sasSelected.delete(input.dataset.sasSelect);updateSasSelection()}});updateSasSelection(); + $('sasSelectAll').onclick=function(){shown.forEach(function(blob){sasSelected.add(blob.name)});renderSas()}; + $('sasSelectNone').onclick=function(){shown.forEach(function(blob){sasSelected.delete(blob.name)});renderSas()}; + $('sasOpen').onclick=function(){var blob=blobs.find(function(x){return sasSelected.has(x.name)});if(sasSelected.size===1&&blob&&blob.url)window.open(blob.url,'_blank','noopener')}; + $('sasCopy').onclick=function(){var blob=blobs.find(function(x){return sasSelected.has(x.name)});if(sasSelected.size===1&&blob&&blob.url)navigator.clipboard.writeText(blob.url).catch(function(error){state.s_error='Could not copy link: '+error.message;renderError()})}; + $('sasDlAll').onclick=async function(){if(sasDownloading||!sasSelected.size)return;var names=Array.from(sasSelected),source=state.s_sasUrl,dir=null;sasDownloading=true;updateSasSelection();for(var name of names){var result=state.s_sasUrl===source?await bridge.send('/sas-download',{name:name}):{ok:false,error:'SAS connection changed; remaining downloads stopped'};if(!result||!result.ok){sasDownloading=false;renderSas();state.s_error='Download failed: '+(result&&result.error||name);renderError();return}dir=result.dir}sasDownloading=false;renderSas();sasShowDl(dir,'Saved '+names.length+' file'+(names.length===1?'':'s')+' to')}; } function renderTransfer(){ var el=$('transfer');var t=state.s_transfer; + $('xplanBtn').disabled=!!state.s_transferProgress; + if(state.s_transferProgress){var progress=state.s_transferProgress;el.innerHTML='
'+uiIcon('busy',{size:13,className:'spin'})+' Reading complete '+esc(progress.side)+' inventory\u2026 '+Number(progress.count).toLocaleString()+' blobs read
';return} if(!t){ - if(state.s_transferErr){el.innerHTML='
\u26A0
Transfer plan failed
'+esc(state.s_transferErr)+'
';return} + if(state.s_transferErr){var warning=state.s_transferSeverity==='warning';el.innerHTML='
'+uiIcon('warning')+'
'+(warning?'Transfer planning limit':'Transfer plan failed')+'
'+esc(state.s_transferErr)+'
';return} el.innerHTML='';return} var p=t.plan; var conflictHi=p.conflictCount>0; var h='
'; - h+='
'+CONTAINER_IC+''+esc(t.sourceLabel)+''; - h+=''+MOVE_ARROW+''; - h+=''+CONTAINER_IC+''+esc(t.destLabel)+''; - h+=''+POLICY_IC+esc(p.resolution)+'
'; + h+='
'+svgIcon('blob')+''+esc(t.sourceLabel)+''; + h+=''+uiIcon('move')+''; + h+=''+svgIcon('blob')+''+esc(t.destLabel)+''; + h+=''+uiIcon('policy',{size:12,style:'margin-right:4px'})+esc(p.resolution)+'
'; h+='
'; h+='
'+p.transferCount+'
To transfer
'; h+='
'+esc(p.transferBytesh)+'
Total size
'; @@ -1673,7 +2380,7 @@ function renderTransfer(){ if(p.resolution==='overwrite'){pcls='pe-ow';pe='Overwrite: all '+p.newCount+' new blob'+(p.newCount===1?'':'s')+' plus all '+p.conflictCount+' existing (conflicting) blob'+(p.conflictCount===1?'':'s')+' will be re-uploaded, replacing the destination copies. Nothing is skipped.'} else if(p.resolution==='skip'){pcls='pe-skip';pe='Skip existing: only the '+p.newCount+' new blob'+(p.newCount===1?'':'s')+' transfer'+(p.newCount===1?'s':'')+'. The '+p.conflictCount+' blob'+(p.conflictCount===1?'':'s')+' that already exist at the destination are left untouched.'} else{pcls='pe-ifn';pe='If source newer: new blobs always transfer; a conflicting blob transfers only if the source copy is newer than the destination. Here '+newerN+' of '+p.conflictCount+' '+(newerN===1?'is':'are')+' newer (will replace), and '+keptN+' '+(keptN===1?'is':'are')+' not newer (kept). '+(newerN===0?'That is why the totals match Skip for this data.':'')} - h+='
'+POLICY_IC+''+pe+'
'; + h+='
'+uiIcon('policy',{size:12,style:'margin-right:4px'})+''+pe+'
'; h+='
'; if(p.conflicts&&p.conflicts.length){ var actLabel=p.resolution==='overwrite'?'Overwrite':(p.resolution==='skip'?'Skip':'If newer'); @@ -1683,153 +2390,53 @@ function renderTransfer(){ h+=p.conflicts.map(function(c){var mod=c.lastModified?String(c.lastModified).slice(0,10):'\xB7';return ''+esc(c.name)+''+esc(c.sizeh)+''+esc(mod)+''+actLabel+''}).join(''); h+='
'; } - h+='
'+TERM_IC+'AzCopy command'+(t.needsLogin?''+LOCK_IC+'Sign-in required':''+CHECK_IC+'Ready to run')+'
'; - h+='
'+esc(t.command)+'
'; - if(t.needsLogin)h+='
'+INFO_IC+'One endpoint has no SAS token. Run azcopy login first, or use a destination SAS URL so no sign-in is needed. Nothing moves until you run this command.
'; - else h+='
'+INFO_IC+'Copy and run in your terminal. Nothing moves until you run it.
'; - if(t.azcopy&&t.azcopy.installed)h+='
'+CHECK_IC+'AzCopy '+esc(t.azcopy.version||'')+' detected on your PATH.
'; - else if(t.azcopy)h+='
'+LOCK_IC+''+esc(t.azcopy.hint||'AzCopy is required to run this command.')+' Install it from aka.ms/downloadazcopy, then re-open the plan.
'; + h+='
'+uiIcon('terminal',{size:13,style:'margin-right:6px'})+'AzCopy command'+(t.needsLogin?''+uiIcon('lock',{size:11,style:'margin-right:4px'})+'Sign-in required':''+uiIcon('check',{size:11,style:'margin-right:4px'})+'Ready to run')+'
'; + h+='
'+esc(t.command.replace(/([?&]sig=)[^&\\s"]*/gi,'$1[hidden]'))+'
'; + if(/[?&]sig=/.test(t.command))h+='
'+uiIcon('info',{size:13,style:'flex:0 0 auto;margin-top:1px'})+'SAS signatures are hidden here. Copy includes the full credentials; keep the command private.
'; + if(t.needsLogin)h+='
'+uiIcon('info',{size:13,style:'flex:0 0 auto;margin-top:1px'})+'An endpoint has no SAS token. Run azcopy login first, or use SAS URLs for both source and destination. Nothing moves until you run this command.
'; + else h+='
'+uiIcon('info',{size:13,style:'flex:0 0 auto;margin-top:1px'})+'Copy and run in your terminal. Nothing moves until you run it.
'; + if(t.azcopy&&t.azcopy.installed)h+='
'+uiIcon('check',{size:11,style:'margin-right:4px'})+'AzCopy '+esc(t.azcopy.version||'')+' detected on your PATH.
'; + else if(t.azcopy)h+='
'+uiIcon('lock',{size:11,style:'margin-right:4px'})+''+esc(t.azcopy.hint||'AzCopy is required to run this command.')+' Install it from aka.ms/downloadazcopy, then re-open the plan.
'; h+='
'; el.innerHTML=h; - if($('xcopyBtn'))$('xcopyBtn').onclick=function(){var txt=t.command;if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(txt)}else{var r=document.createRange();r.selectNode($('xcmd'));var s=window.getSelection();s.removeAllRanges();s.addRange(r);try{document.execCommand('copy')}catch(e){}}$('xcopyBtn').textContent='Copied \u2713';setTimeout(function(){if($('xcopyBtn'))$('xcopyBtn').textContent='Copy command'},1500)}; -} -var ASK_AV=''; -var ASK_SPARK=''; -function doAsk(q){q=(q||'').trim();if(!q)return;var inp=$('askIn');if(inp)inp.value=q;bridge.send('/ask',{question:q})} -function askSuggestions(){ - var k=state.s_kind,item=state.s_item,acct=state.s_account; - if(k==='blob'&&item)return ['What is taking up the most space here?','Find blobs I can clean up','Which blobs are the biggest?','Any old or stale blobs?','Break these blobs down by type']; - if(acct)return ['Which container is the largest?','How many blobs are in this account?','Summarize this storage account']; - return ['What can I ask here?','How do I choose a container to explore?']; -} -function renderAskChips(){ - var el=$('askChips');if(!el)return; - var s=askSuggestions(); - el.innerHTML='Try'+s.map(function(q){return ''}).join(''); - Array.prototype.forEach.call(el.querySelectorAll('.askchip'),function(b){b.onclick=function(){doAsk(b.getAttribute('data-q'))}}); + if($('xcopyBtn'))$('xcopyBtn').onclick=function(){var txt=t.command;if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(txt)}else{var r=document.createRange();r.selectNode($('xcmd'));var s=window.getSelection();s.removeAllRanges();s.addRange(r);try{document.execCommand('copy')}catch(e){}}$('xcopyBtn').innerHTML=uiIcon('check')+' Copied';setTimeout(function(){if($('xcopyBtn'))$('xcopyBtn').innerHTML=uiIcon('copy',{size:12,style:'margin-right:5px'})+'Copy command'},1500)}; } function renderAsk(){ - var el=$('askOut');var a=state.s_ask;if(!a){el.innerHTML='';return} - var h=''; - if(a.question)h+='
'+esc(a.question)+'
'; - if(a.pending){el.innerHTML=h+'
'+ASK_AV+'
Reading your live storage\u2026
';return} - h+='
'+ASK_AV+'
'+mdInline(a.answer)+'
'+visualHtml(a.visual)+'
'; - el.innerHTML=h; + var el=$('resourceAnnotation');var a=state.s_ask; + el.innerHTML=a&&!a.pending&&a.answer?'

Agent notes

'+mdInline(a.answer)+visualHtml(a.visual)+'
':''; } -function renderLog(){var el=$('log');var lines=(state.activityLog||[]);el.textContent=lines.slice(-40).join('\\n');el.scrollTop=el.scrollHeight;var b=$('logCount');if(b)b.textContent=lines.length+(lines.length===1?' entry':' entries')} -var MOVE_ARROW=''; -var CONTAINER_IC=''; -var POLICY_IC=''; -var TERM_IC=''; -var COPY_IC=''; -var LOCK_IC=''; -var CHECK_IC=''; -var INFO_IC=''; +function renderLog(){var el=$('log');var lines=(state.activityLog||[]);el.textContent=lines.slice(-40).join('\\n');el.scrollTop=el.scrollHeight} Array.prototype.forEach.call(document.querySelectorAll('.card-head'),function(head){ - head.onclick=function(){head.parentNode.classList.toggle('min')}; + head.setAttribute('role','button');head.tabIndex=0;head.setAttribute('aria-expanded',String(!head.parentNode.classList.contains('min'))); + head.onclick=function(){head.parentNode.classList.toggle('min');head.setAttribute('aria-expanded',String(!head.parentNode.classList.contains('min')))}; + head.onkeydown=function(e){if(e.key==='Enter'||e.key===' '){e.preventDefault();head.click()}}; }); -var CHEV_LEFT=''; -var CHEV_RIGHT=''; -function expandSide(){var side=$('side'),shell=$('shell');side.classList.remove('collapsed');shell.classList.remove('collapsed');$('sideToggle').innerHTML=CHEV_LEFT;renderCtx()} -function scrollToTreeTarget(kind){ - setTimeout(function(){ - var sel=kind==='account'?'[data-acct="'+state.s_account+'"]':'[data-cat="'+kind+'"]'; - var r=document.querySelector(sel);if(r)r.scrollIntoView({block:'nearest'}); - },80); -} -// rail icon \u2192 jump straight to that service for the active account (not just expand) -function railGo(kind){ - expandSide(); - var acct=state.s_account; - if(!acct){ - // no account opened yet \u2014 reveal the subscription/account list so the user can pick one - (state.s_selectedSubs||[]).forEach(function(s){openSubs[s]=true}); - (state.s_accounts||[]).forEach(function(x){openSubs[x.subscriptionName||'(no subscription)']=true}); - renderTree();return; - } - var entry=null;(state.s_accounts||[]).forEach(function(x){if(x.name===acct)entry=x}); - if(entry)openSubs[entry.subscriptionName||'(no subscription)']=true; - openAccts[acct]=true; - if(kind!=='account'){ - openCat[acct+'|'+kind]=true; - if(kind==='file')bridge.send('/list-shares',{account:acct}); - else if(kind==='queue')bridge.send('/list-queues',{account:acct}); - else if(kind==='table')bridge.send('/list-tables',{account:acct}); - } - renderTree(); - scrollToTreeTarget(kind); -} +function expandSide(){var side=$('side'),shell=$('shell');side.classList.remove('collapsed');shell.classList.remove('collapsed');$('sideToggle').innerHTML=uiIcon('collapse')} $('sideToggle').onclick=function(){ var side=$('side'),shell=$('shell'); side.classList.toggle('collapsed');shell.classList.toggle('collapsed'); - $('sideToggle').innerHTML=side.classList.contains('collapsed')?CHEV_RIGHT:CHEV_LEFT; - renderCtx(); + $('sideToggle').innerHTML=side.classList.contains('collapsed')?uiIcon('expand'):uiIcon('collapse'); }; (function buildRail(){ var r=$('sideRail');if(!r)return; - var h=''; - h+='
'; - h+=''; - h+=''; - h+=''; - h+=''; - h+=''; - h+='
'; - h+=''; - h+=''; - r.innerHTML=h; - Array.prototype.forEach.call(r.querySelectorAll('[data-rail]'),function(b){b.onclick=function(){ - var kind=b.getAttribute('data-rail'); - if(kind==='refresh'){expandSide();bridge.send('/load-subscriptions',{});return} - if(kind==='key'){expandSide();openPicker();return} - if(kind==='search'){expandSide();setTimeout(function(){var s=$('treeSearch');if(s)s.focus()},0);return} - railGo(kind); - }}); + r.innerHTML=''; })(); -$('refreshBtn').onclick=function(){bridge.send('/load-subscriptions',{})}; +$('refreshBtn').onclick=async function(){ + var failed={};(state.s_subscriptionErrors||[]).forEach(function(e){failed[e.subscriptionId]=true}); + var ids=(state.s_selectedSubs||[]).filter(function(id){ + var sub=(state.s_subscriptions||[]).find(function(s){return s.subscriptionId===id}); + return sub&&!failed[id]&&!hiddenSubscriptions[id]&&!hiddenTenants[sub.tenantId]; + }); + var result=await bridge.send('/load-subscriptions',{}); + if(result&&result.ok&&ids.length)bridge.send('/select-subscriptions',{subscriptionIds:ids}); +}; $('treeSearch').oninput=function(){treeQuery=$('treeSearch').value;renderTree()}; -function openPicker(){ - pickOpen=true;pickQuery=''; - pickSel=new Set(state.s_selectedSubs||[]); - $('pickSearch').value=''; - $('pickerOverlay').classList.add('open'); - if(!(state.s_subscriptions||[]).length&&!state.s_subsLoading){bridge.send('/load-subscriptions',{})} - renderPicker();$('pickSearch').focus(); -} -function closePicker(){pickOpen=false;$('pickerOverlay').classList.remove('open')} -function renderPicker(){ - var subs=state.s_subscriptions||[]; - $('pickCount').textContent=subs.length?subs.length+' subscriptions':''; - var q=pickQuery.toLowerCase(); - var list=$('pickList'); - if(state.s_subsLoading&&!subs.length){list.innerHTML='
Loading subscriptions\u2026
'} - else if(!subs.length){list.innerHTML='
No subscriptions found.
'} - else{ - var shown=subs.filter(function(s){if(!q)return true;return (s.displayName||'').toLowerCase().indexOf(q)>=0||(s.subscriptionId||'').toLowerCase().indexOf(q)>=0}); - if(!shown.length){list.innerHTML='
No subscriptions match.
'} - else{ - list.innerHTML=shown.map(function(s){var on=pickSel.has(s.subscriptionId);return ''}).join(''); - Array.prototype.forEach.call(list.querySelectorAll('[data-sub]'),function(r){r.onclick=function(e){e.preventDefault();var id=r.getAttribute('data-sub');if(pickSel.has(id))pickSel.delete(id);else pickSel.add(id);renderPicker()}}); - } - } - $('pickSelected').textContent=pickSel.size+' selected'; - $('pickApply').disabled=!pickSel.size; -} -$('scopeChip').onclick=openPicker; -$('scopeChange').onclick=openPicker; -$('pickClose').onclick=closePicker; -$('pickCancel').onclick=closePicker; -$('pickClear').onclick=function(){pickSel.clear();renderPicker()}; -$('pickSearch').oninput=function(){pickQuery=$('pickSearch').value;renderPicker()}; -$('pickApply').onclick=function(){if(!pickSel.size)return;bridge.send('/select-subscriptions',{subscriptionIds:Array.from(pickSel)});closePicker()}; -$('pickerOverlay').onclick=function(e){if(e.target===$('pickerOverlay'))closePicker()}; -document.addEventListener('keydown',function(e){if(e.key==='Escape'&&pickOpen)closePicker()}); // \u2500\u2500 confirm / prompt dialog (guards every mutation) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -var cfResolve=null; +var cfResolve=null,cfValidate=null; function askConfirm(opts){ return new Promise(function(resolve){ cfResolve=resolve; @@ -1840,20 +2447,21 @@ function askConfirm(opts){ $('cfInput').rows=opts.rows||1; $('cfOk').textContent=opts.okLabel||'OK'; $('cfOk').className=opts.danger?'danger':'primary'; + cfValidate=opts.validate||null;$('cfError').textContent=''; $('confirmOverlay').classList.add('open'); if(opts.input)setTimeout(function(){$('cfInput').focus();$('cfInput').select()},0); }); } function cfClose(val){$('confirmOverlay').classList.remove('open');var r=cfResolve;cfResolve=null;if(r)r(val)} $('cfCancel').onclick=function(){cfClose(null)}; -$('cfOk').onclick=function(){cfClose($('cfInputWrap').style.display==='none'?true:$('cfInput').value)}; +$('cfOk').onclick=function(){var val=$('cfInputWrap').style.display==='none'?true:$('cfInput').value;var error=cfValidate?cfValidate(val):'';if(error){$('cfError').textContent=error;$('cfInput').focus();return}cfClose(val)}; $('cfInput').onkeydown=function(e){if(e.key==='Enter'&&e.metaKey===false&&$('cfInput').rows==1){e.preventDefault();$('cfOk').click()}if(e.key==='Escape')cfClose(null)}; $('confirmOverlay').onclick=function(e){if(e.target===$('confirmOverlay'))cfClose(null)}; // \u2500\u2500 Table entity form (friendly, no raw JSON) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 function entPropRow(name,type,val){ var opt=function(t){return ''+t+''}; - return '
'; + return '
'; } function entInferType(v){if(typeof v==='boolean')return 'Boolean';if(typeof v==='number')return 'Number';return 'String'} function entAddPropRow(name,type,val){ @@ -1908,7 +2516,7 @@ function sasResetCommon(){ } function openSasGen(name){ sasScope='blob';sasGenName=name; - $('sgTitle').textContent='\u{1F517} Create SAS link'; + $('sgTitle').textContent='Create SAS link'; $('sgSub').textContent='A secure, time-limited URL for the blob "'+name+'". No Azure login needed to use it.'; $('sgpLwrap').style.display='none';$('sgpL').checked=false; $('sgpR').checked=true;$('sgpW').checked=false;$('sgpD').checked=false; @@ -1917,7 +2525,7 @@ function openSasGen(name){ function openContainerSas(){ var c=state.s_container;if(!c)return; sasScope='container';sasGenContainer=c; - $('sgTitle').textContent='\u{1F517} Share container (SAS)'; + $('sgTitle').textContent='Share container (SAS)'; $('sgSub').textContent='A secure, time-limited URL to browse & download everything in the container "'+c+'". Share it, or paste it into the Instant SAS tab / Storage Explorer.'; $('sgpLwrap').style.display='';$('sgpL').checked=true; $('sgpR').checked=true;$('sgpW').checked=false;$('sgpD').checked=false; @@ -1939,9 +2547,9 @@ $('sgGo').onclick=function(){ var modeTxt=r.mode==='user-delegation'?'User delegation (AAD)':'Account key'; var scopeTxt=isC?'Container':'Blob'; var res=$('sgResult');res.style.display=''; - res.innerHTML='
\u2713 SAS ready'+scopeTxt+'expires '+esc(exp)+''+esc(modeTxt)+'
'; + res.innerHTML='
SAS ready'+scopeTxt+'expires '+esc(exp)+''+esc(modeTxt)+'
'; var u=$('sgUrl');u.value=r.url;setTimeout(function(){u.focus();u.select()},0); - $('sgCopy').onclick=function(){try{navigator.clipboard.writeText(r.url)}catch(e){}var cb=$('sgCopy');cb.textContent='\u2713 Copied';setTimeout(function(){if($('sgCopy'))cb.textContent='\u{1F4CB} Copy link'},1400)}; + $('sgCopy').onclick=function(){try{navigator.clipboard.writeText(r.url)}catch(e){}var cb=$('sgCopy');cb.textContent='Copied';setTimeout(function(){if($('sgCopy'))cb.textContent='Copy link'},1400)}; $('sgOpen').onclick=function(){var ou=r.url;if(isC)ou=r.url+'&restype=container&comp=list';window.open(ou,'_blank')}; }); }; @@ -1953,9 +2561,13 @@ $('sasOverlay').onclick=function(e){if(e.target===$('sasOverlay'))closeSasGen()} function createInCategory(kind,account){ account=account||state.s_account;if(!account)return; var label=kind==='blob'?'blob container':kind==='queue'?'queue':kind==='table'?'table':'file share'; - askConfirm({title:'New '+label,msg:'Name for the new '+label+':',input:true,okLabel:'Create'}).then(function(name){ + askConfirm({title:'New '+label,msg:'Name for the new '+label+':',input:true,okLabel:'Create',validate:function(name){ + name=String(name||'').trim();if(!name)return 'Enter a name.'; + var tree=(state.s_tree||{})[account]||{},items=kind==='blob'?tree.containers:kind==='file'?tree.shares:kind==='queue'?tree.queues:tree.tables; + if((items||[]).some(function(x){return x.name.toLowerCase()===name.toLowerCase()}))return 'A '+label+' named "'+name+'" already exists. Choose a different name.'; + return ''; + }}).then(function(name){ name=(name||'').trim();if(!name)return; - openCat[account+'|'+kind]=true; if(kind==='blob')bridge.send('/container-create',{container:name,account:account}); else if(kind==='queue')bridge.send('/queue-create',{queue:name,account:account}); else if(kind==='table')bridge.send('/table-create',{table:name,account:account}); @@ -1976,27 +2588,41 @@ function deleteItem(kind,name,account){ } function containerOpen(){return state.s_kind==='blob'&&!!state.s_container} -function cubeIc(){return ''} -function acctIc(){return ''} +function cubeIc(){return svgIcon('blob')} +function acctIc(){return svgIcon('account')} function needContainerHint(){$('stats').innerHTML='
'+cubeIc()+'Open a container first. Pick one from the tree on the left, then Filter its blobs.
'} $('filterBtn').onclick=function(){ if(!containerOpen()){needContainerHint();return} var crit={namePattern:$('fName').value,minSize:$('fMin').value,maxSize:$('fMax').value,olderThanDays:$('fOlder').value,newerThanDays:$('fNewer').value,type:$('fType').value,tiers:$('fTier').value,blobTypes:$('fBlobType').value}; var tag=$('fTag').value.trim(); - if(tag){var eq=tag.indexOf('=');if(eq>-1){var o={};o[tag.slice(0,eq).trim()]=tag.slice(eq+1).trim();crit.tags=o}else{crit.tagKeys=tag}} + var applied=state.s_criteria||{}; + if(tag&&tag===filterTagText(applied)){if(applied.tags)crit.tags=applied.tags;if(applied.tagKeys)crit.tagKeys=applied.tagKeys} + else if(tag){var eq=tag.indexOf('=');if(eq>-1){var o={};o[tag.slice(0,eq).trim()]=tag.slice(eq+1).trim();crit.tags=o}else{crit.tagKeys=tag}} bridge.send('/filter',crit); }; +function filterTagText(f){ + return [].concat(f.tagKeys||[]).concat(Object.keys(f.tags||{}).map(function(key){return key+'='+f.tags[key]})).join(', '); +} function syncFilterForm(f){ f=f||{}; - function setV(id,v){var e=$(id);if(e)e.value=(v==null?'':String(v))} + function setV(id,v){var e=$(id),value=v==null?'':String(v);if(e&&e.value!==value)e.value=value} + function setList(id,v){ + var e=$(id),value=[].concat(v||[]).join(','); + Array.prototype.forEach.call(e.querySelectorAll('[data-applied-filter]'),function(option){option.remove()}); + if(value&&!Array.prototype.some.call(e.options,function(option){return option.value===value})){ + var option=document.createElement('option');option.value=value;option.textContent=value;option.dataset.appliedFilter='true';e.append(option); + } + setV(id,value); + } setV('fName',f.namePattern); setV('fMin',f.minSize); setV('fMax',f.maxSize); setV('fOlder',f.olderThanDays); setV('fNewer',f.newerThanDays); setV('fType',f.type||'all'); - setV('fTier',f.tiers?[].concat(f.tiers)[0]:''); - var card=document.querySelector('[data-card="stats"]');if(card&&card.classList.contains('min'))card.classList.remove('min'); + setList('fTier',f.tiers); + setList('fBlobType',f.blobTypes); + setV('fTag',filterTagText(f)); } $('dlFilteredBtn').onclick=function(){if(!containerOpen()){needContainerHint();return}bridge.send('/download-filtered',{})}; function filteredCount(){return (state.s_filtered||[]).length} @@ -2006,52 +2632,31 @@ $('archiveFilteredBtn').onclick=function(){if(!containerOpen()){needContainerHin $('deleteFilteredBtn').onclick=function(){if(!containerOpen()){needContainerHint();return}if(!filteredCount()){needMatchedHint();return}askConfirm({title:'Permanently delete matched blobs',msg:'Permanently delete '+cleanupSummary()+'? This cannot be undone.',okLabel:'Delete',danger:true}).then(function(ok){if(ok)bridge.send('/cleanup-filtered',{action:'delete'})})}; $('insightBtn').onclick=function(){if(!containerOpen()){needContainerHint();return}if(!state.s_stats){$('stats').innerHTML='
Click Filter first so there are statistics for Copilot to explain.
';return}bridge.send('/insights',{})}; $('sasBtn').onclick=function(){bridge.send('/sas-connect',{url:$('sasIn').value})}; -function xShowErr(msg){state.s_transferErr=msg;state.s_transfer=null;renderTransfer();var e=$('transfer');if(e){var box=e.querySelector('.xerr');(box||e).scrollIntoView({behavior:'smooth',block:'center'})}} +function xShowErr(msg,severity){state.s_transferErr=msg;state.s_transferSeverity=severity||'error';state.s_transfer=null;renderTransfer();var e=$('transfer');if(e){var box=e.querySelector('.xerr');(box||e).scrollIntoView({behavior:'smooth',block:'center'})}} $('xplanBtn').onclick=function(){ var isPick=xDestMode==='pick'; var dest; if(isPick){ - if(!xDestAcct||!xDestCont){xShowErr('Pick a destination account and container, or switch to '+String.fromCharCode(39)+'Paste path / SAS URL'+String.fromCharCode(39)+'.');return} + if(!xDestAcct||!xDestCont){xShowErr('Pick a destination account and container, or choose SAS URL.');return} dest=xDestAcct+'/'+xDestCont; }else{ dest=($('xdest').value||'').trim(); - var isSas=/[?&]sig=/.test(dest); - if(!isSas)dest=dest.split('/').map(function(s){return s.trim()}).filter(function(s){return s}).join('/'); + if(!/[?&]sig=/.test(dest)){xShowErr('Enter a destination container SAS URL.');$('xdest').focus();return} } var isSas2=/[?&]sig=/.test(dest); - if(!state.s_account){xShowErr('Open a storage account first. Pick one in the Explorer on the left.');return} - if(!xSourceCont){xShowErr('Choose a source container from the Source dropdown.');return} + var sourceSas=xSourceMode==='sas'?($('xsrcsas').value||'').trim():''; + if(xSourceMode==='sas'&&!sourceSas){xShowErr('Enter a source container SAS URL.');$('xsrcsas').focus();return} + if(xSourceMode==='pick'&&!state.s_account){xShowErr('Open a storage account first, or choose a source SAS URL.');return} + if(xSourceMode==='pick'&&!xSourceCont){xShowErr('Choose a source container from the Source dropdown.');return} if(!dest){xShowErr('Enter a destination: '+String.fromCharCode(39)+'account/container'+String.fromCharCode(39)+' (e.g. '+esc(state.s_account||'myaccount')+'/backups) or a container SAS URL.');if(!isPick)$('xdest').focus();return} if(!isPick&&!isSas2&&dest.replace('https://','').replace('http://','').split('/').filter(function(x){return x}).length<2){xShowErr('Destination must be '+String.fromCharCode(39)+'account/container'+String.fromCharCode(39)+', not just '+String.fromCharCode(39)+dest+String.fromCharCode(39)+'. Try '+esc(state.s_account||'myaccount')+'/'+dest+', or paste a container SAS URL.');$('xdest').focus();return} var res='overwrite';Array.prototype.forEach.call(document.getElementsByName('xres'),function(r){if(r.checked)res=r.value}); - state.s_transferErr=null;state.s_error=null;renderError();$('transfer').innerHTML='
Planning transfer\u2026
'; + state.s_transferErr=null;state.s_error=null;renderError();$('transfer').innerHTML='
'+uiIcon('busy',{size:13,className:'spin'})+' Planning transfer\u2026
'; $('transfer').scrollIntoView({behavior:'smooth',block:'center'}); - var p=bridge.send('/transfer-plan',{dest:dest,resolution:res,useFiltered:$('xfiltered').checked,source:xSourceCont}); - if(p&&p.then)p.then(function(r){if(r&&r.ok===false)xShowErr(r.error||'Transfer plan failed.')}); + var p=bridge.send('/transfer-plan',{dest:dest,resolution:res,useFiltered:$('xfiltered').checked,source:xSourceMode==='pick'?xSourceCont:undefined,sourceSas:sourceSas||undefined}); + if(p&&p.then)p.then(function(r){if(r&&r.ok===false)xShowErr(r.error||'Transfer plan failed.',r.severity)}); }; -$('askBtn').onclick=function(){doAsk($('askIn').value)}; -$('askIn').onkeydown=function(e){if(e.key==='Enter'){doAsk($('askIn').value)}}; -// Rotate inviting example questions through the Ask placeholder (idle only, so we -// never interrupt typing). Kept apostrophe- and dash-free on purpose. -(function(){ - var inp=$('askIn');if(!inp)return; - var phs=['What is taking up the most space?','Which containers are the largest?','Show me my biggest blobs','How much am I storing in total?','What can I clean up to save money?','Which account is growing the fastest?']; - var i=0; - setInterval(function(){ - if(document.activeElement===inp||inp.value)return; - i=(i+1)%phs.length; - inp.classList.add('ph-dim'); - setTimeout(function(){inp.setAttribute('placeholder',phs[i]);inp.classList.remove('ph-dim')},220); - },3600); -})(); -// Fold Filter & statistics and Transfer planning into the container hub tabs. -// Moving DOM nodes preserves their already-wired event handlers. -(function(){ - var sf=document.querySelector('[data-card="stats"]'); - var hf=$('hubFilter'),hs=$('hubShare'); - if(sf&&hf){var sb=sf.querySelector('.card-body');while(sb&&sb.firstChild)hf.appendChild(sb.firstChild);sf.style.display='none'} - if(hs){hs.innerHTML='
Create a secure, time-limited SAS link for this entire container. Anyone with the link gets the permissions you choose, until it expires. No Azure sign-in required. To share just one file, use the Browse tab.
';var hb=$('hubSasBtn');if(hb)hb.onclick=function(){openContainerSas()}} -})(); +$('hubSasBtn').onclick=function(){openContainerSas()}; render(); `,FEEDBACK_BASE="https://github.com/microsoft/azure-dev-tools/issues/new";function buildFeedbackUrl(info){let title="Azure Storage canvas feedback",body=`Product: Azure Storage Canvas: azure-storage @@ -2060,11 +2665,10 @@ Revision: `+info.revision+` ## Feedback -`;return(FEEDBACK_BASE+"?title="+encodeURIComponent(title)+"&body="+encodeURIComponent(body)).replace(/&/g,"&")}function escAttr(s){return String(s??"").replace(/[&<>"]/g,function(c){return{"&":"&","<":"<",">":">",'"':"""}[c]})}var FOOTER_HTML='
Azure Storage canvas v'+escAttr(BUILD_INFO.version)+' rev '+escAttr(BUILD_INFO.revision)+'
',BODY_WITH_FOOTER=BODY.replace("",FOOTER_HTML);function pageHtml(nonce){let n=nonce?' nonce="'+nonce+'"':"";return'Azure Storage"+BODY_WITH_FOOTER+'"}var DL_ROOT=path2.join(os2.homedir(),"Downloads","azure-storage"),PREFS_FILE=path2.join(os2.homedir(),".azure-storage","prefs.json");async function readPrefs(){try{let raw=await fs.readFile(PREFS_FILE,"utf8"),obj=JSON.parse(raw)||{};return{pinned:obj.pinned&&typeof obj.pinned=="object"?obj.pinned:{},hidden:obj.hidden&&typeof obj.hidden=="object"?obj.hidden:{}}}catch{return{pinned:{},hidden:{}}}}async function writePrefs({pinned,hidden}={}){let safePinned=pinned&&typeof pinned=="object"?pinned:{},safeHidden=hidden&&typeof hidden=="object"?hidden:{};return await fs.mkdir(path2.dirname(PREFS_FILE),{recursive:!0}),await fs.writeFile(PREFS_FILE,JSON.stringify({pinned:safePinned,hidden:safeHidden})),{pinned:safePinned,hidden:safeHidden}}async function saveDownload(container,name,buffer){let root=path2.resolve(DL_ROOT),full=path2.resolve(root,String(container||""),String(name||"").replace(/[\\/]+/g,path2.sep));if(full!==root&&!full.startsWith(root+path2.sep))throw new Error(`refusing to write outside the download folder: ${container}/${name}`);return await fs.mkdir(path2.dirname(full),{recursive:!0}),await fs.writeFile(full,buffer),full}async function openFolder(dir){let target=path2.resolve(dir||DL_ROOT),isDir=!1;try{isDir=(await fs.stat(target)).isDirectory()}catch{isDir=!1}if(!isDir){if(dir)return{ok:!1,target,error:"folder not found"};await fs.mkdir(target,{recursive:!0}).catch(()=>{}),isDir=!0}let attempts=process.platform==="win32"?[["explorer.exe",[target]],[process.env.ComSpec||"cmd.exe",["/c","start","",target]]]:process.platform==="darwin"?[["open",[target]]]:[["xdg-open",[target]]];for(let[cmd,args]of attempts)if(await new Promise(resolve=>{try{let child=spawn3(cmd,args,{detached:!0,stdio:"ignore"}),settled=!1;child.on("error",()=>{settled||(settled=!0,resolve(!1))}),child.unref(),setTimeout(()=>{settled||(settled=!0,resolve(!0))},200)}catch{resolve(!1)}}))return{ok:!0,target};return{ok:!1,target,error:"no file manager available"}}async function start({agent:agent2,port}={}){async function loadAccounts(_p,{setState,log}){setState({s_loading:!0,s_error:null});let r=await safe(()=>listStorageAccounts());return r.ok?(setState({s_loading:!1,s_accounts:r.data}),log(`Loaded ${r.data.length} storage accounts.`),{ok:!0,count:r.data.length}):(setState({s_loading:!1,s_error:r.error,s_needsAuth:r.needsAuth}),log(`Load accounts failed: ${r.error}`),{ok:!1,error:r.error})}async function loadSubscriptions(_p,{setState,log}){warmup(),setState({s_subsLoading:!0,s_error:null,s_seInstalled:storageExplorerInstalled()});let r=await safe(()=>listSubscriptions());if(!r.ok)return setState({s_subsLoading:!1,s_error:r.error,s_needsAuth:r.needsAuth}),log(`Load subscriptions failed: ${r.error}`),{ok:!1,error:r.error};let subs=r.data.slice().sort((a,b)=>(a.displayName||"").localeCompare(b.displayName||""));return setState({s_subsLoading:!1,s_subscriptions:subs}),log(`Found ${subs.length} subscriptions. Pick which to load.`),{ok:!0,count:subs.length}}async function selectSubscriptions({subscriptionIds},{getState,setState,log}){let ids=Array.isArray(subscriptionIds)?subscriptionIds:[],chosen=(getState().s_subscriptions||[]).filter(s=>ids.includes(s.subscriptionId));if(setState({s_selectedSubs:ids,s_loading:!0,s_error:null,s_accounts:[],s_account:null,s_container:null,s_tree:{},s_containers:[],s_blobs:[],s_stats:null,s_filtered:[]}),!chosen.length)return setState({s_loading:!1}),log("Scope cleared. No subscriptions selected."),{ok:!0,count:0};log(`Loading storage accounts for ${chosen.length} subscription(s)\u2026`);let r=await safe(()=>listStorageAccountsForSubs(chosen));if(!r.ok)return setState({s_loading:!1,s_error:r.error,s_needsAuth:r.needsAuth}),log(`Load accounts failed: ${r.error}`),{ok:!1,error:r.error};let{accounts,errors}=r.data,patch={s_loading:!1,s_accounts:accounts};return errors&&errors.length&&(patch.s_error=`${errors.length} subscription(s) could not be read (disabled or missing Reader): ${errors.slice(0,2).map(e=>e.subscriptionName).join(", ")}${errors.length>2?"\u2026":""}`),setState(patch),log(`Loaded ${accounts.length} accounts from ${chosen.length} subscription(s).`),{ok:!0,count:accounts.length}}function setTree({getState,setState},account,patch,loadingPatch){let tree={...getState().s_tree||{}},cur=tree[account]||{containers:null,shares:null,queues:null,tables:null,loading:{}};tree[account]={...cur,...patch||{},loading:{...cur.loading,...loadingPatch||{}}},setState({s_tree:tree})}async function openAccount({account},ctx){let{getState,setState,log}=ctx;if(!account)return{ok:!1,error:"account required"};if(setState({s_error:null,s_account:account,s_container:null,s_blobs:[],s_stats:null,s_filtered:[],s_criteria:null,s_insight:null,s_kind:null,s_item:null,s_files:[],s_messages:[],s_entities:null}),getState().s_tree&&getState().s_tree[account]&&getState().s_tree[account].containers!=null)return log(`Focused account ${account}.`),{ok:!0,cached:!0};setTree(ctx,account,{},{blob:!0}),log(`Opening account ${account}\u2026`);let r=await safe(()=>listContainers(account));return r.ok?(setTree(ctx,account,{containers:r.data},{blob:!1}),log(`${account}: ${r.data.length} containers.`),{ok:!0,containers:r.data.map(c=>c.name)}):(setTree(ctx,account,{},{blob:!1}),setState({s_error:r.error,s_needsAuth:r.needsAuth}),log(`Open ${account} failed: ${r.error}`),{ok:!1,error:r.error})}async function openContainer({container,account},{getState,setState,log}){if(account=account||getState().s_account,!account)return{ok:!1,error:"open an account first"};if(!container)return{ok:!1,error:"container required"};setState({s_loading:!0,s_error:null,s_account:account,s_container:container,s_kind:"blob",s_item:container,s_blobs:[],s_blobsMore:!1,s_stats:null,s_filtered:[],s_criteria:null,s_insight:null}),log(`Listing blobs in ${account}/${container}\u2026`);let r=await safe(()=>listBlobs(account,container));return r.ok?(setState({s_loading:!1,s_blobs:r.data,s_blobsMore:!!r.data.truncated}),log(`${container}: ${r.data.length} blobs${r.data.truncated?"+ (capped at "+r.data.limit+")":""}.`),{ok:!0,blobs:r.data.length}):(setState({s_loading:!1,s_error:r.error,s_needsAuth:r.needsAuth}),log(`List ${container} failed: ${r.error}`),{ok:!1,error:r.error})}async function listShares2({account}={},ctx){let{getState,log}=ctx;if(account=account||getState().s_account,!account)return{ok:!1,error:"open an account first"};setTree(ctx,account,{},{file:!0});let r=await safe(()=>listShares(account));return r.ok?(setTree(ctx,account,{shares:r.data},{file:!1}),log(`${account}: ${r.data.length} file shares.`),{ok:!0,count:r.data.length}):(setTree(ctx,account,{},{file:!1}),ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),log(`List shares failed: ${r.error}`),{ok:!1,error:r.error})}async function listQueues2({account}={},ctx){let{getState,log}=ctx;if(account=account||getState().s_account,!account)return{ok:!1,error:"open an account first"};setTree(ctx,account,{},{queue:!0});let r=await safe(()=>listQueues(account));return r.ok?(setTree(ctx,account,{queues:r.data},{queue:!1}),log(`${account}: ${r.data.length} queues.`),{ok:!0,count:r.data.length}):(setTree(ctx,account,{},{queue:!1}),ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),log(`List queues failed: ${r.error}`),{ok:!1,error:r.error})}async function listTables2({account}={},ctx){let{getState,log}=ctx;if(account=account||getState().s_account,!account)return{ok:!1,error:"open an account first"};setTree(ctx,account,{},{table:!0});let r=await safe(()=>listTables(account));return r.ok?(setTree(ctx,account,{tables:r.data},{table:!1}),log(`${account}: ${r.data.length} tables.`),{ok:!0,count:r.data.length}):(setTree(ctx,account,{},{table:!1}),ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),log(`List tables failed: ${r.error}`),{ok:!1,error:r.error})}async function listContainersTree({account}={},ctx){let{getState,log}=ctx;if(account=account||getState().s_account,!account)return{ok:!1,error:"account required"};if(getState().s_tree&&getState().s_tree[account]&&getState().s_tree[account].containers!=null)return{ok:!0,cached:!0};setTree(ctx,account,{},{blob:!0});let r=await safe(()=>listContainers(account));return r.ok?(setTree(ctx,account,{containers:r.data},{blob:!1}),log(`${account}: ${r.data.length} containers (tree).`),{ok:!0,count:r.data.length}):(setTree(ctx,account,{},{blob:!1}),ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),log(`List containers failed: ${r.error}`),{ok:!1,error:r.error})}async function openQueue({queue,account},{getState,setState,log}){if(account=account||getState().s_account,!account||!queue)return{ok:!1,error:"open an account first"};setState({s_loading:!0,s_error:null,s_account:account,s_kind:"queue",s_item:queue,s_messages:[]}),log(`Peeking messages in ${account} \xB7 queue ${queue}\u2026`);let r=await safe(()=>peekQueueMessages(account,queue));return r.ok?(setState({s_loading:!1,s_messages:r.data}),log(`${queue}: peeked ${r.data.length} message(s).`),{ok:!0,messages:r.data.length}):(setState({s_loading:!1,s_error:r.error,s_needsAuth:r.needsAuth}),log(`Peek ${queue} failed: ${r.error}`),{ok:!1,error:r.error})}async function openTable({table,account},{getState,setState,log}){if(account=account||getState().s_account,!account||!table)return{ok:!1,error:"open an account first"};setState({s_loading:!0,s_error:null,s_account:account,s_kind:"table",s_item:table,s_entities:null}),log(`Querying entities in ${account} \xB7 table ${table}\u2026`);let r=await safe(()=>queryTableEntities(account,table));return r.ok?(setState({s_loading:!1,s_entities:r.data}),log(`${table}: ${r.data.rows.length} entities.`),{ok:!0,entities:r.data.rows.length}):(setState({s_loading:!1,s_error:r.error,s_needsAuth:r.needsAuth}),log(`Query ${table} failed: ${r.error}`),{ok:!1,error:r.error})}async function openShare({share,account},{getState,setState,log}){if(account=account||getState().s_account,!account||!share)return{ok:!1,error:"open an account first"};setState({s_loading:!0,s_error:null,s_account:account,s_kind:"file",s_item:share,s_files:[]}),log(`Listing files in ${account} \xB7 share ${share}\u2026`);let r=await safe(()=>listShareItems(account,share,""));return r.ok?(setState({s_loading:!1,s_files:r.data}),log(`${share}: ${r.data.length} item(s).`),{ok:!0,items:r.data.length}):(setState({s_loading:!1,s_error:r.error,s_needsAuth:r.needsAuth}),log(`List ${share} failed: ${r.error}`),{ok:!1,error:r.error})}async function refreshCategory(account,kind,ctx){if(kind==="blob"){let r=await safe(()=>listContainers(account));r.ok&&setTree(ctx,account,{containers:r.data})}else if(kind==="queue"){let r=await safe(()=>listQueues(account));r.ok&&setTree(ctx,account,{queues:r.data})}else if(kind==="table"){let r=await safe(()=>listTables(account));r.ok&&setTree(ctx,account,{tables:r.data})}else if(kind==="file"){let r=await safe(()=>listShares(account));r.ok&&setTree(ctx,account,{shares:r.data})}}function mutate(fn,{refresh,reopen}={}){return async(payload,ctx)=>{let account=payload&&payload.account||ctx.getState().s_account;if(!account)return{ok:!1,error:"open an account first"};let r=await safe(()=>fn(account,payload,ctx));return r.ok?(refresh&&await refreshCategory(account,refresh,ctx),reopen&&await reopen(account,payload,ctx),r.data??{ok:!0}):(ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),ctx.log(`Operation failed: ${r.error}`),{ok:!1,error:r.error})}}let reopenQueue=(account,{queue},ctx)=>openQueue({queue},ctx),reopenTable=(account,{table},ctx)=>openTable({table},ctx),reopenShare=(account,{share},ctx)=>openShare({share},ctx);async function runBatchDownload(names,{getState,setState,log}){let st=getState(),account=st.s_account,container=st.s_container;if(!account||!container)return{ok:!1,error:"open a container first"};let list=(names||[]).filter(Boolean);if(!list.length)return{ok:!1,error:"no blobs given"};let dir=path2.join(DL_ROOT,container),done=0,okc=0,failed=[];setState({s_batch:{op:"download",total:list.length,done,ok:okc,failed:[],running:!0,dir}}),log(`Downloading ${list.length} blob(s) from ${container}\u2026`);for(let name of list){let r=await safe(()=>downloadBlob(account,container,name));if(done++,r.ok)try{await saveDownload(container,name,r.data),okc++}catch(e){failed.push({name,error:"could not save to disk: "+(e&&e.message?e.message:String(e))})}else failed.push({name,error:r.error});setState({s_batch:{op:"download",total:list.length,done,ok:okc,failed:[...failed],running:donei&&i.name&&i.contentBase64);if(!list.length)return{ok:!1,error:"no items given"};let done=0,okc=0,failed=[];setState({s_batch:{op:"upload",total:list.length,done,ok:okc,failed:[],running:!0}}),log(`Uploading ${list.length} file(s) to ${container}\u2026`);for(let it of list){let buf=Buffer.from(it.contentBase64,"base64"),r=await safe(()=>uploadBlob(account,container,it.name,buf,it.contentType));done++,r.ok?okc++:failed.push({name:it.name,error:r.error}),setState({s_batch:{op:"upload",total:list.length,done,ok:okc,failed:[...failed],running:donelistBlobs(account,container));return rl.ok&&setState({s_blobs:rl.data}),log(`Upload complete: ${okc} ok, ${failed.length} failed.`),{ok:!0,uploaded:okc,failed}}async function runBatchCleanup(action,{getState,setState,log}){let st=getState(),account=st.s_account,container=st.s_container;if(!account||!container)return{ok:!1,error:"open a container first"};let list=(st.s_filtered||[]).filter(Boolean);if(!list.length)return{ok:!1,error:"filter some blobs first, nothing matched"};let op=action==="delete"?"delete":"archive",done=0,okc=0,failed=[];setState({s_batch:{op,total:list.length,done,ok:okc,failed:[],running:!0}}),log(`${op==="delete"?"Deleting":"Archiving"} ${list.length} matched blob(s) in ${container}\u2026`);for(let name of list){let r=op==="delete"?await safe(()=>deleteBlob(account,container,name)):await safe(()=>setBlobTier(account,container,name,"Archive"));done++,r.ok?okc++:failed.push({name,error:r.error}),setState({s_batch:{op,total:list.length,done,ok:okc,failed:[...failed],running:donelistBlobs(account,container));return rl.ok&&setState({s_blobs:rl.data}),log(`${op==="delete"?"Delete":"Archive"} complete: ${okc} ok, ${failed.length} failed.`),{ok:!0,op,cleaned:okc,failed}}let routes={"/load-accounts":loadAccounts,"/load-subscriptions":loadSubscriptions,"/select-subscriptions":selectSubscriptions,"/account":openAccount,"/container":openContainer,"/list-shares":listShares2,"/list-queues":listQueues2,"/list-tables":listTables2,"/tree-containers":listContainersTree,"/open-queue":openQueue,"/open-table":openTable,"/open-share":openShare,"/download-many":({blobs},ctx)=>runBatchDownload(blobs,ctx),"/open-folder":async({dir},_ctx)=>await openFolder(dir),"/dismiss-batch":(_p,{setState})=>(setState({s_batch:null}),{ok:!0}),"/get-prefs":async()=>({ok:!0,prefs:await readPrefs()}),"/save-prefs":async({pinned,hidden}={})=>({ok:!0,prefs:await writePrefs({pinned,hidden})}),"/upload-many":({items},ctx)=>runBatchUpload(items,ctx),"/container-create":mutate((a,{container})=>createContainer(a,container),{refresh:"blob"}),"/container-delete":async({container,account},ctx)=>{let a=account||ctx.getState().s_account,r=await safe(()=>deleteContainer(a,container));return r.ok?(await refreshCategory(a,"blob",ctx),ctx.getState().s_kind==="blob"&&ctx.getState().s_item===container&&ctx.setState({s_kind:null,s_item:null,s_container:null,s_blobs:[]}),ctx.log(`Deleted container ${container}.`),{ok:!0}):(ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),{ok:!1,error:r.error})},"/blob-delete":async({container,name,account},ctx)=>{let a=account||ctx.getState().s_account,r=await safe(()=>deleteBlob(a,container,name));if(!r.ok)return ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),{ok:!1,error:r.error};let rl=await safe(()=>listBlobs(a,container));return rl.ok&&ctx.setState({s_blobs:rl.data}),ctx.log(`Deleted blob ${name}.`),{ok:!0}},"/generate-sas":async({container,name,account,minutes,permissions},{getState,log})=>{let a=account||getState().s_account,c=container||getState().s_container;if(!c||!name)return{ok:!1,error:"no blob selected"};let r=await safe(()=>generateBlobSas(a,c,name,{minutes:Number(minutes)||60,permissions:permissions||"r"}));return r.ok?(log(`Generated ${r.data.mode} SAS for ${name} (${r.data.permissions}, expires ${r.data.expiresOn}).`),{ok:!0,name,...r.data}):{ok:!1,error:r.error,needsAuth:r.needsAuth}},"/generate-container-sas":async({container,account,minutes,permissions},{getState,log})=>{let a=account||getState().s_account,c=container||getState().s_container;if(!c)return{ok:!1,error:"no container selected"};let r=await safe(()=>generateContainerSas(a,c,{minutes:Number(minutes)||1440,permissions:permissions||"rl"}));return r.ok?(log(`Generated ${r.data.mode} container SAS for ${c} (${r.data.permissions}, expires ${r.data.expiresOn}).`),{ok:!0,container:c,...r.data}):{ok:!1,error:r.error,needsAuth:r.needsAuth}},"/queue-create":mutate((a,{queue})=>createQueue(a,queue),{refresh:"queue"}),"/queue-delete":async({queue,account},ctx)=>{let a=account||ctx.getState().s_account,r=await safe(()=>deleteQueue(a,queue));return r.ok?(await refreshCategory(a,"queue",ctx),ctx.getState().s_kind==="queue"&&ctx.getState().s_item===queue&&ctx.setState({s_kind:null,s_item:null,s_messages:[]}),ctx.log(`Deleted queue ${queue}.`),{ok:!0}):(ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),{ok:!1,error:r.error})},"/queue-send":mutate((a,{queue,text})=>sendQueueMessage(a,queue,text),{reopen:reopenQueue}),"/queue-clear":mutate((a,{queue})=>clearQueueMessages(a,queue),{reopen:reopenQueue}),"/queue-msg-update":mutate((a,{queue,messageId,text})=>updateQueueMessage(a,queue,messageId,text),{reopen:reopenQueue}),"/queue-msg-delete":mutate((a,{queue,messageId})=>deleteQueueMessage(a,queue,messageId),{reopen:reopenQueue}),"/table-create":mutate((a,{table})=>createTable(a,table),{refresh:"table"}),"/table-delete":async({table,account},ctx)=>{let a=account||ctx.getState().s_account,r=await safe(()=>deleteTable(a,table));return r.ok?(await refreshCategory(a,"table",ctx),ctx.getState().s_kind==="table"&&ctx.getState().s_item===table&&ctx.setState({s_kind:null,s_item:null,s_entities:null}),ctx.log(`Deleted table ${table}.`),{ok:!0}):(ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),{ok:!1,error:r.error})},"/entity-upsert":mutate((a,{table,entity})=>upsertEntity(a,table,entity),{reopen:reopenTable}),"/entity-delete":mutate((a,{table,partitionKey,rowKey})=>deleteEntity(a,table,partitionKey,rowKey),{reopen:reopenTable}),"/share-create":mutate((a,{share})=>createShare(a,share),{refresh:"file"}),"/share-delete":async({share,account},ctx)=>{let a=account||ctx.getState().s_account,r=await safe(()=>deleteShare(a,share));return r.ok?(await refreshCategory(a,"file",ctx),ctx.getState().s_kind==="file"&&ctx.getState().s_item===share&&ctx.setState({s_kind:null,s_item:null,s_files:[]}),ctx.log(`Deleted file share ${share}.`),{ok:!0}):(ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),{ok:!1,error:r.error})},"/share-mkdir":mutate((a,{share,dir})=>createShareDirectory(a,share,dir),{reopen:reopenShare}),"/share-delete-item":mutate((a,{share,name,kind})=>deleteShareItem(a,share,name,kind),{reopen:reopenShare}),"/share-upload":mutate((a,{share,name,contentBase64})=>uploadShareFile(a,share,name,Buffer.from(contentBase64||"","base64")),{reopen:reopenShare}),"/share-download":async({share,name},ctx)=>{let a=ctx.getState().s_account,r=await safe(()=>downloadShareFile(a,share,name));if(!r.ok)return ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),{ok:!1,error:r.error};let dir=path2.join(DL_ROOT,share);try{await saveDownload(share,name,r.data)}catch(e){let msg="could not save to disk: "+(e&&e.message?e.message:String(e));return ctx.setState({s_error:msg}),{ok:!1,error:msg}}return ctx.setState({s_batch:{op:"download",total:1,done:1,ok:1,failed:[],running:!1,dir}}),ctx.log(`Downloaded ${name} from share ${share} to ${dir}.`),{ok:!0,dir}},"/diagnose":(_p,_ctx)=>({ok:!1,error:"diagnosis removed"}),"/filter":async(criteria,{getState,setState,log})=>{let st=getState();if(!(st.s_kind==="blob"&&st.s_container))return{ok:!1,error:"Open a container first to filter its blobs."};let blobs=st.s_blobs||[],{filtered,stats}=filterAndStat(blobs,criteria||{});return setState({s_criteria:criteria||{},s_filtered:filtered.map(b=>b.name),s_stats:stats}),log(`Filter matched ${filtered.length} of ${blobs.length} blobs.`),{ok:!0,matched:filtered.length,stats}},"/download-filtered":async(_p,ctx)=>{let names=ctx.getState().s_filtered||[];return runBatchDownload(names,ctx)},"/cleanup-filtered":({action},ctx)=>runBatchCleanup(action,ctx),"/transfer-plan":async({dest,resolution,useFiltered,source:srcContainer},{getState,setState,log})=>{let st=getState(),account=st.s_account,container=String(srcContainer||st.s_container||"").trim(),fail=msg=>(setState({s_transferErr:msg,s_transfer:null}),log(`Transfer plan failed: ${msg}`),{ok:!1,error:msg});if(!account)return fail("Open a storage account first. Pick one in the Explorer.");if(!container)return fail("Choose a source container.");let isOpen=st.s_kind==="blob"&&st.s_item===container,allBlobs;if(isOpen)allBlobs=st.s_blobs||[];else{let rs=await safe(()=>listBlobs(account,container));if(!rs.ok)return fail(`Couldn't read source container '${container}': ${rs.error}`);allBlobs=rs.data}let canFilter=isOpen&&(st.s_filtered||[]).length,names=useFiltered&&canFilter?new Set(st.s_filtered):null,source=names?allBlobs.filter(b=>names.has(b.name)):allBlobs,destStr=String(dest||"").trim();if(!destStr)return fail("Enter a destination: 'account/container' (e.g. myaccount/backups) or a container SAS URL.");if(!/[?&]sig=/.test(destStr)&&destStr.replace(/^https?:\/\//,"").split("/").filter(Boolean).length<2)return fail(`Destination must be 'account/container', not just '${destStr}'. Example: ${account}/${destStr||"backups"}, or paste a container SAS URL.`);if(`${account}/${container}`===destStr.replace(/^https?:\/\//,"").replace(".blob.core.windows.net",""))return fail("Source and destination are the same container. Pick a different destination.");let res=["overwrite","skip","if-newer"].includes(resolution)?resolution:"overwrite",destBlobs=[],destUrl="",destSas=!1,r=await safe(async()=>{if(/[?&]sig=/.test(destStr)){let p=parseSasUrl(destStr);if(!p.ok)throw new Error(p.error);return destUrl=p.containerUrl,destSas=!0,await listSasBlobs2(destStr).then(x=>x.blobs)}let m=destStr.replace(/^https?:\/\//,"").split("/"),dAcct=m[0].split(".")[0],dCont=m[1];if(!dAcct||!dCont)throw new Error("Destination must be 'account/container' or a container SAS URL.");return destUrl=`https://${dAcct}.blob.core.windows.net/${dCont}`,await listBlobs(dAcct,dCont)});if(!r.ok)return fail(`Couldn't read the destination: ${r.error}`);destBlobs=r.data;let plan=planTransfer(source,destBlobs,res),sourceUrl=`https://${account}.blob.core.windows.net/${container}`,allSource=!names,{command,needsLogin,empty}=azcopyCommand({sourceUrl,destUrl,resolution:res,includeNames:names?plan.toTransfer:null,allSource});if(empty)return fail("Nothing to transfer \u2014 every selected blob already exists at the destination under the current conflict rule.");let azcopy=await azcopyDoctor();return setState({s_transferErr:null,s_transfer:{plan,command,needsLogin,azcopy,sourceLabel:`${account}/${container}`,destLabel:destSas?`${plan.destCount} blobs (SAS)`:destStr,source:allSource?"all blobs":`${source.length} filtered blobs`}}),log(`Transfer plan: ${plan.transferCount} to transfer, ${plan.conflictCount} conflicts (${res}).${azcopy.installed?` AzCopy ${azcopy.version||"detected"}.`:" AzCopy not found \u2014 install it to run the command."}`),{ok:!0,plan,command,azcopy}},"/azcopy-doctor":async(_p,_ctx)=>({ok:!0,azcopy:await azcopyDoctor({refresh:!0})}),"/sas-connect":async({url},{setState,log})=>{setState({s_loading:!0,s_error:null});let parsed=parseSasUrl(url);if(!parsed.ok)return setState({s_loading:!1,s_error:parsed.error}),{ok:!1,error:parsed.error};log(`Connecting to SAS: ${describeSas(parsed)}`);let r=await safe(()=>listSasBlobs2(url));if(!r.ok)return setState({s_loading:!1,s_error:r.error}),log(`SAS connect failed: ${r.error}`),{ok:!1,error:r.error};let{stats}=filterAndStat(r.data.blobs,{}),withUrls=parsed.canRead?r.data.blobs.map(b=>({...b,url:blobSasUrl(url,b.name)})):r.data.blobs;return setState({s_loading:!1,s_sasUrl:url,s_sasInfo:{account:r.data.account,container:r.data.container,permissions:r.data.permissions,expiresOn:r.data.expiresOn,canRead:parsed.canRead,canList:parsed.canList,canWrite:parsed.canWrite,canDelete:parsed.canDelete,expired:parsed.expired,resource:parsed.resource},s_sasBlobs:withUrls,s_sasStats:stats}),log(`SAS connected: ${r.data.blobs.length} blobs in ${r.data.container}.`),{ok:!0,blobs:r.data.blobs.length}},"/sas-download":async({name},{getState,setState,log})=>{let url=getState().s_sasUrl;if(!url)return{ok:!1,error:"connect a SAS first"};let r=await safe(()=>downloadSasBlob2(url,name));if(!r.ok)return setState({s_error:r.error}),{ok:!1,error:r.error};let container=getState().s_sasInfo?.container||"sas",savedPath;try{savedPath=await saveDownload(container,name,Buffer.from(r.data.contentBase64,"base64"))}catch(e){let msg="could not save to disk: "+(e&&e.message?e.message:String(e));return setState({s_error:msg}),log(`SAS download failed: ${msg}`),{ok:!1,name,error:msg}}return log(`Downloaded '${name}' (${r.data.size} bytes) from SAS.`),{ok:!0,name,size:r.data.size,path:savedPath,dir:path2.dirname(savedPath)}},"/ask":async({question},{setState,log})=>(setState({s_ask:{question,pending:!0,answer:null,visual:null}}),log(`Asked Copilot: "${question}"`),await agent2?.askCanvas?.({question}),{ok:!0}),"/read-state":(_p,{getState})=>{let st=getState();return{ok:!0,state:{account:st.s_account,container:st.s_container,subscriptions:(st.s_subscriptions||[]).length,selectedSubs:st.s_selectedSubs||[],accounts:(st.s_accounts||[]).map(a=>a.name),tree:st.s_tree||{},containers:Object.values(st.s_tree||{}).flatMap(t=>(t.containers||[]).map(c=>c.name)),blobs:st.s_blobs||[],blobsTruncated:!!(st.s_blobs&&st.s_blobs.truncated),stats:st.s_stats||null,filtered:st.s_filtered||[],batch:st.s_batch||null,sasInfo:st.s_sasInfo||null,sasBlobs:(st.s_sasBlobs||[]).map(b=>({name:b.name,size:b.size,lastModified:b.lastModified||null,contentType:b.contentType||null})),transfer:st.s_transfer?(()=>{let{command,sourceUrl,destUrl,...rest}=st.s_transfer;return rest})():null,insight:st.s_insight||null,preview:st.s_preview?{name:st.s_preview.name,isText:st.s_preview.isText,size:st.s_preview.size,truncated:st.s_preview.truncated,lang:st.s_preview.lang,format:st.s_preview.format||null,category:st.s_preview.category||null,magicHex:st.s_preview.magicHex||null,glb:st.s_preview.glb||null,isImage:!!st.s_preview.image,explaining:!!st.s_preview.explaining,explained:!!st.s_preview.explanation,text:st.s_preview.text||null}:null,s_queues:(st.s_queues||[]).map(q=>({name:q.name})),s_tables:(st.s_tables||[]).map(t=>({name:t.name})),s_shares:(st.s_shares||[]).map(x=>({name:x.name})),s_messages:(st.s_messages||[]).map(m=>({messageText:m.messageText})),s_entities:st.s_entities||null,s_files:(st.s_files||[]).map(f=>({name:f.name,kind:f.kind})),s_kind:st.s_kind||null,s_item:st.s_item||null}}},"/canvas-answer":({answer,visual,target,suggestedFilter},{getState,setState})=>{let payload={pending:!1,answer:String(answer||""),visual:visual||null},st=getState(),t=target==="insights"||target==="preview"||target==="ask"?target:null;if(st.s_preview&&st.s_preview.explaining?t="preview":st.s_insight&&st.s_insight.pending?t="insights":t||(t="ask"),t==="insights")setState({s_insight:{...payload,suggestedFilter:suggestedFilter||null}});else if(t==="preview"){let p=st.s_preview||{};setState({s_preview:{...p,explaining:!1,explanation:payload}})}else setState({s_ask:payload});return{ok:!0}},"/insights":async(_p,{getState,setState,log})=>{let st=getState();return st.s_stats?(setState({s_insight:{pending:!0,answer:null,visual:null,suggestedFilter:null}}),log("Asked Copilot to explain the statistics."),await agent2?.requestInsight?.({account:st.s_account,container:st.s_container}),{ok:!0}):{ok:!1,error:"Filter or open a container first so there are statistics to explain."}},"/preview-blob":async({name},{getState,setState,log})=>{let st=getState(),account=st.s_account,container=st.s_container;if(!account||!container||!name)return{ok:!1,error:"Open a container and pick a blob to preview."};setState({s_preview:{name,pending:!0}});let MAX=32*1024,r=await safe(()=>downloadBlob(account,container,name,{maxBytes:MAX}));if(!r.ok)return setState({s_preview:{name,pending:!1,error:r.error}}),log(`Preview failed: ${r.error}`),{ok:!1,error:r.error};let slice=r.data.subarray(0,MAX),{isText,text}=sniffText(slice),bin=isText?null:sniffBinaryFormat(slice,name),mime=isText?null:imageMime(name),sz=await safe(()=>blobSize(account,container,name)),total=sz.ok?sz.data:slice.length,RENDER_MAX=25*1024*1024,GLB_MAX=50*1024*1024,image=mime&&total<=RENDER_MAX?{mime}:null,glb=null;if(bin&&/GLB/.test(bin.format)&&total<=GLB_MAX){let full=await safe(()=>downloadBlob(account,container,name));full.ok&&(glb=parseGlb(full.data))}return setState({s_preview:{name,pending:!1,size:total,truncated:total>MAX,isText,lang:previewLang(name),text:isText?text:null,format:bin?bin.format:null,category:bin?bin.category:null,magicHex:bin?bin.magicHex:null,image,glb,explanation:null}}),log(`Previewed ${name}: ${isText?"text":bin.format}, ${total} bytes.`),{ok:!0,isText,bytes:total}},"/explain-blob":async(_p,{getState,setState})=>{let p=getState().s_preview;return!p||p.pending||p.error?{ok:!1,error:"Preview a blob first."}:!p.isText&&!p.format?{ok:!1,error:"Preview a blob first."}:(setState({s_preview:{...p,explaining:!0,explanation:null}}),await agent2?.requestPreviewExplain?.({name:p.name,isText:!!p.isText,format:p.format||null,category:p.category||null,size:p.size,glb:p.glb||null}),{ok:!0})},"/dismiss-preview":(_p,{setState})=>(setState({s_preview:null}),{ok:!0}),"/dismiss-insight":(_p,{setState})=>(setState({s_insight:null}),{ok:!0}),"/open-storage-explorer":async(_p,{getState,log})=>{let st=getState(),account=st.s_account||null,kind=st.s_kind||null,item=st.s_item||null,where=item?`${account}/${item}`:account||"the app";log(`Opening Azure Storage Explorer \u2192 ${where}\u2026`);let r=await openInStorageExplorer({account,kind,item});return r.ok?(log(r.deepLink?`Opened Storage Explorer at ${where}.`:"Opened Storage Explorer."),{ok:!0,deepLink:r.deepLink}):r.notInstalled?(log("Azure Storage Explorer isn't installed. Opening the download page so you can get the free desktop app."),{ok:!1,notInstalled:!0,install:r.install}):(log(`Storage Explorer: ${r.error}`),{ok:!1,error:r.error})},"/dismiss-error":(_p,{setState})=>(setState({s_error:null,s_needsAuth:!1}),{ok:!0}),"/sign-in":async(_p,ctx)=>{let{setState,log}=ctx;setState({s_signingIn:!0,s_error:null}),log("Opening az login in your browser\u2026");let r=await signIn();return r.ok?(setState({s_signingIn:!1,s_needsAuth:!1,s_error:null}),log("Signed in successfully. Loading subscriptions\u2026"),await loadSubscriptions({},ctx),{ok:!0}):(setState({s_signingIn:!1,s_error:r.error,s_needsAuth:!0}),log(`Sign-in failed: ${r.error}`),{ok:!1,error:r.error})}},initialState={s_subscriptions:[],s_selectedSubs:[],s_subsLoading:!1,s_accounts:[],s_tree:{},s_containers:[],s_blobs:[],s_filtered:[],s_shares:[],s_queues:[],s_tables:[],s_files:[],s_messages:[],s_entities:null,s_kind:null,s_item:null,s_svcLoading:null,s_account:null,s_container:null,s_stats:null,s_batch:null,s_sasInfo:null,s_sasBlobs:[],s_ask:null,s_transfer:null,s_insight:null,s_preview:null,s_transferErr:null,s_loading:!1,s_error:null,activityLog:[],s_signingIn:!1,s_seInstalled:null};async function mediaHandler(pathname,query,{getState}){if(pathname!=="/blob-media")return null;let name=query.get("name"),st=getState(),p=st.s_preview;if(!p||p.name!==name||!p.image)return null;let account=st.s_account,container=st.s_container;if(!account||!container)return null;let r=await safe(()=>downloadBlob(account,container,name));return r.ok?{contentType:p.image.mime||imageMime(name)||"application/octet-stream",buffer:r.data}:null}let server2=await createCanvasServer({html:pageHtml,routes,media:mediaHandler,initialState,port});return server2.loadAccounts=()=>loadAccounts({},{getState:server2.getState,setState:server2.setState,log:server2.log}),server2.loadSubscriptions=()=>loadSubscriptions({},{getState:server2.getState,setState:server2.setState,log:server2.log}),server2}var session;async function post(server2,route,body){let res=await fetch(`http://127.0.0.1:${server2.port}${route}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body||{})});try{return await res.json()}catch{return{ok:res.ok}}}var agent={async askCanvas({question}){session?.send({prompt:`On the "Azure Storage" canvas, the user asked: "${question}" -Call read_canvas_state to pull the live blob list / stats / account context, then answer via answer_on_canvas citing real numbers. If the answer ranks or compares numbers, also pass a visual ({kind:'bar'|'stat'|'table', ...}) so the user SEES it.`,displayPrompt:`\u{1F4AC} Storage canvas \u2014 asked: "${String(question).slice(0,80)}"`})},async requestInsight({container}={}){session?.send({prompt:`On the "Azure Storage" canvas, the user clicked "Explain these statistics"${container?` for container "${container}"`:""}. +`;return(FEEDBACK_BASE+"?title="+encodeURIComponent(title)+"&body="+encodeURIComponent(body)).replace(/&/g,"&")}function escAttr(s){return String(s??"").replace(/[&<>"]/g,function(c){return{"&":"&","<":"<",">":">",'"':"""}[c]})}var BUILD_STAMP_HTML='v'+escAttr(BUILD_INFO.version)+"+"+escAttr(BUILD_INFO.revision)+"",FEEDBACK_HTML='',BODY_WITH_IDENTITY=BODY.replace("","").replace("",BUILD_STAMP_HTML).replace("",FEEDBACK_HTML);function pageHtml(nonce){let n=nonce?' nonce="'+nonce+'"':"";return'Azure Storage"+BODY_WITH_IDENTITY+'"}var DL_ROOT=path2.join(os2.homedir(),"Downloads","azure-storage");function redactEndpointError(message,endpoints){let msg=String(message).replace(/([?&]sig=)[^&\s"'<>]*/gi,"$1[redacted]");for(let raw of endpoints)try{let url=new URL(raw);msg=msg.replaceAll(raw,`${url.origin}${url.pathname}?[redacted]`);let signature=url.searchParams.get("sig");if(signature)for(let value of[signature,encodeURIComponent(signature)])msg=msg.replaceAll(value,"[redacted]")}catch{}return msg}var PREFS_FILE=path2.join(os2.homedir(),".azure-storage","prefs.json");async function readPrefs(){try{let raw=await fs.readFile(PREFS_FILE,"utf8"),obj=JSON.parse(raw)||{};return{pinned:obj.pinned&&typeof obj.pinned=="object"?obj.pinned:{},hidden:obj.hidden&&typeof obj.hidden=="object"?obj.hidden:{},hiddenTenants:obj.hiddenTenants||{},hiddenSubscriptions:obj.hiddenSubscriptions||{}}}catch{return{pinned:{},hidden:{}}}}async function writePrefs({pinned,hidden,hiddenTenants,hiddenSubscriptions}={}){let safePinned=pinned&&typeof pinned=="object"?pinned:{},safeHidden=hidden&&typeof hidden=="object"?hidden:{};await fs.mkdir(path2.dirname(PREFS_FILE),{recursive:!0});let previous=await readPrefs(),prefs={pinned:safePinned,hidden:safeHidden,hiddenTenants:hiddenTenants||previous.hiddenTenants||{},hiddenSubscriptions:hiddenSubscriptions||previous.hiddenSubscriptions||{}};return await fs.writeFile(PREFS_FILE,JSON.stringify(prefs)),prefs}async function saveDownload(container,name,buffer){let root=path2.resolve(DL_ROOT),full=path2.resolve(root,String(container||""),String(name||"").replace(/[\\/]+/g,path2.sep));if(full!==root&&!full.startsWith(root+path2.sep))throw new Error(`refusing to write outside the download folder: ${container}/${name}`);return await fs.mkdir(path2.dirname(full),{recursive:!0}),await fs.writeFile(full,buffer),full}async function openFolder(dir){let target=path2.resolve(dir||DL_ROOT),isDir=!1;try{isDir=(await fs.stat(target)).isDirectory()}catch{isDir=!1}if(!isDir){if(dir)return{ok:!1,target,error:"folder not found"};await fs.mkdir(target,{recursive:!0}).catch(()=>{}),isDir=!0}let attempts=process.platform==="win32"?[["explorer.exe",[target]],[process.env.ComSpec||"cmd.exe",["/c","start","",target]]]:process.platform==="darwin"?[["open",[target]]]:[["xdg-open",[target]]];for(let[cmd,args]of attempts)if(await new Promise(resolve=>{try{let child=spawn3(cmd,args,{detached:!0,stdio:"ignore"}),settled=!1;child.on("error",()=>{settled||(settled=!0,resolve(!1))}),child.unref(),setTimeout(()=>{settled||(settled=!0,resolve(!0))},200)}catch{resolve(!1)}}))return{ok:!0,target};return{ok:!1,target,error:"no file manager available"}}async function start({agent:agent2,port,az=azure_exports,listTransferSas=listSasBlobs2,transferDoctor=azcopyDoctor}={}){let subscriptionGeneration=0,navigationGeneration=0,discoveredSubscriptions=new Map,pageKey=t=>JSON.stringify([t.account||"",t.kind,t.resource||"",t.dir||t.prefix||""]),pageRequests=new Map,pageResults=new Map,serviceViews=new Map,viewFields={blob:["s_container","s_blobPrefix","s_blobPrefixes","s_blobs","s_blobsMore","s_stats","s_filtered","s_criteria","s_insight"],file:["s_fileDir","s_files"],queue:["s_messages"],table:["s_entities"]};function saveServiceView(st){st.s_account&&viewFields[st.s_kind]&&serviceViews.set(JSON.stringify([st.s_account,st.s_kind]),{...Object.fromEntries(["s_kind","s_item",...viewFields[st.s_kind]].map(key=>[key,st[key]])),incomplete:!!st.s_loading})}async function loadStoragePage(target,ctx,direction){let{getState,setState,log}=ctx,key=pageKey(target),old=getState().s_pages?.[key],cachedPages=direction?(pageResults.get(key)||[]).slice():[],index=direction==="last"?Math.max(0,cachedPages.length-1):direction==="next"?(old?.index||0)+1:direction==="previous"?(old?.index||0)-1:0;if(direction&&(!old||index<0||direction==="next"&&!old.nextToken&&!cachedPages[index]))return{ok:!1,error:"That page is not available. Refresh the current view."};let tokens=direction?old.tokens.slice():[null];direction==="next"&&!cachedPages[index]&&(tokens[index]=old.nextToken);let generation=target.resource&&target.kind!=="sas"?++navigationGeneration:null,request=(pageRequests.get(key)||0)+1;pageRequests.set(key,request);let sasUrl=target.kind==="sas"?getState().s_sasUrl:null;setState({s_pageLoading:key,s_error:null});let result=cachedPages[index]?{ok:!0,data:cachedPages[index]}:await safe(()=>az.listStoragePage({...target,token:tokens[index],...sasUrl?{sasUrl}:{}}));if(pageRequests.get(key)!==request||sasUrl&&sasUrl!==getState().s_sasUrl||generation!=null&&(generation!==navigationGeneration||target.account!==getState().s_account||target.kind!==getState().s_kind||target.resource!==getState().s_item))return{ok:!1,error:"Resource navigation changed; older page discarded."};if(!result.ok){let message=redactEndpointError(result.error,[sasUrl||""]);return setState({s_pageLoading:null,s_pageProgress:null,...target.resource?{s_loading:!1}:{},s_error:message,s_needsAuth:result.needsAuth}),log(`Read ${target.kind} page failed: ${message}`),{ok:!1,error:message}}let data=result.data;for(let scope of pageResults.keys()){let[account,kind,resource]=JSON.parse(scope);scope!==key&&!direction&&account===target.account&&kind===target.kind&&!!resource==!!target.resource&&pageResults.delete(scope)}cachedPages[index]=data,pageResults.set(key,cachedPages);let columns;target.kind==="table"&&target.resource&&(columns=[...new Set(cachedPages.flatMap(page=>page?.entities?.columns||[]))]);let patch={s_pages:{...getState().s_pages,[key]:{index,tokens,nextToken:data.nextToken||null,cachedPageCount:cachedPages.length,cachedItemCount:cachedPages.reduce((count,page)=>count+(page?.items?.length||page?.entities?.rows.length||0),0)}},s_pageProgress:null,s_pageLoading:getState().s_pageLoading===key?null:getState().s_pageLoading,...target.resource?{s_loading:!1}:{}};if(target.kind==="sas"){let parsed=parseSasUrl(getState().s_sasUrl);patch.s_sasBlobs=parsed.canRead?data.items.map(b=>({...b,url:blobSasUrl(getState().s_sasUrl,b.name)})):data.items,patch.s_sasStats=filterAndStat(data.items,{}).stats}else if(target.resource)if(target.kind==="blob"){let criteria=getState().s_criteria,filtered=criteria?filterAndStat(data.items,criteria):null;Object.assign(patch,{s_blobs:data.items,s_blobPrefixes:data.prefixes||[],s_blobsMore:!!data.nextToken,s_stats:filtered?.stats||null,s_filtered:filtered?filtered.filtered.map(b=>b.name):[]})}else target.kind==="file"?patch.s_files=data.items:target.kind==="table"&&(patch.s_entities={...data.entities,columns});else{let field={blob:"containers",file:"shares",queue:"queues",table:"tables"}[target.kind];setTree(ctx,target.account,{[field]:data.items},{[target.kind]:!1})}return setState(patch),{ok:!0,...target.kind==="sas"?{blobs:data.items.length}:target.resource?target.kind==="blob"?{blobs:data.items.length,directories:(data.prefixes||[]).length,truncated:!!data.nextToken}:target.kind==="file"?{items:data.items.length}:{entities:data.entities.rows.length}:{count:data.items.length,...target.kind==="blob"?{containers:data.items.map(item=>item.name)}:{}},page:index+1,hasNext:!!data.nextToken}}async function loadAccounts(_p,{setState,log}){setState({s_loading:!0,s_accountsLoading:!0,s_error:null});let r=await safe(()=>az.listStorageAccounts());return r.ok?(setState({s_loading:!1,s_accountsLoading:!1,s_accounts:r.data}),log(`Loaded ${r.data.length} storage accounts.`),{ok:!0,count:r.data.length}):(setState({s_loading:!1,s_accountsLoading:!1,s_error:r.error,s_needsAuth:r.needsAuth}),log(`Load accounts failed: ${r.error}`),{ok:!1,error:r.error})}async function loadSubscriptions(_p,{setState,log}){setState({s_subsLoading:!0,s_error:null,s_seInstalled:az.storageExplorerInstalled()});let r=await safe(()=>az.listSubscriptions());if(!r.ok)return setState({s_subsLoading:!1,s_error:r.error,s_needsAuth:r.needsAuth}),log(`Load subscriptions failed: ${r.error}`),{ok:!1,error:r.error};let inventory=await safe(()=>az.listTenants()),subs=[...r.data.filter(s=>!discoveredSubscriptions.has(s.tenantId)),...[...discoveredSubscriptions.values()].flat()].sort((a,b)=>(a.displayName||"").localeCompare(b.displayName||"")),tenants=inventory.ok?inventory.data:[];for(let sub of subs)tenants.some(t=>t.tenantId===sub.tenantId)||tenants.push({tenantId:sub.tenantId,displayName:"Cached directory",current:!1});return setState({s_subsLoading:!1,s_subscriptions:subs,s_tenants:tenants,s_tenantInventoryError:inventory.ok?null:inventory.error}),inventory.ok||log(`Current tenant inventory failed: ${inventory.error}`),log(`Found ${tenants.length} directories and ${subs.length} cached subscriptions.`),{ok:!0,count:subs.length}}async function selectSubscriptions({subscriptionIds},{getState,setState,log}){let generation=++subscriptionGeneration,requested=Array.isArray(subscriptionIds)?subscriptionIds:[],chosen=(getState().s_subscriptions||[]).filter(s=>requested.includes(s.subscriptionId)),ids=chosen.map(s=>s.subscriptionId),clearResource={s_account:null,s_container:null,s_kind:null,s_item:null,s_tree:{},s_containers:[],s_blobs:[],s_stats:null,s_filtered:[],s_shares:[],s_queues:[],s_tables:[],s_files:[],s_messages:[],s_entities:null},st=getState(),currentAccount=(st.s_accounts||[]).find(a=>a.name===st.s_account),keepResource=currentAccount&&ids.includes(currentAccount.subscriptionId);if(setState({s_selectedSubs:ids,s_loading:!0,s_accountsLoading:!0,s_error:null,s_needsAuth:!1,s_subscriptionErrors:[],...keepResource?{}:{s_accounts:[],...clearResource}}),!chosen.length)return setState({s_loading:!1,s_accountsLoading:!1}),log("Scope cleared. No subscriptions selected."),{ok:!0,count:0};log(`Loading storage accounts for ${chosen.length} subscription(s)\u2026`);let r=await safe(()=>az.listStorageAccountsForSubs(chosen));if(generation!==subscriptionGeneration)return{ok:!1,error:"Subscription selection changed while accounts were loading. Older results were discarded."};if(!r.ok)return setState({s_loading:!1,s_accountsLoading:!1,s_error:r.error,s_needsAuth:r.needsAuth}),log(`Load accounts failed: ${r.error}`),{ok:!1,error:r.error};let{accounts,errors}=r.data,patch={s_loading:!1,s_accountsLoading:!1,s_accounts:accounts};if(getState().s_account&&!accounts.some(a=>a.name===getState().s_account)&&Object.assign(patch,clearResource),patch.s_subscriptionErrors=errors||[],errors&&errors.length){patch.s_error=`${errors.length} subscription(s) could not be read. ${accounts.length} storage account(s) loaded successfully.`;for(let e of errors)log(`Subscription ${e.subscriptionName} (${e.subscriptionId}, tenant ${e.tenantId||"unknown"}) failed: ${e.error}`)}return setState(patch),log(`Loaded ${accounts.length} accounts from ${chosen.length} subscription(s).`),{ok:!errors.length,count:accounts.length,errors}}function setTree({getState,setState},account,patch,loadingPatch){let tree={...getState().s_tree||{}},cur=tree[account]||{containers:null,shares:null,queues:null,tables:null,loading:{}};tree[account]={...cur,...patch||{},loading:{...cur.loading,...loadingPatch||{}}},setState({s_tree:tree})}async function openAccount({account},ctx){++navigationGeneration;let{getState,setState,log}=ctx;if(!account)return{ok:!1,error:"account required"};if(saveServiceView(getState()),setState({s_error:null,s_account:account,s_container:null,s_blobs:[],s_stats:null,s_filtered:[],s_criteria:null,s_insight:null,s_kind:null,s_item:null,s_files:[],s_messages:[],s_entities:null}),getState().s_tree&&getState().s_tree[account]&&getState().s_tree[account].containers!=null)return log(`Focused account ${account}.`),{ok:!0,cached:!0};if(az.listStoragePage)return loadStoragePage({account,kind:"blob"},ctx);setTree(ctx,account,{},{blob:!0}),log(`Opening account ${account}\u2026`);let r=await safe(()=>az.listContainers(account));return r.ok?(setTree(ctx,account,{containers:r.data},{blob:!1}),log(`${account}: ${r.data.length} containers.`),{ok:!0,containers:r.data.map(c=>c.name)}):(setTree(ctx,account,{},{blob:!1}),setState({s_error:r.error,s_needsAuth:r.needsAuth}),log(`Open ${account} failed: ${r.error}`),{ok:!1,error:r.error})}async function openContainer({container,account,prefix=""},ctx){let{getState,setState,log}=ctx,generation=++navigationGeneration;if(account=account||getState().s_account,!account)return{ok:!1,error:"open an account first"};if(!container)return{ok:!1,error:"container required"};if(typeof prefix!="string"||prefix&&!prefix.endsWith("/"))throw new Error("Invalid blob directory prefix.");if(setState({s_loading:!0,s_error:null,s_account:account,s_container:container,s_kind:"blob",s_item:container,s_blobPrefix:prefix,s_blobPrefixes:[],s_blobs:[],s_blobsMore:!1,s_stats:null,s_filtered:[],s_criteria:null,s_insight:null}),az.listStoragePage)return loadStoragePage({account,kind:"blob",resource:container,prefix},ctx);log(`Listing blobs in ${account}/${container}\u2026`);let r=await safe(()=>az.listBlobDirectory(account,container,{prefix}));return generation!==navigationGeneration||getState().s_account!==account||getState().s_kind!=="blob"?{ok:!1,error:"Resource navigation changed; older blob listing discarded."}:r.ok?(setState({s_loading:!1,s_blobs:r.data.blobs,s_blobPrefixes:r.data.prefixes,s_blobsMore:!!r.data.truncated}),log(`${container}/${prefix}: ${r.data.blobs.length} blobs, ${r.data.prefixes.length} directories${r.data.truncated?" (listing capped at "+r.data.limit+" items)":""}.`),{ok:!0,blobs:r.data.blobs.length,directories:r.data.prefixes.length,truncated:r.data.truncated}):(setState({s_loading:!1,s_error:r.error,s_needsAuth:r.needsAuth}),log(`List ${container} failed: ${r.error}`),{ok:!1,error:r.error})}async function listShares2({account}={},ctx){let{getState,log}=ctx;if(account=account||getState().s_account,!account)return{ok:!1,error:"open an account first"};if(az.listStoragePage)return loadStoragePage({account,kind:"file"},ctx);setTree(ctx,account,{},{file:!0});let r=await safe(()=>az.listShares(account));return r.ok?(setTree(ctx,account,{shares:r.data},{file:!1}),log(`${account}: ${r.data.length} file shares.`),{ok:!0,count:r.data.length}):(setTree(ctx,account,{},{file:!1}),ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),log(`List shares failed: ${r.error}`),{ok:!1,error:r.error})}async function listQueues2({account}={},ctx){let{getState,log}=ctx;if(account=account||getState().s_account,!account)return{ok:!1,error:"open an account first"};if(az.listStoragePage)return loadStoragePage({account,kind:"queue"},ctx);setTree(ctx,account,{},{queue:!0});let r=await safe(()=>az.listQueues(account));return r.ok?(setTree(ctx,account,{queues:r.data},{queue:!1}),log(`${account}: ${r.data.length} queues.`),{ok:!0,count:r.data.length}):(setTree(ctx,account,{},{queue:!1}),ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),log(`List queues failed: ${r.error}`),{ok:!1,error:r.error})}async function listTables2({account}={},ctx){let{getState,log}=ctx;if(account=account||getState().s_account,!account)return{ok:!1,error:"open an account first"};if(az.listStoragePage)return loadStoragePage({account,kind:"table"},ctx);setTree(ctx,account,{},{table:!0});let r=await safe(()=>az.listTables(account));return r.ok?(setTree(ctx,account,{tables:r.data},{table:!1}),log(`${account}: ${r.data.length} tables.`),{ok:!0,count:r.data.length}):(setTree(ctx,account,{},{table:!1}),ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),log(`List tables failed: ${r.error}`),{ok:!1,error:r.error})}async function listContainersTree({account}={},ctx){let{getState,log}=ctx;if(account=account||getState().s_account,!account)return{ok:!1,error:"account required"};if(getState().s_tree&&getState().s_tree[account]&&getState().s_tree[account].containers!=null)return{ok:!0,cached:!0};if(az.listStoragePage)return loadStoragePage({account,kind:"blob"},ctx);setTree(ctx,account,{},{blob:!0});let r=await safe(()=>az.listContainers(account));return r.ok?(setTree(ctx,account,{containers:r.data},{blob:!1}),log(`${account}: ${r.data.length} containers (tree).`),{ok:!0,count:r.data.length}):(setTree(ctx,account,{},{blob:!1}),ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),log(`List containers failed: ${r.error}`),{ok:!1,error:r.error})}async function openQueue({queue,account},{getState,setState,log}){let generation=++navigationGeneration;if(account=account||getState().s_account,!account||!queue)return{ok:!1,error:"open an account first"};setState({s_loading:!0,s_error:null,s_account:account,s_kind:"queue",s_item:queue,s_messages:[]}),log(`Peeking messages in ${account} \xB7 queue ${queue}\u2026`);let r=await safe(()=>az.peekQueueMessages(account,queue));return generation!==navigationGeneration||getState().s_account!==account||getState().s_kind!=="queue"||getState().s_item!==queue?{ok:!1,error:"Resource navigation changed; older queue listing discarded."}:r.ok?(setState({s_loading:!1,s_messages:r.data}),log(`${queue}: peeked ${r.data.length} message(s).`),{ok:!0,messages:r.data.length}):(setState({s_loading:!1,s_error:r.error,s_needsAuth:r.needsAuth}),log(`Peek ${queue} failed: ${r.error}`),{ok:!1,error:r.error})}async function openTable({table,account},ctx){let{getState,setState,log}=ctx;if(++navigationGeneration,account=account||getState().s_account,!account||!table)return{ok:!1,error:"open an account first"};if(setState({s_loading:!0,s_error:null,s_account:account,s_kind:"table",s_item:table,s_entities:null}),az.listStoragePage)return loadStoragePage({account,kind:"table",resource:table},ctx);log(`Querying entities in ${account} \xB7 table ${table}\u2026`);let r=await safe(()=>az.queryTableEntities(account,table));return r.ok?(setState({s_loading:!1,s_entities:r.data}),log(`${table}: ${r.data.rows.length} entities.`),{ok:!0,entities:r.data.rows.length}):(setState({s_loading:!1,s_error:r.error,s_needsAuth:r.needsAuth}),log(`Query ${table} failed: ${r.error}`),{ok:!1,error:r.error})}async function openShare({share,account,dir=""},ctx){let{getState,setState,log}=ctx,generation=++navigationGeneration;if(account=account||getState().s_account,!account||!share)return{ok:!1,error:"open an account first"};if(dir=shareDirectory(dir),setState({s_loading:!0,s_error:null,s_account:account,s_kind:"file",s_item:share,s_fileDir:dir,s_files:[]}),az.listStoragePage)return loadStoragePage({account,kind:"file",resource:share,dir},ctx);log(`Listing files in ${account} \xB7 share ${share}\u2026`);let r=await safe(()=>az.listShareItems(account,share,dir));return generation!==navigationGeneration||getState().s_account!==account||getState().s_kind!=="file"?{ok:!1,error:"Resource navigation changed; older file listing discarded."}:r.ok?(setState({s_loading:!1,s_files:r.data}),log(`${share}: ${r.data.length} item(s).`),{ok:!0,items:r.data.length}):(setState({s_loading:!1,s_error:r.error,s_needsAuth:r.needsAuth}),log(`List ${share} failed: ${r.error}`),{ok:!1,error:r.error})}async function refreshCategory(account,kind,ctx){if(az.listStoragePage)return loadStoragePage({account,kind},ctx);kind==="blob"?setTree(ctx,account,{containers:await az.listContainers(account)}):kind==="queue"?setTree(ctx,account,{queues:await az.listQueues(account)}):kind==="table"?setTree(ctx,account,{tables:await az.listTables(account)}):kind==="file"&&setTree(ctx,account,{shares:await az.listShares(account)})}async function afterMutation(read,ctx){let result=await safe(read);if(result.ok&&result.data?.ok!==!1)return{ok:!0};let cause=result.ok?result.data.error:result.error,error="Operation completed, but the listing could not be refreshed. Refresh it before retrying any operation.";return ctx.setState({s_error:error}),ctx.log(`Post-operation refresh failed: ${cause}`),{ok:!1,error,operationCompleted:!0}}function mutate(fn,{refresh,reopen}={}){return async(payload,ctx)=>{let account=payload&&payload.account||ctx.getState().s_account;if(!account)return{ok:!1,error:"open an account first"};let r=await safe(()=>fn(account,payload,ctx));if(!r.ok)return ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),ctx.log(`Operation failed: ${r.error}`),{ok:!1,error:r.error};if(refresh){let result=await afterMutation(()=>refreshCategory(account,refresh,ctx),ctx);if(!result.ok)return result}if(reopen){let result=await afterMutation(()=>reopen(account,payload,ctx),ctx);if(result?.ok===!1)return result}return r.data??{ok:!0}}}let reopenQueue=(account,{queue},ctx)=>openQueue({account,queue},ctx),reopenTable=(account,{table},ctx)=>openTable({account,table},ctx),reopenShare=(account,{share,dir=""},ctx)=>{let st=ctx.getState();return st.s_account!==account||st.s_kind!=="file"||st.s_item!==share||(st.s_fileDir||"")!==dir?{ok:!0}:openShare({share,account,dir},ctx)};function shareDirectory(dir){if(typeof dir!="string"||dir.split("/").some(p=>p==="."||p==="..")||dir.includes("\\"))throw new Error("Invalid file-share directory path.");return dir.split("/").filter(Boolean).join("/")}async function runBatchDownload(names,{getState,setState,log}){let st=getState(),account=st.s_account,container=st.s_container;if(!account||!container)return{ok:!1,error:"open a container first"};let list=(names||[]).filter(Boolean);if(!list.length)return{ok:!1,error:"no blobs given"};let dir=path2.join(DL_ROOT,container),done=0,okc=0,failed=[];setState({s_batch:{op:"download",total:list.length,done,ok:okc,failed:[],running:!0,dir}}),log(`Downloading ${list.length} blob(s) from ${container}\u2026`);for(let name of list){let r=await safe(()=>az.downloadBlob(account,container,name));if(done++,r.ok)try{await saveDownload(container,name,r.data),okc++}catch(e){failed.push({name,error:"could not save to disk: "+(e&&e.message?e.message:String(e))})}else failed.push({name,error:r.error});setState({s_batch:{op:"download",total:list.length,done,ok:okc,failed:[...failed],running:donei&&i.name&&i.contentBase64);if(!list.length)return{ok:!1,error:"no items given"};let done=0,okc=0,failed=[];setState({s_batch:{op:"upload",total:list.length,done,ok:okc,failed:[],running:!0}}),log(`Uploading ${list.length} file(s) to ${container}\u2026`);for(let it of list){let buf=Buffer.from(it.contentBase64,"base64"),r=await safe(()=>az.uploadBlob(account,container,(st.s_blobPrefix||"")+it.name,buf,it.contentType));done++,r.ok?okc++:failed.push({name:it.name,error:r.error}),setState({s_batch:{op:"upload",total:list.length,done,ok:okc,failed:[...failed],running:doneopenContainer({account,container,prefix:st.s_blobPrefix||""},ctx),ctx):{ok:!0};return log(`Upload complete: ${okc} ok, ${failed.length} failed.`),{ok:refreshed.ok&&!failed.length,uploaded:okc,failed,...refreshed.ok?{}:{error:"Upload finished, but listing refresh failed.",operationCompleted:okc>0}}}async function runBatchCleanup(action,ctx){let{getState,setState,log}=ctx,st=getState(),account=st.s_account,container=st.s_container;if(!account||!container)return{ok:!1,error:"open a container first"};let list=(st.s_filtered||[]).filter(Boolean);if(!list.length)return{ok:!1,error:"filter some blobs first, nothing matched"};let op=action==="delete"?"delete":"archive",done=0,okc=0,failed=[];setState({s_batch:{op,total:list.length,done,ok:okc,failed:[],running:!0}}),log(`${op==="delete"?"Deleting":"Archiving"} ${list.length} matched blob(s) in ${container}\u2026`);for(let name of list){let r=op==="delete"?await safe(()=>az.deleteBlob(account,container,name)):await safe(()=>az.setBlobTier(account,container,name,"Archive"));done++,r.ok?okc++:failed.push({name,error:r.error}),setState({s_batch:{op,total:list.length,done,ok:okc,failed:[...failed],running:doneopenContainer({account,container,prefix:st.s_blobPrefix||""},ctx),ctx):{ok:!0};return log(`${op==="delete"?"Delete":"Archive"} complete: ${okc} ok, ${failed.length} failed.`),{ok:refreshed.ok&&!failed.length,op,cleaned:okc,failed,...refreshed.ok?{}:{error:"Cleanup finished, but listing refresh failed.",operationCompleted:okc>0}}}let routes={"/load-accounts":loadAccounts,"/load-subscriptions":loadSubscriptions,"/tenant-subscriptions":async({tenantId},ctx)=>{if(!(ctx.getState().s_tenants||[]).some(t=>t.tenantId===tenantId))throw new Error("Tenant is not in the discovered inventory.");ctx.setState({s_tenantLoading:tenantId});try{let subscriptions=await az.listTenantSubscriptions(tenantId);return discoveredSubscriptions.set(tenantId,subscriptions),ctx.setState({s_subscriptions:[...(ctx.getState().s_subscriptions||[]).filter(s=>s.tenantId!==tenantId),...subscriptions],s_tenantErrors:{...ctx.getState().s_tenantErrors||{},[tenantId]:null}}),ctx.log(`Refreshed ${subscriptions.length} subscriptions for tenant ${tenantId}.`),{ok:!0}}catch(error){let failure=az.discoveryFailure(error);return ctx.setState({s_tenantErrors:{...ctx.getState().s_tenantErrors||{},[tenantId]:failure}}),ctx.log(`Tenant subscription discovery failed: ${failure.error}`),{ok:!1,error:failure.message}}finally{ctx.setState({s_tenantLoading:null})}},"/select-subscriptions":selectSubscriptions,"/account":openAccount,"/activate-service":async({account,kind},ctx)=>{let st=ctx.getState();if(account!==st.s_account||!["blob","file","queue","table"].includes(kind))throw new Error("Service selection changed.");saveServiceView(st),++navigationGeneration;let saved=serviceViews.get(JSON.stringify([account,kind])),{incomplete,...cached}=saved||{};return ctx.setState({s_entities:null,s_blobs:[],s_blobPrefixes:[],s_files:[],s_messages:[],s_stats:null,s_filtered:[],s_criteria:null,s_container:null,...saved?cached:{s_kind:kind,s_item:null},s_loading:!1,s_error:null,s_pageLoading:null,s_pageProgress:null}),incomplete&&cached.s_item?kind==="blob"?openContainer({account,container:cached.s_item,prefix:cached.s_blobPrefix||""},ctx):kind==="file"?openShare({account,share:cached.s_item,dir:cached.s_fileDir||""},ctx):kind==="table"?openTable({account,table:cached.s_item},ctx):openQueue({account,queue:cached.s_item},ctx):{ok:!0,cached:!!saved}},"/container":openContainer,"/list-shares":listShares2,"/list-queues":listQueues2,"/list-tables":listTables2,"/storage-page":async({account,kind,resource,prefix="",dir="",direction},ctx)=>{let st=ctx.getState();if(!["first","last","next","previous","refresh","cancel"].includes(direction))throw new Error("Invalid page direction.");if(kind==="sas"){if(!st.s_sasInfo||account!==st.s_sasInfo.account||resource!==st.s_sasInfo.container)throw new Error("SAS connection changed; refresh its page.")}else if(account!==st.s_account||resource&&(kind!==st.s_kind||resource!==st.s_item||kind==="blob"&&prefix!==(st.s_blobPrefix||"")||kind==="file"&&dir!==(st.s_fileDir||"")))throw new Error("Resource navigation changed; page request discarded.");if(direction==="cancel"){let key=pageKey({account,kind,resource,prefix,dir});return pageRequests.set(key,(pageRequests.get(key)||0)+1),ctx.setState({s_pageLoading:null,s_pageProgress:null,...resource&&kind!=="sas"?{s_loading:!1}:{}}),{ok:!0,cancelled:!0}}return loadStoragePage({account,kind,resource,prefix,dir},ctx,direction==="refresh"?void 0:direction)},"/tree-containers":listContainersTree,"/open-queue":openQueue,"/open-table":openTable,"/open-share":openShare,"/download-many":({blobs},ctx)=>runBatchDownload(blobs,ctx),"/open-folder":async({dir},_ctx)=>await openFolder(dir),"/dismiss-batch":(_p,{setState})=>(setState({s_batch:null}),{ok:!0}),"/get-prefs":async()=>({ok:!0,prefs:await readPrefs()}),"/save-prefs":async(input={})=>({ok:!0,prefs:await writePrefs(input)}),"/upload-many":({items,account,container,prefix},ctx)=>{let st=ctx.getState();if(account!==void 0&&(account!==st.s_account||container!==st.s_container||prefix!==(st.s_blobPrefix||""))){let error="The upload destination changed. Choose files again in the intended directory.";return ctx.setState({s_error:error}),ctx.log(error),{ok:!1,error}}return runBatchUpload(items,ctx)},"/container-create":mutate((a,{container})=>az.createContainer(a,container),{refresh:"blob"}),"/container-delete":async({container,account},ctx)=>{let a=account||ctx.getState().s_account,r=await safe(()=>az.deleteContainer(a,container));if(!r.ok)return ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),{ok:!1,error:r.error};let refreshed=await afterMutation(()=>refreshCategory(a,"blob",ctx),ctx);return ctx.getState().s_kind==="blob"&&ctx.getState().s_item===container&&ctx.setState({s_kind:null,s_item:null,s_container:null,s_blobs:[]}),ctx.log(`Deleted container ${container}.`),refreshed},"/blob-delete":async({container,name,account},ctx)=>{let a=account||ctx.getState().s_account,r=await safe(()=>az.deleteBlob(a,container,name));return r.ok?(await afterMutation(()=>openContainer({account:a,container,prefix:ctx.getState().s_blobPrefix||""},ctx),ctx)).ok?(ctx.log(`Deleted blob ${name}.`),{ok:!0}):{ok:!1,operationCompleted:!0,error:"Blob deleted, but listing refresh failed."}:(ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),{ok:!1,error:r.error})},"/generate-sas":async({container,name,account,minutes,permissions},{getState,log})=>{let a=account||getState().s_account,c=container||getState().s_container;if(!c||!name)return{ok:!1,error:"no blob selected"};let r=await safe(()=>az.generateBlobSas(a,c,name,{minutes:Number(minutes)||60,permissions:permissions||"r"}));return r.ok?(log(`Generated ${r.data.mode} SAS for ${name} (${r.data.permissions}, expires ${r.data.expiresOn}).`),{ok:!0,name,...r.data}):{ok:!1,error:r.error,needsAuth:r.needsAuth}},"/generate-container-sas":async({container,account,minutes,permissions},{getState,log})=>{let a=account||getState().s_account,c=container||getState().s_container;if(!c)return{ok:!1,error:"no container selected"};let r=await safe(()=>az.generateContainerSas(a,c,{minutes:Number(minutes)||1440,permissions:permissions||"rl"}));return r.ok?(log(`Generated ${r.data.mode} container SAS for ${c} (${r.data.permissions}, expires ${r.data.expiresOn}).`),{ok:!0,container:c,...r.data}):{ok:!1,error:r.error,needsAuth:r.needsAuth}},"/queue-create":mutate((a,{queue})=>az.createQueue(a,queue),{refresh:"queue"}),"/queue-delete":async({queue,account},ctx)=>{let a=account||ctx.getState().s_account,r=await safe(()=>az.deleteQueue(a,queue));if(!r.ok)return ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),{ok:!1,error:r.error};let refreshed=await afterMutation(()=>refreshCategory(a,"queue",ctx),ctx);return ctx.getState().s_kind==="queue"&&ctx.getState().s_item===queue&&ctx.setState({s_kind:null,s_item:null,s_messages:[]}),ctx.log(`Deleted queue ${queue}.`),refreshed},"/queue-send":mutate((a,{queue,text})=>az.sendQueueMessage(a,queue,text),{reopen:reopenQueue}),"/queue-clear":mutate((a,{queue})=>az.clearQueueMessages(a,queue),{reopen:reopenQueue}),"/queue-msg-update":mutate((a,{queue,messageId,text})=>az.updateQueueMessage(a,queue,messageId,text),{reopen:reopenQueue}),"/queue-msg-delete":mutate((a,{queue,messageId})=>az.deleteQueueMessage(a,queue,messageId),{reopen:reopenQueue}),"/table-create":mutate((a,{table})=>az.createTable(a,table),{refresh:"table"}),"/table-delete":async({table,account},ctx)=>{let a=account||ctx.getState().s_account,r=await safe(()=>az.deleteTable(a,table));if(!r.ok)return ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),{ok:!1,error:r.error};let refreshed=await afterMutation(()=>refreshCategory(a,"table",ctx),ctx);return ctx.getState().s_kind==="table"&&ctx.getState().s_item===table&&ctx.setState({s_kind:null,s_item:null,s_entities:null}),ctx.log(`Deleted table ${table}.`),refreshed},"/entity-upsert":mutate((a,{table,entity})=>az.upsertEntity(a,table,entity),{reopen:reopenTable}),"/entity-delete":mutate((a,{table,partitionKey,rowKey})=>az.deleteEntity(a,table,partitionKey,rowKey),{reopen:reopenTable}),"/share-create":mutate((a,{share})=>az.createShare(a,share),{refresh:"file"}),"/share-delete":async({share,account},ctx)=>{let a=account||ctx.getState().s_account,r=await safe(()=>az.deleteShare(a,share));if(!r.ok)return ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),{ok:!1,error:r.error};let refreshed=await afterMutation(()=>refreshCategory(a,"file",ctx),ctx);return ctx.getState().s_kind==="file"&&ctx.getState().s_item===share&&ctx.setState({s_kind:null,s_item:null,s_files:[]}),ctx.log(`Deleted file share ${share}.`),refreshed},"/share-mkdir":mutate((a,{share,dir="",name})=>az.createShareDirectory(a,share,shareDirectory(name?[dir,name].filter(Boolean).join("/"):dir)),{reopen:reopenShare}),"/share-delete-item":mutate((a,{share,name,kind,dir=""})=>az.deleteShareItem(a,share,name,kind,shareDirectory(dir)),{reopen:reopenShare}),"/share-upload":mutate((a,{share,name,contentBase64,dir=""})=>az.uploadShareFile(a,share,name,Buffer.from(contentBase64||"","base64"),shareDirectory(dir)),{reopen:reopenShare}),"/share-download":async({account,share,name,dir:directory=""},ctx)=>{let a=account||ctx.getState().s_account;directory=shareDirectory(directory);let r=await safe(()=>az.downloadShareFile(a,share,name,directory));if(!r.ok)return ctx.setState({s_error:r.error,s_needsAuth:r.needsAuth}),{ok:!1,error:r.error};let dir=path2.join(DL_ROOT,share);try{await saveDownload(share,[directory,name].filter(Boolean).join("/"),r.data)}catch(e){let msg="could not save to disk: "+(e&&e.message?e.message:String(e));return ctx.setState({s_error:msg}),{ok:!1,error:msg}}return ctx.setState({s_batch:{op:"download",total:1,done:1,ok:1,failed:[],running:!1,dir}}),ctx.log(`Downloaded ${name} from share ${share} to ${dir}.`),{ok:!0,dir}},"/delete-selected":async({account,kind,resource,dir="",prefix="",names},ctx)=>{let st=ctx.getState();dir=shareDirectory(dir);let fail=error=>(ctx.setState({s_error:error}),ctx.log(`Delete selected failed: ${error}`),{ok:!1,error});if(!["blob","file"].includes(kind)||st.s_account!==account||st.s_kind!==kind||st.s_item!==resource||kind==="file"&&(st.s_fileDir||"")!==dir||kind==="blob"&&(st.s_blobPrefix||"")!==prefix)return fail("The open resource changed. Select items again before deleting.");let items=kind==="blob"?st.s_blobs||[]:st.s_files||[];if(!Array.isArray(names)||!names.length||names.some(n=>!items.some(i=>i.name===n)))return fail("Select items from the current listing before deleting.");let failures=[],deleted=0;for(let name of new Set(names)){let item=items.find(i=>i.name===name),result=await safe(()=>kind==="blob"?az.deleteBlob(account,resource,name):az.deleteShareItem(account,resource,name,item.kind,dir));result.ok?deleted++:(failures.push({name,error:result.error}),ctx.log(`Delete ${name} failed: ${result.error}`))}let refreshed=ctx.getState().s_account===account&&ctx.getState().s_kind===kind&&ctx.getState().s_item===resource&&(kind!=="file"||(ctx.getState().s_fileDir||"")===dir)&&(kind!=="blob"||(ctx.getState().s_blobPrefix||"")===prefix)?await(kind==="blob"?openContainer({account,container:resource,prefix},ctx):openShare({account,share:resource,dir},ctx)):{ok:!0};if(ctx.log(`Delete selected: ${deleted} deleted, ${failures.length} failed.`),failures.length||!refreshed.ok){let error=`${deleted} item(s) deleted; ${failures.length} failed.${refreshed.ok?"":" Listing refresh also failed."} Open diagnostics for details.`;return ctx.setState({s_error:error}),{ok:!1,deleted,failures,error,operationCompleted:deleted>0}}return{ok:!0,deleted}},"/diagnose":(_p,_ctx)=>({ok:!1,error:"diagnosis removed"}),"/filter":async(criteria,{getState,setState,log})=>{let st=getState();if(!(st.s_kind==="blob"&&st.s_container))return{ok:!1,error:"Open a container first to filter its blobs."};let blobs=st.s_blobs||[],{filtered,stats}=filterAndStat(blobs,criteria||{});return setState({s_criteria:criteria||{},s_filtered:filtered.map(b=>b.name),s_stats:stats}),log(`Filter matched ${filtered.length} of ${blobs.length} blobs.`),{ok:!0,matched:filtered.length,stats}},"/download-filtered":async(_p,ctx)=>{let names=ctx.getState().s_filtered||[];return runBatchDownload(names,ctx)},"/cleanup-filtered":({action},ctx)=>runBatchCleanup(action,ctx),"/transfer-plan":async({dest,resolution,useFiltered,source:srcContainer,sourceSas},{getState,setState,log})=>{let st=getState(),account=st.s_account,container=String(srcContainer||st.s_container||"").trim(),sourceSasUrl=String(sourceSas||"").trim(),fail=(message,severity="error")=>{let msg=redactEndpointError(message,[sourceSasUrl,String(dest||"")]);return setState({s_transferErr:msg,s_transferSeverity:severity,s_transferProgress:null,s_transfer:null}),log(`Transfer plan ${severity==="warning"?"blocked":"failed"}: ${msg}`),{ok:!1,error:msg,severity}},parsedSource;if(sourceSasUrl){if(parsedSource=parseSasUrl(sourceSasUrl),!parsedSource.ok)return fail(parsedSource.error);if(!parsedSource.container||parsedSource.blobPath||parsedSource.resource!=="c")return fail("Source must be a container SAS URL, not an account or blob SAS.");if(parsedSource.expired)return fail("Source SAS has expired. Provide a current container SAS.");if(!parsedSource.canRead||!parsedSource.canList)return fail("Source SAS needs Read and List permissions to plan and copy the container.");if(useFiltered)return fail("Filtered transfer applies only to the open account/container source, not a SAS source.")}else{if(!account)return fail("Open a storage account first. Pick one in the Explorer.");if(!container)return fail("Choose a source container.")}let isOpen=!sourceSasUrl&&st.s_kind==="blob"&&st.s_item===container;if(useFiltered&&(!isOpen||!st.s_criteria))return fail("Apply filter criteria to the open source container before planning a filtered transfer.");let progress=side=>count=>setState({s_transferProgress:{side,count}});setState({s_transfer:null,s_transferErr:null,s_transferSeverity:null,s_transferProgress:{side:"source",count:0}});let criteria=useFiltered?{...st.s_criteria,now:st.s_criteria.now||new Date().toISOString()}:null,options={complete:!0,onProgress:progress("source"),...criteria?{prefix:st.s_blobPrefix||"",requireTags:!!(criteria.tags&&Object.keys(criteria.tags).length||criteria.tagKeys?.length)}:{}},allBlobs;if(parsedSource){let rs=await safe(()=>listTransferSas(sourceSasUrl,options));if(!rs.ok)return fail(`Couldn't read the source SAS container: ${rs.error}`);allBlobs=rs.data.blobs}else{let rs=await safe(()=>az.listBlobs(account,container,options));if(!rs.ok)return fail(`Couldn't read source container '${container}': ${rs.error}`);allBlobs=rs.data}if(allBlobs.truncated)return fail("Source inventory is incomplete. No transfer plan was generated.");let prefix=st.s_blobPrefix||"",source=criteria?applyBlobFilter(allBlobs.filter(b=>b.name.startsWith(prefix)&&!b.name.slice(prefix.length).includes("/")),criteria):allBlobs,names=criteria?new Set(source.map(b=>b.name)):null,destStr=String(dest||"").trim();if(!destStr)return fail("Enter a destination: 'account/container' (e.g. myaccount/backups) or a container SAS URL.");if(!/[?&]sig=/.test(destStr)&&destStr.replace(/^https?:\/\//,"").split("/").filter(Boolean).length<2)return fail(`Destination must be 'account/container', not just '${destStr}'. Example: ${account}/${destStr||"backups"}, or paste a container SAS URL.`);if(`${account}/${container}`===destStr.replace(/^https?:\/\//,"").replace(".blob.core.windows.net",""))return fail("Source and destination are the same container. Pick a different destination.");let res=["overwrite","skip","if-newer"].includes(resolution)?resolution:"overwrite",destBlobs=[],destUrl="",destSas=!1;setState({s_transferProgress:{side:"destination",count:0}});let r=await safe(async()=>{if(/[?&]sig=/.test(destStr)){let p=parseSasUrl(destStr);if(!p.ok)throw new Error(p.error);if(!p.container||p.blobPath||p.resource!=="c")throw new Error("Destination must be a container SAS URL, not an account or blob SAS.");if(p.expired)throw new Error("Destination SAS has expired.");if(!p.canList)throw new Error("Destination SAS needs List permission to calculate conflicts.");if(!/w/i.test(p.permissions))throw new Error("Destination SAS needs Write permission for the planned copy.");return destUrl=p.containerUrl,destSas=!0,await listTransferSas(destStr,{complete:!0,onProgress:progress("destination")}).then(x=>x.blobs)}let m=destStr.replace(/^https?:\/\//,"").split("/"),dAcct=m[0].split(".")[0],dCont=m[1];if(!dAcct||!dCont)throw new Error("Destination must be 'account/container' or a container SAS URL.");return destUrl=`https://${dAcct}.blob.core.windows.net/${dCont}`,await az.listBlobs(dAcct,dCont,{complete:!0,onProgress:progress("destination")})});if(!r.ok)return fail(`Couldn't read the destination: ${r.error}`);if(destBlobs=r.data,destBlobs.truncated)return fail("Destination inventory is incomplete. No transfer plan was generated.");let sourceUrl=parsedSource?parsedSource.containerUrl:`https://${account}.blob.core.windows.net/${container}`;if(new URL(sourceUrl).origin+new URL(sourceUrl).pathname===new URL(destUrl).origin+new URL(destUrl).pathname)return fail("Source and destination are the same container. Pick a different destination.");if(res==="if-newer"){let byName=new Map(destBlobs.map(b=>[b.name,b]));if(source.some(b=>byName.has(b.name)&&(!Number.isFinite(Date.parse(b.lastModified))||!Number.isFinite(Date.parse(byName.get(b.name).lastModified)))))return fail("Last modified timestamps are missing for conflicting blobs. Choose Overwrite or Skip existing instead of If source newer.")}let plan=planTransfer(source,destBlobs,res),allSource=!names,{command,needsLogin,empty}=azcopyCommand({sourceUrl,destUrl,resolution:res,includeNames:names?plan.toTransfer:null,allSource});if(empty)return fail("Nothing to transfer \u2014 every selected blob already exists at the destination under the current conflict rule.");let azcopy=await transferDoctor(),summary={...plan,toTransfer:plan.toTransfer.slice(0,100),toTransferTruncated:plan.toTransfer.length>100};return setState({s_transferErr:null,s_transferSeverity:null,s_transferProgress:null,s_transfer:{plan:summary,command,needsLogin,azcopy,sourceLabel:parsedSource?`${parsedSource.account}/${parsedSource.container} (SAS)`:`${account}/${container}`,destLabel:destSas?`${plan.destCount} blobs (SAS)`:destStr,source:allSource?"all blobs":`${source.length} filtered blobs`}}),log(`Transfer plan: ${plan.transferCount} to transfer, ${plan.conflictCount} conflicts (${res}).${azcopy.installed?` AzCopy ${azcopy.version||"detected"}.`:" AzCopy not found \u2014 install it to run the command."}`),{ok:!0,plan:summary,...!parsedSource&&!destSas?{command}:{},needsLogin,azcopy}},"/azcopy-doctor":async(_p,_ctx)=>({ok:!0,azcopy:await azcopyDoctor({refresh:!0})}),"/sas-connect":async({url},ctx)=>{let{setState,log}=ctx;setState({s_loading:!0,s_error:null});let parsed=parseSasUrl(url);if(!parsed.ok)return setState({s_loading:!1,s_error:parsed.error}),{ok:!1,error:parsed.error};if(az.listStoragePage){if(parsed.expired||!parsed.canList||!parsed.container)return setState({s_loading:!1,s_error:"Provide an unexpired container SAS with List permission."}),{ok:!1,error:"Provide an unexpired container SAS with List permission."};let{containerUrl,...info}=parsed;return setState({s_sasUrl:url,s_sasInfo:info,s_sasBlobs:[]}),loadStoragePage({account:parsed.account,kind:"sas",resource:parsed.container},ctx)}log(`Connecting to SAS: ${describeSas(parsed)}`);let r=await safe(()=>listSasBlobs2(url));if(!r.ok)return setState({s_loading:!1,s_error:r.error}),log(`SAS connect failed: ${r.error}`),{ok:!1,error:r.error};let{stats}=filterAndStat(r.data.blobs,{}),withUrls=parsed.canRead?r.data.blobs.map(b=>({...b,url:blobSasUrl(url,b.name)})):r.data.blobs;return setState({s_loading:!1,s_sasUrl:url,s_sasInfo:{account:r.data.account,container:r.data.container,permissions:r.data.permissions,expiresOn:r.data.expiresOn,canRead:parsed.canRead,canList:parsed.canList,canWrite:parsed.canWrite,canDelete:parsed.canDelete,expired:parsed.expired,resource:parsed.resource},s_sasBlobs:withUrls,s_sasStats:stats}),log(`SAS connected: ${r.data.blobs.length} blobs in ${r.data.container}.`),{ok:!0,blobs:r.data.blobs.length}},"/sas-download":async({name},{getState,setState,log})=>{let url=getState().s_sasUrl;if(!url)return{ok:!1,error:"connect a SAS first"};let r=await safe(()=>downloadSasBlob2(url,name));if(!r.ok)return setState({s_error:r.error}),{ok:!1,error:r.error};let container=getState().s_sasInfo?.container||"sas",savedPath;try{savedPath=await saveDownload(container,name,Buffer.from(r.data.contentBase64,"base64"))}catch(e){let msg="could not save to disk: "+(e&&e.message?e.message:String(e));return setState({s_error:msg}),log(`SAS download failed: ${msg}`),{ok:!1,name,error:msg}}return log(`Downloaded '${name}' (${r.data.size} bytes) from SAS.`),{ok:!0,name,size:r.data.size,path:savedPath,dir:path2.dirname(savedPath)}},"/read-state":(_p,{getState})=>{let st=getState(),blobPage=st.s_pages?.[pageKey({account:st.s_account,kind:"blob",resource:st.s_container,prefix:st.s_blobPrefix||""})];return{ok:!0,state:{account:st.s_account,container:st.s_container,subscriptions:(st.s_subscriptions||[]).length,selectedSubs:st.s_selectedSubs||[],selectedSubscriptions:(st.s_subscriptions||[]).filter(s=>(st.s_selectedSubs||[]).includes(s.subscriptionId)),tenants:[...new Set((st.s_subscriptions||[]).map(s=>s.tenantId))],tenantInventory:st.s_tenants||[],tenantInventoryError:st.s_tenantInventoryError||null,loading:!!st.s_loading,error:st.s_error||null,subscriptionErrors:st.s_subscriptionErrors||[],accounts:(st.s_accounts||[]).map(a=>a.name),tree:st.s_tree||{},containers:Object.values(st.s_tree||{}).flatMap(t=>(t.containers||[]).map(c=>c.name)),blobs:st.s_blobs||[],blobsTruncated:blobPage?blobPage.index>0||!!blobPage.nextToken:!!st.s_blobs?.truncated,stats:st.s_stats||null,criteria:st.s_kind==="blob"&&st.s_criteria||null,filtered:st.s_filtered||[],batch:st.s_batch||null,listingPages:Object.entries(st.s_pages||{}).map(([scope,page])=>({scope:JSON.parse(scope),page:page.index+1,hasNext:!!page.nextToken})),sasInfo:st.s_sasInfo||null,sasBlobs:(st.s_sasBlobs||[]).map(b=>({name:b.name,size:b.size,lastModified:b.lastModified||null,contentType:b.contentType||null})),transfer:st.s_transfer?(()=>{let{command,sourceUrl,destUrl,...rest}=st.s_transfer;return rest})():null,insight:st.s_insight||null,preview:st.s_preview?{name:st.s_preview.name,isText:st.s_preview.isText,size:st.s_preview.size,truncated:st.s_preview.truncated,lang:st.s_preview.lang,format:st.s_preview.format||null,category:st.s_preview.category||null,magicHex:st.s_preview.magicHex||null,glb:st.s_preview.glb||null,isImage:!!st.s_preview.image,explaining:!!st.s_preview.explaining,explained:!!st.s_preview.explanation,text:st.s_preview.text||null}:null,s_queues:(st.s_queues||[]).map(q=>({name:q.name})),s_tables:(st.s_tables||[]).map(t=>({name:t.name})),s_shares:(st.s_shares||[]).map(x=>({name:x.name})),s_messages:(st.s_messages||[]).map(m=>({messageId:m.messageId||null,messageText:m.messageText})),s_entities:st.s_entities||null,s_files:(st.s_files||[]).map(f=>({name:f.name,kind:f.kind,size:f.size??null,lastModified:f.lastModified||null})),s_kind:st.s_kind||null,s_item:st.s_item||null,s_fileDir:st.s_fileDir||"",s_blobPrefix:st.s_blobPrefix||"",s_blobPrefixes:st.s_blobPrefixes||[]}}},"/canvas-answer":({answer,visual,target,suggestedFilter},{getState,setState})=>{let payload={pending:!1,answer:String(answer||""),visual:visual||null},st=getState(),t=target==="insights"||target==="preview"||target==="ask"?target:null;if(st.s_preview&&st.s_preview.explaining?t="preview":st.s_insight&&st.s_insight.pending?t="insights":t||(t="ask"),t==="insights")setState({s_insight:{...payload,suggestedFilter:suggestedFilter||null}});else if(t==="preview"){let p=st.s_preview||{};setState({s_preview:{...p,explaining:!1,explanation:payload}})}else setState({s_ask:payload});return{ok:!0}},"/insights":async(_p,{getState,setState,log})=>{let st=getState();return st.s_stats?(setState({s_insight:{pending:!0,answer:null,visual:null,suggestedFilter:null}}),log("Asked Copilot to explain the statistics."),await agent2?.requestInsight?.({account:st.s_account,container:st.s_container}),{ok:!0}):{ok:!1,error:"Filter or open a container first so there are statistics to explain."}},"/preview-blob":async({name},{getState,setState,log})=>{let st=getState(),account=st.s_account,container=st.s_container;if(!account||!container||!name)return{ok:!1,error:"Open a container and pick a blob to preview."};setState({s_preview:{name,pending:!0}});let MAX=32*1024,r=await safe(()=>az.downloadBlob(account,container,name,{maxBytes:MAX}));if(!r.ok)return setState({s_preview:{name,pending:!1,error:r.error}}),log(`Preview failed: ${r.error}`),{ok:!1,error:r.error};let slice=r.data.subarray(0,MAX),{isText,text}=sniffText(slice),bin=isText?null:sniffBinaryFormat(slice,name),mime=isText?null:imageMime(name),sz=await safe(()=>az.blobSize(account,container,name)),total=sz.ok?sz.data:slice.length,RENDER_MAX=25*1024*1024,GLB_MAX=50*1024*1024,image=mime&&total<=RENDER_MAX?{mime}:null,glb=null;if(bin&&/GLB/.test(bin.format)&&total<=GLB_MAX){let full=await safe(()=>az.downloadBlob(account,container,name));full.ok&&(glb=parseGlb(full.data))}return setState({s_preview:{name,pending:!1,size:total,truncated:total>MAX,isText,lang:previewLang(name),text:isText?text:null,format:bin?bin.format:null,category:bin?bin.category:null,magicHex:bin?bin.magicHex:null,image,glb,explanation:null}}),log(`Previewed ${name}: ${isText?"text":bin.format}, ${total} bytes.`),{ok:!0,isText,bytes:total}},"/explain-blob":async(_p,{getState,setState})=>{let p=getState().s_preview;return!p||p.pending||p.error?{ok:!1,error:"Preview a blob first."}:!p.isText&&!p.format?{ok:!1,error:"Preview a blob first."}:(setState({s_preview:{...p,explaining:!0,explanation:null}}),await agent2?.requestPreviewExplain?.({name:p.name,isText:!!p.isText,format:p.format||null,category:p.category||null,size:p.size,glb:p.glb||null}),{ok:!0})},"/dismiss-preview":(_p,{setState})=>(setState({s_preview:null}),{ok:!0}),"/dismiss-insight":(_p,{setState})=>(setState({s_insight:null}),{ok:!0}),"/open-storage-explorer":async(_p,{getState,log})=>{let st=getState(),account=st.s_account||null,kind=st.s_kind||null,item=st.s_item||null,where=item?`${account}/${item}`:account||"the app";log(`Opening Azure Storage Explorer \u2192 ${where}\u2026`);let r=await az.openInStorageExplorer({account,kind,item});return r.ok?(log(r.deepLink?`Opened Storage Explorer at ${where}.`:"Opened Storage Explorer."),{ok:!0,deepLink:r.deepLink}):r.notInstalled?(log("Azure Storage Explorer isn't installed. Opening the download page so you can get the free desktop app."),{ok:!1,notInstalled:!0,install:r.install}):(log(`Storage Explorer: ${r.error}`),{ok:!1,error:r.error})},"/dismiss-error":(_p,{setState})=>(setState({s_error:null,s_needsAuth:!1}),{ok:!0}),"/sign-in":async({tenantId}={},ctx)=>{let{setState,log}=ctx;if(tenantId&&![...ctx.getState().s_subscriptions||[],...ctx.getState().s_tenants||[]].some(s=>s.tenantId===tenantId))throw new Error("The requested tenant is not in the discovered Azure inventory.");setState({s_signingIn:!0,s_error:null}),log("Opening az login in your browser\u2026");let r=await az.signIn({tenantId});return r.ok?(setState({s_signingIn:!1,s_needsAuth:!1,s_error:null}),log("Signed in successfully. Loading subscriptions\u2026"),await loadSubscriptions({},ctx),(ctx.getState().s_selectedSubs||[]).length&&await selectSubscriptions({subscriptionIds:ctx.getState().s_selectedSubs},ctx),{ok:!0}):(setState({s_signingIn:!1,s_error:r.error,s_needsAuth:!0}),log(`Sign-in failed: ${r.error}`),{ok:!1,error:r.error})}},initialState={s_subscriptions:[],s_selectedSubs:[],s_subsLoading:!1,s_accounts:[],s_tree:{},s_containers:[],s_blobs:[],s_filtered:[],s_shares:[],s_queues:[],s_tables:[],s_files:[],s_messages:[],s_entities:null,s_kind:null,s_item:null,s_svcLoading:null,s_account:null,s_container:null,s_stats:null,s_batch:null,s_sasInfo:null,s_sasBlobs:[],s_ask:null,s_transfer:null,s_insight:null,s_preview:null,s_transferErr:null,s_loading:!1,s_accountsLoading:!1,s_error:null,activityLog:[],s_signingIn:!1,s_seInstalled:null};async function mediaHandler(pathname,query,{getState}){if(pathname!=="/blob-media")return null;let name=query.get("name"),st=getState(),p=st.s_preview;if(!p||p.name!==name||!p.image)return null;let account=st.s_account,container=st.s_container;if(!account||!container)return null;let r=await safe(()=>az.downloadBlob(account,container,name));return r.ok?{contentType:p.image.mime||imageMime(name)||"application/octet-stream",buffer:r.data}:null}let server2=await createCanvasServer({html:pageHtml,routes,media:mediaHandler,initialState,port});if(typeof az.registerAccountContext=="function"){let releaseContext=az.registerAccountContext(()=>({accounts:server2.getState().s_accounts,subscriptions:server2.getState().s_subscriptions})),close=server2.close;server2.close=()=>{releaseContext(),close()}}return server2.loadAccounts=()=>loadAccounts({},{getState:server2.getState,setState:server2.setState,log:server2.log}),server2.loadSubscriptions=()=>loadSubscriptions({},{getState:server2.getState,setState:server2.setState,log:server2.log}),server2}var session;async function post(server2,route,body){let res=await fetch(`http://127.0.0.1:${server2.port}${route}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body||{})});try{return await res.json()}catch{return{ok:res.ok}}}var agent={async requestInsight({container}={}){session?.send({prompt:`On the "Azure Storage" canvas, the user clicked "Explain these statistics"${container?` for container "${container}"`:""}. Call read_canvas_state ONCE and read the "stats" object (count, total/avg/max size, size histogram buckets, byType, byTier, byExt, topLargest) and the "filtered" set. The numbers are ALREADY provided \u2014 do NOT run shell commands or call other tools to compute them. Then immediately call answer_on_canvas with target:"insights" and a concise, specific interpretation: what stands out about size distribution, access tiers (Hot/Cool/Cold/Archive), file types, and any cost/cleanup/lifecycle opportunity (e.g. large cold data, many tiny files, old blobs). Cite real numbers. Include a visual ({kind:'bar'|'stat'|'table', ...}) when it helps. If an obvious cleanup/selection would help, ALSO pass suggestedFilter = a storage_filter_blobs criteria object (e.g. {olderThanDays:180} or {tiers:["Archive"],minSize:1048576}) so the user can apply it in one click. Do NOT explain in chat \u2014 render on the canvas.`,displayPrompt:"\u{1F9E0} Storage canvas \u2014 explain statistics"})},async requestPreviewExplain({name,isText,format,category,glb}={}){let glbNote=glb&&!glb.truncated?` The GLB's embedded JSON was parsed \u2014 read preview.glb for the REAL counts (meshes:${glb.meshes}, materials:${glb.materials}, textures:${glb.textures}, nodes:${glb.nodes}, animations:${glb.animations}, vertices:${glb.vertices}, triangles:${glb.triangles}${glb.generator?`, generator:"${glb.generator}"`:""}) and CITE these actual numbers instead of speaking generically.`:"",prompt=isText?`On the "Azure Storage" canvas, the user clicked "Ask Copilot to explain" for the previewed blob "${name}". Call read_canvas_state ONCE and read "preview" ({name, lang, size, truncated, text}). The file content is ALREADY provided in preview.text \u2014 do NOT run any shell commands, do NOT read files from disk, and do NOT call other tools to fetch it. Work only from preview.text. Then immediately call answer_on_canvas with target:"preview" and a concise explanation: what this file is, its structure/schema (columns/keys/format), what the data represents, and anything notable. If it was truncated, say so. Add a visual ({kind:'table'|'bar'|'stat'}) only if it clarifies the structure. Do NOT explain in chat \u2014 render on the canvas.`:`On the "Azure Storage" canvas, the user clicked "Explain this file" for the BINARY blob "${name}" (no text preview). Call read_canvas_state ONCE and read "preview" ({name, size, format, category, magicHex, glb}). The format was detected from the leading magic bytes${format?` and is "${format}"${category?` (${category})`:""}`:""}.${glbNote} The bytes are binary \u2014 do NOT run any shell commands, do NOT download or read the file, and do NOT call other tools. Reason only from the fields in preview. -Then immediately call answer_on_canvas with target:"preview" and a concise explanation of THIS FILE: what "${format||"this format"}" is and what it's used for, ${glb&&!glb.truncated?"PLUS the specific model composition using the real preview.glb counts (meshes/materials/textures/vertices/triangles), and a visual ({kind:'table'} listing those counts). ":""}what tooling opens/produces it, and practical Azure Storage tips (right Content-Type, whether it compresses, viewing, lifecycle/tiering). Keep it specific and do NOT claim to have inspected raw geometry beyond the provided counts. Do NOT explain in chat \u2014 render on the canvas.`;session?.send({prompt,displayPrompt:`\u{1F441} Storage canvas \u2014 explain "${String(name).slice(0,60)}"`})}},server=await start({agent}),canvas=createCanvas({id:"azure-storage",displayName:"Azure Storage",description:"Browse Azure Storage blobs/containers, bulk-transfer with live progress, filter + aggregate statistics, and open SAS links with no login \u2014 all agent-drivable on one live surface.",actions:[{name:"read_canvas_state",description:"Pull the live data shown on the Storage canvas (current account/container, the blob list with name/size/lastModified/contentType, computed stats, filtered set, batch progress, SAS info). Use this before answering any question about what's on the canvas.",inputSchema:{type:"object",properties:{keys:{type:"array",items:{type:"string"}}}},handler:async()=>post(server,"/read-state",{})},{name:"answer_on_canvas",description:"Render an answer back inside the canvas. `target` selects the panel: 'ask' (default, the Ask card), 'insights' (the statistics 'Explain' panel), or 'preview' (the blob preview panel). Always pass text `answer`; also pass an optional `visual` ({kind:'bar'|'stat'|'table', ...}) whenever the answer is quantitative/comparative so the user SEES it. When target='insights' you may also pass `suggestedFilter` (a storage_filter_blobs criteria object) to offer the user a one-click cleanup filter.",inputSchema:{type:"object",properties:{answer:{type:"string"},visual:{type:"object"},target:{type:"string",enum:["ask","insights","preview"]},suggestedFilter:{type:"object"}},required:["answer"]},handler:async({input})=>post(server,"/canvas-answer",{answer:input.answer,visual:input.visual,target:input.target,suggestedFilter:input.suggestedFilter})},{name:"storage_open_account",description:"Open a storage account on the canvas and load its containers live. Pass the account name exactly as shown in the account list (read_canvas_state \u2192 accounts).",inputSchema:{type:"object",properties:{account:{type:"string"}},required:["account"]},handler:async({input})=>post(server,"/account",{account:input.account})},{name:"storage_open_container",description:"Open a blob container on the canvas and list its blobs live (each blob's name/size/lastModified/contentType). Requires an account to be open first.",inputSchema:{type:"object",properties:{container:{type:"string"}},required:["container"]},handler:async({input})=>post(server,"/container",{container:input.container})},{name:"storage_batch_download",description:"Download several blobs at once from the open container with a LIVE progress bar and per-file pass/fail summary. Pass `blobs` = array of exact blob names (choose them by reading the blob list \u2014 e.g. the largest, or those older than a date). Non-destructive to the cloud.",inputSchema:{type:"object",properties:{blobs:{type:"array",items:{type:"string"}}},required:["blobs"]},handler:async({input})=>post(server,"/download-many",{blobs:input.blobs})},{name:"storage_batch_upload",description:"MUTATION \u2014 upload multiple files into the open container with a live progress bar. Only after the user has clearly asked to upload; narrate what you're uploading first. Pass `items` = array of {name, contentBase64, contentType}.",mutates:!0,inputSchema:{type:"object",properties:{items:{type:"array",items:{type:"object"}}},required:["items"]},handler:async({input})=>post(server,"/upload-many",{items:input.items})},{name:"storage_filter_blobs",description:"Filter the open container's blobs and compute LIVE statistics on the canvas (count, total/avg/max size, size histogram, by-type/by-tier/by-extension breakdown, top-10 largest). All criteria optional: namePattern (glob), prefix/suffix/contains (substring match on blob name), minSize/maxSize (bytes), olderThanDays/newerThanDays, type (image|video|audio|text/data|document|archive|binary), tiers (Hot|Cool|Cold|Archive \u2014 array or comma list), blobTypes (BlockBlob|PageBlob|AppendBlob), tagKeys (blob-index tag keys that must exist), tags (object of key=value that must all match). The matched NAMES become the filtered set for storage_download_filtered. This is aggregation MCP cannot do.",inputSchema:{type:"object",properties:{namePattern:{type:"string"},prefix:{type:"string"},suffix:{type:"string"},contains:{type:"string"},minSize:{type:"number"},maxSize:{type:"number"},olderThanDays:{type:"number"},newerThanDays:{type:"number"},type:{type:"string"},tiers:{type:"array",items:{type:"string"}},blobTypes:{type:"array",items:{type:"string"}},tagKeys:{type:"array",items:{type:"string"}},tags:{type:"object"}}},handler:async({input})=>post(server,"/filter",input)},{name:"storage_download_filtered",description:"Download every blob currently matched by the last storage_filter_blobs, with a live progress bar. Use after filtering, e.g. 'download everything older than 30 days'. Non-destructive to the cloud.",inputSchema:{type:"object",properties:{}},handler:async()=>post(server,"/download-filtered",{})},{name:"storage_open_sas",description:"Open a container SAS URL on the canvas WITHOUT any az login \u2014 parses the SAS, lists its blobs, and shows a read view. Use when the user pastes a 'https://acct.blob.core.windows.net/container?...&sig=...' link. Impossible for the Azure/Storage MCP (they require a full account + RBAC).",inputSchema:{type:"object",properties:{url:{type:"string"}},required:["url"]},handler:async({input})=>post(server,"/sas-connect",{url:input.url})},{name:"storage_download_sas_blob",description:"Download one blob from the currently-connected SAS container. Pass the exact blob `name` from the SAS blob list. Read-only.",inputSchema:{type:"object",properties:{name:{type:"string"}},required:["name"]},handler:async({input})=>post(server,"/sas-download",{name:input.name})},{name:"storage_plan_transfer",description:"Plan an AzCopy transfer FROM the currently-open container TO a destination, detecting name conflicts up-front and applying a resolution policy before anything moves. `dest` is either 'account/container' or a container SAS URL. `resolution` \u2208 overwrite|skip|if-newer. Set `useFiltered` true to transfer only the last-filtered set. Returns the plan (counts/conflicts/bytes) and the exact `azcopy copy` command; it does NOT move data. Use for 'copy these blobs to , skip ones that already exist'.",inputSchema:{type:"object",properties:{dest:{type:"string"},resolution:{type:"string",enum:["overwrite","skip","if-newer"]},useFiltered:{type:"boolean"}},required:["dest"]},handler:async({input})=>post(server,"/transfer-plan",{dest:input.dest,resolution:input.resolution,useFiltered:input.useFiltered})},{name:"storage_preview_blob",description:"Open an inline text PREVIEW of one blob from the open container (first 32 KB). Pass the exact blob `name`. Read-only. Binary blobs are detected and reported as non-text. After previewing, you can read the text via read_canvas_state \u2192 preview.text and explain it via answer_on_canvas target:'preview'.",inputSchema:{type:"object",properties:{name:{type:"string"}},required:["name"]},handler:async({input})=>post(server,"/preview-blob",{name:input.name})},{name:"storage_explain_stats",description:"Trigger the canvas's 'Explain these statistics' insight panel \u2014 reads the live filtered stats and renders an interpretation (via answer_on_canvas target:'insights'). Use when the user asks what the storage stats mean or how to clean up / optimize cost.",inputSchema:{type:"object",properties:{}},handler:async()=>post(server,"/insights",{})},{name:"storage_list_queues",description:"List the Storage queues in the open account on the canvas (read-only). Opens the Queues view and shows each queue with its approximate message count. Requires an account to be open first (storage_open_account).",inputSchema:{type:"object",properties:{account:{type:"string"}}},handler:async({input})=>post(server,"/list-queues",{account:input.account})},{name:"storage_open_queue",description:"Open one Storage queue on the canvas and peek its messages live (read-only \u2014 peeking does not dequeue). Pass the exact queue `name` from storage_list_queues. Requires an account to be open first.",inputSchema:{type:"object",properties:{queue:{type:"string"},account:{type:"string"}},required:["queue"]},handler:async({input})=>post(server,"/open-queue",{queue:input.queue,account:input.account})},{name:"storage_list_tables",description:"List the Storage tables in the open account on the canvas (read-only). Opens the Tables view. Requires an account to be open first (storage_open_account).",inputSchema:{type:"object",properties:{account:{type:"string"}}},handler:async({input})=>post(server,"/list-tables",{account:input.account})},{name:"storage_open_table",description:"Open one Storage table on the canvas and list its entities live (read-only). Pass the exact table `name` from storage_list_tables. Requires an account to be open first.",inputSchema:{type:"object",properties:{table:{type:"string"},account:{type:"string"}},required:["table"]},handler:async({input})=>post(server,"/open-table",{table:input.table,account:input.account})},{name:"storage_list_shares",description:"List the Azure File shares in the open account on the canvas (read-only). Opens the File shares view. Requires an account to be open first (storage_open_account).",inputSchema:{type:"object",properties:{account:{type:"string"}}},handler:async({input})=>post(server,"/list-shares",{account:input.account})},{name:"storage_open_share",description:"Open one Azure File share on the canvas and list its files and directories live (read-only). Pass the exact share `name` from storage_list_shares. Requires an account to be open first.",inputSchema:{type:"object",properties:{share:{type:"string"},account:{type:"string"}},required:["share"]},handler:async({input})=>post(server,"/open-share",{share:input.share,account:input.account})},{name:"log_message",description:"Append a message to the canvas activity log visible to the user.",inputSchema:{type:"object",properties:{message:{type:"string"}},required:["message"]},handler:({input})=>(server.log(input.message),{ok:!0})}],open(){return server.loadSubscriptions().catch(()=>{}),{url:`http://127.0.0.1:${server.port}`,title:"Azure Storage",status:"ready"}},onClose(){server.close?.()}});session=await joinSession({canvases:[canvas]}); +Then immediately call answer_on_canvas with target:"preview" and a concise explanation of THIS FILE: what "${format||"this format"}" is and what it's used for, ${glb&&!glb.truncated?"PLUS the specific model composition using the real preview.glb counts (meshes/materials/textures/vertices/triangles), and a visual ({kind:'table'} listing those counts). ":""}what tooling opens/produces it, and practical Azure Storage tips (right Content-Type, whether it compresses, viewing, lifecycle/tiering). Keep it specific and do NOT claim to have inspected raw geometry beyond the provided counts. Do NOT explain in chat \u2014 render on the canvas.`;session?.send({prompt,displayPrompt:`\u{1F441} Storage canvas \u2014 explain "${String(name).slice(0,60)}"`})}},server=await start({agent}),canvas=createCanvas({id:"azure-storage",displayName:"Azure Storage",description:"Browse Azure Storage blobs/containers, bulk-transfer with live progress, filter + aggregate statistics, and open SAS links with no login \u2014 all agent-drivable on one live surface.",actions:[{name:"read_canvas_state",description:"Pull the live data shown on the Storage canvas (current account/directory, blob and file metadata, queue message IDs, applied current-page filter criteria and statistics, filtered set, batch progress, SAS info). Use this before answering any question about what's on the canvas.",inputSchema:{type:"object",properties:{keys:{type:"array",items:{type:"string"}}}},handler:async()=>post(server,"/read-state",{})},{name:"answer_on_canvas",description:"Render shared agent notes alongside resource contents. `target` selects the surface: 'ask' (legacy default name for resource annotations, not a chat), 'insights' (statistics), or 'preview' (blob preview). Conversation stays in the session chat. Pass text `answer` and an optional quantitative `visual` ({kind:'bar'|'stat'|'table', ...}). For target='insights', suggestedFilter can offer a one-click storage filter.",inputSchema:{type:"object",properties:{answer:{type:"string"},visual:{type:"object"},target:{type:"string",enum:["ask","insights","preview"]},suggestedFilter:{type:"object"}},required:["answer"]},handler:async({input})=>post(server,"/canvas-answer",{answer:input.answer,visual:input.visual,target:input.target,suggestedFilter:input.suggestedFilter})},{name:"storage_open_account",description:"Open a storage account on the canvas and load its containers live. Pass the account name exactly as shown in the account list (read_canvas_state \u2192 accounts).",inputSchema:{type:"object",properties:{account:{type:"string"}},required:["account"]},handler:async({input})=>post(server,"/account",{account:input.account})},{name:"storage_open_container",description:"Open a blob container directory on the canvas. Optional prefix is an exact virtual-folder prefix ending in '/'; omit for root. Returns immediate blobs with canonical names and directory prefixes. Capped listings explicitly report incompleteness. Requires an open account.",inputSchema:{type:"object",properties:{container:{type:"string"},prefix:{type:"string"}},required:["container"]},handler:async({input})=>post(server,"/container",{container:input.container,prefix:input.prefix||""})},{name:"storage_batch_download",description:"Download several blobs at once from the open container with a LIVE progress bar and per-file pass/fail summary. Pass `blobs` = array of exact blob names (choose them by reading the blob list \u2014 e.g. the largest, or those older than a date). Non-destructive to the cloud.",inputSchema:{type:"object",properties:{blobs:{type:"array",items:{type:"string"}}},required:["blobs"]},handler:async({input})=>post(server,"/download-many",{blobs:input.blobs})},{name:"storage_batch_upload",description:"MUTATION \u2014 upload multiple files into the open container directory with a live progress bar. The current directory prefix is prepended to each supplied name. Only after the user has clearly asked to upload; narrate what you're uploading first. Pass `items` = array of {name, contentBase64, contentType}.",mutates:!0,inputSchema:{type:"object",properties:{items:{type:"array",items:{type:"object"}}},required:["items"]},handler:async({input})=>post(server,"/upload-many",{items:input.items})},{name:"storage_filter_blobs",description:"Filter the displayed blob page in the open container directory and compute LIVE statistics on the canvas (count, total/avg/max size, size histogram, by-type/by-tier/by-extension breakdown, top-10 largest). This does not enumerate other pages or folders. All criteria optional: namePattern (glob), prefix/suffix/contains (substring match on blob name), minSize/maxSize (bytes), olderThanDays/newerThanDays, type (image|video|audio|text/data|document|archive|binary), tiers (Hot|Cool|Cold|Archive \u2014 array or comma list), blobTypes (BlockBlob|PageBlob|AppendBlob), tagKeys (blob-index tag keys that must exist), tags (object of key=value that must all match). The matched NAMES become the filtered set for storage_download_filtered. read_canvas_state returns the applied criteria.",inputSchema:{type:"object",properties:{namePattern:{type:"string"},prefix:{type:"string"},suffix:{type:"string"},contains:{type:"string"},minSize:{type:"number"},maxSize:{type:"number"},olderThanDays:{type:"number"},newerThanDays:{type:"number"},type:{type:"string"},tiers:{type:"array",items:{type:"string"}},blobTypes:{type:"array",items:{type:"string"}},tagKeys:{type:"array",items:{type:"string"}},tags:{type:"object"}}},handler:async({input})=>post(server,"/filter",input)},{name:"storage_download_filtered",description:"Download every blob currently matched by the last storage_filter_blobs, with a live progress bar. Use after filtering, e.g. 'download everything older than 30 days'. Non-destructive to the cloud.",inputSchema:{type:"object",properties:{}},handler:async()=>post(server,"/download-filtered",{})},{name:"storage_open_sas",description:"Open a container SAS URL on the canvas WITHOUT any az login \u2014 parses the SAS, lists its blobs, and shows a read view. Use when the user pastes a 'https://acct.blob.core.windows.net/container?...&sig=...' link. Impossible for the Azure/Storage MCP (they require a full account + RBAC).",inputSchema:{type:"object",properties:{url:{type:"string"}},required:["url"]},handler:async({input})=>post(server,"/sas-connect",{url:input.url})},{name:"storage_download_sas_blob",description:"Download one blob from the currently-connected SAS container. Pass the exact blob `name` from the SAS blob list. Read-only.",inputSchema:{type:"object",properties:{name:{type:"string"}},required:["name"]},handler:async({input})=>post(server,"/sas-download",{name:input.name})},{name:"storage_browse_page",description:"Browse the current Storage list using cached page results and Azure continuation tokens. First/previous/last use fetched pages; last means last cached page, not an automatic Azure inventory walk. Next fetches an uncached page; refresh invalidates this scope and returns to page one; cancel discards an in-flight read after it finishes. Replaces the displayed page, never modifies cloud data. kind is blob, file, queue (queue list only; message peek has no continuation), table or sas. resource is the open container/share/table, omitted for service resource lists. Retain the exact current blob prefix/file dir. Search and selection apply to the displayed page.",inputSchema:{type:"object",properties:{account:{type:"string"},kind:{type:"string",enum:["blob","file","queue","table","sas"]},resource:{type:"string"},prefix:{type:"string"},dir:{type:"string"},direction:{type:"string",enum:["first","last","next","previous","refresh","cancel"]}},required:["account","kind","direction"]},handler:async({input})=>post(server,"/storage-page",input)},{name:"storage_plan_transfer",description:"Preview an AzCopy transfer from the open container or an explicit sourceSas container URL (Read+List permissions) to dest ('account/container' or a container SAS with List+Write permissions). Follows every inventory page independently of the display cap and calculates conflicts; never moves data. useFiltered reapplies Filter & statistics criteria to all direct blobs in the open folder, not descendants or Search items text. Returns counts/bytes and bounded conflict/name samples with an unsigned command; commands containing SAS credentials stay on the canvas, not in the model response.",inputSchema:{type:"object",properties:{dest:{type:"string"},sourceSas:{type:"string"},resolution:{type:"string",enum:["overwrite","skip","if-newer"]},useFiltered:{type:"boolean"}},required:["dest"]},handler:async({input})=>post(server,"/transfer-plan",{dest:input.dest,sourceSas:input.sourceSas,resolution:input.resolution,useFiltered:input.useFiltered})},{name:"storage_preview_blob",description:"Open an inline text PREVIEW of one blob from the open container (first 32 KB). Pass the exact blob `name`. Read-only. Binary blobs are detected and reported as non-text. After previewing, you can read the text via read_canvas_state \u2192 preview.text and explain it via answer_on_canvas target:'preview'.",inputSchema:{type:"object",properties:{name:{type:"string"}},required:["name"]},handler:async({input})=>post(server,"/preview-blob",{name:input.name})},{name:"storage_explain_stats",description:"Trigger the canvas's 'Explain these statistics' insight panel \u2014 reads the live filtered stats and renders an interpretation (via answer_on_canvas target:'insights'). Use when the user asks what the storage stats mean or how to clean up / optimize cost.",inputSchema:{type:"object",properties:{}},handler:async()=>post(server,"/insights",{})},{name:"storage_list_queues",description:"List the Storage queues in the open account on the canvas (read-only). Opens the Queues view and shows each queue with its approximate message count. Requires an account to be open first (storage_open_account).",inputSchema:{type:"object",properties:{account:{type:"string"}}},handler:async({input})=>post(server,"/list-queues",{account:input.account})},{name:"storage_open_queue",description:"Open one Storage queue on the canvas and peek its messages live (read-only \u2014 peeking does not dequeue). Pass the exact queue `name` from storage_list_queues. Requires an account to be open first.",inputSchema:{type:"object",properties:{queue:{type:"string"},account:{type:"string"}},required:["queue"]},handler:async({input})=>post(server,"/open-queue",{queue:input.queue,account:input.account})},{name:"storage_list_tables",description:"List the Storage tables in the open account on the canvas (read-only). Opens the Tables view. Requires an account to be open first (storage_open_account).",inputSchema:{type:"object",properties:{account:{type:"string"}}},handler:async({input})=>post(server,"/list-tables",{account:input.account})},{name:"storage_open_table",description:"Open one Storage table on the canvas and list its entities live (read-only). Pass the exact table `name` from storage_list_tables. Requires an account to be open first.",inputSchema:{type:"object",properties:{table:{type:"string"},account:{type:"string"}},required:["table"]},handler:async({input})=>post(server,"/open-table",{table:input.table,account:input.account})},{name:"storage_list_shares",description:"List the Azure File shares in the open account on the canvas (read-only). Opens the File shares view. Requires an account to be open first (storage_open_account).",inputSchema:{type:"object",properties:{account:{type:"string"}}},handler:async({input})=>post(server,"/list-shares",{account:input.account})},{name:"storage_open_share",description:"Open one Azure File share directory on the canvas and list its files and directories live (read-only). Pass the exact share `name` from storage_list_shares and optional share-relative dir; omit dir for root. Requires an account to be open first.",inputSchema:{type:"object",properties:{share:{type:"string"},account:{type:"string"},dir:{type:"string"}},required:["share"]},handler:async({input})=>post(server,"/open-share",{share:input.share,account:input.account,dir:input.dir||""})},{name:"log_message",description:"Append a message to the canvas activity log visible to the user.",inputSchema:{type:"object",properties:{message:{type:"string"}},required:["message"]},handler:({input})=>(server.log(input.message),{ok:!0})}],open(){return server.loadSubscriptions().catch(()=>{}),{url:`http://127.0.0.1:${server.port}`,title:"Azure Storage",status:"ready"}},onClose(){server.close?.()}});session=await joinSession({canvases:[canvas]}); diff --git a/canvases/azure-storage/com.github.copilot/extensions/azure-storage/studio-package.json b/canvases/azure-storage/com.github.copilot/extensions/azure-storage/studio-package.json index fd90a0b..3a1cce6 100644 --- a/canvases/azure-storage/com.github.copilot/extensions/azure-storage/studio-package.json +++ b/canvases/azure-storage/com.github.copilot/extensions/azure-storage/studio-package.json @@ -1,4 +1,4 @@ { "name": "azure-storage", - "version": "0.3.0" + "version": "0.3.2" } diff --git a/canvases/azure-storage/package.json b/canvases/azure-storage/package.json index 4945275..0b23126 100644 --- a/canvases/azure-storage/package.json +++ b/canvases/azure-storage/package.json @@ -1,6 +1,6 @@ { "name": "azure-storage", - "version": "0.3.0", + "version": "0.3.2", "private": true, "type": "module", "main": "com.github.copilot/extensions/azure-storage/extension.mjs", diff --git a/canvases/azure-storage/release.json b/canvases/azure-storage/release.json index b4de24c..4665812 100644 --- a/canvases/azure-storage/release.json +++ b/canvases/azure-storage/release.json @@ -2,7 +2,7 @@ "schemaVersion": 2, "format": "plugin", "name": "azure-storage", - "version": "0.3.0", + "version": "0.3.2", "mutableDocumentation": true, "publicationRepository": "microsoft/azure-dev-tools", "plugin": { @@ -49,8 +49,8 @@ }, "build": { "esbuild": "0.25.12", - "lockSha256": "45c8ab04053992765129382fdb4fa2fab86666fcbb1303f59032d6eb39b8edaa", - "inputSha256": "06b977f1268eebbc7e41b7a4a570c0530c44beae82601fb3689a3dfe2499aa7b" + "lockSha256": "2e1e8312ac618fc822ed8ae6f6c836b0a8939417db3b291459d9bacb9f3abcf7", + "inputSha256": "96237f35544ee31a4069222b7c14e09354250668be0bcd9999e96cb63f558f5b" }, "dependencies": [ { diff --git a/canvases/azure-storage/skills/azure-storage/SKILL.md b/canvases/azure-storage/skills/azure-storage/SKILL.md index b57c46d..adbcd46 100644 --- a/canvases/azure-storage/skills/azure-storage/SKILL.md +++ b/canvases/azure-storage/skills/azure-storage/SKILL.md @@ -1,60 +1,60 @@ ---- -name: azure-storage -description: Open Azure Storage canvas first to browse storage accounts, containers, blobs, queues, tables and file shares, run bulk transfers with live progress, filter with statistics, mint SAS links, and plan account to account copies. Not for provisioning storage accounts or infrastructure as code. ---- - -# Open Azure Storage canvas - -Use this skill to explore and operate Azure Storage interactively. It opens a live canvas the agent and the user share, so you can drive the same controls the user sees. - -When this skill is selected: - -1. Immediately call `open_canvas` with: - - `canvasId`: `azure-storage` - - `instanceId`: `azure-storage` -2. Reuse that instance ID so a later matching prompt focuses the existing canvas instead of opening duplicate panels. -3. Do this even if the current session already has related storage code or another repo that looks close to the request. -4. Do not replace the canvas with a generic explanation or a generic Azure coding session. Open the Azure Storage canvas first for browsing, transfers, filtering, SAS links, and cleanup. -5. Tell the user the canvas is open and guide them to sign in, pick a subscription, and open a storage account. -6. Leave storage account provisioning and infrastructure changes to the general Azure skills unless the user explicitly asks for that work after the handoff. - -If `open_canvas` reports that the canvas is not registered or unavailable: - -1. Inspect the host's plugin and extension status. The complete plugin declares its provider for native discovery; do not bootstrap a second source folder provider. -2. If the plugin is enabled, call `extensions_reload`. -3. Retry `open_canvas` once with the same canvas and instance IDs. - -If the retry still fails, use these public installation paths, then stop: - -1. If **Azure Storage** is listed in the **Azure Dev Tools** marketplace, add - `microsoft/azure-dev-tools` (marketplace ID `azure-dev-tools`) in - **Customize → Plugins → marketplace gear** and install the full - `azure-storage` plugin. The CLI equivalent is: - -```bash -copilot plugin marketplace add microsoft/azure-dev-tools -copilot plugin install azure-storage@azure-dev-tools -``` - -2. If the full plugin is unavailable, use the - [canvas-only installation URL](https://github.com/microsoft/azure-dev-tools/tree/azure-storage-latest/canvases/azure-storage/com.github.copilot/extensions/azure-storage) - only when published. This path does not install the routing skill; open - **Azure Storage** from installed canvases afterward. The - [customer README](https://github.com/microsoft/azure-dev-tools/blob/azure-storage-latest/canvases/azure-storage/README.md) - has the setup steps. -3. If neither public path is available, report that and stop. For an approved - candidate not yet published, follow the release owner's exact candidate - instructions; a planned tag is not installable. - -Preserve existing installations and user state. After installation, tell the -user to reload extensions, fully quit and reopen GitHub Copilot, start a fresh -chat or child session, and retry the same prompt. Do not silently fall back to -an unrelated generic storage workflow. - -## Live mode prerequisite - -Live mode requires Azure CLI and an existing `az login`. The canvas uses your own Azure sign in to read storage; it never starts Azure sign in for you. - -## Write safety - -Every mutating action (delete, archive, upload, container create or delete, SAS mint) runs only from an explicit user action on the canvas. Account to account transfer stops at a plan plus the `azcopy` command. Do not perform writes on the user's behalf without that explicit action. +--- +name: azure-storage +description: Open Azure Storage canvas first to browse storage accounts, containers, blobs, queues, tables and file shares, run bulk transfers with live progress, filter with statistics, mint SAS links, and plan account to account copies. Not for provisioning storage accounts or infrastructure as code. +--- + +# Open Azure Storage canvas + +Use this skill to explore and operate Azure Storage interactively. It opens a live canvas the agent and the user share, so you can drive the same controls the user sees. + +When this skill is selected: + +1. Immediately call `open_canvas` with: + - `canvasId`: `azure-storage` + - `instanceId`: `azure-storage` +2. Reuse that instance ID so a later matching prompt focuses the existing canvas instead of opening duplicate panels. +3. Do this even if the current session already has related storage code or another repo that looks close to the request. +4. Do not replace the canvas with a generic explanation or a generic Azure coding session. Open the Azure Storage canvas first for browsing, transfers, filtering, SAS links, and cleanup. +5. Tell the user the canvas is open and guide them to sign in, pick a subscription, and open a storage account. +6. Leave storage account provisioning and infrastructure changes to the general Azure skills unless the user explicitly asks for that work after the handoff. + +If `open_canvas` reports that the canvas is not registered or unavailable: + +1. Inspect the host's plugin and extension status. The complete plugin declares its provider for native discovery; do not bootstrap a second source folder provider. +2. If the plugin is enabled, call `extensions_reload`. +3. Retry `open_canvas` once with the same canvas and instance IDs. + +If the retry still fails, use these public installation paths, then stop: + +1. If **Azure Storage** is listed in the **Azure Dev Tools** marketplace, add + `microsoft/azure-dev-tools` (marketplace ID `azure-dev-tools`) in + **Customize → Plugins → marketplace gear** and install the full + `azure-storage` plugin. The CLI equivalent is: + +```bash +copilot plugin marketplace add microsoft/azure-dev-tools +copilot plugin install azure-storage@azure-dev-tools +``` + +2. If the full plugin is unavailable, use the + [canvas-only installation URL](https://github.com/microsoft/azure-dev-tools/tree/azure-storage-latest/canvases/azure-storage/com.github.copilot/extensions/azure-storage) + only when published. This path does not install the routing skill; open + **Azure Storage** from installed canvases afterward. The + [customer README](https://github.com/microsoft/azure-dev-tools/blob/azure-storage-latest/canvases/azure-storage/README.md) + has the setup steps. +3. If neither public path is available, report that and stop. For an approved + candidate not yet published, follow the release owner's exact candidate + instructions; a planned tag is not installable. + +Preserve existing installations and user state. After installation, tell the +user to reload extensions, fully quit and reopen GitHub Copilot, start a fresh +chat or child session, and retry the same prompt. Do not silently fall back to +an unrelated generic storage workflow. + +## Live mode prerequisite + +Live mode requires Azure CLI and an existing `az login`. The canvas uses your own Azure sign in to read storage; it never starts Azure sign in for you. + +## Write safety + +Every mutating action (delete, archive, upload, container create or delete, SAS mint) runs only from an explicit user action on the canvas. Account to account transfer stops at a plan plus the `azcopy` command. Do not perform writes on the user's behalf without that explicit action.