For holdings with quantity > 0, the input now accepts the total
valuation amount and derives the current price by dividing by quantity.
Holdings with quantity 0 (target-only) still accept price directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CronTrigger had no explicit timezone, defaulting to system timezone
(UTC in Docker containers), causing jobs to run at KST 03:00/03:30
instead of the intended 18:00/18:30.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add "데이터 탐색" menu item to sidebar with Search icon
- Add "수집 데이터 조회" link button on data management page
- Fix sidebar active state to correctly distinguish /admin/data
from /admin/data/explorer
- Add page title mapping for data explorer in header
- Fix .gitignore: add negation for frontend/src/app/admin/data/
so admin data pages are tracked without needing git add -f
- Fix dashboard loading state (return null → skeleton with layout)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues caused DB reset on every deploy:
1. docker-compose.prod.yml used bind mount (./data/postgres) with
PostgreSQL 18's incompatible /var/lib/postgresql/data path.
2. The Gitea CI runner shares Docker socket with the host, but
./data/postgres resolves to a temp path inside the runner container.
Each deploy creates a fresh workspace, so the bind mount always
points to an empty directory on the host.
Fix: Use a named Docker volume (same as docker-compose.yml dev config).
Named volumes are managed by Docker daemon directly, survive container
recreation, and don't depend on working directory resolution.
Also fix deploy.yml: remove unnecessary mkdir for data dirs, write
backup to /tmp instead of relative path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The portfolio API was returning only ticker symbols (e.g., "095570")
without stock names. The Stock table already has Korean names
(e.g., "AJ네트웍스") from data collection.
Backend: Add name field to HoldingWithValue schema, fetch stock names
via RebalanceService.get_stock_names() in the portfolio detail endpoint.
Frontend: Show Korean stock name as primary label with ticker as
subtitle in portfolio detail, donut charts, and target vs actual
comparison. Dashboard donut chart also shows names.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The collect endpoints were defined as async def but called synchronous
collector.run() directly, blocking the single uvicorn event loop for
up to 15+ minutes during price collection. This caused all other
requests (including auth/login) to hang, making the app unusable.
Backend: Run each collector in a daemon thread with its own DB session,
returning HTTP 200 immediately. The collector logs status to JobLog as
before, which the frontend can poll.
Frontend: Auto-poll job status every 3s while any job is "running",
with a visual indicator. Disable collect buttons during active jobs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend:
- Fix Decimal serialization in data_explorer.py (Decimal → FloatDecimal)
- Fix Optional type hints for query parameters in admin.py
- Fix authentication bypass in market.py search_stocks endpoint
Frontend:
- Fix 404 page: link to "/" instead of "/dashboard", proper back button
- Rewrite dashboard with real API data instead of hardcoded samples
- Implement actual equity curve and drawdown charts in backtest detail
- Remove mock data from backtest list, load real results from API
- Fix null dividend_yield display in quality strategy page
- Add skeleton loading states to 7 pages that returned null during load
Infrastructure:
- Fix PostgreSQL 18 volume mount compatibility in docker-compose.yml
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pydantic v2's model_dump(mode="json") serializes Decimal as strings (e.g.,
"33.33" instead of 33.33), causing frontend crashes when calling .toFixed()
on string values. Introduced FloatDecimal type alias with PlainSerializer
to ensure Decimal fields are serialized as floats in JSON responses.
Also fixed frontend Transaction interface to match backend field names
(created_at → executed_at, transaction_type → tx_type).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pykrx get_market_ohlcv returns 6 data columns (시가/고가/저가/종가/거래량/거래대금),
not 7. The 등락률 (change) column does not exist, causing a length mismatch error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The pykrx library uses 'etx' not 'etf' as the module directory name,
and fetch() is the proper method that returns a DataFrame.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
KRX CSV download endpoint blocks requests from cloud/server IPs,
causing "No columns to parse from file" errors. Replaced with pykrx's
JSON-based API (get_market_ticker_list, get_market_cap_by_ticker,
get_market_fundamental_by_ticker) which is more reliable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ETFCollector (KRX master data) and ETFPriceCollector (pykrx OHLCV)
with corresponding admin API endpoints and frontend collection UI buttons.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace passlib with direct bcrypt usage to eliminate the
'module bcrypt has no attribute __about__' warning
- Change Query(regex=) to Query(pattern=) per FastAPI deprecation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Hash passwords with SHA-256 on frontend before transmission to prevent
raw password exposure in network traffic and server logs
- Switch login endpoint from OAuth2 form-data to JSON body
- Auto-create admin user on startup from ADMIN_USERNAME/ADMIN_PASSWORD
env vars, solving login failure after registration was disabled
- Update auth tests to match new SHA-256 + JSON login flow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Alpine's wget resolves localhost to IPv6 [::1] first, but Next.js
standalone listens on 0.0.0.0 (IPv4 only), causing connection refused.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The frontend container needs more time to initialize. Replace fixed
sleep with a retry loop that polls the container health status up to
30 times (150s max) before checking.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Install Docker CLI + Compose plugin in job container
- Remove rsync dependency (work directly from checkout workspace)
- Use --project-name for consistent compose project naming
- Use --env-file for compose variable substitution
- Use docker exec for health checks (job container can't reach host ports)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove nginx from docker-compose.prod.yml (NPM handles reverse proxy)
- Add Next.js rewrites to proxy /api/* to backend (backend fully hidden)
- Bind frontend to 127.0.0.1:3000 only (NPM proxies externally)
- Replace hardcoded localhost:8000 in history page with api client
- Make CORS origins configurable via environment variable
- Restrict CORS methods to GET/POST/PUT/DELETE
- Add Gitea Actions deploy workflow with secrets-based env management
- Add security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy)
- Add BACKEND_URL build arg to frontend Dockerfile for standalone builds
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Toast notifications with sonner
- Loading skeleton components
- Improved loading states
- 404 page
- Cleanup old components
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>