fix mobile screen issues

This commit is contained in:
2026-08-24 13:14:22 +05:30
parent dcf9770ada
commit 02eb48af99
34 changed files with 1214 additions and 291 deletions

View File

@@ -3,16 +3,24 @@
*
* The reference app talks to a Base44 backend through this module. The demo
* keeps the module path, the export name, and the full method contract, and
* swaps the transport for the local store in `store.js` and the local AI engine
* swaps the transport for the Go API in `httpClient.js` and the local AI engine
* in `aiEngine.js`. Nothing downstream — hooks, pages, components — knows or
* cares, which is exactly the point: the seam stays where it was.
*
* Phase 2D moved the transport from a localStorage-backed store to HTTP:
*
* React → base44Client.js → HTTP → Go API → PostgreSQL
*
* The entity surface is unchanged. `store.js` and `seed.js` are no longer the
* source of data — the seeded dataset now lives in PostgreSQL, loaded by the
* backend's `make seed`. `seed.js` is still imported for one thing: the shape
* of the demo user's default preferences, which the synchronous accessor below
* needs before the first response arrives.
*/
import { createEntity, initStore, resetStore } from './store';
import { createEntity, request, isUnauthenticated, API_BASE_URL } from './httpClient';
import { invokeLLM, uploadFile } from './aiEngine';
import { DEMO_USER, seedData } from './seed';
initStore(seedData);
import { DEMO_USER } from './seed';
const ENTITY_NAMES = [
'JobPosting', 'JobApplication', 'AIInterview', 'Staff', 'WorkerProfile',
@@ -33,9 +41,28 @@ const entities = Object.fromEntries(
/* ── Auth ──────────────────────────────────────────────────────────────── */
/**
* The last user the API returned.
*
* This is a **cache of `GET /me`**, not a store. The record itself lives in
* PostgreSQL; this exists for one reason, and it is not offline support.
*
* `auth.preferences()` is synchronous, and it has to stay synchronous:
* `AssistantPanelContext` decides whether Owliver starts open in a `useState`
* initialiser, during the first render, and `krowHooks.js:42` merges the same
* accessor under the async user so the first paint already has real values. One
* tick later is a visible flash of the wrong workspace — the panel opening on
* an account that turned it off, then closing.
*
* So the last known user is mirrored to localStorage and read back at module
* load, and `GET /me` refreshes it. On a return visit the synchronous read is
* already correct; on a first-ever visit it is the seeded defaults for exactly
* as long as the request takes, which is the same thing the old store did with
* an empty key.
*/
const SESSION_KEY = 'krow_demo_user';
function loadUser() {
function readCachedUser() {
try {
const raw = localStorage.getItem(SESSION_KEY);
return raw ? { ...DEMO_USER, ...JSON.parse(raw) } : { ...DEMO_USER };
@@ -44,98 +71,207 @@ function loadUser() {
}
}
let currentUser = loadUser();
/**
* Writes the session user, and says whether it actually landed.
* Mirrors the current user for the next page load's synchronous read.
*
* The old version was `try { setItem } catch {}` — a swallowed
* `QuotaExceededError` or a private-browsing refusal, and the caller was handed
* a user object indistinguishable from a successful write. For preferences that
* is invisible; for `customSkills`, which is where every account-authored skill
* definition lives, it is the whole "I saved it and it was gone" report: the
* toast said added, the list showed it, the reload did not.
*
* The read-back matters as much as the catch. A write can be accepted and then
* evicted, and a serialisation can land truncated; comparing what came back
* with what went in is the only way to know the record is really there.
* Failures are ignored, which is a real change from the old `persistUser` and a
* safe one. That function checked its write and reported failure because
* localStorage was the *only* copy — a swallowed `QuotaExceededError` was how
* account-authored skills silently disappeared. Now the only copy is in
* PostgreSQL, and a refused mirror costs one render of default preferences, not
* data.
*/
function persistUser() {
const payload = JSON.stringify(currentUser);
function cacheUser() {
try {
localStorage.setItem(SESSION_KEY, payload);
} catch (error) {
return { persisted: false, error };
localStorage.setItem(SESSION_KEY, JSON.stringify(currentUser));
} catch {
// Private browsing or quota — the server still has the record.
}
try {
if (localStorage.getItem(SESSION_KEY) !== payload) {
return { persisted: false, error: new Error('The session record did not survive the write.') };
}
} catch (error) {
return { persisted: false, error };
}
return { persisted: true, error: null };
}
/**
* Drops the mirrored user.
*
* Signing out must not leave the next page load rendering the previous
* account's name and preferences out of localStorage while it waits for a
* `GET /me` that is going to 401.
*/
function forgetUser() {
try {
localStorage.removeItem(SESSION_KEY);
} catch {
// Ignore.
}
currentUser = { ...DEMO_USER };
}
let currentUser = readCachedUser();
/**
* Whether the last `GET /me` succeeded.
*
* A cache of the server's answer, not a decision. Nothing here grants access:
* the API refuses an unauthenticated request whatever this says, and a user who
* edits it in the console has changed a boolean in their own tab and nothing
* else. It exists because `isAuthenticated()` is synchronous.
*/
let authenticated = false;
/**
* The first `GET /me`, shared.
*
* Started at module load so the synchronous accessor is corrected as early as
* possible, and shared so the eleven `me()` call sites that fire during the
* first render make one request between them rather than eleven.
*
* A 401 here is the ordinary state of a signed-out visitor, not a failure: the
* app opens on the login page and this request is how it finds that out.
*/
let hydration = request('GET', '/me')
.then((user) => {
currentUser = user;
authenticated = true;
cacheUser();
return user;
})
.catch(() => {
authenticated = false;
return null;
});
const auth = {
/** The demo is always signed in as the seeded employer/admin user. */
/**
* The signed-in user, from the session cookie.
*
* Throws when there is no session — a `KrowApiError` with `status: 401` — and
* that throw is the app's authentication check. `AuthContext` catches it and
* renders the login page. Nothing here decides who the user is; the server
* reads its own session table and answers.
*
* Joins the in-flight hydration if there is one, so the first render's
* callers share a request; refetches afterwards so a change made in another
* tab, or a session that has since expired, is picked up.
*/
async me() {
return { ...currentUser };
if (hydration) {
const user = await hydration;
hydration = null;
if (user) {
authenticated = true;
return { ...user };
}
}
try {
const user = await request('GET', '/me');
currentUser = user;
authenticated = true;
cacheUser();
return { ...user };
} catch (error) {
if (isUnauthenticated(error)) {
authenticated = false;
forgetUser();
}
throw error;
}
},
/**
* Signs in and starts a session.
*
* The response body is the user. The session token is NOT in it — it arrives
* as an HttpOnly cookie the browser stores and this code cannot read, which
* is what stops a script on the page from stealing it. There is deliberately
* nothing here that writes a token anywhere.
*
* Every credential failure comes back as the same 401 with the same message,
* by design: telling the two apart would say whether an address is
* registered. The caller shows that message as-is.
*/
async login({ email, password, rememberMe = false }) {
const user = await request('POST', '/auth/login', {
body: { email, password, remember_me: Boolean(rememberMe) },
});
currentUser = user;
authenticated = true;
hydration = null;
cacheUser();
return { ...user };
},
async updateMe(patch) {
currentUser = { ...currentUser, ...patch };
persistUser();
return { ...currentUser };
const user = await request('PATCH', '/me', { body: patch });
currentUser = user;
cacheUser();
return { ...user };
},
/**
* Preferences, read synchronously.
*
* `me()` is async because the real client fetches, but the panel provider has
* to decide whether Owliver starts open during its first render — one tick
* later is a visible flash of the wrong workspace. The session user is already
* hydrated from localStorage at module load, so this is a plain read of the
* same record `me()` returns, not a second copy of the state.
* A plain read of the same record `me()` returns, defaulted with the shape
* from `seed.js` so a key the server has never stored still resolves. See the
* note on `currentUser` for why this must not become async.
*/
preferences() {
return { ...DEMO_USER.preferences, ...(currentUser.preferences || {}) };
},
/**
* Merges into the stored preferences and persists with the rest of the user.
* Merges into the stored preferences and persists them server-side.
*
* Returns the write's outcome alongside the record, rather than the record
* alone. Preferences are where account-authored skills live, so "did this
* survive the reload" is a question the caller has to be able to answer —
* see `persistUser`.
* `PATCH /me/preferences` shallow-merges and returns the whole merged object,
* which is where `customSkills` and `customAgents` — every account-authored
* definition — now live: `user_preferences.extra`, a real column in a real
* database rather than a browser key.
*
* The `{ user, persisted, error }` shape is kept because `saveFeedback.js`
* reads it. Over HTTP a write that did not land is a non-2xx and therefore a
* throw, so the success path is unconditionally `persisted: true` — the
* question the shape exists to answer is now answered by whether this
* function resolved at all.
*/
async updatePreferences(patch) {
currentUser = { ...currentUser, preferences: { ...auth.preferences(), ...patch } };
const write = persistUser();
return { user: { ...currentUser }, ...write };
},
isAuthenticated() {
return true;
const preferences = await request('PATCH', '/me/preferences', { body: patch });
currentUser = { ...currentUser, preferences };
cacheUser();
return { user: { ...currentUser }, persisted: true, error: null };
},
/**
* There is no identity provider to sign out of, so this clears the local
* session and returns to the requested page.
* Whether the last `GET /me` succeeded.
*
* Synchronous, and therefore only ever a cache of what the server last said.
* It is a hint for rendering, never a gate: every protected endpoint is
* refused by the API on its own authority regardless of this value.
*/
logout(redirectTo = '/') {
isAuthenticated() {
return authenticated;
},
/**
* Signs out and returns to the requested page.
*
* The server revokes the session row and expires the cookie; this clears the
* cached copy of the user so a signed-out tab cannot render a stale name from
* localStorage. The redirect happens either way — a logout that could not
* reach the API must still leave the browser signed out locally, and the
* cookie it keeps will be refused by every request it is sent on.
*/
async logout(redirectTo = '/admin/login') {
try {
localStorage.removeItem(SESSION_KEY);
await request('POST', '/auth/logout');
} catch {
// Ignore.
// Already signed out, or the API is unreachable. Neither is a reason to
// keep the user looking at a signed-in page.
}
currentUser = { ...DEMO_USER };
window.location.href = typeof redirectTo === 'string' ? redirectTo : '/';
authenticated = false;
forgetUser();
window.location.href = typeof redirectTo === 'string' ? redirectTo : '/admin/login';
},
redirectToLogin() {
window.location.href = '/';
window.location.href = '/admin/login';
},
};
@@ -158,8 +294,27 @@ const analytics = {
export const base44 = { entities, auth, integrations, analytics };
/** Restores the shipped demo data, discarding local edits. */
/** Where the entity data actually comes from, for diagnostics. */
export { API_BASE_URL };
/**
* Clears local session state and reloads.
*
* The demo dataset is no longer the browser's to restore: it lives in
* PostgreSQL, and reseeding it is `make seed` in the `krow-backend` repository,
* which upserts the shipped fixture in one transaction. All this can still do
* is drop the cached user and reload, so it says so rather than reporting a
* reset it did not perform.
*/
export function resetDemoData() {
resetStore(seedData);
try {
localStorage.removeItem(SESSION_KEY);
} catch {
// Ignore.
}
console.info(
'[krow-demo] Local session cache cleared. Entity data lives in PostgreSQL — ' +
'restore the shipped dataset with `make seed` in krow-backend.'
);
window.location.reload();
}