mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-04 21:15:50 +00:00
8a55debdaedda3f7e5fe8d1795ab015d049dc101
7134
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8a55debdae |
fix(dashboard): sixth adversarial review — bugs, UX, a11y, perf (#4314)
* fix(dashboard): render in the user's saved language
The dashboard never read the account's preferences: only the desktop did,
on boot. Everyone saw it in English whatever language they had chosen,
until the Files tab's Show hidden toggle switched a running session over,
to the browser's language when none was saved, since that was the
fallback the toggle wrote back.
The desktop's preference loading moves into load_user_preferences, which
the dashboard now runs in the background. The first paint uses the
language this browser last loaded, so nothing waits on the read; a saved
language that differs reloads into it, and a changed Show hidden setting
refreshes the open folder. The tab names are read when the sidebar is
drawn rather than when the modules load. A failed read keeps the cached
preferences instead of resetting them to English.
* fix(dashboard): label credit amounts with a key meant for them
Home's allowance and the Usage tab's total and Credits column used the
`credits` key, which the translations render in its acknowledgments sense:
"5,000 Mitwirkende" (contributors) in German, "Авторы" in Russian,
"特别鸣谢" in Chinese. They now use `usage_credits`, which falls back to
English until it is translated, as `monthly_credits` already does.
* fix(dashboard): enable the token and 2FA cards once the email is confirmed
The dashboard had no handler for the `user.email_confirmed` socket event,
and Account and Security pick the token button's state and whether to
offer 2FA when they render. A user who confirmed their email in another
tab, or in this one through the confirmation prompt, kept a disabled
"Create token" and no 2FA card until a reload.
The dashboard now refreshes the user on that event and renders both tabs
again, as it already does after a temporary account is saved.
* fix(dashboard): save only the profile fields that were edited
Edit profile sent both the display name and the bio on every save, taken
from this session's copy of the profile. That copy comes from the cached
user and can be stale (the profile changed on another device) or empty
(the profile read failed), so changing only the bio wrote the old display
name back, and a save from an empty prefill erased the bio.
The dialog now sends just the fields whose value changed from what it
opened with, and closes without a request when nothing did.
* fix(dashboard): don't show storage as 100% while there is room left
Home and Usage rounded the storage share to a whole percent, so an
account at 99.5% or more read "100%" while uploads still fit. The usage
meter already holds at 99% until the allowance is spent; that rule moves
into meterPercent and the storage meters use it too.
* fix(dashboard): keep keyboard focus inside the open phone drawer
Below 768px the sidebar is a modal drawer over a scrim, but opening it
left focus on the menu button, and Tab past its last entry walked into
the dimmed content behind the scrim. Opening it now focuses the current
page's entry, and Tab and Shift+Tab cycle within it while it is open.
* fix(dashboard): tapping into a Files name field edits the name
On a phone or tablet, tapping the name field of a new folder or a rename
(which iOS needs before it shows the keyboard) reached the row's tap
handler, which opened the folder or launched the file and threw the edit
away. A long-press in the field opened the touch sheet instead of
selecting text, and on desktop a right-click in it showed the file menu
instead of the browser's Cut/Copy/Paste.
The row's tap and menu handlers now leave the name field alone.
* fix(dashboard): don't offer Cut on items the user can't move
A file's menu offered Cut on anything that wasn't immutable, and Shared
rows aren't marked immutable. Cutting a file someone shared with you, or
anything inside a read-only shared folder, then pasting it, ended in a
"Forbidden" alert and an emptied clipboard. Cut now needs the same
permission as Delete in that menu, as Ctrl+X and the selection's Cut
already do.
* fix(dashboard): don't paste or upload into the folder being left
Opening a folder clears the listing at once but only moves currentPath
when the new listing arrives. Ctrl+V, a file dropped from the computer
onto the pane, or Paste from the background menu during that wait went
into the folder the user had just left, and since the refresh then
showed the new one, the items seemed to vanish.
Those three now refuse while another folder is loading, as New Folder
already did.
* fix(dashboard): clear the drop highlight when Escape cancels a drag
Escape ends a Files drag with a synthetic mouseup, which jQuery UI
treats as a release outside every target: it fires no `out`, so the
folder row under the pointer kept its dashed drop outline and tint (and
a sidebar folder or breadcrumb its highlight) after the drag was gone.
The Escape branch now takes those off.
* fix(gui): show the "can't rename in Trash" alert without entities
Pressing F2 on a trashed item (on the desktop or in the dashboard's
Files tab) showed "This item can't be renamed because it's in
the trash": the message was encoded by i18n and then again by UIAlert.
* fix(gui): show the failed-upload title without entities
The upload progress window writes its error title with innerText, but
was handed the HTML-encoded i18n string, so French showed "Échec de
l'importation des fichiers". The Files tab's two upload paths and
the desktop's now pass the plain string.
* feat(dashboard): open a Files row's menu with the Menu key or Shift+F10
Share, Download, Open With and Properties were reachable only by
pointer: the ⋯ button isn't focusable, and the keyboard's menu keys
fired a contextmenu on <body>, which the listing ignores. Both keys now
open the selection's menu at its row's ⋯ button, where arrow keys and
Enter already work.
* fix(dashboard): let Shift+Arrow shrink a Files selection
Shift+Arrow only ever added the next row, so after Shift+Down twice,
Shift+Up left all three rows selected instead of going back to two. A
run of Shift+Arrows now selects the range from the row it started on to
the cursor, on top of whatever was selected before the run, so moving
back shrinks it. A click or a plain arrow starts a new run.
* fix(dashboard): name folders in the Back/Forward history menus as the crumbs do
Holding Back or Forward listed each folder by its path's basename: `/`
was a blank entry, the Shared view read "shared", Trash stayed in
English, and a trashed folder showed its uid. The menus now use the same
names the breadcrumbs show.
* fix(dashboard): show a shortened Files name in full on hover
Grid tiles and a resized Name column shorten long names in the middle
("Quarterl…draft v2.xlsx"), and nothing showed the rest short of opening
Rename or Properties. A shortened name now carries the full one as its
tooltip.
* fix(dashboard): keep keyboard focus on the Files breadcrumbs
Every listing rebuilds the breadcrumbs, which took a keyboard-focused
crumb with it: pressing Enter on one, or any refresh of the folder while
a crumb had focus, left focus on <body>, and the next Tab started over
from the top of the page. Focus now moves to the same folder's crumb,
or to the one for the folder just opened.
* fix(dashboard): reach Home's Shared and Trash rows from the keyboard
At 480px wide or less (a phone, or a desktop browser zoomed in) the
folder sidebar is hidden and Home lists Shared and Trash as rows instead.
Those rows only had a click handler, so a keyboard user couldn't get to
either. They are now focusable links that open on Enter or Space, with
the sidebar's focus ring.
* fix(dashboard): keep the Files selection across a sort or view change
Sorting by a column, switching between list and grid, or toggling Show
hidden lists the folder again, and the rebuild dropped the selection and
the shift anchor: select three files, sort by Size, and nothing was
selected. Only the refresh on returning to the browser tab carried them
across. That logic is now refreshKeepingSelection, and those three use
it too.
* fix(dashboard): don't drop a folder open during a Files refresh
A refresh of the folder on screen (after an upload or paste, Refresh, a
sort, or returning to the browser tab) set the same "already rendering"
flag that a navigation sets, so double-clicking a folder, a sidebar
folder, a crumb or Back during that round-trip did nothing, with no
spinner to explain it.
Opening another folder now takes over from a refresh in flight; the
refresh notices it was superseded and drops its result. A navigation
during another navigation is still ignored, as before.
* fix(dashboard): keep the Files marquee when an item arrives mid-drag
Adding a row clears the empty-folder notice by removing every non-row
child of the listing, and the selection marquee is one. A file added
from elsewhere (an upload, another tab, an app) while the user was
drawing a selection made the rectangle vanish under the pointer.
* perf(dashboard): don't rewrite unchanged Files names while resizing
Every width change (a window resize, collapsing the sidebar) re-fits the
item names, at most once a frame, and wrote each name's text back even
when it came out the same. Each write invalidated layout for the whole
list. With 300 rows that took ~6.4ms a frame in list view and ~4.9ms in
grid; skipping the unchanged writes brings it to ~0.7ms and ~3.2ms.
* fix(dashboard): tell assistive tech the Files Select toggle's state
The Select button only toggled a class, so a screen reader announced a
plain button with no state; it now carries aria-pressed. Sort and View
open menus and now say so with aria-haspopup.
* fix(dashboard): show a person's strongest grant in the share list
When someone held two grants on one item (the owner gave them view
access, and someone the owner let share gave them edit), the share
dialog showed whichever grant the listing returned first, so their row
could read "Can view" while they could edit. A second grant on the same
item now counts toward the row with the stronger of the two levels.
* fix(dashboard): only offer share controls on grants the user can change
The backend lets an item's owner change or withdraw any share of it,
and anyone else only the grants they issued (or their own access). The
share dialog offered the level select and Remove on every row. Someone
the owner let share who removed a person the owner had added got
"Removed bob" while nothing changed, and lowering their level added a
second grant that changed nothing either.
The access list now records who issued each row's grants. A user who
doesn't own the selection gets the controls only on rows whose grants
they issued all of, plus Remove on their own row to leave; other rows
show their level as a tag.
* fix(dashboard): update Files' shared markers after the share dialog closes
The share dialog marks the Files rows it covers as shared or not from
the listings it reloads after each change, and skipped that whole reload
once it was closed. Closing it while a share or removal was still saving
left the row without its shared marker (or with a stale one) until the
folder was reloaded. The markers are now updated either way; only the
dialog's own list is skipped once it's gone.
* fix(dashboard): mark rows shared after sharing more than 50 at once
The share dialog updates the Files rows' shared markers from the share
listings it reloads after a change, and doesn't reload them for more
than 50 items. Sharing such a selection, with a person or by link,
reported "Shared with bob on 55 items" while no row showed the marker
until the folder was reloaded. Those grants now mark their rows
directly.
* fix(gui): unbind each context menu's mousemove handler with the menu
The submenu-aim code bound a mousemove handler on the document for every
menu and submenu and never removed it. Each one kept its closed menu and
items alive and ran on every mouse move for the rest of the session; ten
right-clicks in Files left five such handlers behind. The handler now
comes off when its menu is removed. Moving diagonally into a submenu
still keeps it open.
* fix(gui): ignore clicks on a context menu's dividers
A divider is an <li> like the items, so a click on one (they're 11px
tall, easy to hit between two items) ran the item handler with no
action behind it and threw "Cannot read properties of undefined". The
item handler now skips dividers; the menu stays open, as a native
menu's separator does.
* fix(dashboard): warn that removing a team removes everyone in it
Removing a team from a single item's share dialog asked "Remove Design's
access to this item?", the wording used for one person. The desktop's
share window adds "Everyone in the team loses it."; the dashboard now
uses that confirmation for teams too.
* fix(dashboard): count folders, not items, in "via N folders"
A person who reaches a multi-item selection through more than one shared
ancestor gets a "via N folders" note in the share dialog, and N was the
number of items reached that way: five files under two shared folders
read "via 5 folders". It is now the number of distinct folders.
* fix(dashboard): don't treat a long tail after a dot as a file extension
Files middle-truncates long names and keeps everything after the last
dot whole as the extension. For "Report v1.2 final draft for the board
meeting" that kept ".2 final draft for the board meeting", so the tile
read "….2 final draft for the board meeting" and lost the start of the
name. Only a short tail without spaces now counts as an extension; this
one shortens to "Report v1.…d meeting".
* fix(dashboard): keep Tab inside the touch context menu
The touch menu sheet is modal, but Tab and Shift+Tab left it for the
page behind the backdrop while it stayed open; closing it then no
longer returned focus, since focus was elsewhere. Tab and Shift+Tab now
move between its items like the arrow keys, wrapping at the ends.
* fix(dashboard): say "the other item" when one item is missing a grant
The share dialog's "Add to all" link told screen readers "Give bob
access to the other 1 items" when the person was missing from a single
item of the selection. That case now has its own singular string.
* fix(dashboard): announce a failed Properties load, on both tabs
When Properties couldn't read the item, the General list showed the
error but stayed aria-busy, so screen readers kept it unannounced, and
the Versions tab stayed blank. The list is no longer busy once the
error shows, and Versions shows the same error.
* perf(dashboard): launch the add-app modal's apps without reloading the grid
"Browse the App Center" and "Build with AI" go through the same intro
as a deep link, which first awaited loadApps(). With the grid already
loaded that started a fresh fetch of every app list and the saved
layout, and the launch waited on it: with an 800ms connection, ~1.35s
before anything opened instead of ~0.1s. The intro now uses the grid
already loaded, and only joins or starts a load when there is none.
* fix(dashboard): don't hide the Apps grid on every search keystroke
Search re-rendered the grid behind the first load's fade-in, which waits
for page one's icons. Each keystroke made fresh <img> elements, so an
icon that was slow or failing (an unreachable icon host) blanked the
whole grid until it settled, about a second per keystroke with a 1s
icon and up to 1.5s if it never answers. Search, its clear button and
entering reorder mode now render the results at once.
* fix(dashboard): stop the Apps pager's dots jumping back mid-flip
A page arrow, a dot, or the wheel starts a smooth scroll and marks the
target page at once, but the scroll handler then read the pages passed
over on the way as the current one. The active dot went forward, back
to the old page, and forward again, and a second click on the arrow
added to the stale page: two quick clicks showed 0 → 1 → 0 → 2 → 1 → 2.
While such a scroll is on its way, the pager (and the open folder's)
now ignores the positions in between, until it arrives or a second has
passed. The user's own scrolling still moves the dots.
* fix(dashboard): keep paging while a dragged app is held at the edge
Holding a dragged tile still at the Apps grid's edge (or an open
folder's) turned one page and stopped: the next dwell only started on
another pointer move, so a mouse held steady stayed on the second page
of three. Once a flip settles, the dwell starts again if the pointer is
still in the edge zone.
* fix(dashboard): don't turn Apps pages on a trackpad pinch
A pinch on a trackpad reaches the page as wheel events with ctrlKey
set, and the Apps grid (and an open folder) read them as a wheel flick
and turned the page while the user zoomed. Those events are now left
alone.
The handlers also called preventDefault() from a listener the app makes
passive, which does nothing and logged a console warning on every wheel
tick; that call is gone, as the scroller has no vertical scroll to stop.
* feat(dashboard): open the first Apps search result on Enter
Typing in the Apps search and pressing Enter did nothing; reaching a
result took ArrowDown and then Enter. Enter in a non-empty search now
opens the first match (or a folder that matches), as launchers do. It
does nothing while an IME composition is open, or when nothing matches.
* fix(dashboard): include an open folder's page dots in its Tab cycle
The open folder keeps Tab inside it, but its list of stops left out the
page dots, which are buttons. Tab never reached them, and once one had
focus (clicking it focuses it) the next Tab walked out of the modal
folder to the page behind. The dots are now part of the cycle.
* feat(dashboard): open an app tile's menu with the Menu key or Shift+F10
An app or folder tile's menu (Open in New Tab, Copy Link, Uninstall,
Remove from Folder; a folder's Rename and Ungroup) opened only on a
right-click or long-press, and browsers don't all turn the keyboard's
menu keys into a contextmenu event (macOS has none). The Menu key and
Shift+F10 on a focused tile now open its menu at the tile, where arrow
keys and Enter work.
* fix(dashboard): keep keyboard focus on the grid after Ungroup
Ungrouping a folder from its menu re-rendered the grid without the
folder's tile, so a keyboard user who had it focused was left on
<body> and the next Tab started from the top of the page. Focus now
moves to the folder's first app, which takes the folder's place.
* fix(dashboard): give links AA contrast in the light theme
Link-colored text (Home's "View more" and "Upgrade", the notification
panel's "Mark all read" and "Retry", Properties' website links, the
token dialog's sessions link) used #5271ff, 4.07:1 on white and 3.97:1
on the content background, under the 4.5:1 AA minimum for text that
size. The light theme's link is now #3d5bd9 (5.7:1), with a darker
hover. Dark mode already passed.
* fix(dashboard): give success text AA contrast in the light theme
Success text (the 2FA "enabled" line, Home's active-plan badge, the
share dialog's confirmations) used #03933a: 4.01:1 on white and 3.80:1
on its green tint, under the 4.5:1 minimum. It is now #15803d (5.02:1
and 4.75:1); the success check icons that share the token darken with
it. Dark mode already passed.
* fix(dashboard): give danger buttons and error text AA contrast
White text on the red fills fell short of 4.5:1: the Apps uninstall
confirm (#e53935, 4.23:1), the delete-account button (#f00808, 4.43:1)
and the Files drag "Cancel" zone (#ef4444, 3.76:1). So did light-theme
danger text on its pink tint (#dc2626 on #fef2f2, 4.41:1), used for
form, share, crop and notification errors and the past-due badge.
The fills are now #d32f2f, #d90707 and #dc2626 (4.8:1 to 5.3:1), each
with a darker hover, and danger text is #b91c1c (5.9:1 on the tint).
* fix(dashboard): give the app feedback dialog's Send button AA contrast
The feedback dialog's primary button (Send, and Close once sent) put
white text on a light-blue gradient, 2.66:1 at the top and 3.42:1 at the
bottom. It now uses the dashboard's primary button colors, which meet
4.5:1 in both themes, like the dialogs around it.
* fix(dashboard): show the drop outline and dwell fill on grid folders
Dragging files over a folder in grid view only tinted it like a
selection: the grid's own border, hover and selected rules out-ranked
the shared drop-target and spring-load rules, and the hover rule's
background shorthand wiped the dwell fill. The folder then opened after
700ms with no sign it was about to. Grid and compact-grid folders now
get the list's dashed outline and filling dwell, with the padding
trimmed to match so the tile doesn't shift.
* fix(dashboard): keep the Files preview and drag ghost above the window
The Space-bar image preview (z-index 9999) and the drag ghost (10000)
are appended to <body>, beside the dashboard window. On a touch-only
tablet with a keyboard every window is pinned to 9999999, so the preview
opened invisibly behind it from the first press; on desktop the
window's z-index climbs with each click and overtakes both in a long
session. Both now sit in the band the sidebar tooltip and the toasts
already use.
* fix(dashboard): give small dashboard controls 44px touch targets
Several controls a phone or tablet user taps were under the 44px
minimum touch size, with nothing enlarging them on a coarse pointer:
the Files toolbar buttons (32px), the touch menu's rows (36px tall),
the drawer's close button (32px), the notifications close (30px), the
Properties close (32px), the feedback dialog's ✕ (30px), the add-app
Back arrow (26px), the Apps modal buttons (34px tall) and the share
dialog's Copy link (38px tall).
Following the earlier touch fixes, the icon buttons keep their look
and get a 44x44 hit area, and the text buttons and menu rows grow to
44px. Mouse and trackpad layouts are unchanged.
* fix(dashboard): keep a landscape phone's notch clear of the dashboard
A phone held sideways and wider than 768px (an iPhone Pro Max, a large
Android) gets the desktop layout, and the page asks for the full screen
with viewport-fit=cover. Nothing in that layout read the side safe-area
insets, so with the notch on the left the sidebar's icons sat under it
(x=24 against a 47px inset), and with it on the right the content's
edge and the toasts did.
The sidebar (open or collapsed) now adds the left inset, and the content
area, including Apps and Files, and the toasts add the right one. The
insets are 0 everywhere else, so other layouts don't change.
* fix(dashboard): size the touch menu and bottom sheets to the visible height
The touch context menu, the app feedback dialog and the profile-photo
crop sheet capped their height in vh. On phones 100vh is the viewport
with the browser bars hidden, so while they show these could be taller
than the screen. The touch menu, which ContextMenu.js keeps inside the
visible height, then ran its last rows off the bottom on a landscape
phone, and the bottom sheets could push their header off the top. They
now cap in dvh, as style.css already does.
* fix(gui): move keyboard focus into the Manage sessions dialog
The sessions dialog is modal but never took focus: opening it from the
keyboard left focus on the button behind the backdrop, and Tab walked
the page underneath instead of the session list. It now focuses its
close button on open, keeps Tab inside, and hands focus back to the
control that opened it when it closes.
* fix(dashboard): return focus after the Security tab's dialogs close
Closing Blocked people, or the 2FA setup or disable dialog opened from
the switch, left keyboard focus on <body>, so the next Tab started at
the top of the page. The 2FA switch also disables itself while its
dialog is up, which drops focus even before then. Focus now goes back
to the button or switch that opened the dialog once it has gone.
|
||
|
|
f3e8539eaf |
docs: easy events recipe (PUT-1961) (#3998)
* docs: easy events recipe (PUT-1961) * docs: catch-up-on-notifications events recipe (PUT-1961) * docs: notification examples use notif:app-user, which apps can read * docs: events recipe corrections from a run against a live server (PUT-1961) * refine and update agents md --------- Co-authored-by: Reynaldi Chernando <reynaldichernando@gmail.com> |
||
|
|
b3b02b1ebb |
Fix/dashboard fifth review (#4313)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
* fix(dashboard): close the Apps modals and folders only on a press that starts on the backdrop The uninstall and add-app modals and the open folder closed on any click whose target was their backdrop. Drag-selecting text in the app request box and releasing outside the card lands that click on the backdrop, so the modal and the typed request were thrown away. The backdrop logic the dashboard dialogs already use moves into bindBackdropDismiss, and the three Apps overlays use it too. * fix(dashboard): double-clicking "Add an app" no longer opens App Center The add-app modal opens under the pointer, so the second click of a double-click on the "+" tile landed on its first option and launched App Center (or the AI builder, depending on where the tile sat) without the user choosing anything. An option now ignores the second click of a double-click. * fix(dashboard): act on an app tile once per double-click A tile acted on every click, so a double-click opened an external app's site in two browser tabs. Double-clicking an option in the add-app modal also launched whichever tile sat under the pointer once the modal had closed (App Center and Recorder together, say). A tile now ignores the second click of a double-click; the first already launched, focused or opened what was asked for. * fix(gui): show app launch refusals in a Puter alert, translated launchApp reported "You don't have access to …" and "Couldn't open …" through window.UIAlert, which nothing defines, so both fell through to the browser's native alert(): a blocking, unstyled, English-only dialog over the dashboard or desktop. They now use UIAlert directly, with the text in en.js, and pass the app title unencoded since UIAlert encodes its message itself. * fix(dashboard): clicking a toast with nothing to open keeps apps on screen A toast click brings the dashboard forward before acting on the notification, hiding every app window. Only share notifications lead anywhere, so clicking an "events subscription ended" toast (or any other type) to dismiss it minimized the app the user was working in and showed a dashboard with nothing to look at. Those toasts now just mark the notification read. * fix(dashboard): show an emptied Trash as empty Emptying the Trash from the Files tab took its rows away but left the footer on "N items · X MB" and the pane blank, without the empty-folder notice; the selection bar kept offering actions on rows that were gone. Emptied from another tab, the listing was cleared with .empty(), which also dropped the loading overlay and likewise never updated the footer. Both paths now finish with the empty notice and a fresh footer. * fix(dashboard): say a folder is empty when its last item goes elsewhere When another tab, device or app deleted or moved away the last item in the folder Files was showing, its row faded out and left a blank pane: the item.removed and item.moved handlers updated the footer but never put the "folder is empty" notice up. They now do, for rows that were in the Files listing. * fix(dashboard): list failed drag-and-drop uploads one per line The upload-button path was fixed to put each failed item on its own line, but files dropped onto the listing still joined them with "\n", which UIAlert's HTML collapses into spaces: every failure ran together on one line. Both paths now build the message in failedUploadsMessage. * fix(dashboard): dropping items back into their own folder does nothing Dropping a selection on the sidebar entry for the folder it came from, or spring-loading into a folder and coming back to release in the listing, asked move_items to move every item where it already was, and each one stopped on a "Cannot move item to its current location" alert in turn. The breadcrumbs already ignore such a drop; moveDraggedRows now leaves those rows out too, and a drop with nothing left to move does nothing. * fix(dashboard): a Ctrl- or Alt-drop on the Trash trashes the items Ctrl-dragging a file onto the sidebar Trash (or a Trash breadcrumb) copied it into the Trash: the original stayed put, and the copy carried no original path, so it was offered Restore and Restore failed. Alt-drops made shortcuts there the same way. The four drop handlers' copy/shortcut/move choice moves into dropRowsInto, which only ever trashes what lands on the Trash. * fix(dashboard): the first row click after a marquee released outside Files counts A marquee sets a flag so the click that ends it doesn't clear the new selection; the listing's click handlers consume it. Released over the sidebar or outside the window, that click lands elsewhere, the flag stayed set, and it swallowed the user's next click on a row. The flag now lapses once the release's own click has had its turn. * fix(dashboard): Back and Forward follow a folder the user renamed Open a folder, go back, rename it, then press Forward: history still held the old path, so Files showed "This folder couldn't be opened", and the Back/Forward long-press menus listed the old name. A folder renamed in Files now rebases the history entries in and under it, using the same path rebase the username-change handler does. * fix(dashboard): a folder that can't be opened becomes the current folder When a folder failed to list (deleted elsewhere, share revoked, offline), Files said "This folder couldn't be opened" but stayed on the previous folder underneath: history had moved on while the breadcrumbs, sidebar highlight and New Folder/Upload/Paste still pointed at the old folder, so a new folder or upload landed somewhere other than where the user seemed to be. The tab now moves to the folder it was asked for before showing the message, and creates nothing in it until it lists again. * fix(dashboard): double-clicking Save in the photo crop uploads it once Save closes the crop dialog, but the dialog stays in the page for its 200ms fade-out with its handlers live, so the second click of a double-click ran Save again and sent the profile picture to the server twice. Save does nothing once the dialog has closed. |
||
|
|
bdc9aa9e93 |
Revamp networking api with epoxy try 2 (#3476)
* revamp networking api with epoxy * update to new epoxy build * force wisp auth * emit Error objects from socket 'error' events main tightened the socket error contract to pass an Error (see types/modules/networking.d.ts, which documents the reason as `error.message`); the epoxy rewrite still emitted a bare string, matching the older `(reason: string)` declaration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: cover the epoxy-based networking client Replaces the coverage lost when the wisp implementation was removed: PSocket.test.js and PWispHandler.test.js were bound to PWispHandler, parsers.js, and wispInfo, none of which survive the epoxy rewrite. PSocket.test.js - connect/retry, inbound data, write normalisation, close, and the TLS event remapping. Driven through real ReadableStream/WritableStream pairs so reader locking, cancel, and abort behave as they do in the browser. index.test.js - relay-token exchange (auth header, 401 re-auth and retry, malformed responses) and the epoxy client cache (keying, refresh, in-flight sharing). requests.test.js - pFetch delegation, cache invalidation, and the api call logger's request description. epoxy.test.js - the hand-packed wisp password extension payload. The wasm runtime loader is left to integration coverage; it needs a network fetch and a browser. createPuterPasswordBuilder is exported so the byte layout can be tested against an injected fake runtime. One skipped test records a pre-existing defect: on a failed write, #readLoop's normal-termination path races #closeStreams and emits close(false) after an error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: report socket close flags and epoxy init failures accurately PSocket #readLoop raced the teardown it was being torn down by. A failed write sets #closing and hands the close event to #closeStreams, which has to await reader.cancel() and writer.close() before emitting close(true). That cancel resolves the loop's pending read with {done: true}, so the loop broke, reached its own #emitClose(false), and set #closed first -- callers were told the socket shut down cleanly right after an error. The loop now defers the close event whenever a teardown is under way, in both its normal and its throwing path, and #closeStreams always emits. getEpoxyClient swallowed every init failure and resolved undefined, so a dead relay reached callers as "cannot read properties of undefined (reading 'connect')" instead of its cause. It now re-throws, still without caching the failed attempt. A socket reports "wasm unavailable" or "Failed to create relay token (HTTP 503 ...)" and closes with the error flag set. Two related cache faults fell out of that rewrite: the in-flight early-return ignored `refresh`, handing a caller the very attempt it asked to replace, and it ignored the cache key, so an attempt started before a token change satisfied a request made after it. Reuse is now conditional on both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * restore the public API surface dropped in the epoxy revamp The epoxy rewrite kept the shape of the old networking client but lost three things callers depend on: - JSDoc on the exposed methods. Re-added on PSocket (constructor, on, addListener, write, close), PTLSSocket and pFetch, along with the SocketEvent typedef and a Networking type on netAPI. `on` carries the TLS event aliasing docs that used to live on PTLSSocket, since that behaviour moved here. - socket.write's error message, which lost its trailing '!!'. Apps match on the message, so it is restored verbatim. - pFetch's client-side validation. Epoxy reports a bad request as an opaque wasm value from deep inside the transport, so the scheme check, the Content-Length/body check and the malformed-request TypeError are made before a relay is dialled, rejecting with exactly what they rejected with before. Validation builds its Request from a clone, or it would consume the body epoxy is about to send. The latter two are what the net API suite asserts on node, browser and workerd, and it has been failing on all three since the revamp. addListener takes named parameters so it can be typed, and three unit tests that pinned pFetch's habit of forwarding unvalidatable input were updated to the documented behaviour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * update epoxy client to 04e4930 connectionPrefs changed shape: the handshake and its required extension list are one object now rather than a tuple, and handing the new build the old form throws part way through the handshake. The bump is not optional. The previous build cannot reach the current relay at all: it enforces the required puter password extension even when a relay answers with a v1 CONTINUE, and fails with "Protocol extensions not supported" -- which is what puter.cafe answers with today. Verified against a local relay: fetch, POST bodies, https, raw sockets and TLS sockets, the last exercising connectTls, whose third parameter became an options object in this build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * bump epoxy * dont require ext * chore(puter.js): update Epoxy runtime to 68c6904 * chore(puter.js): update Epoxy runtime to affd9f5 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
21c1e4142e |
fix(dashboard): fourth adversarial review — bugs, UX, a11y, perf (#4312)
* fix(dashboard): keep Escape working when two share or properties dialogs are open
Each share and properties dialog bound its document keydown under one fixed
namespace, so closing either instance unbound every instance's Escape.
Double-clicking Share… or Properties in the context menu opens two; after
the top one closed, Escape no longer closed the one left behind. Each dialog
now binds under its own namespace.
* fix(dashboard): don't close Properties when a text selection ends on the backdrop
Drag-selecting the path or UID in the properties dialog and releasing over
the dimmed backdrop closed the dialog: the click lands on the overlay when
press and release are on different elements. The backdrop now closes it
only when the press started there, as the share dialog already does.
* fix(dashboard): keep focus in Properties after disassociating one of several websites
With more than one website on an item, disassociating one removed the
focused button and returned early before refocusing the dialog, so focus
fell to the page and the next Tab landed in the dashboard behind the modal.
The dialog takes focus back as soon as the entry is removed.
* fix(dashboard): handle the Properties stat failure instead of leaving it unhandled
puter.fs.stat both calls the error callback and rejects. The properties
dialog rendered the error from the callback but dropped the promise, so
opening it on a deleted item or offline logged an unhandled rejection.
* fix(dashboard): don't double-encode the "anyone" and withheld-invite share rows
The share dialog encodes row names when it renders them, but these two
labels came from i18n() already HTML-encoded, so a translation containing
an apostrophe or ampersand would show its entity text. They're now taken
unencoded like every other row name.
* fix(dashboard): make the touch context menu reachable by assistive tech and keyboard
The touch action sheet had no roles and never took focus: it was appended
at the end of the page, so a screen reader announced nothing on open and
focus stayed on the page behind it. It's now a menu of menuitems (with
separators and submenu popups marked), takes focus on open, gives it back
on close, moves between enabled items with the arrow keys and Home/End,
and shows an inset focus ring for keyboard use. Taps draw no ring.
* fix(dashboard): keep Apps folders and order when a settings read fails
The Apps tab read the saved order, folders and uninstalled-apps list with
`.catch(() => null)`, so a failed read looked like a user who had never
saved any. One flaky request emptied the folders and reset the grid to
the default order, and the next drag or folder edit wrote that loss back
to kv for good.
A failed read now keeps what the session already has (the last
uninstalled-apps list included). Until a read of the order or folders has
succeeded, saves stay in memory instead of overwriting the stored value.
* fix(dashboard): drop the Apps tile a failed deep-link launch drew
Landing on /app/<name> (or choosing an option in the add-app modal) adds
the app's tile before the launch runs, because the launch is what installs
it. When the launch failed — no access, no app token, a thrown error — the
tile stayed for the rest of the session, survived refreshes, and every
click on it failed again.
The launch outcome is now passed to settleDeepLinkLaunch, which takes a
tile it drew back out (and restores a previous uninstall record) when
nothing was launched.
* fix(dashboard): put a re-installed app back in its saved slot
Uninstall keeps the app's name in the saved order so a re-install returns
it to its place, but a deep-link landing appended the tile at the end of
the grid. The install animation played on the last page and the tile then
jumped back to its saved slot on the next refresh. The landing now places
the tile through reconcileAppOrder like every other load.
* fix(dashboard): don't double-encode the add-app launch failure alert
UIAlert encodes its message, so passing it the already-encoded i18n string
showed entities on screen in languages with an apostrophe (French read
"s'est mal passé").
* fix(dashboard): say so when an app tile fails to launch
A tile click whose launch rejected (for example the app lookup failing)
only logged to the console: the icon flourish played, settled, and nothing
opened. It now shows the same alert the add-app modal shows for that
failure.
* fix(dashboard): keep focus inside the Apps modals and return it on close
The uninstall and add-app modals are aria-modal but had no Tab trap, so
Tab walked into the search box and tiles behind the scrim, where typing
filtered the grid under the modal. Closing the add-app modal, and
confirming an uninstall, dropped focus to the page body.
Tab now cycles inside both cards; closing the add-app modal returns focus
to the add tile, and an uninstall leaves focus on the tile that takes the
removed app's slot.
* fix(dashboard): upload from a folder's menu into that folder
Upload in a sidebar or breadcrumb folder's menu opened the shared file
picker, whose handler always uploaded into the folder on screen: files
picked for Pictures while viewing Documents landed in Documents. The menu
now hands its folder to the picker.
* fix(dashboard): no New, Upload or Paste where nothing can be created
New Folder, Upload, the New/Paste/Upload menu items and file drops were
only refused in the Trash itself and the Shared view. Inside a trashed
folder they created items without the metadata Restore needs, and at /
every create failed (an upload there threw before its progress window
existed and showed nothing). One check now covers the Trash, folders in
it, the Shared view and /.
* fix(dashboard): make Paste Into Folder work after a cut
The row menu's Paste Into Folder handed a cut to the desktop's
move_clipboard_items, which finds items by their on-screen elements and
plain-string paths. The Files tab's cut stores {path, uid} objects, so
nothing moved and the clipboard was emptied. The menu now takes a paste
handler, and Paste, Ctrl+V and Paste Into Folder share one.
* fix(dashboard): don't cut or copy trashed items out of the Trash
The single-item menu still offered Cut on trashed items, and Ctrl+C
copied them; pasted elsewhere, they kept their raw name in the Trash, a
uid. Trashed items leave through Restore, which puts the original name
back.
* fix(dashboard): Delete Permanently works in every language
The confirmation compared the clicked button's label with 'Delete', so in
any other locale confirming did nothing. The buttons carry values now.
* fix(dashboard): refresh Files on return after a cancelled rename
Escape, or keeping a name unchanged, hid the rename editor but left its
-active class, and the refresh on returning to the browser tab takes that
class for an open editor: the listing stopped refreshing until the next
navigation.
* fix(dashboard): clear a drop target's highlight after the drop
jQuery UI fires no `out` on drop, so the folder row a selection was
dropped on stayed selected (and went to the Trash with the next Delete),
a sidebar folder stayed highlighted as the current one, and a breadcrumb
kept its drop outline.
* fix(dashboard): listing drops keep working after a drop on a folder row
Dropping files from the computer onto a folder row stopped the native
event before the listing's dragster saw it, leaving its enter state stuck:
later drags didn't highlight the listing, and drops on its background
uploaded nothing.
* fix(dashboard): offer Restore only for items Restore can handle
Selecting files inside a trashed folder offered Restore in the
multi-select menu and the selection bar, which then failed once per item:
only direct children of Trash carry restore metadata. The trashed
multi-select menu also drew two separators in a row.
* fix(dashboard): show names as typed in conflict and move-failure alerts
These alerts html-encoded the file name before UIAlert encoded the
message again, so "R&D.txt" read "R&D.txt". "already exists." is
translatable now too.
* fix(dashboard): list failed uploads one per line
The failed-upload list was joined with newlines, which the alert collapses
onto a single line.
* fix(dashboard): translate the shortcut badge's tooltip
* fix(dashboard): select mode toggles rows with any pointer
In select mode, a stylus or mouse tap on a phone or tablet toggled the row
in pointerdown and again in click, leaving it as it was; in a narrow
desktop window a click replaced the selection and the selection bar never
showed. Select mode now toggles in click for every pointer and shows its
bar whenever it is on.
* fix(dashboard): show items added elsewhere while New › Web Link prompts
The New menu's create wrapper suppressed item.added for as long as it ran,
which for Web Link includes its URL prompt, so files added to the folder
meanwhile never appeared. It now counts only from the actual create. A
create that fails now says so instead of failing silently.
* fix(dashboard): keep the Files spinner under the mobile drawer
The loading overlay sat at the maximum z-index, so on phones it drew over
the open drawer and its scrim and took their taps while a folder loaded.
It only needs to sit above the rows.
* fix(dashboard): don't show a failed usage request as zero usage
getMonthlyUsage() resolves with the error body when the request fails, and
Usage and Home read it as a month with nothing used: $0.00 of $0.00, 0%,
and on Usage an empty table replacing the one already on screen. A body
without a usage report is treated as the failure it is, so the last good
numbers stay and a first load shows the "unavailable" note.
* fix(dashboard): draw Files select-mode accents in the selection color
The checked checkbox, the active Select button and the Done button were
filled with --primary-color, the desktop's text color rather than an
accent: charcoal by default, and white (on white text and ticks) for
anyone whose desktop theme uses light text. They use --select-color, the
accent the rest of the selection already uses.
* fix(dashboard): stop the mobile drawer animating under reduced motion
The reduced-motion override for the drawer, its scrim and the menu button
came before the drawer rules it was meant to cancel, and the open
drawer's rule outranked it too, so the drawer still slid and the scrim
still faded. The override now follows those rules and covers the open
state.
* fix(dashboard): make the toast close button readable in dark mode
Dark mode gave the close button the card color and then inverted the
whole element, which turned the disc light grey and the cross white:
about 1.4:1. Without the background the invert gives the light-mode
button's mirror, a dark disc with a white cross.
* fix(dashboard): leave the temporary-account state once the account is saved
Saving a temporary account from the user menu promoted the user in place
without a reload, but Home kept its Save session warning (reopening the
save dialog for a saved account) and Account and Security stayed without
their password and email cards until the page was reloaded. The dashboard
now reacts to account-saved, which fires after the user is updated.
* fix(dashboard): give the API token field 16px text on touch screens
The coarse-pointer rule that sets the token field to 16px, as the other
dialog inputs are, came before the field's own 13px rule of the same
specificity, so the 13px won and iOS zoomed the page when the field was
focused. The touch rule now follows it.
* fix(dashboard): lighten the General access dropdown arrows in dark mode
The dark-mode chevron override covered the share modal's recipient
selects but not the two General access ones, which kept the darker
light-theme stroke on the dark card.
* fix(dashboard): don't leave the sidebar collapse button shaded after a tap
Its hover style was the sidebar's only one not limited to devices that
hover, so on a wide touch screen (an iPad in landscape) the button kept
its hover shade after being tapped.
* fix(dashboard): refresh the Usage tab when the subscription changes
An upgrade from the Usage tab's own button, or a return from the billing
portal, refreshed Home behind it while the Usage meter and plan button
kept the old allowance until the user left the tab and came back. Usage
now handles puter:subscription:changed while it is showing.
* perf(dashboard): load usage and storage once when the dashboard opens
Home and Usage both load the monthly usage and the storage space when the
dashboard opens, whichever tab it opens on, so every open sent
/metering/usage and /df twice. Overlapping loads now share one request.
Nothing is cached: a load after the shared one settles fetches again.
* fix(dashboard): keep keyboard focus on the Usage table's Show more/less
The table redraws without the button that was pressed, so focus fell back
to the page and the next Tab started from the top. Focus now moves to the
button that replaces it, as the sort headers already do.
* fix(dashboard): give primary buttons readable contrast
The dialogs' primary buttons, the share modal's Share and the photo
crop's Save were filled with the link color under white text: 4.07:1 in
light mode, 3.08:1 in dark, where hover lightened it further to 2.38:1.
They take their own fill now, #4a67f5 (4.62:1) darkening to #3d5bd9
(5.68:1) on hover in both themes; links keep their color.
* fix(dashboard): keep collapsed-sidebar labels above the dashboard
The hover label is attached to body at z-index 1000, while the dashboard
window's z-index climbs with every click (focusWindow raises it). Over a
long session the window passed 1000 and the labels rendered behind it,
so hovering the collapsed sidebar showed nothing.
* fix(dashboard): bring muted and tertiary text up to AA contrast
The muted and tertiary greys carry real information (form hints, file
dates and sizes, share "via" lines, the sidebar's team name,
notification text and times) but fell short of 4.5:1: 2.56:1 and 3.54:1
on white, 3.18:1 and 3.13:1 on dark cards. They now clear it on the
cards and the sidebar in both themes (light #5f6b7d / #707070, dark
#8b95a5 / #94949c). Disabled buttons keep the fainter grey through their
own token, so they still read as disabled.
* fix(dashboard): name the plan on self-hosted installs instead of "--"
A self-hosted install has no /marketplace/subscriptions/current route, and
Home treated its 404 like any failed read, leaving "Your Plan" at "--" for
good. A 404 now shows the plan every account starts on. The upgrade badge
and link, which have nothing to reach there, go too, unless the hosted
upgrade dialog is loaded.
* fix(dashboard): make hover and press feedback visible in dark mode
Dark mode's hover color, #2a2a2a, was 1.05:1 against the #262626 cards
and #252525 sidebar, so hovering a notification or sidebar item and
pressing a touch-menu or drawer item showed next to nothing. #333 sits
as far off them as light mode's hover sits off white, and stays below
the selected item's #3d3d3d.
* fix(dashboard): follow a username change in the Files tab
Changing the username, or saving a temporary account under a chosen
name, moves the home folder. The GUI rewrote data-path attributes and the
path globals, but the Files tab kept its current folder, back/forward
history, clipboard and drawn rows on the old path, so sorting, New
Folder, Upload, Up, Back and the breadcrumbs all failed with "This folder
couldn't be opened" until a reload. update_username_in_gui now announces
the change, and the Files tab moves its paths over and redraws.
* perf(dashboard): lay out only the new row when an item is added to Files
Every item added to the open folder from elsewhere (another tab, an
upload, a move into it) restyled and re-truncated every row in the
listing, so a burst of adds was quadratic in the folder's size: 100 adds
into a 300-item folder took ~800ms of main-thread work. The added row now
takes the listing's column layout on its own (~130ms for the same burst).
* fix(dashboard): open the Sort and View menus at their buttons from the keyboard
Enter or Space on either button fires a click with no coordinates, so the
menu opened at the top-left corner of the screen, far from the button.
A keyboard press now opens it below the button; a pointer click still
opens it at the pointer.
* fix(dashboard): open a breadcrumb's long-press menu as the touch sheet
A breadcrumb's long-press opened the desktop context menu at the event's
pointer position, which a long-press doesn't have, so on tablets the menu
landed wherever an unset position put it. Breadcrumbs now open the same
touch sheet, anchored to the crumb, as the sidebar folders and rows do.
* fix(dashboard): don't drag-move items the user can't move
Delete, Cut and their buttons already leave out shared roots and items in
a read-only share, but dropping them on a folder, a sidebar entry or a
breadcrumb still sent the moves, and each came back as a "Forbidden"
alert. Drag moves now leave those rows out too; copying them still works.
* fix(dashboard): clear "1 item selected" when a double-click opens a file
Opening a file by double-click deselects its row, but the footer kept
saying "1 item selected" with nothing selected.
* fix(dashboard): only the Trash gets the Trash label in breadcrumbs
Any folder named "Trash" was shown with the translated Trash label in the
breadcrumbs, so in German ~/Documents/Trash read as "Papierkorb". The
label now goes on the user's Trash folder only.
* fix(dashboard): name a trashed folder by its old name in the breadcrumbs
A trashed item's name on disk is its uid, so opening a trashed folder
showed a crumb like "ffaff8cb-dcaf-44f7-…" while its row in the Trash
read "Old project". The crumb now uses the name the row shows.
* fix(dashboard): show why Delete Permanently failed and keep the item
delete_item fades the item out before the request; when the request
failed it left the item hidden, as if deleted, and passed the alert the
undefined `responseText` of a puter.js error, which threw. The item now
comes back and the alert shows the error's message.
* fix(dashboard): translate the New menu's Web Link and templates strings
"Web Link", its URL prompt, validation and failure messages, the "User
templates" submenu, and the account prompt before publishing a worker
were hardcoded English in the Files New and item menus.
* fix(gui): stop leaking a document click handler per context menu
Every context menu bound its item-click handler on document, delegated
to its own id, and never unbound it. Each menu opened therefore left a
handler that ran on every later click in the page and kept the menu's
items alive, rows and fs entries included. The handler is now bound on
the menu itself and goes with it. It still returns false, so item clicks
keep not reaching document-level click handlers, as before.
* fix(gui): run a context menu item once on a double-click
The menu stays clickable during its 200ms fade-out, so the second click
of a double-click ran the item again: double-clicking Share… or
Properties in the dashboard opened two stacked dialogs. A menu that has
started closing now ignores item clicks.
* fix(dashboard): drag a select-mode selection from its own rows
Select mode returned from pointerdown before recording whether the
pressed row was already selected, so a drag started from that stale
value: dragging selected rows didn't move them, and a stale "selected"
could let a drag from an unselected row clear the selection. The value is
recorded first now.
* fix(dashboard): leave Files select mode when the window widens past 480px
Select mode's Select and Done buttons only show up to 480px, so turning
it on in a narrow window and widening it left every click toggling rows,
with no way out short of narrowing the window again.
|
||
|
|
03e483e372 | PUT-2252 (#4311) | ||
|
|
9c90c7afde |
fix(dashboard): third adversarial review — bugs, UX, a11y, perf (#4310)
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
* chore(dashboard): add the standard file header where it was missing * fix(dashboard): rename editor keeps entity-like names; rows follow remote renames The name editor decoded its already-decoded value a second time, so opening it on a file such as "R&D.txt" and clicking away renamed it to "R&D.txt". A rename or update from another tab only patched a row's attributes, while its open/copy/share handlers kept using the entry they were rendered with; the row now keeps that entry current and refits its name. * fix(dashboard): Files selection, preview and Trash/Shared edge cases - A marquee drag without modifiers now replaces a selection made by click, Ctrl+A or the arrow keys instead of adding to it. - With the image preview open, clicking another image switches to it, and moving onto a non-image closes it instead of keeping the old image up. - Cut (Ctrl+X, multi-select menu) skips trashed items, which leave the Trash through Restore; Ctrl+V refuses the Trash and the Shared view like the Paste menu item does. - Drops on the Shared sidebar entry or breadcrumb are refused (the drag snaps back) instead of sending writes to a path that isn't a folder. - The Trash selection bar shows Delete permanently again after a selection elsewhere had hidden it. * fix(dashboard): keep an open rename across refreshes; re-sort renamed rows A same-folder refresh (an upload or paste finishing, a sort change) rebuilt every row and silently dropped a rename in progress; the editor now comes back on the same item with the text and caret the user left. A renamed row whose new name sorts last stayed where its old name was. * fix(dashboard): Files columns follow the room the listing has - Resizing a column past the listing's width clamps it instead of resetting every column to the defaults; a window too narrow for the saved widths lays out at the defaults but keeps the saved widths for when it widens. - Columns and grid names re-fit when the window or sidebar resizes the listing, not only on the next navigation, and grid names hidden behind another tab are fitted when the Files tab is shown. - Names are measured in the font they render in (the compact grid's is smaller), and a column drag relayouts at most once a frame. * fix(dashboard): Files keyboard and touch edge cases - The marquee selects file rows only, not the Shared/Trash place rows Home shows on narrow screens. - Row shortcuts stand aside while the touch action sheet is open. - Breadcrumb long-press opens its menu on iPadOS too. - A folder rendered before the saved view preferences load uses the defaults instead of throwing and blocking navigation. * fix(dashboard): share and properties modals — focus, Escape, link state Share modal: - Escape pressed in a window opened over the modal (a verification or upgrade gate) no longer closes the modal underneath. - Turning the link on or off for a selection too large to list now shows the result instead of leaving the control on its old state. - Opening the link up starts at view access; it no longer reuses a hidden mode left over from earlier, which could hand out edit access. - Focus returns to the select the user changed. Properties modal: - Takes focus when it opens, keeps Tab inside, gives focus back on close, and leaves Escape to a window opened over it. - Shows a loading state, and opening Versions on a folder before its details load no longer leaves an empty body. - Disassociate is a real button, ignores repeat clicks while pending, reports a failure, and keeps the website badge while other sites remain. - Tabs carry tab/tabpanel roles; transitions respect reduced motion. * fix(dashboard): account dialogs, avatar saves and usage figures - A form dialog ignores Escape and backdrop clicks while its request runs, so the outcome isn't lost behind a closed dialog; the close button still works. - A new API token, shown only once, survives a stray click outside the card. - Copy token works on origins without navigator.clipboard (plain-http self-hosts) instead of throwing, falling back to a selected field. - Saving one profile picture and then another before the first finished can no longer leave the first one's picture or failure on screen. - The storage host bar is clamped to the room left beside Puter's share and hides its label when there is no host figure. - Sub-cent dollar costs read "<$0.01" rather than "$0.00", and the usage meter reads 99% until the allowance is actually spent. * fix(dashboard): Apps folder drag, search, keys and loading - A drag inside an open folder that is cancelled (Escape, blur) or dropped back in place puts the tiles back in their saved order; the cached panel markup used to skip the rebuild, and the next drag then saved the cancelled move too. - Search no longer matches substrings of generated app ids, so "app" or a single hex letter stops returning every website shortcut. - Arrow keys and Enter stay with the notifications panel, an account dialog or the touch action sheet while one is open over the grid. - The grid shows after 1.5s even if an icon host never answers. - Refreshing a grid already on screen keeps the page and skips the fade. - Rename from a folder's menu focuses the name on touch devices too, and the folder dialog's label follows renames and counts the apps shown. - i18n inserts %% values literally, so a name containing $& or $' no longer comes out mangled. * fix(dashboard): shell navigation, a11y and notification sync - Missed notifications replayed on load toast as one burst (a few, then a summary) instead of one toast each. - A notification read in another tab while this tab's first listing is in flight is no longer shown as unread when that listing lands. - Focus stays in the notifications panel when Mark all read or Retry hides the button that had it. - The active sidebar tab carries aria-current, and the account button is named by the username even while the collapsed sidebar hides it. - Landing with ?ref= replaces the history entry, so one Back leaves. - Ctrl/Cmd/Shift-click on a sidebar tab opens it in a new browser tab. - Recently used keeps the newest list when refreshes overlap, and app names are URL-encoded in the links it opens. * fix(dashboard): focus rings, dark-mode contrast and reduced motion - App tiles show a focus ring, not only the hover enlargement; Home card headers get an inset ring the card's rounded clip can't cut off; the Delete account button gets the settings cards' ring. - Pager arrows are mouse-only (the dots page by keyboard), so invisible arrows are no longer Tab stops, and an arrow with no page leaves the accessibility tree. - Dark mode: the Files toolbar's action icons match the nav arrows and stay readable on the hover chip, hovered breadcrumbs keep their contrast, and the shared-item dot's ring follows the row background. - The row '⋯' button is visible at rest on touch screens. - A long username or email wraps inside the profile card. - The touch action sheet, the Files selection bar and the app feedback modal stop scaling/sliding under reduced motion. * fix(gui): desktop rows store remote renames raw; column resize stays in bounds - The desktop's item.renamed/item.updated handlers wrote HTML-escaped names into data-name, the title and the name editor (and titled windows by an undefined uid). They store the raw name now, as rename_file does, so the editor shows the real name now that it no longer decodes a second time. - Resizing a Files column while saved widths are too wide for the listing starts from the widths on screen, and a width is held at its column's minimum, so a drag or auto-fit can't save a negative width. - Double-clicking a row closes the image preview, which would otherwise float above the app window it opens. |
||
|
|
acd3df8c1b |
fix(puter-js): replay the request after a successful token_auth_failed reauth (#3954)
release-please / release-please (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
* fix(puter-js): replay the request after a token_auth_failed reauth
A 401 carrying the legacy `token_auth_failed` code opened the sign-in
prompt and then dropped the re-authenticated token on the floor:
`resolveReauth()` fell through to `return null` instead of returning
`{ action: 'replay' }`, so the original call was never re-issued and
rejected with `Unauthorized` even though the new session was valid.
Return the replay verdict the `reauth_required` branch already returns.
Both replay paths stay one-shot (`ctx.done.add('reauth')`,
`spec._replayed`), and the auth gate rejects before the route handler
runs, so a replay has no side effects.
Closes #3953
* fix(puter-js): sign in once when a replayed XHR is refused again
handle_resp ran the reauth flow on the replay's own 401, so a
token_auth_failed that survived the replay reset the fresh token and
opened a second sign-in before rejecting. Treat a replay's 401 as final,
as the 403 verification-gate branch already does.
---------
Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
|
||
|
|
4729d733bb |
feat(aws): DynamoDB, Polly and Textract can use the default credential chain (#4307)
`useCredentialChain: true` in `dynamo.aws`, `providers['aws-polly']` or `providers['aws-textract']` takes credentials from the default AWS chain instead of static keys, as `s3.s3Config` already does. One provider is shared and refreshes temporary credentials itself. |
||
|
|
26d843778e |
feat(dashboard): account dialogs as dashboard modals instead of windows (#4305)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
* feat(dashboard): account dialogs as dashboard modals instead of windows Edit profile, change username, change password, change email, create API token, and delete account in the Account tab (and change password in the Security tab) now open as dashboard overlay cards, like the share, properties, and confirm dialogs, instead of desktop UIWindows. They are centered cards on desktop and tablet and bottom sheets on phones. - dashboardOverlay.js: the overlay shell UIDashboardDialog already had (reveal, Esc/backdrop/close dismissal, focus trap, focus return), pulled out so the new form dialog shares it. It also pins the overlay to the visual viewport so iOS's keyboard can't cover a sheet's fields, and leaves Esc to a modal opened on top of it. - UIDashboardFormDialog.js: labelled fields, password show/hide, an aria-live status line that marks and focuses the offending field, a busy submit button, and a done state. - postUserProtected.js: the cookie-retry / provider re-sign-in / reauth flow the four account windows each carried, in one place. - Deleting an account is one dialog now (warning plus confirmation) rather than two stacked windows. - Touch: 44px controls, 16px inputs (no iOS focus zoom), no autofocus that pops the keyboard mid-animation, form buttons side by side on phones, safe-area padding dropped while the keyboard is up. - The replaced windows had no other callers and are removed with their tests, which are ported to the new modules. * fix(dashboard): a double-click that opens a dialog no longer dismisses it The second press of the double-click lands on the freshly mounted backdrop and closed the dialog straight away. Backdrop presses with a click count above one no longer count toward dismissal. * fix(dashboard): a failed form submit keeps focus in the dialog Locking the fields disabled the focused control, which drops focus to the page, so after an error Enter no longer retried and screen readers lost their place. The control that had focus gets it back once the fields unlock, unless the handler moved focus elsewhere in the dialog. * fix(dashboard): a form error keeps the field it names in view The status line scrolled the dialog body to the bottom, which on a short screen (a phone with its keyboard up) left the field being focused and marked invalid scrolled off the top. The body now scrolls back up just far enough to show that field. * fix(gui): a blocked re-sign-in popup fails instead of waiting forever openRevalidatePopup polled a popup that was never opened, so when the browser blocked it the account dialog stayed busy indefinitely. It now rejects straight away and the dialog says the sign-in didn't finish. * fix(dashboard): password show/hide reads as one action, not a pressed toggle The button flipped its label between Show and Hide password and also set aria-pressed, so assistive tech announced "Hide password, pressed" while the password was showing. It keeps the action label and drops aria-pressed. * fix(dashboard): screen readers hear a form dialog's success message The done state arrives as a new status region that already holds its text, which screen readers don't announce, so after a change succeeded they only heard "Done, button". The Done button that takes focus is now described by the message. * fix(dashboard): requesting an email change no longer sets the cached email The server only stages the new address until the emailed link is followed, but the dialog wrote it into window.user.email straight away. That cached a wrong address for the session and kept refresh_user_data from updating the Account tab once the change was confirmed, since the two already matched. * fix(dashboard): form dialog labels drop the global label shadow style.css's bare `label` rule gave the field labels a white text-shadow, which ghosted them on the dark theme, and an extra 3px under each label. * fix(dashboard): dialog inputs keep a 1px border when focused style.css's global input :focus rule still set a 2px border width, so a dialog field's text shifted by a pixel each time it took focus. The focus ring is the box-shadow; the border stays 1px. * fix(dashboard): phone dialog inputs stay 16px instead of jumping on focus On phones style.css's .device-phone input rule outranked the dialog's 16px size at rest, so fields drew at 17px and shrank to 16px the moment they took focus. * fix(dashboard): temporary accounts can delete themselves again The server reports oidc_only for every passwordless account, temporary ones included, so the shared user-protected helper sent a temporary account into the linked-provider sign-in, which has no URL for it, and the deletion never went out. Temporary accounts post directly, as the route admits them without credentials. |
||
|
|
80a9cd85ee |
fix(ai): chat requests run to their upstream usage report after the caller disconnects (#4302)
- Providers send upstream calls with a driver-owned `upstreamSignal` instead of the caller's `abortSignal`. A disconnect detaches the chat stream: output stops, the provider keeps reading until the upstream reports usage, and that usage is metered. - A detached stream gets SDK_TIMEOUT_MS to finish. After that the driver aborts the upstream, and the stream is metered like any other stream that ends without usage. - The disconnect check also runs right before dispatch. - The OpenAI stream handlers report usage that arrived before an abort. - Docs: stopping a stream stops the output, and a request that reached the model is billed in full. Detached streams get 10 more minutes. |
||
|
|
63370fcfa5 |
fix(dashboard): mobile and tablet review — layout, touch, safe areas (#4303)
* fix(dashboard): give narrow Files listings the two-row layout The two-row rows (name, then size and date) were keyed to a phone-width viewport, so between 481px and ~930px, where the sidebar and folder column take their share, the fixed size and modified columns squeezed names to a few characters. The layout now switches on the listing's own width with a container query, which phones still meet and wide listings never do. * fix(dashboard): wrap the Files toolbar onto two rows in narrow listings On tablets the nav buttons, crumbs and actions sat on one row wider than the listing, so the sort, view, new-folder and upload buttons ran off the right edge. Narrow listings now use the phone's two-row toolbar (crumbs above, buttons below); only the menu-button clearance stays phone-only. * fix(dashboard): keep a deep folder path on one row with the current folder in view A long path wrapped the crumbs over several lines on top of the column headers and pushed the toolbar actions off-screen; where the crumbs already scrolled (phones), the row opened at the root, hiding the current folder. The crumbs now scroll within their row and are scrolled to their end on render and when the tab is shown. * fix(dashboard): end clipped file names in the list with an ellipsis With the name column auto-sized, which is always the case on phones and tablets, a long name was cut off mid-letter with no sign it continued. * fix(dashboard): keep the Files selection bar under the drawer and sheets The bar's visible state set z-index 99999999999999999 (clamped to the maximum), so in phone select mode it drew over the open navigation drawer, covering the account button, and over the share sheet it opens. Its base z-index already clears the rows and the footer. * fix(dashboard): let the last Files rows scroll clear of the phone selection bar In phone select mode the list's bottom padding only cleared the footer, so the last row stayed behind the floating action bar even fully scrolled. * fix(dashboard): keep the phone Files footer and selection bar above the home indicator The installed app runs edge to edge, so the footer's item count sat under the iPhone home indicator. The footer now grows by the bottom safe-area inset, and the list padding and floating action bar move up with it, as the Apps tab and the dashboard's bottom sheets already do. * fix(dashboard): clear the home indicator in the item properties sheet On phones the sheet sits flush with the bottom edge, and unlike the share sheet it mirrors, its body didn't pad for the bottom safe-area inset, so the last details scrolled under the iPhone home indicator. * fix(dashboard): keep the app feedback sheet's buttons above the home indicator As a phone bottom sheet it sits flush with the bottom edge, so Cancel and Send ended under the iPhone home indicator. * fix(dashboard): size the touch context menu as a card on tablets The touch menu took 90% of the viewport, which on an iPad meant a ~920px strip with its labels far from the row they belong to. It now stops at 400px, which every phone stays under. Its height is also measured after the width is set, so the clamp to the viewport uses the real height. * fix(dashboard): let the Apps modals scroll in a short viewport The Add-an-app and uninstall cards had no height cap and their overlay centers without scrolling, so on a landscape phone with the keyboard up (the viewport resizes for it) the request form's title and buttons sat off-screen with no way to reach them. * fix(dashboard): align Account and Security with the other tabs on phones Below 768px their wrapper added a 16px gutter inside the content area's own, so both tabs sat 32px in while Home, Apps and Usage (and the menu button above them) sit at 16px. The extra padding stays for Apps, whose content area has none. * fix(dashboard): keep Home's recent apps inside their card on tablets Each recent app was a fixed 200px inside 1fr columns, whose minimum is that width, so in the half-width card on a landscape iPad the second column ran past the card's edge and its name was cut off with the ellipsis out of view. The rows now shrink with their column. * fix(dashboard): keep every Files folder reachable in a short viewport Trash was fixed to the bottom of the viewport, so in a landscape phone or a short window it sat on top of Public and Videos, while Shared ran below the column with no way to scroll to it. The folder column now scrolls, and Trash sits at its foot in the flow: in the same spot when there is room, after Shared when there isn't. * fix(dashboard): don't leave Account and Security buttons highlighted after a tap Their hover background wasn't limited to hover-capable pointers like the rest of the dashboard's hover styles, so on touch devices a tapped button (Change Password, Manage Sessions, ...) stayed shaded after its dialog closed. * fix(dashboard): give the Files row menu button a full-size touch target On phones the row's '⋯' is 40x23, right beside a row whose tap opens the item, so a slightly-off tap opened the file or folder instead of its menu. In the two-row layout it now has a 44px square hit area on touch screens. * fix(dashboard): enlarge the Files back, forward and up touch targets They were 28px squares on touch screens. On coarse pointers they now take 36px with a matching negative margin, so the toolbar layout is unchanged. * fix(dashboard): size Home's recent apps card to its apps on phones Its 310px minimum height matches the square welcome card beside it on wider screens. In the single-column layout the welcome card drops its square shape but the minimum stayed, leaving a block of empty card under a short list of recent apps. * fix(dashboard): stop clipping app names' descenders on phones and small tablets Below 768px the tile height budgeted 52px icon + 10px margin + 12px for a label line that is 15.6px tall, so the bottoms of g, p and y were cut off in every app name. The tile now has the full line (78px) and the row gap gives back the 4px, so tiles sit exactly where they did. * fix(dashboard): show Usage amounts in full on phones Every Usage table cell ellipsized, numbers included, so on a phone a token count like 12,345,678 read as "12,345...". Below 480px the units and cost columns now keep their full width and the resource name is the one that truncates. Wider layouts are unchanged. * fix(dashboard): stack Home's recent apps when their card is narrow In the two-column Home on a portrait tablet (iPad Air and Pro 11 at 820-834px, or 769px windows) the Apps card is about 200px wide, and two columns of recent apps cut every name to a letter or two. The list now switches to one column on the card's own width; 1024px and wider keep two. * fix(dashboard): fit two grid tiles a row in narrow Files listings Grid view's 170px column minimum left one 330px tile per row on 360px phones (and in a tablet's narrow listing), so barely two files fit on screen. Narrow listings now use two equal columns, and the tile's icon box shrinks with the tile instead of keeping its fixed 150px. * fix(dashboard): align the Shared and Trash rows' menu button on phones Item rows get position: relative from jQuery UI draggable, but the Shared and Trash rows above Home's folders aren't draggable, so their '⋯' was positioned against the whole list and sat 3px off from every other row's. |
||
|
|
631b264c1c |
test(ai): every AI driver holds and meters at the AI cost factor (#4304)
Image, video, TTS, speech-to-text and voice-changer driver tests run one request at factor 1 and again with every `ai.cost.factor.*` hook set to 2, and check that both the credit hold and the recorded cost double. Chat and OCR already had this coverage. A driver that hands a provider the raw metering service instead of its factored view now fails its test. `withAiCostFactorHook` in integrationTestUtil keeps the hook in place for a callback, since providers record their usage unawaited. |
||
|
|
809c303117 |
fix(ai): treat a gateway 4xx with no upstream error body as a route failure (#4301)
Infron (a new-api gateway) answers "bad response status code N" with the upstream's status when the upstream replied without a JSON error, e.g. a model the upstream no longer serves. That 4xx was classified as the caller's bad request, so the request skipped fallback, the dead route stayed in rotation, and the caller got upstream_bad_request. It now counts as an upstream failure: fallback runs, the route is marked unhealthy, and an exhausted chain reports upstream_provider_unavailable. A gateway error that carries the vendor's own message still stays request-level. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
afad53d3f4 |
feat(gui): add Filipino and restore the language menu (#4263)
The desktop built the language list and never placed it on the user menu, so a saved locale could not be chosen. Register Filipino and show that list. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
8b8dd19eae |
fix(teams): refuse to boot when teams_enabled has nothing registering Teams (#4300)
* fix(teams): refuse to boot when teams_enabled has nothing registering Teams Core's seat checks (team 2FA, seat password reset, email and username changes, self-delete, the signup placeholder claim) read the team store and skip when it is absent. With `teams_enabled` on and no extension registering the store and service, the server now fails at boot instead of serving without those checks. - whoami no longer string-matches the extension proxy's "missing property" error: with the flag on, the store is always there. - Tests that turned the flag on without Teams drop it; the puter.js harness sets it only under the `teams` capability. - Drop a stale services/index.ts comment. * refactor(gui): drop the Teams console styles, which ship with the console now |
||
|
|
1fdde02f6a |
feat(metering): free and no-allowance plan traits, provisional resolver answers (#4284)
* feat(metering): let registered plans declare free and no-allowance traits
A policy passed to registerPolicy can now set `free: true`, so
isFreeSubscription and everything that reads it count the plan as free.
It can also set `noAllowance: true`: the plan grants no monthly
allowance, so only purchased credit can be spent, and the cached
credit check refuses an actor that has none. A non-positive allowance
without the trait still means the plan is not metered.
getActorSubscriptionStatus returns the plan along with whether it was
paid for and whether the answer is provisional (a lookup behind it
failed, or the actor lacks a field resolvers key on), so callers can
tell a fallback from a resolved plan. getActorSubscription is unchanged.
* feat(metering): provisional resolver answers, and tighter no-allowance accounting
A subscription resolver can answer { id, provisional: true }. The answer
wins like any id but is cached only for the fallback window.
On a no-allowance plan, the month record now records allowanceUsed = 0,
so spend that purchased credit paid for is not counted again against an
allowance the actor gets later in the month. An actor that never had
budget raises no credit-state event. setMonthAllowance refuses plans
whose allowance an override never applies to.
* fix(metering): keep resolved plans clean when a default fails, and announce moves onto no budget
A failed or provisional default resolver now marks the answer provisional
only when the default is what applied; an explicit plan that won is
cached for the full window. A provisional answer with no id still marks
the result provisional.
A plan or credit change now re-arms an actor's credit alert instead of
forgetting it, so an actor moving onto a plan with no budget hears
'exhausted' once. An actor never seen with budget still hears nothing.
* chore(metering): let a provisional answer carry no id, and fix two resolver docs
* chore: one-line comments
|
||
|
|
1627650abc |
refactor: make teams an optional feature (#4248)
release-please / release-please (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
* refactor: let core run without a teams registration
Teams becomes optional: `services.team` and `stores.team` may be absent, and
core stops assuming either is there.
Seventeen call sites across share, signup, account deletion and the homepage
now degrade instead of throwing: a team recipient that cannot be resolved is
"no such recipient" rather than a fault, a group share folds in nothing, and
account deletion stops asking what a seat was charged for. This generalises
the pattern `gates.ts` and `SocketService` already used.
Teams' own files take a private handle to their store and service, so the
feature's code stays free of optionality that only core needs.
No behaviour changes where Teams is registered.
* refactor: put a contract between core and teams
`core/teams.ts` holds the shapes core reads and the calls it makes, and the
registries declare the pair as provided rather than built in. The service,
store and controller are registered by whatever runs the feature.
Typing them properly turned up eight AuthController call sites the previous
commit could not see: the controller reads its stores from the built-in
registry, so an absent `team` fell to an index signature and typed as
`unknown` instead of erroring. They now go through the same optional path as
the rest.
The feature's own tests move with it. The cases in core suites that build a
team to exercise a core rule still need relocating, so those suites are red
here.
* test: stub core's seat rules, rehome the rest with the feature
A core suite can no longer build a team, so the rules that refuse a seat get
`testFixtures/seatStub.ts` instead: it answers the contract, which is all
those rules read, and puts the registry back when the test ends.
Seventeen cases move that way. Four could not: group shares and a seat's 2FA
enrolment over HTTP need the real feature and the `/teams` route, so they go
with it, as does the whole of `GroupGrants.test.ts`.
Two whoami cases asserted a seat's team through `extension.import`, which
resolves the registry rather than the stores object. They now assert the
absent case, and the behaviour they covered is asserted against
`buildUserDetails`, which takes its stores as an argument.
Suite: 12252 passed, 38 skipped.
* refactor: take the teams console out of the built-in tabs
`TabTeams`, `teamsConsole` and `teamActionIcons` are no longer built in. The
dashboard already invites a tab through its `dashboard-will-open` event, so
whatever provides Teams contributes the console the same way.
Its 113 strings go with it and register into the shipped English dictionary
at load, so `i18n('teams_*')` resolves as it did. Only `en.js` ever carried
them, so no translation is lost.
Three modules stay, because things other than the console use them:
`teamBadge` (the dashboard shell), `orgSeat` (Home, Account, Security) and
`shareTeams` (the share picker). They already render nothing when the
account has no team. `teams_account_of` stays with `teamBadge` for the same
reason, and the `share_*` and `plan_team_*` strings stay with their callers.
The GUI builds clean with no console at all, and builds it in when the tab
is contributed.
* docs: seats past the owner's current limit get no allowance
|
||
|
|
4968a69d45 |
fix(s3): keep credential-chain credentials an hour from expiry when signing (#4299)
release-please / release-please (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
Presigned URLs die with the credentials that signed them, and the SDK only refreshes temporary credentials five minutes before expiry, so a URL signed late in a credential's life could expire long before its requested lifetime (up to an hour). The chain provider now reports expiry an hour early and force-refreshes credentials that are already inside that window. |
||
|
|
aa47dcd3e5 |
feat(workers): internal teardown that takes the script down before the row, and destroy reports Cloudflare refusals (PUT-2219) (#4297)
release-please / release-please (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
|
||
|
|
c8424f6775 |
fix(fetch): raise HTTP/2 receive windows for plain fetch (#4296)
Node 26's bundled undici negotiates HTTP/2 by default with a 256 KB per-stream receive window, which capped large downloads at about window / RTT (8.6 MB/s vs 46 MB/s over HTTP/1.1 at 23 ms). The guard swaps in an Agent of the same class with 4 MB stream / 16 MB connection windows. |
||
|
|
b3c85c86e9 |
feat(puter.js): open the upgrade page with what was refused (#4294)
The web upgrade dialog's button opens the dashboard with the refusal reason, the app the call ran as, and the refused method as query params, so the dashboard can open its upgrade flow with that context. |
||
|
|
d22d419c4d |
chore: move to Node 26 (#4295)
* chore: move to Node 26 Docker image and CI run on Node 26 (bookworm-slim, same Debian base as before). Node 26's bundled undici reads the global dispatcher from the `undici.globalDispatcher.2` slot, so the guard's 10-minute timeout wrap on `.1` no longer reached plain fetch() and provider calls fell back to the 5-minute headers timeout. The guard now wraps every present slot and tracks wrapped dispatchers in a set, since compose() can return a Proxy that writes a marker property through to the original dispatcher. * test(puter-js): disable Node's web storage in test workers Node 26 defines its own localStorage global, undefined without a storage file, and vitest's jsdom environment doesn't replace globals that already exist, so jsdom tests saw localStorage as undefined. |
||
|
|
0760530d0c | feat(gui): edit display name and bio from the Account tab (#4293) | ||
|
|
e18a5c2b54 |
fix(apps): refuse claimed subdomains as app index_url hosts (PUT-2110) (#4267)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
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
index_url may not point at a system host: the origin, the api host, or a subdomain an extension has claimed. App create and update now apply the same refusal to `<claimed>.<domain>` (any case, any port) that they apply to the origin and api hosts. The host-to-claim match is a new exported `subdomainLabelOf` in hostRedirects, also used by the claim dispatch middleware, so both decide the same way. Co-authored-by: Daniel Salazar <daniel.salazar@puter.com> |
||
|
|
75385501a7 |
fix(fs): support offset and byte_count in read operations (#4266)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
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
* fix(fs): support offset and byte_count in read operations Allow partial content reads via offset and byte_count parameters in puter.fs.read and backend FS controllers. - In puter.js read operation, check options.offset and options.byte_count against null/undefined so offset: 0 is preserved in query params. - In handle_resp, accept any 2xx status code so 206 Partial Content responses resolve successfully. - In backend FS controllers (LegacyFSController and FSController), derive the HTTP byte range from query offset/byte_count parameters when an explicit Range header is not present. - Add unit and integration tests across controllers and SDK test suites. * fix(fs): strict range params, 416 past EOF, drop format churn - resolveReadRange accepts only decimal digit strings and safe integers - readContent maps S3 InvalidRange to 416 instead of a 500 - revert unrelated union-type reformatting, restore truncated test comment --------- Co-authored-by: Daniel Salazar <daniel.salazar@puter.com> |
||
|
|
5b0205f2bf |
fix(hosting): apps publish with write again (#4291)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
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
Reverts the hosting part of #4290. manage stays a user-to-user grant; an app with write on its user's directory can publish and keep serving it. |
||
|
|
3bce3b056e | misc: hardening (#4290) | ||
|
|
f70240784f |
chore(deps): bump follow-redirects from 1.16.0 to 1.16.1 (#4287)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.16.0 to 1.16.1. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.16.0...v1.16.1) --- updated-dependencies: - dependency-name: follow-redirects dependency-version: 1.16.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
356e7b2b94 |
chore(deps): bump follow-redirects in /src/puter-js (#4288)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.16.0 to 1.16.1. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.16.0...v1.16.1) --- updated-dependencies: - dependency-name: follow-redirects dependency-version: 1.16.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
f02b4a6b73 |
feat: tell apps when a launched item is a folder; Open with AI above Open With
A folder opened with an app (Open With, Open with AI) reached puter.js as a file: the GUI never forwarded the signature's fsentry_is_dir, so the launched FSItem always had isDirectory false. The GUI now appends puter.item.is_dir, and puter.js reads it in onLaunchedWithItems and the onItemsOpened launch replay. Both now share one URL-to-FSItem builder. "Open with AI" moves directly under Open, above Open With, in the desktop and dashboard file menus. |
||
|
|
0768978111 |
chore(deps-dev): bump vitest from 4.1.11 to 4.1.12 (#4275)
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 4.1.11 to 4.1.12. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.12/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 4.1.12 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
66093e78f3 |
feat(gui): "Open with AI" in the dashboard Files menu, opening like Open With (#4289)
"Open with AI" now launches the `ai` app with the item the same way
Open With launches any app: launch_app with file_path/file_uid, so the
app gets a signed launch item via puter.ui.onLaunchedWithItems. It
replaces the custom `ai:openFsEntries` postMessage, which went to an
existing AI window.
- The helper moved from UIItem into helpers/openWithAI.js, shared by the
desktop item menu and the dashboard Files menu (generate_file_context_menu).
- A shortcut opens its target.
- Dropped from multi-select menus, since a launch opens a single item.
- Renamed open_in_ai -> open_with_ai ("Open with AI") and removed the
unused ai_app_unavailable string.
|
||
|
|
ed531e3992 |
chore(deps): bump undici from 7.30.0 to 7.30.1 (#4280)
Bumps [undici](https://github.com/nodejs/undici) from 7.30.0 to 7.30.1. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.30.0...v7.30.1) --- updated-dependencies: - dependency-name: undici dependency-version: 7.30.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
53e27f3d41 |
chore(deps): bump mysql2 from 3.24.5 to 3.24.6 (#4279)
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.24.5 to 3.24.6. - [Release notes](https://github.com/sidorares/node-mysql2/releases) - [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md) - [Commits](https://github.com/sidorares/node-mysql2/compare/v3.24.5...v3.24.6) --- updated-dependencies: - dependency-name: mysql2 dependency-version: 3.24.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
deac121675 |
chore(deps-dev): bump eslint-plugin-prettier from 5.5.6 to 5.5.7 (#4278)
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.5.6 to 5.5.7. - [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases) - [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.5.6...v5.5.7) --- updated-dependencies: - dependency-name: eslint-plugin-prettier dependency-version: 5.5.7 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
53e6e3179b |
refactor(gui): remove the AI side panel and its desktop button (#4286)
The desktop no longer launches the `ai` app at boot. This removes the `is_panel` window mode, which existed only for that launch, and the toolbar/taskbar/window/popover offsets tied to it. It also removes the desktop sparkles button that showed the panel, and the `silent_on_failure` launch option, whose only caller was the boot launch. "Open in AI" in the item context menu still launches the app as a normal window. |
||
|
|
73624bf1d0 |
chore(deps): bump smtp-server from 3.19.21 to 3.19.23 (#4276)
Bumps [smtp-server](https://github.com/nodemailer/smtp-server) from 3.19.21 to 3.19.23. - [Release notes](https://github.com/nodemailer/smtp-server/releases) - [Changelog](https://github.com/nodemailer/smtp-server/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodemailer/smtp-server/compare/v3.19.21...v3.19.23) --- updated-dependencies: - dependency-name: smtp-server dependency-version: 3.19.23 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
1a9e43f5e5 |
fix(dashboard): adversarial review — bugs, a11y, perf, i18n (#4285)
* fix(dashboard): size the Notifications sidebar row like its neighbors The bell button's font: inherit overrode the sidebar item's font-size (16px instead of 14px, 15px in the drawer), and its icon wrapper stayed 18px while the drawer grows nav icons to 20px. * test(dashboard): scope the directory-note assertion to the directory row The 2FA row reuses .teams-directory-note, so the unscoped selector read both notes and the test failed. * fix(dashboard): only treat a picked file as a new avatar while the avatar picker is open The Account tab listened for every file_opened on the dashboard window, so choosing an image for a web link's Change Icon in the Files tab opened the profile-picture crop modal, and saving it replaced the avatar. * fix(dashboard): let dialogs over the Files tab keep their keyboard The Files keydown handler ran under an open confirm, so pressing Enter on the focused Delete button of the permanent-delete prompt opened the trashed item instead (stacking a second alert), and arrows or Delete acted on the rows behind the dialog. * fix(dashboard): keep Apps grid keys away from an open context menu Choosing a tile's menu item with Enter (e.g. Open in new tab) also launched the focused app in place, and arrows moved tile focus while they walked the menu. * fix(dashboard): Escape on a menu or modal inside an app folder keeps the folder open The menu's and the uninstall modal's Escape handlers removed them before the folder's listener ran, so it saw nothing above it and closed the folder with the same keystroke. * fix(dashboard): a tap on the drawer or sheet scrim only dismisses it Outside-press handlers closed the mobile drawer and the notifications sheet on touchstart/pointerdown, which took away the scrim's hit area before the click, so the tap also landed on the app tile or file row underneath. * fix(dashboard): let the sidebar nav scroll on short viewports The nav clipped its overflow, so in a landscape phone's drawer (or a short desktop window) Usage, Account, and Security fell below the fold with no way to reach them. * fix(dashboard): keep the mobile drawer full width when the desktop sidebar is collapsed The remembered collapse outranked the drawer's width rule, so below the drawer breakpoint it opened as an unlabeled 50px icon strip, and the toggle that would undo it is hidden there. * perf(dashboard): stop refetching account data each time focus leaves an app Home's window focus listener fired whenever focus came back from an app's iframe, so every click from an app to the dashboard re-pulled whoami, subscription, storage, and usage (5 requests), on any tab. It now only refreshes after the page itself lost focus to another window. * fix(dashboard): show a seat's team name unescaped in the plan card i18n escapes its arguments for HTML, but the note is set with .text(), so a team called "Bob's & Co" read "Managed by Bob's & Co". * fix(dashboard): stop double-escaping app names in uninstall alerts UIAlert escapes its message, so an app titled "Tom & Jerry" read "Tom & Jerry" in the uninstall-failed and sessions-not-ended alerts. * fix(dashboard): roll back an uninstall whose revoke failed puter.perms.revokeApp resolves with error set instead of rejecting, so a failed revoke still removed the tile, marked the app removed for later sessions, and ended its sign-in, while the app kept its permissions and the rollback and error alert never ran. * fix(dashboard): read a team account's suspension when its name matches an Object member The per-account state map was a plain object, so a username such as constructor or toString was already "in" it, its audit entries were skipped, and a suspended account showed as active with no Delete. * fix(dashboard): update the profile card and sidebar name after a username change Neither carried the .username class that the rename refreshes, so both kept showing the old name until reload. * fix(dashboard): ask how to restore a trashed item whose name is taken Restore moved without overwrite or dedupe, so when the original location already had an item of that name the move 409'd and nothing happened: an uncaught error from the item menu, a console line from the others. All three restore entry points now share one path that offers the Replace / Keep Both prompt the paste flow uses, alerts on other errors, and updates the footer and empty notice once the rows are gone. * fix(dashboard): refresh a renamed row's name, icon, and position in the Files tab The Files tab passed rename_file a callback it never accepted, so a renamed row kept the desktop's 20-character truncation even in a wide column, kept its old type icon, and stayed out of sort order. rename_file now takes an optional onDone (carried through undo), which the Files tab uses to fit the name to its column, refresh the icon and type, and move the row into place. A cancelled rename no longer truncates the name either. * fix(dashboard): drop Publish and Properties from the Shared view's menu The Shared view is a query, not a folder, so both acted on the virtual shared path: Properties showed an unknown error and Publish opened a dialog for a path that doesn't exist. * fix(dashboard): close the Files image preview on Escape and when leaving the folder Escape cleared the selection but left the Space preview open with nothing selected, the preview stayed over a different folder after navigating, and a preview whose URL arrived after it was dismissed or replaced still opened. * fix(dashboard): don't move Files history for a navigation that's dropped renderDirectory ignores a navigation while another folder is loading, but every caller had already pushed or stepped history, so after a click during a load the listing and history disagreed and the next Back or Forward seemed to do nothing. History now only moves when the render will run; back/forward steps share one helper. * fix(dashboard): reopening the current folder adds no Files history entry Clicking the folder already on screen (e.g. Home in the sidebar) pushed a duplicate entry, so the next Back reloaded the same folder. * fix(dashboard): show installed apps when the recommended list fails to load get-launch-apps sat in the same Promise.all as the installed apps and its body was parsed without a status check, so a network error or a proxy's HTML error page replaced the whole grid with "Failed to load apps". A failed list now falls back to the last one that loaded. * fix(dashboard): fit the app uninstall dialog on narrow phones The card is mounted outside the dashboard's border-box rule, so its 320px min-width plus padding came to 370px and was clipped on 360px screens; a long unbroken app title also overflowed it. Desktop sizing is unchanged. * fix(dashboard): keep a new team account's one-time password on screen until dismissed The credential box lived only in the DOM inside the panel that every refresh repaints, so revisiting the Teams tab or any later action wiped a password that can't be retrieved again. It is now kept in state, for the team it belongs to, and drawn with the panel. * fix(dashboard): reload the Teams tab after a change made while it was loading refresh() handed back any load already in flight, so an action or team switch made during a revisit's background load awaited data read before the change: a toggled setting snapped back, and a team switch could paint one team's roster next to another's record. A change during a load now queues one more load, the stale one skips its paint, and revisiting the tab doesn't start a second load while init's is still running. * fix(dashboard): send one re-enable request for a double click in Teams Re-enable has no confirm dialog to absorb a second click, and nothing held the action while it ran, so a double click enabled the account twice (two audit entries). A suspend or re-enable already in flight for an account now ignores further clicks. * fix(dashboard): revert the avatar and say so when saving a new photo fails The crop's Save showed the new picture everywhere and revealed Remove without waiting for the upload, and update_profile swallows failures, so a failed save looked saved until reload. The save is now awaited: a failure puts the previous picture back with an error hint, and Remove waits for the upload so it can't race it. * fix(dashboard): hide the Usage tab's plan button for team seats A seat's plan belongs to its team, so Home already hides its upgrade link; the Usage tab still offered Upgrade, which only reaches a refusal. * fix(dashboard): don't drop Home's forced usage refresh during a running load loadUsageData handed any caller the load already in flight before looking at force, so a subscription change (or a return from the billing portal) during a routine load was answered with data read before the change, and the plan card stayed stale until the next refresh. A forced call now runs once more after the running load. * fix(dashboard): activate Apps when the URL names an unknown tab An unknown hash rendered Apps as the active tab but skipped its activation, so the grid kept the generic content padding and Apps' onActivate never ran. * fix(dashboard): make the sidebar user menu reachable from the keyboard The user button was a div with no tabindex, so Log Out, account switching, and Contact Us were out of reach without a pointer. It is now a button (rendering unchanged), with aria-haspopup. * fix(dashboard): name the sidebar's collapse and drawer buttons for assistive tech The collapse toggle held only two unlabeled images, so screen readers announced an unnamed button; it now carries a translated label and aria-expanded that follow its state. The drawer's open/close labels were hardcoded English and now go through i18n. * fix(dashboard): make Home's recent apps reachable from the keyboard Each recent app was a click-only div, so keyboard users couldn't open one. They are now buttons with a focus ring; the icon is decorative since the title names the button. * fix(dashboard): name the Security tab's 2FA switch for assistive tech The checkbox sat in a text-less label, so screen readers announced an unnamed checkbox. It now carries the translated Two Factor Authentication label, as the Teams switches do. * fix(dashboard): sort the Usage table from the keyboard The sortable headers were click-only th cells. Each now holds a button, carries aria-sort, and keeps focus across the re-render a sort causes. The Resource/Units/Cost headers go through i18n. * fix(dashboard): don't rebuild the Files listing under an open menu Returning to the browser tab re-rendered the folder even with a context menu or action sheet open, so its actions then targeted detached rows: Rename did nothing, Restore faded a row that was already gone. The refresh now waits while a menu is up, as it does for a rename. * fix(dashboard): point the Files sidebar's Documents at the current home The entry read window.documents_path, which globals.js sets once from the cached user and nothing updates; login and username changes keep window.docs_path current instead. After a username change, Documents pointed at the old home and showed "This folder couldn't be opened", even after reloading. * fix(dashboard): let only the newest share listing render in the share dialog Each action ends in a refresh, and refreshes from overlapping actions (or the opening load) could finish out of order, so an older listing landed last and undid what the user just did, e.g. the link control snapped back to restricted after turning the link on. Older refreshes now drop their result. * perf(dashboard): load a team's record, roster, and plan in parallel loadSelected awaited the audit log (every page of it), then the members, then the plan, on every visit to the Teams tab. They don't depend on each other, so they are now fetched together. * fix(dashboard): keep keyboard focus on the Teams control that was used Every Teams action repaints the whole panel, which dropped focus to <body>: a keyboard user toggling a switch or suspending an account was sent back to the top of the page. The repaint now refocuses the same control, or the same account's new action when the old one is gone. * fix(dashboard): give the app uninstall prompt dialog semantics and focus The prompt had no dialog role or label and never took focus, so keyboard and screen-reader users stayed on the inert grid behind it. It is now a labelled modal dialog that focuses Cancel on open and returns focus to the app's tile when dismissed. * fix(dashboard): keep the add-an-app tile when there are no apps With no apps at all, the empty state replaced the whole grid, add tile included, so a new user on an instance with no recommended apps had nothing to act on. The empty line now sits above a grid that keeps the tile, as a search with no matches already does. * fix(dashboard): don't rebuild the Apps grid under an active drag An uninstall finishing (or a failed one rolling back) re-rendered the grid even while another tile was being dragged, replacing the dragged tile with a fresh copy: the copy showed under the ghost and the drop was lost. Renders asked for mid-drag now wait for the one the drop does; the folder eject, which rebuilds the grid as part of the drag, still can. * fix(dashboard): return focus to the drawer toggle when the mobile drawer closes Closing the drawer (Escape, the close button, a tab pick) left focus on controls that had just become hidden, so it fell to <body>; closing the notifications sheet opened from the drawer tried to refocus the bell inside the closed drawer. Both now land on the drawer toggle. * fix(dashboard): truncate Files names between graphemes, with a binary search The middle truncation sliced UTF-16 code units, so at many widths a long name with emoji showed a half surrogate (rendered as a replacement character) or a broken joined emoji. It also measured one more character per step, a measureText call per character of every long name on each render and column resize. The cut now falls between graphemes and is found by binary search; output for other names is unchanged. * fix(dashboard): translate the Home tab's hardcoded English The tab label, the recent-apps empty state, card subtitles, the plan badge and trial wording, the upgrade/manage links, and the usage cards' "Used" / "of" lines were English literals. They now go through i18n, with placeholders where values are interpolated. * fix(dashboard): translate the sidebar's remaining English The Apps, Files, and Usage tab labels, the user button's fallback name, and the user menu's Developers item were English literals; they now use i18n keys like the other tabs. * fix(dashboard): translate the Usage tab's hardcoded English The subtitle, the "used of" meter lines (now the existing used_of key), Usage Details, the plan button, Show more/less, and the details error were English literals. * fix(dashboard): translate the Account and Security tabs' hardcoded English Both subtitles, the delete-account description, the sessions hint, the username fallback, and the avatar picker's window title were English literals. * fix(dashboard): translate the Apps tab's hardcoded English The uninstall prompt and its failure alert, empty and no-match notices, pager and folder-dot labels, the search placeholder, the edit toggle, and the tile menu's Open in New Tab / Quit / Uninstall were English literals. * fix(dashboard): translate the Files tab's hardcoded English The folder sidebar, footer counts (now the existing item-count keys), sort and view menus, upload status and errors, the empty and can't-open notices, the reserved-name alerts, the move error, and the loading text were English literals. * fix(dashboard): translate the action sheet's Back row and match delete in any language The touch action sheet styled an item red when its label contained the English word "delete", so Delete lost its danger styling in every other language; it now compares against the translated Delete labels. Its submenu Back row and the properties panel's Directory/disassociate labels were English literals. * fix(dashboard): make the Files sidebar, crumbs, and nav arrows keyboard-operable The folder sidebar, the breadcrumbs, and the back/forward/up arrows were click-only li/span/img elements, out of the tab order. They now take focus, carry button/link roles (the arrows with labels and aria-disabled), answer Enter and Space, and show a focus ring. The rows' Enter/Space shortcuts also no longer swallow those keys from a focused control: with rows selected, Enter on Upload opened the selection. * perf(dashboard): read grid name widths before writing any in the Files tab fitItemNames read each tile's name width right after writing the previous name, forcing a layout per row. Reading them all first, then writing, cuts switching 400 files into grid view from about 300ms to 170ms; the rendered names are identical. |
||
|
|
242a654166 |
feat(ai): add Kling for video, image and speech generation (#4274)
* feat(ai): add Kling for video, image and speech generation One `kling` API key now registers a provider on three drivers: - txt2vid: Kling 3.0 Turbo, 3.0, 3.0 Omni, O1, 2.6 and 2.5 Turbo, with first/last frames, reference images on the omni models, native audio, and editing a source video on 3.0 Omni and O1. Billed per second at the resolution, audio and video-input rate; an edit is held at the longest source and billed for the clip Kling returns. - txt2img: Kling Image 3.0, 3.0 Omni, O1 and 2.1, billed per image. - txt2speech: Kling TTS with its English and Chinese voices, billed per request. Kling's business error codes map onto the driver error contract, so an exhausted resource pack reports as credit exhaustion and a submit-time content block as `moderation_flagged`. "Risk control" failures now count as content-filter refusals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ai): treat Kling's unknown-model codes as a Puter-side failure Kling answers 1202/1203 when the method or model isn't one it serves. Puter picks both, so these meant catalog drift but surfaced as a 400 that blamed the caller and raised no alarm. They now return 502 upstream_failed with a warning alarm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ai): tighten Kling input checks and error mapping - 3.0 Turbo takes at most 2,500 prompt characters with a first frame, the image-to-video endpoint's limit. - O1 refuses reference images together with a first and last frame. - 3.0 Omni accepts a source of up to 15.5 seconds, and an edit is held at 16 seconds to cover it. - A source video Kling can't decode fails as 400 upstream_bad_request instead of a Puter-side 502. - A 1201 about the model counts as catalog drift, like 1202/1203. - Auth failures no longer pass Kling's console wording to the caller. Adds integration tests for the image, video and TTS providers, gated on PUTER_TEST_AI_KLING_API_KEY. * feat(ai): wait up to 14 minutes for a video job Video jobs, Kling edits in particular, can take over 10 minutes. The poll window and the Gemini Omni deadline move to 14 minutes, under the SDK's 15-minute idle timeout, with holds and signed source links lengthened to match. The keep-alive default moves to 920 seconds so it stays above a 15-minute proxy idle timeout. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Daniel Salazar <daniel.salazar@puter.com> |
||
|
|
41e4b582d3 |
fix(subdomains): emit subdomain.delete for sites deleted with an app (#4273)
Sites and workers tied to an app go with it by FK cascade, on app delete and on deleting the app owner's account. Only caches were dropped for them, so the subdomain.delete listeners (CDN purge, worker script teardown, worker sessions, site cleanup) never ran. |
||
|
|
2a49689fad |
fix contradicting jsdocs and docs (#4282)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
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
|
||
|
|
b54b76715a |
feat(fs): finish a home rename that stopped partway (#4255)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
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(fs): finish a home rename that stopped partway FSEntryStore.rebaseUserSubtree runs renameUserHome's owner-scoped cascade on its own (same subtree matching, same rebased-cache invalidation) and also drops the home row's cached copies at either path. countUserSubtree counts the owner's rows under a prefix with that same matching, from the primary. renameUserHome now uses the shared cascade. * fix(telemetry): resolve the tracer per span so a test's provider is the one used withSpan held a ProxyTracer built at module load, which caches the first provider it delegates to. Test files sharing a worker then sent spans to a provider an earlier file had already shut down. * fix(fs): drop cached copies of only the rows a home rebase moves The owner-scoped rewrite read its invalidation set from everything under the new prefix, which for a finishing rebase is the whole live home; it now reads the rows it is about to move. --------- Co-authored-by: Daniel Salazar <daniel.salazar@puter.com> |
||
|
|
d978538c9d |
feat: let a subdomain claim keep the main pipeline (#4268)
`extension.claimSubdomain(name, { mainPipeline: true })` serves
`<name>.<domain>` the way core's own subdomains are served: through CORS,
body parsing, auth and every route, `subdomain: '*'` ones included. Routes
declared for the name are ordinary routes, with no option restrictions.
Gated claims (`{ authorize, checkRoute? }`) behave as before.
onlyoffice's subdomain now comes from its extension's claim, so core no
longer lists it among the subdomains it serves itself. Without that claim
`onlyoffice.<domain>` gets the unclaimed-subdomain 404 like any other name.
|
||
|
|
e83e27f306 |
feat(ai): edit a source video with txt2vid (Gemini Omni, FLUX 3) (#4272)
* feat(ai): edit a source video with txt2vid on Gemini Omni txt2vid takes an `inputVideo` option (sent as `input_video`): a URL, data URI or Puter file path to an MP4, QuickTime or WebM video of up to 32 MiB. Gemini Omni receives it inline next to the prompt, which describes the edit or extension. Google enforces the 10-second limit. Models declare `videoInput` in the catalog, and the driver rejects a source video for any other model instead of letting the provider drop it. The credit hold adds 16,000 input tokens for a source video, since countTokens reports far fewer tokens than the reply bills. loadFileInput takes an optional `timeoutMs` for web URLs, and cancels the body of a failed response. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ai): edit a source video with FLUX 3 on Together AI FLUX 3 takes `input_video` as Together's `media.source_video`. A web URL goes through for Together to fetch; a data URI or Puter file is read with the caller's access, capped at 32 MiB, sniffed as a video and sent inline. Editing is estimated and billed at the 1080p rate of $0.54/s, since FLUX 3 jobs report no cost. Image inputs can't be combined with a source video. Seedance 2.0 ignores `source_video`, treats `reference_videos` as a loose reference rather than an edit, and Seedance 2.5 fails on reference videos, so neither declares `videoInput`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(ai): name FLUX 3 as a video editor in the AI overview Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ai): match Hailuo 02 to Together's price and document estimate billing Together quotes $0.56 for a Hailuo 02 clip; the catalog had $0.49. Together's video jobs no longer report what they cost, so every Together clip is billed at the catalog's highest published rate. The docs said the charge was the amount Together reports; they now describe the estimate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: enforce video edit durations and propagate source fetch cancellation * fix(ai): pass media inputs as Puter files and stream source videos Source videos were read whole into the API process, base64-encoded and sent twice to Gemini (countTokens and the interaction), which held about 128 MiB per in-flight edit for up to ten minutes. - Video inputs are a URL, a Puter file path or a data URI of at most 1 MiB. Together gets a Puter file as a 15-minute presigned S3 URL and fetches it itself; Gemini streams it into the request body with an exact Content-Length, so it's never held as one string. - Gemini counts only the prompt and image for a source video, and both providers read the source only after the credit hold. - txt2img and txt2vid image inputs accept Puter file paths, read within the caller's access right before the provider call. Existing URL, data URI and base64 inputs are unchanged. - puter.js writes a Blob, File or data URI over 1 MiB to a temporary file under .tmp, sends its path, and deletes it once the call settles. - FLUX 3 edits bill at the 720p editing rate, matching their 1280x704 output. * fix(ai): stage inputs after website sign-in, keep images working without a write - puter.js chose the temporary file path before signing a website visitor in, so it fell outside the app's data directory and the upload was refused. It now signs in first, then picks the path. - An image that can't be written (no access, storage full) is sent inline as before instead of failing the call. Videos still fail, since a large inline video isn't accepted. - Telling raw JPEG base64 from a path decodes only the first 24 characters instead of the whole input. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: 404oops <oops404@util.404oops.com> Co-authored-by: Daniel Salazar <daniel.salazar@puter.com> |
||
|
|
54f542ace4 |
fix(gui): refresh Start menu apps without blocking cached rendering (#4244)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
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
* fix(gui): refresh Start menu apps without blocking cached rendering * docs: remove desktop Start menu limits from SDK reference * fix(gui): drop the 1 MiB cap on the launch-apps refresh Recent entries carry each app's raw icon column, which can still hold a data URL of up to 5 MB, so a large icon tripped the cap on every request, including the one at startup, and left the Start menu with no apps. Replace the cap's test with one for the unlaunchable-app filter. --------- Co-authored-by: Daniel Salazar <daniel.salazar@puter.com> |
||
|
|
18b96d529c | fix: misc scan fixes (#4270) | ||
|
|
f146da16d1 |
fix(gui): drop deleted apps from the start menu (#4243)
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
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
The start menu read a session-cached launch-apps list and only refetched when it was empty, so an app deleted after page load (for example in Dev Center) stayed in Recent and Recommended until a reload. Refetch on every open, and filter out entries the backend marks unlaunchable, so a reclaimed or inaccessible app leaves the list too. Apply the same filter to the dashboard's Recent apps. Adds helpers/launchAppsList.js with filterLaunchableApps and its unit test. |
||
|
|
dd56d54753 |
docs(bug-bounty): add eligibility rules (#4269)
Bounties cover user security/privacy and platform security/health only. Reports must show a direct attack without pre-assumed access, and exploits that rely on sharer/sharee trust are hardening or lower severity. Also generalizes the metering non-issue, adds existence-only oracles, and tightens duplicate, proof and first-contact rules. |
||
|
|
e5fd30ccf0 |
fix(ai): validate large base64 by decoded length, not regexps (#4265)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
* fix(ai): validate large base64 without anchored regexp loops
Gemini TTS replies over ~4 MB of base64 (~65 s of audio) failed with
"Maximum call stack size exceeded". Once a process holds enough compiled
code, V8 compiles new regexps unoptimized, and `^[...]+={0,2}$` then
pushes a backtrack entry per character until its stack overflows.
TTS now searches for a character outside the alphabet instead. The raw
base64 icon/reference-image check drops its own anchored regexp;
decodeStrictBase64 already rejects the same inputs.
* perf(ai): validate base64 by decoded length instead of regexp passes
Unoptimized regexp scans dominated large payloads: 186 ms for max-length
TTS audio, 880 ms in decodeStrictBase64 for 30 MB of video. Buffer.from
skips characters outside the alphabet, so comparing the decoded length
with the one the input length predicts catches them for free.
decodeStrictBase64 now does that plus rejecting the URL-safe alphabet
(~10 ms for 30 MB), and TTS reuses it instead of its own check.
Non-canonical trailing bits now decode instead of being rejected.
* fix(ai): reject non-ASCII base64 that Node decodes by its low byte
Node reads code units above 0xFF by their low byte, so U+012B decodes
as '+' and keeps the decoded length the input predicts. Require ASCII
(~1-2 ms for 43M chars) before the length check.
|