Compare commits

97 Commits

Author SHA1 Message Date
juanjo
99de5f06b5 docs: corregir flujo de ramas en CLAUDE.md (pre-dev → dev → master)
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 01:15:04 +02:00
juanjo
fbc5f0f6c4 fix: reemplazar referencias a api_hub_dispatcher por api_config
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
ROOT_URLCONF, WSGI_APPLICATION, DJANGO_SETTINGS_MODULE apuntaban al
módulo renombrado — causaba ModuleNotFoundError al arrancar en Docker.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 01:13:52 +02:00
juanjo
a17f00bad2 fix: env_file .env (relativo a deployments/) en docker-compose
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Jenkins copia el secret a deployments/.env — el path debe ser .env
relativo al docker-compose.yml, no ../.env a la raíz del workspace.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 01:06:28 +02:00
juanjo
41fc2a0aa0 fix: gunicorn + wait-for-db + healthcheck en docker-compose
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
- entrypoint.sh: sustituye runserver por gunicorn (workers=2, timeout=120)
- entrypoint.sh: espera a PostgreSQL antes de migrar cuando DB_HOST está definido
- docker-compose.yml: unifica nombre de servicio db, añade healthcheck robusto,
  corrige env_file path a ../.env

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:39:21 +02:00
juanjo
001bf13d26 chore: añadir CLAUDE.md con convenciones del ecosistema
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:12:18 +02:00
juanjo
47f23a73e1 docs: README completo con despliegue, BD y mantenimiento
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:02:21 +02:00
juanjo
04c37f669c revert: restaurar nombre api_config (revertir renombrado erróneo)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:02:21 +02:00
juanjo
92a00ec75f refactor: renombrar proyecto principal api_config → api_hub_dispatcher
- Renombrar carpeta app/api_config/ → app/api_hub_dispatcher/
- Actualizar DJANGO_SETTINGS_MODULE en asgi.py, wsgi.py, manage.py
- Actualizar ROOT_URLCONF y WSGI_APPLICATION en settings.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:02:21 +02:00
b91f5d09e5 Merge pull request 'dev' (#43) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #43
2026-04-16 17:32:46 +00:00
aee34d797d Merge pull request 'fix engram rtk' (#42) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #42
2026-04-16 16:25:26 +00:00
juanjo
0fc5392bd2 fix engram rtk
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 18:24:13 +02:00
1e1348bc1a Merge pull request 'dev' (#41) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #41
2026-04-16 15:34:48 +00:00
f68029edc1 Merge pull request 'fix: serializar dates/Decimal en LogService con DjangoJSONEncoder' (#40) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #40
2026-04-16 15:34:11 +00:00
juanjo
384f47df5e Merge branch 'master' of https://git.v-encore-lab.com/Proyecto-SaaS/django-core-base
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 17:29:18 +02:00
juanjo
a8dbb62b09 fix: serializar dates/Decimal en LogService con DjangoJSONEncoder
Some checks are pending
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
DEPLOY_MULTI_BRACH/pipeline/pr-dev Build queued...
- Añadir _json_safe() que usa DjangoJSONEncoder para convertir objetos
  no serializables (datetime.date, Decimal, UUID...) antes de guardar
  en JSONField, evitando que response quede vacío silenciosamente
- Corregir condición status_code: usar 'is not None' para permitir status=0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 17:28:48 +02:00
juanjo
2684e251f7 Merge dev: fix serialización LogService 2026-04-16 17:28:48 +02:00
juanjo
94faedecae Merge pre-dev: fix serialización LogService 2026-04-16 17:28:48 +02:00
3f95e92318 Merge pull request 'dev' (#39) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #39
2026-04-16 15:11:49 +00:00
aed8661331 Merge pull request 'fix: corregir ruta de data/ y hacer LogService resiliente a fallos de BD' (#38) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #38
2026-04-16 15:11:27 +00:00
juanjo
e908125d31 fix: corregir ruta de data/ y hacer LogService resiliente a fallos de BD
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
- Mover data/.gitkeep a app/data/.gitkeep (Django busca app/data/db.sqlite3,
  no data/db.sqlite3 en la raíz del repo, porque BASE_DIR apunta a app/)
- Actualizar .gitignore para ignorar también app/data/*
- LogService.gestionar_log ahora captura excepciones internamente para que
  un fallo de BD no rompa la petición (log omitido con WARNING, no 500)
- Mover import de Log dentro del método para evitar problemas de arranque

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 17:07:23 +02:00
juanjo
03663aacb4 Merge dev: fix ruta data/ y LogService resiliente 2026-04-16 17:07:23 +02:00
juanjo
0c18ffc2f9 Merge pre-dev: fix ruta data/ y LogService resiliente 2026-04-16 17:07:23 +02:00
77b722d739 Merge pull request 'dev' (#37) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #37
2026-04-16 14:57:37 +00:00
3c7d47782b Merge pull request 'feat: añadir app general con LogService centralizado' (#36) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #36
2026-04-16 14:57:20 +00:00
juanjo
156b5ad77d Merge dev: feat app general con LogService 2026-04-16 16:51:00 +02:00
juanjo
5d2a6469aa Merge pre-dev: feat app general con LogService 2026-04-16 16:51:00 +02:00
juanjo
2f6564d9a6 feat: añadir app general con LogService centralizado
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/pr-dev This commit looks good
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
- Crear app/general con estructura estándar del proyecto:
  · utilidades/acciones.py → LogService.gestionar_log() (única fuente de logs)
  · utilidades/utils.py → get_client_ip()
  · utilidades/custom_errors.py → ValidationError, ExternalServiceError, NotFoundError
  · exception.py, request.py, serializers.py, validaciones/
- Registrar 'general' en INSTALLED_APPS y añadir general/ a urls.py
- Refactorizar promociones/views.py para usar LogService en lugar de Log directo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:50:53 +02:00
e2ae400889 Merge pull request 'dev' (#35) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #35
2026-04-16 14:37:32 +00:00
e597a05f08 Merge pull request 'refactor: mover BD SQLite a carpeta data/' (#34) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #34
2026-04-16 14:36:39 +00:00
juanjo
4425141cb3 Merge dev: refactor mover BD SQLite a carpeta data/
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 16:34:16 +02:00
juanjo
29db0eb0a2 Merge pre-dev: refactor mover BD SQLite a carpeta data/ 2026-04-16 16:34:00 +02:00
juanjo
91fc6900eb refactor: mover BD SQLite a carpeta data/
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
- Mover db.sqlite3 de raíz a data/db.sqlite3
- Actualizar settings.py: fallback SQLite a data/ cuando DB_HOST no está definido
- Actualizar .gitignore: ignorar data/* pero mantener data/.gitkeep
- Actualizar init_db.py: mensaje apunta a data/db.sqlite3
- Añadir data/.gitkeep para anclar la carpeta en git

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:33:26 +02:00
0a73b91e12 Merge pull request 'fix: corregir name en common/apps.py' (#33) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 14:13:02 +00:00
d444021a00 Merge pull request 'fix: corregir name en common/apps.py' (#32) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 14:12:59 +00:00
juanjo
9fba8938ee fix: corregir name en common/apps.py de 'apps.common' a 'common'
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:12:40 +02:00
508f3f028d Merge pull request 'refactor: reorganizar estructura app/api_config' (#31) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 14:02:19 +00:00
7a151a4768 Merge pull request 'refactor: reorganizar estructura app/api_config' (#30) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 14:02:01 +00:00
juanjo
299428741b refactor: reorganizar estructura del proyecto al estándar app/api_config
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
- core/ → app/api_config/
- apps/backend_admin/ → app/backend_admin/
- apps/common/ → app/common/
- apps/promociones/ → app/promociones/
- manage.py → app/manage.py
- Añadir app/requirements.txt
- Actualizar todos los imports y referencias (DJANGO_SETTINGS_MODULE, ROOT_URLCONF, WSGI_APPLICATION, INSTALLED_APPS)
- Actualizar Dockerfile con nuevo WORKDIR

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:01:17 +02:00
bc82249a29 Merge pull request 'Merge dev into master' (#29) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 13:51:32 +00:00
56e7d77d63 Merge pull request 'Merge pre-dev into dev' (#28) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 13:51:17 +00:00
juanjo
27ccce862d Renombrar actions.py a acciones.py en backend_admin y promociones
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
DEPLOY_MULTI_BRACH/pipeline/pr-dev This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 15:42:21 +02:00
f3514d399e Merge pull request 'dev' (#27) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #27
2026-04-14 23:44:35 +00:00
1e72aa3e44 Merge pull request 'Update 0001_initial.py' (#26) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
DEPLOY_MULTI_BRACH/pipeline/pr-master This commit looks good
Reviewed-on: #26
2026-04-14 23:17:11 +00:00
minguezsanzjuanjose
84baf8fbda Update 0001_initial.py
Some checks are pending
DEPLOY_MULTI_BRACH/pipeline/pr-dev Build queued...
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-15 01:16:07 +02:00
b08e74f459 Merge pull request 'fix' (#25) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #25
2026-04-14 23:02:44 +00:00
minguezsanzjuanjose
5d69f89028 fix
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-15 01:01:46 +02:00
0ac0a859b9 Merge pull request 'dev' (#24) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #24
2026-04-14 21:52:57 +00:00
02aca6a2a6 Merge pull request 'Update Jenkinsfile' (#23) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
DEPLOY_MULTI_BRACH/pipeline/pr-master This commit looks good
Reviewed-on: #23
2026-04-14 21:48:28 +00:00
minguezsanzjuanjose
4a4d0940b3 Update Jenkinsfile
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-14 23:47:24 +02:00
7a9a1686fa Merge pull request 'dev' (#21) from dev into master
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
Reviewed-on: #21
2026-04-14 21:28:08 +00:00
b4ab653c95 Merge pull request 'Update settings.py' (#22) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/pr-master This commit looks good
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #22
2026-04-14 21:25:16 +00:00
minguezsanzjuanjose
c32092d216 Update settings.py
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-14 23:24:27 +02:00
049764b79c Merge pull request 'Delete 0002_alter_log_options.py' (#20) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/pr-master This commit looks good
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #20
2026-04-14 21:03:22 +00:00
minguezsanzjuanjose
7ba79c9739 Delete 0002_alter_log_options.py
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-14 23:02:45 +02:00
c60c140a97 Merge pull request 'cambios en migraciones' (#19) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #19
2026-04-14 20:46:59 +00:00
minguezsanzjuanjose
bf5a38d425 cambios en migraciones
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-14 22:46:18 +02:00
f610eb24d4 Merge pull request 'feat: integración final de Jenkins con inyección de .env segura' (#18) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #18
2026-04-14 19:31:55 +00:00
minguezsanzjuanjose
039349b5b1 feat: integración final de Jenkins con inyección de .env segura
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-14 21:30:19 +02:00
816cc276f8 Merge pull request 'fix' (#17) from pre-dev into dev
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
Reviewed-on: #17
2026-04-13 23:01:41 +00:00
minguezsanzjuanjose
9dd97b34f2 fix
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-14 01:00:37 +02:00
5a7209badb Merge pull request 'dev' (#16) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #16
2026-04-12 20:57:46 +00:00
d39e078fb6 Merge pull request 'fix admin status' (#15) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #15
2026-04-12 20:56:59 +00:00
juanjo
3da81a9495 fix admin status
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-12 22:56:35 +02:00
ac1c024bee Merge pull request 'endpoint de status' (#14) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #14
2026-04-12 20:39:31 +00:00
juanjo
e5908b1880 endpoint de status
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-12 22:38:49 +02:00
147a1d49cc Merge pull request 'pre-dev' (#13) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #13
2026-04-12 20:15:16 +00:00
juanjo
1bf3337616 Update .gitignore
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-12 22:12:35 +02:00
juanjo
6fb3afa472 fix para ficheros que no se suben
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-12 22:11:51 +02:00
8579af3f21 Merge pull request 'fix' (#12) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #12
2026-04-12 14:04:06 +00:00
minguezsanzjuanjose
102d7c6bfa fix
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-12 16:02:58 +02:00
32e3184b59 Merge pull request 'dev' (#11) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #11
2026-04-12 13:42:46 +00:00
5e04933708 Merge pull request 'ignore' (#10) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #10
2026-04-12 13:40:45 +00:00
minguezsanzjuanjose
ac6e336772 ignore
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-12 15:38:01 +02:00
083375c5f0 Merge pull request 'dev' (#9) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #9
2026-04-12 12:10:27 +00:00
d9bba25437 Merge pull request 'Update Jenkinsfile' (#8) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #8
2026-04-12 12:09:27 +00:00
minguezsanzjuanjose
e486a0f556 Update Jenkinsfile
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-12 14:08:36 +02:00
6f84db00cd Merge pull request 'dev' (#7) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #7
2026-04-12 02:53:09 +00:00
b37b581a78 Merge pull request 'pre-dev' (#6) from pre-dev into dev
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/pr-master Build queued...
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
Reviewed-on: #6
2026-04-12 02:52:00 +00:00
minguezsanzjuanjose
0971134702 Update docker-compose.yml
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-12 04:51:32 +02:00
minguezsanzjuanjose
02bc67c5fd Update docker-compose.yml
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-12 04:49:56 +02:00
c54e1e9b9e Merge pull request 'Update .env' (#5) from pre-dev into dev
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
Reviewed-on: #5
2026-04-12 02:46:57 +00:00
minguezsanzjuanjose
02feaf445d Update .env
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-12 04:46:31 +02:00
15f2a21a2c Merge pull request 'fix' (#4) from pre-dev into dev
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
Reviewed-on: #4
2026-04-12 02:43:35 +00:00
minguezsanzjuanjose
c8aa23b564 fix
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-12 04:43:02 +02:00
264c66c7c0 Merge pull request 'Update docker-compose.yml' (#3) from pre-dev into dev
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
Reviewed-on: #3
2026-04-12 02:39:57 +00:00
minguezsanzjuanjose
0ce0554443 Update docker-compose.yml
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-12 04:39:31 +02:00
16cbd85e7f Merge pull request 'fix' (#2) from pre-dev into dev
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
Reviewed-on: #2
2026-04-12 02:37:26 +00:00
minguezsanzjuanjose
375291514e fix
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-12 04:36:59 +02:00
b60ecccad4 Merge pull request 'dev' (#28) from dev into master
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
Reviewed-on: #28
2026-04-11 23:02:30 +02:00
c4e8675fe8 Merge pull request 'dev' (#23) from dev into master
Reviewed-on: #23
2026-04-11 20:48:19 +02:00
04fb83447f Merge pull request 'dev' (#16) from dev into master
Reviewed-on: https://gitea.185.187.169.109.nip.io/Proyecto-SaaS/django-core-base/pulls/16
2026-04-11 18:39:48 +02:00
bdda074fa1 Merge pull request 'dev' (#14) from dev into master
Reviewed-on: https://gitea.185.187.169.109.nip.io/Proyecto-SaaS/django-core-base/pulls/14
2026-04-11 18:23:51 +02:00
64afc3aedb Merge pull request 'dev' (#11) from dev into master
Reviewed-on: https://gitea.185.187.169.109.nip.io/Proyecto-SaaS/django-core-base/pulls/11
2026-04-11 18:04:09 +02:00
156e1aa27c Merge pull request 'dev' (#9) from dev into master
Reviewed-on: https://gitea.185.187.169.109.nip.io/Proyecto-SaaS/django-core-base/pulls/9
2026-04-11 15:21:01 +02:00
16b7f956b3 Merge pull request 'dev' (#7) from dev into master
Reviewed-on: https://gitea.185.187.169.109.nip.io/Proyecto-SaaS/django-core-base/pulls/7
2026-04-11 15:13:03 +02:00
34faf2157e Merge pull request 'dev' (#5) from dev into master
Reviewed-on: https://gitea.185.187.169.109.nip.io/Proyecto-SaaS/django-core-base/pulls/5
2026-04-11 15:07:07 +02:00
2a6723aedf Merge pull request 'dev' (#3) from dev into master
Reviewed-on: https://gitea.185.187.169.109.nip.io/Proyecto-SaaS/django-core-base/pulls/3
2026-04-11 14:57:47 +02:00
1086 changed files with 949286 additions and 246 deletions

View File

@@ -0,0 +1,30 @@
{
"permissions": {
"allow": [
"Bash(gh auth:*)",
"Bash(git config:*)",
"Read(//c/Users/juanm/**)",
"Bash(cmdkey /list)",
"Bash(curl -s -X POST https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2' -H 'Content-Type: application/json' -d '{\"title\":\"Merge pre-dev into dev\",\"head\":\"pre-dev\",\"base\":\"dev\",\"body\":\"Merge de pre-dev a dev\"}')",
"Bash(curl -s -X POST https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2' -H 'Content-Type: application/json' -d '{\"title\":\"Merge dev into master\",\"head\":\"dev\",\"base\":\"master\",\"body\":\"Merge de dev a master\"}')",
"Bash(curl -s -X POST https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls/28/merge -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2' -H 'Content-Type: application/json' -d '{\"Do\":\"merge\",\"merge_message_field\":\"Merge pre-dev into dev\"}')",
"Bash(curl -s https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls/28 -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2')",
"Bash(python -c \"import sys,json; p=json.load\\(sys.stdin\\); print\\('merged:', p['merged'], '| state:', p['state']\\)\")",
"Bash(curl -s -X POST https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls/29/merge -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2' -H 'Content-Type: application/json' -d '{\"Do\":\"merge\",\"merge_message_field\":\"Merge dev into master\"}')",
"Bash(curl -s https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls/29 -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2')",
"Bash(mkdir -p app)",
"Bash(mv core:*)",
"Bash(mv apps/backend_admin app/backend_admin)",
"Bash(mv apps/common app/common)",
"Bash(mv apps/promociones app/promociones)",
"WebSearch",
"Bash(npm install:*)",
"Bash(engram init:*)",
"Bash(dir:*)",
"Bash(engram --help:*)",
"Bash(engram remember:*)",
"Bash(engram stats:*)",
"Bash(engram recall:*)"
]
}
}

10
.env
View File

@@ -1,10 +0,0 @@
# Seguridad
DEBUG=True
SECRET_KEY=una-clave-muy-secreta-y-larga-123456
# Base de Datos (Conectando al PostgreSQL que instalamos)
DB_NAME=gitea
DB_USER=gitea
DB_PASSWORD=gitea_password
DB_HOST=db
DB_PORT=5432

21
.gitignore vendored
View File

@@ -1 +1,22 @@
# Configuración personal y secretos
.env
.env.local
# Docker local (si decides no subirlo)
deployments/docker-compose.override.yml
deployments/docker-compose.yml
# Archivos de datos de la DB local
postgres_data/
local_postgres_data/
# Carpeta de datos (BD SQLite y similares), pero se mantiene la carpeta
data/*
!data/.gitkeep
app/data/*
!app/data/.gitkeep
*.pyc *.pyc
# Bloquear todos los .env en cualquier carpeta
.env
**/core/.env
**/deployments/.env

110
CLAUDE.md Normal file
View File

@@ -0,0 +1,110 @@
# Convenciones del Ecosistema V-Encore Lab
## Ecosistema de microservicios
| Repo | Rol | Puerto |
|------|-----|--------|
| `django-core-base` | Hub orquestador principal | 8000 |
| `api_backoffice` | Consulta y gestión de BD | 8001 |
| `api_comunicaciones` | Emails, notificaciones, webhooks | 8002 |
| `api_documentacion` | Generación y gestión de documentos | 8003 |
| `web_interno` | Panel de gestión (React + Ant Design) | 3000 |
## Stack Django
- Django 5.0 + DRF + SimpleJWT
- Apps bajo `app/`. Prefijo `/api/` en todas las URLs salvo `admin/`.
- Patrón 3 capas: **URL → View → Action**
- SQL con `connection.cursor()` y placeholders (`%s`). Nunca concatenar strings.
## Patrón de vistas — 4 bloques obligatorios
```python
from general.utilidades.acciones import LogService
class MiVista(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
path = '/mi-app/mi-endpoint/'
# Bloque 1 — Inicio Log
log_id = LogService.gestionar_log(self, request, path=path)
try:
# Bloque 2 — Data Cleaning
data = request.data
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_request=data, status_code=100)
params = {'campo': data.get('campo')}
except Exception as error:
response = {'error': str(error)}
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=response, status_code=400)
return JsonResponse(response, status=400)
try:
# Bloque 3 — Action Call
resultado = mi_accion(params)
# Bloque 4 — Cierre Log
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=resultado, status_code=200)
return JsonResponse(resultado, status=200)
except Exception as error:
response = {'error': str(error)}
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=response, status_code=500)
return JsonResponse(response, status=500)
```
## LogService — reglas
- Siempre llamar como `LogService.gestionar_log(self, request, ...)` — pasar `self` de la vista.
- Primera llamada (sin `log_id`): crea el registro, devuelve el `log_id`.
- Llamadas siguientes: actualizar con `log_id=log_id`.
- `body_request` y `body_response` se serializan con `DjangoJSONEncoder` — soporta `datetime.date`, `Decimal`, etc.
- `status_code` como entero. Usar `is not None` para comprobar (0 es válido).
## Base de datos
- `DB_HOST` definido → PostgreSQL
- `DB_HOST` no definido → SQLite en `app/data/db.sqlite3`
- Migraciones siempre desde `app/`: `cd app && python manage.py migrate`
- SQL INSERT con psycopg2: usar `INSERT ... RETURNING id` + `cursor.fetchone()`. **Nunca `cursor.lastrowid`**.
- Booleans en psycopg2: pasar `True`/`False`, no `1`/`0`.
## Flujo de ramas
```
pre-dev → dev → master
```
- `pre-dev`: desarrollo activo — aquí entran todos los cambios
- `dev`: validación previa a producción
- `master`: producción estable
- Merges siempre con `--no-ff`
- **NUNCA** propagar en sentido inverso (master → dev o dev → pre-dev)
- Secuencia correcta:
```bash
git checkout pre-dev && git merge <mi-cambio> --no-ff
git checkout dev && git merge pre-dev --no-ff && git push origin dev
git checkout master && git merge dev --no-ff && git push origin master
```
## Estructura de app Django
```
app/
├── api_config/ # settings.py, urls.py, wsgi.py
├── general/ # LogService, utils — NO tiene modelos propios
├── backend_admin/ # Modelo Log (audit_logs), admin panel
├── common/ # Modelos y utilidades compartidas entre apps
├── <feature_app>/ # Una app por dominio de negocio
├── data/ # SQLite (gitignored, solo .gitkeep versionado)
└── manage.py
```
## Fixtures
- `loaddata` ignora `auto_now_add=True` — incluir fechas explícitas en el JSON o falla con NOT NULL en PostgreSQL.

310
README.md
View File

@@ -1,6 +1,312 @@
# django-core-base # django-core-base · API Hub Orquestador
// V-Encore Lab: Sistema Automatizado v1.0.4 > V-Encore Lab — Microservicio principal. Actúa como orquestador y punto de entrada central del ecosistema SaaS.
> Puerto por defecto: **8000**
---
## Tabla de contenidos
1. [Requisitos previos](#requisitos-previos)
2. [Inicio rápido — Desarrollo local](#inicio-rápido--desarrollo-local)
3. [Variables de entorno](#variables-de-entorno)
4. [Base de datos](#base-de-datos)
5. [Docker — Producción](#docker--producción)
6. [Estructura del proyecto](#estructura-del-proyecto)
7. [Endpoints principales](#endpoints-principales)
8. [Patrón LogService](#patrón-logservice)
9. [Flujo de ramas](#flujo-de-ramas)
10. [Mantenimiento](#mantenimiento)
---
## Requisitos previos
| Herramienta | Versión mínima |
|-------------|---------------|
| Python | 3.11 |
| pip | 23+ |
| Docker | 24+ |
| Docker Compose | v2 |
| Git | 2.40+ |
---
## Inicio rápido — Desarrollo local
```bash
# 1. Clonar
git clone https://git.v-encore-lab.com/Proyecto-SaaS/django-core-base.git
cd django-core-base
# 2. Entorno virtual
python -m venv .venv
source .venv/bin/activate # Linux/Mac
.venv\Scripts\activate # Windows
# 3. Dependencias
pip install -r deployments/requirements.txt
# 4. Variables de entorno
cp .env.example .env
# Editar .env según el entorno (ver sección Variables de entorno)
# 5. Migraciones (SQLite en local)
cd app
python manage.py migrate
# 6. Superusuario (primera vez)
python manage.py createsuperuser
# 7. Servidor de desarrollo
python manage.py runserver 0.0.0.0:8000
```
Accesos:
- API: http://localhost:8000/api/
- Admin: http://localhost:8000/admin/
---
## Variables de entorno
Copia `.env.example` a `.env` y ajusta los valores:
| Variable | Descripción | Ejemplo / Default |
|----------------|--------------------------------------------------|----------------------------|
| `SECRET_KEY` | Clave secreta Django | `django-insecure-...` |
| `DEBUG` | Modo debug | `True` (dev) / `False` (prod) |
| `ALLOWED_HOSTS`| Hosts permitidos (separados por coma) | `localhost,127.0.0.1` |
| `DB_HOST` | Host PostgreSQL. **Si no se define → SQLite** | `localhost` / vacío |
| `DB_PORT` | Puerto PostgreSQL | `5432` |
| `DB_NAME` | Nombre de la base de datos | `vencorelab` |
| `DB_USER` | Usuario PostgreSQL | `postgres` |
| `DB_PASSWORD` | Contraseña PostgreSQL | `postgres` |
---
## Base de datos
### Desarrollo local — SQLite
Si `DB_HOST` **no** está definido en `.env`, Django usa automáticamente SQLite:
```
app/data/db.sqlite3
```
```bash
cd app
python manage.py migrate # crea app/data/db.sqlite3
python manage.py createsuperuser
```
La carpeta `app/data/` está en `.gitignore` (solo se versiona `.gitkeep`).
### Producción — PostgreSQL
Define `DB_HOST` en `.env` para activar el driver PostgreSQL:
```env
DB_HOST=localhost
DB_PORT=5432
DB_NAME=vencorelab
DB_USER=postgres
DB_PASSWORD=supersecret
```
```bash
cd app
python manage.py migrate
```
### Comandos útiles de migración
```bash
cd app
# Ver migraciones pendientes
python manage.py showmigrations
# Crear migraciones de una app
python manage.py makemigrations <app_name>
# Aplicar todas las migraciones
python manage.py migrate
# Revertir migraciones de una app al estado inicial
python manage.py migrate <app_name> zero
```
---
## Docker — Producción
```bash
# Construir e iniciar
docker-compose up --build -d
# Ver logs
docker-compose logs -f
# Parar
docker-compose down
# Parar y eliminar volúmenes (¡cuidado en producción!)
docker-compose down -v
```
El contenedor expone el puerto **8000**.
Para producción con PostgreSQL, asegúrate de que `.env` tenga `DB_HOST` apuntando al host correcto (puede ser el nombre del servicio en la red Docker).
---
## Estructura del proyecto
```
django-core-base/
├── app/
│ ├── api_config/ # Configuración Django (settings, urls, wsgi)
│ ├── general/ # App transversal
│ │ └── utilidades/
│ │ ├── acciones.py # LogService — auditoría centralizada
│ │ └── utils.py # Utilidades HTTP (get_client_ip, etc.)
│ ├── backend_admin/ # App admin: modelo Log, endpoints de gestión
│ ├── common/ # Modelos y utilidades compartidas
│ ├── promociones/ # App de ejemplo
│ ├── automatizados/ # Endpoints para Jenkins/automatizaciones
│ ├── data/ # Directorio de la BD SQLite (gitignored)
│ │ └── .gitkeep
│ └── manage.py
├── deployments/
│ ├── requirements.txt
│ └── Dockerfile
├── docker-compose.yml
├── .env.example
└── README.md
```
---
## Endpoints principales
| Método | Ruta | Descripción | Auth |
|--------|-------------------------------|-----------------------------------|--------------|
| POST | `/api/token/` | Obtener JWT (login) | No |
| POST | `/api/token/refresh/` | Renovar access token | No |
| GET | `/admin/` | Panel de administración Django | Session |
| POST | `/api/promociones/obtener/` | Consultar promociones | JWT Bearer |
| GET | `/api/general/health/` | Health check | No |
Autenticación: `Authorization: Bearer <access_token>`
---
## Patrón LogService
Todas las vistas deben usar `LogService` para auditoría:
```python
from general.utilidades.acciones import LogService
class MiVista(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
path = '/mi-app/mi-endpoint/'
# Bloque 1 — Inicio log
log_id = LogService.gestionar_log(self, request, path=path)
try:
# Bloque 2 — Limpieza de datos
data = request.data
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_request=data, status_code=100)
params = {'campo': data.get('campo')}
except Exception as error:
response = {'error': str(error)}
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=response, status_code=400)
return JsonResponse(response, status=400)
try:
# Bloque 3 — Acción
resultado = mi_accion(params)
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=resultado, status_code=200)
return JsonResponse(resultado, status=200)
except Exception as error:
response = {'error': str(error)}
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=response, status_code=500)
return JsonResponse(response, status=500)
```
`LogService` registra automáticamente en la tabla `audit_logs` el usuario, IP, path, request/response y status code.
---
## Flujo de ramas
```
pre-dev → dev → master
```
- `pre-dev`: desarrollo activo, integración de features
- `dev`: validación previa a producción
- `master`: rama de producción estable
```bash
# Crear feature
git checkout pre-dev
git checkout -b feature/mi-feature
# Mergear a pre-dev
git checkout pre-dev
git merge feature/mi-feature --no-ff
# Promover a dev
git checkout dev
git merge pre-dev --no-ff
# Promover a master
git checkout master
git merge dev --no-ff
```
---
## Mantenimiento
```bash
# Limpiar sesiones expiradas
cd app && python manage.py clearsessions
# Ver estado de la BD
cd app && python manage.py dbshell
# Backup SQLite (desarrollo)
cp app/data/db.sqlite3 app/data/db.sqlite3.bak
# Backup PostgreSQL (producción)
pg_dump -U postgres vencorelab > backup_$(date +%Y%m%d).sql
# Actualizar dependencias
pip list --outdated
pip install -U <paquete>
pip freeze > deployments/requirements.txt
# Reiniciar contenedor Docker
docker-compose restart web
# Ver logs del contenedor
docker-compose logs -f web --tail=100
```
---
> **V-Encore Lab** — Sistema Automatizado v1.0
> Repositorio: `Proyecto-SaaS/django-core-base`

View File

@@ -0,0 +1,3 @@
# core/.env
APP_CUSTOM_SETTING="Este es un valor privado de la app"
EXTERNAL_SERVICE_API_KEY="sk_test_12345"

View File

@@ -1,4 +1,4 @@
import os import os
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_config.settings')
application = get_asgi_application() application = get_asgi_application()

185
app/api_config/settings.py Normal file
View File

@@ -0,0 +1,185 @@
from pathlib import Path
import os
from dotenv import load_dotenv
from datetime import timedelta
SIMPLE_JWT = {
# Cambiamos el tiempo de acceso a 3 horas
'ACCESS_TOKEN_LIFETIME': timedelta(hours=3),
# El tiempo del refresh token suele ser mayor (por ejemplo, 1 día)
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
# Otras configuraciones que ya tengas...
'ALGORITHM': 'HS256',
'AUTH_HEADER_TYPES': ('Bearer',),
}
# 1. RUTA DEL SETTINGS Y CARGA DEL .ENV
# Obtenemos la ruta de la carpeta donde está este archivo (core/)
CURRENT_DIR = Path(__file__).resolve().parent
BASE_DIR = CURRENT_DIR.parent
# Cargamos el .env específico de esta carpeta (core/.env)
load_dotenv(dotenv_path=CURRENT_DIR / '.env')
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-default-key-change-it')
# SECURITY WARNING: don't run with debug turned on in production!
# Mejoramos el parseo de DEBUG para que no falle si viene como string
DEBUG = os.getenv('DEBUG', 'True').lower() in ('true', '1', 't')
# 2. ALLOWED HOSTS
# Limpiamos y centralizamos los hosts permitidos
ALLOWED_HOSTS = [
'v-encore-lab.com',
'dev.v-encore-lab.com',
'185.187.169.109',
'localhost',
'127.0.0.1',
os.getenv('APP_CONTAINER_NAME', 'django_app_dev'), # Dinámico para Docker
]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Plugins
'rest_framework',
'rest_framework.authtoken',
'corsheaders',
# Tus Apps (Asegúrate de que el path sea correcto)
'general',
'promociones',
'automatizados',
'backend_admin',
'common',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'api_config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'api_config.wsgi.application'
# 3. DATABASE
# En producción (cuando DB_HOST está definido) usa PostgreSQL.
# En local/desarrollo sin configuración, cae a SQLite en data/
if os.getenv('DB_HOST'):
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME', 'postgres'),
'USER': os.getenv('DB_USER', 'postgres'),
'PASSWORD': os.getenv('DB_PASSWORD', ''),
'HOST': os.getenv('DB_HOST'),
'PORT': os.getenv('DB_PORT', '5432'),
}
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'data' / 'db.sqlite3',
}
}
# Internationalization
LANGUAGE_CODE = 'es-es'
TIME_ZONE = 'Europe/Madrid'
USE_I18N = True
USE_TZ = True
# Static files
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# REST Framework
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(hours=1),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
}
# CORS & CSRF
CORS_ALLOW_ALL_ORIGINS = DEBUG # Solo permitir todo en modo DEBUG
CSRF_TRUSTED_ORIGINS = [
'https://v-encore-lab.com',
'http://localhost:8000',
'http://127.0.0.1:8000',
]
# Logging simplificado
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {message}',
'style': '{',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'loggers': {
'': {
'handlers': ['console'],
'level': os.getenv('LOG_LEVEL', 'INFO'),
},
},
}
# --- CONFIGURACIONES PERSONALIZADAS DE LA APP ---
# Leemos la variable del .env (cargado previamente con load_dotenv)
# Ponemos un valor por defecto por si se nos olvida ponerlo en el .env
APP_CUSTOM_SETTING = os.getenv('APP_CUSTOM_SETTING', 'valor_por_defecto_seguro')
# Ejemplo de otra variable de API
EXTERNAL_SERVICE_API_KEY = os.getenv('EXTERNAL_SERVICE_API_KEY', None)

10
app/api_config/urls.py Normal file
View File

@@ -0,0 +1,10 @@
from django.urls import path, include
from backend_admin import views as admin_views
urlpatterns = [
path('admin/', include('backend_admin.urls')),
path('api/general/', include('general.urls')),
path('api/promociones/', include('promociones.urls')),
path('api/automatizados/', include('automatizados.urls')),
path('api/token/', admin_views.api_token, name='token_obtain_pair'),
]

View File

@@ -2,6 +2,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
# Este es el enlace con tus configuraciones # Este es el enlace con tus configuraciones
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_config.settings')
application = get_wsgi_application() application = get_wsgi_application()

View File

@@ -0,0 +1,220 @@
import json
import urllib.request
import urllib.error
from datetime import datetime
from django.db import connection
from django.utils import timezone
from common.utils import clean_sql_string, clean_sql_int
# =========================================================
# Helper HTTP (stdlib) — evita dependencia extra en requirements
# =========================================================
def _http_get_json(url, timeout=8):
"""GET a un servicio externo que responde JSON. Usa stdlib para no añadir deps."""
req = urllib.request.Request(url, headers={'User-Agent': 'django-core-base/1.0'})
with urllib.request.urlopen(req, timeout=timeout) as response:
raw = response.read().decode('utf-8')
try:
return json.loads(raw)
except json.JSONDecodeError:
return {'raw': raw}
def _http_get_text(url, timeout=8):
"""GET a un servicio externo que responde texto plano."""
req = urllib.request.Request(url, headers={'User-Agent': 'django-core-base/1.0'})
with urllib.request.urlopen(req, timeout=timeout) as response:
return response.read().decode('utf-8').strip()
# =========================================================
# ACCIÓN 1 — Base de datos (INSERT histórico de ejecución)
# set_parameterized
# =========================================================
def setEjecucion(params):
"""
Inserta una fila en automatizacion_ejecuciones registrando una ejecución.
params esperados: nombre, descripcion, estado, origen, resultado, error
"""
query = """
INSERT INTO automatizacion_ejecuciones
(nombre, descripcion, estado, origen, resultado, error, fecha_inicio, fecha_fin, activo)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
"""
ahora = timezone.now()
resultado_json = json.dumps(params.get('resultado')) if params.get('resultado') is not None else None
parameter_dict = [
clean_sql_string(params.get('nombre')),
clean_sql_string(params.get('descripcion')),
clean_sql_string(params.get('estado') or 'ok'),
clean_sql_string(params.get('origen') or 'manual'),
resultado_json,
clean_sql_string(params.get('error')) if params.get('error') else None,
ahora,
ahora,
bool(params.get('activo', True)),
]
with connection.cursor() as cursor:
cursor.execute(query, parameter_dict)
row = cursor.fetchone()
nuevo_id = row[0] if row else None
return {'id': nuevo_id, 'fecha': ahora.isoformat()}
# =========================================================
# ACCIÓN 2 — Servicio externo: Cat Fact API
# https://catfact.ninja/fact (gratis, sin auth)
# =========================================================
def getCatFact():
url = 'https://catfact.ninja/fact'
data = _http_get_json(url)
return {
'servicio': 'catfact.ninja',
'url': url,
'fact': data.get('fact'),
'length': data.get('length'),
}
# =========================================================
# ACCIÓN 3 — Servicio externo: GitHub Zen
# https://api.github.com/zen (gratis, sin auth)
# =========================================================
def getGithubZen():
url = 'https://api.github.com/zen'
texto = _http_get_text(url)
return {
'servicio': 'api.github.com/zen',
'url': url,
'zen': texto,
}
# =========================================================
# Orquestador — ejecuta las 3 acciones en secuencia
# =========================================================
def ejecutarAutomatizaciones(params):
"""
Ejecuta las 3 acciones automatizadas:
1) Llamada a servicio externo: catfact.ninja
2) Llamada a servicio externo: api.github.com/zen
3) Persistencia en BD: inserta la ejecución en automatizacion_ejecuciones
"""
resultados = {'acciones': []}
errores = []
# Acción 2 — catfact
try:
resultados['acciones'].append({'step': 1, 'ok': True, 'data': getCatFact()})
except Exception as err:
errores.append(f'catfact: {err}')
resultados['acciones'].append({'step': 1, 'ok': False, 'error': str(err)})
# Acción 3 — github zen
try:
resultados['acciones'].append({'step': 2, 'ok': True, 'data': getGithubZen()})
except Exception as err:
errores.append(f'github_zen: {err}')
resultados['acciones'].append({'step': 2, 'ok': False, 'error': str(err)})
# Acción 1 — persistir en BD
estado_final = 'ok' if not errores else 'error'
nombre = clean_sql_string(params.get('nombre')) or f'Ejecucion {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'
origen = clean_sql_string(params.get('origen')) or 'manual'
registro = setEjecucion({
'nombre': nombre,
'descripcion': params.get('descripcion') or 'Ejecución orquestada por /api/automatizados/ejecutar/',
'estado': estado_final,
'origen': origen,
'resultado': resultados,
'error': '; '.join(errores) if errores else None,
'activo': True,
})
resultados['acciones'].append({'step': 3, 'ok': True, 'data': registro})
return {
'ejecucion_id': registro.get('id'),
'estado': estado_final,
'total_acciones': len(resultados['acciones']),
'resultados': resultados,
'errores': errores,
}
# =========================================================
# Lectura — historial (get_parameterized)
# =========================================================
def getHistorial(params):
"""
Devuelve el histórico de ejecuciones. Permite filtrar por estado y limit.
"""
query = """
SELECT id, nombre, descripcion, estado, origen, resultado, error,
fecha_inicio, fecha_fin, activo
FROM automatizacion_ejecuciones
WHERE (%s = '' OR estado = %s)
ORDER BY fecha_inicio DESC
LIMIT %s
"""
estado = clean_sql_string(params.get('estado')) if params.get('estado') else ''
limite = clean_sql_int(params.get('limit')) or 20
parameter_dict = [estado, estado, limite]
with connection.cursor() as cursor:
cursor.execute(query, parameter_dict)
columns = [col[0] for col in cursor.description]
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
# Normalizamos el campo resultado (JSON serializado en SQLite)
for row in rows:
if isinstance(row.get('resultado'), str):
try:
row['resultado'] = json.loads(row['resultado'])
except (ValueError, TypeError):
pass
return {'total': len(rows), 'data': rows}
# =========================================================
# Estado del módulo
# =========================================================
def getEstado():
"""
Devuelve el estado general del módulo de automatizaciones:
cuántas ejecuciones hay, última ejecución, endpoints disponibles.
"""
query = """
SELECT COUNT(*) AS total,
SUM(CASE WHEN estado = 'ok' THEN 1 ELSE 0 END) AS ok,
SUM(CASE WHEN estado = 'error' THEN 1 ELSE 0 END) AS errores,
MAX(fecha_inicio) AS ultima_ejecucion
FROM automatizacion_ejecuciones
"""
with connection.cursor() as cursor:
cursor.execute(query)
columns = [col[0] for col in cursor.description]
row = cursor.fetchone()
resumen = dict(zip(columns, row)) if row else {}
return {
'status': 'ok',
'modulo': 'automatizados',
'endpoints': [
'POST /api/automatizados/ejecutar/',
'POST /api/automatizados/historial/',
'POST /api/automatizados/estado/',
],
'resumen': resumen,
}

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class AutomatizadosConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'automatizados'

View File

@@ -0,0 +1,47 @@
[
{
"model": "automatizados.automatizacionejecucion",
"pk": 1,
"fields": {
"nombre": "Ejecución inicial de prueba",
"descripcion": "Seed de ejemplo para poblar la tabla de ejecuciones automatizadas",
"estado": "ok",
"origen": "seed",
"resultado": {"acciones": 0, "detalle": "fixture de arranque"},
"error": null,
"fecha_inicio": "2026-04-16T10:00:00Z",
"fecha_fin": "2026-04-16T10:00:05Z",
"activo": true
}
},
{
"model": "automatizados.automatizacionejecucion",
"pk": 2,
"fields": {
"nombre": "Chequeo nocturno",
"descripcion": "Ejemplo de ejecución recurrente programada",
"estado": "ok",
"origen": "jenkins",
"resultado": {"acciones": 3, "detalle": "todas las llamadas OK"},
"error": null,
"fecha_inicio": "2026-04-16T03:00:00Z",
"fecha_fin": "2026-04-16T03:00:12Z",
"activo": true
}
},
{
"model": "automatizados.automatizacionejecucion",
"pk": 3,
"fields": {
"nombre": "Ejecución con error simulado",
"descripcion": "Registro de referencia para estado=error",
"estado": "error",
"origen": "manual",
"resultado": null,
"error": "timeout al llamar servicio externo",
"fecha_inicio": "2026-04-15T18:30:00Z",
"fecha_fin": "2026-04-15T18:30:08Z",
"activo": false
}
}
]

View File

View File

@@ -0,0 +1,25 @@
from django.db import models
class AutomatizacionEjecucion(models.Model):
"""
Registro histórico de ejecuciones automatizadas.
Cada vez que el endpoint `ejecutar/` corre, se guarda una fila con el
resultado consolidado de las acciones ejecutadas.
"""
nombre = models.CharField(max_length=255)
descripcion = models.TextField(null=True, blank=True)
estado = models.CharField(max_length=50, default='pendiente') # pendiente | ok | error
origen = models.CharField(max_length=100, default='manual') # manual | jenkins | cron
resultado = models.JSONField(null=True, blank=True)
error = models.TextField(null=True, blank=True)
fecha_inicio = models.DateTimeField(auto_now_add=True)
fecha_fin = models.DateTimeField(null=True, blank=True)
activo = models.BooleanField(default=True)
class Meta:
db_table = 'automatizacion_ejecuciones'
ordering = ['-fecha_inicio']
def __str__(self):
return f'{self.nombre} [{self.estado}]'

View File

@@ -0,0 +1,8 @@
from django.urls import path
from .views import AutomatizadosEjecutar, AutomatizadosHistorial, AutomatizadosEstado
urlpatterns = [
path('ejecutar/', AutomatizadosEjecutar.as_view(), name='automatizados_ejecutar'),
path('historial/', AutomatizadosHistorial.as_view(), name='automatizados_historial'),
path('estado/', AutomatizadosEstado.as_view(), name='automatizados_estado'),
]

132
app/automatizados/views.py Normal file
View File

@@ -0,0 +1,132 @@
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.authentication import JWTAuthentication
from django.http import JsonResponse
from general.utilidades.acciones import LogService
from .acciones import ejecutarAutomatizaciones, getHistorial, getEstado
class AutomatizadosEjecutar(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
path = '/automatizados/ejecutar/'
# --- BLOQUE 1: Inicio Log ---
log_id = LogService.gestionar_log(self, request, path=path)
try:
# --- BLOQUE 2: Data Cleaning ---
data = request.data
status = 100
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
params = {
'nombre': data.get('nombre'),
'descripcion': data.get('descripcion'),
'origen': data.get('origen') or 'manual',
}
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
status = 400
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)
try:
# --- BLOQUE 3: Action Call ---
resultado = ejecutarAutomatizaciones(params)
response = resultado
status = 200
# --- BLOQUE 4: Cierre Log ---
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, safe=False, status=status)
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
status = 500
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)
class AutomatizadosHistorial(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
path = '/automatizados/historial/'
# --- BLOQUE 1: Inicio Log ---
log_id = LogService.gestionar_log(self, request, path=path)
try:
# --- BLOQUE 2: Data Cleaning ---
data = request.data
status = 100
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
params = {
'estado': data.get('estado'),
'limit': data.get('limit') or 20,
}
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
status = 400
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)
try:
# --- BLOQUE 3: Action Call ---
resultado = getHistorial(params)
response = resultado
status = 200
# --- BLOQUE 4: Cierre Log ---
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, safe=False, status=status)
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
status = 500
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)
class AutomatizadosEstado(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
path = '/automatizados/estado/'
# --- BLOQUE 1: Inicio Log ---
log_id = LogService.gestionar_log(self, request, path=path)
try:
# --- BLOQUE 2: Data Cleaning ---
data = request.data
status = 100
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
status = 400
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)
try:
# --- BLOQUE 3: Action Call ---
resultado = getEstado()
response = resultado
status = 200
# --- BLOQUE 4: Cierre Log ---
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, safe=False, status=status)
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
status = 500
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)

View File

@@ -0,0 +1,34 @@
from django.contrib.auth import authenticate
from rest_framework.authtoken.models import Token
from rest_framework_simplejwt.tokens import RefreshToken
class Admin:
def get_status_action(self):
# Tu lógica de status que ya tenías
return {"status": "ok", "service": "Admin Infrastructure"}
def obtener_token_action(self, params):
"""
Capa Action: Valida credenciales y genera un par de tokens JWT.
"""
username = params.get('username')
password = params.get('password')
# 1. Autenticación
user = authenticate(username=username, password=password)
if user is not None:
# 2. Generación de JWT (Access & Refresh)
refresh = RefreshToken.for_user(user)
return {
'refresh': str(refresh),
'access': str(refresh.access_token),
'user': user.username,
'status': 'success'
}
return None

View File

@@ -0,0 +1,35 @@
# Generated by Django 5.0.3 on 2026-04-14 23:15
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Log',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('user_id', models.IntegerField(default=0)),
('user', models.CharField(default='anonimo', max_length=255)),
('app_id', models.IntegerField(default=0)),
('remote_address', models.GenericIPAddressField(blank=True, null=True)),
('request', models.JSONField(blank=True, null=True)),
('response', models.JSONField(blank=True, null=True)),
('status_code', models.CharField(default='0', max_length=10)),
('path', models.CharField(max_length=255)),
('method', models.CharField(max_length=10)),
('createdAt', models.DateTimeField(default=django.utils.timezone.now)),
('updatedAt', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'audit_logs',
},
),
]

View File

@@ -0,0 +1 @@
# Archivo para marcar esta carpeta como paquete de migraciones

View File

@@ -0,0 +1,26 @@
from django.db import models
from django.utils import timezone
class Log(models.Model):
# Usamos BigAutoField para el BIGINT id de tu tabla
id = models.BigAutoField(primary_key=True)
user_id = models.IntegerField(default=0)
user = models.CharField(max_length=255, default='anonimo')
app_id = models.IntegerField(default=0)
# GenericIPAddressField para el tipo INET de Postgres
remote_address = models.GenericIPAddressField(null=True, blank=True)
request = models.JSONField(null=True, blank=True) # Para JSONB
response = models.JSONField(null=True, blank=True) # Para JSONB
status_code = models.CharField(max_length=10, default='0')
path = models.CharField(max_length=255)
method = models.CharField(max_length=10)
createdAt = models.DateTimeField(default=timezone.now)
updatedAt = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'audit_logs'
# managed = True <-- Asegúrate de que no esté en False
def __str__(self):
return f"{self.method} {self.path} ({self.status_code})"

View File

@@ -0,0 +1,7 @@
from django.urls import path
from .views import status_view
urlpatterns = [
# Ruta final: /admin/status/
path('status/', status_view, name='admin_status'),
]

View File

@@ -0,0 +1,95 @@
from django.http import JsonResponse
from .acciones import Admin
import logging
import json
from django.views.decorators.csrf import csrf_exempt
from django.utils import timezone
from .models import Log
logger = logging.getLogger(__name__)
def status_view(request):
# BLOQUE 1: Log de iniciación
logger.info("INICIO - Ejecutando Health Check de Administración.")
# BLOQUE 2: Limpieza y validación de datos
# Para un status simple, el diccionario de limpieza está vacío
data_cleaned = {}
# BLOQUE 3: Llamada a la acción
try:
# Instanciamos la clase Admin y llamamos al método
admin_logic = Admin()
response_data = admin_logic.get_status_action()
status_code = 200
except Exception as e:
logger.error(f"ERROR - Fallo en get_status_action: {str(e)}")
response_data = {"status": "error", "message": "Internal Server Error"}
status_code = 500
# BLOQUE 4: Log de cierre y retorno
logger.info(f"FIN - Health Check completado. Status: {status_code}")
return JsonResponse(response_data, status=status_code)
@csrf_exempt
@staticmethod
def api_token(request):
"""
Endpoint: api/token/
Patrón: 4 bloques con persistencia en Log DB.
"""
# --- BLOQUE 1: LOG INITIATION ---
logger.info("INICIO - Petición de JWT (api/token/)")
# Iniciamos el registro en la base de datos (Estándar compañeros)
log_entry = Log.objects.create(
user='anonimo',
path='api/token/',
method='POST',
createdAt=timezone.now(),
status_code='0'
)
try:
if request.method == 'POST':
# --- BLOQUE 2: DATA CLEANING ---
body_data = json.loads(request.body)
log_entry.request = body_data # Guardamos lo que entró
log_entry.save()
params = {
'username': body_data.get('username'),
'password': body_data.get('password')
}
# --- BLOQUE 3: ACTION CALL ---
admin_logic = Admin()
resultado = admin_logic.obtener_token_action(params)
# --- BLOQUE 4: LOG CLOSURE & RESPONSE ---
if resultado:
status = 200
log_entry.user = resultado['user']
log_entry.response = resultado
log_entry.status_code = str(status)
log_entry.updatedAt = timezone.now()
log_entry.save()
logger.info(f"FIN - JWT generado para: {log_entry.user}")
return JsonResponse(resultado, status=status)
else:
status = 401
response_error = {"error": "Credenciales inválidas"}
log_entry.status_code = str(status)
log_entry.response = response_error
log_entry.save()
return JsonResponse(response_error, status=status)
except Exception as e:
logger.error(f"ERROR CRÍTICO en api_token: {str(e)}")
log_entry.status_code = '500'
log_entry.response = {'error': str(e)}
log_entry.save()
return JsonResponse({'error': 'Error interno'}, status=500)

View File

@@ -1,5 +1,6 @@
# apps/common/apps.py
from django.apps import AppConfig from django.apps import AppConfig
class CommonConfig(AppConfig): class CommonConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.common' name = 'common'

View File

@@ -0,0 +1 @@
# Archivo para marcar esta carpeta como paquete de migraciones

0
app/data/.gitkeep Normal file
View File

0
app/general/__init__.py Normal file
View File

1
app/general/admin.py Normal file
View File

@@ -0,0 +1 @@
from django.contrib import admin

6
app/general/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class GeneralConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'general'

21
app/general/exception.py Normal file
View File

@@ -0,0 +1,21 @@
from rest_framework.views import exception_handler
from rest_framework.response import Response
def custom_exception_handler(exc, context):
"""
Handler global de excepciones para DRF.
Añadir en settings.py:
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'general.exception.custom_exception_handler',
}
"""
response = exception_handler(exc, context)
if response is not None:
response.data = {
'body': {'data': [], 'error': response.data},
'mensaje': str(exc),
}
return response

View File

5
app/general/models.py Normal file
View File

@@ -0,0 +1,5 @@
from django.db import models
# La app general no define modelos propios.
# El modelo Log vive en backend_admin.models y se accede desde
# general.utilidades.acciones.LogService.

24
app/general/request.py Normal file
View File

@@ -0,0 +1,24 @@
import json
from django.http import JsonResponse
def parse_body(request):
"""
Parsea el body de la request como JSON.
Lanza ValueError si el body está vacío o no es JSON válido.
"""
raw = request.body
if not raw:
raise ValueError('El body de la petición está vacío.')
return json.loads(raw)
def build_error_response(message, status=400, data=None):
"""Construye una respuesta de error en el formato estándar del proyecto."""
body = {'data': data if data is not None else [], 'error': message}
return JsonResponse({'body': body, 'mensaje': message}, status=status, safe=False)
def build_success_response(data, status=200):
"""Construye una respuesta de éxito en el formato estándar del proyecto."""
return JsonResponse(data, status=status, safe=False)

View File

@@ -0,0 +1,6 @@
from rest_framework import serializers
class EmptySerializer(serializers.Serializer):
"""Serializer base vacío para endpoints sin body estructurado."""
pass

7
app/general/tests.py Normal file
View File

@@ -0,0 +1,7 @@
from django.test import TestCase
class LogServiceTest(TestCase):
def test_placeholder(self):
"""Placeholder: añadir tests de LogService aquí."""
pass

6
app/general/urls.py Normal file
View File

@@ -0,0 +1,6 @@
from django.urls import path
from .views import status_view
urlpatterns = [
path('status/', status_view, name='general_status'),
]

View File

View File

@@ -0,0 +1,114 @@
import json
import logging
from django.utils import timezone
from django.core.serializers.json import DjangoJSONEncoder
from . import utils
logger = logging.getLogger(__name__)
def _json_safe(value):
"""
Convierte cualquier valor a un tipo seguro para JSONField.
Usa DjangoJSONEncoder para manejar date, datetime, Decimal, UUID, etc.
"""
if value is None or value == '':
return value
try:
return json.loads(json.dumps(value, cls=DjangoJSONEncoder))
except Exception:
return str(value)
class LogService:
"""
Servicio centralizado de gestión de logs.
Única fuente de inserción en la tabla audit_logs.
Uso en vistas:
log_id = LogService.gestionar_log(self, request, path='/mi/path/')
LogService.gestionar_log(self, request, log_id=log_id, body_request=data, status_code=200)
Si la BD no está disponible, el log falla silenciosamente para no
interrumpir la petición del usuario.
"""
def gestionar_log(
self,
request,
log_id=None,
path=None,
user=None,
body_request=None,
body_response=None,
status_code=None,
):
try:
return LogService._ejecutar_log(
self, request, log_id, path, user, body_request, body_response, status_code
)
except Exception as e:
logger.warning('LogService.gestionar_log falló (log omitido): %s', str(e))
return log_id # Devuelve el log_id existente o None si era creación
@staticmethod
def _ejecutar_log(caller, request, log_id, path, user, body_request, body_response, status_code):
# Importación diferida para evitar problemas de arranque si la BD no está lista
from backend_admin.models import Log
# Determinar la app llamante a partir del módulo de la vista
modulo = caller.__class__.__module__
app_nombre = modulo.split('.')[0]
tag_header = request.headers.get('tag')
if tag_header:
usuario_final = tag_header
elif app_nombre:
apps_automatizadas = ['automatizados']
if app_nombre.lower() in apps_automatizadas:
usuario_final = 'Jenkins'
else:
usuario_final = app_nombre
else:
usuario_final = user if user else 'orquestador'
if log_id is None:
# --- CREACIÓN: primer registro del ciclo de vida de la petición ---
path_final = path if path else request.path
nuevo_log = Log.objects.create(
user_id=0,
user=usuario_final,
app_id=0,
remote_address=utils.get_client_ip(request),
request='',
response='',
status_code=status_code if status_code else '0',
path=path_final,
method=request.method,
createdAt=timezone.now(),
)
return nuevo_log.pk
else:
# --- ACTUALIZACIÓN: enriquece el log con datos del procesamiento ---
datos_a_actualizar = {
'updatedAt': timezone.now(),
}
if user:
datos_a_actualizar['user'] = user
if path:
datos_a_actualizar['path'] = path
if body_request is not None:
# _json_safe convierte dates, Decimals, etc. a tipos JSON válidos
datos_a_actualizar['request'] = _json_safe(body_request)
if body_response is not None:
datos_a_actualizar['response'] = _json_safe(body_response)
if status_code is not None:
datos_a_actualizar['status_code'] = str(status_code)
Log.objects.filter(pk=log_id).update(**datos_a_actualizar)
return log_id

View File

@@ -0,0 +1,33 @@
class ValidationError(Exception):
"""Error de validación de datos de entrada."""
def __init__(self, message, field=None):
self.message = message
self.field = field
super().__init__(message)
def to_response(self):
detail = {'error': self.message}
if self.field:
detail['field'] = self.field
return {'body': {'data': [], **detail}, 'mensaje': self.message}
class ExternalServiceError(Exception):
"""Error en la comunicación con un servicio externo."""
def __init__(self, message, service=None):
self.message = message
self.service = service
super().__init__(message)
def to_response(self):
return {'body': {'data': [], 'error': self.message}, 'mensaje': self.message}
class NotFoundError(Exception):
"""Recurso no encontrado."""
def __init__(self, message='Recurso no encontrado'):
self.message = message
super().__init__(message)
def to_response(self):
return {'body': {'data': [], 'error': self.message}, 'mensaje': self.message}

View File

@@ -0,0 +1,6 @@
def get_client_ip(request):
"""Obtiene la IP real del cliente, considerando proxies."""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR')

View File

5
app/general/views.py Normal file
View File

@@ -0,0 +1,5 @@
from django.http import JsonResponse
def status_view(request):
return JsonResponse({'status': 'ok', 'service': 'general'})

View File

@@ -6,7 +6,7 @@ import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
# Apuntamos a la configuración dentro de la carpeta 'core' # Apuntamos a la configuración dentro de la carpeta 'core'
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_config.settings')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:

View File

@@ -1,5 +1,12 @@
from django.db import connection from django.db import connection
from apps.common.utils import clean_sql_string, clean_sql_int from common.utils import clean_sql_string, clean_sql_int
def get_status_action():
"""
Acción simple para comprobar que la lógica de la API responde.
"""
return {"status": "ok", "message": "API is running"}
def getData(params): def getData(params):
""" """

View File

@@ -2,4 +2,4 @@ from django.apps import AppConfig
class PromocionesConfig(AppConfig): class PromocionesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.promociones' name = 'promociones'

View File

@@ -0,0 +1,35 @@
[
{
"model": "promociones.promocion",
"pk": 1,
"fields": {
"nombre": "Oferta de Bienvenida",
"fecha_inicio": "2026-04-01",
"descripcion": "Descuento para nuevos usuarios",
"activo": true,
"categoria_id": 1
}
},
{
"model": "promociones.promocion",
"pk": 2,
"fields": {
"nombre": "Promo Primavera",
"fecha_inicio": "2026-05-01",
"descripcion": "Todo al 20% de descuento",
"activo": true,
"categoria_id": 2
}
},
{
"model": "promociones.promocion",
"pk": 3,
"fields": {
"nombre": "Liquidación Stock",
"fecha_inicio": "2026-04-14",
"descripcion": "Últimas unidades",
"activo": false,
"categoria_id": 1
}
}
]

View File

@@ -0,0 +1 @@
# Archivo para marcar esta carpeta como paquete de migraciones

6
app/promociones/urls.py Normal file
View File

@@ -0,0 +1,6 @@
from django.urls import path
from .views import PromocionObtener
urlpatterns = [
path('obtener/', PromocionObtener.as_view(), name='obtener_promocion'),
]

46
app/promociones/views.py Normal file
View File

@@ -0,0 +1,46 @@
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.authentication import JWTAuthentication
from django.http import JsonResponse
from general.utilidades.acciones import LogService
from .acciones import getData
class PromocionObtener(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
path = '/promociones/obtener/'
# --- LOG: inicio de la petición ---
log_id = LogService.gestionar_log(self, request, path=path)
try:
# --- BLOQUE 2: limpieza y validación del body ---
data = request.data
status = 100
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
params = {'id': data.get('id'), 'activo': data.get('activo')}
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
status = 400
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)
try:
# --- BLOQUE 3: llamada a la acción ---
resultado = getData(params)
response = resultado
status = 200
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, safe=False, status=status)
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
status = 500
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)

7
app/requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
Django==5.0.3
psycopg2-binary==2.9.9
gunicorn==21.2.0
python-dotenv==1.0.1
djangorestframework
django-cors-headers
djangorestframework-simplejwt

View File

@@ -1,7 +0,0 @@
from django.urls import path
from .views import get_promocion_view
urlpatterns = [
# Capa 1: Definición del endpoint
path('obtener/', get_promocion_view, name='get_promocion'),
]

View File

@@ -1,54 +0,0 @@
import logging
from django.http import JsonResponse
from .actions import getData
# Configuración del logger para rastrear la ejecución
logger = logging.getLogger(__name__)
def get_promocion_view(request):
"""
Vista estandarizada para la obtención de una promoción.
"""
# ---------------------------------------------------------
# BLOQUE 1: Log de iniciación
# ---------------------------------------------------------
logger.info("[START] Iniciando ejecución de get_promocion_view")
try:
# ---------------------------------------------------------
# BLOQUE 2: Limpieza de datos (Data Cleaning)
# ---------------------------------------------------------
# Extraemos los parámetros del request y preparamos el diccionario
raw_data = request.GET.dict()
# Aquí es donde ella aplicaría validaciones adicionales si fuera necesario
clean_params = {
'id': raw_data.get('id'),
'activo': raw_data.get('activo', True) # Valor por defecto
}
# ---------------------------------------------------------
# BLOQUE 3: Llamada a la Action (Execution)
# ---------------------------------------------------------
# La lógica de SQL y parametrización vive dentro de esta llamada
resultado_db = getData(clean_params)
# ---------------------------------------------------------
# BLOQUE 4: Log de cierre y respuesta (Closure)
# ---------------------------------------------------------
logger.info(f"[SUCCESS] get_promocion_view finalizada. Registros encontrados: {len(resultado_db)}")
return JsonResponse({
'status': 'success',
'data': resultado_db
}, status=200)
except Exception as e:
# Log de error detallado en caso de fallo
logger.error(f"[ERROR] Fallo crítico en get_promocion_view: {str(e)}")
return JsonResponse({
'status': 'error',
'message': 'Error interno del servidor'
}, status=500)

View File

@@ -1,113 +0,0 @@
from dotenv import load_dotenv
import os
from pathlib import Path
# Cargar variables desde el archivo .env
load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-change-me-for-production')
DEBUG = os.getenv('DEBUG', 'True').lower() == 'true'
ALLOWED_HOSTS = [
'v-encore-lab.com',
'dev.v-encore-lab.com',
'185.187.169.109', # Añade la IP aquí
'localhost',
'127.0.0.1',
'django_app_dev',
'django_app_master'
]
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'apps.promociones',
'apps.common',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
# Para desarrollo local: SQLite
if DEBUG:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
else:
# Producción: PostgreSQL desde .env
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME'),
'USER': os.getenv('DB_USER'),
'PASSWORD': os.getenv('DB_PASSWORD'),
'HOST': os.getenv('DB_HOST'),
'PORT': os.getenv('DB_PORT'),
}
}
# Internationalization
LANGUAGE_CODE = 'es-es'
TIME_ZONE = 'Europe/Madrid'
USE_I18N = True
USE_TZ = True
# Static files
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Logging básico
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'': {
'handlers': ['console'],
'level': 'INFO',
},
},
}

