Commit Graph
87 Commits
Author SHA1 Message Date
darroyo 9f0e94ab8a fix(personajes): flatten capacity table iteration to prevent reverse render order from nested x-for in tbody
CI / Build Native (push) Successful in 6m47s
2026-08-24 10:49:57 -04:00
darroyo 01f1d9da44 refactor(personajes): reorder capacity table grouped by grade (D→S), rows SS/SPS/BSS per grade, rowspan grouping
CI / Build Native (push) Successful in 6m50s
2026-08-24 10:25:56 -04:00
darroyo 5d7e6b610e refactor(personajes): replace per-character capacity table with single reference table (lv0-lv3, baseWeight 115.230)
CI / Build Native (push) Successful in 6m45s
2026-08-24 10:13:58 -04:00
darroyo 38902f972e refactor(personajes): consolidate capacity table into single cross-character grid with totals
CI / Build Native (push) Successful in 6m53s
2026-08-24 09:42:39 -04:00
darroyo c9622200a7 refactor(ui): redesign with amber CRT terminal theme and mobile responsive
CI / Build Native (push) Successful in 6m52s
- Full visual overhaul: amber phosphor CRT aesthetic (gold on near-black)
- JetBrains Mono font family applied site-wide
- Terminal window style: LED amber dot on every panel header
- Header with 'user@shot-crafter:~$' title bar
- Login/register as terminal window cards
- Responsive: grid-5/grid-8 collapse on mobile, tables scroll horizontally
- Tabs with horizontal scroll on mobile
- Article headers wrap cleanly on small screens
- Custom scrollbars, selection color, focus states in amber theme
- New favicon in amber style
- term-section.html reusable partial created
2026-08-23 23:33:18 -04:00
darroyo 39e5530da4 fix(ventas): add generateUid fallback for non-secure contexts (HTTP)
CI / Build Native (push) Successful in 6m46s
2026-08-23 23:00:25 -04:00
darroyo a654020655 feat(ventas): structured item selector with weight calculation and inventory usage
CI / Build Native (push) Successful in 6m50s
2026-08-23 18:26:42 -04:00
darroyo 30037bc5cb fix: add ?v=5 cache-busting to all script tags
CI / Build Native (push) Successful in 6m44s
El error 'window.api.updateCharacterWeight is not a function' se daba
porque el browser tenía cacheada la versión vieja de api.js (que
tenía updateCharacterInventory en vez de updateCharacterWeight).

Agregar ?v=5 a los script src fuerza al browser a re-descargar el JS
cuando el URL cambia (con cada nueva versión bump n). Previene
errores 'is not a function' después de deploys que agregan/renombran
métodos.

Aplica a todos los archivos HTML que cargan JS estático.
2026-08-23 12:49:24 -04:00
darroyo e8e5ab41c7 feat(personajes): capacity by weight (L2 dwarf model)
CI / Build Native (push) Successful in 6m51s
Reemplaza el modelo slots × stack por el modelo real de L2 para
vendedores enanos: capacidad basada en el límite de peso del personaje
con multiplicador por el pasivo 'Weight Limit Increase' del enano.

Modelo:
- maxWeight = baseWeight × (1 + skillLevel)
  - skillLevel 0 → ×1 (sin pasivo)
  - skillLevel 1 → ×2 (+100%, lv1)
  - skillLevel 2 → ×3 (+200%, lv2)
  - skillLevel 3 → ×4 (+300%, lv3)
- capacity(tipo, grado) = floor(maxWeight / shotWeight[tipo][grado])

Backend (V5 migration):
- DROP COLUMN inventory_slots, stack_limit
- ADD COLUMN base_weight DEFAULT 115230
- ADD COLUMN skill_level DEFAULT 0
- Character/CharacterDto/CharacterIn con los nuevos campos
- PUT /api/characters/{id}/weight endpoint (reemplaza /inventory)
- Validación: baseWeight > 0, skillLevel 0..3

Frontend:
- Form: Base weight (default 115230) + Skill level select (0..3)
- Tabla: Peso base + Skill enano (select editable) + Peso máx calculado
- Sección '💼 Capacidad de inventario de shots (por peso)':
  - Por personaje: header con nombre, lvl, base, multiplicador, badge skill
  - Tabla 3×5 con las 15 capacidades (3 tipos × 5 grados)
  - Sub-info: Peso máx = base × (1+skill)
  - Total 15 tipos por personaje
- Shot weights constants en Alpine (SHOT_WEIGHTS, TIPOS, GRADOS)
- Métodos: maxWeightOf(c), capacityOf(c, tipo, grado), totalCapacityOf(c),
  updateWeight(c), tipoShort(tipo)

Pesos de shots (hardcoded):
- SS: D=3, C=3, B=2, A=2, S=2
- SPS: D=5, C=3, B=3, A=2, S=2
- BSS: D=5, C=3, B=3, A=2, S=2

