Compare commits

...

3 Commits

Author SHA1 Message Date
Oliver Kieselbach f8743dcae0 bump pointer: internal (B-3gb done)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:24:21 +01:00
Oliver Kieselbach bd94382edb Web: panel heads without nested buttons, session page fits a phone
The Download and Install heads were one button holding the skipped, pending
and no-download toggles: invalid HTML that hid them from screen readers. The
head is now a div that takes the mouse click, so the whole area still toggles,
with the chevron as the real button (ProgressPanelHeader). The tab order
changes: the panel toggle now follows the pills.

Community submissions had the same flaw with span role=button, which the
keyboard could not reach at all. Title, Show all and Pending only are sibling
buttons now.

At 390 px the session page ran up to 630 px wide. The event type, the event
message and the install error detail hold long words without a break point;
the event row wraps now and those texts break inside a word. Desktop rows stay
on one line. The clone between the two panel heads is gone from the baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:24:21 +01:00
dependabot[bot] 8abd3a8848 chore(deps): bump the actions group with 6 updates (#195)
Bumps the actions group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) | `0.6.4` | `0.8.0` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.1` | `7.0.2` |
| [actions/download-artifact](https://github.com/actions/download-artifact) | `8.0.1` | `8.0.2` |
| [actions/setup-node](https://github.com/actions/setup-node) | `7.0.0` | `7.1.0` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.38.2` | `4.39.1` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.38.2` | `4.39.1` |


Updates `zizmorcore/zizmor-action` from 0.6.4 to 0.8.0
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/cc914d7f3750a2d13d75c7f184a1060aa0e9d482...53290badfa369cb0bf7ad16342cb62c2fa47ac7b)

Updates `actions/upload-artifact` from 7.0.1 to 7.0.2
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/043fb46d1a93c77aae656e7c1c64a875d1fc6a0a...cf430e030ddbb5b0abf93d22962f4752f3646cd9)

Updates `actions/download-artifact` from 8.0.1 to 8.0.2
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c...9000827ccba6bdab643e8b6fd33ac0654aef8333)

Updates `actions/setup-node` from 7.0.0 to 7.1.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/820762786026740c76f36085b0efc47a31fe5020...949feb2413d6458794dcd2491c4babbbce0c15c1)

Updates `github/codeql-action/init` from 4.38.2 to 4.39.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...9b3254085494b55a0f7c4c2b3d2d52cc712ce370)

Updates `github/codeql-action/analyze` from 4.38.2 to 4.39.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...9b3254085494b55a0f7c4c2b3d2d52cc712ce370)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/download-artifact
  dependency-version: 8.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/setup-node
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.39.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.39.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-02 16:46:27 +01:00
17 changed files with 298 additions and 216 deletions
+1 -1
View File
@@ -105,7 +105,7 @@ jobs:
# the local install so the gate and the pre-commit run agree on the finding set.
# Informational findings (template expansion of our own step outputs in pwsh) are
# visible locally but do not fail the gate.
- uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
- uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0
with:
version: 1.30.1
min-severity: low
+4 -4
View File
@@ -247,7 +247,7 @@ jobs:
Get-ChildItem $outDir | ForEach-Object { Write-Host "$($_.Name): $([math]::Round($_.Length / 1MB, 2)) MB" }
- name: Upload unsigned build output
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: agent-unsigned
path: ${{ runner.temp }}/agent-unsigned
@@ -293,7 +293,7 @@ jobs:
run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
- name: Download unsigned build output
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: agent-unsigned
path: ${{ runner.temp }}/agent-unsigned
@@ -790,13 +790,13 @@ jobs:
staging/agent/AutopilotMonitor.SummaryDialog.exe
- name: Upload build artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: AutopilotMonitor-Agent-v${{ needs.reserve.outputs.version }}
path: ${{ steps.artifact.outputs.versioned_zip_release }}
- name: Upload version metadata
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: version-metadata
path: ${{ env.MANIFEST_NAME }}
+6 -6
View File
@@ -127,7 +127,7 @@ jobs:
Write-Host "$env:RUNNER_SCRIPT : Valid, timestamped, signer $($sig.SignerCertificate.Subject.Split(',')[0])"
- name: Upload signed loader
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: bootstrap-loader-signed
path: ${{ env.RUNNER_SCRIPT }}
@@ -148,7 +148,7 @@ jobs:
persist-credentials: false
- name: Download signed loader
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: bootstrap-loader-signed
path: ${{ runner.temp }}/loader
@@ -187,7 +187,7 @@ jobs:
Write-Host "Built $env:STABLE_MSI ($([math]::Round((Get-Item $env:STABLE_MSI).Length / 1KB)) KB)"
- name: Upload unsigned MSI
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: bootstrap-msi-unsigned
path: ${{ env.STABLE_MSI }}
@@ -215,7 +215,7 @@ jobs:
persist-credentials: false
- name: Download unsigned MSI
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: bootstrap-msi-unsigned
path: ${{ runner.temp }}/msi
@@ -310,7 +310,7 @@ jobs:
"version=$version" >> $env:GITHUB_OUTPUT
- name: Download unsigned MSI
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: bootstrap-msi-unsigned
path: ${{ runner.temp }}/msi
@@ -442,7 +442,7 @@ jobs:
subject-path: ${{ env.STABLE_MSI }}
- name: Upload build artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: AutopilotMonitor-Bootstrap-v${{ steps.version.outputs.version }}
path: ${{ env.STABLE_MSI }}
+5 -5
View File
@@ -134,7 +134,7 @@ jobs:
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22'
cache: npm
@@ -176,7 +176,7 @@ jobs:
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22'
cache: npm
@@ -222,7 +222,7 @@ jobs:
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22'
@@ -245,7 +245,7 @@ jobs:
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22'
@@ -276,7 +276,7 @@ jobs:
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22'
cache: npm
+2 -2
View File
@@ -112,7 +112,7 @@ jobs:
dotnet-version: '10.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -123,6 +123,6 @@ jobs:
run: dotnet build AutopilotMonitor.sln --configuration Release
- name: Analyze
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with:
category: /language:${{ matrix.language }}
+2 -2
View File
@@ -32,7 +32,7 @@ jobs:
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22'
@@ -66,7 +66,7 @@ jobs:
# `git push` needs.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22'
+2 -2
View File
@@ -99,7 +99,7 @@ jobs:
run: tar -czf "$RUNNER_TEMP/backend-publish.tgz" -C publish .
- name: Upload publish output
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: backend-publish
path: ${{ runner.temp }}/backend-publish.tgz
@@ -128,7 +128,7 @@ jobs:
run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
- name: Download publish output
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: backend-publish
path: ${{ runner.temp }}/backend-artifact
+4 -4
View File
@@ -74,7 +74,7 @@ jobs:
id: docs
run: echo "sha=$(git -C whats-new-docs rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '22'
cache: npm
@@ -149,7 +149,7 @@ jobs:
sha256sum "$archive"
- name: Upload build artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: portal-${{ github.sha }}
path: ${{ steps.package.outputs.archive }}
@@ -170,7 +170,7 @@ jobs:
persist-credentials: false
- name: Download build artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: portal-${{ github.sha }}
path: ${{ runner.temp }}/portal
@@ -215,7 +215,7 @@ jobs:
run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
- name: Download build artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: portal-${{ github.sha }}
path: ${{ runner.temp }}/portal
+1 -1
View File
@@ -150,7 +150,7 @@ jobs:
# when someone wants to look at the file.
- name: Upload signed scripts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: signed-scripts
path: ${{ runner.temp }}/signed-scripts
@@ -606,7 +606,9 @@ function EventRow({ event, showScriptOutput, prevEvent, clockDeltas }: { event:
<div id={`event-${event.eventId}`} className="bg-gray-50 rounded-lg p-3 hover:bg-gray-100 transition-colors">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3">
{/* flex-wrap: on a phone the event type, one long word, drops below the time and badges
instead of pushing the whole page sideways. */}
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
<span className="text-xs text-gray-500 font-mono">
{(recordedAt ?? new Date(event.timestamp)).toLocaleTimeString()}
{recordedAt && (
@@ -632,9 +634,9 @@ function EventRow({ event, showScriptOutput, prevEvent, clockDeltas }: { event:
⧖ Clamped
</span>
)}
<span className="text-sm font-medium text-gray-900">{event.eventType}</span>
<span className="text-sm font-medium text-gray-900 min-w-0 break-words">{event.eventType}</span>
</div>
<p className="mt-1 text-sm text-gray-600" title={event.message || undefined}>{shortenBuildHashInMessage(event.message)}</p>
<p className="mt-1 text-sm text-gray-600 break-words" title={event.message || undefined}>{shortenBuildHashInMessage(event.message)}</p>
{laterCorrection && (
<p className="mt-0.5 text-xs text-gray-500">
{laterCorrection === "foreign"
@@ -5,6 +5,7 @@ import { partitionHistoricReplayEvents, type ReplayInputEvent } from "@/lib/hist
import TruncatedLabel from "@/components/TruncatedLabel";
import PendingAppRow from "@/components/PendingAppRow";
import PhaseDivider from "@/components/PhaseDivider";
import ProgressPanelHeader from "@/components/ProgressPanelHeader";
import AssignmentPill from "@/components/AssignmentPill";
import { userPhaseSplitIndex, type UserPhaseBoundary } from "@/lib/userPhaseBoundary";
import { shouldSkipLowBytesTotal, shouldSkipNoActivity, hasByteActivity } from "@/lib/downloadProgressFilters";
@@ -369,104 +370,98 @@ export default function DownloadProgress({ events, summaryStats, observedUntilMs
return (
<div className="bg-white shadow rounded-lg p-6 mb-6">
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center justify-between w-full text-left"
>
{/* flex-wrap + gap (not space-x): on narrow screens the pills wrap as whole units
below the title instead of overflowing the viewport; whitespace-nowrap inherits
so no pill breaks internally. */}
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 min-w-0 whitespace-nowrap">
<ProgressPanelHeader
title="Download Progress"
expanded={expanded}
onToggle={() => setExpanded(!expanded)}
icon={
<svg className="w-5 h-5 text-blue-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
<h2 className="text-lg font-semibold text-gray-900">Download Progress</h2>
{totalDownloadable != null && downloadedCount != null ? (
}
>
{totalDownloadable != null && downloadedCount != null ? (
<span
className="text-xs text-gray-400"
title="Install assignments only — skipped apps and uninstall downloads are not counted here."
>
({downloadedCount} of {totalDownloadable} downloaded)
</span>
) : (
<span className="text-xs text-gray-400">({downloads.length} {downloads.length === 1 ? 'download' : 'downloads'})</span>
)}
{totalDuration != null && (
<span className="text-xs text-gray-400">
— Total: {formatDuration(Math.floor(totalDuration / 1000))}
</span>
)}
{totalP2P != null && (
<span
className="text-xs text-gray-400"
title={`${formatBytes(totalP2P.peers)} from peers of ${formatBytes(totalP2P.total)} total`}
>
— P2P: {totalP2P.percent}%
</span>
)}
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
{activeCount > 0 && (
<span className="px-2 py-0.5 rounded-full bg-blue-100 text-blue-700 font-medium">
{activeCount} active
</span>
)}
{completedCount > 0 && (
<span className="px-2 py-0.5 rounded-full bg-green-100 text-green-700 font-medium">
{completedCount} completed
</span>
)}
{incompleteCount > 0 && (
// Slate like the session-level Incomplete badge: unknown outcome, not a failure.
<span
className="text-xs text-gray-400"
title="Install assignments only — skipped apps and uninstall downloads are not counted here."
className="px-2 py-0.5 rounded-full bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 font-medium"
title={frozenText.countTitle}
>
({downloadedCount} of {totalDownloadable} downloaded)
</span>
) : (
<span className="text-xs text-gray-400">({downloads.length} {downloads.length === 1 ? 'download' : 'downloads'})</span>
)}
{totalDuration != null && (
<span className="text-xs text-gray-400">
— Total: {formatDuration(Math.floor(totalDuration / 1000))}
{incompleteCount} {frozenText.countLabel}
</span>
)}
{totalP2P != null && (
{uninstallCount > 0 && (
// Muted gray on purpose (dark-mode covered by the global gray overrides) —
// it's context, not a state the user needs to act on.
<span
className="text-xs text-gray-400"
title={`${formatBytes(totalP2P.peers)} from peers of ${formatBytes(totalP2P.total)} total`}
className="px-2 py-0.5 rounded-full bg-gray-100 text-gray-600 font-medium"
title="Package content downloaded for uninstall assignments — Win32 uninstalls re-download the full package before removing the app."
>
— P2P: {totalP2P.percent}%
{uninstallCount} uninstall
</span>
)}
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
{activeCount > 0 && (
<span className="px-2 py-0.5 rounded-full bg-blue-100 text-blue-700 font-medium">
{activeCount} active
</span>
)}
{completedCount > 0 && (
<span className="px-2 py-0.5 rounded-full bg-green-100 text-green-700 font-medium">
{completedCount} completed
</span>
)}
{incompleteCount > 0 && (
// Slate like the session-level Incomplete badge: unknown outcome, not a failure.
<span
className="px-2 py-0.5 rounded-full bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 font-medium"
title={frozenText.countTitle}
>
{incompleteCount} {frozenText.countLabel}
</span>
)}
{uninstallCount > 0 && (
// Muted gray on purpose (dark-mode covered by the global gray overrides) —
// it's context, not a state the user needs to act on.
<span
className="px-2 py-0.5 rounded-full bg-gray-100 text-gray-600 font-medium"
title="Package content downloaded for uninstall assignments — Win32 uninstalls re-download the full package before removing the app."
>
{uninstallCount} uninstall
</span>
)}
{skippedCount > 0 && (
<button
onClick={(e) => { e.stopPropagation(); setShowSkipped(!showSkipped); }}
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showSkipped ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
title={showSkipped ? "Hide skipped apps" : "Show skipped apps"}
>
{skippedCount} skipped {showSkipped ? "▾" : "▸"}
</button>
)}
{noDownloadCount > 0 && (
<button
onClick={(e) => { e.stopPropagation(); setShowNoDownload(!showNoDownload); }}
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showNoDownload ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
title="Assignments the management extension reported without downloading any package content — typically Store/WinGet uninstalls and skips."
>
{noDownloadCount} no download {showNoDownload ? "▾" : "▸"}
</button>
)}
{pendingNames.length > 0 && (
<button
onClick={(e) => { e.stopPropagation(); setShowPending(!showPending); }}
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showPending ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
title="Assigned to this device but not picked up by the management extension yet — no download activity has been observed."
>
{pendingNames.length} pending {showPending ? "▾" : "▸"}
</button>
)}
</div>
{skippedCount > 0 && (
<button
onClick={(e) => { e.stopPropagation(); setShowSkipped(!showSkipped); }}
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showSkipped ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
title={showSkipped ? "Hide skipped apps" : "Show skipped apps"}
>
{skippedCount} skipped {showSkipped ? "▾" : "▸"}
</button>
)}
{noDownloadCount > 0 && (
<button
onClick={(e) => { e.stopPropagation(); setShowNoDownload(!showNoDownload); }}
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showNoDownload ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
title="Assignments the management extension reported without downloading any package content — typically Store/WinGet uninstalls and skips."
>
{noDownloadCount} no download {showNoDownload ? "▾" : "▸"}
</button>
)}
{pendingNames.length > 0 && (
<button
onClick={(e) => { e.stopPropagation(); setShowPending(!showPending); }}
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showPending ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
title="Assigned to this device but not picked up by the management extension yet — no download activity has been observed."
>
{pendingNames.length} pending {showPending ? "▾" : "▸"}
</button>
)}
</div>
<svg className={`w-5 h-5 text-gray-400 flex-shrink-0 transition-transform duration-200 ${expanded ? 'rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</ProgressPanelHeader>
{expanded && <div className="space-y-3 mt-4">
{userPhaseBoundary && userSplit > 0 && <PhaseDivider boundary={userPhaseBoundary} side="before" />}
@@ -9,6 +9,7 @@ import TruncatedLabel from "@/components/TruncatedLabel";
import { InstallSourcePill } from "@/components/InstallSourcePill";
import PendingAppRow from "@/components/PendingAppRow";
import PhaseDivider from "@/components/PhaseDivider";
import ProgressPanelHeader from "@/components/ProgressPanelHeader";
import AssignmentPill from "@/components/AssignmentPill";
import { userPhaseSplitIndex, type UserPhaseBoundary } from "@/lib/userPhaseBoundary";
@@ -158,100 +159,94 @@ export default function InstallProgress({ events, summaryStats, observedUntilMs
return (
<div className="bg-white shadow rounded-lg p-6 mb-6">
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center justify-between w-full text-left"
>
{/* flex-wrap + gap (not space-x): on narrow screens the pills wrap as whole units
below the title instead of overflowing the viewport; whitespace-nowrap inherits
so no pill breaks internally. */}
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 min-w-0 whitespace-nowrap">
<ProgressPanelHeader
title="Install Progress"
expanded={expanded}
onToggle={() => setExpanded(!expanded)}
icon={
<svg className="w-5 h-5 text-indigo-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
<h2 className="text-lg font-semibold text-gray-900">Install Progress</h2>
{totalFromSummary != null && installedFromSummary != null && (
<span className="text-xs text-gray-400">
({installedFromSummary} of {totalFromSummary} installed)
}
>
{totalFromSummary != null && installedFromSummary != null && (
<span className="text-xs text-gray-400">
({installedFromSummary} of {totalFromSummary} installed)
</span>
)}
{totalDuration != null && (
<span className="text-xs text-gray-400">
— Total: {formatDuration(totalDuration)}
</span>
)}
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
{activeCount > 0 && (
<span className="px-2 py-0.5 rounded-full bg-indigo-100 text-indigo-700 font-medium">
{activeCount} active
</span>
)}
{totalDuration != null && (
<span className="text-xs text-gray-400">
— Total: {formatDuration(totalDuration)}
{completedCount > 0 && (
<span className="px-2 py-0.5 rounded-full bg-green-100 text-green-700 font-medium">
{completedCount} completed
</span>
)}
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
{activeCount > 0 && (
<span className="px-2 py-0.5 rounded-full bg-indigo-100 text-indigo-700 font-medium">
{activeCount} active
</span>
)}
{completedCount > 0 && (
<span className="px-2 py-0.5 rounded-full bg-green-100 text-green-700 font-medium">
{completedCount} completed
</span>
)}
{uninstalledCount > 0 && (
// Deliberately muted (gray family, dark-mode covered by the global overrides) —
// a saturated pill here pulls the eye to what is a successful non-event.
<span
className="px-2 py-0.5 rounded-full bg-gray-100 text-gray-600 font-medium"
title="Uninstall assignments enforced during this enrollment — the app was removed, not installed."
>
{uninstalledCount} uninstalled
</span>
)}
{failedCount > 0 && (
<span className="px-2 py-0.5 rounded-full bg-red-100 text-red-700 font-medium">
{failedCount} failed
</span>
)}
{likelyStuckCount > 0 && (
<span
className="px-2 py-0.5 rounded-full bg-orange-100 text-orange-700 font-medium"
title="ESP timed out while these apps were still installing — final status couldn't be confirmed."
>
{likelyStuckCount} likely stuck
</span>
)}
{incompleteCount > 0 && (
// Slate like the session-level Incomplete badge: unknown outcome, not a failure.
<span
className="px-2 py-0.5 rounded-full bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 font-medium"
title={frozenText.countTitle}
>
{incompleteCount} {frozenText.countLabel}
</span>
)}
{postponedCount > 0 && (
<span className="px-2 py-0.5 rounded-full bg-amber-100 text-amber-700 font-medium">
{postponedCount} postponed
</span>
)}
{skippedCount > 0 && (
<button
onClick={(e) => { e.stopPropagation(); setShowSkipped(!showSkipped); }}
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showSkipped ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
title={showSkipped ? "Hide skipped apps" : "Show skipped apps"}
>
{skippedCount} skipped {showSkipped ? "▾" : "▸"}
</button>
)}
{pendingNames.length > 0 && (
<button
onClick={(e) => { e.stopPropagation(); setShowPending(!showPending); }}
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showPending ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
title="Assigned to this device but not picked up by the management extension yet — no install activity has been observed."
>
{pendingNames.length} pending {showPending ? "▾" : "▸"}
</button>
)}
</div>
{uninstalledCount > 0 && (
// Deliberately muted (gray family, dark-mode covered by the global overrides) —
// a saturated pill here pulls the eye to what is a successful non-event.
<span
className="px-2 py-0.5 rounded-full bg-gray-100 text-gray-600 font-medium"
title="Uninstall assignments enforced during this enrollment — the app was removed, not installed."
>
{uninstalledCount} uninstalled
</span>
)}
{failedCount > 0 && (
<span className="px-2 py-0.5 rounded-full bg-red-100 text-red-700 font-medium">
{failedCount} failed
</span>
)}
{likelyStuckCount > 0 && (
<span
className="px-2 py-0.5 rounded-full bg-orange-100 text-orange-700 font-medium"
title="ESP timed out while these apps were still installing — final status couldn't be confirmed."
>
{likelyStuckCount} likely stuck
</span>
)}
{incompleteCount > 0 && (
// Slate like the session-level Incomplete badge: unknown outcome, not a failure.
<span
className="px-2 py-0.5 rounded-full bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 font-medium"
title={frozenText.countTitle}
>
{incompleteCount} {frozenText.countLabel}
</span>
)}
{postponedCount > 0 && (
<span className="px-2 py-0.5 rounded-full bg-amber-100 text-amber-700 font-medium">
{postponedCount} postponed
</span>
)}
{skippedCount > 0 && (
<button
onClick={(e) => { e.stopPropagation(); setShowSkipped(!showSkipped); }}
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showSkipped ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
title={showSkipped ? "Hide skipped apps" : "Show skipped apps"}
>
{skippedCount} skipped {showSkipped ? "▾" : "▸"}
</button>
)}
{pendingNames.length > 0 && (
<button
onClick={(e) => { e.stopPropagation(); setShowPending(!showPending); }}
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showPending ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
title="Assigned to this device but not picked up by the management extension yet — no install activity has been observed."
>
{pendingNames.length} pending {showPending ? "▾" : "▸"}
</button>
)}
</div>
<svg className={`w-5 h-5 text-gray-400 flex-shrink-0 transition-transform duration-200 ${expanded ? 'rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</ProgressPanelHeader>
{expanded && <div className="space-y-3 mt-4">
{userPhaseBoundary && userSplit > 0 && <PhaseDivider boundary={userPhaseBoundary} side="before" />}
@@ -538,7 +533,7 @@ function InstallItemRow({ item, agentSilent }: { item: InstallItem; agentSilent:
);
})()}
{item.isError && item.errorDetail && !item.isLikelyStuck && (
<div className="text-xs text-red-600">{item.errorDetail}</div>
<div className="text-xs text-red-600 break-words">{item.errorDetail}</div>
)}
{item.isLikelyStuck && (
<div className="text-xs text-orange-700">
@@ -0,0 +1,43 @@
"use client";
import type { ReactNode } from "react";
interface ProgressPanelHeaderProps {
title: string;
icon: ReactNode;
expanded: boolean;
onToggle: () => void;
// Summary text and pills after the title. May hold buttons of their own (the skipped and
// pending toggles), which call stopPropagation so they do not also collapse the panel.
children?: ReactNode;
}
/**
* Collapsible head of the Download and Install panels. A plain div takes the mouse click so the
* whole head stays a click target, and only the chevron is a real button: the pills hold buttons,
* and a button inside a button is invalid HTML that hides the inner ones from screen readers.
*/
export default function ProgressPanelHeader({ title, icon, expanded, onToggle, children }: ProgressPanelHeaderProps) {
return (
<div onClick={onToggle} className="flex items-center justify-between w-full text-left cursor-pointer select-none">
{/* flex-wrap + gap (not space-x): on narrow screens the pills wrap as whole units
below the title instead of overflowing the viewport; whitespace-nowrap inherits
so no pill breaks internally. */}
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 min-w-0 whitespace-nowrap">
{icon}
<h2 className="text-lg font-semibold text-gray-900">{title}</h2>
{children}
</div>
<button
onClick={(e) => { e.stopPropagation(); onToggle(); }}
aria-expanded={expanded}
aria-label={title}
className="flex-shrink-0"
>
<svg className={`w-5 h-5 text-gray-400 flex-shrink-0 transition-transform duration-200 ${expanded ? 'rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
);
}
@@ -0,0 +1,43 @@
import { describe, it, expect } from "vitest";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import ProgressPanelHeader from "../ProgressPanelHeader";
const noop = () => {};
function render(expanded: boolean) {
return renderToStaticMarkup(
createElement(
ProgressPanelHeader,
{ title: "Install Progress", icon: createElement("svg"), expanded, onToggle: noop },
createElement("button", null, "3 skipped"),
),
);
}
// Walks the markup and reports whether a button ever opens while another one is still open.
function hasNestedButton(html: string): boolean {
let depth = 0;
for (const [tag] of html.matchAll(/<\/?button\b/g)) {
depth += tag === "<button" ? 1 : -1;
if (depth > 1) return true;
}
return false;
}
describe("ProgressPanelHeader", () => {
it("keeps the pill buttons and the panel toggle as siblings, never nested", () => {
const html = render(true);
expect(html.match(/<button\b/g)?.length).toBe(2);
expect(hasNestedButton(html)).toBe(false);
});
it("names the toggle after the panel and reports its state", () => {
expect(render(true)).toContain('aria-expanded="true" aria-label="Install Progress"');
expect(render(false)).toContain('aria-expanded="false" aria-label="Install Progress"');
});
it("detects a nested button", () => {
expect(hasNestedButton("<button><span><button></button></span></button>")).toBe(true);
});
});
@@ -95,36 +95,41 @@ export function MySubmissionsList({ kind, getAccessToken, refreshKey, overrideTe
return (
<div className="bg-white rounded-lg shadow">
<button
{/* The div takes the mouse click so the whole head stays a click target; the title and the
filter are sibling buttons, because a button inside a button hides the inner one from
the keyboard and from screen readers. */}
<div
onClick={() => setCollapsed((c) => !c)}
className="w-full flex items-center justify-between px-4 py-3 text-left"
className="w-full flex items-center justify-between px-4 py-3 text-left cursor-pointer select-none"
>
<span className="flex items-center gap-2 text-sm font-medium text-gray-900">
<button
onClick={(e) => { e.stopPropagation(); setCollapsed((c) => !c); }}
aria-expanded={!collapsed}
className="flex items-center gap-2 text-sm font-medium text-gray-900 text-left"
>
<svg className={`w-4 h-4 text-gray-400 transition-transform ${collapsed ? "" : "rotate-90"}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
Community submissions
<span className="text-xs font-normal text-gray-500">({items.length}{pending > 0 ? `, ${pending} pending` : ""})</span>
</span>
</button>
{!collapsed && hidden > 0 && (
<span
role="button"
<button
onClick={(e) => { e.stopPropagation(); setShowAll(true); }}
className="text-xs text-indigo-600 hover:text-indigo-800"
>
Show all ({hidden} more)
</span>
</button>
)}
{!collapsed && showAll && items.length > pending && (
<span
role="button"
<button
onClick={(e) => { e.stopPropagation(); setShowAll(false); }}
className="text-xs text-indigo-600 hover:text-indigo-800"
>
Pending only
</span>
</button>
)}
</button>
</div>
{!collapsed && visible.length === 0 && (
<p className="border-t border-gray-100 px-4 py-2 text-xs text-gray-500">Nothing pending.</p>
)}
-1
View File
@@ -17,7 +17,6 @@
"1d7bffc15ccf7436": 1,
"29bbb90f9b9e306a": 1,
"2a985532f2e52066": 1,
"33829a769f2eacb9": 1,
"376b94382a0f2fc3": 1,
"391740d0bd84d73b": 1,
"3a83e25903a9f78b": 1,