View File

@@ -1,6 +0,0 @@
from django.urls import path, include
urlpatterns = [
# Redirigimos todas las peticiones de /api/promociones/ a nuestra app
path('api/promociones/', include('apps.promociones.urls')),
]

0
data/.gitkeep Normal file
View File

Binary file not shown.

34
deployments/.env.example Normal file
View File

@@ -0,0 +1,34 @@
# =================================================================
# 🏗️ INFRAESTRUCTURA (Docker & Jenkins)
# =================================================================
# Nombres que tomarán los contenedores en Docker
APP_CONTAINER_NAME=
DB_CONTAINER_NAME=
# Nombre del proyecto para Docker Compose (usado en Jenkins)
PROJECT_NAME=
# Puertos que se abrirán al exterior (Host)
PORT=
DATABASE_EXPOSE_PORT=
# =================================================================
# 🐍 CONFIGURACIÓN DE DJANGO
# =================================================================
# True para desarrollo, False para producción
DEBUG=
# Genera una clave segura: https://djecrety.ir/
SECRET_KEY=
# Lista separada por comas: localhost,127.0.0.1,tudominio.com
ALLOWED_HOSTS=
# =================================================================
# 🗄️ BASE DE DATOS (PostgreSQL)
# =================================================================
DB_NAME=
DB_USER=
DB_PASSWORD=
# El HOST debe ser "db" cuando se usa Docker Compose
DB_HOST=
DB_PORT=

