agnets done
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { API_BASE_URL, base44 } from '@/api/base44Client';
|
||||
import { generateJobDescription, screenCandidate, matchTalentForJob } from './krowAi';
|
||||
import { recalcProfilePatch } from './krowScore';
|
||||
import { logActivity } from './userTracking';
|
||||
@@ -177,12 +177,141 @@ export function useUserActivity() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* What Owliver could usefully be asked on this page.
|
||||
*
|
||||
* The server answers, and the server is the only thing that decides: it filters
|
||||
* the readings against the caller's role and — with nothing typed — ranks them
|
||||
* against the organization's actual state in PostgreSQL. This hook requests and
|
||||
* caches; it does not score, filter or reorder.
|
||||
*
|
||||
* Keyed on the page and the query together, so every distinct composer state
|
||||
* has its own cache entry and typing back to something already asked is
|
||||
* answered without a request. `placeholderData` keeps the previous answer on
|
||||
* screen while the next one is in flight, which is what stops the chip row
|
||||
* emptying and refilling between keystrokes.
|
||||
*
|
||||
* A failure resolves to no suggestions rather than to an error state. The panel
|
||||
* still works with none — the composer is what the reader came for — and an
|
||||
* error banner over a suggestion is a bigger interruption than the suggestion
|
||||
* was worth.
|
||||
*
|
||||
* The untyped request is the one that reads the database, so it is invalidated
|
||||
* whenever something the context counts has changed. See `owliverContextKey`.
|
||||
*/
|
||||
export const owliverContextKey = ['owliverSuggestions'];
|
||||
|
||||
/**
|
||||
* Whether this backend serves the endpoint at all.
|
||||
*
|
||||
* A deployment lag, not a bug, and it has happened: the route exists in the Go
|
||||
* source and is not yet on the host a given dev server proxies to, which
|
||||
* answers `404` for it and `200` for everything else. Every keystroke would
|
||||
* then be a request that cannot succeed, and no later keystroke can change the
|
||||
* answer — so the first `404` closes the circuit for the rest of the page load.
|
||||
*
|
||||
* Only `404` closes it. A `401` means the session is not signed in, a `500`
|
||||
* means the server had a bad moment, and an unreachable API means it is being
|
||||
* restarted: all three are worth trying again, and treating them as "this
|
||||
* backend does not have the route" would silence suggestions permanently over a
|
||||
* temporary fault. Module-level rather than component state because the fact is
|
||||
* about the backend, not about one panel.
|
||||
*/
|
||||
let routeUnavailable = false;
|
||||
|
||||
/**
|
||||
* Say it once, out loud.
|
||||
*
|
||||
* The circuit breaker below stops the requests; it must not stop the reader
|
||||
* finding out why the chip row is empty. A suggestion that quietly never
|
||||
* arrives is indistinguishable from a page that has nothing to suggest, and
|
||||
* that ambiguity is what makes this fault expensive — it looks like a frontend
|
||||
* bug and it is a deployment lag.
|
||||
*
|
||||
* So the panel degrades to no suggestions, and the console says exactly which
|
||||
* request failed and what would fix it. Nothing is substituted for the missing
|
||||
* answer: there is no fallback list, and no locally-ranked catalogue standing
|
||||
* in for the server's. An empty row is the honest rendering of "the backend
|
||||
* could not tell us".
|
||||
*/
|
||||
function reportMissingRoute() {
|
||||
if (routeUnavailable) return;
|
||||
routeUnavailable = true;
|
||||
console.warn(
|
||||
`[krow] ${API_BASE_URL}/owliver/suggestions answered 404. The backend this ` +
|
||||
'frontend is talking to does not register that route, so Owliver will show ' +
|
||||
'no suggestion chips until it is deployed. This is a backend deployment ' +
|
||||
'version mismatch, not a frontend fault — the route exists in the Go source ' +
|
||||
'(go-api/internal/httpserver/owliver.go). Nothing is being substituted for ' +
|
||||
'the missing answer.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the API what could usefully be asked here.
|
||||
*
|
||||
* The one place the request is made, so the circuit breaker above cannot be
|
||||
* bypassed by a caller that reaches for `base44.owliver` directly. Resolves to
|
||||
* an empty list on any failure: the panel works with no suggestions — the
|
||||
* composer is what the reader came for — and an error banner over a suggestion
|
||||
* is a bigger interruption than the suggestion was worth.
|
||||
*/
|
||||
/** @param {any} request */
|
||||
export async function fetchOwliverSuggestions({ page, query = '' } = {}) {
|
||||
if (!page || routeUnavailable) return [];
|
||||
try {
|
||||
return await base44.owliver.suggestions({ page, query });
|
||||
} catch (error) {
|
||||
if (error?.status === 404) reportMissingRoute();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What Owliver could usefully be asked on this page.
|
||||
*
|
||||
* The server answers, and the server is the only thing that decides: it filters
|
||||
* the readings against the caller's role and — with nothing typed — ranks them
|
||||
* against the organization's actual state in PostgreSQL. This hook requests and
|
||||
* caches; it does not score, filter or reorder.
|
||||
*
|
||||
* Keyed on the page and the query together, so every distinct composer state
|
||||
* has its own cache entry and typing back to something already asked is
|
||||
* answered without a request. `placeholderData` keeps the previous answer on
|
||||
* screen while the next one is in flight, which is what stops the chip row
|
||||
* emptying and refilling between keystrokes.
|
||||
*
|
||||
* The untyped request is the one that reads the database, so it is invalidated
|
||||
* whenever something the context counts has changed. See `owliverContextKey`.
|
||||
*/
|
||||
/** @param {any} request */
|
||||
export function useOwliverSuggestions({ page, query = '', enabled = true } = {}) {
|
||||
const typed = String(query || '').trim();
|
||||
return useQuery({
|
||||
queryKey: ['owliverSuggestions', page || null, typed],
|
||||
queryFn: () => fetchOwliverSuggestions({ page, query: typed }),
|
||||
enabled: Boolean(page) && enabled,
|
||||
/* Long enough that a keystroke returned to is instant, short enough that a
|
||||
position created in another tab is reflected on the next open. */
|
||||
staleTime: 30_000,
|
||||
placeholderData: (previous) => previous,
|
||||
/* `fetchOwliverSuggestions` never rejects, so a retry would only repeat a
|
||||
request that already resolved. Said explicitly so the default of one
|
||||
retry does not read as a safety net that is doing something. */
|
||||
retry: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateJobPosting() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: /** @param {any} data */ (data) => base44.entities.JobPosting.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['jobPostings'] });
|
||||
/* The position count the server ranks suggestions against has changed, so
|
||||
what Owliver offers next has to be asked again rather than read from a
|
||||
cache written before the record existed. */
|
||||
queryClient.invalidateQueries({ queryKey: owliverContextKey });
|
||||
logActivity('create_position');
|
||||
},
|
||||
});
|
||||
@@ -195,6 +324,7 @@ export function useUpdateJobPosting() {
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['jobPostings'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['jobPosting', variables.id] });
|
||||
queryClient.invalidateQueries({ queryKey: owliverContextKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -205,6 +335,7 @@ export function useCreateApplication() {
|
||||
mutationFn: /** @param {any} data */ (data) => base44.entities.JobApplication.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||
queryClient.invalidateQueries({ queryKey: owliverContextKey });
|
||||
logActivity('apply_job');
|
||||
},
|
||||
});
|
||||
@@ -216,6 +347,7 @@ export function useUpdateApplication() {
|
||||
mutationFn: /** @param {any} vars */ ({ id, data }) => base44.entities.JobApplication.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||
queryClient.invalidateQueries({ queryKey: owliverContextKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -238,6 +370,7 @@ export function useScreenCandidate() {
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||
queryClient.invalidateQueries({ queryKey: owliverContextKey });
|
||||
logActivity('screen_candidate');
|
||||
},
|
||||
});
|
||||
@@ -268,6 +401,7 @@ export function useScreenAllCandidates() {
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||
queryClient.invalidateQueries({ queryKey: owliverContextKey });
|
||||
logActivity('screen_candidate');
|
||||
},
|
||||
});
|
||||
@@ -326,6 +460,7 @@ export function useHireCandidate() {
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||
queryClient.invalidateQueries({ queryKey: owliverContextKey });
|
||||
queryClient.invalidateQueries({ queryKey: ['staff'] });
|
||||
/* The `hire_candidate` entry is written inside the same transaction as the
|
||||
hire, so there is nothing to log here — only something to refetch. A
|
||||
@@ -358,6 +493,7 @@ export function useCreateInterview() {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['interviews'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||
queryClient.invalidateQueries({ queryKey: owliverContextKey });
|
||||
logActivity('start_interview');
|
||||
},
|
||||
});
|
||||
@@ -507,6 +643,7 @@ export function useAssignWorkers() {
|
||||
looking at three different moments. */
|
||||
queryClient.invalidateQueries({ queryKey: ['assignments'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||
queryClient.invalidateQueries({ queryKey: owliverContextKey });
|
||||
queryClient.invalidateQueries({ queryKey: ['jobPostings'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['userActivity'] });
|
||||
},
|
||||
@@ -549,6 +686,7 @@ export function useMarkInterviewReady() {
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||
queryClient.invalidateQueries({ queryKey: owliverContextKey });
|
||||
queryClient.invalidateQueries({ queryKey: ['interviews'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['userActivity'] });
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user