Merge branch 'v2' of https://gitea.bnkops.com/admin/changemaker.lite into v2
This commit is contained in:
commit
576dea2f98
@ -184,6 +184,11 @@ export function SchedulingPollWidget({ pollSlug, showComments = true, title }: S
|
|||||||
setSubmitMsg({ type: 'error', text: 'Please vote on at least one option' });
|
setSubmitMsg({ type: 'error', text: 'Please vote on at least one option' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const trimmedEmail = voterEmail.trim();
|
||||||
|
if (trimmedEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmedEmail)) {
|
||||||
|
setSubmitMsg({ type: 'error', text: 'Please enter a valid email address, or leave the email field blank' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setSubmitMsg(null);
|
setSubmitMsg(null);
|
||||||
@ -191,7 +196,7 @@ export function SchedulingPollWidget({ pollSlug, showComments = true, title }: S
|
|||||||
const storedToken = localStorage.getItem(VOTER_TOKEN_KEY + pollSlug);
|
const storedToken = localStorage.getItem(VOTER_TOKEN_KEY + pollSlug);
|
||||||
const { data } = await axios.post(`${apiBase}/meeting-planner/public/${pollSlug}/vote`, {
|
const { data } = await axios.post(`${apiBase}/meeting-planner/public/${pollSlug}/vote`, {
|
||||||
voterName: voterName.trim(),
|
voterName: voterName.trim(),
|
||||||
voterEmail: voterEmail.trim() || undefined,
|
voterEmail: trimmedEmail || undefined,
|
||||||
voterToken: storedToken || undefined,
|
voterToken: storedToken || undefined,
|
||||||
votes: Object.entries(votes).map(([optionId, value]) => ({ optionId, value })),
|
votes: Object.entries(votes).map(([optionId, value]) => ({ optionId, value })),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -114,13 +114,18 @@ export default function SchedulingPollPage() {
|
|||||||
message.warning('Please vote on at least one option');
|
message.warning('Please vote on at least one option');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const trimmedEmail = voterEmail.trim();
|
||||||
|
if (trimmedEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmedEmail)) {
|
||||||
|
message.warning('Please enter a valid email address, or leave the email field blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const storedToken = localStorage.getItem(VOTER_TOKEN_KEY + slug);
|
const storedToken = localStorage.getItem(VOTER_TOKEN_KEY + slug);
|
||||||
const { data } = await axios.post(`/api/meeting-planner/public/${slug}/vote`, {
|
const { data } = await axios.post(`/api/meeting-planner/public/${slug}/vote`, {
|
||||||
voterName: voterName.trim(),
|
voterName: voterName.trim(),
|
||||||
voterEmail: voterEmail.trim() || undefined,
|
voterEmail: trimmedEmail || undefined,
|
||||||
voterToken: storedToken || undefined,
|
voterToken: storedToken || undefined,
|
||||||
votes: Object.entries(votes).map(([optionId, value]) => ({ optionId, value })),
|
votes: Object.entries(votes).map(([optionId, value]) => ({ optionId, value })),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -37,7 +37,10 @@ export const addOptionsSchema = z.object({
|
|||||||
|
|
||||||
export const submitVotesSchema = z.object({
|
export const submitVotesSchema = z.object({
|
||||||
voterName: z.string().min(1, 'Name is required').max(100),
|
voterName: z.string().min(1, 'Name is required').max(100),
|
||||||
voterEmail: z.string().email().max(200).optional(),
|
voterEmail: z.preprocess(
|
||||||
|
(val) => (typeof val === 'string' && val.trim() === '' ? undefined : val),
|
||||||
|
z.string().email('Please enter a valid email address').max(200).optional(),
|
||||||
|
),
|
||||||
voterToken: z.string().optional(),
|
voterToken: z.string().optional(),
|
||||||
votes: z.array(z.object({
|
votes: z.array(z.object({
|
||||||
optionId: z.string().min(1),
|
optionId: z.string().min(1),
|
||||||
|
|||||||
28
config.sh
28
config.sh
@ -559,6 +559,33 @@ configure_cors() {
|
|||||||
success "CORS origins set for $domain"
|
success "CORS origins set for $domain"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generate_nginx_configs() {
|
||||||
|
header "Nginx Configuration"
|
||||||
|
|
||||||
|
local domain="${CONFIGURED_DOMAIN:-cmlite.org}"
|
||||||
|
local template_dir="$SCRIPT_DIR/nginx/conf.d"
|
||||||
|
local templates_found=0
|
||||||
|
|
||||||
|
for template in "$template_dir"/*.conf.template; do
|
||||||
|
[[ -f "$template" ]] || continue
|
||||||
|
templates_found=$((templates_found + 1))
|
||||||
|
local conf_file="${template%.template}"
|
||||||
|
local conf_name
|
||||||
|
conf_name=$(basename "$conf_file")
|
||||||
|
|
||||||
|
# Substitute ${DOMAIN} while preserving nginx variables ($host, $scheme, etc.)
|
||||||
|
sed "s/\${DOMAIN}/$domain/g" "$template" > "$conf_file"
|
||||||
|
success "Generated $conf_name"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $templates_found -eq 0 ]]; then
|
||||||
|
warn "No nginx .conf.template files found — skipping"
|
||||||
|
else
|
||||||
|
success "Generated $templates_found nginx configs for domain: $domain"
|
||||||
|
info "Restart nginx to apply: docker compose restart nginx"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Homepage services.yaml
|
# Homepage services.yaml
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@ -1053,6 +1080,7 @@ main() {
|
|||||||
configure_features
|
configure_features
|
||||||
configure_pangolin
|
configure_pangolin
|
||||||
configure_cors
|
configure_cors
|
||||||
|
generate_nginx_configs
|
||||||
generate_services_yaml
|
generate_services_yaml
|
||||||
fix_container_permissions
|
fix_container_permissions
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name api.cmlite.org api.betteredmonton.org;
|
server_name api.cmlite.org api.betteredmonton.org api.pridecorner.ca;
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
|
||||||
# Media API endpoints (must come BEFORE / for longest prefix match)
|
# Media API endpoints (must come BEFORE / for longest prefix match)
|
||||||
|
|||||||
@ -40,7 +40,7 @@ server {
|
|||||||
# Grafana — allows iframe embedding from admin (app.cmlite.org)
|
# Grafana — allows iframe embedding from admin (app.cmlite.org)
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name grafana.cmlite.org grafana.betteredmonton.org;
|
server_name grafana.cmlite.org grafana.betteredmonton.org grafana.pridecorner.ca;
|
||||||
add_header Content-Security-Policy "frame-ancestors 'self' app.cmlite.org app.betteredmonton.org" always;
|
add_header Content-Security-Policy "frame-ancestors 'self' app.cmlite.org app.betteredmonton.org" always;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@ -59,7 +59,7 @@ server {
|
|||||||
# NocoDB (data browser) — allows iframe embedding from admin
|
# NocoDB (data browser) — allows iframe embedding from admin
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name db.cmlite.org db.betteredmonton.org;
|
server_name db.cmlite.org db.betteredmonton.org db.pridecorner.ca;
|
||||||
add_header Content-Security-Policy "frame-ancestors 'self' app.cmlite.org app.betteredmonton.org" always;
|
add_header Content-Security-Policy "frame-ancestors 'self' app.cmlite.org app.betteredmonton.org" always;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@ -76,7 +76,7 @@ server {
|
|||||||
# Listmonk
|
# Listmonk
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name listmonk.cmlite.org listmonk.betteredmonton.org;
|
server_name listmonk.cmlite.org listmonk.betteredmonton.org listmonk.pridecorner.ca;
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@ -92,7 +92,7 @@ server {
|
|||||||
# MkDocs — allows iframe embedding from admin
|
# MkDocs — allows iframe embedding from admin
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name docs.cmlite.org docs.betteredmonton.org;
|
server_name docs.cmlite.org docs.betteredmonton.org docs.pridecorner.ca;
|
||||||
add_header Content-Security-Policy "frame-ancestors 'self' app.cmlite.org app.betteredmonton.org" always;
|
add_header Content-Security-Policy "frame-ancestors 'self' app.cmlite.org app.betteredmonton.org" always;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@ -207,7 +207,7 @@ server {
|
|||||||
# Rocket.Chat (team chat) — allows iframe embedding from admin
|
# Rocket.Chat (team chat) — allows iframe embedding from admin
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name chat.cmlite.org chat.betteredmonton.org;
|
server_name chat.cmlite.org chat.betteredmonton.org chat.pridecorner.ca;
|
||||||
add_header Content-Security-Policy "frame-ancestors 'self' app.cmlite.org app.betteredmonton.org" always;
|
add_header Content-Security-Policy "frame-ancestors 'self' app.cmlite.org app.betteredmonton.org" always;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@ -246,7 +246,7 @@ server {
|
|||||||
# Jitsi Meet (video conferencing) — allows iframe embedding from admin (app.cmlite.org)
|
# Jitsi Meet (video conferencing) — allows iframe embedding from admin (app.cmlite.org)
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name meet.cmlite.org meet.betteredmonton.org;
|
server_name meet.cmlite.org meet.betteredmonton.org meet.pridecorner.ca;
|
||||||
add_header Content-Security-Policy "frame-ancestors 'self' app.cmlite.org app.betteredmonton.org" always;
|
add_header Content-Security-Policy "frame-ancestors 'self' app.cmlite.org app.betteredmonton.org" always;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@ -366,7 +366,7 @@ server {
|
|||||||
# Admin GUI — app subdomain
|
# Admin GUI — app subdomain
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name app.cmlite.org app.betteredmonton.org;
|
server_name app.cmlite.org app.betteredmonton.org app.pridecorner.ca;
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
|
||||||
# Social media bot detection for OG meta tags
|
# Social media bot detection for OG meta tags
|
||||||
@ -513,7 +513,7 @@ server {
|
|||||||
# Root domain — routes to admin GUI (supports custom DOMAIN env var)
|
# Root domain — routes to admin GUI (supports custom DOMAIN env var)
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name betteredmonton.org;
|
server_name betteredmonton.org pridecorner.ca;
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
|||||||
@ -26,6 +26,9 @@ CONDITIONAL_CONTAINERS="nginx code-server"
|
|||||||
APP_CONTAINERS="api admin media-api nginx"
|
APP_CONTAINERS="api admin media-api nginx"
|
||||||
# Infrastructure containers (must stay up)
|
# Infrastructure containers (must stay up)
|
||||||
INFRA_CONTAINERS="v2-postgres redis"
|
INFRA_CONTAINERS="v2-postgres redis"
|
||||||
|
# LSIO containers with anonymous /config volumes (must be force-recreated on upgrade
|
||||||
|
# to prevent stale anonymous volumes from shadowing bind mounts underneath /config)
|
||||||
|
LSIO_VOLUME_CONTAINERS="mkdocs-site-server"
|
||||||
|
|
||||||
# User-modifiable paths (auto-resolve keeps user version on conflict)
|
# User-modifiable paths (auto-resolve keeps user version on conflict)
|
||||||
USER_PATHS=(
|
USER_PATHS=(
|
||||||
@ -106,6 +109,36 @@ restore_user_paths() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Verify Gancio config.json in its data volume ---
|
||||||
|
# Gancio uses a named Docker volume for /home/node/data. If the volume loses
|
||||||
|
# config.json (e.g., volume name prefix mismatch after compose project rename),
|
||||||
|
# Gancio detects an existing DB but no config and refuses to start with:
|
||||||
|
# "Non empty db! Please move your current db elsewhere than retry."
|
||||||
|
# This regenerates config.json from .env vars when missing.
|
||||||
|
verify_gancio_config() {
|
||||||
|
local gancio_volume
|
||||||
|
gancio_volume="$(docker volume ls --format '{{.Name}}' | grep 'gancio-data' | head -1 || true)"
|
||||||
|
if [[ -z "$gancio_volume" ]]; then
|
||||||
|
return # No gancio volume exists yet; first run will handle it
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if config.json exists and is non-empty
|
||||||
|
if docker run --rm -v "${gancio_volume}:/data" alpine test -s /data/config.json 2>/dev/null; then
|
||||||
|
success "Gancio config.json present in $gancio_volume"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn "Gancio config.json missing in volume $gancio_volume — regenerating from .env"
|
||||||
|
local base_url="${GANCIO_BASE_URL:-https://events.cmlite.org}"
|
||||||
|
local pg_user="${V2_POSTGRES_USER:-changemaker}"
|
||||||
|
local pg_pass="${V2_POSTGRES_PASSWORD:-changemaker}"
|
||||||
|
local config_json="{\"baseurl\":\"${base_url}\",\"server\":{\"host\":\"0.0.0.0\",\"port\":13120},\"db\":{\"dialect\":\"postgres\",\"host\":\"changemaker-v2-postgres\",\"port\":5432,\"database\":\"gancio\",\"username\":\"${pg_user}\",\"password\":\"${pg_pass}\"}}"
|
||||||
|
|
||||||
|
docker run --rm -v "${gancio_volume}:/data" alpine sh -c \
|
||||||
|
"echo '${config_json}' > /data/config.json && chown 1000:1000 /data/config.json"
|
||||||
|
success "Gancio config.json regenerated"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Lockfile ---
|
# --- Lockfile ---
|
||||||
acquire_lock() {
|
acquire_lock() {
|
||||||
if [[ -f "$LOCK_FILE" ]]; then
|
if [[ -f "$LOCK_FILE" ]]; then
|
||||||
@ -656,6 +689,18 @@ info "Stopping application containers..."
|
|||||||
docker compose stop $APP_CONTAINERS 2>/dev/null || true
|
docker compose stop $APP_CONTAINERS 2>/dev/null || true
|
||||||
success "Application containers stopped"
|
success "Application containers stopped"
|
||||||
|
|
||||||
|
# Force-recreate LSIO containers to prevent anonymous volume shadowing bind mounts.
|
||||||
|
# LSIO images define a VOLUME at /config in their Dockerfile. When a container is
|
||||||
|
# merely restarted, Docker reuses the old anonymous volume whose /config/www is empty,
|
||||||
|
# which shadows the bind mount (e.g., ./mkdocs/site:/config/www → 403 Forbidden).
|
||||||
|
# Removing the container first ensures a fresh anonymous volume that respects bind mounts.
|
||||||
|
info "Removing LSIO containers (clearing anonymous volumes)..."
|
||||||
|
docker compose rm -sf $LSIO_VOLUME_CONTAINERS 2>/dev/null || true
|
||||||
|
success "LSIO containers cleared for fresh recreation"
|
||||||
|
|
||||||
|
# Verify Gancio config.json exists before starting services
|
||||||
|
verify_gancio_config
|
||||||
|
|
||||||
# Ensure infrastructure is running and healthy
|
# Ensure infrastructure is running and healthy
|
||||||
info "Ensuring infrastructure is up..."
|
info "Ensuring infrastructure is up..."
|
||||||
docker compose up -d $INFRA_CONTAINERS
|
docker compose up -d $INFRA_CONTAINERS
|
||||||
@ -757,6 +802,28 @@ if docker ps --format '{{.Names}}' | grep -q 'changemaker-media-api'; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Gancio health (optional)
|
||||||
|
if docker ps --format '{{.Names}}' | grep -q 'gancio-changemaker'; then
|
||||||
|
if docker compose ps gancio --format '{{.Status}}' 2>/dev/null | grep -q "healthy"; then
|
||||||
|
success "Gancio: healthy"
|
||||||
|
elif docker compose ps gancio --format '{{.Status}}' 2>/dev/null | grep -qi "restarting"; then
|
||||||
|
warn "Gancio: restart loop detected (check config.json in gancio-data volume)"
|
||||||
|
VERIFY_FAILED=true
|
||||||
|
else
|
||||||
|
info "Gancio: starting (may take up to 60s)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# MkDocs static site health
|
||||||
|
if docker ps --format '{{.Names}}' | grep -q 'mkdocs-site-server'; then
|
||||||
|
if curl -sf http://localhost:${MKDOCS_SITE_SERVER_PORT:-4004}/ -o /dev/null 2>/dev/null; then
|
||||||
|
success "MkDocs site (port ${MKDOCS_SITE_SERVER_PORT:-4004}): healthy"
|
||||||
|
else
|
||||||
|
warn "MkDocs site (port ${MKDOCS_SITE_SERVER_PORT:-4004}): not responding"
|
||||||
|
VERIFY_FAILED=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Check for containers in restart loop
|
# Check for containers in restart loop
|
||||||
RESTARTING="$(docker compose ps 2>/dev/null | grep -i "restarting" || true)"
|
RESTARTING="$(docker compose ps 2>/dev/null | grep -i "restarting" || true)"
|
||||||
if [[ -n "$RESTARTING" ]]; then
|
if [[ -n "$RESTARTING" ]]; then
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user