2026-05-21 — Dashboard hides Terminal + Sidecar runtime on mobile¶
Motivation¶
On viewports ≤ 760px the dashboard cards wrapped awkwardly and the Terminal and Sidecar runtime panels were not useful at phone width (the user cannot operate the browser terminal on a phone, and the sidecar topology panel is informational only).
User-facing change¶
- Mobile breakpoint (
max-width: 760px): - The "Terminal" card inside the Resource plane is hidden.
- The entire "Sidecar runtime" section is hidden.
- The workspace Settings gear button in the dashboard hero controls is hidden (the Help / Getting Started button stays). Settings are rarely re-opened from a phone, and removing the button cleans up the crowded right edge of the controls row.
- The Getting Started modal no longer auto-opens, and the "?" re-open button is hidden. The checklist is a desktop-only onboarding aid; on a phone it covers the whole viewport and offers no useful action. If the viewport resizes to ≥ 761px in the same session the checklist can open again.
- The Add Cluster CTAs (both the compact header pill on the AKS card and the empty-state dashed "Provision your first cluster" button) are hidden. Mobile is positioned as a read-only status view; provisioning a cluster requires the SetupWizard / Provision modal which is desktop-only.
- Cluster pulse card is compacted for narrow screens:
- The Stop / Delete action buttons next to "Open cluster detail" are hidden. Mobile is read-only; destructive ops stay on desktop.
- Stat labels in the summary row (Submits 15m / Active / Pressure) collapse to just the icon + value so the row fits.
- The 8-cell meta grid (CPU / Mem / GPU / Pods …) reflows from
auto-fit(which produced a single overflowing row) to a fixed 2-column layout so every value stays on-screen. - The Jobs roster table header is hidden and each job row reflows from a 4-column grid to a stacked flex layout (identity on line 1, status + relative time on line 2). The "User" column is hidden because it was almost always empty for personal demos and was the first thing to overflow.
- Resource plane (ACR · Storage) tightens for narrow viewports:
- The Resource plane grid forces a single column so the two cards stack instead of squeezing into ~160 px each at the 760 px cutoff.
- The dashboard section labels (
Cluster plane/Resource plane/Sidecar runtime) shrink to 11 px with tighter margins. - Storage card subtitle drops the
· resourceGroupsuffix so the account name doesn't get truncated by the right-hand controls. - The 4-cell Storage meta grid (Region / SKU / HNS / Network) and the 3-cell ACR summary (Login Server / SKU / Images built) both reflow to two columns. The ACR Login Server cell spans the full width on its own row so the FQDN isn't crushed.
- Storage container rows wrap: the "updated …" relative-time line moves to its own row, leaving the access pill clear of overlap.
- The BLAST Databases sub-section header wraps and hides the redundant "N/M catalog" counter (the "N downloaded" pill already conveys the count).
- Storage warning banners (public endpoint, HNS disabled) use smaller padding and font so they stop dominating the card.
- The "(optional)" suffix on optional ACR image rows is hidden;
the existing
opacity: 0.65already differentiates those rows. - The ACR header Build button and each row's per-image
Build button are hidden on mobile. Mobile is read-only;
kicking off image builds stays on desktop. In-flight banners
(
BuildingBanner/ServerBuildingBanner/BuildDoneBanner) remain visible because they're status info, not actions.
- Desktop / tablet layout is unchanged.
API / IaC diff¶
None — pure frontend layout change.
Files¶
- web/src/pages/Dashboard/DashboardGrid.tsx:
wrap
TerminalCardin adashboard-hide-mobilediv; tag the Sidecar runtime section with the same class. - web/src/pages/Dashboard/DashboardHeader.tsx:
add
dashboard-hide-mobileto the workspace settings gear button and to the Help / Getting Started "?" re-open button. - web/src/pages/Dashboard/useGettingStartedReadiness.ts:
introduce
useIsMobileViewport()(matchMedia(max-width: 760px)), short-circuit the auto-open effect on mobile, force-close the modal if the viewport shrinks while it is open, and makereopenGettingStarteda no-op on mobile. - web/src/components/cards/ClusterCard/AddClusterButton.tsx:
tag both pill and dashed variants with
dashboard-hide-mobile. - web/src/components/cards/ClusterPulse/PulseActions.tsx:
tag the right-side Stop/Delete action group with
dashboard-hide-mobile. - web/src/components/cards/ClusterPulse/atoms.tsx:
split the
PulseStatlabel into its own<span class="pulse-stat-label">so CSS can hide just the label text while keeping the icon and value. - web/src/components/cards/ClusterPulse/PulseMetaGrid.tsx:
tag the grid with
pulse-meta-gridso mobile CSS can overridegrid-template-columnsto a 2-column layout. - web/src/components/cards/ClusterPulse/JobsSection.tsx:
tag
JobsTableHeaderwithpulse-jobs-headerand the user header span withpulse-job-user; tag the skeleton job rows withpulse-job-row pulse-job-skeleton. - web/src/components/cards/ClusterPulse/JobLine.tsx:
tag each rendered job row with
pulse-job-rowand the user cell withpulse-job-user. - web/src/theme/dashboard-layout.css:
add
.dashboard-hide-mobileand a cluster-pulse mobile block inside the existing@media (max-width: 760px)block (label hide, meta grid 2-col, jobs header hide, job row reflow + user-cell hide). Append a Resource plane block that forces 1-col, compacts section labels, reflowsdv3-cell-grid--4/--3to 2 columns with a full-width first cell for ACR Login Server, wraps container rows, wraps the BLAST DB head while hiding the catalog counter, compacts.storage-warningbanners, and hides.acr-optional-tag. - web/src/components/cards/StorageCard.tsx:
wrap the subtitle's
· resourceGroupsuffix in<span class="storage-subtitle-rg">so it can be hidden on mobile while desktop keeps the full identity. - web/src/components/cards/storage/StorageWarnings.tsx:
tag both banner divs with
storage-warning. - web/src/components/cards/AcrCard/AcrImageRow.tsx:
tag the optional-suffix span with
acr-optional-tag. - web/src/components/Layout.css:
reset
.layout__nav { margin-left: 0 }inside the@media (max-width: 1320px)block. Without this, the desktop rule'smargin-left: 16pxleaked into the mobile drawer, sotransform: translateX(-100%)only shifted the drawer by its own width but the 16 px margin pushed its right edge back into the viewport — a visible vertical strip on the left side that covered content while the menu was "closed". Also switch.layoutfrommin-height: 100vhto100dvh(with a100vhfallback) so the bottom of the page is reachable on mobile browsers with retractable address bars. The large-viewport100vhvalue caused the last card to sit behind the iOS Safari / Chrome Android chrome. -
web/src/theme/dashboard-layout.css: inside the
@media (max-width: 760px)block, addpadding-bottom: calc(72px + env(safe-area-inset-bottom, 0px))on.dashboard-workspace. The last visible card on mobile is the Storage card, whoseBlastDbSectionfetches its chip list async — if the user scrolls to the bottom before that fetch resolves, the page height jumps and the new chip rows render right at the visible edge (and in narrow desktop windows, behind the OS taskbar). The extra gutter keeps the last row clear of the browser/OS chrome. -
web/src/components/cards/ClusterPulse/PulseRowSummary.tsx: wrap the three
<PulseStat>(Submits 15m / Active / Pressure) in a<div class="pulse-row-stats">and adjustgridTemplateColumnsaccordingly; tag the API-p95 sub-line<span>withpulse-row-subline. Both are hidden on mobile because the values are duplicated in thePulseMetaGridimmediately below, so the header row was confusing on a phone (three nearly-unlabelled numbers). - web/src/components/ClusterItem/DatabaseChipStrip.tsx:
tag the
warming · ready · failedlegend span withcluster-db-legendso mobile CSS can hide it. Each chip already shows the same colour next to the db name, so the legend is pure noise on a narrow screen. -
web/src/theme/dashboard-layout.css: inside the
@media (max-width: 760px)block, add hide rules for.pulse-row-stats,.pulse-row-subline, and.cluster-db-legend. -
web/src/components/cards/ClusterPulse/JobLine.tsx: add a 3-px phase-coloured left accent border on every job row so the pass/fail state scans vertically without reading the pill; replace the static 7-px phase dot with a
Loader2 spinicon when the job is active so RUNNING/PENDING jobs are visibly live. Tag the status pill (pulse-job-status-pill), the time block (pulse-job-timeblock+pulse-job-timeago/pulse-job-durationchildren) so mobile CSS can bump their fonts. - web/src/components/cards/ClusterPulse/JobsSection.tsx:
tag the "Jobs" eyebrow with
pulse-jobs-labeland append a· Ncount badge when the roster is non-empty; tag the inline caption withpulse-jobs-caption; convert theN unknownandN failed / 15minline spans intopulse-jobs-chipchips so the most actionable signals look like chips on mobile; tag the "+N more" pill withpulse-jobs-more-btnso it can stretch full-width on mobile. The header row now allowsflex-wrap: wrapso the chips drop to their own line on narrow screens. -
web/src/theme/dashboard-layout.css: inside the
@media (max-width: 760px)block, add Jobs-section font bumps and signal promotion: section label 12 px (was 10), caption 11.5 px (was 10), chip background fill for unknown/failed, status pill 11 px + 3×8 padding + 700 weight (was 9 px / 1×5 padding / 600), identity title 13 px (was 11.5), identity meta 11 px (was 10), time-ago 11.5 px bold (was 10), duration 10.5 px (was 9), "More jobs" pill stretches full-width with 12.5 px text and 8 px padding. -
web/src/components/Layout.tsx + web/src/components/Layout.css: mobile nav drawer polish.
- Add a
layout--mobile-nav-openmodifier on the layout root so CSS can drive sibling visibility while the drawer is open. - Inside the
@media (max-width: 1320px)block, hide every topbar child other than the hamburger, logo, and nav while the drawer is open (.layout--mobile-nav-open .layout__topbar > *:not(...)). Previously the cluster-status pill, Live indicator, and the theme / help / account buttons all stayed visible behind the drawer header, which split the user's attention. - Hug-content the active nav item on mobile (
align-self: flex-start) so the highlight pill matches the desktop look instead of stretching the entire drawer width — the active row no longer looks "wider" than the inactive ones. - Strengthen the drawer's top edge (
border-topnow uses--border-medium; add an inset shadow) so the drawer reads as a distinct surface rather than blending into the topbar. - Cluster detail modal becomes a full-screen sheet on mobile and gets a larger close button:
web/src/components/ClusterDetailModal/DetailsModal.tsx— tag the backdrop withcluster-detail-backdrop, the modal root withcluster-detail-dialog, and the body scroll wrapper withcluster-detail-modal-body(desktop sizing is unchanged; the modal stays inline-styled atmaxWidth: 1180, width: calc(100vw - 48px), maxHeight: 92vhwith an inneroverflow-y: autobody).web/src/components/ClusterDetailModal/ModalHeader.tsx— tag the outer header withcluster-detail-modal-header, the stat cards grid (NODES / K8S / POOLS / OS) withcluster-detail-modal-stats, the X button withcluster-detail-close, and addaria-label="Close cluster detail".web/src/components/ClusterDetailModal/NodePoolsTable.tsx— tag the table wrapper withcluster-detail-pools-wrapso it can scroll horizontally on phones (7 columns don't fit).web/src/components/WarmupSection.tsx— tag the DbRow grid withwarmup-db-cardand its action group withwarmup-db-card__actions. The 3-column grid (name | status | actions) was overflowing on phones, leaving the Warm / Release buttons partially visible at the right edge. On mobile the grid collapses to a single column and the action group is hidden — those destructive operations stay on desktop where the user has a proper pointer.web/src/components/ClusterDiagnostics/K8sNodesSection.tsxandK8sPodsSection.tsx— tag the scrollable table wrappers withk8s-nodes-table-wrap/k8s-pods-table-wrap. On mobile the horizontal scroll is dropped and non-essential columns are hidden so the rest fits without a scrollbar: Nodes keeps NAME + STATUS (drops VERSION / IP / OS / RUNTIME); Pods keeps NAME + READY + STATUS + Logs button (drops NS / RESTARTS / NODE). The hidden columns remain in the DOM for desktop and screen readers.K8sPodsSection.tsx— Logs button now usesinline-flexandwhite-space: nowrap(plus ak8s-pods-logs-buttonclass with slightly larger mobile padding / 28px min-height) so the label no longer wraps to "Lo / gs" inside the narrow action column.PodLogsDialog.tsx+ CSS — tag the backdrop / dialog withpod-logs-backdrop/pod-logs-dialogand apply the same mobile fullscreen treatment as the cluster detail modal (position: fixed; inset: 0, 100vw × 100dvh, no border-radius,overscroll-behavior: contain). Previously the Pod Logs modal opened as a narrow centered card pushed up against the cluster detail modal's scroll position, leaving header / close button clipped.dashboard-layout.css— generalized the fullscreen-on-mobile rule to all glass dialogs via.glass-dialog-backdrop > .glass-card(covers BlastDbModal, ConfirmDialog, PodLogsDialog, ClusterDetail DetailsModal, TaxonomyModal, QuerySection example dialog, and any future modal that follows the same backdrop-then-card structure). Per-modal classes (cluster-detail-dialog,pod-logs-dialog, …) now only carry behavior extras (e.g. whole-modal scrolling for the cluster detail). Desktop styling is untouched — the rules live inside the existing@media (max-width: 760px)block.- The same generic rule now also adds
padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 16px)to the card so the last row of any modal's scrollable body isn't hidden behind mobile browser chrome (URL bar) or OS bottom strip (iOS home indicator, Windows taskbar overlapping the DevTools mobile preview). Inlinepadding: 0on each card is overridden because!importantin the stylesheet trumps non-important inline styles. For cluster-detail-dialog this composes with the existing 72px body padding so users can scroll well clear of any browser/OS chrome. - Promoted the page-level bottom safe-area gutter from
.dashboard-workspaceto the universal.layout__mainwrapper (still inside@media (max-width: 760px)). This fixes "last element hidden behind chrome" on every page — including the Run BLAST button at the bottom of the New Search form which was being covered by the Windows taskbar in DevTools mobile preview. The dashboard-specific rule was removed because.layout__mainalready wraps.dashboard-workspaceand applying the same value twice would double-pad. cluster-detail-modal-bodynow carries apadding-bottom: calc(72px + env(safe-area-inset-bottom, 0px))so the last DB / diagnostic card can scroll fully into view above the mobile browser chrome and the iOS / Android home-bar safe area — previously the bottom card was cut off by the OS / browser bottom strip.web/src/theme/dashboard-layout.css(@media (max-width: 760px)):.cluster-detail-backdrop { padding: 0 !important; align-items / justify-items: stretch !important; },.cluster-detail-dialog { position: fixed !important; inset: 0 !important; width: 100vw !important; height: 100dvh !important; max-width / max-height: 100vw / 100dvh !important; min-width: 0 !important; margin: 0 !important; border-radius: 0 !important; overflow-y: auto !important; overflow-x: hidden !important; overscroll-behavior: contain !important; }— theposition: fixed; inset: 0pins the dialog directly to the viewport so the underlying dashboard can never peek through even when the scroll content is shorter than the viewport, andoverscroll-behavior: containblocks the rubber-band that was exposing the dashboard at the bottom edge. Then.cluster-detail-modal-body { overflow: visible !important; flex: initial !important; padding: 12px !important; }so the body is no longer the inner scroll container,.cluster-detail-modal-header { padding: 10px 12px 12px !important; }to compress the header, and.cluster-detail-modal-stats { display: none !important; }to drop the redundant NODES / K8S / POOLS / OS cards (those values still live in the Node Pools table inside the body, and in the cluster card's PulseMetaGrid). Plus.cluster-detail-pools-wrap { overflow-x: auto !important; }, the touch-target promotion of.cluster-detail-close(44×44 with a 22px icon), and.warmup-db-card { grid-template-columns: 1fr !important; }with.warmup-db-card__actions { display: none !important; }so the Warm / Release buttons no longer leak past the right edge on phones. The!importantis intentional because the desktop sizing lives on inlinestyle={…}and we did not want to thread a viewport hook just for this. The selectors are scoped to this modal only.
Validation¶
cd web && npm run build(Vite type-check + bundle).- Visual: open
http://127.0.0.1:8090/in DevTools mobile preview ≤ 760px → Terminal card and Sidecar runtime section are gone; desktop view at ≥ 761px still shows both.
Hardening pass (critical review)¶
After the universal .layout__main safe-area promotion landed, a follow-up
critical review surfaced three regressions / gaps that the generic modal
rule missed:
- Double bottom padding inside the cluster-detail dialog. Once the
dialog itself became the scroll container (via
.cluster-detail-dialog { overflow-y: auto !important }) and the generic.glass-dialog-backdrop > .glass-cardrule started reservingenv(safe-area-inset-bottom) + 16px, the body's pre-existingpadding-bottom: calc(72px + env(safe-area-inset-bottom))stacked on top of that, producing roughly 88px + 2× safe-area of dead space after the last card. Reduced the body topadding-bottom: 24px !importantso the only safe-area calc lives on the dialog (the scroll container) and the inner gutter stays a sensible 24 px. KeyboardShortcuts(?overlay) overflowed phones horizontally. The inner dialog had an inlinewidth: 480(nomin()wrapper), so on any viewport narrower than 480 CSS pixels the modal scrolled the whole page sideways. Tagged the inner div withclassName="shortcut-dialog-card"and added a mobile rule that forces 100vw / 100dvh with the same safe-area gutter as.glass-dialog-backdrop > .glass-card. The backdrop already has.shortcut-overlay, so the only source change is a single new className on the inner card.ProvisionModal("Create AKS Cluster") was a pinched centered card on phones. It's an inline-styled portal so it never participated in the.glass-dialog-backdropcascade. AddedclassName="provision-modal-backdrop"andclassName="provision-modal-card"to its two divs and gave them the same fullscreen + safe-area treatment under the 760 px media query. Desktop styling is untouched because the inlinewidth: "min(760px, calc(100vw - 32px))"still wins outside the media query.
Modals deliberately left as-is:
- SettingsPanel is a side drawer (top: 0; right: 0; bottom: 0;
width: min(520px, calc(100vw - 24px))) with its own visual language,
not a centered modal. On phones the 24 px gutter is intentional
affordance, not a bug.
- SidecarsCard HTTP inspector is a dev/inspect tool surfaced behind
an internal toggle and not part of the daily user surface; deferred.
Hardening validation¶
cd web && npm run build→✓ built in 7.34s, no type errors.- Targets exercised in DevTools mobile preview (375 × 812):
?keyboard shortcut overlay now spans 100vw / 100dvh; the bottom of the "Resources" tab clears the OS strip.- Create AKS Cluster modal spans full width; the SKU group lists no longer require horizontal scroll inside a 760 px-wide cell.
- Cluster detail dialog: scrolling to the bottom of the warmup list lands ~24 px above the last DB card instead of leaving an 88 px empty band.
Recent searches detail header (BlastJobHeader)¶
User reported the Recent searches detail page (/blast/jobs/{id}) was
visibly broken on phones: the long search title overlapped the action
buttons and the metadata grid's right-hand column (Submitted / Molecule
type / Region) was clipped past the viewport edge.
Root causes:
1. The action row was flexWrap: "wrap" with an unconstrained
<h1>{jobTitle}</h1>. Long titles like
20260521-1200 MPXV F3L - NC_003310.1 never wrapped inside the h1,
so the h1 forced its parent wider than the viewport and the
flex: 1 made the buttons share whatever was left over — they ended
up drawn on top of the title.
2. The metadata <dl> used a 4-column grid
(min-content max-content min-content 1fr). On a 360 px-wide screen
the four tracks summed to more than the viewport, so every
right-column value was clipped (only the labels "SUBMITTED",
"MOLECULE", "REGION", "DATE" rendered with no value behind them).
Hardening:
- Added className="blast-job-header" to the header, plus
blast-job-header__title-row and blast-job-header__meta-grid to
the two affected wrappers (source change is three className adds —
no inline-style edits, desktop layout untouched).
- New mobile rules in dashboard-layout.css inside
@media (max-width: 760px):
- h1 → 18 px font, overflow-wrap: anywhere, word-break: break-word
so the search title wraps cleanly instead of overflowing.
- title row → stacks flex-direction: column, action buttons go
full-width and centered so Cancel / Edit search / Save settings
don't fight for inline space.
- meta grid collapses to two columns (max-content 1fr); the
spanning rows (DB title / description / snapshot, query
description) use an attribute selector
dd[style*="span 3"] { grid-column: 1 / -1 !important } so the
inline span 3 doesn't blow the grid out to 3 implicit tracks.
- code values get word-break: break-all so the Search ID and
cluster name wrap rather than push the grid wider.
Validation:
- cd web && npm run build → ✓ built in 11.11s.
- DevTools mobile preview at 375 × 812 on
/blast/jobs/{id}?tab=descriptions: title now wraps to ≤ 2 lines
inside the visible width, buttons stack below it full-width, and
every metadata field (Search ID / Submitted / Program / Database /
Molecule type / Cluster / Region) renders its value without
clipping.
API Reference page (/docs) mobile optimization¶
Motivation¶
The API Reference page (web/src/pages/ApiReference.tsx) was built
desktop-first: a sticky 240-280 px sidebar plus a fluid endpoint
column, a hero row with title + 3-stat chips + baseUrl pill + Swagger
UI button + Refresh button, endpoint cards with a 6-item header (method
badge + path + summary + copy-link + Try + chevron) and an expanded
body split into a 1fr 1fr grid (description/params | try-it). On a
phone the sidebar squeezed the content column to ~80 px, the hero row
overflowed horizontally, endpoint paths wrapped onto the chevron, and
the expanded body became unreadable. User explicitly granted
permission to hide non-essential controls on mobile
("불필요한거는 안보여도될것 같아").
User-facing change¶
On viewports ≤ 760 px the /docs page now:
- hides the sticky
ApiReferenceSidebar(users scroll the tag sections instead); - stacks
ApiHerovertically: title block on top, action buttons below, with the long baseUrl pill and Swagger-UI external link hidden (only Refresh remains useful on a phone); - hides the 3-stat row (Endpoints / Groups / Methods) — redundant vertical noise on small screens;
- collapses each
EndpointCardheader so the path occupies its own full-width line under the method badge, and drops the truncated summary preview and the rarely-used copy-link button; - collapses the expanded body grid from
1fr 1frto a single column (description + parameters first, try-it form below); - collapses each parameter row from
120 px / 60 px / 1frto a single column (name + description stacked), and hides the dedicated "type" cell (thereqbadge in the name cell still marks required params).
Desktop layout is untouched — all rules sit inside
@media (max-width: 760px) in
web/src/theme/dashboard-layout.css, and the only .tsx changes are
new anchor classNames on existing wrappers (no inline styles
removed).
Files changed¶
web/src/pages/ApiReference.tsx— no edits (already hadapi-reference-page+api-reference-layoutclasses).web/src/pages/apiReference/ApiHero.tsx— addedapi-hero__row,api-hero__actions,api-hero__stats,api-hero__base-url,api-hero__swagger.web/src/pages/apiReference/EndpointCard.tsx— addedendpoint-card,endpoint-card__header,endpoint-card__summary,endpoint-card__copylink,endpoint-card__body,endpoint-card__param-row.web/src/theme/dashboard-layout.css— new mobile block under theBlastJobHeaderrules (~80 lines, all inside the existing@media (max-width: 760px)).
Validation¶
cd web && npm run build→✓ built in 7.23s(clean).- Manually inspected the new CSS: every selector is scoped under
@media (max-width: 760px)and uses!importantonly to beat inline desktopstyle={...}props; no rule leaks to desktop.
Recent searches list page — table no longer pushes page out of viewport¶
Motivation¶
On mobile widths the /blast/jobs (Recent BLAST searches) page was
extending past the viewport: the desktop-sized 5-column table inside
each DateGroupSection (Job / User / Status / Time / Delete) used
whiteSpace: nowrap on the User / Status / Time / Delete cells, so
the table's intrinsic min-content exceeded the phone viewport and
pushed the whole page wider than 100 vw, producing horizontal page
scroll and clipping the right edge of the content.
The table already lives inside a .table-scroll wrapper with
overflow-x: auto, which should have contained the overflow as
internal horizontal scroll. It didn't, because .page-stack.jobs-page
is a flex column and flex children default to min-width: auto
(content-based). That let each DateGroupSection's root <div>
inflate to its table's intrinsic width, dragging the page with it.
User-facing change¶
On /blast/jobs at any width, the per-group table now scrolls
horizontally inside its .table-scroll container instead of
expanding the page. On phones the page no longer scrolls sideways;
swiping the table reveals the User / Status / Time / Delete columns.
Desktop layout is unchanged because at wider widths the table
naturally fits inside the row and never needs to scroll.
Files changed¶
web/src/theme/glass.css— addedmin-width: 0to.jobs-pageand.jobs-page > *so flex children can shrink and let.table-scroll's existing horizontal scroll contain the table. Two lines of CSS, no new selectors, no inline-style edits.
Validation¶
cd web && npm run build→✓ built in 11.32s(clean).- Live check at
http://127.0.0.1:8090/blast/jobswith.layout__main { max-width: 390px }injected:documentElement.scrollWidthno longer exceeds the viewport width;.table-scrollreportsclientWidth ≈ 364 px,scrollWidth ≈ 602 px(correctly contained with internal horizontal scroll); table bounding rect (578 px) stays clipped inside.table-scroll.
Follow-up: table content was still clipped behind the internal scroll¶
The first pass only stopped the page from extending — it left the
table itself ~578 px wide inside the now-contained .table-scroll,
so on a phone the Status / Time / Delete columns were hidden behind
the (mostly invisible) internal horizontal scroll. User screenshot
confirmed "COMP…" was clipped at the right edge of the card.
Tightened the table so all visible columns actually fit at 360 px
container width (in dashboard-layout.css @media (max-width: 760px),
.jobs-page block):
- hide the
Usercolumn entirely (header + cells vianth-child(2)) — single-user workspaces always show—here anyway; - shrink TD padding to
4 px, font-size to11 px, drop the first/last cell side padding; - override the inline
white-space: nowrapon Status (nth-child(3)) and Time (nth-child(4)) TDs, and on the Job-title anchor, so cells can shrink — the title now wraps into 2 short lines instead of forcing the column to its intrinsic width; - hide the Time TD's second sub-line ("Duration N m N s"); the headline "X ago" is enough on a phone;
- change
.jobs-page .table-scrolltooverflow-x: hiddenon mobile (no longer needed since the table now fits) and tighten its inner padding to8 px.
Verified via DOM: at injected 390 px viewport, table now 348 px wide
(< 364 px container), scrollWidth === clientWidth on
.table-scroll, every row shows Job + Status + Time + Delete in
view. Build clean (✓ built in 8.04s). Desktop layout unchanged
(rules scoped to @media (max-width: 760px)).
Follow-up #2: Today fit but Yesterday/older sections still overflowed¶
User reported Today rendered correctly but Yesterday (and older
sections) still clipped on mobile. Reproduced by injecting
.layout__main { max-width: 390px }: Today + This Week tables fit
at 348 px (< 364 px container) but Yesterday's table was 502 px.
Root cause: every Yesterday row has a long unbreakable title token
(20260520-2309 MPXV F3L - NC_003310.1) plus the worker_lost /
elb-cluster inline-flex badges in the meta line. Under
table-layout: auto the column's min-content was the widest of
those badges (which themselves are inline-flex single-token boxes
that don't honor word-break), so the Job column refused to shrink
below ~340 px and the whole table inflated to 502 px regardless of
white-space: normal. Today's rows happened to have shorter
titles so they squeezed under the limit by luck.
Switched the table to table-layout: fixed; width: 100% on mobile
and pinned the right-side columns to explicit widths
(Status 78 px, Time 72 px, Delete 32 px). The Job column now
absorbs the remaining ~178 px and the inline-flex badges wrap or
clip inside that column instead of dictating table width.
Verified: all three sections (Today, Yesterday, This Week)
report tableW = 348 px, scrollWidth === clientWidth = 364 px,
no horizontal scroll. Build clean (✓ built in 10.32s). Desktop
layout untouched.
Follow-up #3: BLAST Databases modal leaked the dashboard behind it on mobile¶
User reported that on mobile, opening the BLAST Databases modal from
the dashboard and scrolling exposed the dashboard's Storage card
(results container row, the inline BlastDbSection summary) below
the modal, as if the modal didn't fully cover the viewport.
Root cause: the mobile fullscreen rule used height: 100dvh !important
(plus matching max-height/min-height). On engines that don't
understand the dynamic-viewport unit (dvh), the entire declaration
becomes invalid and is dropped — leaving the card on its inline
maxHeight: 86vh with height: auto, so the card shrinks to its
content and the body behind the backdrop becomes visible underneath.
Fix: drop the explicit height entirely and rely on position: fixed
+ inset: 0 to pin all four edges to the viewport (which gives a
fully-covering box on every engine, no dvh needed), and override
the inline maxHeight with max-height: none !important. The same
treatment is applied to the bespoke modal cards
(.shortcut-dialog-card, .provision-modal-card), where inset: 0
isn't available because they're inline-styled portals, so they get a
100vh fallback declared before 100dvh (older engines apply
100vh, modern engines override with 100dvh).
Verified by injecting the new rule outside @media on the desktop
viewport (1914 × 897): with inset: 0 + max-height: none, the
BlastDb modal card resolves to 0,0 1914×897 — coversFullViewport:
true, dashboard cards no longer leak through. Build clean
(✓ built in 10.85s). Desktop layout unchanged (all rules scoped
to @media (max-width: 760px)).