Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Pewiz/ulagos360/llms.txt
Use this file to discover all available pages before exploring further.
All global application state in ULagos 360° lives in a single Zustand store defined in src/stores/spacesStore.js. The store uses Zustand’s persist middleware to automatically serialise a subset of state to localStorage under the key spaces-storage, so that space occupancy data and the current tutor’s identity survive page refreshes, accidental tab closes, and brief network outages without any extra saving logic in components.
State Shape
interface SpacesState {
spaces: Record<string, Space>; // keyed by space id (e.g. "aula-magna")
currentUser: User | null; // { id: string, name: string }
connected: boolean; // store field, NOT persisted; live status comes from useSocketConnection
loading: boolean; // true until initializeSpaces() completes
sessionId: string; // stable session token across reloads
}
connected and loading are intentionally excluded from persistence (see Persistence Configuration below). In practice, the live WebSocket status consumed by components comes from useSocketConnection’s own local useState(false) rather than this store field. loading resets to true on every page load and is set to false only after initializeSpaces() completes.
Space Object Shape
Each entry in the spaces map conforms to the following interface. Fields marked optional are only set for specific space categories.
interface Space {
id: string; // unique slug, e.g. "hall-principal"
name: string; // human-readable label shown in SpaceCard
capacity?: number; // max group size (omitted for some campus stops)
status: SpaceStatus; // see status values below
occupiedBy: string | null; // tutor name when status is "ocupado"
reservedBy: string | null; // tutor name when status is "reservado" | "en_camino"
lastUpdate: string; // ISO 8601 timestamp of last change
updatedBy: string | null; // name of the tutor who last changed status
category: SpaceType; // space group (bienvenida, coffee, etc.)
type: SpaceType; // mirrors category; kept for server compatibility
supervisor?: string; // Talleres only — assigned supervisor name
location?: string; // Talleres only — physical location label
establishment?: string; // Tours en Curso only — associated establishment
}
Valid SpaceStatus values (from constants/spaces.js):
| Value | Meaning |
|---|
disponible | Space is free and ready to receive a group |
en_camino | A group is on its way but has not yet arrived |
reservado | Space is reserved for a specific group |
ocupado | Space is currently in use |
en_curso | Activity is actively running (Talleres / Tours) |
terminado | Activity has finished; awaiting reset |
mantenimiento | Space temporarily out of service |
Store Actions
Every action is a synchronous function exposed from the Zustand store. Async side-effects (socket emissions) are handled externally in useRealTimeSpaces.
| Action | Signature | Description |
|---|
initializeSpaces | () => void | Seeds spaces from the SPACES_DATA catalogue if the map is empty; sets loading: false. No-op when persisted data already exists. |
updateSpace | (spaceId: string, updates: Partial<Space>) => void | Shallow-merges updates into the target space, stamps lastUpdate with the current ISO timestamp, and writes the full spaces map to ulagos360_spaces_backup in localStorage. |
updateSpaceStatus | (spaceId: string, status: SpaceStatus, userData?: { name: string }) => Partial<Space> | High-level status change that derives occupiedBy / reservedBy from status semantics, clears the opposing field, sets updatedBy, then writes the updated space directly to the store via set() and to ulagos360_spaces_backup in localStorage. Returns the updates object. |
setUser | (user: User) => void | Writes currentUser to the store and persists the full user object to ulagos360_current_user in localStorage. |
setConnected | (connected: boolean) => void | Flips the store’s connected flag. Defined in the store but not called by useSocketConnection — that hook manages its own local useState(false) for connection status and exposes it through its return value. |
getSpacesByCategory | (category: SpaceType) => Space[] | Returns all spaces whose category matches the argument. Non-mutating selector. |
getAvailableSpaces | (category?: SpaceType) => Space[] | Returns all spaces with status === "disponible", optionally filtered by category. Non-mutating selector. |
clearAllData | () => void | Removes all five localStorage keys, resets every space to disponible, and clears currentUser. Does not regenerate sessionId. |
logout | () => void | Same as clearAllData plus clears lastStateSync from localStorage and generates a fresh sessionId via generateSessionId(). |
forceSync | () => void | Manually serialises the current spaces, currentUser, and sessionId to both spaces-storage and ulagos360_spaces_backup without waiting for Zustand’s automatic persist flush. |
getLocalStateForServer | () => { spaces, user, timestamp } | Returns a plain-object snapshot of the current state, ready to be embedded in a sync_spaces socket payload. |
shouldAcceptServerUpdate | (serverData: { spaceId, lastUpdate }) => boolean | Conflict-resolution guard. Returns true if the server’s lastUpdate is newer than or within 1 second of the local timestamp; false if local data is strictly more recent by more than 1 second. |
updateSpaceStatus returns the updates object it applied. The handleUpdateSpaceStatus wrapper in useRealTimeSpaces captures this return value and passes it directly to sendSpaceUpdate() — so the same derived data is used for both the optimistic local update and the Socket.IO emission, with zero risk of the two falling out of sync.
updateSpaceStatus Status Logic
The action automatically sets or clears occupiedBy and reservedBy based on which status is being applied:
switch (status) {
case "ocupado":
updates.occupiedBy = userData?.name || currentUser?.name;
updates.reservedBy = null;
break;
case "reservado":
case "en_camino":
updates.reservedBy = userData?.name || currentUser?.name;
updates.occupiedBy = null;
break;
case "disponible":
updates.occupiedBy = null;
updates.reservedBy = null;
break;
}
The updatedBy field is always set to the current user’s name, or "Usuario anónimo" if no user is authenticated.
Persistence Configuration
The persist middleware is configured with the following options:
{
name: "spaces-storage", // localStorage key
partialize: (state) => ({
spaces: state.spaces, // full spaces map
currentUser: state.currentUser, // authenticated tutor
sessionId: state.sessionId, // stable session token
}),
version: 1, // schema version for future migrations
}
connected and loading are excluded from partialize. On startup loading is always true until initializeSpaces() resolves, and connected is always false until the socket handshake completes — persisting these flags would cause the UI to show stale connection state.
localStorage Key Inventory
The application writes to several localStorage keys, managed across the store, hooks, and backup system:
| Key | Written by | Contents |
|---|
spaces-storage | Zustand persist + forceSync | Persisted store slice: spaces, currentUser, sessionId |
ulagos360_current_user | setUser, useSocketConnection | Raw user object { id, name } |
ulagos360_session_id | generateSessionId() | Stable session token string |
ulagos360_spaces_backup | updateSpace, updateSpaceStatus, forceSync, usePersistentBackup | Raw spaces map (no wrapper) |
ulagos360_emergency_backup | usePersistentBackup | { spaces, timestamp, version: "2.0" } |
lastStateSync | useSocketConnection | Unix ms timestamp of last server sync |
sessionId | useSocketConnection | Socket-level session id (mirrors ulagos360_session_id) |
currentUser | useSocketConnection | Duplicate user record for quick socket-layer access |
clearAllData and logout both remove the five primary keys. However, useRealTimeSpaces.handleLogout performs a more thorough sweep, explicitly deleting all eight keys listed above before reloading the page.