Fix some issues
This commit is contained in:
@@ -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
|
||||
|
||||
42
frontend/package-lock.json
generated
42
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
83
frontend/src/api/services/publicContact.ts
Normal file
83
frontend/src/api/services/publicContact.ts
Normal 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;
|
||||
185
frontend/src/components/domain/auth/PrelaunchEmailForm.tsx
Normal file
185
frontend/src/components/domain/auth/PrelaunchEmailForm.tsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
export type {
|
||||
LoginFormProps,
|
||||
RegistrationContainerProps,
|
||||
RegistrationContainerProps,
|
||||
PasswordResetFormProps,
|
||||
ProfileSettingsProps
|
||||
ProfileSettingsProps,
|
||||
PrelaunchEmailFormProps
|
||||
} from './types';
|
||||
|
||||
// Component metadata for documentation
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
17
frontend/src/config/prelaunch.ts
Normal file
17
frontend/src/config/prelaunch.ts
Normal 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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
AlertCircle,
|
||||
HelpCircle
|
||||
} from 'lucide-react';
|
||||
import { publicContactService } from '../../api/services/publicContact';
|
||||
|
||||
interface ContactMethod {
|
||||
id: string;
|
||||
@@ -73,25 +74,35 @@ 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);
|
||||
|
||||
setSubmitStatus('success');
|
||||
setTimeout(() => {
|
||||
setSubmitStatus('idle');
|
||||
setFormState({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
bakeryName: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
type: 'general',
|
||||
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,
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
setSubmitStatus('success');
|
||||
setTimeout(() => {
|
||||
setSubmitStatus('idle');
|
||||
setFormState({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
bakeryName: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
type: 'general',
|
||||
});
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
console.error('Contact form submission error:', error);
|
||||
setSubmitStatus('error');
|
||||
setTimeout(() => setSubmitStatus('idle'), 5000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
AlertCircle,
|
||||
Star
|
||||
} from 'lucide-react';
|
||||
import { publicContactService } from '../../api/services/publicContact';
|
||||
|
||||
interface FeedbackCategory {
|
||||
id: string;
|
||||
@@ -90,24 +91,33 @@ 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);
|
||||
|
||||
setSubmitStatus('success');
|
||||
setTimeout(() => {
|
||||
setSubmitStatus('idle');
|
||||
setFormState({
|
||||
name: '',
|
||||
email: '',
|
||||
category: 'suggestion',
|
||||
title: '',
|
||||
description: '',
|
||||
rating: 0,
|
||||
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,
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
setSubmitStatus('success');
|
||||
setTimeout(() => {
|
||||
setSubmitStatus('idle');
|
||||
setFormState({
|
||||
name: '',
|
||||
email: '',
|
||||
category: 'suggestion',
|
||||
title: '',
|
||||
description: '',
|
||||
rating: 0,
|
||||
});
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
console.error('Feedback form submission error:', error);
|
||||
setSubmitStatus('error');
|
||||
setTimeout(() => setSubmitStatus('idle'), 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryColor = (color: string) => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
1
frontend/src/vite-env.d.ts
vendored
1
frontend/src/vite-env.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
56
gateway/app/routes/public.py
Normal file
56
gateway/app/routes/public.py
Normal 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")
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
53
infrastructure/monitoring/signoz/k8s-infra-values-dev.yaml
Normal file
53
infrastructure/monitoring/signoz/k8s-infra-values-dev.yaml
Normal 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"
|
||||
76
infrastructure/monitoring/signoz/k8s-infra-values-prod.yaml
Normal file
76
infrastructure/monitoring/signoz/k8s-infra-values-prod.yaml
Normal 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
|
||||
@@ -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
|
||||
|
||||
81
infrastructure/testing/check_alert_flow.sh
Normal file
81
infrastructure/testing/check_alert_flow.sh
Normal 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 "=========================================="
|
||||
545
services/notification/app/api/public_contact.py
Normal file
545
services/notification/app/api/public_contact.py
Normal 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."
|
||||
)
|
||||
@@ -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"])
|
||||
|
||||
@@ -480,12 +480,32 @@ 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)",
|
||||
equipment_id=str(equipment.id),
|
||||
tenant_id=str(tenant_id))
|
||||
# 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))
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error triggering failure notifications",
|
||||
@@ -523,11 +543,32 @@ 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)",
|
||||
equipment_id=str(equipment.id),
|
||||
tenant_id=str(tenant_id))
|
||||
# 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))
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error triggering repair notifications",
|
||||
@@ -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)",
|
||||
equipment_id=str(equipment.id),
|
||||
tenant_id=str(tenant_id),
|
||||
support_email=support_email,
|
||||
subject=f"🚨 URGENTE: Fallo de Equipo - {equipment.name}")
|
||||
# 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,
|
||||
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",
|
||||
|
||||
@@ -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,42 +467,77 @@ class AlertEventConsumer:
|
||||
logger.debug("Email notifications disabled")
|
||||
return False
|
||||
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
# 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)
|
||||
|
||||
# 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"
|
||||
)
|
||||
|
||||
# Create message
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = self.notification_config['email']['from_address']
|
||||
msg['To'] = ', '.join(self.notification_config['email']['recipients'])
|
||||
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
|
||||
|
||||
# Attach HTML body
|
||||
html_part = MIMEText(body, 'html')
|
||||
msg.attach(html_part)
|
||||
# Build email content
|
||||
subject = self._format_email_subject(notification_type, data)
|
||||
body = self._format_email_body(notification_type, data)
|
||||
|
||||
# Send email
|
||||
smtp_config = self.notification_config['email']
|
||||
with smtplib.SMTP(smtp_config['smtp_host'], smtp_config['smtp_port']) as server:
|
||||
if smtp_config['use_tls']:
|
||||
server.starttls()
|
||||
# Create message
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = self.notification_config['email']['from_address']
|
||||
msg['To'] = ', '.join(self.notification_config['email']['recipients'])
|
||||
|
||||
if smtp_config['smtp_username'] and smtp_config['smtp_password']:
|
||||
server.login(smtp_config['smtp_username'], smtp_config['smtp_password'])
|
||||
# Attach HTML body
|
||||
html_part = MIMEText(body, 'html')
|
||||
msg.attach(html_part)
|
||||
|
||||
server.send_message(msg)
|
||||
# Send email
|
||||
smtp_config = self.notification_config['email']
|
||||
with smtplib.SMTP(smtp_config['smtp_host'], smtp_config['smtp_port']) as server:
|
||||
if smtp_config['use_tls']:
|
||||
server.starttls()
|
||||
|
||||
logger.info(
|
||||
"Email notification sent",
|
||||
tenant_id=tenant_id,
|
||||
notification_type=notification_type,
|
||||
recipients=len(self.notification_config['email']['recipients'])
|
||||
)
|
||||
return True
|
||||
if smtp_config['smtp_username'] and smtp_config['smtp_password']:
|
||||
server.login(smtp_config['smtp_username'], smtp_config['smtp_password'])
|
||||
|
||||
server.send_message(msg)
|
||||
|
||||
logger.info(
|
||||
"Email notification sent via direct SMTP (fallback)",
|
||||
tenant_id=tenant_id,
|
||||
notification_type=notification_type,
|
||||
recipients=len(self.notification_config['email']['recipients'])
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user