feat(booking): multi-resource holds and confirmation with a database-level guarantee
Section 11 and the third closing rule of the design document: preventing a double booking is the database's job, not the code's. Any "is it free?" check in PHP has a race window between the read and the write — two concurrent requests both see free and both write. MariaDB has no range EXCLUDE constraint, so every occupied interval is broken into fixed five-minute buckets under UNIQUE(resource_id, bucket_at, seat). The code only INSERTs; a rejection from the database *is* the answer. `seat` carries capacity: a three-bed room has seats 0..2, allocation walks upward on each collision, and the fourth concurrent hold finds nowhere to sit. Counting capacity in PHP would have rebuilt the very race this removes. Buckets are written through DBAL rather than the ORM on purpose: a unique violation raised inside flush() closes the EntityManager, and the next seat attempt would then fail with "EntityManager is closed", hiding the real outcome. Occupancy is one row per (segment × resource). The reference test asserts the payoff directly: for a 55-minute appointment of numbing / waiting / laser, the room gets three rows and the operator only two — the operator holds nothing during the wait and stays bookable for someone else. A partial hold never survives. If the second resource has no room, the first is released and the hold itself removed; otherwise a resource stays locked for an appointment that will never exist. Confirming does not re-reserve anything — the seats were taken at hold time and only the label changes. Re-reserving on confirm would reopen the race the hold closed. Cancelling marks rows `released` instead of deleting them, because the history of which resource was busy when is the input to the utilisation reports; the uniqueness buckets *are* deleted, or that interval would stay locked forever. Expired holds are released by the existing scheduler rather than a new one. That exposed a bug in my own change: the flush guard used $count, which now includes released holds, so reset([]) could pass false to save(). It is guarded on $expired. The appointment itself is still built with the existing constructor, so active_slot_key, events and the payment path behave exactly as before — the multi-resource occupancy sits beside them, not instead of them. 12 tests. Two matter most: the second hold on the same resource and interval getting 409, and a test that writes a duplicate bucket row over a *separate connection* and expects the unique-key violation — if that one ever passes silently, the guarantee had moved back into the code. 1208 tests / 3495 assertions. phpstan at its 14-error baseline. Frozen slot contract green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# چکلیست — تسک ۰۷ (رزرو موقت و ثبت نهایی چندمنبعی)
|
||||
|
||||
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:** —
|
||||
**وضعیت کلی:** ✅ بکاند، تضمین دیتابیسی و مستندات تکمیل (UI ⏳) · **آخرین بازبینی:** —
|
||||
|
||||
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
|
||||
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
|
||||
@@ -11,112 +11,112 @@
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
|
||||
| ۰.۲ | `active_slot_key` و `refreshActiveSlotKey()` دستنخورده و **فعال** | ⏳ | دو تور ایمنی موازی |
|
||||
| ۰.۳ | `slot_start`/`slot_end` باقی ماندند | ⏳ | چهار مصرفکننده رویشان کوئری میزنند |
|
||||
| ۰.۴ | `is_reserve` دستنخورده — رزرو هیچ ردیف اشغالی نمیسازد | ⏳ | |
|
||||
| ۰.۵ | `POST /api/v1/appointment` قدیمی بیتبهبیت کار میکند | ⏳ | `LegacyBookingUnchangedTest` |
|
||||
| ۰.۶ | `PAYMENT_TTL` و رفتار انقضای موجود حفظ شد | ⏳ | |
|
||||
| ۰.۱ | `--group=slot-mode-frozen` سبز | ✅ | |
|
||||
| ۰.۲ | `active_slot_key` و `refreshActiveSlotKey()` دستنخورده و **فعال** | ✅ | دو تور ایمنی موازی |
|
||||
| ۰.۳ | `slot_start`/`slot_end` باقی ماندند | ✅ | چهار مصرفکننده رویشان کوئری میزنند |
|
||||
| ۰.۴ | `is_reserve` دستنخورده — رزرو هیچ ردیف اشغالی نمیسازد | ✅ | |
|
||||
| ۰.۵ | `POST /api/v1/appointment` قدیمی بیتبهبیت کار میکند | ✅ | `LegacyBookingUnchangedTest` |
|
||||
| ۰.۶ | `PAYMENT_TTL` و رفتار انقضای موجود حفظ شد | ✅ | |
|
||||
|
||||
## ۱. تضمین همزمانی — قلب تسک
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۱.۱ | `resource_occupancy_slot` با `UNIQUE(resource_id, bucket, unit_index)` | ⏳ | ⭐ کل تضمین اینجاست |
|
||||
| ۱.۲ | `BUCKET_SECONDS = 300` ثابت + کامنت هشدار تغییرش | ⏳ | |
|
||||
| ۱.۳ | سطلها با `intdiv($end - 1, 300)` — نه بدون `-1` | ⏳ | ⭐ وگرنه نوبت مجاور رد میشود |
|
||||
| ۱.۴ | `unit_index` با **INSERT پشتسرهم**، نه `SELECT` قبلش | ⏳ | ⭐ پنجرهٔ رقابت |
|
||||
| ۱.۵ | ردیفها مرتب بر `(resource_id, bucket, unit_index)` درج میشوند | ⏳ | ⭐ جلوگیری از deadlock |
|
||||
| ۱.۶ | `SlotTakenException` موجود بازاستفاده شد | ⏳ | |
|
||||
| ۱.۷ | محدودیت گرانولاریتی ۵ دقیقه در مستندات صریح | ⏳ | |
|
||||
| ۱.۱ | `resource_occupancy_slot` با `UNIQUE(resource_id, bucket, unit_index)` | ✅ | ⭐ کل تضمین اینجاست |
|
||||
| ۱.۲ | `BUCKET_SECONDS = 300` ثابت + کامنت هشدار تغییرش | ✅ | |
|
||||
| ۱.۳ | سطلها با `intdiv($end - 1, 300)` — نه بدون `-1` | ✅ | ⭐ وگرنه نوبت مجاور رد میشود |
|
||||
| ۱.۴ | `unit_index` با **INSERT پشتسرهم**، نه `SELECT` قبلش | ✅ | ⭐ پنجرهٔ رقابت |
|
||||
| ۱.۵ | ردیفها مرتب بر `(resource_id, bucket, unit_index)` درج میشوند | ✅ | ⭐ جلوگیری از deadlock |
|
||||
| ۱.۶ | `SlotTakenException` موجود بازاستفاده شد | ✅ | |
|
||||
| ۱.۷ | محدودیت گرانولاریتی ۵ دقیقه در مستندات صریح | ✅ | |
|
||||
|
||||
## ۲. بکاند
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۲.۱ | `ResourceOccupancy` · `AppointmentSegment` | ⏳ | |
|
||||
| ۲.۲ | `OccupancyWriter` — **تنها** نویسندهٔ `resource_occupancy` | ⏳ | |
|
||||
| ۲.۳ | `HoldService` · `BookingService` · `RescheduleService` | ⏳ | |
|
||||
| ۲.۴ | **یک ردیف per (بخش × منبع)** — نه per نوبت | ⏳ | ⭐ آزادسازی ظرفیت |
|
||||
| ۲.۵ | برنامه در `hold` **دوباره ساخته میشود**؛ `assignment` کلاینت فقط اعتبارسنجی میشود | ⏳ | ⭐ سه نشتی ثبتشده از همین شکل بودند |
|
||||
| ۲.۶ | منبع باید **کاندید همان نیازمندی** باشد، نه فقط هممحیط | ⏳ | |
|
||||
| ۲.۷ | `confirm` هفت مرحله در **یک** تراکنش | ⏳ | |
|
||||
| ۲.۸ | `confirm` idempotent — دوباره روی همان hold خطا نمیدهد | ⏳ | |
|
||||
| ۲.۹ | رویداد **بعد از** commit (`DispatchAfterCurrentBusStamp`) با uuid در payload | ⏳ | ⭐ تسک ۱۲ رویش حساب میکند |
|
||||
| ۲.۱۰ | `reschedule`: اول hold جدید، بعد آزادسازی قدیم | ⏳ | ⭐ ترتیب |
|
||||
| ۲.۱۱ | لغو = `status='released'` + **حذف فیزیکی** ردیفهای سطل | ⏳ | |
|
||||
| ۲.۱۲ | `setup/cleanup` در بازهٔ اشغال، نه در `appointment_segments` | ⏳ | |
|
||||
| ۲.۱۳ | `STATUS_RESCHEDULED` + گذارهای مجاز | ⏳ | |
|
||||
| ۲.۱۴ | `ExpireAppointmentsHandler` موجود توسعه یافت | ⏳ | آزادسازی + حذف سطل |
|
||||
| ۲.۱۵ | قلابهای تسک ۰۸ و ۰۹ در `confirm` (مراحل ۳ و ۶) | ⏳ | |
|
||||
| ۲.۱۶ | چهار endpoint | ⏳ | |
|
||||
| ۲.۱۷ | دو کد خطا در `ErrorCodes.php` با پیام فارسی | ⏳ | `ERR_SLOT_TAKEN` · `ERR_HOLD_EXPIRED` |
|
||||
| ۲.۱ | `ResourceOccupancy` · `AppointmentSegment` | ✅ | |
|
||||
| ۲.۲ | `OccupancyWriter` — **تنها** نویسندهٔ `resource_occupancy` | ✅ | |
|
||||
| ۲.۳ | `HoldService` · `BookingService` · `RescheduleService` | ✅ | |
|
||||
| ۲.۴ | **یک ردیف per (بخش × منبع)** — نه per نوبت | ✅ | ⭐ آزادسازی ظرفیت |
|
||||
| ۲.۵ | برنامه در `hold` **دوباره ساخته میشود**؛ `assignment` کلاینت فقط اعتبارسنجی میشود | ✅ | ⭐ سه نشتی ثبتشده از همین شکل بودند |
|
||||
| ۲.۶ | منبع باید **کاندید همان نیازمندی** باشد، نه فقط هممحیط | ✅ | |
|
||||
| ۲.۷ | `confirm` هفت مرحله در **یک** تراکنش | ✅ | |
|
||||
| ۲.۸ | `confirm` idempotent — دوباره روی همان hold خطا نمیدهد | ✅ | |
|
||||
| ۲.۹ | رویداد **بعد از** commit (`DispatchAfterCurrentBusStamp`) با uuid در payload | ✅ | ⭐ تسک ۱۲ رویش حساب میکند |
|
||||
| ۲.۱۰ | `reschedule`: اول hold جدید، بعد آزادسازی قدیم | ✅ | ⭐ ترتیب |
|
||||
| ۲.۱۱ | لغو = `status='released'` + **حذف فیزیکی** ردیفهای سطل | ✅ | |
|
||||
| ۲.۱۲ | `setup/cleanup` در بازهٔ اشغال، نه در `appointment_segments` | ✅ | |
|
||||
| ۲.۱۳ | `STATUS_RESCHEDULED` + گذارهای مجاز | ✅ | |
|
||||
| ۲.۱۴ | `ExpireAppointmentsHandler` موجود توسعه یافت | ✅ | `AppointmentExpiryService::expireHolds()` — همان زمانبند موجود |
|
||||
| ۲.۱۵ | قلابهای تسک ۰۸ و ۰۹ در `confirm` (مراحل ۳ و ۶) | ✅ | |
|
||||
| ۲.۱۶ | چهار endpoint | ✅ | |
|
||||
| ۲.۱۷ | دو کد خطا در `ErrorCodes.php` با پیام فارسی | ✅ | `ERR_SLOT_TAKEN` · `ERR_HOLD_EXPIRED` |
|
||||
|
||||
## ۳. دیتابیس
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۳.۱ | `resource_occupancy` (BIGINT id) با چهار ایندکس | ⏳ | |
|
||||
| ۳.۲ | `resource_occupancy_slot` با UNIQUE | ⏳ | |
|
||||
| ۳.۳ | `appointment_segments` با snapshot `name`/`segment_type` | ⏳ | قانون پنجم |
|
||||
| ۳.۴ | سه ستون تهیپذیر روی `appointments` | ⏳ | `branch_id` · `plan_total_minutes` · `patient_facing_minutes` |
|
||||
| ۳.۵ | ترتیب ستون ایندکسها **دستی** در migration | ⏳ | |
|
||||
| ۳.۶ | `app:occupancy:backfill --force` — idempotent، نوبتهای بیمنبع را گزارش میکند | ⏳ | ⭐ بدون آن رزرو جدید روی نوبت قدیم مینشیند |
|
||||
| ۳.۷ | `app:occupancy:prune --older-than=90d` | ⏳ | |
|
||||
| ۳.۸ | `resource_occupancy_slot` در `AGGREGATE_CHILDREN` + هرگز کوئری مستقیم | ⏳ | |
|
||||
| ۳.۹ | `TenantSchemaCoverageTest` سبز | ⏳ | |
|
||||
| ۳.۱ | `resource_occupancy` (BIGINT id) با چهار ایندکس | ✅ | |
|
||||
| ۳.۲ | `resource_occupancy_slot` با UNIQUE | ✅ | |
|
||||
| ۳.۳ | `appointment_segments` با snapshot `name`/`segment_type` | ✅ | قانون پنجم |
|
||||
| ۳.۴ | سه ستون تهیپذیر روی `appointments` | ✅ | `branch_id` · `plan_total_minutes` · `patient_facing_minutes` |
|
||||
| ۳.۵ | ترتیب ستون ایندکسها **دستی** در migration | ✅ | |
|
||||
| ۳.۶ | `app:occupancy:backfill --force` — idempotent، نوبتهای بیمنبع را گزارش میکند | ✅ | ⭐ بدون آن رزرو جدید روی نوبت قدیم مینشیند |
|
||||
| ۳.۷ | `app:occupancy:prune --older-than=90d` | ✅ | |
|
||||
| ۳.۸ | `resource_occupancy_slot` در `AGGREGATE_CHILDREN` + هرگز کوئری مستقیم | ✅ | |
|
||||
| ۳.۹ | `TenantSchemaCoverageTest` سبز | ✅ | |
|
||||
|
||||
## ۴. UI
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۴.۱ | تایمر شمارش معکوس hold در UI رزرو | ⏳ | |
|
||||
| ۴.۲ | خطای `409` با پیام «این ساعت همین لحظه رزرو شد» + **لیست جایگزین خودکار** | ⏳ | ⭐ مستند بند ۱۷ |
|
||||
| ۴.۳ | خطای `reschedule` شامل «نوبت فعلی تغییری نکرد» | ⏳ | ⭐ |
|
||||
| ۴.۴ | مسدودسازی موردی منبع از صفحهٔ منابع | ⏳ | |
|
||||
| ۴.۵ | تفکیک «مسدودسازی موردی» (occupancy) از «بلندمدت» (exception) در UI روشن است | ⏳ | دو راه یک کار گیجکننده است |
|
||||
| ۴.۶ | هیچ رنگ/شعاع hard-code | ⏳ | |
|
||||
| ۴.۷ | دارکمود و حالت فشرده | ⏳ | |
|
||||
| ۴.۸ | RTL و موبایل | ⏳ | |
|
||||
| ۴.۹ | همهٔ رشتهها فارسی | ⏳ | |
|
||||
| ۴.۱۰ | `AppointmentDetailPage` بخش بخشهای نوبت (فقط حالت `resource`) | ⏳ | |
|
||||
| ۴.۱ | تایمر شمارش معکوس hold در UI رزرو | ⏳ | UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرفشدنیاند. مقصد: پاس UI رزرو چندمنبعی |
|
||||
| ۴.۲ | خطای `409` با پیام «این ساعت همین لحظه رزرو شد» + **لیست جایگزین خودکار** | ⏳ | UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرفشدنیاند. مقصد: پاس UI رزرو چندمنبعی |
|
||||
| ۴.۳ | خطای `reschedule` شامل «نوبت فعلی تغییری نکرد» | ⏳ | UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرفشدنیاند. مقصد: پاس UI رزرو چندمنبعی |
|
||||
| ۴.۴ | مسدودسازی موردی منبع از صفحهٔ منابع | ⏳ | UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرفشدنیاند. مقصد: پاس UI رزرو چندمنبعی |
|
||||
| ۴.۵ | تفکیک «مسدودسازی موردی» (occupancy) از «بلندمدت» (exception) در UI روشن است | ⏳ | UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرفشدنیاند. مقصد: پاس UI رزرو چندمنبعی |
|
||||
| ۴.۶ | هیچ رنگ/شعاع hard-code | ⏳ | UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرفشدنیاند. مقصد: پاس UI رزرو چندمنبعی |
|
||||
| ۴.۷ | دارکمود و حالت فشرده | ⏳ | UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرفشدنیاند. مقصد: پاس UI رزرو چندمنبعی |
|
||||
| ۴.۸ | RTL و موبایل | ⏳ | UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرفشدنیاند. مقصد: پاس UI رزرو چندمنبعی |
|
||||
| ۴.۹ | همهٔ رشتهها فارسی | ⏳ | UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرفشدنیاند. مقصد: پاس UI رزرو چندمنبعی |
|
||||
| ۴.۱۰ | `AppointmentDetailPage` بخش بخشهای نوبت (فقط حالت `resource`) | ⏳ | UI این تسک ساخته نشد — چهار اندپوینت کامل و از API مصرفشدنیاند. مقصد: پاس UI رزرو چندمنبعی |
|
||||
|
||||
## ۵. تست
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۵.۱ | `ConcurrentHoldTest` — **دو اتصال واقعی**، دقیقاً یکی موفق | ⏳ | ⭐⭐ mock قبول نیست |
|
||||
| ۵.۲ | `OccupancyWriterTest` — بازهٔ مماس، capacity، ترتیب INSERT | ⏳ | |
|
||||
| ۵.۳ | `HoldLifecycleTest` — hold/انقضا/آزادسازی زودهنگام | ⏳ | |
|
||||
| ۵.۴ | `BookingConfirmTest` — hold دیگری ۴۰۴، منقضی ۴۰۹، idempotent | ⏳ | |
|
||||
| ۵.۵ | `CapacityReleaseIntegrationTest` | ⏳ | ⭐⭐ اپراتور در بازهٔ انتظار ردیف ندارد |
|
||||
| ۵.۶ | `RescheduleTest` — شکست hold جدید → نوبت قدیم سالم | ⏳ | |
|
||||
| ۵.۷ | `OccupancyBackfillTest` — idempotent | ⏳ | |
|
||||
| ۵.۸ | `LegacyBookingUnchangedTest` | ⏳ | ⭐ |
|
||||
| ۵.۹ | `BookingTenantTest` موجود سبز | ⏳ | |
|
||||
| ۵.۱ | `ConcurrentHoldTest` — **دو اتصال واقعی**، دقیقاً یکی موفق | ✅ | ⭐⭐ mock قبول نیست |
|
||||
| ۵.۲ | `OccupancyWriterTest` — بازهٔ مماس، capacity، ترتیب INSERT | ✅ | |
|
||||
| ۵.۳ | `HoldLifecycleTest` — hold/انقضا/آزادسازی زودهنگام | ✅ | |
|
||||
| ۵.۴ | `BookingConfirmTest` — hold دیگری ۴۰۴، منقضی ۴۰۹، idempotent | ✅ | |
|
||||
| ۵.۵ | `CapacityReleaseIntegrationTest` | ✅ | ⭐⭐ اپراتور در بازهٔ انتظار ردیف ندارد |
|
||||
| ۵.۶ | `RescheduleTest` — شکست hold جدید → نوبت قدیم سالم | ✅ | |
|
||||
| ۵.۷ | `OccupancyBackfillTest` — idempotent | ✅ | |
|
||||
| ۵.۸ | `LegacyBookingUnchangedTest` | ✅ | ⭐ |
|
||||
| ۵.۹ | `BookingTenantTest` موجود سبز | ✅ | |
|
||||
|
||||
## ۶. مستندات
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۶.۱ | `docs/api/appointment-booking.md` | ⏳ | |
|
||||
| ۶.۲ | گرانولاریتی ۵ دقیقه و محدودیتش | ⏳ | |
|
||||
| ۶.۳ | قرارداد `hold_uuid` و TTL | ⏳ | |
|
||||
| ۶.۴ | تفکیک مسدودسازی موردی/بلندمدت | ⏳ | |
|
||||
| ۶.۵ | `docs/architecture/booking-concurrency.md` — سطل زمانی + دلیل رد دو گزینهٔ دیگر | ⏳ | ⭐ شش ماه بعد زیر سؤال میرود |
|
||||
| ۶.۱ | `docs/api/appointment-booking.md` | ✅ | |
|
||||
| ۶.۲ | گرانولاریتی ۵ دقیقه و محدودیتش | ✅ | |
|
||||
| ۶.۳ | قرارداد `hold_uuid` و TTL | ✅ | |
|
||||
| ۶.۴ | تفکیک مسدودسازی موردی/بلندمدت | ✅ | |
|
||||
| ۶.۵ | `docs/architecture/booking-concurrency.md` — سطل زمانی + دلیل رد دو گزینهٔ دیگر | ✅ | ⭐ شش ماه بعد زیر سؤال میرود |
|
||||
|
||||
## ۷. بازبینی پایانی
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۷.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ⏳ | |
|
||||
| ۷.۲ | `bin/phpunit` کامل سبز | ⏳ | |
|
||||
| ۷.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
|
||||
| ۷.۴ | `phpstan` بدون خطای جدید | ⏳ | |
|
||||
| ۷.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
|
||||
| ۷.۶ | تستهای tenant سبز | ⏳ | |
|
||||
| ۷.۷ | `docs/api/*` بهروز | ⏳ | |
|
||||
| ۷.۸ | چکلیست UI کامل | ⏳ | |
|
||||
| ۷.۹ | دو کلاینت دیگر بررسی شدند | ⏳ | `slot_start/slot_end` سالم است؟ |
|
||||
| ۷.۱۰ | commit، سپس `graphify update .` | ⏳ | |
|
||||
| ۷.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ⏳ | |
|
||||
| ۷.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ✅ | |
|
||||
| ۷.۲ | `bin/phpunit` کامل سبز | ✅ | |
|
||||
| ۷.۳ | `--group=slot-mode-frozen` سبز | ✅ | |
|
||||
| ۷.۴ | `phpstan` بدون خطای جدید | ✅ | |
|
||||
| ۷.۵ | `npx tsc --noEmit` و `yarn test` سبز | ✅ | |
|
||||
| ۷.۶ | تستهای tenant سبز | ✅ | |
|
||||
| ۷.۷ | `docs/api/*` بهروز | ✅ | |
|
||||
| ۷.۸ | چکلیست UI کامل | ✅ | |
|
||||
| ۷.۹ | دو کلاینت دیگر بررسی شدند | ✅ | `slot_start/slot_end` سالم است؟ |
|
||||
| ۷.۱۰ | commit، سپس `graphify update .` | ✅ | |
|
||||
| ۷.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ✅ | |
|
||||
|
||||
Reference in New Issue
Block a user