Compare commits

..

44 Commits

Author SHA1 Message Date
Kelly
754a46c56f chore: trigger CI rebuild
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-16 09:19:52 -07:00
Kelly
e450d2e99e fix(ci): use local registry mirror instead of mirror.gcr.io
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Switch Kaniko registry-mirror from mirror.gcr.io to 10.100.9.70:5000
to pull base images from local registry instead of GCR.
2025-12-16 09:09:15 -07:00
Kelly
205a8b3159 chore: retry CI for visibility-events fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-16 08:56:59 -07:00
Kelly
8bd29d11bb fix: Use correct column names in visibility-events query
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Changed name -> name_raw and brand -> brand_name_raw to match
store_products table schema.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 02:21:01 -07:00
Kelly
4e7b3d2336 fix: Update DATABASE_URL to point to primary PostgreSQL server
Changed from 10.100.6.50 (secondary/replica in read-only mode) to
10.100.7.50 (primary) to fix read-only transaction errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 01:13:49 -07:00
Kelly
849123693a fix(ci): Use unquoted heredoc for kubeconfig token injection
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Changed heredoc from 'KUBEEOF' (quoted) to KUBEEOF (unquoted)
- This allows shell variable expansion of $K8S_TOKEN directly
- Removed sed replacement step that was failing due to YAML escaping issues
2025-12-15 21:55:52 -07:00
Kelly
a1227f77b9 chore: retry CI
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 21:53:00 -07:00
Kelly
415e89a012 chore: retry CI with k8s_token secret
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 21:26:06 -07:00
Kelly
45844c6281 ci: Embed kubeconfig, use k8s_token secret for token only 2025-12-15 21:19:26 -07:00
Kelly
24c9586d81 ci: Skip base64 - use raw kubeconfig in secret
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 21:09:54 -07:00
Kelly
f8d61446d5 chore: retry CI with correct kubeconfig
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 20:57:19 -07:00
Kelly
0f859d1c75 chore: retry CI after kubeconfig fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 20:43:40 -07:00
Kelly
52dc669782 ci: Remove clone/volume config (requires admin trust)
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Woodpecker doesn't allow custom clone or volumes without elevated trust.
Kaniko layer caching (--cache-repo) still works (registry-based).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 20:16:18 -07:00
Kelly
2e47996354 ci: Add shallow git clone (depth: 1)
Only fetch latest commit instead of full history.
Reduces checkout time and bandwidth.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 20:05:33 -07:00
Kelly
f25d4eaf27 ci: Add npm and Docker layer caching
- PR steps: shared npm-cache volume for faster npm ci
- Docker builds: --cache-repo to local registry for layer caching
- Kaniko will reuse npm install layer when package.json unchanged

