Make the dashboard the default interface at root (#3345)

* feat: make the dashboard the default interface at root

The dashboard now loads at / (with /dashboard kept as an alias), and the
desktop moves to a new /desktop route. An "Open Desktop" button pinned at
the bottom of the dashboard sidebar switches to the desktop.

Root URLs that carry a desktop-only flow keep booting the desktop:
auth popups (?embedded_in_popup=), app deep links (?app=), direct
downloads (?download=), fullpage mode (?puter.fullpage=), and iframe
embeds.

Desktop-context redirects are retargeted so desktop users stay on the
desktop: logout, the QR "continue on phone" link, the auth cover-page
logo click, and window close/hide URL resets now use /desktop. OIDC
login/signup gains a strictly whitelisted return_to param (/desktop or
/dashboard) so social login returns to the interface it started from.

Also: the dashboard strips ?ref= like the desktop does, ?auth_token=
cleanup preserves the path and tab hash, the logged-out session list
gets the wallpaper back, ?download= also works at /desktop, and the GUI
dev server serves /desktop and /dashboard.

* Add Open Desktop bento card and CSS

Replace the previous desktop-switch UI with a full-width "Open Desktop" bento card that links to /desktop. Removes the pinned sidebar Open Desktop button and the old desktop-switch button/handler, and adds new CSS variables and styles for the desktop card (icon background, shadow, surface/header colors), hover shadow and arrow transition, and layout/padding adjustments.

Changed files: TabHome.js (removed old switch, added anchor card), UIDashboard.js (removed sidebar button), dashboard.css (new desktop color vars and styles; removed legacy desktop-switch/button styles).

* Remove box-shadow from myapps search input

Delete the box-shadow declarations from input.myapps-search::placeholder in dashboard.css. This simplifies the placeholder styling (removes subtle and light shadow layers), likely to reduce visual noise or fix rendering inconsistency.

* Show index_url hostname for opaque app IDs

Only replace an app's title with the hostname from index_url when the app appears to use an opaque id. Add a check for names starting with 'app-' so the hostname is used only if title, name and uuid are identical and the name looks like an opaque app id. This avoids overwriting meaningful user-facing names that may coincidentally match the uuid and clarifies the intent in the comment.

* Update dashboard.css
This commit is contained in:
Nariman Jelveh
2026-07-06 13:22:27 -07:00
committed by GitHub
parent 3a26f1f1c2
commit c69fb52d5c
13 changed files with 155 additions and 73 deletions
@@ -173,12 +173,14 @@ describe('HomepageController shell routes', () => {
expect(String(captured.body)).toMatch(/<!DOCTYPE html>/i);
});
it('serves the shell on /settings, /dashboard, /action, /@:username', async () => {
it('serves the shell on /settings, /dashboard, /desktop, /action, /@:username', async () => {
for (const path of [
'/settings',
'/settings/*splat',
'/dashboard',
'/dashboard/',
'/desktop',
'/desktop/',
'/action/*splat',
'/@:username',
]) {
@@ -82,6 +82,9 @@ export class HomepageController extends PuterController {
router.get('/dashboard', {}, (req, res) => sendShell(req, res));
router.get('/dashboard/', {}, (req, res) => sendShell(req, res));
router.get('/desktop', {}, (req, res) => sendShell(req, res));
router.get('/desktop/', {}, (req, res) => sendShell(req, res));
router.get('/action/*splat', {}, (req, res) => sendShell(req, res));
router.get('/@:username', {}, (req, res) => sendShell(req, res));
@@ -139,6 +139,19 @@ export class OIDCController extends PuterController {
let appRedirectUri = flowRedirects[flow] ?? (origin || '/');
// Optional GUI return path so login started from /desktop or
// /dashboard lands back there. Strict whitelist — never a
// client-supplied URL (no open redirect).
const rawReturnTo = Array.isArray(req.query.return_to)
? req.query.return_to[0]
: req.query.return_to;
if (
(flow === 'login' || flow === 'signup') &&
(rawReturnTo === '/desktop' || rawReturnTo === '/dashboard')
) {
appRedirectUri = `${origin}${rawReturnTo}`;
}
// Popup support
const rawPopup = Array.isArray(req.query.embedded_in_popup)
? req.query.embedded_in_popup[0]
+1 -1
View File
@@ -52,7 +52,7 @@ startServer(1);
// build the GUI
build();
app.get(['/', '/app/*', '/action/*'], (req, res) => {
app.get(['/', '/app/*', '/action/*', '/desktop', '/dashboard'], (req, res) => {
res.send(generateDevHtml({
env: env,
api_origin: 'https://api.puter.com',
+20 -14
View File
@@ -28,10 +28,12 @@ function buildRecentAppsHTML() {
// Show up to 6 recent apps (2 columns x 3 rows)
const recentApps = window.launch_apps.recent.slice(0, 6);
for (const app_info of recentApps) {
// if title, name and uuid are the same and index_url is set, then show the hostname of index_url
// if title, name and uuid are the same and start with 'app-' and index_url
// is set, then show the hostname of index_url instead of the opaque app id
if (
app_info.name === app_info.title &&
app_info.name === app_info.uuid &&
app_info.name?.startsWith('app-') &&
app_info.index_url
) {
app_info.title = new URL(app_info.index_url).hostname;
@@ -178,14 +180,6 @@ const TabHome = {
h += '</div>';
h += '</div>';
// Desktop switch card (spans full width)
// h += '<div class="bento-card bento-desktop-switch">';
// h += '<div class="bento-desktop-switch-inner">';
// h += '<span class="bento-desktop-switch-text">Looking for Puter\'s desktop interface?</span>';
// h += '<button class="bento-desktop-switch-btn">Switch to Desktop</button>';
// h += '</div>';
// h += '</div>';
// Usage card (spans full width on second row)
h += '<div class="bento-card bento-usage">';
h +=
@@ -208,6 +202,23 @@ const TabHome = {
h += '</div>';
h += '</div>';
// Open Desktop card (spans full width, links to the desktop interface)
h += '<a href="/desktop" target="_blank" rel="noopener" class="bento-card bento-desktop allow-native-ctxmenu">';
h += '<div class="bento-card-fancy-icon bento-card-fancy-icon-desktop">';
h +=
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>';
h += '</div>';
h += '<div class="bento-card-fancy-text">';
h += `<h2>${i18n('open_desktop')}</h2>`;
h += '<span class="bento-card-fancy-subtitle">';
h +=
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>';
h += 'Switch to the desktop interface';
h += '</span>';
h += '</div>';
h += '<span class="bento-desktop-arrow"></span>';
h += '</a>';
h += '</div>';
return h;
},
@@ -271,11 +282,6 @@ const TabHome = {
},
);
// Handle desktop switch button
$el_window.on('click', '.bento-desktop-switch-btn', function () {
window.location.href = '/';
});
// Handle "Save Account" warning click
$el_window.on('click', '.bento-save-account-warning', function (e) {
e.preventDefault();
+8 -2
View File
@@ -149,6 +149,11 @@ async function UIDashboard (options) {
const $el_window = $(el_window);
// Remove `?ref=...` from the URL, keeping the path and tab hash (mirrors UIDesktop)
if ( window.url_query_params?.has('ref') ) {
window.history.pushState(null, document.title, window.location.pathname + window.location.hash);
}
// Restore sidebar collapsed state
if ( localStorage.getItem('dashboard-sidebar-collapsed') === '1' ) {
$el_window.find('.dashboard-sidebar').addClass('collapsed');
@@ -285,8 +290,9 @@ async function UIDashboard (options) {
window.addEventListener('hashchange', handleRouteChange);
window.addEventListener('popstate', handleRouteChange);
// Sidebar item click handler
$el_window.on('click', '.dashboard-sidebar-item', function (e) {
// Sidebar item click handler — only tab items ([data-section]); plain links
// like the "Open Desktop" button navigate natively
$el_window.on('click', '.dashboard-sidebar-item[data-section]', function (e) {
e.preventDefault();
const $this = $(this);
const section = $this.attr('data-section');
+8 -6
View File
@@ -709,9 +709,9 @@ async function UIDesktop (options) {
window.sidebar_items = val;
});
// Remove `?ref=...` from navbar URL
// Remove `?ref=...` from navbar URL, keeping the current path
if ( window.url_query_params.has('ref') ) {
window.history.pushState(null, document.title, '/');
window.history.pushState(null, document.title, window.location.pathname);
}
//show_hidden_files
@@ -1732,7 +1732,7 @@ async function UIDesktop (options) {
try {
stat = await puter.fs.stat({ path: item_path, consistency: 'eventual' });
} catch ( e ) {
window.history.replaceState(null, document.title, '/');
window.history.replaceState(null, document.title, '/desktop');
UIAlert({
message: i18n('error_user_or_path_not_found'),
type: 'error',
@@ -1816,9 +1816,11 @@ async function UIDesktop (options) {
//--------------------------------------------------------------------------------------
// Direct download link
// i.e. https://puter.com/?download=<file_url>
// i.e. https://puter.com/?download=<file_url> or https://puter.com/desktop?download=<file_url>
//--------------------------------------------------------------------------------------
if ( window.url_paths.length === 0 && window.url_query_params.has('download') ) {
const is_bare_desktop_path = window.url_paths.length === 0
|| (window.url_paths.length === 1 && window.url_paths[0]?.toLocaleLowerCase() === 'desktop');
if ( is_bare_desktop_path && window.url_query_params.has('download') ) {
const url = window.url_query_params.get('download');
let file_name = url.split('/').pop().split('?')[0];
@@ -2051,7 +2053,7 @@ $(document).on('contextmenu taphold', '.toolbar', function (event) {
$(document).on('click', '.qr-btn', async function (e) {
UIWindowQR({
message_i18n_key: 'scan_qr_c2a',
text: `${window.gui_origin }?auth_token=${ window.auth_token}`,
text: `${window.gui_origin }/desktop?auth_token=${ window.auth_token}`,
});
});
+2 -2
View File
@@ -3561,7 +3561,7 @@ $.fn.close = async function (options) {
// only reset the URL if this window was the one that owns it (mirrors the open-side check in focusWindow)
const update_window_url = $(this).attr('data-update_window_url');
if ( ! window.is_dashboard_mode && (update_window_url === 'true' || update_window_url === null) ) {
window.history.replaceState(null, document.title, '/');
window.history.replaceState(null, document.title, '/desktop');
}
// bring focus to the last window in the window-stack (only if not minimized)
let next_window_focused = false;
@@ -4015,7 +4015,7 @@ $.fn.hideWindow = async function (options) {
// update title and window URL — only if this window was the one that owns the URL
const update_window_url = $(this).attr('data-update_window_url');
if ( ! window.is_dashboard_mode && (update_window_url === 'true' || update_window_url === null) ) {
window.history.replaceState(null, document.title, '/');
window.history.replaceState(null, document.title, '/desktop');
document.title = i18n('window_title_puter');
}
}
+8 -1
View File
@@ -381,7 +381,8 @@ async function UIWindowLogin (options) {
if ( logo_clickable ) {
$(el_window).find('.auth-logo').on('click', function () {
window.location.href = '/';
// keep desktop users on the desktop; everyone else goes to the root dashboard
window.location.href = window.location.pathname === '/desktop' ? '/desktop' : '/';
});
}
@@ -407,6 +408,12 @@ async function UIWindowLogin (options) {
$(el_window).find(btnClass).on('click', function () {
let url = `${window.gui_origin}/auth/oidc/${provider}/start?flow=login`;
// return to the interface the login started from (backend whitelists the path)
const return_to = window.location.pathname;
if ( return_to === '/desktop' || return_to === '/dashboard' ) {
url += `&return_to=${encodeURIComponent(return_to)}`;
}
const referrer = options.referrer ?? window.referrerStr ?? window.openerOrigin;
if ( referrer ) {
url += `&referrer=${encodeURIComponent(referrer)}`;
+7 -1
View File
@@ -148,7 +148,8 @@ function UIWindowSignup (options) {
}
if ( logo_clickable ) {
$(el_window).find('.auth-logo').on('click', function () {
window.location.href = '/';
// keep desktop users on the desktop; everyone else goes to the root dashboard
window.location.href = window.location.pathname === '/desktop' ? '/desktop' : '/';
});
}
@@ -191,6 +192,11 @@ function UIWindowSignup (options) {
$(el_window).find(btnClass).css('display', 'flex');
$(el_window).find(btnClass).on('click', function () {
let url = `${window.gui_origin}/auth/oidc/${provider}/start?flow=signup`;
// return to the interface the signup started from (backend whitelists the path)
const return_to = window.location.pathname;
if ( return_to === '/desktop' || return_to === '/dashboard' ) {
url += `&return_to=${encodeURIComponent(return_to)}`;
}
const referrer = options.referrer ?? window.referrerStr;
if ( referrer ) {
url += `&referrer=${encodeURIComponent(referrer)}`;
+45 -36
View File
@@ -75,6 +75,11 @@
--dashboard-bento-usage-surface: #eef2ff;
--dashboard-bento-usage-header-bg: #e0e7ff;
--dashboard-bento-usage-bar-fill: #ea580c;
/* Emerald (matches the green icon accent used elsewhere) */
--dashboard-bento-desktop-icon-bg: #059669;
--dashboard-bento-desktop-icon-shadow: rgba(5, 150, 105, 0.22);
--dashboard-bento-desktop-surface: #ecfdf5;
--dashboard-bento-desktop-header-bg: #d1fae5;
--dashboard-shadow-subtle: rgba(0, 0, 0, 0.04);
--dashboard-shadow-light: rgba(0, 0, 0, 0.06);
@@ -166,6 +171,10 @@ body {
--dashboard-bento-usage-surface: #1c1d2e;
--dashboard-bento-usage-header-bg: #252a42;
--dashboard-bento-usage-bar-fill: #fb923c;
--dashboard-bento-desktop-icon-bg: #10b981;
--dashboard-bento-desktop-icon-shadow: rgba(16, 185, 129, 0.32);
--dashboard-bento-desktop-surface: #16241f;
--dashboard-bento-desktop-header-bg: #172e26;
--dashboard-shadow-subtle: rgba(0, 0, 0, 0.2);
--dashboard-shadow-light: rgba(0, 0, 0, 0.3);
@@ -643,7 +652,7 @@ body {
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
.myapps-search-wrap {
background: rgba(255, 255, 255, 0.52);
background: rgba(255, 255, 255, 0);
backdrop-filter: blur(20px) saturate(1.5);
-webkit-backdrop-filter: blur(20px) saturate(1.5);
}
@@ -1074,9 +1083,6 @@ input.myapps-search::placeholder {
border-radius: 20px;
overflow: hidden;
border: 1px solid var(--dashboard-shadow-light);
box-shadow:
0 1px 2px var(--dashboard-shadow-subtle),
0 4px 16px var(--dashboard-shadow-light);
}
/* Welcome Card — uses same surface as .bento-card (no separate welcome tint) */
@@ -1246,6 +1252,11 @@ input.myapps-search::placeholder {
color: #fff;
}
.dashboard .bento-card-fancy-icon-desktop {
background-color: var(--dashboard-bento-desktop-icon-bg);
color: #fff;
}
.dashboard .bento-card-fancy-text {
display: flex;
flex-direction: column;
@@ -1386,46 +1397,39 @@ input.myapps-search::placeholder {
color: var(--dashboard-text-tertiary);
}
/* Desktop switch bento card */
.dashboard .bento-desktop-switch {
/* Open Desktop bento card (full width; links to the desktop interface) */
.dashboard .bento-desktop {
grid-column: 1 / -1;
min-height: auto;
background-color: var(--dashboard-bento-apps-surface);
padding: 16px 24px;
}
.dashboard .bento-desktop-switch-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.dashboard .bento-desktop-switch-text {
font-size: 14px;
font-weight: 500;
color: var(--dashboard-text-secondary);
}
.dashboard .bento-desktop-switch-btn {
flex-shrink: 0;
padding: 8px 16px;
font-size: 13px;
font-weight: 500;
color: var(--dashboard-background);
background: var(--dashboard-icon-blue-start);
border: 1px solid var(--dashboard-primary);
border-radius: 8px;
cursor: pointer;
gap: 14px;
padding: 18px 24px;
background-color: var(--dashboard-bento-desktop-header-bg);
text-decoration: none;
transition: background 0.15s, color 0.15s;
color: inherit;
transition: box-shadow 0.15s ease;
}
@media (hover: hover) {
.dashboard .bento-desktop-switch-btn:hover {
background: var(--dashboard-icon-blue-end);
color: white;
.dashboard .bento-desktop:hover {
box-shadow:
0 2px 4px var(--dashboard-shadow-subtle),
0 10px 28px var(--dashboard-bento-desktop-icon-shadow);
}
.dashboard .bento-desktop:hover .bento-desktop-arrow {
transform: translateX(3px);
color: var(--dashboard-text-primary);
}
}
.dashboard .bento-desktop-arrow {
margin-left: auto;
font-size: 22px;
font-weight: 300;
line-height: 1;
color: var(--dashboard-text-hint);
transition: transform 0.15s ease, color 0.15s ease;
}
/* Usage bento card (cool indigo surfaces; orange bars stay as progress accent) */
@@ -1701,6 +1705,11 @@ input.myapps-search::placeholder {
gap: 12px;
}
.dashboard .bento-desktop {
padding: 16px 20px;
gap: 12px;
}
.dashboard .bento-card-fancy-icon {
width: 40px;
height: 40px;
+1
View File
@@ -206,6 +206,7 @@ const en = {
or: 'or',
open: 'Open',
new_window: 'New Window',
open_desktop: 'Open Desktop',
open_in_ai: 'Open in AI',
open_in_new_tab: 'Open in New Tab',
open_in_new_window: 'Open in New Window',
+36 -9
View File
@@ -157,9 +157,29 @@ if ( jQuery ) {
}
// are we in dashboard mode?
if ( window.location.pathname === '/dashboard' || window.location.pathname === '/dashboard/' ) {
window.is_dashboard_mode = true;
window.dashboard_initial_route = parseDashboardRoute();
// The dashboard is the default interface at the root path; `/dashboard` is kept as an
// alias, and `/desktop` loads the desktop instead. Root URLs that carry a desktop-only
// flow keep booting the desktop: auth popups (`?embedded_in_popup=`), app deep links
// (`?app=`), direct downloads (`?download=`), fullpage mode (`?puter.fullpage=`), and
// iframe embeds.
{
const pathname = window.location.pathname;
const search_params = new URLSearchParams(window.location.search);
if ( ['true', '1'].includes(search_params.get('embedded_in_popup')) ) {
window.embedded_in_popup = true;
}
// note: iframe detection is inlined because globals.js (window.is_embedded) loads after this module
const in_iframe = window.location !== window.parent.location;
const needs_desktop_at_root = window.embedded_in_popup
|| in_iframe
|| search_params.has('puter.fullpage')
|| search_params.has('app')
|| search_params.has('download');
const is_dashboard_alias = pathname === '/dashboard' || pathname === '/dashboard/';
if ( is_dashboard_alias || (pathname === '/' && !needs_desktop_at_root) ) {
window.is_dashboard_mode = true;
window.dashboard_initial_route = parseDashboardRoute();
}
}
/**
@@ -431,9 +451,9 @@ window.initgui = async function (options) {
//--------------------------------------------------------------------------------------
// Is GUI embedded in a popup?
// i.e. https://puter.com/?embedded_in_popup=true
// (the flag itself is parsed once at module level, alongside the dashboard-mode check)
//--------------------------------------------------------------------------------------
if ( window.url_query_params.has('embedded_in_popup') && (window.url_query_params.get('embedded_in_popup') === 'true' || window.url_query_params.get('embedded_in_popup') === '1') ) {
window.embedded_in_popup = true;
if ( window.embedded_in_popup ) {
$('body').addClass('embedded-in-popup');
// determine the origin of the opener (preserved across OIDC redirect via URL param, else referrer or messaging)
@@ -720,8 +740,9 @@ window.initgui = async function (options) {
// update auth data
await window.update_auth_data(query_param_auth_token, whoami, api_origin);
}
// remove auth_token from URL
window.history.pushState(null, document.title, '/');
// remove auth_token from URL, keeping the current path (e.g. `/` or `/desktop`)
// and hash (dashboard tab links like /#usage)
window.history.pushState(null, document.title, window.location.pathname + window.location.hash);
}
/**
@@ -845,8 +866,9 @@ window.initgui = async function (options) {
$('.taskbar').remove();
// disable native browser exit confirmation
window.onbeforeunload = null;
// go to home page
window.location.replace('/');
// go back to the interface the user was in: dashboard users to the root
// dashboard, desktop users to /desktop
window.location.replace(window.is_dashboard_mode ? '/' : '/desktop');
});
// -------------------------------------------------------------------------------------
@@ -1251,6 +1273,11 @@ window.initgui = async function (options) {
const needs_action = action === 'authme' || action === 'copyauth';
const reload_on_success = needs_action;
if ( window.logged_in_users.length > 0 ) {
// dashboard mode skips the wallpaper (fullpage), but the session list
// has no cover page — restore the wallpaper so it isn't on a blank page
if ( window.is_dashboard_mode ) {
window.refresh_desktop_background();
}
await UIWindowSessionList({
redirect_url: needs_action ? window.location.href : undefined,
});