feat: initialize project with docker-compose infrastructure and server application logic
This commit is contained in:
@@ -41,7 +41,8 @@ services:
|
||||
MINIO_PUBLIC_URL: ${MINIO_PUBLIC_URL:-https://greenlenspro.com/storage}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||
OPENAI_SCAN_MODEL: ${OPENAI_SCAN_MODEL:-gpt-5-mini}
|
||||
OPENAI_HEALTH_MODEL: ${OPENAI_HEALTH_MODEL:-gpt-5-mini}
|
||||
OPENAI_SCAN_MODEL_PRO: ${OPENAI_SCAN_MODEL_PRO:-gpt-5.4}
|
||||
OPENAI_HEALTH_MODEL: ${OPENAI_HEALTH_MODEL:-gpt-4o-mini}
|
||||
REVENUECAT_WEBHOOK_SECRET: ${REVENUECAT_WEBHOOK_SECRET:-}
|
||||
REVENUECAT_PRO_ENTITLEMENT_ID: ${REVENUECAT_PRO_ENTITLEMENT_ID:-pro}
|
||||
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required}
|
||||
|
||||
@@ -180,7 +180,7 @@ const toPlantResult = (entry, confidence) => {
|
||||
};
|
||||
};
|
||||
|
||||
const pickCatalogFallback = (entries, imageUri, preferHighConfidence = false) => {
|
||||
const pickCatalogFallback = (entries, imageUri, preferHighConfidence = false, { silent = false } = {}) => {
|
||||
if (!Array.isArray(entries) || entries.length === 0) return null;
|
||||
const baseHash = hashString(`${imageUri || ''}|${entries.length}`);
|
||||
const index = baseHash % entries.length;
|
||||
@@ -188,11 +188,13 @@ const pickCatalogFallback = (entries, imageUri, preferHighConfidence = false) =>
|
||||
const confidence = preferHighConfidence
|
||||
? 0.22 + ((baseHash % 3) / 100)
|
||||
: 0.18 + ((baseHash % 7) / 100);
|
||||
console.warn('Using hash-based catalog fallback — OpenAI is unavailable or returned null.', {
|
||||
plant: entries[index]?.name,
|
||||
confidence,
|
||||
imageHint: (imageUri || '').slice(0, 80),
|
||||
});
|
||||
if (!silent) {
|
||||
console.warn('Using hash-based catalog fallback — OpenAI is unavailable or returned null.', {
|
||||
plant: entries[index]?.name,
|
||||
confidence,
|
||||
imageHint: (imageUri || '').slice(0, 80),
|
||||
});
|
||||
}
|
||||
return toPlantResult(entries[index], confidence);
|
||||
};
|
||||
|
||||
@@ -637,7 +639,7 @@ app.post('/v1/scan', async (request, response) => {
|
||||
const accountSnapshot = await getAccountSnapshot(db, userId);
|
||||
const scanPlan = accountSnapshot.plan === 'pro' ? 'pro' : 'free';
|
||||
const catalogEntries = await getPlants(db, { limit: 500 });
|
||||
let result = pickCatalogFallback(catalogEntries, imageUri, false);
|
||||
let result = pickCatalogFallback(catalogEntries, imageUri, false, { silent: true });
|
||||
let usedOpenAi = false;
|
||||
|
||||
if (isOpenAiConfigured()) {
|
||||
@@ -662,7 +664,10 @@ app.post('/v1/scan', async (request, response) => {
|
||||
modelPath.push('openai-primary');
|
||||
if (grounded.grounded) modelPath.push('catalog-grounded-primary');
|
||||
} else {
|
||||
console.warn(`OpenAI primary identification returned null for user ${userId}`);
|
||||
console.warn(`OpenAI primary identification returned null for user ${userId} — using catalog fallback.`, {
|
||||
attemptedModels: openAiPrimary?.attemptedModels,
|
||||
plant: result?.name,
|
||||
});
|
||||
modelPath.push('openai-primary-failed');
|
||||
modelPath.push('catalog-primary-fallback');
|
||||
}
|
||||
@@ -711,11 +716,13 @@ app.post('/v1/scan', async (request, response) => {
|
||||
modelPath.push('openai-review');
|
||||
if (grounded.grounded) modelPath.push('catalog-grounded-review');
|
||||
} else {
|
||||
console.warn(`OpenAI review identification returned null for user ${userId}`);
|
||||
console.warn(`OpenAI review identification returned null for user ${userId}.`, {
|
||||
attemptedModels: openAiReview?.attemptedModels,
|
||||
});
|
||||
modelPath.push('openai-review-failed');
|
||||
}
|
||||
} else {
|
||||
const reviewFallback = pickCatalogFallback(catalogEntries, `${imageUri}|review`, true);
|
||||
const reviewFallback = pickCatalogFallback(catalogEntries, `${imageUri}|review`, true, { silent: true });
|
||||
if (reviewFallback) {
|
||||
result = reviewFallback;
|
||||
}
|
||||
@@ -827,7 +834,13 @@ app.post('/v1/health-check', async (request, response) => {
|
||||
});
|
||||
const analysis = analysisResponse?.analysis;
|
||||
if (!analysis) {
|
||||
const error = new Error('OpenAI health check failed. Please verify API key, model, and network access.');
|
||||
console.warn('Health check analysis was null — all models returned unusable output.', {
|
||||
attemptedModels: analysisResponse?.attemptedModels,
|
||||
modelUsed: analysisResponse?.modelUsed,
|
||||
});
|
||||
const error = new Error(
|
||||
`Health check AI failed. Tried: ${(analysisResponse?.attemptedModels || []).join(', ')}. Verify API key, model access, and network.`
|
||||
);
|
||||
error.code = 'PROVIDER_ERROR';
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -137,15 +137,16 @@ const normalizeHealthAnalysis = (raw, language) => {
|
||||
const actionsNowRaw = getStringArray(raw.actionsNow).slice(0, 8);
|
||||
const plan7DaysRaw = getStringArray(raw.plan7Days).slice(0, 10);
|
||||
|
||||
if (scoreRaw == null || !statusRaw || !Array.isArray(issuesRaw)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const status = statusRaw === 'healthy' || statusRaw === 'watch' || statusRaw === 'critical'
|
||||
// Use safe defaults instead of returning null — bad/partial JSON falls through
|
||||
// to the graceful "Uncertain analysis" fallback at line 164 rather than
|
||||
// propagating null → PROVIDER_ERROR to the caller.
|
||||
const score = scoreRaw ?? 50;
|
||||
const status = (statusRaw === 'healthy' || statusRaw === 'watch' || statusRaw === 'critical')
|
||||
? statusRaw
|
||||
: 'watch';
|
||||
const issuesInput = Array.isArray(issuesRaw) ? issuesRaw : [];
|
||||
|
||||
const likelyIssues = issuesRaw
|
||||
const likelyIssues = issuesInput
|
||||
.map((entry) => {
|
||||
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null;
|
||||
const title = getString(entry.title);
|
||||
@@ -168,7 +169,7 @@ const normalizeHealthAnalysis = (raw, language) => {
|
||||
? 'La IA no pudo extraer senales de salud estables.'
|
||||
: 'AI could not extract stable health signals.';
|
||||
return {
|
||||
overallHealthScore: Math.round(clamp(scoreRaw, 0, 100)),
|
||||
overallHealthScore: Math.round(clamp(score, 0, 100)),
|
||||
status,
|
||||
likelyIssues: [
|
||||
{
|
||||
@@ -191,7 +192,7 @@ const normalizeHealthAnalysis = (raw, language) => {
|
||||
}
|
||||
|
||||
return {
|
||||
overallHealthScore: Math.round(clamp(scoreRaw, 0, 100)),
|
||||
overallHealthScore: Math.round(clamp(score, 0, 100)),
|
||||
status,
|
||||
likelyIssues,
|
||||
actionsNow: actionsNowRaw,
|
||||
@@ -305,10 +306,14 @@ const postChatCompletion = async ({ modelChain, messages, imageUri, temperature
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
let parsedError = {};
|
||||
try { parsedError = JSON.parse(body); } catch {}
|
||||
console.warn('OpenAI request HTTP error.', {
|
||||
status: response.status,
|
||||
model,
|
||||
endpoint: OPENAI_CHAT_COMPLETIONS_URL,
|
||||
openAiCode: parsedError?.error?.code,
|
||||
openAiMessage: parsedError?.error?.message,
|
||||
image: summarizeImageUri(imageUri),
|
||||
bodyPreview: body.slice(0, 300),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user