Verificado en navegador:
- Migration V5 aplicada limpiamente (5 migrations total)
- Personajes existentes preservados con default baseWeight=115230 skillLevel=0
- Math correcta: skillLevel=3, baseWeight=115230 → maxWeight=460.920 →
  SS D=153.640, BSS S=230.460
- Edición inline de skill en la tabla persiste via PUT /weight
- Visualización 3×5 por personaje con badge dinámico
2026-08-23 12:34:47 -04:00
darroyo c11f2e5aac feat(personajes): inventory capacity for shots per character
CI / Build Native (push) Successful in 6m57s
Add 2 new fields per character:
- inventorySlots (default 80) — slots disponibles en el inventario
- stackLimit (default 9999) — límite por stack (config del server)

Capacidad por personaje = slots × stack limit (total shots que puede
cargar). Útil para responder 'tengo N shots, ¿me alcanzan los chars?'.

Backend:
- V4__character_inventory.sql migration adds the 2 columns with defaults
- Character entity + CharacterDto + CharacterIn records include them
- CharacterService.updateInventory() + CharacterResource PUT
  /api/characters/{id}/inventory endpoint for inline edits
- Validation: slots and stackLimit must be > 0

Frontend:
- Agregar personaje form: 2 nuevos inputs (Slots default 80, Stack default 9999)
- Mis personajes table: columnas Slots, Stack y Capacidad (shots),
  editables inline con @blur y @keydown.enter (mismo patrón que level)
- Nueva sección '💼 Capacidad de inventario de shots' con:
  - Total de la cuenta (verde, grande)
  - Sub-info: N personajes · M slots · stack máx X
  - Cards por personaje (slots × stack)
- Alpine helpers nuevos: capacityOf(), totalCapacity(), totalSlots(),
  maxStack(), updateInventory()

Verificado en navegador:
- Math correcta: 100×9999 + 100×99999 = 10.999.800 shots ✓
- Edición inline persiste via PUT /inventory
- Migration V4 aplicada limpiamente en el boot (4 migrations total)
2026-08-23 11:15:49 -04:00
darroyo c109a59d3d feat(historial): recalcular run con precios actuales
CI / Build Native (push) Successful in 6m54s
H4: en el modal de detalle de un run del historial, agregar un botón
que recalcula ganancia, costo y venta usando los precios actuales de
insumos en lugar del snapshot guardado.

Cambios:
- Nuevo campo modalMode ('snapshot' | 'current') en historialSection.
- displayItems()/recomputeItem(): si modalMode='current', recalcula
  cada item multiplicando las cantidades guardadas (cristalesUsados,
  oreNecesario, crafteosPosibles, shotsObtenidos) por los precios
  actuales de insumos. Las formulas del snapshot no se rederivan — solo
  se cambian los precios.
- displayTotals(): agrega totales en el modo activo.
- recomputeDelta(): snapshot_profit - current_profit para indicador.
- Refactor de costCristalesTotal/Ore/Dwarven/Pct + totalCrafteos +
  marginPct/costPerShot/profitPerShot para usar displayItems/displayTotals
  en lugar de modalDetails directo.
- Nuevos marginPctCurrent/costPerShotCurrent/profitPerShotCurrent.
- Modal totals cards + tabla de items leen de displayTotals() /
  displayItems().
- Toggle button 'Recalcular con precios actuales' ↔ 'Volver al snapshot'
  en el header del modal.
- Badge amarillo 'Precios actuales' cuando modalMode='current'.
- Indicador de delta en la card de Ganancia (↑ verde si ganás más,
  ↓ rojo si perdés más), formateado como '↑ X.XXX vs snapshot'.

Verificado en navegador: cambiar venta en Insumos → en el modal del
historial, el toggle actualiza todos los valores (totales, desglose,
distribución, items) usando precios nuevos. La matemática es consistente
con la calculadora principal.
2026-08-21 20:07:41 -04:00
darroyo e0ac9e844f feat(calculadora): complete cost-split coverage in all modes
CI / Build Native (push) Successful in 6m56s
- Ranking de rentabilidad: add 3 columns (C. Cristales / C. Ore / C. Fee)
  alongside the existing Costo column. Update rankingFiltrado() to include
  the 3 sub-costs in its map output.
- Totales Presupuesto (3 sub-vistas: simple/maximizado/mezcla): add
  Distribución del costo block with bar + legend under existing 3 cards.
- Totales Objetivo: extend objetivoTotales() to aggregate sub-costs, then
  add the same distribution block.
- Totales Plan cristales: same distribution block (planTotalesCristales
  already aggregated sub-costs).
- Add 3 generic helpers pctCOf/pctOOf/pctFOf that take a totals object
  (handles both 'costoTotal' and 'costo' field names).
- Fix x-show condition for Presupuesto distribution: use .costo (the field
  actually returned by planTotalesPresupuesto) instead of .costoTotal.

