fix docker deployment
This commit is contained in:
1 parent
ded52d1ece
commit
fb0f996776
17 files changed
+49
-18
No files matched your search
@@ -0,0 +1,6 @@
|
||||
docker/**/*.sh text eol=lf
|
||||
docker/crontab text eol=lf
|
||||
docker/Dockerfile* text eol=lf
|
||||
docker/nginx.conf text eol=lf
|
||||
docker/entrypoint-*.sh text eol=lf
|
||||
|
||||
@@ -36,7 +36,7 @@ Deploy **dashboard-cpsp-executive** (HQ) with Docker Compose. Same layout as sit
|
||||
|
||||
- Docker 20.10+
|
||||
- Docker Compose v2+
|
||||
- Ports available: **80** (UI), **18001** (direct API), **15433** (Postgres on localhost)
|
||||
- Ports available: **80** (UI; set `FRONTEND_PORT=8080` in `.env` if busy), **18001** (direct API), **15433** (Postgres on localhost)
|
||||
|
||||
```bash
|
||||
docker --version
|
||||
|
||||
@@ -33,6 +33,7 @@ Same Compose layout as the site app. On the HQ host: UI **:80**, API debug **:18
|
||||
```bash
|
||||
cp docker/.env.example .env
|
||||
# set SECRET_KEY, DB_PASSWORD, BOOTSTRAP_ADMIN_PASSWORD
|
||||
# optional: FRONTEND_PORT=8080 if host :80 is busy
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import httpx
|
||||
|
||||
from apps.farms.models import Cycle
|
||||
|
||||
@@ -32,11 +32,11 @@ def push_close_decision(cycle: Cycle, *, approved: bool) -> None:
|
||||
payload["end_date"] = cycle.end_date.isoformat()
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
response = httpx.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"X-API-Key": city.api_key, "Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
timeout=30.0,
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
raise SiteClosePushError(
|
||||
@@ -44,6 +44,6 @@ def push_close_decision(cycle: Cycle, *, approved: bool) -> None:
|
||||
)
|
||||
except SiteClosePushError:
|
||||
raise
|
||||
except requests.RequestException as exc:
|
||||
except httpx.HTTPError as exc:
|
||||
logger.exception("Push close decision failed for cycle %s", cycle.pk)
|
||||
raise SiteClosePushError("Gagal kirim keputusan ke situs — coba lagi") from exc
|
||||
@@ -32,6 +32,12 @@ function phasesFor(totalDays: number): Phase[] {
|
||||
|
||||
const statusConfig = {
|
||||
active: { label: 'Active', bg: 'bg-red-100', text: 'text-red-800', icon: 'fa-running' },
|
||||
pending_close: {
|
||||
label: 'Pending Close',
|
||||
bg: 'bg-amber-100',
|
||||
text: 'text-amber-800',
|
||||
icon: 'fa-hourglass-half',
|
||||
},
|
||||
closed: {
|
||||
label: 'Completed',
|
||||
bg: 'bg-green-100',
|
||||
|
||||
@@ -138,7 +138,9 @@ describe('UserManagementPage', () => {
|
||||
it('resets password', async () => {
|
||||
render(<UserManagementPage />);
|
||||
await screen.findByText('gm1');
|
||||
fireEvent.click(screen.getAllByRole('button', { name: /Reset password/i })[0]);
|
||||
const resetBtn = screen.getAllByRole('button', { name: /Reset password/i })[0];
|
||||
expect(resetBtn).toBeTruthy();
|
||||
fireEvent.click(resetBtn!);
|
||||
fireEvent.change(screen.getByPlaceholderText(/Password baru/i), {
|
||||
target: { value: 'reset-me' },
|
||||
});
|
||||
|
||||
@@ -116,7 +116,9 @@ const DataManagementPage: React.FC<{ onSettings: () => void }> = ({ onSettings }
|
||||
const kpiByDate = new Map(kpis.map((k) => [k.date, k]));
|
||||
|
||||
const lastCycleDay = Math.max(0, (selectedCycle.total_days || 1) - 1);
|
||||
const endDay = cycleDayForDate(selectedCycle.start_date, selectedCycle.end_date);
|
||||
const endDay = selectedCycle.end_date
|
||||
? cycleDayForDate(selectedCycle.start_date, selectedCycle.end_date)
|
||||
: lastCycleDay;
|
||||
const calendarDay = cycleDayForDate(selectedCycle.start_date, todayIso());
|
||||
const maxDay = Math.min(calendarDay, lastCycleDay, endDay);
|
||||
const today = todayIso();
|
||||
|
||||
@@ -65,7 +65,9 @@ const ReportPreviewOverlay: React.FC<Props> = ({ open, reportType, onClose }) =>
|
||||
const endCycleDay = selectedCycle
|
||||
? Math.min(
|
||||
selectedCycle.total_days,
|
||||
cycleDayForDate(selectedCycle.start_date, selectedCycle.end_date)
|
||||
selectedCycle.end_date
|
||||
? cycleDayForDate(selectedCycle.start_date, selectedCycle.end_date)
|
||||
: Math.max(0, (selectedCycle.total_days || 1) - 1)
|
||||
)
|
||||
: 0;
|
||||
const effectiveUpToDay = isEndCycle ? endCycleDay : draftDay;
|
||||
|
||||
+3
-2
@@ -144,7 +144,8 @@ services:
|
||||
container_name: dashboard-cpsp-executive-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
# Override with FRONTEND_PORT in .env when host :80 is busy (e.g. local Windows).
|
||||
- "${FRONTEND_PORT:-80}:80"
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_healthy
|
||||
@@ -152,7 +153,7 @@ services:
|
||||
- dashboard-cpsp-executive-network
|
||||
healthcheck:
|
||||
test:
|
||||
["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/health"]
|
||||
["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:80/health"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
|
||||
@@ -13,6 +13,9 @@ ALLOWED_HOSTS=localhost,127.0.0.1,api,frontend,executive.local
|
||||
CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1,http://executive.local
|
||||
CORS_ALLOWED_ORIGINS=http://localhost,http://127.0.0.1,http://executive.local
|
||||
|
||||
# Host port for the UI (default 80). Use 8080 if :80 is already taken.
|
||||
FRONTEND_PORT=80
|
||||
|
||||
# Production admin (bootstrap_admin — no demo data)
|
||||
BOOTSTRAP_ADMIN_USER=admin
|
||||
BOOTSTRAP_ADMIN_PASSWORD=Pr04dm1n
|
||||
|
||||
@@ -22,10 +22,12 @@ COPY backend/ .
|
||||
COPY docker/entrypoint-api.sh docker/entrypoint-cron.sh docker/karung_sync_loop.sh /app/
|
||||
COPY docker/crontab /etc/cron.d/executive
|
||||
|
||||
# /etc/cron.d files include a username field; system cron reads them directly
|
||||
# (do not install with `crontab`, which expects user-crontab format).
|
||||
RUN chmod +x /app/entrypoint-api.sh /app/entrypoint-cron.sh /app/karung_sync_loop.sh \
|
||||
&& mkdir -p /app/data /app/media /app/staticfiles \
|
||||
&& chmod 0644 /etc/cron.d/executive \
|
||||
&& crontab /etc/cron.d/executive
|
||||
&& touch /var/log/cron.log
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
|
||||
@@ -20,6 +20,6 @@ COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD wget --quiet --tries=1 --spider http://localhost:80/health || exit 1
|
||||
CMD wget --quiet --tries=1 --spider http://127.0.0.1:80/health || exit 1
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -7,6 +7,7 @@ printenv | grep -E '^(PATH|DJANGO_|SECRET_|DB_|KARUNG_|IOT_|CHICKEN_COUNTING_|DA
|
||||
|
||||
# Render publish-time cron from DASHBOARD_PUBLISH_* (default 17:00).
|
||||
# Edge syncs are OFF by default at HQ (mirror via city-sites); commands no-op when disabled.
|
||||
# File lives in /etc/cron.d (requires username field) — do not feed it to `crontab`.
|
||||
PUBLISH_HOUR="${DASHBOARD_PUBLISH_HOUR:-17}"
|
||||
PUBLISH_MINUTE="${DASHBOARD_PUBLISH_MINUTE:-0}"
|
||||
CRON_FILE=/etc/cron.d/executive
|
||||
@@ -20,9 +21,10 @@ CRON_FILE=/etc/cron.d/executive
|
||||
echo ""
|
||||
echo "# Daily publish — optional 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"
|
||||
echo ""
|
||||
} > "$CRON_FILE"
|
||||
chmod 0644 "$CRON_FILE"
|
||||
crontab "$CRON_FILE"
|
||||
touch /var/log/cron.log
|
||||
|
||||
if [ "${KARUNG_WEB_ADMIN_SYNC_ENABLED:-false}" = "true" ] || [ "${KARUNG_WEB_ADMIN_SYNC_ENABLED:-false}" = "True" ]; then
|
||||
/app/karung_sync_loop.sh &
|
||||
|
||||
@@ -14,7 +14,7 @@ const cycle: Cycle = {
|
||||
chick_in_weight: 45,
|
||||
doc_in_count: 20000,
|
||||
feed_initial_balance: 0,
|
||||
status: 'gm',
|
||||
status: 'active',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ const cycle = {
|
||||
chick_in_weight: 45,
|
||||
doc_in_count: 1000,
|
||||
feed_initial_balance: 0,
|
||||
status: 'gm',
|
||||
status: 'active',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
} satisfies Cycle;
|
||||
|
||||
@@ -50,7 +50,9 @@ export function buildInMemoryReport(input: BuildInMemoryReportInput): DashboardR
|
||||
// Day numbering stays 0-based (day 0 = start_date), but the report must never
|
||||
// run past the cycle end date. lastDay = start_date → end_date in days, which
|
||||
// is total_days - 1 for a contiguous cycle.
|
||||
const lastDay = cycleDayForDate(cycle.start_date, cycle.end_date);
|
||||
const lastDay = cycle.end_date
|
||||
? cycleDayForDate(cycle.start_date, cycle.end_date)
|
||||
: Math.max(0, (cycle.total_days || 1) - 1);
|
||||
const endCycleUpTo = Math.min(cycle.total_days, lastDay);
|
||||
const defaultUpTo =
|
||||
input.reportType === 'end_cycle'
|
||||
@@ -202,7 +204,7 @@ export function buildInMemoryReport(input: BuildInMemoryReportInput): DashboardR
|
||||
return {
|
||||
cycleId: String(cycle.id),
|
||||
cycleStart: cycle.start_date,
|
||||
cycleEnd: cycle.end_date,
|
||||
cycleEnd: cycle.end_date ?? '',
|
||||
kandangId: input.kandangId,
|
||||
kandangName: input.kandangName,
|
||||
area: input.area,
|
||||
|
||||
@@ -38,7 +38,9 @@ function stockAkhirByDate(
|
||||
const manualByDate = new Map(manualInputs.map((m) => [m.date, m]));
|
||||
|
||||
const lastCycleDay = Math.max(0, (cycle.total_days || 1) - 1);
|
||||
const endDay = cycleDayForDate(cycle.start_date, cycle.end_date);
|
||||
const endDay = cycle.end_date
|
||||
? cycleDayForDate(cycle.start_date, cycle.end_date)
|
||||
: lastCycleDay;
|
||||
const maxDay = Math.min(lastCycleDay, endDay);
|
||||
|
||||
const result = new Map<string, number>();
|
||||
|
||||
Reference in new issue
Block a user