update docker deployment and changing seed kandang name
This commit is contained in:
1 parent
76f4d0106a
commit
74dd3faf60
40 files changed
+1253
-122
No files matched your search
@@ -0,0 +1,66 @@
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
|
||||
from apps.accounts.models import ApiKey, User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Create production superadmin login only (no demo sites/cycles/data). "
|
||||
"Uses BOOTSTRAP_ADMIN_USER and BOOTSTRAP_ADMIN_PASSWORD from settings."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--force-password",
|
||||
action="store_true",
|
||||
help="Reset password even when the admin user already has one",
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def handle(self, *args, **options):
|
||||
user_name = (settings.BOOTSTRAP_ADMIN_USER or "admin").strip()
|
||||
password = settings.BOOTSTRAP_ADMIN_PASSWORD or ""
|
||||
if not password:
|
||||
raise CommandError(
|
||||
"BOOTSTRAP_ADMIN_PASSWORD is required. Set it in .env before running bootstrap_admin."
|
||||
)
|
||||
|
||||
user, created = User.objects.get_or_create(
|
||||
user_name=user_name,
|
||||
defaults={
|
||||
"status": User.STATUS_SUPERADMIN,
|
||||
"is_staff": True,
|
||||
"is_superuser": True,
|
||||
},
|
||||
)
|
||||
|
||||
should_set_password = created or not user.has_usable_password() or options["force_password"]
|
||||
if should_set_password:
|
||||
user.set_password(password)
|
||||
user.status = User.STATUS_SUPERADMIN
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
user.save()
|
||||
action = "Created" if created else "Updated password for"
|
||||
self.stdout.write(f"{action} superadmin {user_name}")
|
||||
else:
|
||||
user.status = User.STATUS_SUPERADMIN
|
||||
user.is_superuser = True
|
||||
user.save(update_fields=["status", "is_superuser", "updated_at"])
|
||||
self.stdout.write(f"Superadmin {user_name} already exists (password unchanged)")
|
||||
|
||||
bootstrap_key = settings.BOOTSTRAP_API_KEY
|
||||
if bootstrap_key:
|
||||
api_key, _ = ApiKey.generate(user, "bootstrap")
|
||||
api_key.prefix = bootstrap_key[:8]
|
||||
api_key.key_hash = ApiKey.hash_key(bootstrap_key)
|
||||
api_key.save(update_fields=["prefix", "key_hash", "updated_at"])
|
||||
self.stdout.write(f"Bootstrap API key installed (prefix={api_key.prefix})")
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Bootstrap OK: login={user_name} (no demo farm data — use Pengaturan to add sites/kandang/cycles)"
|
||||
)
|
||||
)
|
||||
@@ -3,28 +3,33 @@ from django.db.models import Sum
|
||||
|
||||
from apps.farms.models import Cycle
|
||||
from apps.operations.models import ChickenCounting, FeedSacks, KPI
|
||||
from apps.operations.services.visibility import visible_through_date
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Recompute simple KPI rollups from ops tables for active cycles"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
cutoff = visible_through_date()
|
||||
count = 0
|
||||
for cycle in Cycle.objects.filter(status=Cycle.STATUS_ACTIVE):
|
||||
counting = ChickenCounting.objects.filter(cycle=cycle).aggregate(
|
||||
def visible(model):
|
||||
return model.objects.filter(cycle=cycle, date__lte=cutoff)
|
||||
|
||||
counting = visible(ChickenCounting).aggregate(
|
||||
mortality=Sum("mortality_count"),
|
||||
life=Sum("total_count"),
|
||||
)
|
||||
feed_sack = FeedSacks.objects.filter(cycle=cycle).order_by("-date", "-pk").first()
|
||||
feed_sack = visible(FeedSacks).order_by("-date", "-pk").first()
|
||||
|
||||
latest_cc = ChickenCounting.objects.filter(cycle=cycle).order_by("-date").first()
|
||||
latest_cc = visible(ChickenCounting).order_by("-date").first()
|
||||
latest_cw = None
|
||||
latest_fs = feed_sack
|
||||
latest_mi = None
|
||||
|
||||
from apps.operations.models import ChickenWeight, ManualInput
|
||||
latest_cw = ChickenWeight.objects.filter(cycle=cycle).order_by("-date").first()
|
||||
latest_mi = ManualInput.objects.filter(cycle=cycle).order_by("-date").first()
|
||||
latest_cw = visible(ChickenWeight).order_by("-date").first()
|
||||
latest_mi = visible(ManualInput).order_by("-date").first()
|
||||
|
||||
if not all([latest_cc, latest_cw, latest_fs, latest_mi]):
|
||||
self.stdout.write(f"cycle={cycle.pk} skipped (missing source records)")
|
||||
|
||||
@@ -8,8 +8,8 @@ from apps.accounts.models import ApiKey, User
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Seed demo superadmin login + API key, then Sukawarna Kandang 4 "
|
||||
"and Kandang 5 as the default closed cycles"
|
||||
"Seed demo superadmin login + API key, then Sukawarna Kandang 1 "
|
||||
"and Kandang 2 as the default closed cycles"
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
@@ -43,6 +43,6 @@ class Command(BaseCommand):
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
"Seed OK: Sukawarna Kandang 4 & Kandang 5 closed cycles + 10-minute IoT panels"
|
||||
"Seed OK: Sukawarna Kandang 1 & Kandang 2 closed cycles + 10-minute IoT panels"
|
||||
)
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Seed 10-minute IoT panel readings (144/day) for every flock.
|
||||
|
||||
Kandang 5 Lantai 1 is seeded from real sensor snapshots pulled from
|
||||
Kandang 2 Lantai 1 is seeded from real sensor snapshots pulled from
|
||||
``dashboard.cpsp.id/api/iot/flocks/`` (cached in ``scripts/iot_panel_10min_extract.json``);
|
||||
every other flock/floor is generated from that data with a deterministic per-house
|
||||
jitter so each cycle day holds a full 00:00..23:50 run of 144 readings.
|
||||
@@ -157,9 +157,9 @@ class Command(BaseCommand):
|
||||
self.stdout.write(self.style.SUCCESS(f"Seeded {seeded} IoT panel readings"))
|
||||
|
||||
def profile_for(self, kandang_name: str, flock_name: str) -> dict:
|
||||
"""Per-house transform applied to the real Kandang 5 Lantai 1 readings."""
|
||||
"""Per-house transform applied to the real Kandang 2 Lantai 1 readings."""
|
||||
is_lantai1 = flock_name == "Lantai 1"
|
||||
if kandang_name == "Kandang 5":
|
||||
if kandang_name == "Kandang 2":
|
||||
if is_lantai1:
|
||||
return {"real": True}
|
||||
return {
|
||||
@@ -168,7 +168,7 @@ class Command(BaseCommand):
|
||||
"temperature_delta": -0.3,
|
||||
"water_factor": 0.96,
|
||||
}
|
||||
# Kandang 4: deterministic jitter (same approach as seed_sukawarna_kandang4).
|
||||
# Kandang 1: deterministic jitter (same approach as seed_sukawarna_kandang4).
|
||||
return {
|
||||
"temperature_jitter": (-0.4, 0.4),
|
||||
"humidity_jitter": (-3.0, 3.0),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Seed Sukawarna / Kandang 4 closed cycle.
|
||||
"""Seed Sukawarna / Kandang 1 closed cycle.
|
||||
|
||||
Kandang 4 is a generated dataset modeled on the Kandang 5 seed
|
||||
Kandang 1 is a generated dataset modeled on the Kandang 2 seed
|
||||
(``seed_sukawarna_kandang5``). The two houses run the same 49-day window at
|
||||
the same site, but Kandang 4 is stocked with 45,000 DOC and performs slightly
|
||||
the same site, but Kandang 1 is stocked with 45,000 DOC and performs slightly
|
||||
better (lower mortality, marginally better FCR / EEF).
|
||||
|
||||
Every daily series is hand-tuned so the numbers stay internally consistent:
|
||||
@@ -17,11 +17,11 @@ Every daily series is hand-tuned so the numbers stay internally consistent:
|
||||
57.47 t live weight, avg ~1,412 g/bird at ~33.1 days; tail harvests leave
|
||||
fewer than 50 birds in the house at close-out.
|
||||
* ``kpi`` / ``manual`` – FCR/EEF/lot-weight follow the same formulas as the
|
||||
Kandang 5 preview:
|
||||
Kandang 2 preview:
|
||||
FCR = feed_total_kg / (harvest_weight_total_g + lot_weight_g)
|
||||
EEF = life_pct * (avg_harvest_weight/1000) / (FCR * avg_harvest_day) * 100
|
||||
lot = chicken_life * average_weight
|
||||
* ``iot_panel`` – derived from the real Kandang 5 sensor extract with
|
||||
* ``iot_panel`` – derived from the real Kandang 2 sensor extract with
|
||||
deterministic per-house jitter (same site, same season).
|
||||
"""
|
||||
|
||||
@@ -159,7 +159,7 @@ def _round(value: float, ndigits: int = 2) -> float:
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Seed Sukawarna / Kandang 4 closed cycle (generated, modeled on Kandang 5)"
|
||||
help = "Seed Sukawarna / Kandang 1 closed cycle (generated, modeled on Kandang 2)"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--password", default=DEFAULT_PASSWORD)
|
||||
@@ -195,7 +195,7 @@ class Command(BaseCommand):
|
||||
site.save(update_fields=["user", "updated_at"])
|
||||
|
||||
kandang, _ = Kandang.objects.get_or_create(
|
||||
kandang_name="Kandang 4",
|
||||
kandang_name="Kandang 1",
|
||||
site=site,
|
||||
)
|
||||
|
||||
@@ -209,7 +209,7 @@ class Command(BaseCommand):
|
||||
IotPanel.objects.filter(flock__cycle=existing).delete()
|
||||
Flock.objects.filter(cycle=existing).delete()
|
||||
existing.delete()
|
||||
self.stdout.write("Reset previous Kandang 4 cycle")
|
||||
self.stdout.write("Reset previous Kandang 1 cycle")
|
||||
|
||||
cycle, cycle_created = Cycle.objects.get_or_create(
|
||||
kandang=kandang,
|
||||
@@ -397,7 +397,7 @@ class Command(BaseCommand):
|
||||
|
||||
mort_total += MORTALITY[i]
|
||||
|
||||
# --- IoT panel: Kandang 5 sensors with per-house jitter -------------
|
||||
# --- IoT panel: Kandang 2 sensors with per-house jitter -------------
|
||||
panel_count = 0
|
||||
for flock_block in iot["flocks"]:
|
||||
flock = flocks_by_name[flock_block["name"]]
|
||||
|
||||
@@ -35,7 +35,7 @@ OUT_TODAY = [0] * 42 + [4, 5, 4, 3, 2, 1, 1]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Seed Sukawarna / Kandang 5 closed cycle from preview xlsx + IoT extract"
|
||||
help = "Seed Sukawarna / Kandang 2 closed cycle from preview xlsx + IoT extract"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--password", default=DEFAULT_PASSWORD)
|
||||
@@ -82,7 +82,7 @@ class Command(BaseCommand):
|
||||
site.save(update_fields=["user", "updated_at"])
|
||||
|
||||
kandang, _ = Kandang.objects.get_or_create(
|
||||
kandang_name="Kandang 5",
|
||||
kandang_name="Kandang 2",
|
||||
site=site,
|
||||
)
|
||||
|
||||
@@ -99,7 +99,7 @@ class Command(BaseCommand):
|
||||
IotPanel.objects.filter(flock__cycle=existing).delete()
|
||||
Flock.objects.filter(cycle=existing).delete()
|
||||
existing.delete()
|
||||
self.stdout.write("Reset previous Kandang 5 cycle")
|
||||
self.stdout.write("Reset previous Kandang 2 cycle")
|
||||
|
||||
cycle, cycle_created = Cycle.objects.get_or_create(
|
||||
kandang=kandang,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from apps.operations.services.iot_api import IotApiError, sync_iot_panels
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Sync IoT panel readings from the external IoT flocks API (10-minute snapshots)"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if not settings.IOT_SYNC_ENABLED:
|
||||
self.stdout.write("IOT_SYNC_ENABLED=false; skipping")
|
||||
return
|
||||
try:
|
||||
summary = sync_iot_panels()
|
||||
except IotApiError as exc:
|
||||
self.stderr.write(str(exc))
|
||||
return
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"IoT sync OK: upserted={summary.upserted} skipped={summary.skipped} pages={summary.pages}"
|
||||
)
|
||||
)
|
||||
Reference in new issue
Block a user