diff --git a/backend/apps/accounts/tests_scoping.py b/backend/apps/accounts/tests_scoping.py index 4d262db..599c23a 100644 --- a/backend/apps/accounts/tests_scoping.py +++ b/backend/apps/accounts/tests_scoping.py @@ -30,22 +30,32 @@ class VisibilityScopeTests(TestCase): self.active_a = ActiveSite.objects.create( code="a", name="Site A", + source_site_id="site-a-0001", api_base_url="https://a.example/api/v1", managed_by=self.gm, ) self.active_b = ActiveSite.objects.create( code="b", name="Site B", + source_site_id="site-b-0001", api_base_url="https://b.example/api/v1", managed_by=self.gm_other, ) self.site = Site.objects.create( - site_name="Site A", user=self.gm, active_site=self.active_a + site_id="site-a-0001", + site_name="Site A", + user=self.gm, + active_site=self.active_a, ) self.site_other = Site.objects.create( - site_name="Site B", user=self.gm_other, active_site=self.active_b + site_id="site-b-0001", + site_name="Site B", + user=self.gm_other, + active_site=self.active_b, + ) + self.orphan = Site.objects.create( + site_id="orphan-seed-0001", site_name="Orphan Seed", user=self.gm ) - self.orphan = Site.objects.create(site_name="Orphan Seed", user=self.gm) Kandang.objects.create(kandang_name="K1", site=self.site) Kandang.objects.create(kandang_name="K2", site=self.site_other) @@ -95,6 +105,7 @@ class VisibilityScopeTests(TestCase): ActiveSite.objects.create( code="orphan-seed", name="Orphan Seed", + source_site_id="orphan-seed-0001", api_base_url="https://orphan.example/api/v1", api_key="k", managed_by=None, diff --git a/backend/apps/farms/migrations/0019_site_id_slug.py b/backend/apps/farms/migrations/0019_site_id_slug.py new file mode 100644 index 0000000..c8e016e --- /dev/null +++ b/backend/apps/farms/migrations/0019_site_id_slug.py @@ -0,0 +1,238 @@ +# Convert HQ sites.site_id from bigint to farm-matching slug (varchar). + +from django.db import migrations, models +import django.db.models.deletion + + +def _unique_slug(candidate: str, used: set[str]) -> str: + final = candidate[:64] + n = 0 + while final in used or not final: + n += 1 + suffix = f"-{n}" + final = f"{candidate[: 64 - len(suffix)]}{suffix}" + used.add(final) + return final + + +def forwards_convert_site_pk(apps, schema_editor): + from django.utils.text import slugify + + connection = schema_editor.connection + Site = apps.get_model("farms", "Site") + Kandang = apps.get_model("farms", "Kandang") + ActiveSite = apps.get_model("sync", "ActiveSite") + + sites = list( + Site.objects.all().values( + "site_id", + "site_name", + "user_id", + "active_site_id", + "created_at", + "updated_at", + ) + ) + kandangs = list( + Kandang.objects.all().values( + "kandang_id", + "kandang_name", + "feed_in_button_urls", + "site_id", + "created_at", + "updated_at", + ) + ) + active_source = { + row.pk: str(row.source_site_id).strip() + for row in ActiveSite.objects.all() + if str(row.source_site_id or "").strip() + } + + used: set[str] = set() + mapping: dict[int, str] = {} + for row in sites: + old = row["site_id"] + candidate = active_source.get(row["active_site_id"]) or "" + if not candidate: + base = slugify(row["site_name"] or "")[:40] or "site" + candidate = f"{base}-legacy-{old}" + mapping[old] = _unique_slug(candidate, used) + + with connection.cursor() as cursor: + if connection.vendor == "sqlite": + cursor.execute("PRAGMA foreign_keys=OFF") + cursor.execute( + """ + CREATE TABLE sites_new ( + site_id varchar(64) NOT NULL PRIMARY KEY, + site_name varchar(30) NOT NULL, + user_id bigint NOT NULL REFERENCES user_access (user_id) + DEFERRABLE INITIALLY DEFERRED, + active_site_id bigint UNIQUE REFERENCES active_sites (active_site_id) + DEFERRABLE INITIALLY DEFERRED, + created_at datetime NOT NULL, + updated_at datetime NOT NULL + ) + """ + ) + for row in sites: + cursor.execute( + """ + INSERT INTO sites_new + (site_id, site_name, user_id, active_site_id, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + """, + [ + mapping[row["site_id"]], + row["site_name"], + row["user_id"], + row["active_site_id"], + row["created_at"], + row["updated_at"], + ], + ) + cursor.execute("DROP TABLE sites") + cursor.execute("ALTER TABLE sites_new RENAME TO sites") + cursor.execute( + "CREATE INDEX IF NOT EXISTS idx_sites_user_id ON sites (user_id)" + ) + + cursor.execute( + """ + CREATE TABLE kandang_new ( + kandang_id integer NOT NULL PRIMARY KEY AUTOINCREMENT, + kandang_name varchar(30) NOT NULL, + feed_in_button_urls text NOT NULL DEFAULT '[]', + site_id varchar(64) NOT NULL REFERENCES sites (site_id) + DEFERRABLE INITIALLY DEFERRED, + created_at datetime NOT NULL, + updated_at datetime NOT NULL + ) + """ + ) + import json + + for row in kandangs: + urls = row["feed_in_button_urls"] + if not isinstance(urls, str): + urls = json.dumps(urls if urls is not None else []) + cursor.execute( + """ + INSERT INTO kandang_new + (kandang_id, kandang_name, feed_in_button_urls, site_id, + created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + """, + [ + row["kandang_id"], + row["kandang_name"], + urls, + mapping[row["site_id"]], + row["created_at"], + row["updated_at"], + ], + ) + cursor.execute("DROP TABLE kandang") + cursor.execute("ALTER TABLE kandang_new RENAME TO kandang") + cursor.execute( + "CREATE INDEX IF NOT EXISTS idx_kandang_site_id ON kandang (site_id)" + ) + cursor.execute("PRAGMA foreign_keys=ON") + elif connection.vendor == "postgresql": + cursor.execute( + "ALTER TABLE sites ADD COLUMN IF NOT EXISTS site_id_new varchar(64)" + ) + for old, new in mapping.items(): + cursor.execute( + "UPDATE sites SET site_id_new = %s WHERE site_id = %s", + [new, old], + ) + cursor.execute( + "ALTER TABLE kandang ADD COLUMN IF NOT EXISTS site_id_new varchar(64)" + ) + cursor.execute( + """ + UPDATE kandang AS k + SET site_id_new = s.site_id_new + FROM sites AS s + WHERE k.site_id = s.site_id + """ + ) + cursor.execute( + """ + ALTER TABLE kandang DROP CONSTRAINT IF EXISTS kandang_site_id_fkey + """ + ) + # Django default name variants + cursor.execute( + """ + DO $$ + DECLARE r RECORD; + BEGIN + FOR r IN + SELECT conname FROM pg_constraint + WHERE conrelid = 'kandang'::regclass + AND contype = 'f' + AND pg_get_constraintdef(oid) ILIKE '%%site_id%%' + LOOP + EXECUTE format('ALTER TABLE kandang DROP CONSTRAINT %I', r.conname); + END LOOP; + END $$; + """ + ) + cursor.execute("ALTER TABLE kandang DROP COLUMN site_id") + cursor.execute( + "ALTER TABLE kandang RENAME COLUMN site_id_new TO site_id" + ) + cursor.execute("ALTER TABLE sites DROP CONSTRAINT sites_pkey") + cursor.execute("ALTER TABLE sites DROP COLUMN site_id") + cursor.execute("ALTER TABLE sites RENAME COLUMN site_id_new TO site_id") + cursor.execute("ALTER TABLE sites ALTER COLUMN site_id SET NOT NULL") + cursor.execute("ALTER TABLE sites ADD PRIMARY KEY (site_id)") + cursor.execute("ALTER TABLE kandang ALTER COLUMN site_id SET NOT NULL") + cursor.execute( + """ + ALTER TABLE kandang + ADD CONSTRAINT kandang_site_id_fkey + FOREIGN KEY (site_id) REFERENCES sites (site_id) ON DELETE CASCADE + """ + ) + cursor.execute( + "CREATE INDEX IF NOT EXISTS idx_kandang_site_id ON kandang (site_id)" + ) + else: + raise NotImplementedError( + f"sites.site_id slug migration unsupported for {connection.vendor}" + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("farms", "0018_rename_city_site_to_active_site"), + ("sync", "0007_uniq_source_site_id"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=[ + migrations.RunPython(forwards_convert_site_pk, migrations.RunPython.noop), + ], + state_operations=[ + migrations.AlterField( + model_name="site", + name="site_id", + field=models.SlugField(max_length=64, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name="kandang", + name="site", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="kandangs", + to="farms.site", + ), + ), + ], + ), + ] diff --git a/backend/apps/farms/models.py b/backend/apps/farms/models.py index 7076e87..4392fa3 100644 --- a/backend/apps/farms/models.py +++ b/backend/apps/farms/models.py @@ -1,14 +1,42 @@ from datetime import date +import secrets from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone +from django.utils.text import slugify from apps.accounts.models import User +def site_name_slug(site_name: str) -> str: + """Slug portion of a site id (no numeric suffix).""" + code = slugify(str(site_name or "").strip())[:55] + if not code: + raise ValidationError({"site_name": "Nama site tidak valid untuk dijadikan id."}) + return code + + +def site_id_from_name(site_name: str, *, numeric_suffix: int | None = None) -> str: + """Site PK: `{name-slug}-{4-digit number}`, matching farm dashboard identity.""" + base = site_name_slug(site_name) + if numeric_suffix is None: + numeric_suffix = secrets.randbelow(10_000) + return f"{base}-{numeric_suffix:04d}" + + +def allocate_site_id(site_name: str) -> str: + """Pick an unused site_id for HQ-only creates (seeds / manual).""" + for _ in range(32): + candidate = site_id_from_name(site_name) + if not Site.objects.filter(pk=candidate).exists(): + return candidate + raise ValidationError({"site_name": "Gagal membuat id site unik — coba lagi."}) + + class Site(models.Model): - site_id = models.BigAutoField(primary_key=True) + # Same shape as farm dashboard Site.site_id; when linked, equals ActiveSite.source_site_id. + site_id = models.SlugField(primary_key=True, max_length=64) site_name = models.CharField(max_length=30) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sites") # Linked when GM/GM Admin adds the site dashboard to their Pengaturan view list. @@ -29,6 +57,11 @@ class Site(models.Model): def __str__(self) -> str: return self.site_name + def save(self, *args, **kwargs): + if not self.site_id: + self.site_id = allocate_site_id(self.site_name) + super().save(*args, **kwargs) + class Kandang(models.Model): kandang_id = models.BigAutoField(primary_key=True) diff --git a/backend/apps/farms/serializers.py b/backend/apps/farms/serializers.py index a7d207f..3d35105 100644 --- a/backend/apps/farms/serializers.py +++ b/backend/apps/farms/serializers.py @@ -4,7 +4,9 @@ from apps.accounts.serializers import PkAsIdMixin from apps.farms.models import Cycle, Flock, Kandang, Site -class SiteSerializer(PkAsIdMixin, serializers.ModelSerializer): +class SiteSerializer(serializers.ModelSerializer): + # Slug PK (matches farm site_id); do not use IntegerField PkAsIdMixin. + id = serializers.CharField(source="pk", read_only=True) owner_name = serializers.CharField(source="user.display_name", read_only=True) class Meta: diff --git a/backend/apps/farms/services/active_site_registry.py b/backend/apps/farms/services/active_site_registry.py index 26afc44..d5a770e 100644 --- a/backend/apps/farms/services/active_site_registry.py +++ b/backend/apps/farms/services/active_site_registry.py @@ -2,27 +2,56 @@ from __future__ import annotations -from apps.farms.models import Site +from django.db import transaction +from django.utils.text import slugify + +from apps.farms.models import Kandang, Site from apps.sync.models import ActiveSite +def desired_hq_site_id(active_site: ActiveSite) -> str: + """HQ Site PK should match the farm site slug when known.""" + source = str(active_site.source_site_id or "").strip() + if source: + return source[:64] + code = slugify(str(active_site.code or "").strip())[:64] + if code: + return code + raise ValueError("ActiveSite needs source_site_id or code to create an HQ site.") + + +@transaction.atomic def ensure_hq_site_for_active_site(active_site: ActiveSite) -> Site: """ Ensure the active-site registry entry has an HQ Site for farm visibility. - Prefers an existing linked site, then an unlinked site owned by the - registrar with a matching name (so seeded Sukawarna attaches when - registered), otherwise creates a new Site. + Site.site_id matches ActiveSite.source_site_id (farm Site.site_id) when set. + Prefers an existing linked site, then a site already keyed by that id, then an + unlinked site owned by the registrar with a matching name (re-keyed if needed), + otherwise creates a new Site. """ - existing = Site.objects.filter(active_site=active_site).first() - if existing: - return existing - owner = active_site.managed_by if owner is None: raise ValueError("ActiveSite.managed_by is required to create an HQ site.") + desired_id = desired_hq_site_id(active_site) site_name = (active_site.name or active_site.code or "Site")[:30] + + existing = Site.objects.filter(active_site=active_site).first() + if existing is not None: + if existing.pk == desired_id: + return existing + return _rekey_site(existing, desired_id=desired_id, site_name=site_name, owner=owner) + + by_pk = Site.objects.filter(pk=desired_id).first() + if by_pk is not None: + by_pk.active_site = active_site + by_pk.user = owner + if by_pk.site_name != site_name: + by_pk.site_name = site_name + by_pk.save(update_fields=["active_site", "user", "site_name", "updated_at"]) + return by_pk + orphan = ( Site.objects.filter( user=owner, active_site__isnull=True, site_name__iexact=site_name @@ -31,7 +60,6 @@ def ensure_hq_site_for_active_site(active_site: ActiveSite) -> Site: .first() ) if orphan is None and active_site.code: - # e.g. code "sukawarna" ↔ site_name "Sukawarna" orphan = ( Site.objects.filter( user=owner, active_site__isnull=True, site_name__iexact=active_site.code @@ -40,10 +68,45 @@ def ensure_hq_site_for_active_site(active_site: ActiveSite) -> Site: .first() ) if orphan is not None: - orphan.active_site = active_site - orphan.save(update_fields=["active_site", "updated_at"]) - return orphan + if orphan.pk == desired_id: + orphan.active_site = active_site + orphan.save(update_fields=["active_site", "updated_at"]) + return orphan + return _rekey_site( + orphan, + desired_id=desired_id, + site_name=site_name, + owner=owner, + active_site=active_site, + ) return Site.objects.create( - site_name=site_name, user=owner, active_site=active_site + site_id=desired_id, + site_name=site_name, + user=owner, + active_site=active_site, ) + + +def _rekey_site( + old: Site, + *, + desired_id: str, + site_name: str, + owner, + active_site: ActiveSite | None = None, +) -> Site: + """Move an HQ site (and its kandangs) onto the farm slug primary key.""" + link = active_site if active_site is not None else old.active_site + new_site = Site.objects.create( + site_id=desired_id, + site_name=site_name or old.site_name, + user=owner, + active_site=None, + ) + Kandang.objects.filter(site=old).update(site=new_site) + old_active = old.active_site + old.delete() + new_site.active_site = link or old_active + new_site.save(update_fields=["active_site", "updated_at"]) + return new_site diff --git a/backend/apps/farms/tests.py b/backend/apps/farms/tests.py index 1c8dac2..3a8fc0a 100644 --- a/backend/apps/farms/tests.py +++ b/backend/apps/farms/tests.py @@ -12,14 +12,19 @@ from apps.sync.models import ActiveSite def _gm_visible_site(*, owner: User, site_name: str, code: str) -> Site: """GM scope only sees sites linked to an ActiveSite they manage.""" + site_id = f"{code}-0001" active_site = ActiveSite.objects.create( code=code, name=site_name, + source_site_id=site_id, api_base_url=f"https://{code}.example/api/v1", managed_by=owner, ) return Site.objects.create( - site_name=site_name, user=owner, active_site=active_site + site_id=site_id, + site_name=site_name, + user=owner, + active_site=active_site, ) diff --git a/backend/apps/operations/tests.py b/backend/apps/operations/tests.py index 54566a8..f6fdaaf 100644 --- a/backend/apps/operations/tests.py +++ b/backend/apps/operations/tests.py @@ -7,6 +7,25 @@ from rest_framework.test import APIClient from apps.accounts.models import User from apps.farms.models import Cycle, Flock, Kandang, Site from apps.operations.models import ChickenWeight, FeedSacks, IotPanel +from apps.sync.models import ActiveSite + + +def _linked_site(user: User, *, site_name: str = "Sukawarna", code: str = "sukawarna") -> Site: + """GM-visible site linked to an ActiveSite (Pengaturan registry).""" + site_id = f"{code}-0001" + active = ActiveSite.objects.create( + code=code, + name=site_name, + source_site_id=site_id, + api_base_url=f"https://{code}.example/api/v1", + managed_by=user, + ) + return Site.objects.create( + site_id=site_id, + site_name=site_name, + user=user, + active_site=active, + ) @override_settings(DASHBOARD_PUBLISH_HOUR=0) @@ -15,7 +34,7 @@ class OperationsAPITests(TestCase): self.user = User.objects.create_user(user_name="tester", password="secret") self.client = APIClient() self.client.force_authenticate(self.user) - site = Site.objects.create(site_name="Sukawarna", user=self.user) + site = _linked_site(self.user) self.kandang = Kandang.objects.create(kandang_name="Kandang 1", site=site) self.cycle = Cycle.objects.create( kandang=self.kandang, @@ -314,7 +333,7 @@ class DashboardVisibilityTests(TestCase): self.user = User.objects.create_user(user_name="tester", password="secret") self.client = APIClient() self.client.force_authenticate(self.user) - site = Site.objects.create(site_name="Sukawarna", user=self.user) + site = _linked_site(self.user) self.kandang = Kandang.objects.create(kandang_name="Kandang 1", site=site) self.cycle = Cycle.objects.create( kandang=self.kandang, diff --git a/backend/apps/operations/tests_chicken_counting_edge.py b/backend/apps/operations/tests_chicken_counting_edge.py index 9e1fe27..0d10490 100644 --- a/backend/apps/operations/tests_chicken_counting_edge.py +++ b/backend/apps/operations/tests_chicken_counting_edge.py @@ -15,6 +15,24 @@ from apps.operations.services.chicken_counting_edge import ( location_prefix_for_kandang, sync_chicken_counting_for_cycle, ) +from apps.sync.models import ActiveSite + + +def _linked_site(user: User, *, site_name: str = "Sukawarna", code: str = "sukawarna") -> Site: + site_id = f"{code}-0001" + active = ActiveSite.objects.create( + code=code, + name=site_name, + source_site_id=site_id, + api_base_url=f"https://{code}.example/api/v1", + managed_by=user, + ) + return Site.objects.create( + site_id=site_id, + site_name=site_name, + user=user, + active_site=active, + ) DB_HISTORY_SAMPLE = [ {"date": "2026-09-02", "location": "K1-L1", "total": 14591}, @@ -133,7 +151,7 @@ class ChickenCountingSyncApiTests(TestCase): self.user = User.objects.create_user(user_name="tester", password="secret") self.client = APIClient() self.client.force_authenticate(self.user) - site = Site.objects.create(site_name="Sukawarna", user=self.user) + site = _linked_site(self.user) self.kandang = Kandang.objects.create(kandang_name="Kandang 1", site=site) self.cycle = Cycle.objects.create( kandang=self.kandang, diff --git a/backend/apps/sync/models.py b/backend/apps/sync/models.py index f8b8fab..34c1d54 100644 --- a/backend/apps/sync/models.py +++ b/backend/apps/sync/models.py @@ -20,7 +20,7 @@ class ActiveSite(models.Model): blank=True, default="", db_index=True, - help_text="Farm-location Site.site_id (slug) from the registering dashboard", + help_text="Farm-location Site.site_id (slug); equals HQ sites.site_id when claimed", ) api_base_url = models.URLField( max_length=255, diff --git a/backend/apps/sync/tests_register.py b/backend/apps/sync/tests_register.py index 6c873ca..87ebf5d 100644 --- a/backend/apps/sync/tests_register.py +++ b/backend/apps/sync/tests_register.py @@ -149,6 +149,7 @@ class ClaimActiveSiteTests(TestCase): self.active = ActiveSite.objects.create( code="sukawarna", name="Sukawarna", + source_site_id="sukawarna-0001", api_base_url="https://sukawarna.example/api/v1", api_key="site-secret", managed_by=None, @@ -164,7 +165,18 @@ class ClaimActiveSiteTests(TestCase): self.assertEqual(response.status_code, 200, response.data) self.active.refresh_from_db() self.assertEqual(self.active.managed_by_id, self.gm.pk) - self.assertTrue(Site.objects.filter(active_site=self.active, user=self.gm).exists()) + hq = Site.objects.get(active_site=self.active, user=self.gm) + self.assertEqual(hq.pk, "sukawarna-0001") + self.assertEqual(hq.site_id, self.active.source_site_id) + + def test_claimed_site_api_id_is_farm_slug_string(self): + self.client.force_authenticate(self.gm) + self.client.post("/api/v1/active-sites/", {"code": "sukawarna"}, format="json") + listed = self.client.get("/api/v1/sites/") + self.assertEqual(listed.status_code, 200) + row = next(r for r in listed.data["results"] if r["site_name"] == "Sukawarna") + self.assertEqual(row["id"], "sukawarna-0001") + self.assertIsInstance(row["id"], str) def test_claim_unknown_code_404(self): self.client.force_authenticate(self.gm) @@ -191,7 +203,10 @@ class ClaimActiveSiteTests(TestCase): self.active.source_site_id = "sukawarna-0001" self.active.save(update_fields=["managed_by", "source_site_id"]) hq_site = Site.objects.create( - site_name="Sukawarna", user=self.gm, active_site=self.active + site_id="sukawarna-0001", + site_name="Sukawarna", + user=self.gm, + active_site=self.active, ) self.client.force_authenticate(self.gm) response = self.client.delete(f"/api/v1/active-sites/{self.active.pk}/") diff --git a/backend/database/erd_schema.sql b/backend/database/erd_schema.sql index c9dbf4a..ed3d765 100644 --- a/backend/database/erd_schema.sql +++ b/backend/database/erd_schema.sql @@ -10,6 +10,7 @@ -- is_active true, is_superuser false. Product roles never use is_staff.) -- -- user_access → sites → kandang → cycles +-- (sites.site_id = farm slug = active_sites.source_site_id when linked) -- kandang → flock → iot_panel -- user_access → api_keys -- user_access → active_sites (managed_by) @@ -103,10 +104,12 @@ CREATE INDEX IF NOT EXISTS idx_active_sites_managed_by_id ON active_sites (manag CREATE INDEX IF NOT EXISTS idx_active_sites_source_site_id ON active_sites (source_site_id); -- ----------------------------------------------------------------------------- --- 4. Sites +-- 4. Sites (HQ farm tree) +-- site_id is the farm dashboard Site.site_id slug (e.g. sukawarna-4821). +-- When linked, it equals active_sites.source_site_id. -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS sites ( - site_id BIGSERIAL PRIMARY KEY, + site_id VARCHAR(64) PRIMARY KEY, site_name VARCHAR(30) NOT NULL, user_id BIGINT NOT NULL REFERENCES user_access (user_id) ON DELETE CASCADE, active_site_id BIGINT UNIQUE REFERENCES active_sites (active_site_id) ON DELETE SET NULL, @@ -123,7 +126,7 @@ CREATE TABLE IF NOT EXISTS kandang ( kandang_id BIGSERIAL PRIMARY KEY, kandang_name VARCHAR(30) NOT NULL, feed_in_button_urls JSONB NOT NULL DEFAULT '[]'::jsonb, - site_id BIGINT NOT NULL REFERENCES sites (site_id) ON DELETE CASCADE, + site_id VARCHAR(64) NOT NULL REFERENCES sites (site_id) ON DELETE CASCADE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP ); diff --git a/components/Header.tsx b/components/Header.tsx index c78338e..77866ae 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -119,7 +119,7 @@ const Header: React.FC = ({