View File

@@ -1,29 +1,36 @@
# Usamos una imagen ligera de Python # 1. Usamos una imagen ligera de Python
FROM python:3.12-slim FROM python:3.12-slim
# Evitar que Python genere archivos .pyc y que el buffer se sature # 2. Evitar archivos .pyc y saturación del buffer
ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
ENV TZ=Europe/Madrid
# Directorio de trabajo # 3. Directorio de trabajo interno del contenedor
WORKDIR /app WORKDIR /app
# Instalar dependencias del sistema necesarias # 4. Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
libpq-dev \ libpq-dev \
gcc \ gcc \
gettext \ gettext \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Instalar dependencias de Python # 5. COPIAR REQUISITOS
COPY requirements.txt /app/ # OJO: Ahora el contexto es la RAÍZ, así que el archivo está en deployments/
COPY deployments/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Copiar el resto del código # 6. COPIAR EL CÓDIGO
COPY . /app/ COPY . .
# Exponer el puerto de Django # Cambiamos al directorio de la app donde está manage.py
WORKDIR /app/app
# 7. EXPOSICIÓN Y SCRIPT DE ENTRADA
EXPOSE 8000 EXPOSE 8000
# Comando por defecto para arrancar (usaremos manage.py en dev y gunicorn en prod) COPY deployments/entrypoint.sh /entrypoint.sh
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -5,37 +5,58 @@ pipeline {
stage('Configurar Entorno') { stage('Configurar Entorno') {
steps { steps {
script { script {
// Selección de configuración según la rama
if (env.BRANCH_NAME == 'master') { if (env.BRANCH_NAME == 'master') {
env.PROJECT_NAME = "django_master" env.PROJECT_NAME = "django_master"
env.CONTAINER_NAME = "django_app_master" env.APP_CONTAINER_NAME = "django_app_master"
env.PORT = "8001" env.PORT = "8001"
env.DEBUG_MODE = "0" env.DEBUG_MODE = "0"
} else { env.ENV_CREDENTIAL_ID = "2"
} else if (env.BRANCH_NAME == 'dev') {
env.PROJECT_NAME = "django_dev" env.PROJECT_NAME = "django_dev"
env.CONTAINER_NAME = "django_app_dev" env.APP_CONTAINER_NAME = "django_app_dev"
env.PORT = "8000" env.PORT = "8000"
env.DEBUG_MODE = "1" env.DEBUG_MODE = "1"
env.ENV_CREDENTIAL_ID = "1"
} }
} }
} }
} }
stage('Despliegue') { stage('Fase Final: Containerización') {
when { anyOf { branch 'dev'; branch 'master' } } when { anyOf { branch 'dev'; branch 'master' } }
steps { steps {
echo "DESPLEGANDO: ${env.CONTAINER_NAME} en el puerto ${env.PORT}" withCredentials([file(credentialsId: env.ENV_CREDENTIAL_ID, variable: 'SECRET_ENV')]) {
sh """
// Usamos docker-compose con guion para asegurar compatibilidad echo "--> Preparando configuración segura..."
sh """ cp \$SECRET_ENV deployments/.env
CONTAINER_NAME=${env.CONTAINER_NAME} \
PORT=${env.PORT} \ echo "--> 🚀 DESPLEGANDO PROYECTO: ${env.PROJECT_NAME}"
DEBUG_MODE=${env.DEBUG_MODE} \
docker-compose -p ${env.PROJECT_NAME} -f deployments/docker-compose.yml up -d --build web # 1. Limpieza de contenedores previos para evitar conflictos de nombres
""" docker compose -p ${env.PROJECT_NAME} -f deployments/docker-compose.yml down --remove-orphans
echo "Ejecutando migraciones en ${env.CONTAINER_NAME}..." # 2. Despliegue forzando la lectura del archivo .env específico
sh "docker exec ${env.CONTAINER_NAME} python manage.py migrate --noinput" # CRÍTICO: --env-file asegura que DATABASE_EXPOSE_PORT se lea correctamente
docker compose \
-p ${env.PROJECT_NAME} \
-f deployments/docker-compose.yml \
--env-file deployments/.env \
up -d --build web
"""
}
} }
} }
} }
post {
success {
echo "✅ Despliegue de ${env.BRANCH_NAME} completado con éxito."
sh "rm -f deployments/.env"
}
failure {
echo "❌ Error en el despliegue de ${env.BRANCH_NAME}."
sh "rm -f deployments/.env"
}
}
} }

