Fix some issues

This commit is contained in:
2026-01-25 20:07:37 +01:00
parent e0be1b22f9
commit 6c6a9fc58c
32 changed files with 1719 additions and 226 deletions

View File

@@ -1388,50 +1388,40 @@ kubectl get pods -n bakery-ia -l app.kubernetes.io/instance=signoz
### Step 7.5: Deploy Kubernetes Infrastructure Monitoring (Required for SigNoz Infrastructure View)
> **Purpose:** Deploy kube-state-metrics and node-exporter to enable Kubernetes infrastructure metrics in SigNoz. Without these components, the SigNoz Infrastructure section will be empty.
> **Purpose:** Deploy the official SigNoz k8s-infra chart to enable comprehensive Kubernetes infrastructure metrics in SigNoz. This replaces the need for separate kube-state-metrics and node-exporter deployments. ❌ Removed legacy components: kube-state-metrics and node-exporter.
**Components Deployed:**
| Component | Purpose | Metrics |
|-----------|---------|---------|
| **kube-state-metrics** | Kubernetes object metrics | Pods, Deployments, Nodes, PVCs, etc. |
| **node-exporter** | Host-level metrics | CPU, Memory, Disk, Network |
| **SigNoz k8s-infra** | Unified Kubernetes infrastructure monitoring | Host metrics (CPU, Memory, Disk, Network), Kubelet metrics (Pod/container usage), Cluster metrics (Deployments, Pods, Nodes), Kubernetes events |
**Deploy using the automated script:**
**Deploy using the official SigNoz k8s-infra chart:**
```bash
# Navigate to the k8s-infra monitoring directory
cd /root/bakery-ia
# Add SigNoz Helm repository (if not already added)
helm repo add signoz https://charts.signoz.io
helm repo update
# Make the script executable (if not already)
chmod +x infrastructure/monitoring/k8s-infra/deploy-k8s-infra-monitoring.sh
# Deploy kube-state-metrics and node-exporter
./infrastructure/monitoring/k8s-infra/deploy-k8s-infra-monitoring.sh --microk8s install
```
**Upgrade SigNoz to scrape the new metrics:**
```bash
# The signoz-values-prod.yaml already includes the Prometheus receiver configuration
# Upgrade SigNoz to apply the scraping configuration
microk8s helm3 upgrade signoz signoz/signoz \
# Install the k8s-infra chart
helm upgrade --install k8s-infra signoz/k8s-infra \
-n bakery-ia \
-f infrastructure/monitoring/signoz/signoz-values-prod.yaml
-f infrastructure/monitoring/signoz/k8s-infra-values-prod.yaml \
--timeout 10m
# Wait for the DaemonSet to be ready
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=signoz-agent -n bakery-ia --timeout=300s
```
**Verify deployment:**
**Verify k8s-infra deployment:**
```bash
# Check pods are running
microk8s kubectl get pods -n bakery-ia | grep -E "(kube-state|node-exporter)"
# Check if the k8s-infra agent is running (should see one pod per node)
kubectl get pods -n bakery-ia -l app.kubernetes.io/name=signoz-agent
# Expected output:
# kube-state-metrics-xxxxxxxxxx-xxxxx 1/1 Running 0 1m
# node-exporter-prometheus-node-exporter-xxxxx 1/1 Running 0 1m
# Check status
./infrastructure/monitoring/k8s-infra/deploy-k8s-infra-monitoring.sh --microk8s status
# Expected output (one pod per cluster node):
# signoz-agent-xxxxx 1/1 Running 0 1m
# signoz-agent-yyyyy 1/1 Running 0 1m
```
**Verify metrics in SigNoz:**
@@ -1440,22 +1430,34 @@ After a few minutes, you should see:
- **Infrastructure → Kubernetes**: Pod status, deployments, nodes, PVCs
- **Infrastructure → Hosts**: CPU, memory, disk, network usage
**Important Notes:**
1. **Legacy Components Removal:** If you previously had kube-state-metrics or node-exporter deployed, you should remove them to avoid duplicate metrics:
```bash
# Remove legacy components if they exist
helm uninstall kube-state-metrics -n bakery-ia 2>/dev/null || true
helm uninstall node-exporter-prometheus-node-exporter -n bakery-ia 2>/dev/null || true
```
2. **Configuration:** The k8s-infra chart is configured via `k8s-infra-values-prod.yaml` which specifies:
- Connection to your SigNoz OTel collector endpoint
- Collection intervals and presets for different metric types
- Resource limits for the monitoring agents
**Troubleshooting:**
```bash
# Check if metrics are being scraped
microk8s kubectl port-forward svc/kube-state-metrics 8080:8080 -n bakery-ia &
curl localhost:8080/metrics | head -20
# Check k8s-infra agent logs
kubectl logs -l app.kubernetes.io/name=signoz-agent -n bakery-ia --tail=50
# Check OTel Collector logs for scraping errors
microk8s kubectl logs -l app.kubernetes.io/name=signoz-otel-collector -n bakery-ia --tail=50
# Verify the agent can connect to SigNoz collector
kubectl logs -l app.kubernetes.io/name=signoz-agent -n bakery-ia | grep -i error
```
> **Files Location:**
> - Helm values: `infrastructure/monitoring/k8s-infra/kube-state-metrics-values.yaml`
> - Helm values: `infrastructure/monitoring/k8s-infra/node-exporter-values.yaml`
> - Deploy script: `infrastructure/monitoring/k8s-infra/deploy-k8s-infra-monitoring.sh`
> - Documentation: `infrastructure/monitoring/k8s-infra/README.md`
> - Helm values: `infrastructure/monitoring/signoz/k8s-infra-values-prod.yaml`
> - Helm values: `infrastructure/monitoring/signoz/k8s-infra-values-dev.yaml`
> - Documentation: `infrastructure/monitoring/signoz/README.md`
---
@@ -1528,30 +1530,119 @@ kubectl exec -n bakery-ia deployment/redis -- redis-cli ping
### Configure Stripe Keys (Required Before Going Live)
Before accepting payments, configure your Stripe credentials:
**IMPORTANT**: Before going live, you MUST replace test keys with live Stripe keys.
#### Step 1: Get Your Live Stripe Keys
1. Go to [Stripe Dashboard](https://dashboard.stripe.com/apikeys)
2. Make sure you're in **Live mode** (toggle in top right)
3. Copy your **Publishable key** (starts with `pk_live_`)
4. Copy your **Secret key** (starts with `sk_live_`)
5. Get your **Webhook signing secret** from Stripe webhook settings
#### Step 2: Update Configuration Files
```bash
# Edit ConfigMap for publishable key
# 1. Update the common configmap with your live publishable key
nano infrastructure/environments/common/configs/configmap.yaml
# Add: VITE_STRIPE_PUBLISHABLE_KEY: "pk_live_XXXXXXXXXXXX"
# Encode your secret keys
echo -n "sk_live_XXXXXXXXXX" | base64 # Your secret key
echo -n "whsec_XXXXXXXXXX" | base64 # Your webhook secret
# Find and replace these lines:
VITE_STRIPE_PUBLISHABLE_KEY: "pk_test_51QuxKyIzCdnBmAVTGM8fvXYkItrBUILz6lHYwhAva6ZAH1HRi0e8zDRgZ4X3faN0zEABp5RHjCVBmMJL3aKXbaC200fFrSNnPl"
VITE_STRIPE_ACCOUNT_ID: "acct_1QuxKsIucMC6K1cg"
# Edit Secrets
# Replace with your live key and account ID:
VITE_STRIPE_PUBLISHABLE_KEY: "pk_live_your_publishable_key_here"
VITE_STRIPE_ACCOUNT_ID: "acct_1QuxKsIucMC6K1cg" # Keep your account ID, just remove "test_" prefix if needed
# 2. Encode your live secret keys (required for Kubernetes secrets)
echo -n "sk_live_your_secret_key_here" | base64
# Example output: c2tfbGl2ZV95b3VyX3NlY3JldF9rZXlfaGVyZQ==
echo -n "whsec_your_webhook_secret_here" | base64
# Example output: d2hzZWNfeW91cl93ZWJob29rX3NlY3JldF9oZXJl
# 3. Update the secrets file
nano infrastructure/environments/common/configs/secrets.yaml
# Add to payment-secrets section:
# STRIPE_SECRET_KEY: <base64-encoded>
# STRIPE_WEBHOOK_SECRET: <base64-encoded>
# Apply the updated configuration
kubectl apply -k infrastructure/environments/prod/k8s-manifests
# Find the payment-secrets section and update:
STRIPE_SECRET_KEY: c2tfbGl2ZV95b3VyX3NlY3JldF9rZXlfaGVyZQ== # Replace with your encoded live secret key
STRIPE_WEBHOOK_SECRET: d2hzZWNfeW91cl93ZWJob29rX3NlY3JldF9oZXJl # Replace with your encoded webhook secret
# Restart services that use Stripe
kubectl rollout restart deployment/payment-service -n bakery-ia
# 4. Update production kustomization
nano infrastructure/environments/prod/k8s-manifests/kustomization.yaml
# Find and update the Stripe configuration patch:
- op: replace
path: /data/VITE_STRIPE_PUBLISHABLE_KEY
value: "pk_live_your_publishable_key_here"
- op: add
path: /data/VITE_STRIPE_ACCOUNT_ID
value: "acct_1QuxKsIucMC6K1cg"
```
#### Step 3: Apply Configuration and Restart Services
```bash
# Apply the updated configuration
kubectl apply -k infrastructure/environments/prod/k8s-manifests/
# Restart services that use Stripe (order matters)
kubectl rollout restart deployment/tenant-service -n bakery-ia
kubectl rollout restart deployment/gateway -n bakery-ia
kubectl rollout restart deployment/frontend -n bakery-ia
# Monitor the restart process
kubectl get pods -n bakery-ia -w
```
#### Step 4: Verify Stripe Configuration
```bash
# Check that the configmap was updated correctly
kubectl get configmap bakery-config -n bakery-ia -o yaml | grep STRIPE
# Check that secrets are properly encoded
kubectl get secret payment-secrets -n bakery-ia -o yaml | grep STRIPE
# Test a small payment (€1.00) with a real card
# Use Stripe test cards first: 4242 4242 4242 4242
```
#### Step 5: Update Stripe Webhooks (Critical)
```bash
# 1. Update your Stripe webhook endpoint to use the live URL:
# https://bakewise.ai/api/webhooks/stripe
# 2. Update the webhook signing secret in Stripe dashboard
# to match what you configured in secrets.yaml
# 3. Test webhooks:
stripe trigger payment_intent.succeeded
stripe trigger invoice.paid
```
#### Step 6: PCI Compliance Checklist
Before going live, ensure:
- [ ] All payment pages use HTTPS (check your ingress TLS configuration)
- [ ] No card data is logged or stored in your databases
- [ ] Your server meets PCI DSS requirements
- [ ] You have a vulnerability management process
- [ ] Regular security audits are scheduled
#### Step 7: Go Live Checklist
- [ ] Stripe live keys configured in all services
- [ ] Webhooks tested and working
- [ ] PCI compliance verified
- [ ] Test payments successful in live mode
- [ ] Refund process tested
- [ ] Customer support ready for payment issues
- [ ] Monitoring set up for payment failures
**WARNING**: Once you switch to live keys, real money will be processed. Start with small test transactions and monitor closely.
### Backup Strategy
```bash
@@ -1730,3 +1821,79 @@ This guide provides a complete, step-by-step process for deploying Bakery-IA to
4. **Scalable:** Designed for 10-100+ tenants with clear scaling path
For questions or issues, refer to the troubleshooting guide or consult the support resources listed above.
### Email System Configuration
#### Setting Up email-secrets Properly
**Important:** The `email-secrets` must be configured to use the Mailu admin account credentials for proper email functionality.
**Recommended Approach:**
1. **Use Mailu Admin Account** (instead of creating separate postmaster account):
```bash
# Get the admin password from mailu-admin-credentials
ADMIN_PASSWORD=$(kubectl get secret mailu-admin-credentials -n bakery-ia -o jsonpath='{.data.password}' | base64 -d)
# Update email-secrets to use admin account
kubectl edit secret email-secrets -n bakery-ia
# Change the values to:
# SMTP_USER: admin@bakewise.ai
# SMTP_PASSWORD: [the admin password you retrieved]
```
2. **Alternative: Create Postmaster Account** (if you prefer separate accounts):
```bash
# Log in to Mailu admin panel
# URL: https://mail.bakewise.ai/admin
# Username: admin@bakewise.ai
# Password: [from mailu-admin-credentials]
# Navigate to Users -> Create New User
# Email: postmaster@bakewise.ai
# Password: [generate secure password]
# Role: Admin (or create custom role with email sending permissions)
# Update email-secrets with the postmaster credentials
kubectl edit secret email-secrets -n bakery-ia
```
**Verifying Email Configuration:**
```bash
# Test email sending via notification service
kubectl exec -n bakery-ia deployment/notification-service -it -- bash
# Inside the container:
python -c "
from app.services.email_service import EmailService
from app.core.config import settings
es = EmailService()
print('Testing email service...')
result = await es.health_check()
print(f'Email service healthy: {result}')
"
```
**Troubleshooting Email Issues:**
```bash
# Check Mailu logs
kubectl logs -n bakery-ia deployment/mailu-postfix | tail -50
# Check notification service logs
kubectl logs -n bakery-ia deployment/notification-service | grep -i email | tail -20
# Test SMTP connection manually
kubectl run -it --rm smtp-test --image=alpine --
apk add openssl &&
openssl s_client -connect mailu-postfix:587 -starttls smtp
```
**DOVEADM_PASSWORD Note:**
- This is for IMAP administration (rarely used)
- Only needed if you require advanced mailbox management
- Can be safely removed if not using IMAP admin features

View File

@@ -148,7 +148,6 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -2278,6 +2277,7 @@
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz",
"integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/intl-localematcher": "0.6.2",
@@ -2290,6 +2290,7 @@
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
@@ -2299,6 +2300,7 @@
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz",
"integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"@formatjs/icu-skeleton-parser": "1.8.16",
@@ -2310,6 +2312,7 @@
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz",
"integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"tslib": "^2.8.0"
@@ -2320,6 +2323,7 @@
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz",
"integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
@@ -2748,7 +2752,6 @@
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -2988,7 +2991,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -6329,7 +6331,6 @@
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-4.10.0.tgz",
"integrity": "sha512-KrMOL+sH69htCIXCaZ4JluJ35bchuCCznyPyrbN8JXSGQfwBI1SuIEMZNwvy8L8ykj29t6sa5BAAiL7fNoLZ8A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.16"
}
@@ -6419,7 +6420,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.89.0.tgz",
"integrity": "sha512-SXbtWSTSRXyBOe80mszPxpEbaN4XPRUp/i0EfQK1uyj3KCk/c8FuPJNIRwzOVe/OU3rzxrYtiNabsAmk1l714A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/query-core": "5.89.0"
},
@@ -6911,7 +6911,6 @@
"integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -6923,7 +6922,6 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -7065,7 +7063,6 @@
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "6.21.0",
"@typescript-eslint/types": "6.21.0",
@@ -7404,7 +7401,6 @@
"integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/utils": "1.6.1",
"fast-glob": "^3.3.2",
@@ -7502,7 +7498,6 @@
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -8045,7 +8040,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -8251,7 +8245,6 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@kurkle/color": "^0.3.0"
},
@@ -8616,8 +8609,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/d3-array": {
"version": "3.2.4",
@@ -8799,7 +8791,6 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.21.0"
},
@@ -8842,7 +8833,8 @@
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
@@ -9326,7 +9318,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -9430,7 +9421,6 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -10856,7 +10846,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.2"
}
@@ -10905,7 +10894,6 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
"integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -11887,8 +11875,7 @@
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause",
"peer": true
"license": "BSD-2-Clause"
},
"node_modules/leven": {
"version": "3.1.0",
@@ -13077,7 +13064,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -13244,7 +13230,6 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -13541,7 +13526,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -13614,7 +13598,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -13680,7 +13663,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",
"integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -14286,7 +14268,6 @@
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -15169,7 +15150,6 @@
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -15701,7 +15681,6 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16113,7 +16092,6 @@
"integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -16677,7 +16655,6 @@
"integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/expect": "1.6.1",
"@vitest/runner": "1.6.1",
@@ -17059,7 +17036,6 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",

View File

@@ -79,6 +79,9 @@ class ApiClient {
const publicEndpoints = [
'/demo/accounts',
'/demo/session/create',
'/public/contact',
'/public/feedback',
'/public/prelaunch-subscribe',
];
// Endpoints that require authentication but not a tenant ID (user-level endpoints)

View File

@@ -0,0 +1,83 @@
/**
* Public Contact API Service
* Handles public form submissions (contact, feedback, prelaunch)
* These endpoints don't require authentication
*/
import axios from 'axios';
import { getApiUrl } from '../../config/runtime';
const publicApiClient = axios.create({
baseURL: getApiUrl(),
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
// Types
export interface ContactFormData {
name: string;
email: string;
phone?: string;
bakery_name?: string;
type: 'general' | 'technical' | 'sales' | 'feedback';
subject: string;
message: string;
}
export interface FeedbackFormData {
name: string;
email: string;
category: 'suggestion' | 'bug' | 'feature' | 'praise' | 'complaint';
title: string;
description: string;
rating?: number;
}
export interface PrelaunchEmailData {
email: string;
}
export interface ContactFormResponse {
success: boolean;
message: string;
}
// API Functions
export const publicContactService = {
/**
* Submit a contact form
*/
submitContactForm: async (data: ContactFormData): Promise<ContactFormResponse> => {
const response = await publicApiClient.post<ContactFormResponse>(
'/v1/public/contact',
data
);
return response.data;
},
/**
* Submit a feedback form
*/
submitFeedbackForm: async (data: FeedbackFormData): Promise<ContactFormResponse> => {
const response = await publicApiClient.post<ContactFormResponse>(
'/v1/public/feedback',
data
);
return response.data;
},
/**
* Submit a prelaunch email subscription
*/
submitPrelaunchEmail: async (data: PrelaunchEmailData): Promise<ContactFormResponse> => {
const response = await publicApiClient.post<ContactFormResponse>(
'/v1/public/prelaunch-subscribe',
data
);
return response.data;
},
};
export default publicContactService;

View File

@@ -0,0 +1,185 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Mail, Rocket, CheckCircle, Loader, ArrowLeft } from 'lucide-react';
import { Button, Input, Card } from '../../ui';
import { publicContactService } from '../../../api/services/publicContact';
interface PrelaunchEmailFormProps {
onLoginClick?: () => void;
className?: string;
}
export const PrelaunchEmailForm: React.FC<PrelaunchEmailFormProps> = ({
onLoginClick,
className = '',
}) => {
const { t } = useTranslation(['auth', 'common']);
const [email, setEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const [error, setError] = useState<string | null>(null);
const validateEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (!email.trim()) {
setError(t('auth:prelaunch.email_required'));
return;
}
if (!validateEmail(email)) {
setError(t('auth:prelaunch.email_invalid'));
return;
}
setIsSubmitting(true);
try {
await publicContactService.submitPrelaunchEmail({ email });
setIsSubmitted(true);
} catch {
setError(t('auth:prelaunch.submit_error'));
} finally {
setIsSubmitting(false);
}
};
if (isSubmitted) {
return (
<Card className={`max-w-lg mx-auto p-8 ${className}`}>
<div className="text-center">
<div className="mx-auto w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mb-6">
<CheckCircle className="w-8 h-8 text-green-600 dark:text-green-400" />
</div>
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-3">
{t('auth:prelaunch.success_title')}
</h2>
<p className="text-[var(--text-secondary)] mb-6">
{t('auth:prelaunch.success_message')}
</p>
<div className="space-y-3">
<Button
onClick={() => window.location.href = '/'}
variant="outline"
className="w-full"
>
<ArrowLeft className="w-4 h-4 mr-2" />
{t('auth:prelaunch.back_to_home')}
</Button>
{onLoginClick && (
<p className="text-sm text-[var(--text-secondary)]">
{t('auth:register.have_account')}{' '}
<button
onClick={onLoginClick}
className="text-[var(--color-primary)] hover:underline font-medium"
>
{t('auth:register.login_link')}
</button>
</p>
)}
</div>
</div>
</Card>
);
}
return (
<Card className={`max-w-lg mx-auto p-8 ${className}`}>
<div className="text-center mb-8">
<div className="mx-auto w-16 h-16 bg-gradient-to-br from-amber-500 to-orange-500 rounded-full flex items-center justify-center mb-6 shadow-lg">
<Rocket className="w-8 h-8 text-white" />
</div>
<h1 className="text-3xl font-bold text-[var(--text-primary)] mb-3">
{t('auth:prelaunch.title')}
</h1>
<p className="text-lg text-[var(--text-secondary)] mb-2">
{t('auth:prelaunch.subtitle')}
</p>
<p className="text-[var(--text-tertiary)]">
{t('auth:prelaunch.description')}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<Input
type="email"
label={t('auth:register.email')}
placeholder={t('auth:register.email_placeholder')}
value={email}
onChange={(e) => setEmail(e.target.value)}
leftIcon={<Mail className="w-5 h-5" />}
error={error || undefined}
isRequired
size="lg"
/>
<Button
type="submit"
disabled={isSubmitting}
className="w-full py-4 text-lg font-semibold"
>
{isSubmitting ? (
<>
<Loader className="w-5 h-5 mr-2 animate-spin" />
{t('auth:prelaunch.submitting')}
</>
) : (
<>
<Mail className="w-5 h-5 mr-2" />
{t('auth:prelaunch.subscribe_button')}
</>
)}
</Button>
</form>
<div className="mt-8 pt-6 border-t border-[var(--border-primary)]">
<h3 className="text-sm font-semibold text-[var(--text-primary)] mb-3">
{t('auth:prelaunch.benefits_title')}
</h3>
<ul className="space-y-2 text-sm text-[var(--text-secondary)]">
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-500" />
{t('auth:prelaunch.benefit_1')}
</li>
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-500" />
{t('auth:prelaunch.benefit_2')}
</li>
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-500" />
{t('auth:prelaunch.benefit_3')}
</li>
</ul>
</div>
{onLoginClick && (
<div className="mt-6 text-center">
<p className="text-sm text-[var(--text-secondary)]">
{t('auth:register.have_account')}{' '}
<button
onClick={onLoginClick}
className="text-[var(--color-primary)] hover:underline font-medium"
>
{t('auth:register.login_link')}
</button>
</p>
</div>
)}
</Card>
);
};
export default PrelaunchEmailForm;

View File

@@ -29,7 +29,12 @@ const getStripeKey = (): string => {
return import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || 'pk_test_51234567890123456789012345678901234567890123456789012345678901234567890123456789012345';
};
const stripePromise = loadStripe(getStripeKey());
// Force Stripe to use test environment by loading from test endpoint
const stripePromise = loadStripe(getStripeKey(), {
stripeAccount: import.meta.env.VITE_STRIPE_ACCOUNT_ID,
apiVersion: '2023-10-16',
betas: ['elements_v2']
});
interface RegistrationContainerProps {
onSuccess?: () => void;

View File

@@ -3,13 +3,15 @@ export { default as LoginForm } from './LoginForm';
export { default as RegistrationContainer } from './RegistrationContainer';
export { default as PasswordResetForm } from './PasswordResetForm';
export { default as ProfileSettings } from './ProfileSettings';
export { default as PrelaunchEmailForm } from './PrelaunchEmailForm';
// Re-export types for convenience
export type {
LoginFormProps,
RegistrationContainerProps,
PasswordResetFormProps,
ProfileSettingsProps
ProfileSettingsProps,
PrelaunchEmailFormProps
} from './types';
// Component metadata for documentation

View File

@@ -29,6 +29,11 @@ export interface ProfileSettingsProps {
initialTab?: 'profile' | 'security' | 'preferences' | 'notifications';
}
export interface PrelaunchEmailFormProps {
onLoginClick?: () => void;
className?: string;
}
// Additional types for internal use
export type RegistrationStep = 'personal' | 'bakery' | 'security' | 'verification';

View File

@@ -52,6 +52,8 @@ const getStripePublishableKey = (): string => {
const stripePromise = loadStripe(getStripePublishableKey(), {
betas: ['elements_v2'],
locale: 'auto',
stripeAccount: import.meta.env.VITE_STRIPE_ACCOUNT_ID,
apiVersion: '2023-10-16'
});
/**

View File

@@ -11,6 +11,7 @@ import {
SUBSCRIPTION_TIERS
} from '../../api';
import { getRegisterUrl } from '../../utils/navigation';
import { PRELAUNCH_CONFIG } from '../../config/prelaunch';
type BillingCycle = 'monthly' | 'yearly';
type DisplayMode = 'landing' | 'settings' | 'selection';
@@ -411,12 +412,16 @@ export const SubscriptionPricingCards: React.FC<SubscriptionPricingCardsProps> =
)
: mode === 'settings'
? t('ui.change_subscription', 'Cambiar Suscripción')
: PRELAUNCH_CONFIG.enabled
? t('ui.notify_me', 'Avísame del Lanzamiento')
: t('ui.start_free_trial')}
</Button>
{/* Footer */}
<p className={`text-xs text-center mt-3 ${(isPopular || isSelected) && !isCurrentPlan ? 'text-white/80' : 'text-[var(--text-secondary)]'}`}>
{showPilotBanner
{PRELAUNCH_CONFIG.enabled
? t('ui.prelaunch_footer', 'Lanzamiento oficial próximamente')
: showPilotBanner
? t('ui.free_trial_footer', { months: pilotTrialMonths })
: t('ui.free_trial_footer', { months: 0 })
}

View File

@@ -0,0 +1,17 @@
/**
* Pre-launch Mode Configuration
* Uses build-time environment variables
*
* When VITE_PRELAUNCH_MODE=true:
* - Registration page shows email capture form instead of Stripe flow
* - Pricing cards link to the same page but show interest form
*
* When VITE_PRELAUNCH_MODE=false (or not set):
* - Normal registration flow with Stripe payments
*/
export const PRELAUNCH_CONFIG = {
enabled: import.meta.env.VITE_PRELAUNCH_MODE === 'false',
};
export default PRELAUNCH_CONFIG;

View File

@@ -116,6 +116,23 @@
"secure_payment": "Your payment information is protected with end-to-end encryption",
"payment_info_secure": "Your payment information is secure"
},
"prelaunch": {
"title": "Coming Soon",
"subtitle": "We're preparing something special for your bakery",
"description": "Be the first to know when we officially launch. Leave your email and we'll notify you.",
"email_required": "Email is required",
"email_invalid": "Please enter a valid email address",
"submit_error": "An error occurred. Please try again.",
"subscribe_button": "Notify Me",
"submitting": "Submitting...",
"success_title": "You're on the list!",
"success_message": "We'll send you an email when we're ready to launch. Thanks for your interest!",
"back_to_home": "Back to Home",
"benefits_title": "By subscribing you'll receive:",
"benefit_1": "Early access to launch",
"benefit_2": "Exclusive offers for early adopters",
"benefit_3": "Product news and updates"
},
"steps": {
"info": "Information",
"subscription": "Plan",

View File

@@ -157,6 +157,8 @@
"payment_details": "Payment Details",
"payment_info_secure": "Your payment information is protected with end-to-end encryption",
"updating_payment": "Updating...",
"cancel": "Cancel"
"cancel": "Cancel",
"notify_me": "Notify Me of Launch",
"prelaunch_footer": "Official launch coming soon"
}
}

View File

@@ -127,6 +127,23 @@
"go_to_login": "Ir a inicio de sesión",
"try_again": "Intentar registro de nuevo"
},
"prelaunch": {
"title": "Próximamente",
"subtitle": "Estamos preparando algo especial para tu panadería",
"description": "Sé el primero en saber cuándo lancemos oficialmente. Déjanos tu email y te avisaremos.",
"email_required": "El correo electrónico es obligatorio",
"email_invalid": "Por favor, introduce un correo electrónico válido",
"submit_error": "Ha ocurrido un error. Por favor, inténtalo de nuevo.",
"subscribe_button": "Quiero que me avisen",
"submitting": "Enviando...",
"success_title": "¡Genial! Te hemos apuntado",
"success_message": "Te enviaremos un email cuando estemos listos para el lanzamiento. ¡Gracias por tu interés!",
"back_to_home": "Volver al inicio",
"benefits_title": "Al suscribirte recibirás:",
"benefit_1": "Acceso anticipado al lanzamiento",
"benefit_2": "Ofertas exclusivas para early adopters",
"benefit_3": "Noticias y actualizaciones del producto"
},
"steps": {
"info": "Información",
"subscription": "Plan",

View File

@@ -157,6 +157,8 @@
"payment_details": "Detalles de Pago",
"payment_info_secure": "Tu información de pago está protegida con encriptación de extremo a extremo",
"updating_payment": "Actualizando...",
"cancel": "Cancelar"
"cancel": "Cancelar",
"notify_me": "Avísame del Lanzamiento",
"prelaunch_footer": "Lanzamiento oficial próximamente"
}
}

View File

@@ -12,6 +12,7 @@ import {
AlertCircle,
HelpCircle
} from 'lucide-react';
import { publicContactService } from '../../api/services/publicContact';
interface ContactMethod {
id: string;
@@ -73,11 +74,16 @@ const ContactPage: React.FC = () => {
e.preventDefault();
setSubmitStatus('loading');
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500));
// In production, this would be an actual API call
console.log('Form submitted:', formState);
try {
await publicContactService.submitContactForm({
name: formState.name,
email: formState.email,
phone: formState.phone || undefined,
bakery_name: formState.bakeryName || undefined,
type: formState.type,
subject: formState.subject,
message: formState.message,
});
setSubmitStatus('success');
setTimeout(() => {
@@ -92,6 +98,11 @@ const ContactPage: React.FC = () => {
type: 'general',
});
}, 3000);
} catch (error) {
console.error('Contact form submission error:', error);
setSubmitStatus('error');
setTimeout(() => setSubmitStatus('idle'), 5000);
}
};
return (

View File

@@ -13,6 +13,7 @@ import {
AlertCircle,
Star
} from 'lucide-react';
import { publicContactService } from '../../api/services/publicContact';
interface FeedbackCategory {
id: string;
@@ -90,11 +91,15 @@ const FeedbackPage: React.FC = () => {
e.preventDefault();
setSubmitStatus('loading');
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500));
// In production, this would be an actual API call
console.log('Feedback submitted:', formState);
try {
await publicContactService.submitFeedbackForm({
name: formState.name,
email: formState.email,
category: formState.category,
title: formState.title,
description: formState.description,
rating: formState.rating > 0 ? formState.rating : undefined,
});
setSubmitStatus('success');
setTimeout(() => {
@@ -108,6 +113,11 @@ const FeedbackPage: React.FC = () => {
rating: 0,
});
}, 3000);
} catch (error) {
console.error('Feedback form submission error:', error);
setSubmitStatus('error');
setTimeout(() => setSubmitStatus('idle'), 5000);
}
};
const getCategoryColor = (color: string) => {

View File

@@ -1,7 +1,8 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { RegistrationContainer } from '../../components/domain/auth';
import { RegistrationContainer, PrelaunchEmailForm } from '../../components/domain/auth';
import { PublicLayout } from '../../components/layout';
import { PRELAUNCH_CONFIG } from '../../config/prelaunch';
const RegisterPage: React.FC = () => {
const navigate = useNavigate();
@@ -14,6 +15,27 @@ const RegisterPage: React.FC = () => {
navigate('/login');
};
// Show prelaunch email form or full registration based on build-time config
if (PRELAUNCH_CONFIG.enabled) {
return (
<PublicLayout
variant="centered"
maxWidth="lg"
headerProps={{
showThemeToggle: true,
showAuthButtons: false,
showLanguageSelector: true,
variant: "minimal"
}}
>
<PrelaunchEmailForm
onLoginClick={handleLoginClick}
className="mx-auto"
/>
</PublicLayout>
);
}
return (
<PublicLayout
variant="centered"

View File

@@ -10,6 +10,7 @@ interface ImportMetaEnv {
readonly VITE_PILOT_MODE_ENABLED?: string
readonly VITE_PILOT_COUPON_CODE?: string
readonly VITE_PILOT_TRIAL_MONTHS?: string
readonly VITE_PRELAUNCH_MODE?: string
}
interface ImportMeta {

View File

@@ -25,7 +25,7 @@ from app.middleware.rate_limiting import APIRateLimitMiddleware
from app.middleware.subscription import SubscriptionMiddleware
from app.middleware.demo_middleware import DemoMiddleware
from app.middleware.read_only_mode import ReadOnlyModeMiddleware
from app.routes import auth, tenant, registration, nominatim, subscription, demo, pos, geocoding, poi_context, webhooks, telemetry
from app.routes import auth, tenant, registration, nominatim, subscription, demo, pos, geocoding, poi_context, webhooks, telemetry, public
# Initialize logger
logger = structlog.get_logger()
@@ -172,6 +172,9 @@ app.include_router(webhooks.router, prefix="", tags=["webhooks"])
# Include telemetry routes for frontend OpenTelemetry data
app.include_router(telemetry.router, prefix="/api/v1", tags=["telemetry"])
# Include public routes (contact forms, feedback, pre-launch subscriptions)
app.include_router(public.router, prefix="/api/v1/public", tags=["public"])
# ================================================================
# SERVER-SENT EVENTS (SSE) HELPER FUNCTIONS

View File

@@ -50,7 +50,10 @@ PUBLIC_ROUTES = [
"/api/v1/webhooks/generic", # Generic webhook endpoint
"/api/v1/telemetry/v1/traces", # Frontend telemetry traces - no auth for performance
"/api/v1/telemetry/v1/metrics", # Frontend telemetry metrics - no auth for performance
"/api/v1/telemetry/health" # Telemetry health check
"/api/v1/telemetry/health", # Telemetry health check
"/api/v1/public/contact", # Public contact form - no auth required
"/api/v1/public/feedback", # Public feedback form - no auth required
"/api/v1/public/prelaunch-subscribe" # Pre-launch email subscription - no auth required
]
# Routes accessible with demo session (no JWT required, just demo session header)

View File

@@ -0,0 +1,56 @@
# gateway/app/routes/public.py
"""
Public routes for API Gateway - Handles unauthenticated public endpoints
"""
from fastapi import APIRouter, Request
import httpx
import logging
from app.core.config import settings
logger = logging.getLogger(__name__)
router = APIRouter()
async def _proxy_to_notification_service(request: Request, path: str):
"""Proxy request to notification service"""
try:
body = await request.body()
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.request(
method=request.method,
url=f"{settings.NOTIFICATION_SERVICE_URL}{path}",
content=body,
headers={
"Content-Type": request.headers.get("Content-Type", "application/json"),
}
)
return response.json() if response.content else {}
except httpx.TimeoutException:
logger.error(f"Timeout proxying to notification service: {path}")
return {"success": False, "message": "Service temporarily unavailable"}
except Exception as e:
logger.error(f"Error proxying to notification service: {e}")
return {"success": False, "message": "Internal error"}
@router.post("/contact")
async def submit_contact_form(request: Request):
"""Proxy contact form submission to notification service"""
return await _proxy_to_notification_service(request, "/api/v1/public/contact")
@router.post("/feedback")
async def submit_feedback_form(request: Request):
"""Proxy feedback form submission to notification service"""
return await _proxy_to_notification_service(request, "/api/v1/public/feedback")
@router.post("/prelaunch-subscribe")
async def submit_prelaunch_email(request: Request):
"""Proxy pre-launch email subscription to notification service"""
return await _proxy_to_notification_service(request, "/api/v1/public/prelaunch-subscribe")

View File

@@ -387,6 +387,7 @@ data:
VITE_PILOT_COUPON_CODE: "PILOT2025"
VITE_PILOT_TRIAL_MONTHS: "3"
VITE_STRIPE_PUBLISHABLE_KEY: "pk_test_51QuxKyIzCdnBmAVTGM8fvXYkItrBUILz6lHYwhAva6ZAH1HRi0e8zDRgZ4X3faN0zEABp5RHjCVBmMJL3aKXbaC200fFrSNnPl"
VITE_STRIPE_ACCOUNT_ID: "acct_1QuxKsIucMC6K1cg"
# ================================================================
# LOCATION SETTINGS (Nominatim Geocoding)

View File

@@ -107,6 +107,12 @@ patches:
- op: add
path: /data/VITE_ENVIRONMENT
value: "production"
- op: replace
path: /data/VITE_STRIPE_PUBLISHABLE_KEY
value: "pk_test_51QuxKyIzCdnBmAVTGM8fvXYkItrBUILz6lHYwhAva6ZAH1HRi0e8zDRgZ4X3faN0zEABp5RHjCVBmMJL3aKXbaC200fFrSNnPl"
- op: add
path: /data/VITE_STRIPE_ACCOUNT_ID
value: "acct_1QuxKsIucMC6K1cg"
# Add imagePullSecrets to all Deployments for gitea registry authentication
- target:
kind: Deployment

View File

@@ -0,0 +1,53 @@
# SigNoz k8s-infra Helm Chart Values - Development Environment
# Collects Kubernetes infrastructure metrics and sends to SigNoz
#
# Official Chart: https://github.com/SigNoz/charts/tree/main/charts/k8s-infra
# Install Command: helm upgrade --install k8s-infra signoz/k8s-infra -n bakery-ia -f k8s-infra-values-dev.yaml
# ============================================================================
# OTEL COLLECTOR ENDPOINT
# ============================================================================
otelCollectorEndpoint: "signoz-otel-collector.bakery-ia.svc.cluster.local:4317"
otelInsecure: true
clusterName: "bakery-ia-dev"
# ============================================================================
# PRESETS - Minimal configuration for development
# ============================================================================
presets:
hostMetrics:
enabled: true
collectionInterval: 60s # Less frequent in dev
kubeletMetrics:
enabled: true
collectionInterval: 60s
kubernetesAttributes:
enabled: true
kubernetesEvents:
enabled: false # Disabled in dev to reduce noise
logsCollection:
enabled: false
# ============================================================================
# OTEL AGENT - Minimal resources for dev
# ============================================================================
otelAgent:
enabled: true
resources:
requests:
memory: "128Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "250m"
otelDeployment:
enabled: false
commonLabels:
app.kubernetes.io/part-of: "signoz"
environment: "development"

View File

@@ -0,0 +1,76 @@
# SigNoz k8s-infra Helm Chart Values - Production Environment
# Collects ALL Kubernetes infrastructure metrics and sends to SigNoz
#
# This chart REPLACES the need for:
# - kube-state-metrics (delete after deploying this)
# - node-exporter (delete after deploying this)
#
# Official Chart: https://github.com/SigNoz/charts/tree/main/charts/k8s-infra
#
# Install Command:
# helm upgrade --install k8s-infra signoz/k8s-infra -n bakery-ia -f k8s-infra-values-prod.yaml
#
# After install, remove redundant exporters:
# helm uninstall kube-state-metrics -n bakery-ia
# helm uninstall node-exporter-prometheus-node-exporter -n bakery-ia
# (or: helm uninstall prometheus -n bakery-ia if installed via prometheus stack)
# ============================================================================
# CONNECTION TO SIGNOZ
# ============================================================================
otelCollectorEndpoint: "signoz-otel-collector.bakery-ia.svc.cluster.local:4317"
otelInsecure: true
clusterName: "bakery-ia-prod"
# ============================================================================
# PRESETS - What metrics to collect
# ============================================================================
presets:
# Host metrics: CPU, memory, disk, filesystem, network, load
# Replaces node-exporter
hostMetrics:
enabled: true
collectionInterval: 30s
# Kubelet metrics: Pod/container CPU, memory usage
# Essential for seeing resource usage per pod in SigNoz
kubeletMetrics:
enabled: true
collectionInterval: 30s
# Kubernetes cluster metrics: deployments, pods, nodes status
# Replaces kube-state-metrics
clusterMetrics:
enabled: true
collectionInterval: 30s
# Enriches all telemetry with k8s metadata (pod name, namespace, etc.)
kubernetesAttributes:
enabled: true
# Kubernetes events (pod scheduled, failed, etc.)
kubernetesEvents:
enabled: true
# Container logs - disabled (apps send logs via OTLP directly)
logsCollection:
enabled: false
# ============================================================================
# OTEL AGENT (DaemonSet) - Runs on each node
# ============================================================================
otelAgent:
enabled: true
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
# ============================================================================
# OTEL DEPLOYMENT - Disabled (using DaemonSet only)
# ============================================================================
otelDeployment:
enabled: false

View File

@@ -3,18 +3,21 @@
# DEPLOYED IN bakery-ia NAMESPACE - Ingress managed by SigNoz Helm chart
#
# Official Chart: https://github.com/SigNoz/charts
# Install Command: helm install signoz signoz/signoz -n bakery-ia -f signoz-values-prod.yaml
# Install Command: helm upgrade --install signoz signoz/signoz -n bakery-ia -f signoz-values-prod.yaml
#
# IMPORTANT: This chart works together with k8s-infra chart for infrastructure monitoring
# Deploy k8s-infra after this: helm upgrade --install k8s-infra signoz/k8s-infra -n bakery-ia -f k8s-infra-values-prod.yaml
#
# MEMORY OPTIMIZATION NOTES:
# - ClickHouse memory increased to 8Gi to prevent OOM errors
# - Retention reduced to 3 days for traces, 7 days for metrics/logs
global:
storageClass: "microk8s-hostpath" # For MicroK8s, use "microk8s-hostpath" or custom storage class
storageClass: "microk8s-hostpath"
clusterName: "bakery-ia-prod"
domain: "monitoring.bakewise.ai"
# Docker Hub credentials - applied to all sub-charts (including Zookeeper, ClickHouse, etc)
# Ingress configuration for SigNoz Frontend
# Configured to use HTTPS with TLS termination at ingress controller
# NOTE: SigNoz Helm chart expects ingress under "signoz.ingress", not "frontend.ingress"
# Reference: https://github.com/SigNoz/charts/blob/main/charts/signoz/values.yaml
signoz:
ingress:
enabled: true
@@ -39,56 +42,50 @@ signoz:
- monitoring.bakewise.ai
secretName: bakery-ia-prod-tls-cert
# Resource configuration for production
# Optimized for 8 CPU core VPS deployment
# ============================================================================
# CLICKHOUSE CONFIGURATION
# Increased memory to 8Gi to prevent OOM errors (was 4Gi, causing code 241 errors)
# ============================================================================
clickhouse:
persistence:
size: 20Gi
resources:
requests:
memory: "2Gi"
cpu: "500m"
limits:
memory: "4Gi"
cpu: "1000m"
otelCollector:
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
# Additional config for Kubernetes infrastructure metrics scraping
config:
receivers:
prometheus:
config:
scrape_configs:
# Kube-state-metrics - Kubernetes object metrics
- job_name: 'kube-state-metrics'
static_configs:
- targets: ['kube-state-metrics.bakery-ia.svc.cluster.local:8080']
scrape_interval: 30s
metric_relabel_configs:
- source_labels: [__name__]
regex: 'kube_(daemonset|deployment|pod|namespace|node|statefulset|replicaset|job|cronjob|persistentvolume|persistentvolumeclaim|resourcequota|service|configmap|secret).*'
action: keep
# Node-exporter - Host-level metrics
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter-prometheus-node-exporter.bakery-ia.svc.cluster.local:9100']
scrape_interval: 30s
metric_relabel_configs:
- source_labels: [__name__]
regex: 'node_(cpu|memory|disk|filesystem|network|load).*'
action: keep
service:
pipelines:
metrics:
receivers: [otlp, prometheus]
memory: "8Gi"
cpu: "2000m"
# Server-level settings only (NOT user-level settings like max_threads)
# User-level settings must go in profiles section
settings:
# Max server memory usage: 80% of container limit (6.4GB of 8GB)
max_server_memory_usage: "6400000000"
# Mark cache size (256MB)
mark_cache_size: "268435456"
# Uncompressed cache (256MB)
uncompressed_cache_size: "268435456"
# Max concurrent queries
max_concurrent_queries: "100"
# User-level settings go in profiles
profiles:
default:
# Max memory per query: 2GB
max_memory_usage: "2000000000"
# Max threads per query
max_threads: "4"
# Background merges memory limit
max_bytes_to_merge_at_max_space_in_pool: "1073741824"
coldStorage:
enabled: false
# ============================================================================
# DATA RETENTION CONFIGURATION
# Reduced retention to minimize storage and memory pressure
# ============================================================================
queryService:
resources:
requests:
@@ -97,7 +94,33 @@ queryService:
limits:
memory: "2Gi"
cpu: "1000m"
# Retention configuration via environment variables
configVars:
# Trace retention: 3 days (72 hours)
SIGNOZ_TRACE_TTL_DURATION_HOURS: "72"
# Logs retention: 7 days (168 hours)
SIGNOZ_LOGS_TTL_DURATION_HOURS: "168"
# Metrics retention: 7 days (168 hours)
SIGNOZ_METRICS_TTL_DURATION_HOURS: "168"
# ============================================================================
# OTEL COLLECTOR CONFIGURATION
# This collector receives data from:
# - Application services (traces, logs, metrics via OTLP)
# - k8s-infra chart (infrastructure metrics)
# ============================================================================
otelCollector:
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
# ============================================================================
# ALERTMANAGER CONFIGURATION
# ============================================================================
alertmanager:
resources:
requests:
@@ -106,3 +129,17 @@ alertmanager:
limits:
memory: "1Gi"
cpu: "500m"
# ============================================================================
# ZOOKEEPER CONFIGURATION
# ============================================================================
zookeeper:
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
persistence:
size: 5Gi

View File

@@ -0,0 +1,81 @@
#!/bin/bash
# Usage: ./check_alert_flow.sh <TENANT_ID>
# Checks the complete alert flow for a demo session in production
# Use microk8s kubectl directly to avoid wrapper issues
KUBECTL="microk8s kubectl"
TENANT_ID="${1:-your-default-tenant-id}"
# Try different secret names for Redis password
REDIS_PASSWORD=$($KUBECTL get secret redis-credentials -n bakery-ia -o jsonpath='{.data.REDIS_PASSWORD}' 2>/dev/null | base64 -d 2>/dev/null)
if [ -z "$REDIS_PASSWORD" ]; then
REDIS_PASSWORD=$($KUBECTL get secret redis-secret -n bakery-ia -o jsonpath='{.data.REDIS_PASSWORD}' 2>/dev/null | base64 -d 2>/dev/null)
fi
if [ -z "$REDIS_PASSWORD" ]; then
# Try to get from configmap or use default
REDIS_PASSWORD="redis_pass123"
fi
echo "=========================================="
echo "Alert Flow Health Check for Tenant: $TENANT_ID"
echo "=========================================="
echo -e "\n=== 1. RabbitMQ Queues ==="
$KUBECTL exec -n bakery-ia deployment/rabbitmq -- \
rabbitmqctl list_queues name messages consumers 2>/dev/null | grep -E "alert|event" || echo " No alert/event queues found or RabbitMQ not accessible"
echo -e "\n=== 2. Alert Processor DB Events (last 10) ==="
$KUBECTL exec -n bakery-ia deployment/alert-processor-db -- \
psql -U alert_processor_user -d alert_processor_db -c \
"SELECT event_type, priority_score, type_class, status, created_at
FROM events WHERE tenant_id = '$TENANT_ID'
ORDER BY created_at DESC LIMIT 10;" 2>/dev/null || echo " Could not query alert-processor-db"
echo -e "\n=== 3. Redis SSE Channels ==="
$KUBECTL exec -n bakery-ia deployment/redis -- \
redis-cli -a "$REDIS_PASSWORD" --no-auth-warning PUBSUB CHANNELS "*" 2>/dev/null | grep -i "$TENANT_ID" || echo " No active SSE channels for this tenant (channels only exist when frontend is connected)"
echo -e "\n=== 4. Demo Session Status ==="
$KUBECTL exec -n bakery-ia deployment/demo-session-db -- \
psql -U demo_session_user -d demo_session_db -c \
"SELECT id, status, virtual_tenant_id, demo_account_type, data_cloned, created_at, cloning_completed_at
FROM demo_sessions
WHERE virtual_tenant_id::text = '$TENANT_ID' OR id::text = '$TENANT_ID'
ORDER BY created_at DESC LIMIT 1;" 2>/dev/null || echo " Could not query demo-session-db"
echo -e "\n=== 5. Recent Alert Processor Logs ==="
$KUBECTL logs -n bakery-ia deployment/alert-processor --tail=100 2>/dev/null | \
grep -iE "received|enriched|stored|error|consuming|rabbitmq|sse" | tail -15 || echo " No relevant logs found"
echo -e "\n=== 6. Gateway SSE Logs ==="
$KUBECTL logs -n bakery-ia deployment/gateway --tail=100 2>/dev/null | \
grep -iE "sse|pubsub|events_stream" | tail -10 || echo " No SSE logs found"
echo -e "\n=== 7. Service Health ==="
# Check deployment status directly
for deploy in alert-processor gateway demo-session-service orchestrator-service inventory-service production-service procurement-service rabbitmq redis; do
READY=$($KUBECTL get deployment/$deploy -n bakery-ia -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
DESIRED=$($KUBECTL get deployment/$deploy -n bakery-ia -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "?")
if [ -z "$READY" ]; then READY="0"; fi
echo " $deploy: $READY/$DESIRED ready"
done
echo -e "\n=== 8. RabbitMQ Connection Status ==="
$KUBECTL exec -n bakery-ia deployment/rabbitmq -- \
rabbitmqctl list_connections name state 2>/dev/null | head -10 || echo " Could not list connections"
echo -e "\n=== 9. Recent Demo Session Service Logs (enrichment) ==="
$KUBECTL logs -n bakery-ia deployment/demo-session-service --tail=100 2>/dev/null | \
grep -iE "enrichment|alert|trigger|clone|post_clone" | tail -10 || echo " No relevant logs found"
echo -e "\n=== 10. Total Events in Alert Processor DB ==="
$KUBECTL exec -n bakery-ia deployment/alert-processor-db -- \
psql -U alert_processor_user -d alert_processor_db -c \
"SELECT COUNT(*) as total_events,
COUNT(*) FILTER (WHERE tenant_id = '$TENANT_ID') as tenant_events
FROM events;" 2>/dev/null || echo " Could not query"
echo -e "\n=========================================="
echo "Health Check Complete"
echo "=========================================="

View File

@@ -0,0 +1,545 @@
# ================================================================
# services/notification/app/api/public_contact.py
# ================================================================
"""
Public contact form API endpoints
Handles contact, feedback, and prelaunch email submissions
These endpoints are public (no authentication required)
"""
import structlog
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, EmailStr, Field
from typing import Optional, Literal
from datetime import datetime
from app.services.email_service import EmailService
from app.core.config import settings
logger = structlog.get_logger()
router = APIRouter(prefix="/api/v1/public", tags=["public-contact"])
# ================================================================
# PYDANTIC MODELS
# ================================================================
class ContactFormRequest(BaseModel):
"""Contact form submission request"""
name: str = Field(..., min_length=2, max_length=100)
email: EmailStr
phone: Optional[str] = Field(None, max_length=20)
bakery_name: Optional[str] = Field(None, max_length=100)
type: Literal["general", "technical", "sales", "feedback"] = "general"
subject: str = Field(..., min_length=5, max_length=200)
message: str = Field(..., min_length=10, max_length=5000)
class FeedbackFormRequest(BaseModel):
"""Feedback form submission request"""
name: str = Field(..., min_length=2, max_length=100)
email: EmailStr
category: Literal["suggestion", "bug", "feature", "praise", "complaint"]
title: str = Field(..., min_length=5, max_length=200)
description: str = Field(..., min_length=10, max_length=5000)
rating: Optional[int] = Field(None, ge=1, le=5)
class PrelaunchEmailRequest(BaseModel):
"""Prelaunch email subscription request"""
email: EmailStr
class ContactFormResponse(BaseModel):
"""Response for form submissions"""
success: bool
message: str
# ================================================================
# EMAIL TEMPLATES
# ================================================================
CONTACT_EMAIL_TEMPLATE_TEXT = """
Nuevo mensaje de contacto recibido
==============================================
DATOS DEL CONTACTO
==============================================
Nombre: {name}
Email: {email}
Teléfono: {phone}
Panadería: {bakery_name}
Tipo de consulta: {type_label}
==============================================
ASUNTO
==============================================
{subject}
==============================================
MENSAJE
==============================================
{message}
==============================================
Recibido: {timestamp}
"""
CONTACT_EMAIL_TEMPLATE_HTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }}
.container {{ max-width: 600px; margin: 0 auto; background-color: #ffffff; }}
.header {{ background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); padding: 30px; text-align: center; }}
.header h1 {{ color: white; margin: 0; font-size: 24px; }}
.content {{ padding: 30px; }}
.info-box {{ background-color: #fef3c7; border-left: 4px solid #f59e0b; padding: 15px; margin: 20px 0; }}
.info-row {{ display: flex; margin-bottom: 10px; }}
.info-label {{ font-weight: bold; color: #92400e; min-width: 120px; }}
.info-value {{ color: #374151; }}
.message-box {{ background-color: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; margin: 20px 0; }}
.message-box h3 {{ color: #1f2937; margin-top: 0; }}
.footer {{ background-color: #f3f4f6; padding: 20px; text-align: center; font-size: 12px; color: #6b7280; }}
.type-badge {{ display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: bold; }}
.type-general {{ background-color: #dbeafe; color: #1e40af; }}
.type-technical {{ background-color: #fce7f3; color: #9d174d; }}
.type-sales {{ background-color: #d1fae5; color: #065f46; }}
.type-feedback {{ background-color: #e0e7ff; color: #3730a3; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Nuevo Mensaje de Contacto</h1>
</div>
<div class="content">
<div class="info-box">
<div class="info-row">
<span class="info-label">Nombre:</span>
<span class="info-value">{name}</span>
</div>
<div class="info-row">
<span class="info-label">Email:</span>
<span class="info-value"><a href="mailto:{email}">{email}</a></span>
</div>
<div class="info-row">
<span class="info-label">Teléfono:</span>
<span class="info-value">{phone}</span>
</div>
<div class="info-row">
<span class="info-label">Panadería:</span>
<span class="info-value">{bakery_name}</span>
</div>
<div class="info-row">
<span class="info-label">Tipo:</span>
<span class="info-value"><span class="type-badge type-{type}">{type_label}</span></span>
</div>
</div>
<div class="message-box">
<h3>{subject}</h3>
<p style="white-space: pre-wrap; color: #374151;">{message}</p>
</div>
</div>
<div class="footer">
Recibido: {timestamp}<br>
BakeWise - Sistema de Contacto
</div>
</div>
</body>
</html>
"""
FEEDBACK_EMAIL_TEMPLATE_TEXT = """
Nuevo feedback recibido
==============================================
DATOS DEL USUARIO
==============================================
Nombre: {name}
Email: {email}
Categoría: {category_label}
Valoración: {rating_display}
==============================================
TÍTULO
==============================================
{title}
==============================================
DESCRIPCIÓN
==============================================
{description}
==============================================
Recibido: {timestamp}
"""
FEEDBACK_EMAIL_TEMPLATE_HTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }}
.container {{ max-width: 600px; margin: 0 auto; background-color: #ffffff; }}
.header {{ background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%); padding: 30px; text-align: center; }}
.header h1 {{ color: white; margin: 0; font-size: 24px; }}
.content {{ padding: 30px; }}
.info-box {{ background-color: #ede9fe; border-left: 4px solid #8b5cf6; padding: 15px; margin: 20px 0; }}
.info-row {{ display: flex; margin-bottom: 10px; }}
.info-label {{ font-weight: bold; color: #5b21b6; min-width: 120px; }}
.info-value {{ color: #374151; }}
.message-box {{ background-color: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; margin: 20px 0; }}
.message-box h3 {{ color: #1f2937; margin-top: 0; }}
.footer {{ background-color: #f3f4f6; padding: 20px; text-align: center; font-size: 12px; color: #6b7280; }}
.category-badge {{ display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: bold; }}
.category-suggestion {{ background-color: #dbeafe; color: #1e40af; }}
.category-bug {{ background-color: #fee2e2; color: #991b1b; }}
.category-feature {{ background-color: #d1fae5; color: #065f46; }}
.category-praise {{ background-color: #fef3c7; color: #92400e; }}
.category-complaint {{ background-color: #fce7f3; color: #9d174d; }}
.stars {{ color: #f59e0b; font-size: 18px; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Nuevo Feedback Recibido</h1>
</div>
<div class="content">
<div class="info-box">
<div class="info-row">
<span class="info-label">Nombre:</span>
<span class="info-value">{name}</span>
</div>
<div class="info-row">
<span class="info-label">Email:</span>
<span class="info-value"><a href="mailto:{email}">{email}</a></span>
</div>
<div class="info-row">
<span class="info-label">Categoría:</span>
<span class="info-value"><span class="category-badge category-{category}">{category_label}</span></span>
</div>
<div class="info-row">
<span class="info-label">Valoración:</span>
<span class="info-value stars">{rating_display}</span>
</div>
</div>
<div class="message-box">
<h3>{title}</h3>
<p style="white-space: pre-wrap; color: #374151;">{description}</p>
</div>
</div>
<div class="footer">
Recibido: {timestamp}<br>
BakeWise - Sistema de Feedback
</div>
</div>
</body>
</html>
"""
PRELAUNCH_EMAIL_TEMPLATE_TEXT = """
Nueva suscripción de pre-lanzamiento
==============================================
NUEVO INTERESADO
==============================================
Email: {email}
El usuario ha mostrado interés en BakeWise y quiere ser notificado cuando se lance oficialmente.
==============================================
Recibido: {timestamp}
"""
PRELAUNCH_EMAIL_TEMPLATE_HTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }}
.container {{ max-width: 600px; margin: 0 auto; background-color: #ffffff; }}
.header {{ background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 30px; text-align: center; }}
.header h1 {{ color: white; margin: 0; font-size: 24px; }}
.content {{ padding: 30px; text-align: center; }}
.email-box {{ background-color: #d1fae5; border: 2px solid #10b981; border-radius: 12px; padding: 20px; margin: 20px 0; }}
.email-box h2 {{ color: #065f46; margin: 0 0 10px 0; }}
.email-box a {{ color: #059669; font-size: 18px; font-weight: bold; }}
.footer {{ background-color: #f3f4f6; padding: 20px; text-align: center; font-size: 12px; color: #6b7280; }}
.rocket {{ font-size: 48px; margin-bottom: 10px; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Nueva Suscripción Pre-Lanzamiento</h1>
</div>
<div class="content">
<div class="rocket">🚀</div>
<p style="color: #374151; font-size: 16px;">Un nuevo usuario quiere ser notificado del lanzamiento de BakeWise</p>
<div class="email-box">
<h2>Email del interesado:</h2>
<a href="mailto:{email}">{email}</a>
</div>
<p style="color: #6b7280;">Añade este email a la lista de espera para el lanzamiento oficial.</p>
</div>
<div class="footer">
Recibido: {timestamp}<br>
BakeWise - Lista de Espera
</div>
</div>
</body>
</html>
"""
# ================================================================
# HELPER FUNCTIONS
# ================================================================
def get_type_label(type_code: str) -> str:
"""Get human-readable label for contact type"""
labels = {
"general": "Consulta General",
"technical": "Soporte Técnico",
"sales": "Ventas",
"feedback": "Feedback"
}
return labels.get(type_code, type_code)
def get_category_label(category: str) -> str:
"""Get human-readable label for feedback category"""
labels = {
"suggestion": "Sugerencia",
"bug": "Error/Bug",
"feature": "Nueva Funcionalidad",
"praise": "Elogio",
"complaint": "Queja"
}
return labels.get(category, category)
def get_rating_display(rating: Optional[int]) -> str:
"""Convert rating to star display"""
if rating is None:
return "N/A"
return "" * rating + "" * (5 - rating)
# ================================================================
# API ENDPOINTS
# ================================================================
@router.post("/contact", response_model=ContactFormResponse)
async def submit_contact_form(request: ContactFormRequest):
"""
Submit a contact form message.
Sends an email to contact@bakewise.ai with the form data.
"""
try:
email_service = EmailService()
timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
type_label = get_type_label(request.type)
# Format email content
text_content = CONTACT_EMAIL_TEMPLATE_TEXT.format(
name=request.name,
email=request.email,
phone=request.phone or "No proporcionado",
bakery_name=request.bakery_name or "No proporcionado",
type=request.type,
type_label=type_label,
subject=request.subject,
message=request.message,
timestamp=timestamp
)
html_content = CONTACT_EMAIL_TEMPLATE_HTML.format(
name=request.name,
email=request.email,
phone=request.phone or "No proporcionado",
bakery_name=request.bakery_name or "No proporcionado",
type=request.type,
type_label=type_label,
subject=request.subject,
message=request.message,
timestamp=timestamp
)
# Send email to contact@bakewise.ai
success = await email_service.send_email(
to_email="contact@bakewise.ai",
subject=f"[{type_label}] {request.subject}",
text_content=text_content,
html_content=html_content,
reply_to=request.email
)
if success:
logger.info("Contact form submitted successfully",
from_email=request.email,
type=request.type,
subject=request.subject)
return ContactFormResponse(
success=True,
message="Mensaje enviado correctamente. Te responderemos pronto."
)
else:
logger.error("Failed to send contact form email",
from_email=request.email)
raise HTTPException(
status_code=500,
detail="Error al enviar el mensaje. Por favor, inténtalo de nuevo."
)
except HTTPException:
raise
except Exception as e:
logger.error("Contact form submission error", error=str(e))
raise HTTPException(
status_code=500,
detail="Error interno. Por favor, inténtalo más tarde."
)
@router.post("/feedback", response_model=ContactFormResponse)
async def submit_feedback_form(request: FeedbackFormRequest):
"""
Submit a feedback form.
Sends an email to contact@bakewise.ai with the feedback data.
"""
try:
email_service = EmailService()
timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
category_label = get_category_label(request.category)
rating_display = get_rating_display(request.rating)
# Format email content
text_content = FEEDBACK_EMAIL_TEMPLATE_TEXT.format(
name=request.name,
email=request.email,
category=request.category,
category_label=category_label,
rating_display=rating_display,
title=request.title,
description=request.description,
timestamp=timestamp
)
html_content = FEEDBACK_EMAIL_TEMPLATE_HTML.format(
name=request.name,
email=request.email,
category=request.category,
category_label=category_label,
rating_display=rating_display,
title=request.title,
description=request.description,
timestamp=timestamp
)
# Send email to contact@bakewise.ai
success = await email_service.send_email(
to_email="contact@bakewise.ai",
subject=f"[Feedback - {category_label}] {request.title}",
text_content=text_content,
html_content=html_content,
reply_to=request.email
)
if success:
logger.info("Feedback form submitted successfully",
from_email=request.email,
category=request.category,
title=request.title)
return ContactFormResponse(
success=True,
message="Gracias por tu feedback. Lo revisaremos pronto."
)
else:
logger.error("Failed to send feedback form email",
from_email=request.email)
raise HTTPException(
status_code=500,
detail="Error al enviar el feedback. Por favor, inténtalo de nuevo."
)
except HTTPException:
raise
except Exception as e:
logger.error("Feedback form submission error", error=str(e))
raise HTTPException(
status_code=500,
detail="Error interno. Por favor, inténtalo más tarde."
)
@router.post("/prelaunch-subscribe", response_model=ContactFormResponse)
async def submit_prelaunch_email(request: PrelaunchEmailRequest):
"""
Submit a prelaunch email subscription.
Sends a notification email to contact@bakewise.ai.
"""
try:
email_service = EmailService()
timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
# Format email content
text_content = PRELAUNCH_EMAIL_TEMPLATE_TEXT.format(
email=request.email,
timestamp=timestamp
)
html_content = PRELAUNCH_EMAIL_TEMPLATE_HTML.format(
email=request.email,
timestamp=timestamp
)
# Send email to contact@bakewise.ai
success = await email_service.send_email(
to_email="contact@bakewise.ai",
subject=f"[Pre-Lanzamiento] Nueva suscripción: {request.email}",
text_content=text_content,
html_content=html_content,
reply_to=request.email
)
if success:
logger.info("Prelaunch subscription submitted successfully",
email=request.email)
return ContactFormResponse(
success=True,
message="Te hemos apuntado a la lista de espera."
)
else:
logger.error("Failed to send prelaunch subscription email",
email=request.email)
raise HTTPException(
status_code=500,
detail="Error al registrar tu email. Por favor, inténtalo de nuevo."
)
except HTTPException:
raise
except Exception as e:
logger.error("Prelaunch subscription error", error=str(e))
raise HTTPException(
status_code=500,
detail="Error interno. Por favor, inténtalo más tarde."
)

View File

@@ -15,6 +15,7 @@ from app.api.notification_operations import router as notification_operations_ro
from app.api.analytics import router as analytics_router
from app.api.audit import router as audit_router
from app.api.whatsapp_webhooks import router as whatsapp_webhooks_router
from app.api.public_contact import router as public_contact_router
from app.services.sse_service import SSEService
from app.services.notification_orchestrator import NotificationOrchestrator
from app.services.email_service import EmailService
@@ -306,6 +307,7 @@ service.setup_custom_endpoints()
# where {notification_id} would match literal paths like "audit-logs"
service.add_router(audit_router, tags=["audit-logs"])
service.add_router(whatsapp_webhooks_router, tags=["whatsapp-webhooks"])
service.add_router(public_contact_router, tags=["public-contact"])
service.add_router(notification_operations_router, tags=["notification-operations"])
service.add_router(analytics_router, tags=["notifications-analytics"])
service.add_router(notification_router, tags=["notifications"])

View File

@@ -480,10 +480,30 @@ async def trigger_failure_notifications(notification_service: any, tenant_id: UU
}
html_content = template.render(**template_vars)
text_content = f"Equipment failure alert: {equipment.name} - {failure_data.get('failureType', 'Unknown')}"
# Send via notification service (which will handle the actual email sending)
# This is a simplified approach - in production you'd want to get manager emails from DB
logger.info("Failure notifications triggered (template rendered)",
# Send via notification service API
if notification_service:
result = await notification_service.send_email(
tenant_id=str(tenant_id),
to_email="managers@bakeryia.com", # Should be configured from DB in production
subject=f"🚨 Fallo de Equipo: {equipment.name}",
message=text_content,
html_content=html_content,
priority="high"
)
if result:
logger.info("Failure notifications sent via notification service",
equipment_id=str(equipment.id),
tenant_id=str(tenant_id),
notification_id=result.get('notification_id'))
else:
logger.error("Failed to send failure notifications via notification service",
equipment_id=str(equipment.id),
tenant_id=str(tenant_id))
else:
logger.warning("Notification service not available, failure notifications not sent",
equipment_id=str(equipment.id),
tenant_id=str(tenant_id))
@@ -523,9 +543,30 @@ async def trigger_repair_notifications(notification_service: any, tenant_id: UUI
}
html_content = template.render(**template_vars)
text_content = f"Equipment repair completed: {equipment.name} - {repair_data.get('repairDescription', 'Repair completed')}"
# Send via notification service
logger.info("Repair notifications triggered (template rendered)",
# Send via notification service API
if notification_service:
result = await notification_service.send_email(
tenant_id=str(tenant_id),
to_email="managers@bakeryia.com", # Should be configured from DB in production
subject=f"✅ Equipo Reparado: {equipment.name}",
message=text_content,
html_content=html_content,
priority="normal"
)
if result:
logger.info("Repair notifications sent via notification service",
equipment_id=str(equipment.id),
tenant_id=str(tenant_id),
notification_id=result.get('notification_id'))
else:
logger.error("Failed to send repair notifications via notification service",
equipment_id=str(equipment.id),
tenant_id=str(tenant_id))
else:
logger.warning("Notification service not available, repair notifications not sent",
equipment_id=str(equipment.id),
tenant_id=str(tenant_id))
@@ -565,14 +606,35 @@ async def send_support_contact_notification(notification_service: any, tenant_id
}
html_content = template.render(**template_vars)
text_content = f"Equipment failure alert: {equipment.name} - {failure_data.get('failureType', 'Unknown')}"
# TODO: Actually send email via notification service
# For now, just log that we would send to the support email
logger.info("Support contact notification prepared (would send to support)",
# Send via notification service API
if notification_service and support_email:
result = await notification_service.send_email(
tenant_id=str(tenant_id),
to_email=support_email,
subject=f"🚨 URGENTE: Fallo de Equipo - {equipment.name}",
message=text_content,
html_content=html_content,
priority="high"
)
if result:
logger.info("Support contact notification sent via notification service",
equipment_id=str(equipment.id),
tenant_id=str(tenant_id),
support_email=support_email,
subject=f"🚨 URGENTE: Fallo de Equipo - {equipment.name}")
notification_id=result.get('notification_id'))
else:
logger.error("Failed to send support contact notification via notification service",
equipment_id=str(equipment.id),
tenant_id=str(tenant_id),
support_email=support_email)
else:
logger.warning("Notification service not available or no support email provided",
equipment_id=str(equipment.id),
tenant_id=str(tenant_id),
support_email=support_email)
except Exception as e:
logger.error("Error sending support contact notification",

View File

@@ -22,9 +22,10 @@ class AlertEventConsumer:
Handles email and Slack notifications for critical alerts
"""
def __init__(self, db_session: AsyncSession):
def __init__(self, db_session: AsyncSession, notification_client: Optional[any] = None):
self.db_session = db_session
self.notification_config = self._load_notification_config()
self.notification_client = notification_client
def _load_notification_config(self) -> Dict[str, Any]:
"""
@@ -451,7 +452,7 @@ class AlertEventConsumer:
data: Dict[str, Any]
) -> bool:
"""
Send email notification
Send email notification using notification service API
Args:
tenant_id: Tenant ID
@@ -466,6 +467,41 @@ class AlertEventConsumer:
logger.debug("Email notifications disabled")
return False
# Use notification service client if available
if self.notification_client:
# Build email content
subject = self._format_email_subject(notification_type, data)
body = self._format_email_body(notification_type, data)
# Send via notification service API
result = await self.notification_client.send_email(
tenant_id=tenant_id,
to_email=", ".join(self.notification_config['email']['recipients']),
subject=subject,
message="Supplier alert notification", # Plain text fallback
html_content=body,
priority="high" if data.get('severity') == 'critical' else "normal"
)
if result:
logger.info(
"Email notification sent via notification service",
tenant_id=tenant_id,
notification_type=notification_type,
recipients=len(self.notification_config['email']['recipients'])
)
return True
else:
logger.error(
"Notification service failed to send email",
tenant_id=tenant_id,
notification_type=notification_type
)
return False
else:
# Fallback to direct SMTP for backward compatibility
logger.warning("Notification client not available, falling back to direct SMTP")
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
@@ -496,7 +532,7 @@ class AlertEventConsumer:
server.send_message(msg)
logger.info(
"Email notification sent",
"Email notification sent via direct SMTP (fallback)",
tenant_id=tenant_id,
notification_type=notification_type,
recipients=len(self.notification_config['email']['recipients'])
@@ -785,6 +821,6 @@ class AlertEventConsumer:
# Factory function for creating consumer instance
def create_alert_event_consumer(db_session: AsyncSession) -> AlertEventConsumer:
def create_alert_event_consumer(db_session: AsyncSession, notification_client: Optional[any] = None) -> AlertEventConsumer:
"""Create alert event consumer instance"""
return AlertEventConsumer(db_session)
return AlertEventConsumer(db_session, notification_client)