feat(logging): implement log pruning functionality

- Add LogPruneService to handle the deletion of old logs based on retention settings.
- Create PruneLogsCommand to provide a console command for log pruning.
- Introduce PruneLogsMessage and PruneLogsHandler for message handling related to log pruning.
- Update the AST cache with new classes and their relationships.
This commit is contained in:
hamed
2026-07-01 21:50:51 +03:30
parent a31e8b4314
commit 7814bcc0de
27 changed files with 1667 additions and 802 deletions
+148 -32
View File
@@ -1,12 +1,14 @@
import React, { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { TrashIcon } from '@heroicons/react/24/outline';
import { api } from '../lib/api';
import type { PaginatedResponse } from '../lib/api';
import type { PaginatedResponse, ApiResponse } from '../lib/api';
import type { AppLog } from '../types';
import { formatDateTime } from '../lib/utils';
import DataTable, { Column } from '../components/ui/DataTable';
import Pagination from '../components/ui/Pagination';
import Modal from '../components/ui/Modal';
import ConfirmDialog from '../components/ui/ConfirmDialog';
import SearchableSelect from '../components/ui/SearchableSelect';
const LEVEL_META: Record<string, { label: string; cls: string }> = {
@@ -27,12 +29,17 @@ const LEVEL_FILTER_OPTIONS = [
{ value: 'critical', label: 'بحرانی' },
];
type Tab = 'logs' | 'settings';
export default function LogsPage() {
const [tab, setTab] = useState<Tab>('logs');
const [page, setPage] = useState(1);
const [level, setLevel] = useState('');
const [search, setSearch] = useState('');
const [viewLog, setViewLog] = useState<AppLog | null>(null);
const [confirmClear, setConfirmClear] = useState(false);
const limit = 25;
const queryClient = useQueryClient();
const logsQuery = useQuery({
queryKey: ['admin-logs', page, level, search],
@@ -44,6 +51,15 @@ export default function LogsPage() {
),
});
const clearMutation = useMutation({
mutationFn: () => api.delete<ApiResponse<{ deleted: number }>>('/api/v1/admin/logs'),
onSuccess: () => {
setConfirmClear(false);
setPage(1);
queryClient.invalidateQueries({ queryKey: ['admin-logs'] });
},
});
const columns: Column<AppLog>[] = [
{ key: 'created_at', header: 'زمان', render: (l) => formatDateTime(l.created_at) },
{
@@ -78,42 +94,71 @@ export default function LogsPage() {
<h1 className="section-title">لاگها</h1>
<div className="muted">رویدادهای ثبتشدهی سیستم (اخطار و بالاتر)</div>
</div>
{tab === 'logs' && (
<button
className="btn danger sm"
onClick={() => setConfirmClear(true)}
disabled={(logsQuery.data?.meta?.totalRecords ?? 0) === 0}
>
<TrashIcon style={{ width: 16, height: 16 }} />
حذف همه لاگها
</button>
)}
</div>
<div className="card">
<div className="card-pad">
<div style={{ marginBottom: 12, display: 'flex', gap: 12, justifyContent: 'flex-end', flexWrap: 'wrap' }}>
<input
className="input"
style={{ maxWidth: 260 }}
placeholder="جستجو در متن..."
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
/>
<div style={{ width: 200 }}>
<SearchableSelect
options={LEVEL_FILTER_OPTIONS}
value={level}
onChange={(v) => { setLevel(v ? String(v) : ''); setPage(1); }}
placeholder="همه سطوح"
<div style={{ display: 'flex', gap: 8, marginBottom: 'var(--gap)' }}>
<button
className={`btn ${tab === 'logs' ? 'primary' : 'ghost'} sm`}
onClick={() => setTab('logs')}
>
فهرست لاگها
</button>
<button
className={`btn ${tab === 'settings' ? 'primary' : 'ghost'} sm`}
onClick={() => setTab('settings')}
>
مدت نگهداری
</button>
</div>
{tab === 'logs' && (
<div className="card">
<div className="card-pad">
<div style={{ marginBottom: 12, display: 'flex', gap: 12, justifyContent: 'flex-end', flexWrap: 'wrap' }}>
<input
className="input"
style={{ maxWidth: 260 }}
placeholder="جستجو در متن..."
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
/>
<div style={{ width: 200 }}>
<SearchableSelect
options={LEVEL_FILTER_OPTIONS}
value={level}
onChange={(v) => { setLevel(v ? String(v) : ''); setPage(1); }}
placeholder="همه سطوح"
/>
</div>
</div>
</div>
<DataTable<AppLog>
columns={columns}
data={logsQuery.data?.data ?? []}
loading={logsQuery.isLoading}
emptyMessage="لاگی یافت نشد"
/>
<Pagination
page={page}
total={logsQuery.data?.meta?.totalRecords ?? 0}
limit={limit}
onPageChange={setPage}
/>
<DataTable<AppLog>
columns={columns}
data={logsQuery.data?.data ?? []}
loading={logsQuery.isLoading}
emptyMessage="لاگی یافت نشد"
/>
<Pagination
page={page}
total={logsQuery.data?.meta?.totalRecords ?? 0}
limit={limit}
onPageChange={setPage}
/>
</div>
</div>
</div>
)}
{tab === 'settings' && <RetentionSettings />}
<Modal open={!!viewLog} title="جزئیات لاگ" onClose={() => setViewLog(null)}>
{viewLog && (
@@ -137,6 +182,77 @@ export default function LogsPage() {
</div>
)}
</Modal>
<ConfirmDialog
open={confirmClear}
title="حذف همه لاگ‌ها"
message="همه‌ی لاگ‌های سیستم برای همیشه حذف می‌شوند. این عمل قابل بازگشت نیست. ادامه می‌دهید؟"
confirmLabel="حذف همه"
danger
loading={clearMutation.isPending}
onConfirm={() => clearMutation.mutate()}
onCancel={() => setConfirmClear(false)}
/>
</div>
);
}
function RetentionSettings() {
const queryClient = useQueryClient();
const [days, setDays] = useState<string>('');
const [touched, setTouched] = useState(false);
const settingsQuery = useQuery({
queryKey: ['admin-settings'],
queryFn: () => api.get<ApiResponse<Record<string, string | boolean>>>('/api/v1/admin/settings'),
});
const current = String(settingsQuery.data?.data?.log_retention_days ?? '');
const value = touched ? days : current;
const saveMutation = useMutation({
mutationFn: (retentionDays: string) =>
api.patch<ApiResponse<Record<string, string | boolean>>>('/api/v1/admin/settings', {
log_retention_days: retentionDays,
}),
onSuccess: () => {
setTouched(false);
queryClient.invalidateQueries({ queryKey: ['admin-settings'] });
},
});
return (
<div className="card">
<div className="card-pad">
<div style={{ maxWidth: 420, display: 'flex', flexDirection: 'column', gap: 12 }}>
<div>
<label className="muted" style={{ fontSize: 13, display: 'block', marginBottom: 6 }}>
مدت نگهداری لاگها (روز)
</label>
<input
className="input"
type="number"
min={0}
value={value}
placeholder={settingsQuery.isLoading ? 'در حال بارگذاری...' : '90'}
onChange={(e) => { setDays(e.target.value); setTouched(true); }}
style={{ maxWidth: 200 }}
/>
<div className="muted" style={{ fontSize: 12, marginTop: 6, lineHeight: 1.7 }}>
لاگهای قدیمیتر از این مدت بهصورت روزانه بهصورت خودکار حذف میشوند. مقدار ۰ یعنی نگهداری نامحدود.
</div>
</div>
<div>
<button
className="btn primary sm"
disabled={saveMutation.isPending || !touched || value === current}
onClick={() => saveMutation.mutate(String(parseInt(value || '0', 10)))}
>
{saveMutation.isPending ? 'در حال ذخیره...' : 'ذخیره'}
</button>
</div>
</div>
</div>
</div>
);
}
+1
View File
@@ -17,6 +17,7 @@ framework:
routing:
'App\Shared\Message\SendSmsMessage': async
'App\Appointment\Message\ExpireAppointmentsMessage': scheduler_default
'App\Shared\Logging\Message\PruneLogsMessage': scheduler_default
when@test:
framework:
+23
View File
@@ -845,6 +845,7 @@ Returns all site configuration values.
"support_phone": "",
"max_cancel_hours_before": "24",
"appointment_reminder_hours": "2",
"log_retention_days": "90",
"payment_test_mode": "0",
"mellat_terminal_id": "",
"mellat_username": "",
@@ -984,6 +985,7 @@ Reject a pending request. **Permission:** `ROLE_ADMIN`
| `tax_percent` | `10` | درصد مالیات |
| `sms_panel_fee_rials` | `1500000` | هزینه ثابت پنل پیامک به ریال (از نوبت و اشتراک کسر می‌شود) |
| `appointment_fee_rials` | `150000` | مبلغ هر نوبت به ریال؛ مبلغی که بیمار هنگام رزرو آنلاین پرداخت می‌کند. backend از همین کلید می‌خواند و در `GET /api/v1/payment/config` expose می‌شود |
| `log_retention_days` | `90` | مدت نگهداری لاگ‌ها (روز)؛ کاماند روزانه `app:prune-logs` لاگ‌های قدیمی‌تر را حذف می‌کند. `0` = نگهداری نامحدود |
**ترتیب محاسبه** (در `CommissionService`): ۱) کسر `sms_panel_fee_rials` ۲) مالیاتِ استخراجی `afterSms × tax/(100+tax)` ۳) پورسانت = `netAfterTax × percent/100`. سهم نماینده به کیف‌پولش (`WalletTransaction` credit) واریز و یک ردیف `FinancialBreakdown` ثبت می‌شود (idempotent بر اساس `payment_id`).
@@ -1128,3 +1130,24 @@ Ordered by newest first (`id DESC`).
Notes:
- `context` is a JSON string (or `null`); a `Throwable` in the context is stored as a compact `Class: message @ file:line` string, never the raw object.
- `created_at` is a Unix timestamp (integer).
### DELETE `/api/v1/admin/logs`
Delete **all** persisted logs (truncate the `app_log` table). Irreversible.
**Permission:** `ROLE_ADMIN`
#### Response `200`
```json
{ "success": true, "data": { "deleted": 137 } }
```
- `deleted` — number of rows removed.
### Log Retention
Logs are pruned automatically based on the `log_retention_days` setting (see [Site Settings](#) — `GET`/`PATCH /api/v1/admin/settings`, whitelisted key `log_retention_days`, default `90`, `0` = keep forever).
- A daily scheduled task (`App\Shared\Logging\Message\PruneLogsMessage`, registered in `src/Schedule.php`, routed to `scheduler_default`) deletes logs older than `log_retention_days`.
- Manual prune: `php bin/console app:prune-logs` (reads the same setting, deletes older-than-retention rows, prints the count).
- Requires the scheduler worker: `php bin/console messenger:consume scheduler_default`.
+2 -1
View File
@@ -677,5 +677,6 @@
"675": "Community 675",
"676": "Community 676",
"677": "Community 677",
"678": "Community 678"
"678": "Community 678",
"679": "Community 679"
}
+98 -113
View File
@@ -1,16 +1,16 @@
# Graph Report - clinicpro (2026-07-01)
## Corpus Check
- 668 files · ~472,125 words
- 672 files · ~472,879 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 8432 nodes · 11619 edges · 679 communities (551 shown, 128 thin omitted)
- Extraction: 98% EXTRACTED · 2% INFERRED · 0% AMBIGUOUS · INFERRED: 264 edges (avg confidence: 0.8)
- 8457 nodes · 11652 edges · 680 communities (547 shown, 133 thin omitted)
- Extraction: 98% EXTRACTED · 2% INFERRED · 0% AMBIGUOUS · INFERRED: 265 edges (avg confidence: 0.8)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `31201322`
- Built from commit: `a31e8b43`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -679,6 +679,7 @@
- [[_COMMUNITY_Community 676|Community 676]]
- [[_COMMUNITY_Community 677|Community 677]]
- [[_COMMUNITY_Community 678|Community 678]]
- [[_COMMUNITY_Community 679|Community 679]]
## God Nodes (most connected - your core abstractions)
1. `BaseController` - 76 edges
@@ -688,46 +689,46 @@
5. `Clinic` - 50 edges
6. `Doctor` - 48 edges
7. `useAuthStore` - 43 edges
8. `ApiResponse` - 40 edges
8. `ApiResponse` - 41 edges
9. `formatDate()` - 39 edges
10. `formatRial()` - 36 edges
10. `AdminApiController` - 37 edges
## Surprising Connections (you probably didn't know these)
- `ServiceTariffModal()` --calls--> `formatNumber()` [EXTRACTED]
assets/admin/components/ServiceTariffModal.tsx → assets/admin/lib/utils.ts
- `PersianDatePicker()` --calls--> `formatDate()` [EXTRACTED]
assets/admin/components/ui/PersianDatePicker.tsx → assets/admin/lib/utils.ts
- `LogsPage()` --calls--> `formatDateTime()` [EXTRACTED]
assets/admin/pages/LogsPage.tsx → assets/admin/lib/utils.ts
- `SmsPage()` --calls--> `formatDateTime()` [EXTRACTED]
assets/admin/pages/SmsPage.tsx → assets/admin/lib/utils.ts
- `NewAppointmentModal()` --calls--> `useAuthStore` [EXTRACTED]
assets/admin/pages/AppointmentsPage.tsx → assets/admin/stores/authStore.ts
- `LogoUploadField()` --calls--> `useAuthStore` [EXTRACTED]
assets/admin/pages/CategoriesPage.tsx → assets/admin/stores/authStore.ts
## Import Cycles
- None detected.
## Communities (679 total, 128 thin omitted)
## Communities (680 total, 133 thin omitted)
### Community 0 - "Community 0"
Cohesion: 0.05
Nodes (40): ALLOWED_ROLES, PrivateRoute(), PublicRoute(), RoleRoute(), queryClient, toastStyle, LogoUploadField(), TabActions() (+32 more)
Nodes (37): ALLOWED_ROLES, PrivateRoute(), PublicRoute(), RoleRoute(), queryClient, toastStyle, AddForm, addSchema (+29 more)
### Community 1 - "Community 1"
Cohesion: 0.03
Nodes (41): AddressData, AddrForm, addrSchema, AVATAR_COLORS, BookingMeta, CityOpt, DateOverrideData, DEFAULT_BOOKING_META (+33 more)
### Community 2 - "Community 2"
Cohesion: 0.06
Nodes (26): get, api, ApiError, getToken(), refreshOnce(), request(), { refreshMock, logoutMock }, replaceMock (+18 more)
Cohesion: 0.07
Nodes (26): get, PaymentConfig, PaymentGatewayInfo, api, ApiError, getToken(), refreshOnce(), request() (+18 more)
### Community 3 - "Community 3"
Cohesion: 0.05
Nodes (50): ApiResponse, PaginatedResponse, formatDate(), ALL_STATUSES, AppointmentDetailPage(), STATUS_FILTERS, FILTERS, Breakdown (+42 more)
Cohesion: 0.03
Nodes (80): ApiResponse, PaginatedResponse, formatDate(), ALL_STATUSES, AppointmentDetailPage(), STATUS_FILTERS, FILTERS, Breakdown (+72 more)
### Community 4 - "Community 4"
Cohesion: 0.05
Nodes (11): DoctorServiceController, DoctorService, Doctor, DoctorServiceRepository, Collection, self, User, WeeklySchedule (+3 more)
Cohesion: 0.06
Nodes (8): DoctorService, Doctor, DoctorServiceRepository, Collection, self, User, WeeklySchedule, ManagerRegistry
### Community 5 - "Community 5"
Cohesion: 0.07
@@ -754,28 +755,28 @@ Cohesion: 0.04
Nodes (46): Clinic Address Management, Clinic API, DELETE `/api/v1/admin/clinic/{clinicUuid}/doctor/{doctorUuid}`, `DELETE /api/v1/clinic/{clinicUuid}/address/{addressUuid}`, Errors, Errors, Errors, Errors (+38 more)
### Community 11 - "Community 11"
Cohesion: 0.06
Nodes (38): PaymentConfig, PaymentGatewayInfo, usePaymentConfig(), SmsPage(), STATUS_LOG_META, Tab, TAG_FILTER_OPTIONS, TAG_LABELS (+30 more)
Cohesion: 0.33
Nodes (4): BlogController, JsonResponse, Request, User
### Community 12 - "Community 12"
Cohesion: 0.05
Nodes (44): Clinic Doctor Invitation API, DELETE `/api/v1/admin/clinic/invitation/{invUuid}`, Errors, Errors, Errors, Errors, Errors, Errors (+36 more)
### Community 13 - "Community 13"
Cohesion: 0.07
Nodes (37): FreeVisitPrice(), Pricing, cn(), formatDateTime(), formatRial(), iranMobileOptionalSchema, iranMobileSchema, isValidIranMobile() (+29 more)
Cohesion: 0.06
Nodes (35): FreeVisitPrice(), Pricing, usePaymentConfig(), formatDateTime(), formatRial(), InsurancePricingPage(), LogsPage(), FinancialSummary (+27 more)
### Community 14 - "Community 14"
Cohesion: 0.15
Nodes (8): AppointmentSettingsController, Holiday, HolidayRepository, JsonResponse, Request, User, Doctor, ManagerRegistry
### Community 15 - "Community 15"
Cohesion: 0.09
Nodes (13): PaymentController, MellatGateway, MockGateway, JsonResponse, Payment, PaymentGatewayInterface, Request, Response (+5 more)
Cohesion: 0.07
Nodes (16): PaymentController, MellatGateway, MockGateway, SepGateway, JsonResponse, Payment, PaymentGatewayInterface, Request (+8 more)
### Community 16 - "Community 16"
Cohesion: 0.06
Nodes (8): PatientSession, SmsWallet, AppointmentExpiryService, Appointment, Collection, PatientRecord, self, SessionService
Cohesion: 0.07
Nodes (7): PatientSession, SmsWallet, Appointment, Collection, PatientRecord, self, SessionService
### Community 17 - "Community 17"
Cohesion: 0.05
@@ -798,8 +799,8 @@ Cohesion: 0.05
Nodes (37): formatNumber(), ClinicDetailPage(), AdminCharts, AdminDashboard(), AdminRecent, AdminStats, APPT_CLS, APPT_COLOR (+29 more)
### Community 22 - "Community 22"
Cohesion: 0.09
Nodes (15): RatingController, Like, Rate, CommentListNPlusOneTest, LikeRepository, RateRepository, JsonResponse, Request (+7 more)
Cohesion: 0.15
Nodes (9): RatingController, Like, LikeRepository, JsonResponse, Request, User, Comment, ManagerRegistry (+1 more)
### Community 23 - "Community 23"
Cohesion: 0.05
@@ -814,8 +815,8 @@ Cohesion: 0.05
Nodes (39): Appointment API, Error Responses, Errors, Errors, Errors, Errors, Errors, Errors (+31 more)
### Community 26 - "Community 26"
Cohesion: 0.24
Nodes (4): InsuranceController, JsonResponse, Request, User
Cohesion: 0.15
Nodes (7): InsuranceController, EntityInsurancePricing, EntityInsurancePricingRepository, JsonResponse, Request, User, ManagerRegistry
### Community 27 - "Community 27"
Cohesion: 0.05
@@ -826,8 +827,8 @@ Cohesion: 0.09
Nodes (4): Appointment, Doctor, self, User
### Community 29 - "Community 29"
Cohesion: 0.10
Nodes (21): Bulk import / export, DELETE `/api/v1/admin/city/{id}`, DELETE `/api/v1/admin/province/{id}`, Errors, Errors, GET `/api/v1/admin/provinces`, GET `/api/v1/categorys/{bundle}` *(Legacy)*, GET `/api/v1/provinces` (+13 more)
Cohesion: 0.06
Nodes (35): Bulk import / export, DELETE `/api/v1/admin/city/{id}`, DELETE `/api/v1/admin/province/{id}`, Errors, Errors, Errors, Errors, GET `/api/v1/admin/cities` (+27 more)
### Community 30 - "Community 30"
Cohesion: 0.08
@@ -842,8 +843,8 @@ Cohesion: 0.06
Nodes (32): 10. Modal / Dialog, 11. Toast Notifications, 12. Empty States & Loading, 13. Page Header (هر صفحه), 14. تکنولوژی Stack, 15. Responsive Breakpoints, 16. Dark Mode (اختیاری — فاز دوم), 17. نمونه رنگ‌بندی صفحه داشبورد (+24 more)
### Community 33 - "Community 33"
Cohesion: 0.04
Nodes (40): CoverageRow, Draft, KIND, TenantInsurance, ServiceTariffModal(), TariffResponse, TariffRow, Contract (+32 more)
Cohesion: 0.08
Nodes (20): CoverageRow, Draft, KIND, TenantInsurance, ServiceTariffModal(), TariffResponse, TariffRow, EMPTY_ITEMS (+12 more)
### Community 34 - "Community 34"
Cohesion: 0.06
@@ -874,8 +875,8 @@ Cohesion: 0.09
Nodes (4): User, PasswordAuthenticatedUserInterface, self, UserInterface
### Community 41 - "Community 41"
Cohesion: 0.06
Nodes (21): CityForm, citySchema, ImportError, InsuranceForm, insuranceSchema, ProvinceForm, provinceSchema, ServiceForm (+13 more)
Cohesion: 0.05
Nodes (29): Contract, InsuranceOption, KIND_LABEL, CityForm, citySchema, ImportError, InsuranceForm, insuranceSchema (+21 more)
### Community 42 - "Community 42"
Cohesion: 0.07
@@ -922,8 +923,8 @@ Cohesion: 0.07
Nodes (26): الزامات UI, باگ‌فیکس صفحه نوبت‌ها, باگ ۱ — کرش تقویم, باگ ۲ — روز هفته در DateNavigator, باگ ۳ — پیام «slot نیست», باگ ۴ — نوبت جدید: نام اجباری + find-or-create patient, باگ ۵ — patient_mobile نشان می‌دهد موبایل پزشک, باگ ۶ — نوبت‌های رزرو شده در نمایش زمانبندی (+18 more)
### Community 53 - "Community 53"
Cohesion: 0.08
Nodes (15): AppLogRepository, CityRepository, ClaimItemRepository, InvoiceItemRepository, PreRegistrationRepository, SiteConfigRepository, ServiceEntityRepository, ManagerRegistry (+7 more)
Cohesion: 0.09
Nodes (12): AppLogRepository, ClaimItemRepository, PreRegistrationRepository, SiteConfigRepository, TaxRateHistoryRepository, ServiceEntityRepository, ManagerRegistry, ManagerRegistry (+4 more)
### Community 54 - "Community 54"
Cohesion: 0.10
@@ -942,8 +943,8 @@ Cohesion: 0.08
Nodes (24): Blog API, DELETE `/api/v1/blog/{uuid}`, Errors, Errors, Errors, Errors, Errors, GET `/api/v1/blog/{slug}` (+16 more)
### Community 58 - "Community 58"
Cohesion: 0.15
Nodes (8): Blog, BlogController, BlogRepository, JsonResponse, Request, User, ManagerRegistry, QueryBuilder
Cohesion: 0.28
Nodes (4): Blog, BlogRepository, ManagerRegistry, QueryBuilder
### Community 59 - "Community 59"
Cohesion: 0.22
@@ -966,8 +967,8 @@ Cohesion: 0.22
Nodes (5): AuthController, RateLimiterFactory, JsonResponse, Request, User
### Community 64 - "Community 64"
Cohesion: 0.32
Nodes (4): SmsWalletController, JsonResponse, Request, User
Cohesion: 0.20
Nodes (7): SmsWalletController, SmsSettingsRepository, SmsSettings, JsonResponse, Request, User, ManagerRegistry
### Community 65 - "Community 65"
Cohesion: 0.08
@@ -1010,8 +1011,8 @@ Cohesion: 0.16
Nodes (5): DoctorSecretary, Clinic, Doctor, self, User
### Community 77 - "Community 77"
Cohesion: 0.08
Nodes (15): Claim, ClaimItem, DebtRow, InsuranceOption, KIND_LABEL, STATUS_FILTERS, STATUS_META, IbanItem (+7 more)
Cohesion: 0.10
Nodes (13): Claim, ClaimItem, DebtRow, InsuranceOption, KIND_LABEL, STATUS_FILTERS, STATUS_META, IbanItem (+5 more)
### Community 78 - "Community 78"
Cohesion: 0.12
@@ -1039,7 +1040,7 @@ Nodes (29): devDependencies, @babel/core, @babel/preset-env, @babel/preset-react
### Community 86 - "Community 86"
Cohesion: 0.06
Nodes (15): AppointmentExpiryServiceTest, LowTierFixesTest, SendCodeMobileRateLimitTest, ClaimsListPaginationTest, ServiceItemStaffOwnershipTest, EntityManagerInterface, KernelBrowser, CommentPaginationTest (+7 more)
Nodes (15): AppointmentExpiryServiceTest, LowTierFixesTest, SendCodeMobileRateLimitTest, ClaimsListPaginationTest, ServiceItemDeleteCleanupTest, ServiceItemStaffOwnershipTest, EntityManagerInterface, KernelBrowser (+7 more)
### Community 87 - "Community 87"
Cohesion: 0.10
@@ -1126,8 +1127,8 @@ Cohesion: 0.32
Nodes (6): AppointmentController, Appointment, Doctor, JsonResponse, Request, User
### Community 108 - "Community 108"
Cohesion: 0.13
Nodes (10): BaseController, CategoryController, CategoryImportController, SmsMessageController, JsonResponse, JsonResponse, Request, JsonResponse (+2 more)
Cohesion: 0.12
Nodes (12): AbstractController, AdminController, BaseController, CategoryController, CategoryImportController, HomeController, Response, JsonResponse (+4 more)
### Community 109 - "Community 109"
Cohesion: 0.29
@@ -1327,7 +1328,7 @@ Nodes (15): Date Overrides, DELETE `/api/v1/appointment-settings/date-override/{
### Community 161 - "Community 161"
Cohesion: 0.24
Nodes (7): ClaimSubmitterInterface, ClaimService, ManualClaimSubmitter, Claim, Invoice, Claim, ClaimSubmissionResult
Nodes (5): ClaimsListNPlusOneTest, ClaimItem, ClaimService, Claim, Invoice
### Community 162 - "Community 162"
Cohesion: 0.13
@@ -1474,12 +1475,12 @@ Cohesion: 0.14
Nodes (13): Endpoint ها, PATCH /api/v1/secretary/{uuid}, POST /api/v1/secretary, تسک ۱۴: ماژول منشی, توضیح, زمان تخمینی, ساختار JSON, سیستم مجوزها — Resource-Based Permissions (مقیاس‌پذیر) (+5 more)
### Community 201 - "Community 201"
Cohesion: 0.15
Nodes (13): Admin API, Clinic Invitation Management, GET `/api/v1/admin/representations`, GET `/api/v1/admin/secretaries`, GET /api/v1/admin/settings, PATCH /api/v1/admin/settings, Query Parameters, Query Parameters (+5 more)
Cohesion: 0.20
Nodes (10): Admin API, Clinic Invitation Management, GET `/api/v1/admin/comments`, GET `/api/v1/admin/rates`, GET /api/v1/admin/settings, PATCH /api/v1/admin/settings, Query Parameters, Query Parameters (+2 more)
### Community 202 - "Community 202"
Cohesion: 0.15
Nodes (13): DELETE `/api/v1/appointment-settings/holidays/{uuid}`, Errors, GET `/api/v1/appointment-settings/holidays/list/{doctorUuid}`, GET `/api/v1/appointment-settings/holidays/{uuid}`, Holidays, PATCH `/api/v1/appointment-settings/holidays/{uuid}`, POST `/api/v1/appointment-settings/holidays`, Request Body (+5 more)
Cohesion: 0.10
Nodes (19): Appointment Settings API, Available Locations, DELETE `/api/v1/appointment-settings/holidays/{uuid}`, Errors, Errors, `GET /api/v1/appointment-settings/available-locations/{doctorUuid}`, GET `/api/v1/appointment-settings/holidays/list/{doctorUuid}`, GET `/api/v1/appointment-settings/holidays/{uuid}` (+11 more)
### Community 203 - "Community 203"
Cohesion: 0.15
@@ -1490,8 +1491,8 @@ Cohesion: 0.21
Nodes (6): AuthenticationException, ExceptionSubscriber, SecurityHeadersSubscriber, EventSubscriberInterface, ExceptionEvent, ResponseEvent
### Community 205 - "Community 205"
Cohesion: 0.13
Nodes (13): Command, CancelExpiredAppointmentsCommand, CreateAdminCommand, SeedCategoriesCommand, SeedSmsMessageTemplatesCommand, InputInterface, OutputInterface, InputInterface (+5 more)
Cohesion: 0.07
Nodes (20): Command, CancelExpiredAppointmentsCommand, CreateAdminCommand, PruneLogsCommand, SeedCategoriesCommand, SeedSmsMessageTemplatesCommand, SmsMessageTemplateRepository, SmsTextResolver (+12 more)
### Community 206 - "Community 206"
Cohesion: 0.35
@@ -1566,8 +1567,8 @@ Cohesion: 0.30
Nodes (6): AbstractAuthenticator, Passport, PasswordAuthenticator, Request, Response, TokenInterface
### Community 228 - "Community 228"
Cohesion: 0.05
Nodes (43): Bulk import / export, DELETE `/api/v1/admin/insurance/{id}`, DELETE `/api/v1/billing/tenant-insurances/{uuid}`, DELETE `/api/v1/insurance/{id}`, EntityInsurancePricing — قیمت‌گذاری ویزیت بر اساس بیمه, Errors, Errors, Errors (+35 more)
Cohesion: 0.04
Nodes (47): Bulk import / export, DELETE `/api/v1/admin/insurance/{id}`, DELETE `/api/v1/billing/tenant-insurances/{uuid}`, DELETE `/api/v1/insurance/{id}`, EntityInsurancePricing — قیمت‌گذاری ویزیت بر اساس بیمه, Errors, Errors, Errors (+39 more)
### Community 229 - "Community 229"
Cohesion: 0.15
@@ -1674,8 +1675,8 @@ Cohesion: 0.29
Nodes (3): FinancialBreakdown, Payment, User
### Community 258 - "Community 258"
Cohesion: 0.22
Nodes (5): ExpireAppointmentsMessage, ExpireAppointmentsHandler, ScheduleProviderInterface, Schedule, SymfonySchedule
Cohesion: 0.15
Nodes (7): ExpireAppointmentsMessage, ExpireAppointmentsHandler, PruneLogsHandler, PruneLogsMessage, ScheduleProviderInterface, Schedule, SymfonySchedule
### Community 259 - "Community 259"
Cohesion: 0.18
@@ -1702,8 +1703,8 @@ Cohesion: 0.33
Nodes (4): RepresentationRepository, ManagerRegistry, Representation, User
### Community 265 - "Community 265"
Cohesion: 0.15
Nodes (17): calcFinalPrice(), EMPTY_RECORDS, EMPTY_SESSIONS, fileNumber(), getPatientName(), getPatientPhone(), InsurancePricing, MyPatientsPageInner() (+9 more)
Cohesion: 0.08
Nodes (27): calcFinalPrice(), EMPTY_RECORDS, EMPTY_SESSIONS, fileNumber(), getPatientName(), getPatientPhone(), InsurancePricing, MyPatientsPageInner() (+19 more)
### Community 266 - "Community 266"
Cohesion: 0.33
@@ -1738,7 +1739,7 @@ Cohesion: 0.18
Nodes (10): Endpoint های موجود که تغییر می‌کنند, GET /api/v1/admin/dashboard/charts?from=UNIX&to=UNIX, GET /api/v1/dashboard/clinic, GET /api/v1/dashboard/doctor, تسک ۱۶: داشبورد هوشمند — چارت + فیلتر زمانی, توضیح, زمان تخمینی, فیلتر بازه زمانی (+2 more)
### Community 274 - "Community 274"
Cohesion: 0.21
Cohesion: 0.20
Nodes (5): Authentication, ClinicPro — API Documentation Index, Error Code Reference, Modules, Standard Response Envelope
### Community 275 - "Community 275"
@@ -1818,8 +1819,8 @@ Cohesion: 0.24
Nodes (10): gridItemStyle, JALALI_MONTHS, jalaliFirstWeekday(), jalaliToGregorian(), navBtnStyle, PersianCalendar(), pf, Props (+2 more)
### Community 295 - "Community 295"
Cohesion: 0.22
Nodes (8): AbstractController, AdminController, HomeController, SeoController, Response, Response, Request, Response
Cohesion: 0.57
Nodes (3): SeoController, Request, Response
### Community 296 - "Community 296"
Cohesion: 0.22
@@ -1855,7 +1856,7 @@ Nodes (16): آماده‌سازی پروژه ClinicPro برای دیپلوی ر
### Community 304 - "Community 304"
Cohesion: 0.22
Nodes (9): 29. 🟢 `GET` clinic list 🆕, 30. 🟢 `GET` get 🆕, 33. 🔵 `POST` image_clinic, 6. کلینیک (Clinic), هدرهای اضافی, پارامترهای Query, پاسخ‌ها, پاسخ‌ها (+1 more)
Nodes (9): 29. 🟢 `GET` clinic list 🆕, 30. 🟢 `GET` get 🆕, 36. 🟢 `GET` get my rate, 6. کلینیک (Clinic), هدرهای اضافی, پارامترهای Query, پاسخ‌ها, پاسخ‌ها (+1 more)
### Community 306 - "Community 306"
Cohesion: 0.07
@@ -2022,8 +2023,8 @@ Cohesion: 0.50
Nodes (4): UserActiveContextRepository, ManagerRegistry, User, UserActiveContext
### Community 352 - "Community 352"
Cohesion: 0.25
Nodes (4): SmsMessageTemplateRepository, SmsTextResolver, SmsMessageTemplate, ManagerRegistry
Cohesion: 0.36
Nodes (3): DoctorServiceController, JsonResponse, Request
### Community 354 - "Community 354"
Cohesion: 0.36
@@ -2186,8 +2187,8 @@ Cohesion: 0.33
Nodes (6): Refactoring Plan, فاز ۰ — مستندسازی (۳ تا ۵ روز، قبل از هر کدنویسی), فاز ۱ — زیرساخت پایه (task-01), فاز ۲ — پیاده‌سازی ماژول‌ها (به ترتیب dependency), فاز ۳ — بهینه‌سازی (بعد از پیاده‌سازی), فاز ۴ — آماده‌سازی تولید
### Community 397 - "Community 397"
Cohesion: 0.21
Nodes (6): ClaimAmountBoundsTest, ClaimsListNPlusOneTest, ClaimItem, Claim, Doctor, User
Cohesion: 0.36
Nodes (4): ClaimAmountBoundsTest, Claim, Doctor, User
### Community 398 - "Community 398"
Cohesion: 0.29
@@ -2214,20 +2215,20 @@ Cohesion: 0.11
Nodes (18): [F10] راهنمای کهنه در `CLAUDE.md`: endpoint `categorys/{bundle}` منتقل شده, [F11] داشبورد دکتر `GET /api/v1/dashboard/doctor` همیشه 500 (فیلد ناموجود در DQL) — ✅ رفع شد, [F1] phpstan: مقایسهٔ همیشه‌درست در محاسبهٔ estimated SMS — ✅ رفع شد, [F2] تست‌های PHPUnit به API خارجی Kavenegar درخواست واقعی می‌زنند, [F3] دیتابیس تست seed نشده — فقط کاربر ادمین وجود دارد, [F4] اسکریپت seeder `create_test_users.php` وجود ندارد, [F5] ادمین با JWT معتبر به `/api/doc` (Swagger UI) دسترسی ندارد (401), [F6] ناسازگاری کدهای خطا بین دامنه‌ها (+10 more)
### Community 452 - "Community 452"
Cohesion: 0.08
Nodes (16): ClinicAddress, ClinicDoctorItem, ClinicInvitation, EditForm, editSchema, HUES_LIST, INV_STATUS_MAP, IRAN_CENTER (+8 more)
Cohesion: 0.05
Nodes (34): cn(), iranMobileOptionalSchema, iranMobileSchema, isValidIranMobile(), maskMobile(), sanitizeMobileInput(), toDate(), toEnglishDigits() (+26 more)
### Community 456 - "Community 456"
Cohesion: 0.29
Nodes (6): Appointment Settings API, Available Locations, Errors, `GET /api/v1/appointment-settings/available-locations/{doctorUuid}`, Response `200`, Slot Calculation Logic (Reference)
Cohesion: 0.38
Nodes (5): Rate, RateRepository, Doctor, ManagerRegistry, User
### Community 458 - "Community 458"
Cohesion: 0.47
Nodes (6): formatPersianDate(), gToJ(), jFirstDayOfWeek(), PersianDateInput(), todayGregorian(), toPersianNums()
### Community 459 - "Community 459"
Cohesion: 0.40
Nodes (5): API موجود (نیاز به تغییر ندارند), اپیک ۳ — منشی (Secretary) — تکمیل, تغییرات مورد نیاز, توضیح, نیازمندی‌های کارکردی
Cohesion: 0.33
Nodes (6): API موجود (نیاز به تغییر ندارند), اپیک‌ها, اپیک ۳ — منشی (Secretary) — تکمیل, تغییرات مورد نیاز, توضیح, نیازمندی‌های کارکردی
### Community 460 - "Community 460"
Cohesion: 0.33
@@ -2294,8 +2295,8 @@ Cohesion: 0.33
Nodes (5): ایندکس‌ها, جدول: sms_accounts (حساب پیامک), جدول: sms_queue (صف پیامک), نکات مهم, پایگاه داده — تسک ۱۷: ماژول پیامک
### Community 478 - "Community 478"
Cohesion: 0.40
Nodes (5): GET `/api/v1/admin/comments`, GET `/api/v1/admin/rates`, Query Parameters, Query Parameters, Rating & Comment Management
Cohesion: 0.39
Nodes (3): CityRepository, City, ManagerRegistry
### Community 479 - "Community 479"
Cohesion: 0.40
@@ -2330,8 +2331,8 @@ Cohesion: 0.40
Nodes (5): Admin Endpoints, GET /api/v1/admin/sms/settings/review, GET /api/v1/admin/sms/wallet-report, POST /api/v1/admin/sms/settings/{id}/approve, POST /api/v1/admin/sms/settings/{id}/reject
### Community 491 - "Community 491"
Cohesion: 0.15
Nodes (5): EntityInsurancePricing, TenantInsuranceCleanupTest, EntityInsurancePricingRepository, TenantInsuranceCleanupService, ManagerRegistry
Cohesion: 0.43
Nodes (3): SmsMessageController, JsonResponse, Request
### Community 493 - "Community 493"
Cohesion: 0.20
@@ -2362,8 +2363,8 @@ Cohesion: 0.40
Nodes (5): addMinutes(), calcSlotCount(), hasOverlap(), parseMinutes(), SessionEditor()
### Community 500 - "Community 500"
Cohesion: 0.33
Nodes (6): API موجود (نیاز به endpoint جدید ندارد), اپیک‌ها, اپیک ۷ — داشبورد هوشمند (Smart Dashboard), تغییر مورد نیاز, توضیح, نیازمندی‌های کارکردی
Cohesion: 0.40
Nodes (5): API موجود (نیاز به endpoint جدید ندارد), اپیک ۷ — داشبورد هوشمند (Smart Dashboard), تغییر مورد نیاز, توضیح, نیازمندی‌های کارکردی
### Community 501 - "Community 501"
Cohesion: 0.40
@@ -2502,8 +2503,8 @@ Cohesion: 0.67
Nodes (3): Errors, GET `/api/v1/notification-mobile/{target}`, Response `200`
### Community 540 - "Community 540"
Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/admin/insurance`, Request Body (`application/json`), Response `201`
Cohesion: 0.43
Nodes (3): InvoiceItemRepository, InvoiceItem, ManagerRegistry
### Community 542 - "Community 542"
Cohesion: 0.15
@@ -2666,8 +2667,8 @@ Cohesion: 0.20
Nodes (10): Configuration, DELETE `/api/v1/sms/template/{uuid}`, Errors, Errors, GET `/api/v1/admin/sms/templates`, GET `/api/v1/sms/template/{uuid}`, Response `200`, Response `200` (+2 more)
### Community 598 - "Community 598"
Cohesion: 0.31
Nodes (3): SepGateway, PaymentInitResult, PaymentVerifyResult
Cohesion: 0.53
Nodes (4): ClaimSubmitterInterface, ManualClaimSubmitter, Claim, ClaimSubmissionResult
### Community 599 - "Community 599"
Cohesion: 0.67
@@ -2706,8 +2707,8 @@ Cohesion: 0.67
Nodes (3): بک‌اند, فرانت‌اند, وضعیت فعلی کد (مهم — قبل از تغییر بخوان)
### Community 618 - "Community 618"
Cohesion: 0.16
Nodes (5): ServiceItemDeleteCleanupTest, ServiceCoverageNPlusOneTest, TenantServiceCoverageRepository, ManagerRegistry, TenantServiceCoverage
Cohesion: 0.12
Nodes (6): ServiceCoverageNPlusOneTest, TenantInsuranceCleanupTest, TenantServiceCoverageRepository, TenantInsuranceCleanupService, ManagerRegistry, TenantServiceCoverage
### Community 631 - "Community 631"
Cohesion: 0.50
@@ -2729,10 +2730,6 @@ Nodes (8): ۲.۲ انواع دسته‌بندی (Category Types), ۲.۲.۱ تگ
Cohesion: 0.43
Nodes (5): BeforeInstallPromptEvent, usePwaInstall(), PwaInstallBanner(), detectIOS(), PwaLoginCard()
### Community 636 - "Community 636"
Cohesion: 0.48
Nodes (3): SmsSettingsRepository, SmsSettings, ManagerRegistry
### Community 638 - "Community 638"
Cohesion: 0.67
Nodes (3): GET `/api/v1/clinic-pro/doctor-addresses/{doctorId}`, Path Parameters, Response `200`
@@ -2757,10 +2754,6 @@ Nodes (5): 31. 🟡 `PATCH` patch, Request Body, مثال Request, هدرهای
Cohesion: 0.40
Nodes (5): 32. 🔵 `POST` post, Request Body, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 651 - "Community 651"
Cohesion: 0.53
Nodes (3): TaxRateHistoryRepository, ManagerRegistry, TaxRateHistory
### Community 652 - "Community 652"
Cohesion: 0.50
Nodes (4): GET `/api/v1/admin/pre-registrations`, POST `/api/v1/admin/pre-registrations/{uuid}/approve`, POST `/api/v1/admin/pre-registrations/{uuid}/reject`, Pre-Registration Management
@@ -2787,11 +2780,11 @@ Nodes (4): GET `/api/v1/admin/payments`, Payment Management, Query Parameters, R
### Community 658 - "Community 658"
Cohesion: 0.50
Nodes (4): Errors, PATCH `/api/v1/admin/province/{id}`, Path Parameters, Response `200`
Nodes (4): GET `/api/v1/admin/representations`, Query Parameters, Representation Management, Response `200`
### Community 659 - "Community 659"
Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/admin/city`, Request Body (`application/json`), Response `201`
Nodes (4): GET `/api/v1/admin/secretaries`, Query Parameters, Response `200`, Secretary Management
### Community 660 - "Community 660"
Cohesion: 0.50
@@ -2802,8 +2795,8 @@ Cohesion: 0.50
Nodes (4): 40. 🔵 `POST` post, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 662 - "Community 662"
Cohesion: 0.50
Nodes (4): Application Logs, GET `/api/v1/admin/logs`, Query Parameters, Response `200`
Cohesion: 0.29
Nodes (7): Application Logs, DELETE `/api/v1/admin/logs`, GET `/api/v1/admin/logs`, Log Retention, Query Parameters, Response `200`, Response `200`
### Community 664 - "Community 664"
Cohesion: 0.50
@@ -2829,21 +2822,13 @@ Nodes (4): ۲.۱۳ نظرات، لایک و امتیازدهی, ۲.۱۳.۱ نظ
Cohesion: 0.50
Nodes (4): GET `/api/v1/admin/settlements`, Query Parameters, Response `200`, Settlement Management
### Community 673 - "Community 673"
Cohesion: 0.67
Nodes (3): GET `/api/v1/admin/cities`, Query Parameters, Response `200`
### Community 674 - "Community 674"
Cohesion: 0.67
Nodes (3): GET `/api/v1/cities`, Query Parameters, Response `200`
### Community 675 - "Community 675"
Cohesion: 0.67
Nodes (3): 34. 🔵 `POST` image logo, هدرهای اضافی, پاسخ‌ها
### Community 676 - "Community 676"
Cohesion: 0.67
Nodes (3): 36. 🟢 `GET` get my rate, هدرهای اضافی, پاسخ‌ها
Nodes (3): 33. 🔵 `POST` image_clinic, هدرهای اضافی, پاسخ‌ها
### Community 677 - "Community 677"
Cohesion: 0.67
@@ -2854,24 +2839,24 @@ Cohesion: 0.67
Nodes (3): 43. 🟢 `GET` get, هدرهای اضافی, پاسخ‌ها
## Knowledge Gaps
- **3640 isolated node(s):** `ALLOWED_ROLES`, `Pricing`, `TenantInsurance`, `CoverageRow`, `Draft` (+3635 more)
- **3644 isolated node(s):** `ALLOWED_ROLES`, `Pricing`, `TenantInsurance`, `CoverageRow`, `Draft` (+3639 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **128 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
- **133 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `BaseController` connect `Community 108` to `Community 4`, `Community 6`, `Community 138`, `Community 139`, `Community 14`, `Community 15`, `Community 275`, `Community 20`, `Community 22`, `Community 26`, `Community 164`, `Community 295`, `Community 300`, `Community 301`, `Community 175`, `Community 176`, `Community 177`, `Community 58`, `Community 59`, `Community 63`, `Community 64`, `Community 70`, `Community 71`, `Community 75`, `Community 206`, `Community 230`, `Community 103`, `Community 104`, `Community 107`, `Community 109`, `Community 121`, `Community 122`, `Community 252`?**
_High betweenness centrality (0.030) - this node is a cross-community bridge._
- **Why does `BaseController` connect `Community 108` to `Community 6`, `Community 138`, `Community 11`, `Community 139`, `Community 14`, `Community 15`, `Community 275`, `Community 20`, `Community 22`, `Community 26`, `Community 164`, `Community 300`, `Community 301`, `Community 175`, `Community 176`, `Community 177`, `Community 59`, `Community 63`, `Community 64`, `Community 70`, `Community 71`, `Community 75`, `Community 206`, `Community 352`, `Community 230`, `Community 103`, `Community 104`, `Community 107`, `Community 491`, `Community 109`, `Community 121`, `Community 122`, `Community 252`?**
_High betweenness centrality (0.031) - this node is a cross-community bridge._
- **Why does `AppointmentRepository` connect `Community 119` to `Community 53`?**
_High betweenness centrality (0.017) - this node is a cross-community bridge._
_High betweenness centrality (0.021) - this node is a cross-community bridge._
- **Why does `Version20260614181657` connect `Community 431` to `Community 399`?**
_High betweenness centrality (0.015) - this node is a cross-community bridge._
- **What connects `ALLOWED_ROLES`, `Pricing`, `TenantInsurance` to the rest of the system?**
_3640 weakly-connected nodes found - possible documentation gaps or missing edges._
_3644 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `Community 0` be split into smaller, more focused modules?**
_Cohesion score 0.05012531328320802 - nodes in this community are weakly interconnected._
_Cohesion score 0.054426705370101594 - nodes in this community are weakly interconnected._
- **Should `Community 1` be split into smaller, more focused modules?**
_Cohesion score 0.028985507246376812 - nodes in this community are weakly interconnected._
- **Should `Community 2` be split into smaller, more focused modules?**
_Cohesion score 0.06280193236714976 - nodes in this community are weakly interconnected._
_Cohesion score 0.06565656565656566 - nodes in this community are weakly interconnected._
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_logging_messagehandler_prunelogshandler_php", "label": "PruneLogsHandler.php", "file_type": "code", "source_file": "src/Shared/Logging/MessageHandler/PruneLogsHandler.php", "source_location": "L1"}, {"id": "messagehandler_prunelogshandler_prunelogshandler", "label": "PruneLogsHandler", "file_type": "code", "source_file": "src/Shared/Logging/MessageHandler/PruneLogsHandler.php", "source_location": "L9"}, {"id": "messagehandler_prunelogshandler_prunelogshandler_construct", "label": ".__construct()", "file_type": "code", "source_file": "src/Shared/Logging/MessageHandler/PruneLogsHandler.php", "source_location": "L12"}, {"id": "messagehandler_prunelogshandler_prunelogshandler_invoke", "label": ".__invoke()", "file_type": "code", "source_file": "src/Shared/Logging/MessageHandler/PruneLogsHandler.php", "source_location": "L14"}, {"id": "prunelogsmessage", "label": "PruneLogsMessage", "file_type": "code", "source_file": "src/Shared/Logging/MessageHandler/PruneLogsHandler.php", "source_location": "L14"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_logging_messagehandler_prunelogshandler_php", "target": "logpruneservice", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Logging/MessageHandler/PruneLogsHandler.php", "source_location": "L5", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_logging_messagehandler_prunelogshandler_php", "target": "prunelogsmessage", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Logging/MessageHandler/PruneLogsHandler.php", "source_location": "L6", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_logging_messagehandler_prunelogshandler_php", "target": "asmessagehandler", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Logging/MessageHandler/PruneLogsHandler.php", "source_location": "L7", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_logging_messagehandler_prunelogshandler_php", "target": "messagehandler_prunelogshandler_prunelogshandler", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/Shared/Logging/MessageHandler/PruneLogsHandler.php", "source_location": "L9", "weight": 1.0}, {"source": "messagehandler_prunelogshandler_prunelogshandler", "target": "messagehandler_prunelogshandler_prunelogshandler_construct", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Logging/MessageHandler/PruneLogsHandler.php", "source_location": "L12", "weight": 1.0}, {"source": "messagehandler_prunelogshandler_prunelogshandler", "target": "messagehandler_prunelogshandler_prunelogshandler_invoke", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Logging/MessageHandler/PruneLogsHandler.php", "source_location": "L14", "weight": 1.0}, {"source": "messagehandler_prunelogshandler_prunelogshandler_invoke", "target": "prunelogsmessage", "relation": "references", "confidence": "EXTRACTED", "source_file": "src/Shared/Logging/MessageHandler/PruneLogsHandler.php", "source_location": "L14", "weight": 1.0, "context": "parameter_type"}], "raw_calls": [{"caller_nid": "messagehandler_prunelogshandler_prunelogshandler_invoke", "callee": "pruneByRetention", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Logging/MessageHandler/PruneLogsHandler.php", "source_location": "L16", "receiver": null}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_logging_logpruneservice_php", "label": "LogPruneService.php", "file_type": "code", "source_file": "src/Shared/Logging/LogPruneService.php", "source_location": "L1"}, {"id": "logging_logpruneservice_logpruneservice", "label": "LogPruneService", "file_type": "code", "source_file": "src/Shared/Logging/LogPruneService.php", "source_location": "L11"}, {"id": "logging_logpruneservice_logpruneservice_construct", "label": ".__construct()", "file_type": "code", "source_file": "src/Shared/Logging/LogPruneService.php", "source_location": "L13"}, {"id": "logging_logpruneservice_logpruneservice_prunebyretention", "label": ".pruneByRetention()", "file_type": "code", "source_file": "src/Shared/Logging/LogPruneService.php", "source_location": "L19"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_logging_logpruneservice_php", "target": "siteconfigrepository", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Logging/LogPruneService.php", "source_location": "L5", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_logging_logpruneservice_php", "target": "logging_logpruneservice_logpruneservice", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/Shared/Logging/LogPruneService.php", "source_location": "L11", "weight": 1.0}, {"source": "logging_logpruneservice_logpruneservice", "target": "logging_logpruneservice_logpruneservice_construct", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Logging/LogPruneService.php", "source_location": "L13", "weight": 1.0}, {"source": "logging_logpruneservice_logpruneservice", "target": "logging_logpruneservice_logpruneservice_prunebyretention", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Logging/LogPruneService.php", "source_location": "L19", "weight": 1.0}], "raw_calls": [{"caller_nid": "logging_logpruneservice_logpruneservice_prunebyretention", "callee": "get", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Logging/LogPruneService.php", "source_location": "L21", "receiver": null}, {"caller_nid": "logging_logpruneservice_logpruneservice_prunebyretention", "callee": "time", "is_member_call": false, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Logging/LogPruneService.php", "source_location": "L26", "receiver": null}, {"caller_nid": "logging_logpruneservice_logpruneservice_prunebyretention", "callee": "deleteOlderThan", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Logging/LogPruneService.php", "source_location": "L28", "receiver": null}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_logging_message_prunelogsmessage_php", "label": "PruneLogsMessage.php", "file_type": "code", "source_file": "src/Shared/Logging/Message/PruneLogsMessage.php", "source_location": "L1"}, {"id": "message_prunelogsmessage_prunelogsmessage", "label": "PruneLogsMessage", "file_type": "code", "source_file": "src/Shared/Logging/Message/PruneLogsMessage.php", "source_location": "L5"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_logging_message_prunelogsmessage_php", "target": "message_prunelogsmessage_prunelogsmessage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/Shared/Logging/Message/PruneLogsMessage.php", "source_location": "L5", "weight": 1.0}], "raw_calls": []}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1231 -638
View File
File diff suppressed because it is too large Load Diff
+37 -17
View File
@@ -810,8 +810,8 @@
"semantic_hash": ""
},
"src/Admin/Controller/AdminApiController.php": {
"mtime": 1782750165.3401215,
"ast_hash": "09658d49a5db4c963fdbb7a167c8a38f",
"mtime": 1782929751.8803875,
"ast_hash": "8e5c78db08130ce9f65763e3306cc291",
"semantic_hash": ""
},
"src/Admin/Controller/AdminController.php": {
@@ -1150,8 +1150,8 @@
"semantic_hash": ""
},
"src/Config/Controller/SiteConfigController.php": {
"mtime": 1782893916.7348526,
"ast_hash": "cb994abce239f65aa6ea56e240beaaf9",
"mtime": 1782929760.8594074,
"ast_hash": "1a0cbe1845407beebacb2eba7d07024a",
"semantic_hash": ""
},
"src/Config/Entity/SiteConfig.php": {
@@ -1165,8 +1165,8 @@
"semantic_hash": ""
},
"src/Config/Repository/SiteConfigRepository.php": {
"mtime": 1782893889.148054,
"ast_hash": "fafe887c5ba8de7a9185969f028a79d9",
"mtime": 1782929759.6605961,
"ast_hash": "d86906f3319b64f8ac975f31f438cac2",
"semantic_hash": ""
},
"src/Config/Repository/TaxRateHistoryRepository.php": {
@@ -1470,12 +1470,12 @@
"semantic_hash": ""
},
"src/Schedule.php": {
"mtime": 1781551622.4124491,
"ast_hash": "e2679b3623e4a90ba9997ba7d8f9cda1",
"mtime": 1782929801.2075908,
"ast_hash": "af316acd9e95a609a79930f784f9b0d7",
"semantic_hash": ""
},
"src/Secretary/Controller/SecretaryController.php": {
"mtime": 1782902851.0718935,
"mtime": 1782929066.3195713,
"ast_hash": "9cbf2e62e58b071b044dda089612dd4a",
"semantic_hash": ""
},
@@ -2230,8 +2230,8 @@
"semantic_hash": ""
},
"config/packages/messenger.yaml": {
"mtime": 1781551622.4095502,
"ast_hash": "44c3176965808e27425e9db57027b29b",
"mtime": 1782929813.4472997,
"ast_hash": "2bcd86d1b6bfe2890abcb27a71bb7c65",
"semantic_hash": ""
},
"config/packages/nelmio_api_doc.yaml": {
@@ -2340,8 +2340,8 @@
"semantic_hash": ""
},
"docs/api/admin.md": {
"mtime": 1782750435.7712955,
"ast_hash": "6a68974b5f71e8d4104ffea4cab99edf",
"mtime": 1782930000.5470378,
"ast_hash": "27318238aa2741292c79a491870ba31b",
"semantic_hash": ""
},
"docs/api/appointment-settings.md": {
@@ -3375,8 +3375,8 @@
"semantic_hash": ""
},
"assets/admin/pages/LogsPage.tsx": {
"mtime": 1782750290.0145962,
"ast_hash": "76db47af844faf3459083afa2ded8aaf",
"mtime": 1782929861.6704445,
"ast_hash": "2e9aed8e9f683011985c6c932644892a",
"semantic_hash": ""
},
"migrations/Version20260629161536.php": {
@@ -3390,8 +3390,8 @@
"semantic_hash": ""
},
"src/Shared/Logging/AppLogRepository.php": {
"mtime": 1782749721.788873,
"ast_hash": "b0bba22cca2483a041b2c719812d7729",
"mtime": 1782929739.4106045,
"ast_hash": "3cbcf00989521e8b47167454984b7314",
"semantic_hash": ""
},
"src/Shared/Logging/DbLogger.php": {
@@ -3483,5 +3483,25 @@
"mtime": 1782896060.8114219,
"ast_hash": "9d1436d8787330a1cb06c54c4e896069",
"semantic_hash": ""
},
"src/Shared/Logging/Command/PruneLogsCommand.php": {
"mtime": 1782929791.8492818,
"ast_hash": "7ca54a0ec12d6d65f31ca365023eced1",
"semantic_hash": ""
},
"src/Shared/Logging/LogPruneService.php": {
"mtime": 1782929784.9897113,
"ast_hash": "42fa8560fd10cfdfc42203baf01d4406",
"semantic_hash": ""
},
"src/Shared/Logging/Message/PruneLogsMessage.php": {
"mtime": 1782929786.004423,
"ast_hash": "88c8235f5fd97e0410fe8dd634c59f7a",
"semantic_hash": ""
},
"src/Shared/Logging/MessageHandler/PruneLogsHandler.php": {
"mtime": 1782929788.2918966,
"ast_hash": "7d306f9d2eba4693cd01ff21f2eda173",
"semantic_hash": ""
}
}
@@ -1989,4 +1989,12 @@ class AdminApiController extends BaseController
'created_at' => (int) $l['createdAt'],
], $rows), (int) $total, $page, $limit);
}
#[Route('/api/v1/admin/logs', methods: ['DELETE'])]
public function clearLogs(): JsonResponse
{
$deleted = $this->em->getRepository(AppLog::class)->deleteAll();
return $this->success(['deleted' => $deleted]);
}
}
@@ -28,6 +28,7 @@ class SiteConfigController extends BaseController
'support_phone',
'max_cancel_hours_before',
'appointment_reminder_hours',
'log_retention_days',
// payment gateways
'payment_test_mode',
'payment_allowed_frontend_hosts',
@@ -22,6 +22,8 @@ class SiteConfigRepository extends ServiceEntityRepository
'support_phone' => '',
'max_cancel_hours_before' => '24',
'appointment_reminder_hours' => '2',
// logging (0 = نگهداری نامحدود)
'log_retention_days' => '90',
// payment gateways
'payment_test_mode' => '0',
'mellat_terminal_id' => '',
+4
View File
@@ -3,6 +3,7 @@
namespace App;
use App\Appointment\Message\ExpireAppointmentsMessage;
use App\Shared\Logging\Message\PruneLogsMessage;
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule as SymfonySchedule;
@@ -24,6 +25,9 @@ class Schedule implements ScheduleProviderInterface
->processOnlyLastMissedRun(true) // ensure only last missed task is run
->add(
RecurringMessage::every('1 minute', new ExpireAppointmentsMessage())
)
->add(
RecurringMessage::every('1 day', new PruneLogsMessage())
);
}
}
+20
View File
@@ -8,4 +8,24 @@ use Doctrine\Persistence\ManagerRegistry;
class AppLogRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry) { parent::__construct($registry, AppLog::class); }
/** حذف همه‌ی لاگ‌ها؛ تعداد ردیف‌های حذف‌شده را برمی‌گرداند. */
public function deleteAll(): int
{
return (int) $this->createQueryBuilder('l')
->delete()
->getQuery()
->execute();
}
/** حذف لاگ‌های قدیمی‌تر از timestamp داده‌شده؛ تعداد حذف‌شده را برمی‌گرداند. */
public function deleteOlderThan(int $timestamp): int
{
return (int) $this->createQueryBuilder('l')
->delete()
->where('l.createdAt < :ts')
->setParameter('ts', $timestamp)
->getQuery()
->execute();
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Shared\Logging\Command;
use App\Shared\Logging\LogPruneService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'app:prune-logs', description: 'حذف لاگ‌های قدیمی‌تر از مدت نگهداری تنظیم‌شده')]
final class PruneLogsCommand extends Command
{
public function __construct(private readonly LogPruneService $pruneService)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$deleted = $this->pruneService->pruneByRetention();
$output->writeln(sprintf('%d لاگ حذف شد.', $deleted));
return Command::SUCCESS;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Shared\Logging;
use App\Config\Repository\SiteConfigRepository;
/**
* حذف لاگ‌های قدیمی‌تر از مدت نگهداری تنظیم‌شده (`log_retention_days`).
* مقدار ۰ یا نامعتبر یعنی نگهداری نامحدود چیزی حذف نمی‌شود.
*/
class LogPruneService
{
public function __construct(
private readonly AppLogRepository $logRepo,
private readonly SiteConfigRepository $configRepo,
) {}
/** تعداد لاگ‌های حذف‌شده را برمی‌گرداند. */
public function pruneByRetention(): int
{
$days = (int) ($this->configRepo->get('log_retention_days') ?? 0);
if ($days <= 0) {
return 0;
}
$cutoff = time() - ($days * 86400);
return $this->logRepo->deleteOlderThan($cutoff);
}
}
@@ -0,0 +1,7 @@
<?php
namespace App\Shared\Logging\Message;
class PruneLogsMessage
{
}
@@ -0,0 +1,18 @@
<?php
namespace App\Shared\Logging\MessageHandler;
use App\Shared\Logging\LogPruneService;
use App\Shared\Logging\Message\PruneLogsMessage;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final class PruneLogsHandler
{
public function __construct(private readonly LogPruneService $pruneService) {}
public function __invoke(PruneLogsMessage $message): void
{
$this->pruneService->pruneByRetention();
}
}