feat: Oracle Canvas Component Schema and Qwen 3.6 integration (#31)

Co-authored-by: Sagnik <sagnik7896@gmail.com>
Reviewed-on: sagnik/Project_Velocity#31
This commit is contained in:
2026-04-20 01:43:39 +05:30
parent 57144e1bd3
commit e519339cc9
129 changed files with 625213 additions and 262 deletions

View File

@@ -26,6 +26,10 @@ Key files:
- `desineuron-ingress-home-ip-sync.service`: systemd oneshot service for the IP sync
- `desineuron-ingress-home-ip-sync.timer`: persistent timer that reruns the sync every 5 minutes and on boot
- `install_linux_ingress_ip_sync.sh`: Linux-side installer for the IP sync service
- `deploy_velocity_site.sh`: canonical manual, timer, and webhook deploy entrypoint on the Linux origin
- `gitea_velocity_webhook_receiver.py`: authenticated Gitea push-hook receiver on Linux origin
- `desineuron-velocity-gitea-webhook.service`: systemd service for the webhook receiver
- `install_linux_velocity_webhook.sh`: targeted installer for the webhook receiver
Manual Cloudflare work still required unless API credentials are provided:
- set the six hostnames to DNS-only
@@ -36,3 +40,20 @@ Dynamic home IP handling:
- `rathole` control port `2333/tcp` is intentionally open on the ingress so public services do not break when the ISP IP changes
- SSH fallback on the ingress remains restricted to the current home public IP on `22/tcp`
- the Linux-side IP sync service keeps that SSH fallback rule current after ISP churn or reboot
Project Velocity deploy triggers:
- Manual:
- `sudo systemctl start desineuron-velocity-site-update.service`
- or `sudo /usr/local/bin/deploy_velocity_site.sh`
- Timer:
- `desineuron-velocity-site-update.timer`
- Webhook:
- `https://velocity.desineuron.in/hooks/gitea/project-velocity`
- secret is stored in `/etc/desineuron-velocity-webhook.env`
- only `push` events for `refs/heads/main` on `sagnik/Project_Velocity` trigger deploy
Webhook design:
- receiver binds `127.0.0.1:8788` only
- nginx proxies the public HTTPS hook path to the local receiver
- signature is verified with `X-Gitea-Signature`
- deploy execution is serialized with `flock` so overlapping pushes cannot race each other

View File

@@ -0,0 +1,21 @@
server {
listen 443 ssl http2;
server_name api.desineuron.in;
ssl_certificate /etc/letsencrypt/live/desineuron-infra/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/desineuron-infra/privkey.pem;
access_log /var/log/nginx/api.desineuron.in.access.log;
error_log /var/log/nginx/api.desineuron.in.error.log;
location / {
proxy_pass http://127.0.0.1:8001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -12,6 +12,12 @@ CURRENT_DIR="${CURRENT_DIR:-$SERVE_ROOT/current}"
STATE_DIR="${STATE_DIR:-$APP_ROOT/state}"
REVISION_FILE="${REVISION_FILE:-$STATE_DIR/current_revision.txt}"
PERSISTENT_VIDEO_DIR="${PERSISTENT_VIDEO_DIR:-$APP_ROOT/shared/videos}"
BACKEND_SERVICE="${BACKEND_SERVICE:-desineuron-velocity-backend}"
BACKEND_HEALTH_URL="${BACKEND_HEALTH_URL:-http://127.0.0.1:8001/health}"
BACKEND_HEALTH_TIMEOUT_S="${BACKEND_HEALTH_TIMEOUT_S:-60}"
RUN_BACKEND_RESTART="${RUN_BACKEND_RESTART:-1}"
RUN_DB_MIGRATIONS="${RUN_DB_MIGRATIONS:-0}"
DB_MIGRATION_CMD="${DB_MIGRATION_CMD:-}"
mkdir -p "$APP_ROOT" "$STATE_DIR" "$SERVE_ROOT" "$PERSISTENT_VIDEO_DIR"
@@ -25,11 +31,28 @@ if ! command -v npm >/dev/null 2>&1; then
exit 1
fi
if ! command -v curl >/dev/null 2>&1; then
echo "curl is required" >&2
exit 1
fi
if [ ! -d "$REPO_DIR/.git" ]; then
git clone "$REPO_URL" "$REPO_DIR"
fi
git -C "$REPO_DIR" fetch --all --prune
REMOTE_REVISION="$(git -C "$REPO_DIR" rev-parse "origin/$BRANCH")"
CURRENT_REVISION=""
if [ -f "$REVISION_FILE" ]; then
CURRENT_REVISION="$(tr -d '\r\n' < "$REVISION_FILE")"
fi
if [ -n "$CURRENT_REVISION" ] && [ "$CURRENT_REVISION" = "$REMOTE_REVISION" ]; then
date -u +"%Y-%m-%dT%H:%M:%SZ" > "$STATE_DIR/last_check_utc.txt"
echo "No new revision on origin/$BRANCH. Current revision: $CURRENT_REVISION"
exit 0
fi
git -C "$REPO_DIR" checkout "$BRANCH"
git -C "$REPO_DIR" reset --hard "origin/$BRANCH"
@@ -51,7 +74,30 @@ if [ -d "$PERSISTENT_VIDEO_DIR" ] && [ "$(find "$PERSISTENT_VIDEO_DIR" -maxdepth
cp -a "$PERSISTENT_VIDEO_DIR"/. "$CURRENT_DIR/videos"/
fi
git -C "$REPO_DIR" rev-parse HEAD > "$REVISION_FILE"
if [ "$RUN_DB_MIGRATIONS" = "1" ] && [ -n "$DB_MIGRATION_CMD" ]; then
echo "Running DB migration command..."
bash -lc "$DB_MIGRATION_CMD"
fi
if [ "$RUN_BACKEND_RESTART" = "1" ]; then
echo "Restarting backend service: $BACKEND_SERVICE"
systemctl restart "$BACKEND_SERVICE"
fi
echo "Waiting for backend health: $BACKEND_HEALTH_URL"
deadline=$(( $(date +%s) + BACKEND_HEALTH_TIMEOUT_S ))
until curl -fsS "$BACKEND_HEALTH_URL" >/dev/null 2>&1; do
if [ "$(date +%s)" -ge "$deadline" ]; then
echo "Backend health check failed for $BACKEND_HEALTH_URL" >&2
if command -v journalctl >/dev/null 2>&1; then
journalctl -u "$BACKEND_SERVICE" -n 80 --no-pager || true
fi
exit 1
fi
sleep 2
done
printf '%s\n' "$REMOTE_REVISION" > "$REVISION_FILE"
date -u +"%Y-%m-%dT%H:%M:%SZ" > "$STATE_DIR/last_deploy_utc.txt"
echo "Deployed revision $(cat "$REVISION_FILE") to $CURRENT_DIR"

View File

@@ -0,0 +1,15 @@
[Unit]
Description=Project Velocity Gitea webhook receiver
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
EnvironmentFile=-/etc/desineuron-velocity-webhook.env
ExecStart=/usr/bin/python3 /usr/local/bin/gitea_velocity_webhook_receiver.py
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target

View File

@@ -2,8 +2,8 @@
Description=Periodically refresh Project Velocity site from Gitea
[Timer]
OnBootSec=2min
OnUnitActiveSec=2min
OnBootSec=60s
OnUnitActiveSec=60s
Unit=desineuron-velocity-site-update.service
Persistent=true

View File

@@ -0,0 +1,122 @@
#!/usr/bin/env python3
import hashlib
import hmac
import json
import os
import subprocess
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
HOST = os.getenv("WEBHOOK_BIND_HOST", "127.0.0.1")
PORT = int(os.getenv("WEBHOOK_BIND_PORT", "8788"))
PATH = os.getenv("WEBHOOK_PATH", "/hooks/gitea/project-velocity")
SECRET = os.getenv("GITEA_WEBHOOK_SECRET", "")
EXPECTED_REF = os.getenv("GITEA_EXPECTED_REF", "refs/heads/main")
EXPECTED_REPO = os.getenv("GITEA_REPO_FULL_NAME", "sagnik/Project_Velocity")
LOCK_FILE = os.getenv("DEPLOY_LOCK_FILE", "/tmp/desineuron-velocity-deploy.lock")
DEPLOY_CMD = os.getenv("DEPLOY_COMMAND", "/usr/local/bin/deploy_velocity_site.sh")
def verify_signature(secret: str, body: bytes, signature: str) -> bool:
if not secret:
return False
digest = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(digest, signature or "")
class Handler(BaseHTTPRequestHandler):
server_version = "VelocityWebhook/1.0"
def _respond(self, code: int, payload: dict) -> None:
body = json.dumps(payload).encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, fmt: str, *args) -> None:
print("%s - - [%s] %s" % (self.address_string(), self.log_date_time_string(), fmt % args))
def do_GET(self) -> None:
if self.path == "/health":
self._respond(200, {"ok": True, "service": "desineuron-velocity-gitea-webhook"})
return
self._respond(404, {"error": "not_found"})
def do_POST(self) -> None:
if self.path != PATH:
self._respond(404, {"error": "not_found"})
return
length = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(length)
signature = self.headers.get("X-Gitea-Signature", "")
event = self.headers.get("X-Gitea-Event", "")
if not verify_signature(SECRET, body, signature):
self._respond(401, {"error": "invalid_signature"})
return
try:
payload = json.loads(body.decode("utf-8"))
except json.JSONDecodeError:
self._respond(400, {"error": "invalid_json"})
return
if event == "ping":
self._respond(200, {"ok": True, "message": "ping_received"})
return
if event != "push":
self._respond(202, {"ok": True, "ignored": True, "reason": f"unsupported_event:{event}"})
return
ref = payload.get("ref")
repo_full_name = ((payload.get("repository") or {}).get("full_name")) or ""
if ref != EXPECTED_REF:
self._respond(202, {"ok": True, "ignored": True, "reason": f"unexpected_ref:{ref}"})
return
if EXPECTED_REPO and repo_full_name != EXPECTED_REPO:
self._respond(202, {"ok": True, "ignored": True, "reason": f"unexpected_repo:{repo_full_name}"})
return
cmd = ["/usr/bin/flock", "-n", LOCK_FILE, "/bin/bash", "-lc", DEPLOY_CMD]
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode == 0:
self._respond(
202,
{
"ok": True,
"deployed": True,
"ref": ref,
"repository": repo_full_name,
"stdout_tail": proc.stdout.strip().splitlines()[-10:],
},
)
return
self._respond(
500,
{
"ok": False,
"deployed": False,
"ref": ref,
"repository": repo_full_name,
"returncode": proc.returncode,
"stdout_tail": proc.stdout.strip().splitlines()[-10:],
"stderr_tail": proc.stderr.strip().splitlines()[-10:],
},
)
def main() -> None:
server = ThreadingHTTPServer((HOST, PORT), Handler)
print(f"Velocity Gitea webhook listening on http://{HOST}:{PORT}{PATH}")
server.serve_forever()
if __name__ == "__main__":
main()

View File

@@ -6,6 +6,9 @@ TIMER_FILE=/etc/systemd/system/desineuron-velocity-site-update.timer
ENV_FILE=/etc/desineuron-velocity-site.env
SCRIPT_PATH=/usr/local/bin/deploy_velocity_site.sh
NGINX_PATH=/etc/nginx/conf.d/velocity.desineuron.in.conf
WEBHOOK_SERVICE_FILE=/etc/systemd/system/desineuron-velocity-gitea-webhook.service
WEBHOOK_SCRIPT_PATH=/usr/local/bin/gitea_velocity_webhook_receiver.py
WEBHOOK_ENV_FILE=/etc/desineuron-velocity-webhook.env
sudo apt-get update
sudo apt-get install -y git curl rsync nginx
@@ -16,8 +19,10 @@ if ! command -v node >/dev/null 2>&1; then
fi
sudo install -m 0755 /tmp/desineuron_ingress/deploy_velocity_site.sh "$SCRIPT_PATH"
sudo install -m 0755 /tmp/desineuron_ingress/gitea_velocity_webhook_receiver.py "$WEBHOOK_SCRIPT_PATH"
sudo install -m 0644 /tmp/desineuron_ingress/desineuron-velocity-site-update.service "$SERVICE_FILE"
sudo install -m 0644 /tmp/desineuron_ingress/desineuron-velocity-site-update.timer "$TIMER_FILE"
sudo install -m 0644 /tmp/desineuron_ingress/desineuron-velocity-gitea-webhook.service "$WEBHOOK_SERVICE_FILE"
sudo install -m 0644 /tmp/desineuron_ingress/velocity.desineuron.in.nginx.conf "$NGINX_PATH"
sudo tee "$ENV_FILE" >/dev/null <<'EOF'
@@ -31,13 +36,32 @@ SERVE_ROOT=/var/www/velocity.desineuron.in
CURRENT_DIR=/var/www/velocity.desineuron.in/current
STATE_DIR=/opt/desineuron-velocity-site/state
REVISION_FILE=/opt/desineuron-velocity-site/state/current_revision.txt
BACKEND_SERVICE=desineuron-velocity-backend
BACKEND_HEALTH_URL=http://127.0.0.1:8001/health
BACKEND_HEALTH_TIMEOUT_S=60
RUN_BACKEND_RESTART=1
RUN_DB_MIGRATIONS=0
EOF
sudo chmod 0640 "$ENV_FILE"
if [ ! -f "$WEBHOOK_ENV_FILE" ]; then
sudo tee "$WEBHOOK_ENV_FILE" >/dev/null <<'EOF'
WEBHOOK_BIND_HOST=127.0.0.1
WEBHOOK_BIND_PORT=8788
WEBHOOK_PATH=/hooks/gitea/project-velocity
GITEA_EXPECTED_REF=refs/heads/main
GITEA_REPO_FULL_NAME=sagnik/Project_Velocity
DEPLOY_LOCK_FILE=/tmp/desineuron-velocity-deploy.lock
DEPLOY_COMMAND=/usr/local/bin/deploy_velocity_site.sh
GITEA_WEBHOOK_SECRET=replace-me
EOF
fi
sudo chmod 0600 "$WEBHOOK_ENV_FILE"
sudo mkdir -p /var/www/velocity.desineuron.in /opt/desineuron-velocity-site/state
sudo nginx -t
sudo systemctl daemon-reload
sudo systemctl enable --now desineuron-velocity-site-update.timer
sudo systemctl enable --now desineuron-velocity-gitea-webhook.service
sudo systemctl start desineuron-velocity-site-update.service
sudo systemctl reload nginx
sudo systemctl --no-pager --full status desineuron-velocity-site-update.service desineuron-velocity-site-update.timer
sudo systemctl --no-pager --full status desineuron-velocity-site-update.service desineuron-velocity-site-update.timer desineuron-velocity-gitea-webhook.service

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
SERVICE_FILE=/etc/systemd/system/desineuron-velocity-gitea-webhook.service
SCRIPT_PATH=/usr/local/bin/gitea_velocity_webhook_receiver.py
ENV_FILE=/etc/desineuron-velocity-webhook.env
NGINX_PATH=/etc/nginx/conf.d/velocity.desineuron.in.conf
sudo apt-get update
sudo apt-get install -y python3 nginx
sudo install -m 0755 /tmp/desineuron_ingress/gitea_velocity_webhook_receiver.py "$SCRIPT_PATH"
sudo install -m 0644 /tmp/desineuron_ingress/desineuron-velocity-gitea-webhook.service "$SERVICE_FILE"
sudo install -m 0644 /tmp/desineuron_ingress/velocity.desineuron.in.nginx.conf "$NGINX_PATH"
if [ ! -f "$ENV_FILE" ]; then
sudo tee "$ENV_FILE" >/dev/null <<'EOF'
WEBHOOK_BIND_HOST=127.0.0.1
WEBHOOK_BIND_PORT=8788
WEBHOOK_PATH=/hooks/gitea/project-velocity
GITEA_EXPECTED_REF=refs/heads/main
GITEA_REPO_FULL_NAME=sagnik/Project_Velocity
DEPLOY_LOCK_FILE=/tmp/desineuron-velocity-deploy.lock
DEPLOY_COMMAND=/usr/local/bin/deploy_velocity_site.sh
GITEA_WEBHOOK_SECRET=replace-me
EOF
sudo chmod 0600 "$ENV_FILE"
fi
sudo nginx -t
sudo systemctl daemon-reload
sudo systemctl enable --now desineuron-velocity-gitea-webhook.service
sudo systemctl reload nginx
sudo systemctl --no-pager --full status desineuron-velocity-gitea-webhook.service

View File

@@ -14,4 +14,13 @@ server {
location / {
try_files $uri $uri/ /index.html;
}
location = /hooks/gitea/project-velocity {
proxy_pass http://127.0.0.1:8788/hooks/gitea/project-velocity;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
}
}