View File

@@ -1,25 +1,41 @@
version: '3.8'
services: services:
db:
image: postgres:15-alpine
container_name: ${DB_CONTAINER_NAME:-django_core_db}
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-django_core_db}
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "${DATABASE_EXPOSE_PORT:-5432}:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-django_core_db}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
web: web:
build: build:
context: .. context: ..
dockerfile: deployments/Dockerfile dockerfile: deployments/Dockerfile
container_name: ${CONTAINER_NAME} container_name: ${APP_CONTAINER_NAME:-django_core_app}
restart: always restart: unless-stopped
working_dir: /app # <--- Vital para que encuentre 'core' env_file:
- .env
environment: environment:
- DEBUG=${DEBUG_MODE} - DB_HOST=db
- PYTHONPATH=/app # <--- Asegura que Python vea las carpetas
- DB_NAME=gitea
- DB_USER=gitea
- DB_PASSWORD=gitea
- DB_HOST=gitea-db-1
- DB_PORT=5432 - DB_PORT=5432
networks:
- gitea_net
ports: ports:
- "${PORT}:8000" - "${PORT:-8000}:8000"
depends_on:
db:
condition: service_healthy
networks: volumes:
gitea_net: postgres_data:
external: true
name: frontend

41
deployments/entrypoint.sh Normal file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
# Salir inmediatamente si un comando falla
set -e
# --- Esperar a PostgreSQL si estamos en modo BD remota ---
if [ -n "$DB_HOST" ]; then
echo "--> Esperando a PostgreSQL en $DB_HOST:${DB_PORT:-5432}..."
until python -c "
import sys, psycopg2, os
try:
psycopg2.connect(
host=os.environ['DB_HOST'],
port=os.environ.get('DB_PORT', 5432),
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD'],
dbname=os.environ['DB_NAME']
)
sys.exit(0)
except Exception:
sys.exit(1)
" 2>/dev/null; do
echo " PostgreSQL no disponible, reintentando en 2s..."
sleep 2
done
echo "--> PostgreSQL listo."
fi
echo "--> Ejecutando migraciones..."
python manage.py migrate --noinput
echo "--> Cargando semillas (si existen)..."
python manage.py loaddata semillas 2>/dev/null || echo " Sin semillas, continuando."
echo "--> Arrancando servidor con Gunicorn..."
exec gunicorn api_config.wsgi:application \
--bind 0.0.0.0:8000 \
--workers 2 \
--timeout 120 \
--access-logfile - \
--error-logfile -

