Commit Graph
6111 Commits
Author SHA1 Message Date
jelveh a1e95068ee Dashboard pill: left-align the title against the icon
The pill is asymmetric — one icon on the left, two buttons on the
right — so a title centered in its fixed box landed off the pill's
true center and read as misaligned. Left-aligned, icon + title
cluster as the identity on the left and the controls cluster on
the right.
2026-07-22 21:39:02 -07:00
jelveh 6d27731a13 Dashboard: restored windows come back with the pill collapsed
The restore re-played the open intro (expand + auto-retract). A restore
should bring back the app, not the chrome — the user already met the
controls on open, and they just USED them to minimize. showWindow now
forces the pill shut instead, which also covers the Back-button
minimize path, where the window hides without touching pill state.
2026-07-22 21:34:27 -07:00
jelveh 37604f50d3 Dashboard: fix minimized apps getting permanently buried under the dashboard
Fullpage/dashboard app windows are created stay_on_top in the
99999999+ z band, but showWindow's restore path demoted them to a
plain counter z-index — and since focusWindow deliberately never
raises stay_on_top windows, the demotion stuck: one dashboard focus
after a minimize/restore buried the app permanently (URL and tab
title still naming an app the user could no longer see). Restore now
re-raises stay-on-top windows into their band.

Also make the control pill focus its window on mousedown, as the
titlebar it replaces did. The document-level activation handler works
off mouseover_window, which only mousemove refreshes — a tap with no
intervening mousemove (touch, restored windows) would activate the
stale window and could re-raise the dashboard over the app. Deferred
a tick so it runs after that handler and wins.
2026-07-22 21:30:19 -07:00
Daniel Salazar e35b57068b test: add extra tests (#3428) 2026-07-22 21:13:44 -07:00
Daniel Salazar 5faed55076 fix: autoclaim app when making a subdomain (#3426) 2026-07-22 21:12:38 -07:00
Daniel Salazar 928d5fec16 chore: cleanup AI module for puter-js (#3423) 2026-07-22 18:18:54 -07:00
velzie ebcc4bb597 Add loopback support to puter.peer (#3425)
* allow setting port instead of invite code in puter.peer

* fix grabbing fallbackice under wrong response
2026-07-22 20:13:35 -04:00
Neal Shah 51541d3739 initial pjs helper stuff (#3421)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
* initial pjs helper stuff

* move resolveReauth

* move location of PuterClient and rename to networkUtils
2026-07-22 18:48:11 -04:00
Nariman Jelveh 43b9200205 Dashboard: headless full-tab apps with a floating control pill (#3424)
* Dashboard: headless full-tab apps with a floating control pill

Maximized app windows in dashboard mode no longer render a titlebar —
the app covers the full tab. The head's controls move to lighter-weight
affordances:

- A floating pill overlaid top-center of the app (parent DOM, above the
  iframe): expanded it shows the app icon, title, minimize, and close;
  it opens expanded so first-time and deep-link users see it, then
  collapses to a subtle capsule that re-expands on hover, tap, or
  keyboard focus. Minimize routes through the same URL-entry consumption
  as the Back button; close tears the window down normally. Dark
  translucent glass so it reads over any app content; safe-area aware;
  bigger targets on coarse pointers; title hidden under 500px; respects
  prefers-reduced-motion.
- Apps-tab tiles double as the app switcher: a macOS-dock-style running
  dot marks tiles with a live (visible or minimized) window, and the
  tile context menu gains a Quit item for running apps. UIWindow
  broadcasts window open/close so the dots stay current.
- puter.ui.setWindowTitle also updates the pill title.

Only maximized, URL-owning app windows in dashboard mode go headless:
dialogs, explorer, non-maximized child windows, and everything on the
desktop keep their titlebars. dashboard.css's 29px head-height
compensation is scoped to windows that still have heads, so headless
app bodies fill the window exactly.

* Fix pill buttons swallowed by their own hover halo

The pill's invisible hover halo (an absolutely-positioned ::after with
negative inset) painted above the pill's static children, so real
pointer clicks hit-tested to the halo and never reached the minimize/
close buttons — while element.click() (which skips hit-testing) worked,
hiding the bug from programmatic checks. Stack the halo at z-index -1
(the pill is its own stacking context, so the outside ring still sits
above the app's iframe) and raise the interactive children explicitly.

* Review round: pill/dashboard edge-case fixes

- Latch pop_dashboard_app_url while its history.back() is in flight: a
  double-click on minimize (or a close racing a minimize) issued two
  back()s, and the second popped the dashboard's own entry — navigating
  clean out of the page. Any popstate or push settles the latch.
- Close a running app's windows when it is uninstalled: the tile is a
  headless app's only switcher, so a minimized instance would otherwise
  be stranded — impossible to restore or quit.
- puter.ui.setWindowTitle in dashboard mode now also updates the
  browser-tab title (while that app owns the URL) and the window's
  data-name, so a later restore re-applies the app's current title
  instead of its launch-time one. Desktop behavior unchanged.
- Nudge collapsed-pill contrast (opacity 0.55 → 0.65, border alpha
  0.16 → 0.22) so the capsule stays findable over dark app content.

* Adjust dashboard tile spacing and label sizing

Tweaks My Apps tile layout constants to match the real icon/label geometry: tile height is increased, label top margin is widened, and the running-status dot is moved slightly lower to avoid overlap during icon hover scaling. It also raises dashboard app pill title line-height to prevent descenders from being clipped when ellipsis overflow is active.

* Pill and running-dot visual polish

- Fixed-width pill: the title gets a fixed 150px centered box instead of
  a cap, so every app's pill is identical and the minimize/close buttons
  sit at the same screen position in every app — they're operated by
  muscle memory, like any titlebar's controls.
- Give the pill title line-height 1.35: overflow:hidden (needed for the
  ellipsis) also clips vertically, and at line-height 1 it cut the
  descenders off g/p/y.
- More air around the tile running dot: label margin 6px → 10px and the
  dot 2px lower (tile row height 78px → 82px to keep the label line in
  budget — the pager layout reads the CSS var, so it follows). The
  icon's 1.08x hover growth no longer touches the dot.

* Size the pill icon to balance the capsule

16px read as undersized next to the 26px buttons in the 36px capsule —
bump the expanded icon to 20px with a proportional (25%) radius, and
object-fit: cover in case an app icon isn't square. Collapsed size is
unchanged.
2026-07-22 14:28:06 -07:00
jelveh d5e9417d2d Dashboard apps own the URL: /app/<name> entries with Back-to-minimize
- Opening an app in dashboard mode pushes a real history entry at
  /app/<name>; Back minimizes the app (it keeps running), Forward
  restores it — or relaunches it if it was closed
- The minimize/close buttons consume the entry via history.back() so
  the address bar never names an app that is no longer on screen
- Landing directly on /app/<name> now boots the dashboard with the app
  open maximized in-page instead of the desktop; embeds, popups, and
  explicit fullpage boots keep the desktop flow, and fullpage_on_landing
  no longer opts a landing out of the dashboard
- Desktop mode no longer rewrites the URL at all (no /app/<name> or
  folder-path replaceState on focus, no /desktop resets on close or
  minimize); tab titles still track the focused window
2026-07-22 10:50:10 -07:00
jelveh dec205d959 Lighten tile-to-window restore animation
Update `showWindow` to use a shorter, window-only morph when restoring a minimized app from a dashboard tile. `morph_window_from_tile` now accepts options for `icon_half` and `duration`, gates icon ghost creation accordingly, and derives transition/cleanup timing from the configured duration so un-minimizing feels distinct from a full app launch.
2026-07-22 09:57:24 -07:00
jelveh d87f3464fc Support Cmd/Ctrl+click and adaptive bg color
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Update the dashboard app tile click handler so Ctrl/Cmd+click opens apps in a new tab (matching the context menu behavior) while keeping existing target-link/app-name routing. Also switch the default body background to white and add a dark-mode media query to use a dark background color.
2026-07-21 20:33:27 -07:00
jelveh 25410cac92 Allow tile morph animations on phones
Removes `isMobile.phone` checks that previously disabled dashboard tile morph transitions. Window open/close morph selection and tile-launch ghost setup now run on phone the same way they do elsewhere, while still honoring animation-disable and reduced-motion conditions.
2026-07-21 19:13:48 -07:00
jelveh 66a1baef24 Preplay dashboard tile launch morph animation
Add click-time launch feedback for dashboard app tiles so users get immediate visual response while app startup is still loading. TabApps now starts and settles tile launch state around `launch_app`, logs launch failures, and keeps duplicate-launch protection.

UIWindow now tracks in-flight tile feedback with a WeakMap, introduces `begin_dashboard_tile_launch`/`settle_dashboard_tile_launch`, and updates `morph_window_from_tile` to avoid replaying the icon half when it already ran at click time. Ghost/icon timing and cleanup were adjusted so fallback and failed launches do not leave stale tile state.
2026-07-21 19:09:32 -07:00
jelveh 5c4df9cbd7 Revert "feat(gui): start the tile open-morph on click, before the launch fetches"
This reverts commit f89b3a888a.
2026-07-21 18:25:39 -07:00
jelveh f89b3a888a feat(gui): start the tile open-morph on click, before the launch fetches
A fresh launch has a server round-trip (app info, signing) between the
click and the window's creation, so the open morph used to start late —
on a slow connection the click gave no feedback at all. The two halves
of the morph are now decoupled:

- On click, begin_dashboard_tile_launch starts the icon's half
  immediately: the ghost enlarges in place and dissolves exactly as
  before, and the real icon stays hidden — the empty slot reads as
  'launching' — while the fetches run.
- When the window opens, morph_window_from_tile claims the click-time
  ghost instead of spawning a second one, runs the window half alone,
  and restores the icon when it lands. On a fast launch the halves
  overlap into one motion; on a slow one the window zooms out of the
  empty slot.
- settle_dashboard_tile_launch (in the launch's finally) puts the icon
  back whenever the window never claims it: launch failed, app opened
  in background, or the morph fell back to the plain fade. The launch
  also gained a .catch, so a failed launch logs instead of leaking an
  unhandled rejection.

The un-minimize path has no fetch and is untouched: it still spawns its
own ghost inside the morph.
2026-07-21 17:58:05 -07:00
Nariman Jelveh bf545dda25 Dashboard: open apps maximized in-page, with minimize and single-instance behavior (#3419)
* feat(gui): open dashboard apps maximized in-page with minimize + single instance

Apps launched from the dashboard's Apps tab now open as maximized Puter
windows in the same page (via launch_app) instead of a new browser tab.
External website shortcuts still open in a new tab since arbitrary sites
can't reliably be iframed.

Dashboard app windows keep their minimize button (fullpage mode normally
hides it since there's no taskbar). With no taskbar item to animate
toward, hideWindow now hides the window in place and flags it with
data-minimized_in_place; showWindow un-hides it via that flag. An
explicit flag is used because data-orig-* can't distinguish the two
minimize paths - drag/maximize handlers set those attrs too.

Tiles are single-instance: clicking a tile un-hides a minimized
instance or focuses a visible one instead of launching a duplicate, and
launches in flight swallow repeat clicks.

Also fixes .window-body-app height in dashboard mode: fullpage mode
sizes it to 100% assuming no titlebar, which clipped the bottom 29px of
every app.

* feat(gui): iOS-style minimize-to-icon morph in dashboard mode

Minimizing a window in dashboard mode now zooms it into its app's tile
on the Apps tab — but only when the user can see that tile (Apps section
active AND the tile on the pager page currently in view); otherwise the
existing hide-in-place fade is kept, as on phones and with
prefers-reduced-motion.

The animation is a two-layer morph on one 450ms cubic-bezier(0.32,
0.72, 0, 1) path: the live window scales onto the tile's icon box
(transform + opacity only — no per-frame iframe reflow) while its corner
radius grows toward the icon squircle, and an enlarged clone of the icon
flies congruently with it, cross-fading in as the window fades out and
landing exactly on the real icon's slot (hidden until the handoff), so
the window reads as becoming the icon.

Window geometry is never touched: only inline transform/opacity/radius
are set and restored byte-identical when the animation ends, and
showWindow()'s existing minimized-in-place path restores as before —
including a reopen mid-animation.

* fix(gui): keep the minimize-morph ghost icon square (contain-fit)

The icon clone previously mapped rect-onto-rect over the window, which
stretched the square icon anamorphically. A covering square was tried
and jittered: it protruded past the card's short edge and the halo
popped in mid-crossfade.

Contain-fit resolves both: the ghost scales uniformly (always square),
sized to the window's short edge and centered — its side tracks the
card's short edge exactly for the whole flight (both interpolate
linearly between the same endpoints on the same curve), so the card's
rectangular flanks melt away around a steady, undistorted icon.

* feat(gui): add 'Open in new tab' to app tile context menu

Every tile's right-click menu now leads with Open in new tab, which
opens the app the way tiles did before in-page windows: /app/<name> in
a new browser tab (noopener), or the site link for external tiles.
Non-uninstallable apps previously had no menu at all; they now get this
single item, and uninstallable ones show it above a divider + Uninstall.

* feat(gui): reverse morph — dashboard tile icons zoom into their opening window

Opening an app in dashboard mode now runs the minimize morph backwards:
the window grows out of its tile's icon box (transform + opacity only,
radius relaxing from the 22% squircle to the resting value) while a clone
of the icon enlarges IN PLACE on its slot — capped at 2.5x, fully dissolved
by ~250% growth — as the window fades in underneath on the same 450ms
cubic-bezier(0.32, 0.72, 0, 1) path with the crossfade handoff mirrored
(window in at 80-240ms, ghost out at 120-320ms).

Both open paths get the morph, with the same visibility conditions as
minimize (Apps section active, tile on the pager page in view, not phone,
no reduced motion), falling back to the existing fades otherwise:

- fresh launch: TabApps passes window_options.morph_from_dashboard_tile,
  and UIWindow morphs instead of the 70ms opening fade. Since .window is
  display:none from the stylesheet until later in the pipeline, the hook
  shows it before measuring (nothing has painted yet, so neither the early
  show nor the hide-back on a declined morph can flash).
- un-minimize: showWindow's minimized-in-place branch morphs instead of
  fadeIn(150), keeping the z-index bump and 80ms focus timing.

A data-window_morphing flag guards against opposing morphs fighting over
the window's inline styles: minimize clicked mid-open-zoom (or a tile
click mid-minimize-zoom) falls back to the pre-morph fade behavior, and
each animation still restores every inline property byte-identical when
it ends.

* Update dashboard.css

* feat(gui): cap the open-morph ghost icon at 2x, dissolving in place

The tile icon no longer flies toward the window center nor grows to full
contain-fit (10x+ reads as a giant blurry sticker): it enlarges in place
on its own slot and its growth is capped so the fade-out completes right
around 200% of its size — the window alone carries the motion and the
rest of the growth.

* fix(gui): drop the tile :active press-shrink that stuttered the open morph

Clicking a tile launches the app and the open morph grows the icon in
place — the 0.95 press-shrink made every launch read as shrink-then-grow.
The icon now holds its hover scale through the click and enlarges
straight from there (the morph ghost is measured from the rendered rect,
so it picks up the hovered size seamlessly).
2026-07-21 17:41:45 -07:00
Neal Shah c706b4472d use max_tokens instead of hardcoded value 1000 (#3422)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
2026-07-21 18:25:40 -04:00
Neal Shah 93b8041c3d don't allow prototype defined method to be called in drivers (#3413)
* don't allow prototype defined method to be called in drivers

* fix test
2026-07-21 18:01:52 -04:00
Daniel Salazar 89f9f9728f fix: PUT-1355 PUT-1351 PUT-1208 (#3420)
* fix: PUT-1355 PUT-1351 PUT-1208

* fix: dev center header
2026-07-21 14:17:33 -07:00
Daniel Salazar 645409eddb fix: misc hardening (#3418) 2026-07-21 13:55:00 -07:00
tamilselvanp57 58a763bcd1 Add Dev Center UI for viewing app users (#3349) (#3417)
Adds a 'View Users' link on the app Analytics tab that opens a modal
listing users who have authenticated into the app (username + email,
where the user has granted email:read to the app).

Consumes the existing puter.apps.get(...).users()/.getUsers() client
SDK method, backed by the app-telemetry driver's get_users - no new
backend work needed, per discussion on the issue.

Relates to #3349
2026-07-21 13:52:48 -07:00
Daniel Salazar fd91d72654 wip: better contributing docks (#3415) 2026-07-21 12:07:59 -07:00
Reynaldi ChernandoandCopilot Autofix powered by AI 7fbf491e6f Update docs meta tags and description (#3416)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
* Update docs meta tags and description

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-21 16:43:00 +07:00
Daniel Salazar a8833de9d5 fix: misc hardening (#3414) 2026-07-20 23:53:37 -07:00
Daniel Salazar 3a8b6394de feat: standardized api pagination (#3412)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
2026-07-20 15:58:23 -07:00
Nariman Jelveh f5d656650f feat(email): accept custom message headers in sendRaw (#3411)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
* feat(email): accept custom message headers in sendRaw

Lets callers set transport-level headers such as List-Unsubscribe /
List-Unsubscribe-Post. Also documents the puter-email driver's automatic
unsubscribe / report-abuse footer and the new `suppressed` result field
in the puter.js email module.

* feat(email): envelope override in sendRaw + suppressed in EmailSendResult type

Adds an optional `envelope` field to SendMailOptions so callers can set
transport recipients independently of the visible To/Cc headers (used by
the email-send driver for per-recipient deliveries), and adds the
`suppressed: string[]` field to the public EmailSendResult typing to
match the driver's documented result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(email): document per-recipient best-effort delivery via `failed`

The email-send driver now attempts every recipient's private delivery
even when one fails, returning failed addresses in the result's
`failed` array instead of failing the whole call — so callers retry
only the failed subset and never re-mail delivered recipients. Adds
the field to the public EmailSendResult typing and module docs.
2026-07-20 11:55:46 -07:00
Daniel Salazar dd314da16d feat: require app or api tokens for ai api usages (#3407) 2026-07-20 08:34:44 -07:00
jelveh 47c1d4268f Fix overlapping context menu state on file rows
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
2026-07-19 20:08:00 -07:00
jelveh b9e87935cc Update dashboard.css 2026-07-19 20:00:18 -07:00
jelveh 73dd95bd41 Open Desktop by default in the Files tab 2026-07-19 19:53:52 -07:00
jelveh c5bc358ef2 Keep file rows visually active while their context menu is open 2026-07-19 19:51:38 -07:00
jelveh 57c9eb9e1a Fix grid badge drop shadow and hover overlap with item menu 2026-07-19 19:23:02 -07:00
jelveh a0c0544928 Remove extra top padding from files header 2026-07-19 19:15:51 -07:00
jelveh 6dcae81a56 Fix uninstall animation snapping after a prior FLIP slide 2026-07-19 18:57:04 -07:00
jelveh 09baee6381 Delay uninstall tile animation until after the modal closes 2026-07-19 18:48:06 -07:00
jelveh 68aab718a1 Add keyboard navigation to the Apps tab 2026-07-19 18:43:57 -07:00
jelveh 5d6b2f3265 Uninstall apps optimistically with a tile removal animation 2026-07-19 18:35:01 -07:00
jelveh 28730010c3 Fix inconsistent icon sizing in dashboard grid views
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
2026-07-19 12:24:21 -07:00
jelveh 817ece40ba Modernize folder icon yellows 2026-07-19 12:24:20 -07:00
jelveh 7c868e947a Fix Public folder icon grid and align glyph system 2026-07-19 12:07:10 -07:00
jelveh 66984cd3d6 Improve glyph sizing on system icons 2026-07-19 12:03:09 -07:00
jelveh 5fa508ba91 improve dashboard file list and grid styles 2026-07-19 11:48:46 -07:00
jelveh c1fca467bf Use bundled fallback for null app icons 2026-07-19 10:52:56 -07:00
jelveh e726c19311 Add compact grid option to Files view menu
Reworked the Files view toggle into a context menu with List, Compact Grid (`grid-sm`), and Grid modes, and refactored view handling into shared helpers (`isGridView`, `viewClass`, `setView`, `applyViewMode`) so grid behavior is applied consistently across rendering and drag previews. Added compact-grid CSS overrides to reduce tile, icon, text, and badge sizing while reusing existing grid layout behavior.
2026-07-19 09:48:31 -07:00
jelveh 6643642aa7 Fix item-more color for selected rows in dashboard
Ensure the item-more icon uses the correct sidebar item color when a dashboard file row is selected, including on hover.
2026-07-19 09:26:54 -07:00
jelveh 65dc3b5f4c Use thinner dashboard file nav icons
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
Notify HeyPuter / notify (push) Has been cancelled
release-please / release-please (push) Has been cancelled
Switches the Files tab header navigation buttons to new thin arrow SVG assets and aligns icon styling with a lighter visual weight. It also updates several header action SVG paths (new folder, upload, list/grid, sort, select) and sets path action icon color to #5c5c5c so the controls look consistent.
2026-07-18 21:44:02 -07:00
jelveh c225fabbd4 Fix clipped app tiles and soften files header hover styling
Apps tab: tile rows only budget one label line, so two-line app names
made flexbox shrink the icon box into a landscape crop and clip the
label mid-glyph. Clamp labels to a single line and exempt the icon box
from flex shrinking.

Files tab: header hover states (nav arrows, breadcrumbs, action
buttons) now use a neutral #EEE chip and no longer recolor the glyph;
nav arrows also render at full opacity when enabled.
2026-07-18 21:27:24 -07:00
jelveh fdcc2e301b Fix My Apps tile icon and label overflow
Prevent app tile icons from shrinking when labels overflow, and clamp tile labels to a single line so they fit within the fixed tile height. This keeps the grid layout stable while preserving full app names in the existing tooltip.
2026-07-18 20:54:09 -07:00
Nariman Jelveh 183ab0780d fix: verified Dashboard bug fixes (routing, files, apps, usage, mobile menu, CSS) (#3406)
* fix: don't double-refresh Dashboard tabs on browser back/forward

A single back/forward navigation fires both popstate and hashchange, so
handleRouteChange ran twice, calling the tab's onActivate twice — every
back/forward to the Apps tab issued duplicate /installedApps and
/get-launch-apps requests and re-rendered the tab twice. Track the last
handled URL and skip the second event.

* fix: store raw values in Dashboard file-row data attributes

renderItem passed html_encode()d strings to setAttribute, which stores
them literally — a file named "Tom & Jerry.txt" got
data-path="...Tom &amp; Jerry.txt". Every flow that reads the attribute
back (keyboard cut/copy/paste, delete-to-trash, drag-and-drop, selection
actions) then hit the server with the mangled path and failed with
"Entry not found". The socket item.renamed/item.updated handlers had the
same problem via jQuery .attr(), and item.moved/item.removed matched rows
with html_encode()d selector needles that can't match raw attributes.

Store raw values everywhere (matching the desktop convention and the
raw readers throughout TabFiles), match paths by comparison instead of
selector interpolation, and repair the html_encode(x) ?? fallback
expressions that could never take their right-hand side. Verified live:
copy/paste of a name containing '&' now succeeds.

* fix: escape folder names in Dashboard nav-history menu (stored XSS)

The back/forward taphold menus interpolated path.basename(history_item)
straight into UIContextMenu item html, which renders it verbatim. A
folder named with an HTML/script payload executed when the user opened
the history menu after visiting it. The desktop file manager already
encodes this value; match it with html_encode(). Verified live: the
payload now renders as inert text and its onerror never fires.

* fix: harden Dashboard hash routing against malformed and unknown tabs

Three routing defects, all reachable from a shareable URL:

- A malformed percent-sequence (e.g. `#100%`) made parseDashboardRoute's
  decodeURIComponent throw at module load, blanking the entire GUI. Guard
  the decode and fall back to the raw hash. Verified: `#100%` now boots.

- A hash whose tab segment contained a quote (`#foo"bar`) was interpolated
  into a jQuery selector that throws, leaving dashboard event handlers
  unbound. Resolve the route tab against the known tab set (falling back to
  Apps) before it ever reaches a selector. This also fixes an unknown hash
  activating the Apps section without its `.dashboard-content.apps` styling.

- Re-clicking the already-active sidebar tab pushed duplicate history
  entries, so Back became a no-op until pressed repeatedly. Only pushState
  when the hash actually changes.

Verified live for all three.

* fix: keep Dashboard file list footer and size cells in sync on live updates

Incremental updates only touched hidden data attributes, so the visible
UI drifted from the data:

- Adding a file via the socket (item.added) or creating one inserted a
  row but never called updateFooterStats, so the "N items · size" footer
  stayed frozen at the old count. Removals and moves had the same gap.
- item.updated (and the in-place update in UIDashboardFileItem) rewrote
  data-size/data-modified but not the on-screen .item-size/.item-modified
  cells, so a remote overwrite left a stale size (e.g. "5 B" for a file
  that had grown to 2 KB).

Refresh the footer after add/remove/move and repaint the size/modified
cells on update. Verified live: add, overwrite, and delete now all
reflect immediately.

* fix: Dashboard Files shift-click selection after navigation and byte formatting

Shift-click selection:
- renderDirectory cleared the .selected class but left
  window.latest_selected_item pointing at a now-detached row from the
  previous directory. The shift handler set shift_clicked=true, found the
  anchor's index as -1, skipped the range select, and onclick then
  early-returned on shift_clicked — so the first shift-click in a new
  directory selected nothing. Reset the anchor when it detaches, only take
  the range path when the anchor is still in the list, and treat a
  shift-click with no valid anchor as a plain select that becomes the
  anchor (onclick otherwise ignores clicks while Shift is held).

formatFileSize:
- Returned "NaN undefined" for missing/invalid sizes and "1.5 undefined"
  for terabyte-plus files (sizes array stopped at GB). Guard non-finite
  and non-positive input, clamp the unit index, and add TB/PB.

Verified live: first shift-click selects the item, a second shift-click
extends the range, and formatFileSize returns 0 B / 1.5 TB / 2 PB.

* fix: show a message instead of a blank pane when a Dashboard folder fails to open

renderDirectory empties the file list before awaiting readdir, so a
readdir rejection (permission lost, folder deleted from another session,
network error) left a completely blank pane with only a console error.
Render a centered "This folder couldn't be opened." message on the catch
path. Verified live: a simulated readdir failure shows the message,
clears the spinner, and leaves navigation working (next folder loads).

* fix: Dashboard Usage/Home tab data freshness, errors, and dead controls

TabUsage:
- The Upgrade/Manage button was shown unconditionally and its click did
  `new window.UIUpgradeAccount()`, which only exists on hosted puter.com —
  on self-hosted it threw a TypeError from a dead button. Hide the button
  when UIUpgradeAccount is absent and guard the click.
- The tab had no onActivate and bound its refresh to a nonexistent
  element, so usage numbers were frozen at page-load for the whole
  session. Add onActivate to refetch (verified: reactivating refetches).
- Resource names rendered the backend's `_dot_` escaping literally
  ("gemini-2_dot_5-flash"); un-escape for display and html_encode the cell.
- The two loaders had no error handling; a failed fetch left the tab
  blank with an unhandled rejection. Catch each and show a fallback.
- Guard capacity 0 so storage no longer renders "NaN%".

TabHome:
- Plan button kept saying "Manage →" after a subscription lapsed; reset it
  to "Upgrade →" in the free branch.
- Guard capacity 0 ("NaN%") on the storage card.
- "Your Plan ›" card header had the arrow affordance but no target and did
  nothing on click; point it at the Usage tab like its siblings.
- Returning to the tab fired both focus and visibilitychange, and
  refreshAndBroadcast both called refresh() and dispatched the event its
  own listener handles — up to 4 duplicate reloads per focus. Drop the
  direct call and coalesce the focus/visibility pair.

TabHome + TabApps:
- window.open(externalAppUrl, '_blank') lacked noopener, exposing the
  dashboard tab to reverse tabnabbing from an external app site. Add
  noopener,noreferrer.

Verified live: Usage and Home render without NaN or console errors, the
upgrade button is hidden on self-hosted, and 'Your Plan' now navigates.

* fix: multiple Dashboard CSS defects (dead rules, contrast, responsive)

- The desktop rule that hides the row ⋮ button ended two selectors with a
  comma before an @media block, so the whole construct was invalid and
  discarded — the ⋮ showed on every list-view row on desktop instead of
  deferring to right-click. Verified: it's now hidden (display:none).
- Native-drop dark-mode styles keyed off .window[data-color-scheme="dark"],
  an attribute never set anywhere; converted to @media (prefers-color-scheme:
  dark) like every other dark rule so they actually apply.
- .myapps-tile-label set white-space:nowrap after a 2-line -webkit-line-clamp,
  defeating the clamp so long app names clipped mid-glyph on one line;
  removed it.
- .dashboard-sidebar-separator was defined a second time with a contradicting
  box model (background line + full-width margin), rendering a doubled,
  edge-to-edge divider; removed the duplicate so the inset border rule stands.
- Grid-view .item-icon had a hardcoded background:white and border-radius:2px
  overriding its own border-radius:8px (glaring white tiles in dark mode,
  square-ish corners); use var(--dashboard-background) and keep 8px.
- .files-footer used hardcoded #666/#CCC on theme-variable backgrounds
  (dim in dark, near-invisible separator in light); use the text/muted vars.
- Fixed the .ui-droppable-over typo (jQuery UI emits ui-droppable-hover).
- The context-menu backdrop's "desktop transparent" rule used min-width:768px
  while every mobile rule uses max-width:768px, overlapping at exactly 768px
  (a common tablet width) — a non-dimming, invisible modal shield; bumped to
  769px.
- At 481-768px the fixed hamburger toggle sat on top of the Files directories
  column's first folder because .dashboard-content.files padding wins over the
  media-query padding; add top clearance to the directories column there.

Verified light-mode desktop is unchanged (directories padding still 16px, ⋮
hidden, footer colors resolve).

* fix: only offer Uninstall for Dashboard apps where it actually sticks

Uninstall was suppressed via a hardcoded 8-name allowlist that had drifted
out of sync with the backend's ~26 recommended apps. For the ~18 unlisted
recommended apps (Calculator, Code, the games, …) the menu offered
Uninstall, revokeApp resolved, the tile vanished — then get-launch-apps
re-added it on the next load and it reappeared, so the uninstall silently
reverted.

Compute uninstallability from the actual lists: an app is uninstallable
only if it's in the user's installedApps AND not in the recommended list
(and not a protected core app). Recommended apps — installed or not —
resurrect on reload, so Uninstall is hidden for them. Verified live: a
synthetic installed-not-recommended app shows Uninstall; a recommended
app shows none.

* fix: guard Dashboard Apps loads against stale overwrites and drag/error clobbering

loadApps only checked for an in-progress drag before its await, and its
catch wiped whatever was on screen. Three concurrency issues followed:

- A slow, older load resolving after a newer one could overwrite the
  newer app list (and clobber a reorder the user saved while the stale
  fetch was in flight). Tag each load with an increasing id and skip
  applying one only when a strictly newer load has already applied —
  gating on "already applied" (not "latest started") so the first load to
  resolve still populates the list for the pager's ResizeObserver.
- A drag that began while a load was awaiting could have the grid rebuilt
  out from under it; re-check the drag after the await.
- A transient re-fetch error replaced a working grid with "Failed to load
  apps"; only show that placeholder when nothing has loaded yet.

Verified live: a slow older load no longer clobbers a newer one, and the
grid still renders normally on activation.

* fix: page through all installed apps in the Dashboard instead of capping at 100

The /installedApps endpoint clamps limit to 100 and paginates, but the
Apps tab fetched a single page — a user with more than 100 installed apps
silently lost the alphabetically-last ones from both the grid and search,
with no way to launch or uninstall them from the dashboard. Loop pages
until a short one comes back (the common <100-app case still makes a
single request). Verified: the request now carries &page=1 and stops
after one page for a small account; the loop pulls all pages otherwise.

* fix: make the mobile Dashboard context menu handle submenus, disabled items, and positioning

The touch context-menu modal (used whenever maxTouchPoints > 0) had three
defects:

- Items with a submenu and no onClick ("New", "Open With") rendered as
  plain buttons that did nothing and didn't even close the menu — the two
  submenu-bearing actions were simply unreachable on touch. Drill into the
  submenu on tap, with a Back row to return.
- disabled items were rendered as active buttons and executed their
  onClick, so a folder's disabled "Paste Into Folder"/"Publish as website"
  ran anyway. Render them inert (disabled attribute + dimmed class + a
  click-handler guard), mirroring the desktop UIContextMenu.
- Non-touch devices that still route here (touchscreen laptops) got the
  modal pinned at a hardcoded left:300px, far from the tapped item on a
  wide screen. Center it over the target and clamp to the viewport.

Verified live (with maxTouchPoints forced): "Open With" opens its submenu
and Back returns; disabled "Paste Into Folder" is inert; the modal is
positioned near the item.

* fix: repair Apps-tab regressions from the Dashboard bug-fix pass

- A non-array /installedApps response (an error payload) was read as
  end-of-pagination, silently rendering the grid without any installed
  apps; fail the load so the explicit error state shows instead.
- A loadApps result that resolved mid-drag was discarded with no retry,
  freezing the grid on stale data for the rest of the visit; stash it
  and apply it after the drag ends, reconciled against the drag's final
  order so it can't undo a reorder whose save is still in flight.
- Uninstall disappeared for recommended/recent apps, removing the only
  UI path that revokes their permissions; offer it again and instead set
  expectations in the confirm modal (the tile stays for apps that
  get-launch-apps re-adds).
- init and the initial-route onActivate both fired a full load on open;
  share the in-flight load instead of issuing a duplicate request trio.

* fix: repair Files/routing regressions from the Dashboard bug-fix pass

- The no-anchor shift-click fallback also fired when shift-clicking the
  current anchor, collapsing an existing multi-selection to one item,
  and it ignored Ctrl/Cmd; only run it when there is genuinely no
  anchor, and keep the existing selection when Ctrl/Cmd is held.
- Unknown hash values (a stale bookmark, an in-page anchor) were
  coerced to 'apps' and yanked the user off their current tab with a
  refetch and autofocus; ignore them outright, both at boot and on
  hashchange, while still keeping untrusted hashes out of selectors.
- The item.updated socket handler re-implemented the row-refresh from
  TabFiles and had already drifted (trashed items showed their raw UID
  instead of metadata.original_name); extract a shared row updater and
  delegate to it.

* fix: repair Usage-tab regressions from the Dashboard bug-fix pass

- Hiding the plan button whenever window.UIUpgradeAccount was missing
  at check time could hide it for the whole session on hosted
  deployments that attach the script after dashboard init; keep
  re-checking for a while before concluding the install is self-hosted.
- init and the initial-route onActivate both refreshed usage on a
  direct #usage open; share the in-flight refresh instead of issuing
  duplicate request pairs.

* fix: repair Dashboard CSS regressions from the bug-fix pass

- Repairing the malformed item-more rule put display:none into effect
  for the first time, removing the visible-and-used desktop row '⋯'
  button; drop the rule instead (it was never in effect on any release).
- The desktop context-menu backdrop breakpoint (min-width: 769px) left
  fractional viewport widths between 768px and 769px with the mobile
  dimming; use the exact complement of the mobile max-width: 768px
  rules instead.

* fix: make Dashboard app uninstall honest about recents and resilient mid-flight

- Recently-opened apps were classified as sticky-removable, but the
  recent list is built from app-open history that a revoke doesn't
  touch, so their tiles reappeared on the next load — the very bug the
  uninstall gating set out to fix. Treat recents like recommended.
- The confirm modal claimed every staying tile was 'provided by Puter',
  which is false for third-party recents; describe the actual reason.
- Uninstall now marks in-flight loads stale so a fetch that started
  before the revoke can't resurrect the removed tile.
- A pagination failure after the first page no longer discards the
  pages already fetched — one flaky request among N used to turn the
  whole grid into 'Failed to load apps'.

* fix: keep the Dashboard Usage tab stable through transient failures

- A failed refresh no longer wipes an already-rendered usage table;
  the unavailable note only shows when there is nothing on screen yet
  (mirrors the Apps grid's error handling).
- The plan-button check now re-checks indefinitely with backoff instead
  of giving up after ~10s, so an arbitrarily late UIUpgradeAccount
  attachment can't leave a subscriber without the button all session.

* fix: Dashboard Files footer sync and Home refresh resilience

- The shared row updater now refreshes the footer's item-count/total
  line, which is computed from the data-size attributes it just wrote;
  a remote overwrite no longer leaves the row and footer disagreeing.
- Size/Modified are only written when the update payload carries them,
  so a minimal event can't zero out correct values on screen.
- A failed readdir now resets the footer instead of keeping the
  previous directory's counts over an empty pane.
- Home's focus-refresh caps its wait on refresh_user_data, so a whoami
  that never settles can't leave the plan/usage cards stale forever.

* fix: contain partial Apps loads and keep the uninstall flag current

- A partial installed-apps list (a page beyond the first failed) now
  renders only when the grid is empty — it must never replace a
  complete grid already on screen — and saveOrder refuses to persist
  while the list is partial, since overwriting the saved order with a
  truncated one would drop the missing apps' positions for good.
- Opening an app from the grid now flips its tile's uninstallable flag:
  the open puts it in the server-side recent list, so a revoke from
  that moment on leaves the tile in place, and the modal must not act
  on the stale load-time snapshot.
- The in-flight-load invalidation moved into _invalidateInFlightLoads()
  so the seq protocol lives in one place.

* fix: Usage-tab error guard, dead spinner, and poll decay

- The transient-error guard now tracks 'a table has rendered' instead
  of data length, so an account with legitimately zero usage doesn't
  get its empty table replaced by the unavailable note on a flaky
  refresh.
- update_usage_details animated a spinner element that exists nowhere
  in this tab and held every refresh open for an artificial 1s minimum;
  both halves of the dead control are now gone.
- The plan-button recheck backs off to a 60s heartbeat instead of
  polling at 2s forever on installs where UIUpgradeAccount never
  appears.

* fix: guard minimal file updates; tighten selection and socket hot paths

- The shared row updater only writes the fields an update payload
  actually carries, so a minimal event can't blank a row's name/path —
  the same hazard the size/modified guards already covered.
- The all-rows lookup in the selection handler now happens only under
  Shift instead of on every click, and the single-select block shared
  by the no-anchor shift-click and drag-handle paths lives in one
  selectSingle helper.
- item.removed and item.moved narrow by data-uid (O(1) selector) before
  comparing paths instead of scanning every row per socket event.

* fix: replace partial-load and uninstall-flag patches with convergent designs

The previous round's containment patches each spawned new corner cases
(a persistently failing page froze all refreshes; the partial-load
saveOrder bail silently discarded reorders; the tile-open flag flip
lost races with in-flight loads, server recording, and popup blockers).
Replace both mechanisms:

- A partial installed-apps fetch (later page failed) now always applies,
  supplemented with the apps already known from the previous list — the
  grid and search can't shrink from one flaky request, refreshes never
  freeze, and an empty grid still gets the fetched pages.
- saveOrder carries over saved names missing from the current list
  instead of refusing to persist, so no app's saved position is ever
  dropped; stale names are harmless (reconcileAppOrder ignores them)
  and preserve the position of apps that return.
- The load-time uninstallable snapshot is no longer patched client-side
  on tile open. Uninstall now revokes, optimistically removes the tile
  when expected to stick, and refetches — the grid converges to
  server-side truth instead of guessing at recents state.

* fix: don't blank filenames on metadata-only updates; unify range selection

- A payload carrying metadata but no name resolved to an empty display
  name and blanked the row's visible filename — skip the write when the
  resolved name is empty.
- The shift-range branch now goes through the same applySelection
  helper as the no-anchor and drag-handle paths (it takes the row set),
  removing the last hand-rolled copy of the selection bookkeeping.

* fix: rank-preserving saved-order merge; keep the pager put after uninstall

- The saved-order carryover appended missing apps' names at the tail,
  permanently demoting their saved grid positions — the very thing it
  claimed to protect. Replace it with mergeSavedOrder in appOrder.js
  (beside its tested siblings, with unit tests): each missing name
  keeps its rank among surviving names, so a drag during a partial-load
  session neither drops hidden apps' positions nor teleports them
  behind the dragged tile.
- The post-uninstall convergence refetch rendered without preservePage,
  snapping the pager back to page 1 and replaying the load fade; thread
  render options through loadApps so the background sync keeps the
  user's page.

* fix: keep hidden apps' saved ranks through the mid-drag stash path

- _applyPendingLoad reconciled a stashed load against the visible-only
  on-screen order, tail-appending any app returning to the grid and
  contradicting the order the drag had just saved — the rank-demotion
  defect resurfacing through one more path. It now prefers
  _savedOrderNames, the canonical saved list that saveOrder keeps
  merged with hidden apps at their ranks, over the possibly pre-drag kv
  snapshot the stashed load fetched.
- The post-uninstall optimistic render now also skips the load fade;
  it was the one in-place rebuild still hiding the grid behind the
  opacity-0 icon gate.
- mergeSavedOrder's rank rescan collapsed to a single forward
  insertion pointer (verified output-identical; the 20 unit tests pin
  the behavior).

* fix: gate stale kv-order snapshots on the latest local save

A drag that started and ended while a load was in flight escaped the
stash path entirely: the load applied normally and replayed its
pre-drag kv snapshot, visibly reverting the just-saved reorder (and
permanently clobbering it after the next save). Conversely, the stash
path unconditionally preferred the local record, discarding a genuinely
fresher order fetched from another window when the drag committed
nothing.

Record the load-seq boundary at each saveOrder; a resolving load (both
apply paths, via _resolveOrderNames) replays its fetched kv order only
when it was issued after the latest local save — otherwise the
canonical in-memory saved list wins. Also rewrites the _applyPendingLoad
header, which described the pre-rank-merge implementation.

* Simplify app uninstall tile handling

Remove the uninstallability flag and modal warning logic, and treat uninstall as a local UI removal after permission revoke. This avoids an immediate reload that could re-add recommended/recent apps, while stale in-flight loads are still invalidated. Also simplifies app loading by dropping unused render options from `loadApps`/`_fetchAndRenderApps` and related merge comments.

* fix: match raw item paths in move_items cleanup and shortcut re-point

Item rows store data-path/data-shortcut_to_path unencoded, so the
html_encode()d attribute selectors missed names containing & < > " '
and the destination-row exclusion guard could remove a legitimate row
just created by a concurrent item.moved handler. Compare raw values
case-insensitively instead.

* fix: bound the Usage plan-button retry poll

The UIUpgradeAccount retry chain decayed to a 60s heartbeat but never
stopped, leaving self-hosted installs polling forever and pinning the
captured $el_window. Give up after ~30s of decaying retries; onActivate
re-checks on every return to the tab, so late script loads still get
the button.

* fix: drop the recent-opens list from the Apps tab grid

Recents are open history, not installs: they resurrected uninstalled
apps' tiles and showed merely-visited sites as if installed. The grid
is now recommended + installedApps; anything the user actually uses
still appears because opening an app grants it a permission. Recents
still power the Home tab.
2026-07-18 20:39:57 -07:00