Verified in browser across all 5 distribution points; percentages sum to
100% and math matches per-row calculations.
2026-08-21 10:00:11 -04:00
darroyo 5c100024d7 chore: remove stale src/frontend (React/Vite) + cleanup .gitignore
CI / Build Native (push) Successful in 6m57s
Frontend was migrated to Qute/Alpine in 34e5246. The src/frontend/ tree
was no longer built, not referenced in any build file (pom.xml,
Dockerfile, .dockerignore), and not present on disk. Removing:

- 36 tracked-but-deleted React/TS/Vite/Tailwind files
- 5 untracked screenshot artifacts in repo root
- 4 stale entries in .gitignore (src/frontend/node_modules/, dist/, etc.)
2026-08-20 19:16:58 -04:00
darroyo d73f946cb2 feat(calculadora): cost split breakdown + plan por cristales mode
CI / Build Native (push) Has been cancelled
- Add costoCristales/costoOre/costoDwarven fields to calc.js results and
  RunItem/RunIn/RunSummary/HistoryStats records (no DB migration needed —
  items stored as JSON in TEXT column).
- Show 4 explicit columns (C. Cristales / C. Ore / C. Fee / Costo total)
  in all calculator tables: Cristales mode, Presupuesto (simple/maximizado/
  mezcla), and Objetivo. Totales globales now includes a 3-segment
  distribution bar with %.
- Historial: 3 new stats cards + global distribution bar; per-run modal
  shows breakdown + bar; per-item table has 3 sub-columns; CSV export
  extended. CSS: cost-split-bar, cost-split-segment-{cristales,ore,fee},
  cost-legend-dot.
- New 'Plan por cristales' mode (4th): single-grade + quantity input with
  greedy max-ROI/cristal algorithm and shared cost split columns.
  Verificado end-to-end en navegador.
2026-08-20 19:15:42 -04:00
darroyo 34e524616d chore: commit all missing files from Qute/Alpine migration
CI / Build Native (push) Successful in 7m9s
This commit adds all the files that were created during the frontend
migration (Qute templates + Alpine.js) but were never committed to git.

Includes:
- TemplateData.java (shared Qute page helpers)
- Static resources: app.js, defaults.js, calc.js, alpine.min.js, pico.min.css, favicon.svg
- Qute templates: changePassword, formulas, insumos, login, register, usuarios
- Partial templates: header.html, layout/base.html
- Modified: MustChangePasswordFilter.java (added login/register paths)
- Modified: AppState.java (added price fields)
- Modified: format.js (simplified number formatting)
- Modified: application.properties (static resources comment)
- Modified: calculadora.html (Qute/Alpine rewrite)
2026-08-18 10:26:10 -04:00
darroyo f1c22f79d6 fix(ci): add explicit Quarkus Maven repository to pom.xml
CI / Build Native (push) Failing after 1m53s
The CI runner fails to resolve quarkus-qute dependency. Adding
explicit repository configuration ensures Maven can download from
Sonatype releases repo if not available in default repo.
2026-08-18 10:22:11 -04:00
darroyo 25aba44377 fix(ci): clear Quarkus Maven cache before native build
CI / Build Native (push) Failing after 1m10s
2026-08-18 10:18:11 -04:00
darroyo 0e072cfcb7 fix(ci): add -U flag to force fresh dependency resolution in native build
CI / Build Native (push) Failing after 58s
2026-08-18 10:15:29 -04:00
darroyo 661ac4d953 feat: add personajes page with level tracking and fix timer reactivity
CI / Build Native (push) Failing after 58s
- New /personajes page with CRUD for characters (name, class, level)
- Level editable inline, deletable (blocked if has active sales)
- ventas.html now loads characters list as datalist suggestions
- Fix timer: add reactive 'now' property so countdown updates
- Fix script loading: add defaults.js + app.js to ventas/personajes
- Fix table auto-reload: use loadCharacters()/loadSales() after mutations
- Timer now uses setTimeout recursion + visibilitychange handler
2026-08-18 10:09:10 -04:00
darroyo af323da3d3 feat: add character sales tracking with 12h countdown timer
CI / Build Native (push) Failing after 1m11s
- New page /ventas to track characters selling items
- Default 12h duration per character sale
- Real-time countdown with color-coded badges (green/yellow/red/gray)
- Sound alert toggle when sales expire
- CRUD API: GET/POST/DELETE /api/sales
- Flyway migration V2 for character_sales table
- Private per-user (same user sees only their own characters)
- Expired sales remain visible with 'Expirado' badge until manually deleted
2026-08-18 09:22:41 -04:00
darroyo 8ca11252d7 fix: install Node.js 20 in CI container (required by JS-based actions)
CI / Build Native (push) Successful in 6m37s
2026-08-16 00:15:42 -04:00
darroyo cebd2c0365 fix: filter snapshot prices to only show items used in run
CI / Build Native (push) Failing after 47s
- Add filteredSnapshotCristales/Ores/Venta getters that filter based on modalDetails.items
- Cristales: only show grados that appear in items
- Ores: only show Soul/Spirit Ore based on item tipos
- Venta: only show tipo-grado combos in items
- Backward compatible: old runs with 15 items show full snapshot
2026-08-15 23:24:48 -04:00
darroyo 05db929da1 fix: only save items with cristalesDisponibles > 0
CI / Build Native (push) Failing after 43s
- saveRun(): filter items where cristalesDisponibles > 0 before persisting
- allItems(): show only saved items in modal (no padding with zeros)
2026-08-15 23:19:24 -04:00
darroyo e913b5ceb0 fix(historial): clamp sparkline bar height between 15-90% to prevent overflow
CI / Build Native (push) Failing after 39s
When range=0 (1 run or all same profit), use absolute scale with clamped heightPct
2026-08-15 23:11:45 -04:00
darroyo 4b564d9fae fix(historial): use SS/SPS/BSS abbreviations for shot types
CI / Build Native (push) Failing after 44s
- Replace broken chained replace() with tipoShort() helper
- SS = Soulshot, SPS = Spiritshot, BSS = Blessed Spiritshot
- Add tooltip (:title) on chips for full name on hover
- CSS: add cursor:help, font-weight:600, letter-spacing
2026-08-15 23:05:44 -04:00
darroyo 54fd68aa08 feat(historial): comprehensive enrichment of history page
CI / Build Native (push) Failing after 1m21s
Backend:
- RunSummary: add profitMarginPct, costPerShot, profitPerShot, itemCount, tiposUsados, wasProfitable
- HistoryStats: add avgProfitMarginPct, avgCostPerShot, avgProfitPerShot, totalOreUsed, totalItems, tiposBreakdown
- HistoryService: compute all new fields, parse items for best/worst run