View File

@@ -1,2 +1,7 @@
Django==5.0.3 Django==5.0.3
psycopg2-binary==2. psycopg2-binary==2.9.9
gunicorn==21.2.0
python-dotenv==1.0.1
djangorestframework
django-cors-headers
djangorestframework-simplejwt

12
frontend/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Django Core Base</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

16
frontend/node_modules/.bin/nanoid generated vendored Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../nanoid/bin/nanoid.cjs" "$@"
else
exec node "$basedir/../nanoid/bin/nanoid.cjs" "$@"
fi

17
frontend/node_modules/.bin/nanoid.cmd generated vendored Normal file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\nanoid\bin\nanoid.cjs" %*

28
frontend/node_modules/.bin/nanoid.ps1 generated vendored Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
} else {
& "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
} else {
& "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
frontend/node_modules/.bin/rolldown generated vendored Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../rolldown/bin/cli.mjs" "$@"
else
exec node "$basedir/../rolldown/bin/cli.mjs" "$@"
fi

17
frontend/node_modules/.bin/rolldown.cmd generated vendored Normal file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rolldown\bin\cli.mjs" %*

28
frontend/node_modules/.bin/rolldown.ps1 generated vendored Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../rolldown/bin/cli.mjs" $args
} else {
& "$basedir/node$exe" "$basedir/../rolldown/bin/cli.mjs" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../rolldown/bin/cli.mjs" $args
} else {
& "node$exe" "$basedir/../rolldown/bin/cli.mjs" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
frontend/node_modules/.bin/tsc generated vendored Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
else
exec node "$basedir/../typescript/bin/tsc" "$@"
fi

17
frontend/node_modules/.bin/tsc.cmd generated vendored Normal file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\typescript\bin\tsc" %*

28
frontend/node_modules/.bin/tsc.ps1 generated vendored Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
} else {
& "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../typescript/bin/tsc" $args
} else {
& "node$exe" "$basedir/../typescript/bin/tsc" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
frontend/node_modules/.bin/tsserver generated vendored Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
else
exec node "$basedir/../typescript/bin/tsserver" "$@"
fi

17
frontend/node_modules/.bin/tsserver.cmd generated vendored Normal file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\typescript\bin\tsserver" %*

28
frontend/node_modules/.bin/tsserver.ps1 generated vendored Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../typescript/bin/tsserver" $args
} else {
& "$basedir/node$exe" "$basedir/../typescript/bin/tsserver" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../typescript/bin/tsserver" $args
} else {
& "node$exe" "$basedir/../typescript/bin/tsserver" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
frontend/node_modules/.bin/vite generated vendored Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
else
exec node "$basedir/../vite/bin/vite.js" "$@"
fi

17
frontend/node_modules/.bin/vite.cmd generated vendored Normal file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\vite\bin\vite.js" %*

28
frontend/node_modules/.bin/vite.ps1 generated vendored Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args
} else {
& "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../vite/bin/vite.js" $args
} else {
& "node$exe" "$basedir/../vite/bin/vite.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

536
frontend/node_modules/.package-lock.json generated vendored Normal file
View File

@@ -0,0 +1,536 @@
{
"name": "frontend",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@oxc-project/types": {
"version": "0.124.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@reduxjs/toolkit": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
"immer": "^11.0.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
"integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.7",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
"integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
"dev": true,
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-react": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
"integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rolldown/pluginutils": "1.0.0-rc.7"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
"babel-plugin-react-compiler": "^1.0.0",
"vite": "^8.0.0"
},
"peerDependenciesMeta": {
"@rolldown/plugin-babel": {
"optional": true
},
"babel-plugin-react-compiler": {
"optional": true
}
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/immer": {
"version": "11.1.4",
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
"integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.32.0",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-darwin-x64": "1.32.0",
"lightningcss-freebsd-x64": "1.32.0",
"lightningcss-linux-arm-gnueabihf": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-arm64-musl": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0",
"lightningcss-linux-x64-musl": "1.32.0",
"lightningcss-win32-arm64-msvc": "1.32.0",
"lightningcss-win32-x64-msvc": "1.32.0"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/react": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.5"
}
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"license": "MIT",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/rolldown": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
"integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.124.0",
"@rolldown/pluginutils": "1.0.0-rc.15"
},
"bin": {
"rolldown": "bin/cli.mjs"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.15",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
"@rolldown/binding-darwin-x64": "1.0.0-rc.15",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
}
},
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
"dev": true,
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/typescript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/vite": {
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.8",
"rolldown": "1.0.0-rc.15",
"tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.0",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
"sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"@vitejs/devtools": {
"optional": true
},
"esbuild": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
}
}
}

