diff --git a/DOCKER.md b/DOCKER.md index db5d275..5ed1192 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -166,10 +166,10 @@ Adds `127.0.0.1:5432:5432` while keeping the server’s `15432` mapping. The `cron` service runs: -- **Every 10 min** — `sync_iot_from_api` (IoT panel, real-time — not gated by publish cutoff) +- **Every 1 min** — `sync_chicken_counting_from_edge` (chicken counting + mortality + weight edge sync) - **At `DASHBOARD_PUBLISH_HOUR`:`DASHBOARD_PUBLISH_MINUTE` daily** — `sync_karung_from_web_admin` then `recompute_kpi_rollups` (entrypoint renders cron from these env vars; default 17:00) -Dashboard reads hide today's KPI/counting/weight/karung data until the configured publish time WIB. **IoT panel is excluded** and updates live every 10 minutes. +Dashboard reads hide today's KPI/counting/weight/karung data until the configured publish time WIB. **IoT panel is excluded** (IoT devices push data directly via webhook) and updates live. Logs: `docker compose exec cron tail -f /var/log/cron.log` diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx index f5bf9b2..3a5c8da 100644 --- a/components/Dashboard.tsx +++ b/components/Dashboard.tsx @@ -74,6 +74,10 @@ const Dashboard: React.FC = ({ setView }) => { } setError(null); try { + await Promise.all([ + api.countings.sync({ cycle_id: cycleId }).catch(() => null), + api.weights.sync({ cycle_id: cycleId }).catch(() => null), + ]); const [kpiRows, countingRows, manualRows, weightRows, freshCycle] = await Promise.all([ api.kpis.list({ cycle_id: cycleId }), api.countings.list({ cycle_id: cycleId }), diff --git a/components/WeightMonitoringPage.tsx b/components/WeightMonitoringPage.tsx index e051ac7..6cb1ae3 100644 --- a/components/WeightMonitoringPage.tsx +++ b/components/WeightMonitoringPage.tsx @@ -36,6 +36,12 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings } setError(null); try { + try { + await api.weights.sync({ cycle_id: cycleId }); + } catch { + // Best-effort edge sync: don't fail page load if edge vision API is temporarily unreachable + } + const [list, freshCycle] = await Promise.all([ api.weights.list({ cycle_id: cycleId }), api.cycles?.retrieve?.(cycleId)?.catch(() => null) ?? Promise.resolve(null), diff --git a/components/counting/DataManagementPage.tsx b/components/counting/DataManagementPage.tsx index 05d1450..b7ce8eb 100644 --- a/components/counting/DataManagementPage.tsx +++ b/components/counting/DataManagementPage.tsx @@ -68,6 +68,12 @@ const DataManagementPage: React.FC<{ onSettings: () => void }> = ({ onSettings } } setError(null); try { + try { + await api.countings.sync({ cycle_id: cycleId }); + } catch { + // Best-effort edge sync: don't fail page load if edge vision API is temporarily unreachable + } + const [k, c, m, freshCycle] = await Promise.all([ api.kpis.list({ cycle_id: cycleId }), api.countings.list({ cycle_id: cycleId }), diff --git a/components/counting/__tests__/DataManagementPage.test.tsx b/components/counting/__tests__/DataManagementPage.test.tsx index 2368f31..5518740 100644 --- a/components/counting/__tests__/DataManagementPage.test.tsx +++ b/components/counting/__tests__/DataManagementPage.test.tsx @@ -116,6 +116,15 @@ describe('DataManagementPage', () => { expect(screen.getAllByText('0.0%').length).toBeGreaterThan(0); }); + it('automatically calls chicken counting sync on load', async () => { + render( undefined} />); + + await screen.findByRole('heading', { name: 'Hitung Ayam' }); + await waitFor(() => { + expect(api.countings.sync).toHaveBeenCalledWith({ cycle_id: 1 }); + }); + }); + it('calls chicken counting sync when Sync is clicked', async () => { render( undefined} />); diff --git a/docker/Dockerfile.api b/docker/Dockerfile.api index 6f5e20c..62b0695 100644 --- a/docker/Dockerfile.api +++ b/docker/Dockerfile.api @@ -19,10 +19,10 @@ COPY backend/requirements.txt . RUN pip install -r requirements.txt COPY backend/ . -COPY docker/entrypoint-api.sh docker/entrypoint-cron.sh docker/karung_sync_loop.sh /app/ +COPY docker/entrypoint-api.sh docker/entrypoint-cron.sh docker/karung_sync_loop.sh docker/edge_sync_loop.sh /app/ COPY docker/crontab /etc/cron.d/newdashboard -RUN chmod +x /app/entrypoint-api.sh /app/entrypoint-cron.sh /app/karung_sync_loop.sh \ +RUN chmod +x /app/entrypoint-api.sh /app/entrypoint-cron.sh /app/karung_sync_loop.sh /app/edge_sync_loop.sh \ && mkdir -p /app/data /app/media /app/staticfiles \ && chmod 0644 /etc/cron.d/newdashboard \ && crontab /etc/cron.d/newdashboard diff --git a/docker/crontab b/docker/crontab index 986bb01..fb47885 100644 --- a/docker/crontab +++ b/docker/crontab @@ -4,12 +4,6 @@ PATH=/usr/local/bin:/usr/bin:/bin # NOTE: docker/entrypoint-cron.sh regenerates this file from DASHBOARD_PUBLISH_HOUR/MINUTE # at container start. Keep this checked-in copy as documentation / fallback defaults (17:00). -# IoT panel: real-time sync every 10 minutes (not gated by publish cutoff). -*/10 * * * * root cd /app && /usr/local/bin/python manage.py sync_iot_from_api >> /var/log/cron.log 2>&1 - -# Chicken counting + mortality + weight edge sync every 10 minutes (not gated by publish cutoff). -*/10 * * * * root cd /app && /usr/local/bin/python manage.py sync_chicken_counting_from_edge >> /var/log/cron.log 2>&1 - -# Karung-web-admin: every 30s via karung_sync_loop.sh (started by entrypoint-cron.sh). +# Karung-web-admin & Edge Chicken Counting: every 30s via sync loops (started by entrypoint-cron.sh). # Daily publish — recompute KPI rollups (hour/minute come from DASHBOARD_PUBLISH_* at runtime). 0 17 * * * root cd /app && /usr/local/bin/python manage.py sync_karung_from_web_admin >> /var/log/cron.log 2>&1 && /usr/local/bin/python manage.py recompute_kpi_rollups >> /var/log/cron.log 2>&1 diff --git a/docker/edge_sync_loop.sh b/docker/edge_sync_loop.sh new file mode 100644 index 0000000..c594f59 --- /dev/null +++ b/docker/edge_sync_loop.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# Sub-minute chicken counting + mortality + weight edge sync. +set -eu +cd /app +while true; do + /usr/local/bin/python manage.py sync_chicken_counting_from_edge >> /var/log/cron.log 2>&1 || true + sleep 30 +done diff --git a/docker/entrypoint-cron.sh b/docker/entrypoint-cron.sh index 5151e1b..49687b4 100644 --- a/docker/entrypoint-cron.sh +++ b/docker/entrypoint-cron.sh @@ -13,13 +13,8 @@ CRON_FILE=/etc/cron.d/newdashboard echo "SHELL=/bin/sh" echo "PATH=/usr/local/bin:/usr/bin:/bin" echo "" - echo "# IoT panel: real-time sync every 10 minutes (not gated by publish cutoff)." - echo "*/10 * * * * root cd /app && /usr/local/bin/python manage.py sync_iot_from_api >> /var/log/cron.log 2>&1" - echo "" - echo "# Chicken counting + mortality + weight edge sync every 10 minutes." - echo "*/10 * * * * root cd /app && /usr/local/bin/python manage.py sync_chicken_counting_from_edge >> /var/log/cron.log 2>&1" - echo "" echo "# Karung-web-admin: every 30s via karung_sync_loop.sh (started below)." + echo "# Chicken counting + weight edge: every 30s via edge_sync_loop.sh (started below)." echo "# Daily publish — karung sync then KPI rollups at configured cutoff." echo "${PUBLISH_MINUTE} ${PUBLISH_HOUR} * * * root cd /app && /usr/local/bin/python manage.py sync_karung_from_web_admin >> /var/log/cron.log 2>&1 && /usr/local/bin/python manage.py recompute_kpi_rollups >> /var/log/cron.log 2>&1" } > "$CRON_FILE" @@ -27,4 +22,5 @@ chmod 0644 "$CRON_FILE" crontab "$CRON_FILE" /app/karung_sync_loop.sh & +/app/edge_sync_loop.sh & exec cron -f