Frontend (historial.html):
- 8 stats cards instead of 5: +Margen promedio, +Total invertido, +Shots, +Costo/shot
- SVG sparkline showing last 10 runs profit trend
- Ganancia por tipo breakdown with horizontal bars
- Table: sortable columns (click headers), new cols: Margen%, Costo/shot, Items chips, delta vs avg
- Sortable by: Fecha, Label, Costo, Venta, Ganancia, Margen
- Modal: badge Profit/Loss, 8 metric cards, snapshot of prices at save time, comparison vs current prices
- Export buttons: CSV (resumen) + JSON (completo con items y snapshot)

Format.js:
- Add fmtPercent() with es-CL locale

CSS:
- Add sparkline, tipos-breakdown, sortable columns, delta badges, metric cards, diff badges, modal-lg
2026-08-15 21:30:01 -04:00
darroyo 209ef9fb05 fix(ci): remove obsolete Node.js setup, use Gitea-compatible syntax
CI / Build Native (push) Failing after 1m7s
- Remove Node.js installation (frontend is now static Qute/Alpine)
- Remove src/frontend/node_modules cache (no longer exists)
- Change github.workflow to gitea.workflow for Gitea Actions compatibility
- Increase container memory from 8g to 10g for native builds
- Add BUILDKIT_INLINE_CACHE=1 for faster Docker builds
2026-08-15 19:52:06 -04:00
darroyo 71d8050515 fix(db): tune H2 URL to actually flush writes to disk on commit
CI / Build Native (push) Successful in 6m50s
Sin estos parametros, H2 mantiene buffers en memoria y los cambios
hechos por change-password (u otras mutaciones) no llegaban al
archivo .mv.db hasta un checkpoint que el SIGKILL del container
se saltaba. Resultado: User aparece persistido en memoria (login
con la nueva pass da 200 OK dentro de la misma corrida), pero al
restart el archivo en disco tiene la password vieja.

WRITE_DELAY=0  -- flush cada commit
LOCK_MODE=0   -- no usar lock file (quitamos el .lock.db)
MV_STORE=TRUE -- usar el multi-version store que es mas cooperativo
                 con escrituras frecuentes

Verificado: el archivo .mv.db crece de 94KB a 106KB tras un
change-password, y la nueva password sobrevive un docker rm + run.
2026-08-15 13:54:26 -04:00
darroyo 0ef2991bcd fix(change-password): return updated user via record outcome w/ flush
CI / Build Native (push) Successful in 7m49s
User.getEntityManager().refresh() requiere una tx activa, y el
caller (AuthResource.changePassword) no es @Transactional -> ese
approach tiraba TransactionRequiredException.

