اکتپکت — ردیاب عادت و چالش، برای کاربر ایرانی
یک اپلیکیشن عادت و چالش که تقویم جلالی در عمق آن است، نه رویش: رابط فارسی، راستبهچپ از سر تا ته، و ورود با پیامک بدون نیاز به ایمیل.
روی actpact.ir بالاست: حدود ۱۶ هزار خط پایتون، ۱۹ جدول و ۸۰۶ تست — نصبشدنی بهعنوان PWA و منتشرشده بهعنوان اپ اندروید.
مسئله
ردیابهای عادت فرض میکنند ماه میلادی است و رابط، لاتین و چپبهراست — و کاربر ایرانی در هیچکدام زندگی نمیکند. سهمیهی ماهانه («این ماه دوازده جلسه») باید اول فروردین صفر شود، نه اول مارس. یک ماه میلادی روی دو ماه جلالی میافتد؛ پس اگر بر اساس ماه میلادی دستهبندی کنی، سهمیهی یک نفر بین دو دوره تکه میشود و هیچکس نمیفهمد چرا شمارشش نصف شده است.
یک اپلیکیشن FastAPI با فرانتاند Jinja2 که سمت سرور رندر میشود، و یک پوستهی Capacitor دور همان سایتِ دیپلویشده برای اندروید.
تقویم تا ته پایین میرود. کلید هر occurrence جلالی است، مرزهای دوره مرزهای واقعی جلالیاند، و هفته شنبه شروع میشود. تبدیل، حساب ساده است و هیچ وابستگی ندارد؛ تستش تمام ~۴۴٬۰۰۰ روزِ ۱۳۰۰ تا ۱۴۲۰ را با تقویم فارسی ICU مقایسه میکند — همان مرجعی که مرورگر با آن فرمت میکند. پس سرور نمیتواند تاریخی را در ماهی بگذارد که رابط کاربری اسم دیگری رویش میگذارد.
«امروز» سؤالی دربارهی عضو است، نه دربارهی ساعتِ سرور. هر ثبتنام منطقهی زمانی خودش را دارد؛ یک لحظهی UTC یکسان برای دو نفر در دو منطقه دو کلید متفاوت میدهد. برای همین موتور occurrence یک ماژول خالص است، بدون دیتابیس و بدون route: میشود کامل تستش کرد، و شده. ثبت انجام، با یک unique constraint idempotent است — روی گوشی با آنتن ضعیف، تلاش دوباره حالت عادی است نه خطا.
مجوز یک گزارهی SQL است، نه یک if. دسترسی به هر شیء چهار WHERE ترکیبشده
است؛ ردیفی که کاربر حق دیدنش را ندارد اصلاً بارگذاری نمیشود و نبودنش از دل
کوئری درمیآید، نه از تصمیمی که بعد از خواندن گرفته شده باشد. چهار نظام نقش —
اپ، چالش، گروه، دوره — عمداً هرگز ادغام نشدهاند و تستها روی چیزی تأکید میکنند
که یک ادمین نمیتواند انجام دهد: ادمین حق مدیریت و حذف دارد و حق
CHALLENGE_EDIT ندارد، چون عوضکردن وضعیت یک چالش مدیریت است و عوضکردن
حرفش تألیف.
هر چیزی که بشود محاسبه کرد، ذخیره نمیشود. استریک با پیمودن رو به عقبِ
occurrenceهای مورد انتظار دوباره حساب میشود؛ در کل کد یک += 1 وجود ندارد.
رتبهی جدول هم زنده شمرده میشود، چون یک شمارندهی همیشهصعودی چالشِ رهاشده را
بالاتر از چالشِ زنده مینشاند.
Web Push مستقیم روی RFC 8291 و RFC 8188 با VAPID مطابق RFC 8292 نوشته شده — حدود صد خط، یک وابستگی بهجای سه تا. مهمتر از رمزنگاری، ترتیب بود: نوتیفیکیشن فقط وقتی فرستاده میشود که تراکنش قطعاً تمام شده باشد، چون نوتیفیکیشنی که روی صفحهقفل نشسته پس گرفته نمیشود.
نه باندلر، نه فریمورک، نه node_modules در وباپ. سرویسورکر فایلهای استاتیک
را کش میکند و از کشکردن صفحهی HTML سر باز میزند: هر صفحه حلقهها و
شمارندههایش را زنده حساب میکند و یک صفحهی کششده یعنی یک صفحه عددِ غلط
که مطمئن به نظر میرسند. مخزن کد خصوصی است، چون این محصولی است که میخواهم اجرایش کنم؛
معماریاش مستند است و با کمال میل هر بخشش را توضیح میدهم.