mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
fixed some backend issues
This commit is contained in:
1 parent
466d0a0f9f
commit
2b2b4cf48f
13 files changed
+6800
-62
No files matched your search
@@ -13,6 +13,13 @@ COPY requirements.txt .
|
|||||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Non-root user for celery workers/beat. UID matches typical host (1000) so
|
||||||
|
# bind-mounted volumes (./backend, ./legacy, media/) stay writable. Override
|
||||||
|
# by rebuilding with --build-arg APP_UID=<your uid> if your host differs.
|
||||||
|
ARG APP_UID=1000
|
||||||
|
ARG APP_GID=1000
|
||||||
|
RUN groupadd -g ${APP_GID} app && useradd -u ${APP_UID} -g app -m -s /bin/bash app
|
||||||
|
|
||||||
# Copy the rest of the backend code
|
# Copy the rest of the backend code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
|||||||
+22
-11
@@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
@@ -14,15 +15,26 @@ from api.serializers import (
|
|||||||
JobSerializer,
|
JobSerializer,
|
||||||
SystemConfigSerializer,
|
SystemConfigSerializer,
|
||||||
)
|
)
|
||||||
|
from api.tasks import (
|
||||||
|
backup_db_task,
|
||||||
|
cleanup_duplicates_task,
|
||||||
|
seed_banners_task,
|
||||||
|
seed_legacy_task,
|
||||||
|
sync_flavors_task,
|
||||||
|
)
|
||||||
|
|
||||||
JOB_TASK_MAP = {
|
# Maps Job.name → (celery task name for beat schedules, callable for direct dispatch).
|
||||||
"sync_flavors": "api.sync_flavors",
|
# Direct callables let .delay() honor CELERY_TASK_ALWAYS_EAGER in dev.
|
||||||
"cleanup_duplicates": "api.cleanup_duplicates",
|
JOB_TASKS: dict[str, tuple[str, Any]] = {
|
||||||
"backup_db": "api.backup_db",
|
"sync_flavors": ("api.sync_flavors", sync_flavors_task),
|
||||||
"seed_legacy": "api.seed_legacy",
|
"cleanup_duplicates": ("api.cleanup_duplicates", cleanup_duplicates_task),
|
||||||
"seed_banners": "api.seed_banners",
|
"backup_db": ("api.backup_db", backup_db_task),
|
||||||
|
"seed_legacy": ("api.seed_legacy", seed_legacy_task),
|
||||||
|
"seed_banners": ("api.seed_banners", seed_banners_task),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JOB_TASK_MAP = {name: meta[0] for name, meta in JOB_TASKS.items()}
|
||||||
|
|
||||||
|
|
||||||
def _sync_periodic_task(job: Job) -> None:
|
def _sync_periodic_task(job: Job) -> None:
|
||||||
"""Create/update a django_celery_beat PeriodicTask for this Job.
|
"""Create/update a django_celery_beat PeriodicTask for this Job.
|
||||||
@@ -106,25 +118,24 @@ class AdminViewSet(viewsets.ViewSet):
|
|||||||
|
|
||||||
@action(detail=True, methods=["post"])
|
@action(detail=True, methods=["post"])
|
||||||
def trigger_job(self, request: Request, pk=None) -> Response:
|
def trigger_job(self, request: Request, pk=None) -> Response:
|
||||||
from celery import current_app
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
job = Job.objects.get(pk=pk)
|
job = Job.objects.get(pk=pk)
|
||||||
except Job.DoesNotExist:
|
except Job.DoesNotExist:
|
||||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
task_name = JOB_TASK_MAP.get(job.name)
|
meta = JOB_TASKS.get(job.name)
|
||||||
if not task_name:
|
if not meta:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": f"No celery task registered for {job.name}"},
|
{"error": f"No celery task registered for {job.name}"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
task_name, task_fn = meta
|
||||||
job.status = "pending"
|
job.status = "pending"
|
||||||
job.error_message = ""
|
job.error_message = ""
|
||||||
job.save(update_fields=["status", "error_message"])
|
job.save(update_fields=["status", "error_message"])
|
||||||
|
|
||||||
current_app.send_task(task_name)
|
task_fn.delay()
|
||||||
return Response({"status": "Job queued", "task": task_name})
|
return Response({"status": "Job queued", "task": task_name})
|
||||||
|
|
||||||
@action(detail=True, methods=["patch"])
|
@action(detail=True, methods=["patch"])
|
||||||
|
|||||||
+2968
File diff suppressed because it is too large.
Load diff
@@ -5,6 +5,9 @@ services:
|
|||||||
container_name: holy-redis
|
container_name: holy-redis
|
||||||
command: redis-server --appendonly yes --save 60 1
|
command: redis-server --appendonly yes --save 60 1
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
sysctls:
|
||||||
|
- net.core.somaxconn=512
|
||||||
|
- vm.overcommit_memory=1
|
||||||
networks:
|
networks:
|
||||||
- holy-net
|
- holy-net
|
||||||
volumes:
|
volumes:
|
||||||
@@ -51,6 +54,7 @@ services:
|
|||||||
context: ./backend
|
context: ./backend
|
||||||
container_name: holy-worker
|
container_name: holy-worker
|
||||||
command: celery -A holy_backend worker -l info --concurrency=2
|
command: celery -A holy_backend worker -l info --concurrency=2
|
||||||
|
user: "app"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- holy-net
|
- holy-net
|
||||||
@@ -83,6 +87,7 @@ services:
|
|||||||
context: ./backend
|
context: ./backend
|
||||||
container_name: holy-beat
|
container_name: holy-beat
|
||||||
command: celery -A holy_backend beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler --pidfile /tmp/celerybeat.pid
|
command: celery -A holy_backend beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler --pidfile /tmp/celerybeat.pid
|
||||||
|
user: "app"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- holy-net
|
- holy-net
|
||||||
|
|||||||
Generated
+247
@@ -42,6 +42,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"jsdom": "^29.0.2",
|
"jsdom": "^29.0.2",
|
||||||
|
"openapi-typescript": "^7.13.0",
|
||||||
"prettier": "^3.8.2",
|
"prettier": "^3.8.2",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.48.0",
|
"typescript-eslint": "^8.48.0",
|
||||||
@@ -2039,6 +2040,82 @@
|
|||||||
"url": "https://opencollective.com/popperjs"
|
"url": "https://opencollective.com/popperjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@redocly/ajv": {
|
||||||
|
"version": "8.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz",
|
||||||
|
"integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2",
|
||||||
|
"uri-js-replace": "^1.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/ajv/node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/config": {
|
||||||
|
"version": "0.22.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz",
|
||||||
|
"integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/openapi-core": {
|
||||||
|
"version": "1.34.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.12.tgz",
|
||||||
|
"integrity": "sha512-b32XWsz6enN6K4bx8xWsqUaXTJR/DnYT3lL1CzDYzIYKw243NNlz6fexmr71q/U4HrEcMoJGBvwAfcxOb8ymQw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@redocly/ajv": "8.11.2",
|
||||||
|
"@redocly/config": "0.22.0",
|
||||||
|
"colorette": "1.4.0",
|
||||||
|
"https-proxy-agent": "7.0.6",
|
||||||
|
"js-levenshtein": "1.1.6",
|
||||||
|
"js-yaml": "4.1.1",
|
||||||
|
"minimatch": "5.1.9",
|
||||||
|
"pluralize": "8.0.0",
|
||||||
|
"yaml-ast-parser": "0.0.43"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17.0",
|
||||||
|
"npm": ">=9.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/openapi-core/node_modules/brace-expansion": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/openapi-core/node_modules/minimatch": {
|
||||||
|
"version": "5.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
|
||||||
|
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-rc.3",
|
"version": "1.0.0-rc.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
||||||
@@ -3163,6 +3240,16 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
@@ -3180,6 +3267,16 @@
|
|||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-colors": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
@@ -3452,6 +3549,13 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/change-case": {
|
||||||
|
"version": "5.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
|
||||||
|
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/clsx": {
|
"node_modules/clsx": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
@@ -3502,6 +3606,13 @@
|
|||||||
"simple-swizzle": "^0.2.2"
|
"simple-swizzle": "^0.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/colorette": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/colorthief": {
|
"node_modules/colorthief": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/colorthief/-/colorthief-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/colorthief/-/colorthief-2.6.0.tgz",
|
||||||
@@ -4513,6 +4624,20 @@
|
|||||||
"void-elements": "3.1.0"
|
"void-elements": "3.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/https-proxy-agent": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/i18next": {
|
"node_modules/i18next": {
|
||||||
"version": "24.2.3",
|
"version": "24.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz",
|
||||||
@@ -4619,6 +4744,19 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/index-to-position": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iota-array": {
|
"node_modules/iota-array": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz",
|
||||||
@@ -4689,6 +4827,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/js-levenshtein": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -5067,6 +5215,58 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/openapi-typescript": {
|
||||||
|
"version": "7.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz",
|
||||||
|
"integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@redocly/openapi-core": "^1.34.6",
|
||||||
|
"ansi-colors": "^4.1.3",
|
||||||
|
"change-case": "^5.4.4",
|
||||||
|
"parse-json": "^8.3.0",
|
||||||
|
"supports-color": "^10.2.2",
|
||||||
|
"yargs-parser": "^21.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"openapi-typescript": "bin/cli.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openapi-typescript/node_modules/parse-json": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.26.2",
|
||||||
|
"index-to-position": "^1.1.0",
|
||||||
|
"type-fest": "^4.39.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openapi-typescript/node_modules/supports-color": {
|
||||||
|
"version": "10.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
|
||||||
|
"integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -5234,6 +5434,16 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pluralize": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
@@ -6055,6 +6265,19 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/type-fest": {
|
||||||
|
"version": "4.41.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
||||||
|
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
@@ -6157,6 +6380,13 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uri-js-replace": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.3.1",
|
"version": "7.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||||
@@ -6446,6 +6676,23 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml-ast-parser": {
|
||||||
|
"version": "0.0.43",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
|
||||||
|
"integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "21.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|||||||
@@ -12,7 +12,10 @@
|
|||||||
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,json,css}\"",
|
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,json,css}\"",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"schema:export": "cd ../backend && ./venv/bin/python manage.py spectacular --file schema.yaml",
|
||||||
|
"openapi:gen": "openapi-typescript ../backend/schema.yaml -o src/api/schema.d.ts",
|
||||||
|
"openapi:sync": "npm run schema:export && npm run openapi:gen"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@catppuccin/palette": "^1.7.1",
|
"@catppuccin/palette": "^1.7.1",
|
||||||
@@ -49,6 +52,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"jsdom": "^29.0.2",
|
"jsdom": "^29.0.2",
|
||||||
|
"openapi-typescript": "^7.13.0",
|
||||||
"prettier": "^3.8.2",
|
"prettier": "^3.8.2",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.48.0",
|
"typescript-eslint": "^8.48.0",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import api from '../../lib/api';
|
import api from '../../lib/api';
|
||||||
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
|
|
||||||
export interface CurrentUserLite {
|
export interface CurrentUserLite {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -10,8 +11,9 @@ export interface CurrentUserLite {
|
|||||||
* Reads /users/me/ for cases where we need a quick "is this me / am I admin"
|
* Reads /users/me/ for cases where we need a quick "is this me / am I admin"
|
||||||
* check outside of AuthContext. Disabled when not authenticated.
|
* check outside of AuthContext. Disabled when not authenticated.
|
||||||
*/
|
*/
|
||||||
export const useCurrentUserLite = () =>
|
export const useCurrentUserLite = () => {
|
||||||
useQuery({
|
const { user } = useAuth();
|
||||||
|
return useQuery({
|
||||||
queryKey: ['currentUserLite'],
|
queryKey: ['currentUserLite'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await api.get<CurrentUserLite>('users/me/', {
|
const res = await api.get<CurrentUserLite>('users/me/', {
|
||||||
@@ -21,6 +23,7 @@ export const useCurrentUserLite = () =>
|
|||||||
});
|
});
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
enabled: !!localStorage.getItem('access'),
|
enabled: !!user,
|
||||||
staleTime: 60_000,
|
staleTime: 60_000,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import api from '../../lib/api';
|
import api from '../../lib/api';
|
||||||
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
|
|
||||||
export interface CurrentUserFull {
|
export interface CurrentUserFull {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -12,8 +13,9 @@ export interface CurrentUserFull {
|
|||||||
unread_notifications_count?: number;
|
unread_notifications_count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMe = () =>
|
export const useMe = () => {
|
||||||
useQuery({
|
const { user } = useAuth();
|
||||||
|
return useQuery({
|
||||||
queryKey: ['me'],
|
queryKey: ['me'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await api.get<CurrentUserFull>('users/me/', {
|
const res = await api.get<CurrentUserFull>('users/me/', {
|
||||||
@@ -21,5 +23,6 @@ export const useMe = () =>
|
|||||||
});
|
});
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
enabled: !!localStorage.getItem('access'),
|
enabled: !!user,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
Vendored
+3506
File diff suppressed because it is too large.
Load diff
+20
-39
@@ -1,25 +1,25 @@
|
|||||||
export interface Reply {
|
import type { components } from './schema';
|
||||||
id: number;
|
|
||||||
user: string;
|
|
||||||
text: string;
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RatedItem {
|
export type Schemas = components['schemas'];
|
||||||
id: number;
|
|
||||||
flavor: number;
|
|
||||||
flavor_name: string;
|
|
||||||
flavor_image: string | null;
|
|
||||||
category_name: string;
|
|
||||||
category_slug: string;
|
|
||||||
score: number;
|
|
||||||
comment: string | null;
|
|
||||||
created_at: string;
|
|
||||||
replies: Reply[];
|
|
||||||
is_legacy?: boolean;
|
|
||||||
is_available?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Re-exported from generated OpenAPI schema.
|
||||||
|
// Regenerate with `npm run openapi:sync` after backend serializer changes.
|
||||||
|
export type Banner = Schemas['Banner'];
|
||||||
|
export type Category = Schemas['Category'];
|
||||||
|
export type Flavor = Schemas['Flavor'];
|
||||||
|
export type Notification = Schemas['Notification'];
|
||||||
|
export type Rating = Schemas['Rating'];
|
||||||
|
export type Reply = Schemas['Reply'];
|
||||||
|
export type Ticket = Schemas['Ticket'];
|
||||||
|
export type TicketMessage = Schemas['TicketMessage'];
|
||||||
|
export type User = Schemas['User'];
|
||||||
|
|
||||||
|
// `BannerConfig` was the legacy alias for the Banner schema; kept for callers.
|
||||||
|
export type BannerConfig = Banner;
|
||||||
|
// `RatedItem` was the legacy alias for Rating in dashboard payload.
|
||||||
|
export type RatedItem = Rating;
|
||||||
|
|
||||||
|
// Custom (non-DRF-serialized) shapes for endpoints without a serializer_class.
|
||||||
export interface MissingFlavor {
|
export interface MissingFlavor {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -39,22 +39,3 @@ export interface DashboardData {
|
|||||||
missing_flavors: MissingFlavor[];
|
missing_flavors: MissingFlavor[];
|
||||||
my_ratings: RatedItem[];
|
my_ratings: RatedItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BannerConfig {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
description: string;
|
|
||||||
is_active: boolean;
|
|
||||||
settings: Record<string, unknown> | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NotificationItem {
|
|
||||||
id: number;
|
|
||||||
verb: string;
|
|
||||||
actor: string;
|
|
||||||
target: string | null;
|
|
||||||
url: string | null;
|
|
||||||
is_read: boolean;
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
@@ -30,13 +30,12 @@ export const NotificationProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
// Refetch the auth user on the same 60s cadence so the unread count badge
|
// Refetch the auth user on the same 60s cadence so the unread count badge
|
||||||
// stays in sync with anything that mutated server-side outside this client.
|
// stays in sync with anything that mutated server-side outside this client.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!user) return;
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (localStorage.getItem('access')) {
|
|
||||||
refetchUser();
|
refetchUser();
|
||||||
}
|
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [refetchUser]);
|
}, [refetchUser, user]);
|
||||||
|
|
||||||
const fetchNotifications = useCallback(async () => {
|
const fetchNotifications = useCallback(async () => {
|
||||||
await refetch();
|
await refetch();
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
import { useNotificationsQuery } from '../api/queries/useNotifications';
|
import { useNotificationsQuery } from '../api/queries/useNotifications';
|
||||||
import { useCreateReply } from '../api/mutations/useRatingMutations';
|
import { useCreateReply } from '../api/mutations/useRatingMutations';
|
||||||
import { useTitle } from '../hooks/useTitle';
|
import { useTitle } from '../hooks/useTitle';
|
||||||
|
import { useAuth } from '../hooks/useAuth';
|
||||||
import RichText from '../components/RichText';
|
import RichText from '../components/RichText';
|
||||||
import { formatDate } from '../utils/date';
|
import { formatDate } from '../utils/date';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -82,7 +83,8 @@ const CommunityFeed: React.FC<CommunityFeedProps> = ({ adminMode }) => {
|
|||||||
const [replyInputs, setReplyInputs] = useState<Record<number, string>>({});
|
const [replyInputs, setReplyInputs] = useState<Record<number, string>>({});
|
||||||
const [expandedReplies, setExpandedReplies] = useState<Record<number, boolean>>({});
|
const [expandedReplies, setExpandedReplies] = useState<Record<number, boolean>>({});
|
||||||
|
|
||||||
const isAuthed = !!localStorage.getItem('access');
|
const { user } = useAuth();
|
||||||
|
const isAuthed = !!user;
|
||||||
const { data: feed, isLoading: feedLoading, error: feedError } = useCommunityFeed(page);
|
const { data: feed, isLoading: feedLoading, error: feedError } = useCommunityFeed(page);
|
||||||
const { data: following = [] } = useFollowingList();
|
const { data: following = [] } = useFollowingList();
|
||||||
const { data: notificationsAll = [] } = useNotificationsQuery(isAuthed);
|
const { data: notificationsAll = [] } = useNotificationsQuery(isAuthed);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
useTopFlavors,
|
useTopFlavors,
|
||||||
} from '../api/queries/useMainPageQueries';
|
} from '../api/queries/useMainPageQueries';
|
||||||
import { useTitle } from '../hooks/useTitle';
|
import { useTitle } from '../hooks/useTitle';
|
||||||
|
import { useAuth } from '../hooks/useAuth';
|
||||||
import RatingBadge from '../components/RatingBadge';
|
import RatingBadge from '../components/RatingBadge';
|
||||||
import StatusBadge from '../components/StatusBadge';
|
import StatusBadge from '../components/StatusBadge';
|
||||||
import {
|
import {
|
||||||
@@ -86,7 +87,8 @@ const MainPage: React.FC<MainPageProps> = (_props) => {
|
|||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
|
|
||||||
const query = new URLSearchParams(location.search).get('q') || '';
|
const query = new URLSearchParams(location.search).get('q') || '';
|
||||||
const isLoggedIn = !!localStorage.getItem('access');
|
const { user } = useAuth();
|
||||||
|
const isLoggedIn = !!user;
|
||||||
|
|
||||||
const { data: searchResults = [], isLoading: searchLoading } = useFlavorSearch(query);
|
const { data: searchResults = [], isLoading: searchLoading } = useFlavorSearch(query);
|
||||||
const { data: topFlavorsAll = [], isLoading: topLoading } = useTopFlavors(null);
|
const { data: topFlavorsAll = [], isLoading: topLoading } = useTopFlavors(null);
|
||||||
|
|||||||
Reference in new issue
Block a user