((resolve) =>
+ setTimeout(() => {
+ order.push(`${label}:end`);
+ resolve(value);
+ }, 20),
+ );
+ };
+
+ const permSpy = vi
+ .spyOn(server.services.permission, 'grantUserAppPermission')
+ .mockImplementation(() => defer('grant', undefined as never));
+ const tokenSpy = vi
+ .spyOn(server.services.auth, 'getUserAppToken')
+ .mockImplementation(() => defer('token', 'signed.jwt.value'));
+ const mkdirSpy = vi
+ .spyOn(server.services.fs, 'mkdir')
+ .mockImplementation(() => defer('mkdir', undefined as never));
+
+ try {
+ await inCtx(actor, () =>
+ controller.handleGetUserAppToken(
+ makeReq({ app_uid: app.uid }, { actor }),
+ makeRes(),
+ ),
+ );
+ } finally {
+ permSpy.mockRestore();
+ tokenSpy.mockRestore();
+ mkdirSpy.mockRestore();
+ }
+
+ // All three must be in flight before any of them settles.
+ const firstEnd = order.findIndex((e) => e.endsWith(':end'));
+ const starts = order.slice(0, firstEnd);
+ expect(starts).toHaveLength(3);
+ expect(starts).toEqual(
+ expect.arrayContaining([
+ 'grant:start',
+ 'token:start',
+ 'mkdir:start',
+ ]),
+ );
+ });
+
it('after get-user-app-token, check-app reports authenticated:true and returns a token', async () => {
// Ensure the flag is granted (re-run is idempotent).
await inCtx(actor, () =>
diff --git a/src/backend/controllers/auth/AuthController.ts b/src/backend/controllers/auth/AuthController.ts
index ada4fdf4c..6a9cdaa25 100644
--- a/src/backend/controllers/auth/AuthController.ts
+++ b/src/backend/controllers/auth/AuthController.ts
@@ -3174,9 +3174,9 @@ export class AuthController extends PuterController {
legacyCode: 'not_found',
});
}
- // Grant the app-is-authenticated flag
+
const userPermGrantPromise =
- await this.services.permission.grantUserAppPermission(
+ this.services.permission.grantUserAppPermission(
req.actor!,
app_uid,
'flag:app-is-authenticated',
@@ -3184,7 +3184,7 @@ export class AuthController extends PuterController {
{},
);
- const token = await this.services.auth.getUserAppToken(
+ const tokenPromise = this.services.auth.getUserAppToken(
req.actor!,
app_uid,
);
@@ -3211,7 +3211,11 @@ export class AuthController extends PuterController {
}
})();
- await Promise.all([userPermGrantPromise, missingFSPathPromise]);
+ const [, token] = await Promise.all([
+ userPermGrantPromise,
+ tokenPromise,
+ missingFSPathPromise,
+ ]);
try {
const a = app as {
diff --git a/src/gui/src/UI/Dashboard/TabHome.js b/src/gui/src/UI/Dashboard/TabHome.js
index 7ce71a1a1..81f1c48bc 100644
--- a/src/gui/src/UI/Dashboard/TabHome.js
+++ b/src/gui/src/UI/Dashboard/TabHome.js
@@ -3,22 +3,28 @@
*
* This file is part of Puter.
*
- * Puter is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
+ * Puter is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Affero General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
*
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
*
* You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
+ * along with this program. If not, see
+ * [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/).
*/
import UIWindowSaveAccount from '../UIWindowSaveAccount.js';
+// How long a completed usage load stays fresh enough to skip a repeat. Long
+// enough to absorb the init/onActivate/routing burst on a single dashboard
+// open, short enough that a user watching the panel still sees movement.
+const USAGE_REFRESH_DEBOUNCE_MS = 5_000;
+
function buildRecentAppsHTML() {
let h = '';
@@ -73,7 +79,8 @@ function buildUsageHTML() {
// Your Plan section
h +=
'';
- h += '';
@@ -224,8 +231,10 @@ const TabHome = {
h += '
';
// Open Desktop card (spans full width, links to the desktop interface)
- h += '