22
frontend/node_modules/@oxc-project/types/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2024-present VoidZero Inc. & Contributors
Copyright (c) 2023 Boshen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
frontend/node_modules/@oxc-project/types/README.md generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Oxc Types
Typescript definitions for Oxc AST nodes.

26
frontend/node_modules/@oxc-project/types/package.json generated vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "@oxc-project/types",
"version": "0.124.0",
"description": "Types for Oxc AST nodes",
"keywords": [
"AST",
"Parser"
],
"homepage": "https://oxc.rs",
"bugs": "https://github.com/oxc-project/oxc/issues",
"license": "MIT",
"author": "Boshen and oxc contributors",
"repository": {
"type": "git",
"url": "git+https://github.com/oxc-project/oxc.git",
"directory": "npm/oxc-types"
},
"funding": {
"url": "https://github.com/sponsors/Boshen"
},
"files": [
"types.d.ts"
],
"type": "module",
"types": "types.d.ts"
}

1912
frontend/node_modules/@oxc-project/types/types.d.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

21
frontend/node_modules/@reduxjs/toolkit/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Mark Erikson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

110
frontend/node_modules/@reduxjs/toolkit/README.md generated vendored Normal file
View File

@@ -0,0 +1,110 @@
# Redux Toolkit
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/reduxjs/redux-toolkit/tests.yml?style=flat-square)
[![npm version](https://img.shields.io/npm/v/@reduxjs/toolkit.svg?style=flat-square)](https://www.npmjs.com/package/@reduxjs/toolkit)
[![npm downloads](https://img.shields.io/npm/dm/@reduxjs/toolkit.svg?style=flat-square&label=RTK+downloads)](https://www.npmjs.com/package/@reduxjs/toolkit)
**The official, opinionated, batteries-included toolset for efficient Redux development**
## Installation
### Create a React Redux App
The recommended way to start new apps with React and Redux Toolkit is by using [our official Redux Toolkit + TS template for Vite](https://github.com/reduxjs/redux-templates), or by creating a new Next.js project using [Next's `with-redux` template](https://github.com/vercel/next.js/tree/canary/examples/with-redux).
Both of these already have Redux Toolkit and React-Redux configured appropriately for that build tool, and come with a small example app that demonstrates how to use several of Redux Toolkit's features.
```bash
# Vite with our Redux+TS template
# (using the `degit` tool to clone and extract the template)
npx degit reduxjs/redux-templates/packages/vite-template-redux my-app
# Next.js using the `with-redux` template
npx create-next-app --example with-redux my-app
```
We do not currently have official React Native templates, but recommend these templates for standard React Native and for Expo:
- https://github.com/rahsheen/react-native-template-redux-typescript
- https://github.com/rahsheen/expo-template-redux-typescript
### An Existing App
Redux Toolkit is available as a package on NPM for use with a module bundler or in a Node application:
```bash
# NPM
npm install @reduxjs/toolkit
# Yarn
yarn add @reduxjs/toolkit
```
The package includes a precompiled ESM build that can be used as a [`<script type="module">` tag](https://unpkg.com/@reduxjs/toolkit/dist/redux-toolkit.browser.mjs) directly in the browser.
## Documentation
The Redux Toolkit docs are available at **https://redux-toolkit.js.org**, including API references and usage guides for all of the APIs included in Redux Toolkit.
The Redux core docs at https://redux.js.org includes the full Redux tutorials, as well usage guides on general Redux patterns.
## Purpose
The **Redux Toolkit** package is intended to be the standard way to write Redux logic. It was originally created to help address three common concerns about Redux:
- "Configuring a Redux store is too complicated"
- "I have to add a lot of packages to get Redux to do anything useful"
- "Redux requires too much boilerplate code"
We can't solve every use case, but in the spirit of [`create-react-app`](https://github.com/facebook/create-react-app), we can try to provide some tools that abstract over the setup process and handle the most common use cases, as well as include some useful utilities that will let the user simplify their application code.
Because of that, this package is deliberately limited in scope. It does _not_ address concepts like "reusable encapsulated Redux modules", folder or file structures, managing entity relationships in the store, and so on.
Redux Toolkit also includes a powerful data fetching and caching capability that we've dubbed "RTK Query". It's included in the package as a separate set of entry points. It's optional, but can eliminate the need to hand-write data fetching logic yourself.
## What's Included
Redux Toolkit includes these APIs:
- `configureStore()`: wraps `createStore` to provide simplified configuration options and good defaults. It can automatically combine your slice reducers, add whatever Redux middleware you supply, includes `redux-thunk` by default, and enables use of the Redux DevTools Extension.
- `createReducer()`: lets you supply a lookup table of action types to case reducer functions, rather than writing switch statements. In addition, it automatically uses the [`immer` library](https://github.com/mweststrate/immer) to let you write simpler immutable updates with normal mutative code, like `state.todos[3].completed = true`.
- `createAction()`: generates an action creator function for the given action type string. The function itself has `toString()` defined, so that it can be used in place of the type constant.
- `createSlice()`: combines `createReducer()` + `createAction()`. Accepts an object of reducer functions, a slice name, and an initial state value, and automatically generates a slice reducer with corresponding action creators and action types.
- `combineSlices()`: combines multiple slices into a single reducer, and allows "lazy loading" of slices after initialisation.
- `createListenerMiddleware()`: lets you define "listener" entries that contain an "effect" callback with additional logic, and a way to specify when that callback should run based on dispatched actions or state changes. A lightweight alternative to Redux async middleware like sagas and observables.
- `createAsyncThunk()`: accepts an action type string and a function that returns a promise, and generates a thunk that dispatches `pending/resolved/rejected` action types based on that promise
- `createEntityAdapter()`: generates a set of reusable reducers and selectors to manage normalized data in the store
- The `createSelector()` utility from the [Reselect](https://github.com/reduxjs/reselect) library, re-exported for ease of use.
For details, see [the Redux Toolkit API Reference section in the docs](https://redux-toolkit.js.org/api/configureStore).
## RTK Query
**RTK Query** is provided as an optional addon within the `@reduxjs/toolkit` package. It is purpose-built to solve the use case of data fetching and caching, supplying a compact, but powerful toolset to define an API interface layer for your app. It is intended to simplify common cases for loading data in a web application, eliminating the need to hand-write data fetching & caching logic yourself.
RTK Query is built on top of the Redux Toolkit core for its implementation, using [Redux](https://redux.js.org/) internally for its architecture. Although knowledge of Redux and RTK are not required to use RTK Query, you should explore all of the additional global store management capabilities they provide, as well as installing the [Redux DevTools browser extension](https://github.com/reduxjs/redux-devtools), which works flawlessly with RTK Query to traverse and replay a timeline of your request & cache behavior.
RTK Query is included within the installation of the core Redux Toolkit package. It is available via either of the two entry points below:
```ts no-transpile
import { createApi } from '@reduxjs/toolkit/query'
/* React-specific entry point that automatically generates
hooks corresponding to the defined endpoints */
import { createApi } from '@reduxjs/toolkit/query/react'
```
### What's included
RTK Query includes these APIs:
- `createApi()`: The core of RTK Query's functionality. It allows you to define a set of endpoints describe how to retrieve data from a series of endpoints, including configuration of how to fetch and transform that data. In most cases, you should use this once per app, with "one API slice per base URL" as a rule of thumb.
- `fetchBaseQuery()`: A small wrapper around fetch that aims to simplify requests. Intended as the recommended baseQuery to be used in createApi for the majority of users.
- `<ApiProvider />`: Can be used as a Provider if you do not already have a Redux store.
- `setupListeners()`: A utility used to enable refetchOnMount and refetchOnReconnect behaviors.
See the [**RTK Query Overview**](https://redux-toolkit.js.org/rtk-query/overview) page for more details on what RTK Query is, what problems it solves, and how to use it.
## Contributing
Please refer to our [contributing guide](/CONTRIBUTING.md) to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to Redux Toolkit.

View File

@@ -0,0 +1,6 @@
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./redux-toolkit.production.min.cjs')
} else {
module.exports = require('./redux-toolkit.development.cjs')
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2662
frontend/node_modules/@reduxjs/toolkit/dist/index.d.mts generated vendored Normal file

File diff suppressed because it is too large Load Diff

2662
frontend/node_modules/@reduxjs/toolkit/dist/index.d.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./rtk-query.production.min.cjs')
} else {
module.exports = require('./rtk-query.development.cjs')
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More