En lugar de refrescar en el caller, cambio AuthService.changePassword
para devolver la User ya actualizada en un record
ChangePasswordOutcome, despues de un flush() explicito para que los
cambios lleguen a DB antes de cerrar la tx. Asi el response del
endpoint refleja el estado real de la fila.
2026-08-15 13:30:26 -04:00
darroyo 7b76bde15b fix(change-password): force entity refresh in response to avoid stale mustChangePassword
CI / Build Native (push) Successful in 7m2s
El cambio-password persistia bien a la DB (login con la nueva pass
daba 200 OK), pero la respuesta del endpoint seguia reportando
mustChangePassword: true. Era un problema de Panache/Hibernate cache:
la user entity quedaba cacheada en el persistence context con el
estado pre-cambio, y User.findById() siguiente devolvia esa misma
instancia cacheada.

Fix: despues de findById, forzar refresh desde DB con
User.getEntityManager().refresh(refreshed). Asi el response
refleja el estado actual real de la fila.
2026-08-15 12:02:57 -04:00
darroyo 615b78a847 feat(db): migrate to Flyway for schema persistence
CI / Build Native (push) Successful in 8m10s
'create-drop' borra las tablas en cada shutdown, perdiendo todos
los usuarios (y el cambio de password). Aunque el bind mount
persiste el archivo H2 en /root/docker/manual/lineage2/data/, las
tablas se recreaban vacias en cada restart del container.

Solucion: Flyway para manejar el schema, Hibernate solo valida.

- pom.xml: + quarkus-flyway
- src/main/resources/db/migration/V1__init.sql: schema de users,
  user_state, production_runs + index idx_runs_user_created
- application.properties: database.generation=create-drop -> validate
  + quarkus.flyway.migrate-at-start=true + baseline-on-migrate=true

BootstrapAdmin queda igual: chequea User.count() y crea admin si 0.
Flyway corre antes de Hibernate, asi que las tablas existen cuando
StartupEvent dispara.
2026-08-15 11:40:16 -04:00
darroyo 2ceb7ca7ea fix(frontend): reorder await api.me() before setUser in changePassword
CI / Build Native (push) Successful in 6m22s
En el fix anterior, await api.me() estaba DESPUES de setUser(u). Eso
significaba que React podria re-renderizar AuthenticatedApp entre el
setUser y el await, disparando el usePersistedState.load() que llama
api.getState() mientras la cookie todavia no estaba aplicada al
cookie store del browser. El await api.me() quedaba corriendo mientras
el 401 ya habia sido reportado.

Invertir el orden: await api.me() PRIMERO (confirma que la cookie
quedo realmente aplicada), despues setUser(u). Asi React re-renderiza
con la cookie ya activa para los fetches internos.
2026-08-15 11:24:36 -04:00
darroyo b161710952 fix(frontend): post-change-password cookie timing + defensive 401 fallback
CI / Build Native (push) Successful in 7m45s
Dos cambios para destrabar el flujo post-change-password:

1. AuthContext.changePassword: despues de setUser(u), hacer un round-trip
   a api.me() para confirmar que la nueva cookie ya es utilizable. Sin
   este paso, React monta AuthenticatedApp tan rapido que el primer
   fetch desde usePersistedState.load() corre antes de que el browser
   haya propagado el Set-Cookie (race condition reportado en Chrome).

2. usePersistedState.load: en 401, caer en makeDefaultAppState() en lugar
   de setStateInternal(null). Antes dejaba al usuario varado en un
   <LoadingScreen>Cargando tu estado...</LoadingScreen> para siempre. Ahora
   ve la app vacia con SaveIndicator, y al primer cambio real el
   auto-save sube el estado al server.
2026-08-15 03:27:31 -04:00
darroyo ec5ac6735c chore: revert debug logging after the auth issue was diagnosed
CI / Build Native (push) Successful in 6m24s
Found the actual issue was a stale Docker image in the LXC (the CI
runner was stuck for ~30h starting at task 323 on Aug 14). The newly
pulled image with all prior fixes (no @Authenticated, records w/
@RegisterForReflection, create-drop for tables) has been verified
end-to-end: login returns 200, /me returns 200 with the user,
change-password returns 200 after password validation.

So we can drop the LOG statements and the SQL debug log without
regressing anything.
2026-08-15 02:39:23 -04:00
darroyo e53a8107cb noop: trigger CI
CI / Build Native (push) Failing after 1m18s
2026-08-15 02:05:00 -04:00
darroyo 5a83470587 debug(auth): trace de login en AuthService + SQL log en app props
CI / Build Native (push) Failing after 1m19s
Para diagnosticar por qué el login devuelve 401 con la password
correcta del bootstrap admin. Imprime:
- username/pw.length al entrar
- si la busqueda del user en la DB da null
- hash.len y hash.prefix para confirmar que el user esta bien
  almacenado
- bcrypt.matches(true/false)

