mirror of
https://github.com/clucraft/PriceGhost.git
synced 2026-07-14 16:32:14 +02:00
Merge 89c71a3dde into 33b944588d
This commit is contained in:
commit
4d25d09ffe
12 changed files with 570 additions and 318 deletions
4
backend/package-lock.json
generated
4
backend/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "priceghost-backend",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "priceghost-backend",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.24.0",
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export interface AISettings {
|
|||
anthropic_model: string | null;
|
||||
openai_api_key: string | null;
|
||||
openai_model: string | null;
|
||||
openai_base_url: string | null;
|
||||
ollama_base_url: string | null;
|
||||
ollama_model: string | null;
|
||||
gemini_api_key: string | null;
|
||||
|
|
@ -244,11 +245,11 @@ export const userQueries = {
|
|||
return (result.rowCount ?? 0) > 0;
|
||||
},
|
||||
|
||||
getAISettings: async (id: number): Promise<AISettings | null> => {
|
||||
getAISettings: async (id: number): Promise<AISettings | null> => {
|
||||
const result = await pool.query(
|
||||
`SELECT ai_enabled, COALESCE(ai_verification_enabled, false) as ai_verification_enabled,
|
||||
ai_provider, anthropic_api_key, anthropic_model, openai_api_key, openai_model,
|
||||
ollama_base_url, ollama_model, gemini_api_key, gemini_model
|
||||
openai_base_url, ollama_base_url, ollama_model, gemini_api_key, gemini_model
|
||||
FROM users WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
|
@ -291,6 +292,10 @@ export const userQueries = {
|
|||
fields.push(`openai_model = $${paramIndex++}`);
|
||||
values.push(settings.openai_model);
|
||||
}
|
||||
if (settings.openai_base_url !== undefined) {
|
||||
fields.push(`openai_base_url = $${paramIndex++}`);
|
||||
values.push(settings.openai_base_url);
|
||||
}
|
||||
if (settings.ollama_base_url !== undefined) {
|
||||
fields.push(`ollama_base_url = $${paramIndex++}`);
|
||||
values.push(settings.ollama_base_url);
|
||||
|
|
@ -315,7 +320,7 @@ export const userQueries = {
|
|||
`UPDATE users SET ${fields.join(', ')} WHERE id = $${paramIndex}
|
||||
RETURNING ai_enabled, COALESCE(ai_verification_enabled, false) as ai_verification_enabled,
|
||||
ai_provider, anthropic_api_key, anthropic_model, openai_api_key, openai_model,
|
||||
ollama_base_url, ollama_model, gemini_api_key, gemini_model`,
|
||||
openai_base_url, ollama_base_url, ollama_model, gemini_api_key, gemini_model`,
|
||||
values
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ router.post('/', async (req: AuthRequest, res: Response) => {
|
|||
// If user is confirming a price selection, use the old scraper with their choice
|
||||
if (selectedPrice !== undefined && selectedMethod) {
|
||||
// User has selected a price from candidates - use it directly
|
||||
const scrapedData = await scrapeProduct(url, userId);
|
||||
const scrapedData = await scrapeProductWithVoting(url, userId);
|
||||
|
||||
// Create product with the user-selected price
|
||||
const product = await productQueries.create(
|
||||
|
|
|
|||
|
|
@ -335,6 +335,7 @@ router.get('/ai', async (req: AuthRequest, res: Response) => {
|
|||
anthropic_model: settings.anthropic_model || null,
|
||||
openai_api_key: settings.openai_api_key || null,
|
||||
openai_model: settings.openai_model || null,
|
||||
openai_base_url: settings.openai_base_url || null,
|
||||
ollama_base_url: settings.ollama_base_url || null,
|
||||
ollama_model: settings.ollama_model || null,
|
||||
gemini_api_key: settings.gemini_api_key || null,
|
||||
|
|
@ -358,6 +359,7 @@ router.put('/ai', async (req: AuthRequest, res: Response) => {
|
|||
anthropic_model,
|
||||
openai_api_key,
|
||||
openai_model,
|
||||
openai_base_url,
|
||||
ollama_base_url,
|
||||
ollama_model,
|
||||
gemini_api_key,
|
||||
|
|
@ -372,6 +374,7 @@ router.put('/ai', async (req: AuthRequest, res: Response) => {
|
|||
anthropic_model,
|
||||
openai_api_key,
|
||||
openai_model,
|
||||
openai_base_url,
|
||||
ollama_base_url,
|
||||
ollama_model,
|
||||
gemini_api_key,
|
||||
|
|
@ -391,6 +394,7 @@ router.put('/ai', async (req: AuthRequest, res: Response) => {
|
|||
anthropic_model: settings.anthropic_model || null,
|
||||
openai_api_key: settings.openai_api_key || null,
|
||||
openai_model: settings.openai_model || null,
|
||||
openai_base_url: settings.openai_base_url || null,
|
||||
ollama_base_url: settings.ollama_base_url || null,
|
||||
ollama_model: settings.ollama_model || null,
|
||||
gemini_api_key: settings.gemini_api_key || null,
|
||||
|
|
|
|||
|
|
@ -253,9 +253,12 @@ async function extractWithAnthropic(
|
|||
async function extractWithOpenAI(
|
||||
html: string,
|
||||
apiKey: string,
|
||||
model?: string | null
|
||||
model?: string | null,
|
||||
baseURL?: string | null
|
||||
): Promise<AIExtractionResult> {
|
||||
const openai = new OpenAI({ apiKey });
|
||||
const openaiConfig: Record<string, string> = { apiKey };
|
||||
if (baseURL) openaiConfig.baseURL = baseURL;
|
||||
const openai = new OpenAI(openaiConfig);
|
||||
|
||||
const preparedHtml = prepareHtmlForAI(html);
|
||||
const modelToUse = model || DEFAULT_OPENAI_MODEL;
|
||||
|
|
@ -383,9 +386,12 @@ async function verifyWithOpenAI(
|
|||
scrapedPrice: number,
|
||||
currency: string,
|
||||
apiKey: string,
|
||||
model?: string | null
|
||||
model?: string | null,
|
||||
baseURL?: string | null
|
||||
): Promise<AIVerificationResult> {
|
||||
const openai = new OpenAI({ apiKey });
|
||||
const openaiConfig: Record<string, string> = { apiKey };
|
||||
if (baseURL) openaiConfig.baseURL = baseURL;
|
||||
const openai = new OpenAI(openaiConfig);
|
||||
|
||||
const preparedHtml = prepareHtmlForAI(html);
|
||||
const prompt = VERIFICATION_PROMPT
|
||||
|
|
@ -509,9 +515,12 @@ async function verifyStockStatusWithOpenAI(
|
|||
variantPrice: number,
|
||||
currency: string,
|
||||
apiKey: string,
|
||||
model?: string | null
|
||||
model?: string | null,
|
||||
baseURL?: string | null
|
||||
): Promise<AIStockStatusResult> {
|
||||
const openai = new OpenAI({ apiKey });
|
||||
const openaiConfig: Record<string, string> = { apiKey };
|
||||
if (baseURL) openaiConfig.baseURL = baseURL;
|
||||
const openai = new OpenAI(openaiConfig);
|
||||
|
||||
const preparedHtml = prepareHtmlForAI(html);
|
||||
const prompt = STOCK_STATUS_PROMPT
|
||||
|
|
@ -808,7 +817,7 @@ export async function extractWithAI(
|
|||
if (settings.ai_provider === 'anthropic' && settings.anthropic_api_key) {
|
||||
return extractWithAnthropic(html, settings.anthropic_api_key, settings.anthropic_model);
|
||||
} else if (settings.ai_provider === 'openai' && settings.openai_api_key) {
|
||||
return extractWithOpenAI(html, settings.openai_api_key, settings.openai_model);
|
||||
return extractWithOpenAI(html, settings.openai_api_key, settings.openai_model, settings.openai_base_url);
|
||||
} else if (settings.ai_provider === 'ollama' && settings.ollama_base_url && settings.ollama_model) {
|
||||
return extractWithOllama(html, settings.ollama_base_url, settings.ollama_model);
|
||||
} else if (settings.ai_provider === 'gemini' && settings.gemini_api_key) {
|
||||
|
|
@ -841,7 +850,7 @@ export async function tryAIExtraction(
|
|||
} else if (settings.ai_provider === 'openai' && settings.openai_api_key) {
|
||||
const modelToUse = settings.openai_model || DEFAULT_OPENAI_MODEL;
|
||||
console.log(`[AI] Using OpenAI (${modelToUse}) for ${url}`);
|
||||
return await extractWithOpenAI(html, settings.openai_api_key, settings.openai_model);
|
||||
return await extractWithOpenAI(html, settings.openai_api_key, settings.openai_model, settings.openai_base_url);
|
||||
} else if (settings.ai_provider === 'ollama' && settings.ollama_base_url && settings.ollama_model) {
|
||||
console.log(`[AI] Using Ollama (${settings.ollama_model}) for ${url}`);
|
||||
return await extractWithOllama(html, settings.ollama_base_url, settings.ollama_model);
|
||||
|
|
@ -883,7 +892,7 @@ export async function tryAIVerification(
|
|||
} else if (settings.ai_provider === 'openai' && settings.openai_api_key) {
|
||||
const modelToUse = settings.openai_model || DEFAULT_OPENAI_MODEL;
|
||||
console.log(`[AI Verify] Using OpenAI (${modelToUse}) to verify $${scrapedPrice} for ${url}`);
|
||||
return await verifyWithOpenAI(html, scrapedPrice, currency, settings.openai_api_key, settings.openai_model);
|
||||
return await verifyWithOpenAI(html, scrapedPrice, currency, settings.openai_api_key, settings.openai_model, settings.openai_base_url);
|
||||
} else if (settings.ai_provider === 'ollama' && settings.ollama_base_url && settings.ollama_model) {
|
||||
console.log(`[AI Verify] Using Ollama (${settings.ollama_model}) to verify $${scrapedPrice} for ${url}`);
|
||||
return await verifyWithOllama(html, scrapedPrice, currency, settings.ollama_base_url, settings.ollama_model);
|
||||
|
|
@ -926,7 +935,7 @@ export async function tryAIStockStatusVerification(
|
|||
} else if (settings.ai_provider === 'openai' && settings.openai_api_key) {
|
||||
const modelToUse = settings.openai_model || DEFAULT_OPENAI_MODEL;
|
||||
console.log(`[AI Stock] Using OpenAI (${modelToUse}) to verify stock status for $${variantPrice} variant at ${url}`);
|
||||
return await verifyStockStatusWithOpenAI(html, variantPrice, currency, settings.openai_api_key, settings.openai_model);
|
||||
return await verifyStockStatusWithOpenAI(html, variantPrice, currency, settings.openai_api_key, settings.openai_model, settings.openai_base_url);
|
||||
} else if (settings.ai_provider === 'ollama' && settings.ollama_base_url && settings.ollama_model) {
|
||||
console.log(`[AI Stock] Using Ollama (${settings.ollama_model}) to verify stock status for $${variantPrice} variant at ${url}`);
|
||||
return await verifyStockStatusWithOllama(html, variantPrice, currency, settings.ollama_base_url, settings.ollama_model);
|
||||
|
|
@ -1009,9 +1018,12 @@ async function arbitrateWithOpenAI(
|
|||
html: string,
|
||||
candidates: PriceCandidate[],
|
||||
apiKey: string,
|
||||
model?: string | null
|
||||
model?: string | null,
|
||||
baseURL?: string | null
|
||||
): Promise<AIArbitrationResult> {
|
||||
const openai = new OpenAI({ apiKey });
|
||||
const openaiConfig: Record<string, string> = { apiKey };
|
||||
if (baseURL) openaiConfig.baseURL = baseURL;
|
||||
const openai = new OpenAI(openaiConfig);
|
||||
|
||||
const candidatesList = candidates.map((c, i) =>
|
||||
`${i}. ${c.price} ${c.currency} (method: ${c.method}, context: ${c.context || 'none'})`
|
||||
|
|
@ -1180,7 +1192,7 @@ export async function tryAIArbitration(
|
|||
} else if (settings.ai_provider === 'openai' && settings.openai_api_key) {
|
||||
const modelToUse = settings.openai_model || DEFAULT_OPENAI_MODEL;
|
||||
console.log(`[AI Arbitrate] Using OpenAI (${modelToUse}) to arbitrate ${candidates.length} prices for ${url}`);
|
||||
return await arbitrateWithOpenAI(html, candidates, settings.openai_api_key, settings.openai_model);
|
||||
return await arbitrateWithOpenAI(html, candidates, settings.openai_api_key, settings.openai_model, settings.openai_base_url);
|
||||
} else if (settings.ai_provider === 'ollama' && settings.ollama_base_url && settings.ollama_model) {
|
||||
console.log(`[AI Arbitrate] Using Ollama (${settings.ollama_model}) to arbitrate ${candidates.length} prices for ${url}`);
|
||||
return await arbitrateWithOllama(html, candidates, settings.ollama_base_url, settings.ollama_model);
|
||||
|
|
|
|||
|
|
@ -335,7 +335,7 @@ interface SiteScraper {
|
|||
const siteScrapers: SiteScraper[] = [
|
||||
// Amazon
|
||||
{
|
||||
match: (url) => /amazon\.(com|co\.uk|ca|de|fr|es|it|co\.jp|in|com\.au)/i.test(url),
|
||||
match: (url) => /(?:a\.co|amazon\.(?:com|co\.uk|ca|de|fr|es|it|co\.jp|in|com\.au))/i.test(url),
|
||||
scrape: ($) => {
|
||||
// Helper to check if element is inside a coupon/savings container
|
||||
const isInCouponContainer = (el: ReturnType<typeof $>) => {
|
||||
|
|
@ -1147,16 +1147,6 @@ export async function scrapeProduct(url: string, userId?: number): Promise<Scrap
|
|||
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Cache-Control': 'no-cache',
|
||||
Pragma: 'no-cache',
|
||||
'Sec-Ch-Ua': '"Not A(Brand";v="99", "Google Chrome";v="121", "Chromium";v="121"',
|
||||
'Sec-Ch-Ua-Mobile': '?0',
|
||||
'Sec-Ch-Ua-Platform': '"Windows"',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-User': '?1',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
},
|
||||
timeout: 20000,
|
||||
maxRedirects: 5,
|
||||
|
|
@ -1408,16 +1398,6 @@ export async function scrapeProductWithVoting(
|
|||
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Cache-Control': 'no-cache',
|
||||
Pragma: 'no-cache',
|
||||
'Sec-Ch-Ua': '"Not A(Brand";v="99", "Google Chrome";v="121", "Chromium";v="121"',
|
||||
'Sec-Ch-Ua-Mobile': '?0',
|
||||
'Sec-Ch-Ua-Platform': '"Windows"',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-User': '?1',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
},
|
||||
timeout: 20000,
|
||||
maxRedirects: 5,
|
||||
|
|
|
|||
|
|
@ -119,6 +119,17 @@ BEGIN
|
|||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Migration: Add AI base URL columns to users if they don't exist
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND column_name = 'openai_base_url'
|
||||
) THEN
|
||||
ALTER TABLE users ADD COLUMN openai_base_url VARCHAR(512);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Price history table
|
||||
CREATE TABLE IF NOT EXISTS price_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
|
|
|||
588
frontend/package-lock.json
generated
588
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -259,6 +259,7 @@ export const settingsApi = {
|
|||
anthropic_model?: string | null;
|
||||
openai_api_key?: string | null;
|
||||
openai_model?: string | null;
|
||||
openai_base_url?: string | null;
|
||||
ollama_base_url?: string | null;
|
||||
ollama_model?: string | null;
|
||||
gemini_api_key?: string | null;
|
||||
|
|
@ -284,6 +285,7 @@ export interface AISettings {
|
|||
anthropic_model: string | null;
|
||||
openai_api_key: string | null;
|
||||
openai_model: string | null;
|
||||
openai_base_url: string | null;
|
||||
ollama_base_url: string | null;
|
||||
ollama_model: string | null;
|
||||
gemini_api_key: string | null;
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export default function Settings() {
|
|||
const [anthropicModel, setAnthropicModel] = useState('');
|
||||
const [openaiApiKey, setOpenaiApiKey] = useState('');
|
||||
const [openaiModel, setOpenaiModel] = useState('');
|
||||
const [openaiBaseUrl, setOpenaiBaseUrl] = useState('');
|
||||
const [ollamaBaseUrl, setOllamaBaseUrl] = useState('');
|
||||
const [ollamaModel, setOllamaModel] = useState('');
|
||||
const [availableOllamaModels, setAvailableOllamaModels] = useState<string[]>([]);
|
||||
|
|
@ -135,6 +136,7 @@ export default function Settings() {
|
|||
setAnthropicModel(aiRes.data.anthropic_model || '');
|
||||
setOpenaiApiKey(aiRes.data.openai_api_key || '');
|
||||
setOpenaiModel(aiRes.data.openai_model || '');
|
||||
setOpenaiBaseUrl(aiRes.data.openai_base_url || '');
|
||||
setOllamaBaseUrl(aiRes.data.ollama_base_url || '');
|
||||
setOllamaModel(aiRes.data.ollama_model || '');
|
||||
setGeminiApiKey(aiRes.data.gemini_api_key || '');
|
||||
|
|
@ -462,6 +464,7 @@ export default function Settings() {
|
|||
anthropic_model: aiProvider === 'anthropic' ? anthropicModel || null : undefined,
|
||||
openai_api_key: openaiApiKey || undefined,
|
||||
openai_model: aiProvider === 'openai' ? openaiModel || null : undefined,
|
||||
openai_base_url: aiProvider === 'openai' ? openaiBaseUrl || null : undefined,
|
||||
ollama_base_url: aiProvider === 'ollama' ? ollamaBaseUrl || null : undefined,
|
||||
ollama_model: aiProvider === 'ollama' ? ollamaModel || null : undefined,
|
||||
gemini_api_key: geminiApiKey || undefined,
|
||||
|
|
@ -471,6 +474,7 @@ export default function Settings() {
|
|||
setAIVerificationEnabled(response.data.ai_verification_enabled ?? false);
|
||||
setAnthropicModel(response.data.anthropic_model || '');
|
||||
setOpenaiModel(response.data.openai_model || '');
|
||||
setOpenaiBaseUrl(response.data.openai_base_url || '');
|
||||
setGeminiModel(response.data.gemini_model || '');
|
||||
setAnthropicApiKey('');
|
||||
setOpenaiApiKey('');
|
||||
|
|
@ -1763,6 +1767,21 @@ export default function Settings() {
|
|||
{aiSettings?.openai_model && ` (currently: ${aiSettings.openai_model})`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="settings-form-group">
|
||||
<label>Base URL (optional — for LiteLLM / OpenAI-compatible proxies)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={openaiBaseUrl}
|
||||
onChange={(e) => setOpenaiBaseUrl(e.target.value)}
|
||||
placeholder="https://api.openai.com/v1"
|
||||
/>
|
||||
<p className="hint">
|
||||
Leave blank for default OpenAI API. Set to your LiteLLM proxy URL (e.g., http://litellm:4000/v1)
|
||||
to use a custom model provider.
|
||||
{aiSettings?.openai_base_url && ` (currently: ${aiSettings.openai_base_url})`}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/components/aistatusbadge.tsx","./src/components/authform.tsx","./src/components/layout.tsx","./src/components/notificationbell.tsx","./src/components/particlebackground.tsx","./src/components/passwordinput.tsx","./src/components/pricechart.tsx","./src/components/priceselectionmodal.tsx","./src/components/productcard.tsx","./src/components/productform.tsx","./src/components/sparkline.tsx","./src/components/stocktimeline.tsx","./src/context/authcontext.tsx","./src/context/toastcontext.tsx","./src/hooks/useauth.ts","./src/pages/dashboard.tsx","./src/pages/login.tsx","./src/pages/notificationhistory.tsx","./src/pages/productdetail.tsx","./src/pages/register.tsx","./src/pages/settings.tsx"],"version":"5.9.3"}
|
||||
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/components/AIStatusBadge.tsx","./src/components/AuthForm.tsx","./src/components/Layout.tsx","./src/components/NotificationBell.tsx","./src/components/ParticleBackground.tsx","./src/components/PasswordInput.tsx","./src/components/PriceChart.tsx","./src/components/PriceSelectionModal.tsx","./src/components/ProductCard.tsx","./src/components/ProductForm.tsx","./src/components/Sparkline.tsx","./src/components/StockTimeline.tsx","./src/context/AuthContext.tsx","./src/context/ToastContext.tsx","./src/hooks/useAuth.ts","./src/pages/Dashboard.tsx","./src/pages/Login.tsx","./src/pages/NotificationHistory.tsx","./src/pages/ProductDetail.tsx","./src/pages/Register.tsx","./src/pages/Settings.tsx"],"version":"5.9.3"}
|
||||
185
kubernetes/priceghost.yaml
Normal file
185
kubernetes/priceghost.yaml
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# =============================================================================
|
||||
# PriceGhost — Kubernetes Deployment Manifests
|
||||
#
|
||||
# Deployed via GitHub Actions (build-priceghost.yml).
|
||||
# Secrets (DATABASE_URL, JWT_SECRET) are managed separately via
|
||||
# the priceghost-secrets Secret in the priceghost namespace.
|
||||
# =============================================================================
|
||||
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: priceghost
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: priceghost
|
||||
namespace: priceghost
|
||||
labels:
|
||||
app: priceghost
|
||||
spec:
|
||||
entryPoints:
|
||||
- web
|
||||
routes:
|
||||
- kind: Rule
|
||||
match: Host(`priceghost.mommycaniplay.com`) || Host(`priceghost.mommycaniwatch.com`)
|
||||
middlewares:
|
||||
- name: authelia-chain
|
||||
namespace: priceghost
|
||||
services:
|
||||
- name: frontend
|
||||
port: 80
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: backend
|
||||
namespace: priceghost
|
||||
labels:
|
||||
app: priceghost
|
||||
component: backend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: priceghost
|
||||
component: backend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: priceghost
|
||||
component: backend
|
||||
spec:
|
||||
containers:
|
||||
- name: backend
|
||||
image: ghcr.io/clucraft/priceghost-backend:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3001
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: database-url
|
||||
name: priceghost-secrets
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: jwt-secret
|
||||
name: priceghost-secrets
|
||||
- name: PORT
|
||||
value: "3001"
|
||||
- name: NODE_ENV
|
||||
value: production
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 3001
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 3001
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: frontend
|
||||
namespace: priceghost
|
||||
labels:
|
||||
app: priceghost
|
||||
component: frontend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: priceghost
|
||||
component: frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: priceghost
|
||||
component: frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: frontend
|
||||
image: ghcr.io/clucraft/priceghost-frontend:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 80
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 80
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "200m"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: backend
|
||||
namespace: priceghost
|
||||
labels:
|
||||
app: priceghost
|
||||
component: backend
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: http
|
||||
port: 3001
|
||||
targetPort: 3001
|
||||
protocol: TCP
|
||||
selector:
|
||||
app: priceghost
|
||||
component: backend
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: frontend
|
||||
namespace: priceghost
|
||||
labels:
|
||||
app: priceghost
|
||||
component: frontend
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 80
|
||||
protocol: TCP
|
||||
selector:
|
||||
app: priceghost
|
||||
component: frontend
|
||||
Loading…
Add table
Add a link
Reference in a new issue