User management: signing in is required before anything is loaded, the sign-in screen is drawn in the same canvas (user name, password as bullets, Enter moves on and submits) and the editor is built only after it; the API gained auth/session, auth/login and auth/logout while every other route answers 401 without a session, projects carry an owner and a foreign one comes back as 404, accounts live in data/users.json with a password hash and sessions as files in data/sessions with a sliding 30 days, so a build under the open tab does not sign anybody out; Logout sits at the far right of the toolbar and, as asked in the issue, does not reload the page: Editor.dispose() takes the editor down while the canvas stays, which is why the autosave, the focus handlers, the window listeners of the tilemap panel, the shortcuts and the plugins all got a way back; the tools sign in as e2e (tools/lib/auth.mjs, password from GAME_MAKER_E2E_PASSWORD or the git-ignored data/e2e-password.txt) and the browser gets the same session by cookie, new e2e module 45 covers the screen, the wrong pair, the bullets, logout without a reload and signing in again, new unit tests for HttpAuthStore and for what a session leaves behind (#177)
game-maker / gate (push) Successful in 2m5s
game-maker / suite (push) Failing after 6m22s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-20 12:50:52 +02:00
1 parent 60ea56a2ce
commit 8e4bb6e1f9
38 files changed
+1530 -139

No files matched your search

+25 -6
View File
@@ -34,6 +34,7 @@ import {
Theme,
ui,
VStack,
type Widget,
} from '../../ui';
import type { EditorState, TilemapFocus, TilemapTool } from '../state';
import { TilePalette } from '../tile/TilePalette';
@@ -164,6 +165,8 @@ export class TilemapPanel extends Panel {
/** As set, before the header upper-cases it – for the check scripts. */
title = 'Tilemap';
onTargetChange: () => void = () => {};
/** What this panel registered outside itself, as the calls that take it away again. */
private readonly cleanups: (() => void)[] = [];
constructor(private readonly state: EditorState) {
super({ title: 'Tilemap', padding: 0, gap: 0 });
@@ -177,20 +180,36 @@ export class TilemapPanel extends Panel {
if (ui().focus.isWithin(this)) this.rebuildWhenLeft = true;
else this.sync();
});
ui().focus.changeHandlers.push(() => {
if (this.rebuildWhenLeft && !ui().focus.isWithin(this)) this.sync();
});
this.cleanups.push(
ui().focus.onFocusChange(() => {
if (this.rebuildWhenLeft && !ui().focus.isWithin(this)) this.sync();
}),
);
// Holding Alt picks (#133). The focus manager only hears presses, so the release has its own
// listener; a handled Alt is kept from the browser, which would otherwise open its menu bar.
const onAlt = (held: boolean) => (event: KeyboardEvent) => {
if (event.key === 'Alt' && !event.repeat && holdPick(state, held)) event.preventDefault();
};
window.addEventListener('keydown', onAlt(true));
window.addEventListener('keyup', onAlt(false));
window.addEventListener('blur', () => holdPick(state, false));
const onKeyDown = onAlt(true);
const onKeyUp = onAlt(false);
const onBlur = () => holdPick(state, false);
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
window.addEventListener('blur', onBlur);
this.cleanups.push(() => {
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
window.removeEventListener('blur', onBlur);
});
this.sync();
}
/** Signing out destroys the panel while the page stays up (#177): its listeners go with it. */
override destroy(options?: Parameters<Widget['destroy']>[0]): void {
for (const cleanup of this.cleanups.splice(0)) cleanup();
super.destroy(options);
}
/** Whether there is a map to paint – the editor adds "and the scene tab is showing". */
get hasTarget(): boolean {
return built !== null;
+7 -2
View File
@@ -14,6 +14,8 @@ export interface ToolbarActions {
togglePlay(): void;
/** Hold or resume the running world; only reachable in play mode. */
togglePause(): void;
/** Ends the session and returns to the sign-in screen. */
logOut(): void;
}
const SAVE_LABELS: Record<SaveStatus, string> = {
@@ -92,15 +94,18 @@ export class Toolbar extends HStack {
this.add(this.pauseButton);
this.add(new Spacer(), 1);
// Flexible, not a fixed strip: it is the one thing here that may shrink, and since the Logout
// button (#177) the row no longer fits a narrow window otherwise.
this.notice = new Label('', { style: 'small', align: 'right' });
this.notice.minWidth = 260;
this.add(this.notice);
this.add(this.notice, 1);
this.status = new Label('', { style: 'small', align: 'right' });
this.status.minWidth = 110;
this.add(this.status);
this.fps = new Label('– fps', { style: 'small', align: 'right' });
this.fps.minWidth = 56;
this.add(this.fps);
// Far right, where the account lives; who is signed in stands in the window title.
this.add(new Button('Logout', { onClick: () => actions.logOut() }));
state.on('project', () => this.refresh());
state.on('load', () => this.refresh());