Tambien activamos quarkus.hibernate-orm.log.sql=true y
quarkus.log.category.'com.l2.shots.auth'.level=DEBUG.
2026-08-15 01:59:08 -04:00
darroyo 995ed39dbc debug: enable SQL logging + auth category logging
CI / Build Native (push) Successful in 6m23s
Para diagnosticar por qué el login devuelve 401 con la password
correcta del bootstrap admin. Vamos a ver qué queries corre
Hibernate y qué pasa en el authenticate.
2026-08-15 01:46:22 -04:00
darroyo 7041fc206b fix(auth): drop @Authenticated/@RolesAllowed on cookie-based endpoints
CI / Build Native (push) Successful in 6m14s
Quarkus security solo lee JWT del header 'Authorization: Bearer ...'
por defecto. Esta app entrega el JWT en una cookie HttpOnly
('auth-token'), entonces @Authenticated/@RolesAllowed rebotaban con
401 antes de que el endpoint pudiera validar manualmente.

Reemplazos:
- @Authenticated en /change-password: sacada. El endpoint ya hace
  jwtCookieAuth.extractToken() y devuelve 401 manual si falla.
- @Authenticated en /check: sacada. Endpoint ahora extrae y valida
  el JWT a mano.
- @Authenticated en /me: never estuvo, OK.
- @RolesAllowed('admin') en /admin/reset-password y /admin/users:
  reemplazada por chequeo manual jwt.getGroups().contains('admin'),
  devolviendo 403 si no es admin.

Sigue funcionando el MustChangePasswordFilter porque sigue siendo
un ContainerRequestFilter con @Priority(AUTHENTICATION+100) y no
requiere auth previa para correr: simplemente aborta con 403 si
mustChangePassword=true salvo allowlist (change-password, logout).

Removida inyeccion no usada de currentJwt (JsonWebToken) que solo
estaba para @RolesAllowed.
2026-08-15 01:26:46 -04:00
darroyo cc09fe8943 fix(dtos): annotate records with @RegisterForReflection for native Jackson
CI / Build Native (push) Successful in 6m18s
Las clases record NO se serializan bien en Quarkus 3.20.1 native-image
aunque Jackson 2.17+ las soporta en JVM. GraalVM strip-ea la metadata
<Erecord> del bytecode mas los accessors auto-generados, y Jackson no
puede detectar que la clase es un record -> cae al BeanSerializer
clasico -> 'no properties discovered to create BeanSerializer'.

@RegisterForReflection le indica a Quarkus: incluí esta clase en la
metadata de reflexion del native-image, asi Jackson la ve completa.

Aplicado a los 16 DTOs (records + nested records):
- auth: AuthMeResponse, AdminUserSummary, Credentials,
        ChangePasswordRequest, AdminResetPasswordRequest, ErrorBody,
        MustChangeBody
- history: RunSummary, RunDetails, RunItem, RunSnapshot, RunIn,
           HistoryStats
- state: AppState + Insumos + FormulaDto (nested)
2026-08-15 01:10:47 -04:00
darroyo 017c58a2b2 refactor(dtos): convert all DTOs to Java records for native-mode Jackson
CI / Build Native (push) Successful in 6m23s
En Quarkus native-image, Jackson no introspecta public fields de clases
con normales (deja de refleccionar field metadata). Salia error:

  No serializer found for class com.l2.shots.auth.AuthMeResponse and
  no properties discovered to create BeanSerializer

Los records tienen auto-accessors (id(), username(), etc) que Jackson
serializa nativamente, sin necesidad de reflection o
@RegisterForReflection.

Convertidos:
- auth: AuthMeResponse, AdminUserSummary, Credentials,
         ChangePasswordRequest, AdminResetPasswordRequest, ErrorBody,
         MustChangeBody
- history: RunSummary, RunDetails, RunItem, RunSnapshot, RunIn,
           HistoryStats
- state: AppState (con Insumos y FormulaDto anidados)

Callers actualizados para usar accessors en vez de field access:
- AuthService.listUsersForAdmin -> AdminUserSummary.from
- AuthService.saveRun -> RunSummary.from / RunDetails.of
- AuthResource.register/login -> creds.username() / creds.password()
- HistoryResource.saveRun -> input.items() etc

Notas:
- RunDetails NO puede extender RunSummary en records (JLS no permite
  extends entre records). Va como record independiente con todos los
  campos. El JSON que produce matchea la interface RunDetails del
  frontend (que extendia RunSummary).
- HistoryStats ahora se construye de una sola vez al final de
  computeStats en lugar de ir mutando campos.
- MustChangeBody quedo con un constructor no-canonico no-arg para
  mantener el call site original (new MustChangeBody()).
2026-08-15 00:46:19 -04:00
darroyo 89a224ee0c fix(bootstrap): use create-drop so tables exist on fresh H2
CI / Build Native (push) Successful in 6m16s
El schema-gen con database.generation=update NO corre en Quarkus native
(3.20.1) ni forzando quarkus.hibernate-orm.schema-management.run-on-startup=true.
El primer 'Table USERS not found' sigue saliendo en BootstrapAdmin.onStart.

