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 20.10+
|
||||||
- Docker Compose v2+
|
- 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
|
```bash
|
||||||
docker --version
|
docker --version
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ Same Compose layout as the site app. On the HQ host: UI **:80**, API debug **:18
|
|||||||
```bash
|
```bash
|
||||||
cp docker/.env.example .env
|
cp docker/.env.example .env
|
||||||
# set SECRET_KEY, DB_PASSWORD, BOOTSTRAP_ADMIN_PASSWORD
|
# set SECRET_KEY, DB_PASSWORD, BOOTSTRAP_ADMIN_PASSWORD
|
||||||
|
# optional: FRONTEND_PORT=8080 if host :80 is busy
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import requests
|
import httpx
|
||||||
|
|
||||||
from apps.farms.models import Cycle
|
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()
|
payload["end_date"] = cycle.end_date.isoformat()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = httpx.post(
|
||||||
url,
|
url,
|
||||||
json=payload,
|
json=payload,
|
||||||
headers={"X-API-Key": city.api_key, "Content-Type": "application/json"},
|
headers={"X-API-Key": city.api_key, "Content-Type": "application/json"},
|
||||||
timeout=30,
|
timeout=30.0,
|
||||||
)
|
)
|
||||||
if response.status_code >= 400:
|
if response.status_code >= 400:
|
||||||
raise SiteClosePushError(
|
raise SiteClosePushError(
|
||||||
@@ -44,6 +44,6 @@ def push_close_decision(cycle: Cycle, *, approved: bool) -> None:
|
|||||||
)
|
)
|
||||||
except SiteClosePushError:
|
except SiteClosePushError:
|
||||||
raise
|
raise
|
||||||
except requests.RequestException as exc:
|
except httpx.HTTPError as exc:
|
||||||
logger.exception("Push close decision failed for cycle %s", cycle.pk)
|
logger.exception("Push close decision failed for cycle %s", cycle.pk)
|
||||||
raise SiteClosePushError("Gagal kirim keputusan ke situs — coba lagi") from exc
|
raise SiteClosePushError("Gagal kirim keputusan ke situs — coba lagi") from exc
|
||||||
@@ -32,6 +32,12 @@ function phasesFor(totalDays: number): Phase[] {
|
|||||||
|
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
active: { label: 'Active', bg: 'bg-red-100', text: 'text-red-800', icon: 'fa-running' },
|
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: {
|
closed: {
|
||||||
label: 'Completed',
|
label: 'Completed',
|
||||||
bg: 'bg-green-100',
|
bg: 'bg-green-100',
|
||||||
|
|||||||
@@ -138,7 +138,9 @@ describe('UserManagementPage', () => {
|
|||||||
it('resets password', async () => {
|
it('resets password', async () => {
|
||||||
render(<UserManagementPage />);
|
render(<UserManagementPage />);
|
||||||
await screen.findByText('gm1');
|
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), {
|
fireEvent.change(screen.getByPlaceholderText(/Password baru/i), {
|
||||||
target: { value: 'reset-me' },
|
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 kpiByDate = new Map(kpis.map((k) => [k.date, k]));
|
||||||
|
|
||||||
const lastCycleDay = Math.max(0, (selectedCycle.total_days || 1) - 1);
|
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 calendarDay = cycleDayForDate(selectedCycle.start_date, todayIso());
|
||||||
const maxDay = Math.min(calendarDay, lastCycleDay, endDay);
|
const maxDay = Math.min(calendarDay, lastCycleDay, endDay);
|
||||||
const today = todayIso();
|
const today = todayIso();
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ const ReportPreviewOverlay: React.FC<Props> = ({ open, reportType, onClose }) =>
|
|||||||
const endCycleDay = selectedCycle
|
const endCycleDay = selectedCycle
|
||||||
? Math.min(
|
? Math.min(
|
||||||
selectedCycle.total_days,
|
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;
|
: 0;
|
||||||
const effectiveUpToDay = isEndCycle ? endCycleDay : draftDay;
|
const effectiveUpToDay = isEndCycle ? endCycleDay : draftDay;
|
||||||
|
|||||||
+3
-2
@@ -144,7 +144,8 @@ services:
|
|||||||
container_name: dashboard-cpsp-executive-frontend
|
container_name: dashboard-cpsp-executive-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
# Override with FRONTEND_PORT in .env when host :80 is busy (e.g. local Windows).
|
||||||
|
- "${FRONTEND_PORT:-80}:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
api:
|
api:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -152,7 +153,7 @@ services:
|
|||||||
- dashboard-cpsp-executive-network
|
- dashboard-cpsp-executive-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
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
|
interval: 30s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 3
|
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
|
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
|
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)
|
# Production admin (bootstrap_admin — no demo data)
|
||||||
BOOTSTRAP_ADMIN_USER=admin
|
BOOTSTRAP_ADMIN_USER=admin
|
||||||
BOOTSTRAP_ADMIN_PASSWORD=Pr04dm1n
|
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/entrypoint-api.sh docker/entrypoint-cron.sh docker/karung_sync_loop.sh /app/
|
||||||
COPY docker/crontab /etc/cron.d/executive
|
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 \
|
RUN chmod +x /app/entrypoint-api.sh /app/entrypoint-cron.sh /app/karung_sync_loop.sh \
|
||||||
&& mkdir -p /app/data /app/media /app/staticfiles \
|
&& mkdir -p /app/data /app/media /app/staticfiles \
|
||||||
&& chmod 0644 /etc/cron.d/executive \
|
&& chmod 0644 /etc/cron.d/executive \
|
||||||
&& crontab /etc/cron.d/executive
|
&& touch /var/log/cron.log
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
|||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
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;"]
|
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).
|
# 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.
|
# 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_HOUR="${DASHBOARD_PUBLISH_HOUR:-17}"
|
||||||
PUBLISH_MINUTE="${DASHBOARD_PUBLISH_MINUTE:-0}"
|
PUBLISH_MINUTE="${DASHBOARD_PUBLISH_MINUTE:-0}"
|
||||||
CRON_FILE=/etc/cron.d/executive
|
CRON_FILE=/etc/cron.d/executive
|
||||||
@@ -20,9 +21,10 @@ CRON_FILE=/etc/cron.d/executive
|
|||||||
echo ""
|
echo ""
|
||||||
echo "# Daily publish — optional karung sync then KPI rollups at configured cutoff."
|
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 "${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"
|
} > "$CRON_FILE"
|
||||||
chmod 0644 "$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
|
if [ "${KARUNG_WEB_ADMIN_SYNC_ENABLED:-false}" = "true" ] || [ "${KARUNG_WEB_ADMIN_SYNC_ENABLED:-false}" = "True" ]; then
|
||||||
/app/karung_sync_loop.sh &
|
/app/karung_sync_loop.sh &
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const cycle: Cycle = {
|
|||||||
chick_in_weight: 45,
|
chick_in_weight: 45,
|
||||||
doc_in_count: 20000,
|
doc_in_count: 20000,
|
||||||
feed_initial_balance: 0,
|
feed_initial_balance: 0,
|
||||||
status: 'gm',
|
status: 'active',
|
||||||
created_at: '',
|
created_at: '',
|
||||||
updated_at: '',
|
updated_at: '',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const cycle = {
|
|||||||
chick_in_weight: 45,
|
chick_in_weight: 45,
|
||||||
doc_in_count: 1000,
|
doc_in_count: 1000,
|
||||||
feed_initial_balance: 0,
|
feed_initial_balance: 0,
|
||||||
status: 'gm',
|
status: 'active',
|
||||||
created_at: '',
|
created_at: '',
|
||||||
updated_at: '',
|
updated_at: '',
|
||||||
} satisfies Cycle;
|
} 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
|
// 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
|
// run past the cycle end date. lastDay = start_date → end_date in days, which
|
||||||
// is total_days - 1 for a contiguous cycle.
|
// 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 endCycleUpTo = Math.min(cycle.total_days, lastDay);
|
||||||
const defaultUpTo =
|
const defaultUpTo =
|
||||||
input.reportType === 'end_cycle'
|
input.reportType === 'end_cycle'
|
||||||
@@ -202,7 +204,7 @@ export function buildInMemoryReport(input: BuildInMemoryReportInput): DashboardR
|
|||||||
return {
|
return {
|
||||||
cycleId: String(cycle.id),
|
cycleId: String(cycle.id),
|
||||||
cycleStart: cycle.start_date,
|
cycleStart: cycle.start_date,
|
||||||
cycleEnd: cycle.end_date,
|
cycleEnd: cycle.end_date ?? '',
|
||||||
kandangId: input.kandangId,
|
kandangId: input.kandangId,
|
||||||
kandangName: input.kandangName,
|
kandangName: input.kandangName,
|
||||||
area: input.area,
|
area: input.area,
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ function stockAkhirByDate(
|
|||||||
const manualByDate = new Map(manualInputs.map((m) => [m.date, m]));
|
const manualByDate = new Map(manualInputs.map((m) => [m.date, m]));
|
||||||
|
|
||||||
const lastCycleDay = Math.max(0, (cycle.total_days || 1) - 1);
|
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 maxDay = Math.min(lastCycleDay, endDay);
|
||||||
|
|
||||||
const result = new Map<string, number>();
|
const result = new Map<string, number>();
|
||||||
|
|||||||
Reference in new issue
Block a user