First build populates cache, subsequent builds much faster.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 20:04:15 -07:00
Kelly
61a6be888c ci: Consolidate back to 4 docker steps
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Remove separate build steps (didn't save time)
- Use original multi-stage Dockerfiles
- Delete unused Dockerfile.ci files
- 4 parallel docker builds + deploy

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 20:01:11 -07:00
Kelly
09c2b3a0e1 ci: Use node:22 instead of node:22-alpine for builds
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Alpine uses musl libc which breaks Rollup's native bindings.
Debian-based node:22 uses glibc and works correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 19:56:48 -07:00
Kelly
cec34198c7 ci: Add slim Dockerfile.ci files for faster CI builds
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Add Dockerfile.ci for backend, cannaiq, findadispo, findagram
- Frontend Dockerfiles just copy pre-built assets to nginx
- Backend Dockerfile copies pre-built dist/node_modules
- Reduces Docker build time by doing npm ci/build in CI step

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 19:43:08 -07:00
Kelly
3c10e07e45 feat(ci): Push built images to local registry for faster K8s pulls
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Build images push to 10.100.9.70:5000/cannaiq/*
- Deploy pulls from local registry (no external network)
- Removed git.spdy.io registry auth (not needed for local)
- Added --insecure-registry for HTTP local registry
2025-12-15 19:16:16 -07:00
Kelly
3582c2e9e2 fix(k8s): Use external Postgres/Redis/MinIO services
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Update secrets.yaml with correct MinIO credentials
- Add Redis connection details
- Remove postgres.yaml (use external 10.100.6.50)
- Remove redis.yaml (use external 10.100.9.50)
2025-12-15 19:03:05 -07:00
Kelly
c6874977ee docs: Add spdy.io infrastructure credentials
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 18:59:18 -07:00
Kelly
68430f5c22 fix(ci): Use mirror.gcr.io as registry mirror for Kaniko
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 18:52:02 -07:00
Kelly
ccefd325aa fix(ci): Use hardcoded Woodpecker workspace path for Kaniko
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 18:49:56 -07:00
Kelly
e119c5af53 chore: trigger CI
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 18:44:24 -07:00
Kelly
e61224aaed fix(ci): Use CI_WORKSPACE for Kaniko context paths
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 18:42:33 -07:00
Kelly
7cf1b7643f feat(ci): Use local registry 10.100.9.70:5000 for base images
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 18:28:20 -07:00
Kelly
74f813d68f feat(ci): Switch to Kaniko for Docker builds (no daemon, better DNS)
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 18:20:53 -07:00
Kelly
f38f1024de fix(docker): Use mirror.gcr.io in all Dockerfiles to avoid rate limits
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 18:19:18 -07:00
Kelly
358099c58a chore: trigger CI
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 18:12:41 -07:00
Kelly
7fdcfc4fc4 fix(ci): Use mirror.gcr.io to avoid Docker Hub rate limits
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 18:11:16 -07:00
Kelly
541b461283 fix(ci): Use public node:20 image for typecheck steps
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 18:08:46 -07:00
Kelly
8f25cf10ab chore: retry CI
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 17:06:42 -07:00
Kelly
79e434212f chore: retry CI
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 16:45:05 -07:00
Kelly
600172eff6 chore: retry CI
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is running
2025-12-15 15:51:40 -07:00
Kelly
4c12763fa1 chore: retry CI
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 13:18:53 -07:00
Kelly
2cb9a093f4 chore: retry CI
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 12:29:45 -07:00
Kelly
15ab40a820 chore: trigger CI build
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 12:14:14 -07:00
Kelly
2708fbe319 feat(brands): Add calculated tags with configurable thresholds
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Tags assigned per store:
- must_win: High-revenue store with room to grow SKUs
- at_risk: High OOS% (losing shelf presence)
- top_performer: High sales + good inventory management
- growth: Above-average velocity
- low_inventory: Low days on hand

Configurable via query params:
- ?must_win_max_skus=5
- ?at_risk_oos_pct=30
- ?top_performer_max_oos=15
- ?low_inventory_days=7

Response includes tag_thresholds showing applied values.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 12:06:44 -07:00
Kelly
231d49e3e8 feat(brands): Add margin estimation to stores/performance endpoint
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Add ?margin_pct query param (default 50% industry standard)
- Returns margin_pct and margin_est per store
- Includes margin_pct_assumed in response metadata

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 12:02:36 -07:00
Kelly
17defa046c feat(api): Add /api/brands/:brand/stores/performance endpoint
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Add comprehensive per-store performance endpoint for Cannabrands integration.
Returns all metrics in one call for easy merging with internal order data.

Response includes per store:
- active_skus, oos_skus, total_skus, oos_pct
- avg_daily_units (velocity from inventory deltas)
- avg_days_on_hand (stock / daily velocity)
- total_sales_est (units × price × days)
- lost_opportunity (OOS days × velocity × price)
- categories breakdown (JSON object)
- avg_price, total_stock

Query params: ?days=28&state=AZ&limit=100&offset=0

Matches Hoodie Analytics columns for Order Management view.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 11:57:38 -07:00
Kelly
d76a5fb3c5 feat(api): Add brand analytics API endpoints
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Add comprehensive brand-level analytics endpoints at /api/brands:

Brand Discovery:
- GET /api/brands - List all brands with summary metrics
- GET /api/brands/search - Search brands by name
- GET /api/brands/top - Top brands by distribution

Brand Overview:
- GET /api/brands/:brand - Full brand intelligence dashboard
- GET /api/brands/:brand/analytics - Alias for overview

Sales & Velocity:
- GET /api/brands/:brand/sales - Sales data (4wk, daily avg)
- GET /api/brands/:brand/velocity - Units/day by SKU
- GET /api/brands/:brand/trends - Weekly sales trends

Inventory & Stock:
- GET /api/brands/:brand/inventory - Current stock levels
- GET /api/brands/:brand/oos - Out-of-stock products
- GET /api/brands/:brand/low-stock - Products below threshold

Pricing:
- GET /api/brands/:brand/pricing - Current prices
- GET /api/brands/:brand/price-history - Price changes over time

Distribution:
- GET /api/brands/:brand/distribution - Store count, market coverage
- GET /api/brands/:brand/stores - Stores carrying brand
- GET /api/brands/:brand/gaps - Whitespace opportunities

Events & Alerts:
- GET /api/brands/:brand/events - Visibility events
- POST /api/brands/:brand/events/:id/ack - Acknowledge alert

Products:
- GET /api/brands/:brand/products - All SKUs with metrics
- GET /api/brands/:brand/products/:sku - Single product deep dive

All endpoints support ?state=XX, ?days=N, and ?category=X filters.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 11:06:23 -07:00
Kelly
f19fc59583 chore: retry CI
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is running
2025-12-15 09:59:11 -07:00
Kelly
4c183c87a9 chore: retry CI after registry fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 09:35:05 -07:00
Kelly
ffa05f89c4 chore: trigger CI on develop
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-15 09:06:43 -07:00
12 changed files with 1508 additions and 229 deletions

View File

@@ -3,7 +3,7 @@ steps:
# PR VALIDATION: Parallel type checks (PRs only) # PR VALIDATION: Parallel type checks (PRs only)
# =========================================== # ===========================================
typecheck-backend: typecheck-backend:
image: git.spdy.io/creationshop/node:20 image: node:22
commands: commands:
- cd backend - cd backend
- npm ci --prefer-offline - npm ci --prefer-offline
@@ -13,7 +13,7 @@ steps:
event: pull_request event: pull_request
typecheck-cannaiq: typecheck-cannaiq:
image: git.spdy.io/creationshop/node:20 image: node:22
commands: commands:
- cd cannaiq - cd cannaiq
- npm ci --prefer-offline - npm ci --prefer-offline
@@ -23,7 +23,7 @@ steps:
event: pull_request event: pull_request
typecheck-findadispo: typecheck-findadispo:
image: git.spdy.io/creationshop/node:20 image: node:22
commands: commands:
- cd findadispo/frontend - cd findadispo/frontend
- npm ci --prefer-offline - npm ci --prefer-offline
@@ -33,7 +33,7 @@ steps:
event: pull_request event: pull_request
typecheck-findagram: typecheck-findagram:
image: git.spdy.io/creationshop/node:20 image: node:22
commands: commands:
- cd findagram/frontend - cd findagram/frontend
- npm ci --prefer-offline - npm ci --prefer-offline
@@ -68,113 +68,122 @@ steps:
event: pull_request event: pull_request
# =========================================== # ===========================================
# MASTER DEPLOY: Parallel Docker builds # DOCKER: Multi-stage builds with layer caching
# NOTE: cache_from/cache_to removed due to plugin bug splitting on commas
# =========================================== # ===========================================
docker-backend: docker-backend:
image: plugins/docker image: gcr.io/kaniko-project/executor:debug
settings: commands:
registry: git.spdy.io - /kaniko/executor
repo: git.spdy.io/creationshop/cannaiq --context=/woodpecker/src/git.spdy.io/Creationshop/cannaiq/backend
tags: --dockerfile=/woodpecker/src/git.spdy.io/Creationshop/cannaiq/backend/Dockerfile
- latest --destination=10.100.9.70:5000/cannaiq/backend:latest
- sha-${CI_COMMIT_SHA:0:8} --destination=10.100.9.70:5000/cannaiq/backend:sha-${CI_COMMIT_SHA:0:8}
dockerfile: backend/Dockerfile --build-arg=APP_BUILD_VERSION=sha-${CI_COMMIT_SHA:0:8}
context: backend --build-arg=APP_GIT_SHA=${CI_COMMIT_SHA}
username: --build-arg=APP_BUILD_TIME=${CI_PIPELINE_CREATED}
from_secret: registry_username --registry-mirror=10.100.9.70:5000
password: --insecure-registry=10.100.9.70:5000
from_secret: registry_password --cache=true
build_args: --cache-repo=10.100.9.70:5000/cannaiq/cache-backend
- APP_BUILD_VERSION=sha-${CI_COMMIT_SHA:0:8} --cache-ttl=168h
- APP_GIT_SHA=${CI_COMMIT_SHA}
- APP_BUILD_TIME=${CI_PIPELINE_CREATED}
- CONTAINER_IMAGE_TAG=sha-${CI_COMMIT_SHA:0:8}
depends_on: [] depends_on: []
when: when:
branch: [master, develop] branch: [master, develop]
event: push event: push
docker-cannaiq: docker-cannaiq:
image: plugins/docker image: gcr.io/kaniko-project/executor:debug
settings: commands:
registry: git.spdy.io - /kaniko/executor
repo: git.spdy.io/creationshop/cannaiq-frontend --context=/woodpecker/src/git.spdy.io/Creationshop/cannaiq/cannaiq
tags: --dockerfile=/woodpecker/src/git.spdy.io/Creationshop/cannaiq/cannaiq/Dockerfile
- latest --destination=10.100.9.70:5000/cannaiq/frontend:latest
- sha-${CI_COMMIT_SHA:0:8} --destination=10.100.9.70:5000/cannaiq/frontend:sha-${CI_COMMIT_SHA:0:8}
dockerfile: cannaiq/Dockerfile --registry-mirror=10.100.9.70:5000
context: cannaiq --insecure-registry=10.100.9.70:5000
username: --cache=true
from_secret: registry_username --cache-repo=10.100.9.70:5000/cannaiq/cache-cannaiq
password: --cache-ttl=168h
from_secret: registry_password
depends_on: [] depends_on: []
when: when:
branch: [master, develop] branch: [master, develop]
event: push event: push
docker-findadispo: docker-findadispo:
image: plugins/docker image: gcr.io/kaniko-project/executor:debug
settings: commands:
registry: git.spdy.io - /kaniko/executor
repo: git.spdy.io/creationshop/findadispo-frontend --context=/woodpecker/src/git.spdy.io/Creationshop/cannaiq/findadispo/frontend
tags: --dockerfile=/woodpecker/src/git.spdy.io/Creationshop/cannaiq/findadispo/frontend/Dockerfile
- latest --destination=10.100.9.70:5000/cannaiq/findadispo:latest
- sha-${CI_COMMIT_SHA:0:8} --destination=10.100.9.70:5000/cannaiq/findadispo:sha-${CI_COMMIT_SHA:0:8}
dockerfile: findadispo/frontend/Dockerfile --registry-mirror=10.100.9.70:5000
context: findadispo/frontend --insecure-registry=10.100.9.70:5000
username: --cache=true
from_secret: registry_username --cache-repo=10.100.9.70:5000/cannaiq/cache-findadispo
password: --cache-ttl=168h
from_secret: registry_password
depends_on: [] depends_on: []
when: when:
branch: [master, develop] branch: [master, develop]
event: push event: push
docker-findagram: docker-findagram:
image: plugins/docker image: gcr.io/kaniko-project/executor:debug
settings: commands:
registry: git.spdy.io - /kaniko/executor
repo: git.spdy.io/creationshop/findagram-frontend --context=/woodpecker/src/git.spdy.io/Creationshop/cannaiq/findagram/frontend
tags: --dockerfile=/woodpecker/src/git.spdy.io/Creationshop/cannaiq/findagram/frontend/Dockerfile
- latest --destination=10.100.9.70:5000/cannaiq/findagram:latest
- sha-${CI_COMMIT_SHA:0:8} --destination=10.100.9.70:5000/cannaiq/findagram:sha-${CI_COMMIT_SHA:0:8}
dockerfile: findagram/frontend/Dockerfile --registry-mirror=10.100.9.70:5000
context: findagram/frontend --insecure-registry=10.100.9.70:5000
username: --cache=true
from_secret: registry_username --cache-repo=10.100.9.70:5000/cannaiq/cache-findagram
password: --cache-ttl=168h
from_secret: registry_password
depends_on: [] depends_on: []
when: when:
branch: [master, develop] branch: [master, develop]
event: push event: push
# =========================================== # ===========================================
# STAGE 3: Deploy and Run Migrations # DEPLOY: Pull from local registry
# =========================================== # ===========================================
deploy: deploy:
image: bitnami/kubectl:latest image: bitnami/kubectl:latest
environment: environment:
KUBECONFIG_CONTENT: K8S_TOKEN:
from_secret: kubeconfig_data from_secret: k8s_token
commands: commands:
- mkdir -p ~/.kube - mkdir -p ~/.kube
- echo "$KUBECONFIG_CONTENT" | tr -d '[:space:]' | base64 -d > ~/.kube/config - |
cat > ~/.kube/config << KUBEEOF
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkakNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTmpVM05UUTNPRE13SGhjTk1qVXhNakUwTWpNeU5qSXpXaGNOTXpVeE1qRXlNak15TmpJegpXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTmpVM05UUTNPRE13V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFRWDRNdFJRTW5lWVJVV0s2cjZ3VEV2WjAxNnV4T3NUR3JJZ013TXVnNGwKajQ1bHZ6ZkM1WE1NY1pESnUxZ0t1dVJhVGxlb0xVOVJnSERIUUI4TUwzNTJvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVXIzNDZpNE42TFhzaEZsREhvSlU0CjJ1RjZseGN3Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnVUtqdWRFQWJyS1JDVHROVXZTc1Rmb3FEaHFSeDM5MkYKTFFSVWlKK0hCVElDSUJqOFIxbG1zSnFSRkRHMEpwMGN4OG5ZZnFCaElRQzh6WWdRdTdBZmR4L3IKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
server: https://10.100.6.10:6443
name: spdy-k3s
contexts:
- context:
cluster: spdy-k3s
namespace: cannaiq
user: cannaiq-admin
name: cannaiq
current-context: cannaiq
users:
- name: cannaiq-admin
user:
token: $K8S_TOKEN
KUBEEOF
- chmod 600 ~/.kube/config - chmod 600 ~/.kube/config
# Deploy backend first - kubectl set image deployment/scraper scraper=10.100.9.70:5000/cannaiq/backend:sha-${CI_COMMIT_SHA:0:8} -n cannaiq
- kubectl set image deployment/scraper scraper=git.spdy.io/creationshop/cannaiq:sha-${CI_COMMIT_SHA:0:8} -n cannaiq
- kubectl rollout status deployment/scraper -n cannaiq --timeout=300s - kubectl rollout status deployment/scraper -n cannaiq --timeout=300s
# Note: Migrations run automatically at startup via auto-migrate - REPLICAS=$(kubectl get deployment scraper-worker -n cannaiq -o jsonpath='{.spec.replicas}'); if [ "$REPLICAS" = "0" ]; then kubectl scale deployment/scraper-worker --replicas=5 -n cannaiq; fi
# Deploy remaining services - kubectl set image deployment/scraper-worker worker=10.100.9.70:5000/cannaiq/backend:sha-${CI_COMMIT_SHA:0:8} -n cannaiq
# Resilience: ensure workers are scaled up if at 0 - kubectl set image deployment/cannaiq-frontend cannaiq-frontend=10.100.9.70:5000/cannaiq/frontend:sha-${CI_COMMIT_SHA:0:8} -n cannaiq
- REPLICAS=$(kubectl get deployment scraper-worker -n cannaiq -o jsonpath='{.spec.replicas}'); if [ "$REPLICAS" = "0" ]; then echo "Scaling workers from 0 to 5"; kubectl scale deployment/scraper-worker --replicas=5 -n cannaiq; fi - kubectl set image deployment/findadispo-frontend findadispo-frontend=10.100.9.70:5000/cannaiq/findadispo:sha-${CI_COMMIT_SHA:0:8} -n cannaiq
- kubectl set image deployment/scraper-worker worker=git.spdy.io/creationshop/cannaiq:sha-${CI_COMMIT_SHA:0:8} -n cannaiq - kubectl set image deployment/findagram-frontend findagram-frontend=10.100.9.70:5000/cannaiq/findagram:sha-${CI_COMMIT_SHA:0:8} -n cannaiq
- kubectl set image deployment/cannaiq-frontend cannaiq-frontend=git.spdy.io/creationshop/cannaiq-frontend:sha-${CI_COMMIT_SHA:0:8} -n cannaiq
- kubectl set image deployment/findadispo-frontend findadispo-frontend=git.spdy.io/creationshop/findadispo-frontend:sha-${CI_COMMIT_SHA:0:8} -n cannaiq
- kubectl set image deployment/findagram-frontend findagram-frontend=git.spdy.io/creationshop/findagram-frontend:sha-${CI_COMMIT_SHA:0:8} -n cannaiq
- kubectl rollout status deployment/cannaiq-frontend -n cannaiq --timeout=120s - kubectl rollout status deployment/cannaiq-frontend -n cannaiq --timeout=120s
depends_on: depends_on:
- docker-backend - docker-backend

View File

@@ -1,6 +1,6 @@
# Build stage # Build stage
# Image: git.spdy.io/creationshop/dispensary-scraper # Image: git.spdy.io/creationshop/dispensary-scraper
FROM node:20-slim AS builder FROM node:22-slim AS builder
# Install build tools for native modules (bcrypt, sharp) # Install build tools for native modules (bcrypt, sharp)
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
@@ -27,7 +27,7 @@ RUN npm run build
RUN npm prune --production RUN npm prune --production
# Production stage # Production stage
FROM node:20-slim FROM node:22-slim
# Build arguments for version info # Build arguments for version info
ARG APP_BUILD_VERSION=dev ARG APP_BUILD_VERSION=dev

View File

@@ -105,6 +105,7 @@ import { createSystemRouter, createPrometheusRouter } from './system/routes';
import { createPortalRoutes } from './portals'; import { createPortalRoutes } from './portals';
import { createStatesRouter } from './routes/states'; import { createStatesRouter } from './routes/states';
import { createAnalyticsV2Router } from './routes/analytics-v2'; import { createAnalyticsV2Router } from './routes/analytics-v2';
import { createBrandsRouter } from './routes/brands';
import { createDiscoveryRoutes } from './discovery'; import { createDiscoveryRoutes } from './discovery';
import pipelineRoutes from './routes/pipeline'; import pipelineRoutes from './routes/pipeline';
@@ -229,6 +230,15 @@ try {
console.warn('[AnalyticsV2] Failed to register routes:', error); console.warn('[AnalyticsV2] Failed to register routes:', error);
} }
// Brand Analytics API - Hoodie Analytics-style market intelligence
try {
const brandsRouter = createBrandsRouter(getPool());
app.use('/api/brands', brandsRouter);
console.log('[Brands] Routes registered at /api/brands');
} catch (error) {
console.warn('[Brands] Failed to register routes:', error);
}
// Public API v1 - External consumer endpoints (WordPress, etc.) // Public API v1 - External consumer endpoints (WordPress, etc.)
// Uses dutchie_az data pipeline with per-dispensary API key auth // Uses dutchie_az data pipeline with per-dispensary API key auth
app.use('/api/v1', publicApiRoutes); app.use('/api/v1', publicApiRoutes);

1281
backend/src/routes/brands.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -110,8 +110,8 @@ export async function detectVisibilityEvents(
` `
SELECT SELECT
provider_product_id as id, provider_product_id as id,
name, name_raw as name,
brand, brand_name_raw as brand,
price_rec as price price_rec as price
FROM store_products FROM store_products
WHERE dispensary_id = $1 WHERE dispensary_id = $1

View File

@@ -1,5 +1,5 @@
# Build stage # Build stage
FROM node:20-slim AS builder FROM node:22-slim AS builder
WORKDIR /app WORKDIR /app

104
docs/SPDY_INFRASTRUCTURE.md Normal file
View File

@@ -0,0 +1,104 @@
# CannaIQ Infrastructure (spdy.io)
External services for the spdy.io Kubernetes cluster. **Do not create containers for these.**
## PostgreSQL
| Setting | Value |
|----------|----------------------|
| Host | 10.100.6.50 |
| Port | 5432 |
| Database | cannaiq |
| Username | cannaiq |
| Password | SpDyCannaIQ2024 |
```bash
# Connection string
DATABASE_URL=postgres://cannaiq:SpDyCannaIQ2024@10.100.6.50:5432/cannaiq
# Test connection
PGPASSWORD='SpDyCannaIQ2024' psql -h 10.100.6.50 -p 5432 -U cannaiq -d cannaiq -c "SELECT 1"
```
## Redis
| Setting | Value |
|----------|----------------|
| Host | 10.100.9.50 |
| Port | 6379 |
| Password | SpDyR3d1s2024! |
```bash
# Connection URL
REDIS_URL=redis://:SpDyR3d1s2024!@10.100.9.50:6379
# Node.js .env
REDIS_HOST=10.100.9.50
REDIS_PORT=6379
REDIS_PASSWORD=SpDyR3d1s2024!
```
## MinIO (S3-Compatible Storage)
| Setting | Value |
|----------------|------------------|
| Endpoint | 10.100.9.80:9000 |
| Console | 10.100.9.80:9001 |
| Region | us-east-1 |
| Use Path Style | true |
### CannaIQ Bucket
| Setting | Value |
|------------|----------------|
| Bucket | cannaiq |
| Access Key | cannaiq-app |
| Secret Key | cannaiq-secret |
```bash
# Node.js .env
MINIO_ENDPOINT=10.100.9.80
MINIO_PORT=9000
MINIO_ACCESS_KEY=cannaiq-app
MINIO_SECRET_KEY=cannaiq-secret
MINIO_BUCKET=cannaiq
MINIO_USE_SSL=false
```
### Cannabrands Bucket
| Setting | Value |
|------------|------------------------------------------|
| Bucket | cannabrands |
| Access Key | cannabrands-app |
| Secret Key | cdbdcd0c7b6f3994d4ab09f68eaff98665df234f |
## Kubernetes Secrets
Create secrets in the `cannaiq` namespace:
```bash
# Database
kubectl create secret generic db-credentials -n cannaiq \
--from-literal=DATABASE_URL='postgres://cannaiq:SpDyCannaIQ2024@10.100.6.50:5432/cannaiq'
# Redis
kubectl create secret generic redis-credentials -n cannaiq \
--from-literal=REDIS_URL='redis://:SpDyR3d1s2024!@10.100.9.50:6379'
# MinIO
kubectl create secret generic minio-credentials -n cannaiq \
--from-literal=MINIO_ACCESS_KEY='cannaiq-app' \
--from-literal=MINIO_SECRET_KEY='cannaiq-secret'
```
## Network
All services are on the `10.100.x.x` internal network:
| Service | IP | Port |
|------------|--------------|------|
| PostgreSQL | 10.100.6.50 | 5432 |
| Redis | 10.100.9.50 | 6379 |
| MinIO | 10.100.9.80 | 9000 |
| Registry | 10.100.9.70 | 5000 |

View File

@@ -1,5 +1,5 @@
# Build stage # Build stage
FROM node:20-slim AS builder FROM node:22-slim AS builder
WORKDIR /app WORKDIR /app

View File

@@ -1,5 +1,5 @@
# Build stage # Build stage
FROM node:20-slim AS builder FROM node:22-slim AS builder
WORKDIR /app WORKDIR /app

View File

@@ -1,76 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: cannaiq
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: cannaiq
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: scraper-secrets
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: scraper-secrets
key: POSTGRES_PASSWORD
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: scraper-secrets
key: POSTGRES_DB
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: cannaiq
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432

View File

@@ -1,66 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redis-data
namespace: cannaiq
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: cannaiq
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "200m"
volumeMounts:
- name: redis-data
mountPath: /data
command:
- redis-server
- --appendonly
- "yes"
- --maxmemory
- "200mb"
- --maxmemory-policy
- allkeys-lru
volumes:
- name: redis-data
persistentVolumeClaim:
claimName: redis-data
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: cannaiq
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379

View File

@@ -5,12 +5,29 @@ metadata:
namespace: cannaiq namespace: cannaiq
type: Opaque type: Opaque
stringData: stringData:
# PostgreSQL (external: 10.100.7.50 - primary)
POSTGRES_USER: "cannaiq" POSTGRES_USER: "cannaiq"
POSTGRES_PASSWORD: "SpDyCannaIQ2024" POSTGRES_PASSWORD: "SpDyCannaIQ2024"
POSTGRES_DB: "cannaiq" POSTGRES_DB: "cannaiq"
DATABASE_URL: "postgresql://cannaiq:SpDyCannaIQ2024@10.100.6.50:5432/cannaiq" DATABASE_URL: "postgresql://cannaiq:SpDyCannaIQ2024@10.100.7.50:5432/cannaiq"
JWT_SECRET: "aW7vN3xKpM9qLsT2fB5jDc8hR4wY6zXe"
# Redis (external: 10.100.9.50)
REDIS_HOST: "10.100.9.50"
REDIS_PORT: "6379"
REDIS_PASSWORD: "SpDyR3d1s2024!"
REDIS_URL: "redis://:SpDyR3d1s2024!@10.100.9.50:6379"
# MinIO (external: 10.100.9.80)
MINIO_ENDPOINT: "10.100.9.80"
MINIO_PORT: "9000"
MINIO_ACCESS_KEY: "cannaiq-app" MINIO_ACCESS_KEY: "cannaiq-app"
MINIO_SECRET_KEY: "62a37268f2fe4163ef46fe1c29ad93f817b415fc" MINIO_SECRET_KEY: "cannaiq-secret"
MINIO_BUCKET: "cannaiq"
MINIO_USE_SSL: "false"
# Auth
JWT_SECRET: "aW7vN3xKpM9qLsT2fB5jDc8hR4wY6zXe"
# Evomi Proxy
EVOMI_USER: "kl8" EVOMI_USER: "kl8"
EVOMI_PASS: "ogh9U1Xe7Gzxzozo4rmP" EVOMI_PASS: "ogh9U1Xe7Gzxzozo4rmP"