Cambio a 'create-drop':
- En startup: crea las tablas que falten (no dropa las existentes).
- En shutdown limpio: drop -> POR CADA docker stop SE PIERDEN LOS USERS.

Para persistencia real a futuro: integrar Flyway con migrations y pasar
database.generation=validate. Para esta entrega, dejamos create-drop
que al menos arranca la app + bootstrap admin en un H2 fresco, y los
restarts sin SIGTERM (docker kill, caida de energia) preservan datos.

Saca tambien el run-on-startup=true que no estaba surtiendo efecto.
2026-08-15 00:15:24 -04:00
darroyo 07588979e4 fix(bootstrap): force EMF init + enable schema-gen on startup
CI / Build Native (push) Successful in 6m24s
El error 'Table USERS not found' en BootstrapAdmin.User.count() se daba
porque en Quarkus native-image, con database.generation=update, el
schema-gen NO corre en startup del container por defecto (corre solo en
dev mode). Resultado: en un H2 fresco, el primer SELECT COUNT(*) del
bootstrap pierde contra una DB sin tablas.

Dos cambios:
1. application.properties: agregar
   quarkus.hibernate-orm.schema-management.run-on-startup=true
   para forzar el schema-gen en startup en prod/native.
2. BootstrapAdmin:
   - @Transactional + EntityManager (em) inyectados
   - em.createNativeQuery('SELECT 1') al inicio de onStart() para
     asegurar que el EMF esté listo y la session esté abierta antes
     del count(). Belt-and-suspenders.
2026-08-15 00:04:17 -04:00
darroyo 7ed41ac825 fix(docker): add diagnostic logging to entrypoint to debug permission issue
CI / Build Native (push) Successful in 6m53s
El chown sigue sin funcionar despues del primer fix. Agregamos logs
para ver:
- con que uid arranca el entrypoint
- ls -la /work/data antes y despues del chown
- si chown falla, lo logueamos en lugar de tragarnos el error

Tambien asegura que /work y /work/application tengan owner 1001:1001
y mode 0755, por si la imagen viene con owner root (que seria el
sintoma de que el build no usaria USER directive).
2026-08-14 23:39:14 -04:00
darroyo 0bcd66ba32 fix(docker): entrypoint chowns /work/data so H2 can create on fresh volume
CI / Build Native (push) Successful in 6m25s
Cuando el volumen named 'shot-crafter-data' es nuevo, Docker lo monta
con owner root:root. La aplicacion corre como UID 1001 y no puede
escribir -> H2 falla al crear shots.mv.db -> BootstrapAdmin no crea
el admin -> no se puede login.

Quitar 'USER 1001' del Dockerfile, agregar entrypoint.sh que:
1. chown -R 1001:1001 /work/data (la app va a poder escribir)
2. chmod 0755 /work
3. setpriv --reuid=1001 --regid=1001 --init-groups -- /work/application
   (drop a no-root antes del exec)

Resultado: el contenedor arranca como root el primer instante, hace
el fix de permisos una sola vez, y desde ese momento la app corre
como 1001. Docker defaults USER a root cuando no hay directiva USER.
2026-08-14 22:58:28 -04:00
darroyo 6a7f01289b fix(auth): move SecureRandom out of static field for native-image compat
CI / Build Native (push) Successful in 7m25s
GraalVM native-image rejects static fields of type Random/SecureRandom
(their internal state is captured at build time and can't be replayed
deterministically). Move the instance into generatePassword() as a
local variable - it is only used once at startup anyway.
2026-08-14 22:16:36 -04:00
darroyo a171943f98 feat(auth): admin UI for user management + change-password frontend
CI / Build Native (push) Failing after 3m4s
Backend additions:
- User.lastLoginAt column (updated on each successful login)
- AdminUserSummary DTO (id, username, createdAt, lastLoginAt, isAdmin, mustChangePassword)
- GET /api/auth/admin/users (RolesAllowed("admin")) -> array of summaries
- AuthService.listUsersForAdmin() + AuthService.authenticate() now @Transactional and bumps lastLoginAt

Frontend (Phase 1):
- User type extended with mustChangePassword + isAdmin
- api.changePassword() / api.adminResetPassword() / api.adminListUsers()
- AuthContext exposes changePassword
- ChangePasswordPage.tsx (full-page, current + new + confirm, errors inline)
- App.tsx routes LoginPage -> ChangePasswordPage -> AuthenticatedApp

Frontend (Phase 2):
- TabBar supports optional 'usuarios' tab (shown only if user.isAdmin)
- UsersAdminPage.tsx: lista todos los usuarios con badges de rol y estado,
  botón "Resetear contraseña" con modal inline que llama adminResetPassword
- Header de AuthenticatedApp muestra un badge 'admin' al lado del username

Both mvn compile and npm tsc + vite build pass clean.
2026-08-14 20:07:12 -04:00
darroyo 1d6fc08a25 feat(auth): bootstrap admin on first boot + forced password change + admin reset
CI / Build Native (push) Has been cancelled
Backend changes (no frontend yet):

Schema (User entity)
- + mustChange_password (boolean NOT NULL, default false)
- + is_admin (boolean NOT NULL, default false)
Hibernate update mode adds both columns automatically.

BootstrapAdmin (new, ApplicationScoped, @Observes StartupEvent)
- runs only when User.count() == 0 and app.bootstrap.admin.enabled=true
- generates a 20-char random password (alphabet without 0/o/O/1/l/I)
- persists the user with isAdmin=true, mustChangePassword=true
- prints a banner to stdout AND to the JBoss logger so docker logs
  picks it up:
    BOOTSTRAP-ADMIN-USERNAME admin
    BOOTSTRAP-ADMIN-PASSWORD <random>
    BOOTSTRAP-ADMIN-CHANGE   This password MUST be changed on first login ...
- idempotent: skips if any user already exists

MustChangePasswordFilter (new, @Provider ContainerRequestFilter)
- runs after JWT auth (Priorities.AUTHENTICATION + 100)
- for authenticated requests with mustChangePassword=true, returns
  403 with {error, mustChangePassword:true} unless the path is
  /api/auth/change-password or /api/auth/logout

Change-password endpoint (POST /api/auth/change-password)
- @Authenticated, body {currentPassword, newPassword}
- verifies currentPassword via bcrypt, validates newPassword>=8 chars,
  updates hash and sets mustChangePassword=false
- returns updated AuthMeResponse and re-issues the auth cookie

Admin reset endpoint (POST /api/auth/admin/reset-password)
- @RolesAllowed("admin")
- body {username, newPassword}
- sets target's passwordHash and mustChangePassword=true (forces change
  on next login)
- security: only users in the JWT 'admin' group can hit it; isAdmin
  is stored on the user record so a stale token can't promote itself

JWT groups now include 'admin' for isAdmin users; previously everyone
was just 'user'.

Config (application.properties)
- app.bootstrap.admin.enabled=true
- app.bootstrap.admin.username=admin
2026-08-14 20:04:44 -04:00
darroyo fb021b165b ci: point Docker registry at internal Gitea (10.5.0.195:3000)
CI / Build Native (push) Successful in 6m17s
Cut the dependency on the public hostname gitea.danielarroyo.cl and
push straight to the LAN Gitea instance. Internal Gitea is plain HTTP,
so the docker daemon on the LXC needs 'insecure-registries' set, and
the gitea-runner-lxc has to be re-registered against the new URL
(separate manual steps; not in this repo).

- ci.yml: docker login, both docker tag, both push_with_retry, and
  docker logout now target 10.5.0.195:3000
- compose.yaml: image URL switched to 10.5.0.195:3000

The CI workflow itself, the Dockerfile, and the build command are
unchanged - only the registry target moved.
2026-08-14 18:29:08 -04:00
darroyo 2833e76914 fix(docker): switch runtime base to ubuntu:22.04 (glibc 2.35)
CI / Build Native (push) Has been cancelled
The native binary is built against glibc 2.35 (Ubuntu jammy in CI) but
quarkus-micro-image:2.0 ships glibc 2.34 (UBI 9), hence the
'GLIBC_2.35 not found' at runtime. Switching to ubuntu:22.04 as the
runtime base matches glibc exactly and lets us stop fighting with
musl / static binaries.

Dockerfile:
- FROM ubuntu:22.04 (was quarkus-micro-image:2.0)
- install curl + ca-certificates via apt (apt + bash are present, so
  chmod/echo securerandom run in-line again)
- single-stage: no more curl-builder multi-stage
- useradd UID 1001 (matches the in-container USER)

CI:
- drop binary-type=STATIC and --libc=musl (binary is dynamic again)
- drop the rm -f target/*-runner (cache invalidate)
- 'Stage binary for Docker' is now just 'cp'
- verify step is informational only

Image is ~5 MB larger than quarkus-micro-image but the runtime now
matches the build glibc, so the container starts cleanly.
2026-08-14 15:45:05 -04:00
darroyo 6e3db41ba4 fix(ci): force musl linking for the static native binary
CI / Build Native (push) Failing after 6m13s
binary-type=STATIC alone picks glibc on Ubuntu jammy hosts, hence
the GLIBC_2.35 error at runtime. Explicitly force musl:

- -Dquarkus.native.libc=musl
- -Dquarkus.native.additional-build-args=--libc=musl
- delete target/*-runner first so we don't reuse a previously-built
  glibc-based binary that the Quarkus plugin cache might return

Verify step now exits 1 if the binary isn't statically linked, so
the next CI run tells us definitively whether we shipped static.
2026-08-14 14:53:39 -04:00