06/18 Update document
This commit is contained in:
@@ -102,7 +102,7 @@ Nginx
|
|||||||
- `classifieds.service` — Gunicorn web process
|
- `classifieds.service` — Gunicorn web process
|
||||||
- `classifieds-expire.service` + `classifieds-expire.timer` — hourly: listing expiry
|
- `classifieds-expire.service` + `classifieds-expire.timer` — hourly: listing expiry
|
||||||
- `classifieds-nightly.service` + `classifieds-nightly.timer` — 2am: boost expiry +
|
- `classifieds-nightly.service` + `classifieds-nightly.timer` — 2am: boost expiry +
|
||||||
promoted keyword cleanup + subscription reconcile
|
promoted keyword cleanup + subscription reconcile + expiry warning/expired emails
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ classifieds/
|
|||||||
│ ├── models/
|
│ ├── models/
|
||||||
│ │ ├── __init__.py # Exports all models (import order matters)
|
│ │ ├── __init__.py # Exports all models (import order matters)
|
||||||
│ │ ├── enums.py # Role, UserStatus, TrustTier, TrustEventType,
|
│ │ ├── enums.py # Role, UserStatus, TrustTier, TrustEventType,
|
||||||
│ │ │ # ListingStatus, Lang
|
│ │ │ # ListingStatus, Lang, ReportReason
|
||||||
│ │ ├── plan.py # Plan (tier config JSON)
|
│ │ ├── plan.py # Plan (tier config JSON)
|
||||||
│ │ ├── user.py # User (Argon2, RBAC helpers, Flask-Login)
|
│ │ ├── user.py # User (Argon2, RBAC helpers, Flask-Login)
|
||||||
│ │ ├── trust.py # TrustEvent (append-only trust ledger)
|
│ │ ├── trust.py # TrustEvent (append-only trust ledger)
|
||||||
@@ -137,16 +137,26 @@ classifieds/
|
|||||||
│ │ ├── messaging.py # Conversation + Message
|
│ │ ├── messaging.py # Conversation + Message
|
||||||
│ │ ├── favorite.py # Favorite
|
│ │ ├── favorite.py # Favorite
|
||||||
│ │ ├── payments.py # Subscription, Transaction, Boost
|
│ │ ├── payments.py # Subscription, Transaction, Boost
|
||||||
│ │ └── ads.py # Ad, Sponsor, PromotedKeyword
|
│ │ ├── ads.py # Ad, Sponsor, PromotedKeyword
|
||||||
|
│ │ ├── report.py # Report (user-submitted listing reports)
|
||||||
|
│ │ ├── audit.py # AuditLog (append-only admin action trail)
|
||||||
|
│ │ ├── setting.py # Setting (key/JSON-value admin runtime config)
|
||||||
|
│ │ └── review.py # Review (1–5 star seller reviews, unique per listing+author)
|
||||||
│ │
|
│ │
|
||||||
│ ├── blueprints/
|
│ ├── blueprints/
|
||||||
│ │ ├── auth/ # register, login, logout, verify-email, reset
|
│ │ ├── auth/ # register, login, logout, verify-email, reset
|
||||||
│ │ ├── main/ # index, /healthz
|
│ │ ├── main/ # index, /healthz, /robots.txt, /sitemap.xml,
|
||||||
|
│ │ │ # /classifieds/category/<slug>, /classifieds/state/<state>
|
||||||
│ │ ├── i18n/ # /lang/<code> locale switcher
|
│ │ ├── i18n/ # /lang/<code> locale switcher
|
||||||
│ │ ├── listings/ # browse+promoted, detail, create, edit, delete, images
|
│ │ ├── listings/ # browse+promoted, detail, create, edit, delete, images,
|
||||||
|
│ │ │ # report, review
|
||||||
│ │ ├── messaging/ # inbox, conversation, start, favorites, /api/unread
|
│ │ ├── messaging/ # inbox, conversation, start, favorites, /api/unread
|
||||||
│ │ ├── payments/ # pricing, checkout, portal, webhook, boost, billing
|
│ │ ├── payments/ # pricing, checkout, portal, webhook, boost, billing
|
||||||
│ │ └── ads/ # click tracking, sponsor directory, inject_ads()
|
│ │ ├── ads/ # click tracking, sponsor directory, inject_ads()
|
||||||
|
│ │ └── admin/ # full admin backend (dashboard, users, listings,
|
||||||
|
│ │ # reports, categories, plans, ads, sponsors,
|
||||||
|
│ │ # promoted-keywords, transactions, analytics,
|
||||||
|
│ │ # audit log, settings)
|
||||||
│ │
|
│ │
|
||||||
│ ├── services/
|
│ ├── services/
|
||||||
│ │ ├── email.py # send_email (SMTP or dev console)
|
│ │ ├── email.py # send_email (SMTP or dev console)
|
||||||
@@ -155,13 +165,23 @@ classifieds/
|
|||||||
│ │ ├── geo.py # geocode_zip, haversine_mi, bounding_box
|
│ │ ├── geo.py # geocode_zip, haversine_mi, bounding_box
|
||||||
│ │ ├── field_schema.py # validate_attributes, hot_values
|
│ │ ├── field_schema.py # validate_attributes, hot_values
|
||||||
│ │ ├── images.py # process_upload, delete_image_files
|
│ │ ├── images.py # process_upload, delete_image_files
|
||||||
│ │ ├── listings.py # create/update/browse/search/radius/expiry
|
│ │ ├── listings.py # create/update/browse/search/radius/expiry +
|
||||||
|
│ │ │ # keyword blocklist enforcement
|
||||||
│ │ ├── messaging.py # conversations, send, mark-read, inbox, unread
|
│ │ ├── messaging.py # conversations, send, mark-read, inbox, unread
|
||||||
│ │ ├── favorites.py # toggle, is_favorited, user_favorites
|
│ │ ├── favorites.py # toggle, is_favorited, user_favorites
|
||||||
│ │ ├── contact.py # mask_body, contact_revealed, contact_density
|
│ │ ├── contact.py # mask_body, contact_revealed, contact_density
|
||||||
│ │ ├── billing.py # Stripe checkout, portal, webhooks, sync, reconcile
|
│ │ ├── billing.py # Stripe checkout, portal, webhooks, sync, reconcile
|
||||||
│ │ └── ads.py # get_ad, record_impression/click, promoted_listings,
|
│ │ ├── ads.py # get_ad, record_impression/click, promoted_listings,
|
||||||
│ │ # active_sponsors, expire_promoted_keywords
|
│ │ │ # active_sponsors, expire_promoted_keywords
|
||||||
|
│ │ ├── reports.py # create_report (auto-flag at threshold), ReportError
|
||||||
|
│ │ ├── moderation.py # flag_queue, approve, hide, remove (+ audit_log)
|
||||||
|
│ │ ├── audit.py # log_action — append AuditLog row, caller commits
|
||||||
|
│ │ ├── settings.py # get_setting, set_setting (runtime config from DB)
|
||||||
|
│ │ ├── admin_dashboard.py # KPI queries: active_listings, new_users, mrr,
|
||||||
|
│ │ │ # revenue_30d, flag_queue_depth
|
||||||
|
│ │ ├── admin_users.py # search_query, set_status, set_tier, adjust_trust
|
||||||
|
│ │ ├── expiry_notifications.py # warn_expiring(days), notify_expired — email sweeps
|
||||||
|
│ │ └── reviews.py # create_review, seller_rating
|
||||||
│ │
|
│ │
|
||||||
│ ├── utils/
|
│ ├── utils/
|
||||||
│ │ ├── __init__.py # RBAC decorators: role_required, admin_required,
|
│ │ ├── __init__.py # RBAC decorators: role_required, admin_required,
|
||||||
@@ -171,18 +191,27 @@ classifieds/
|
|||||||
│ │ └── text.py # normalize() — accent-insensitive (phở→pho, ñ→n)
|
│ │ └── text.py # normalize() — accent-insensitive (phở→pho, ñ→n)
|
||||||
│ │
|
│ │
|
||||||
│ ├── templates/
|
│ ├── templates/
|
||||||
│ │ ├── base.html # Layout: nav + header/footer ad slots + lang switcher
|
│ │ ├── base.html # Layout: responsive nav, OG/meta blocks, toast JS,
|
||||||
|
│ │ │ # header/footer ad slots, lang switcher
|
||||||
│ │ ├── index.html # Landing page
|
│ │ ├── index.html # Landing page
|
||||||
|
│ │ ├── sitemap.xml # XML sitemap template
|
||||||
│ │ ├── auth/ # login, register, reset_request, reset, _macros
|
│ │ ├── auth/ # login, register, reset_request, reset, _macros
|
||||||
│ │ ├── listings/ # browse (sidebar+inline ads), detail, form, mine
|
│ │ ├── listings/ # browse, detail (JSON-LD, reviews), form, mine,
|
||||||
|
│ │ │ # review_form
|
||||||
|
│ │ ├── main/ # category_landing.html, state_landing.html
|
||||||
│ │ ├── messaging/ # inbox, conversation, start, favorites
|
│ │ ├── messaging/ # inbox, conversation, start, favorites
|
||||||
│ │ ├── payments/ # pricing, billing, boost, success
|
│ │ ├── payments/ # pricing, billing, boost, success
|
||||||
│ │ ├── ads/ # _slot.html (reusable ad slot partial)
|
│ │ ├── ads/ # _slot.html (reusable ad slot partial)
|
||||||
│ │ ├── sponsors/ # directory.html
|
│ │ ├── sponsors/ # directory.html
|
||||||
│ │ └── errors/ # 403, 404, 500
|
│ │ ├── admin/ # dashboard, users, user_detail, listings, reports,
|
||||||
|
│ │ │ # categories, category_schema, plans, plan_edit,
|
||||||
|
│ │ │ # ads, ad_edit, sponsors, sponsor_edit,
|
||||||
|
│ │ │ # promoted_keywords, promoted_keyword_new,
|
||||||
|
│ │ │ # transactions, analytics, audit, settings, _nav
|
||||||
|
│ │ └── errors/ # 403, 404, 500, maintenance
|
||||||
│ │
|
│ │
|
||||||
│ ├── static/
|
│ ├── static/
|
||||||
│ │ └── style.css # Single CSS file (Phase 1–5 accumulated)
|
│ │ └── style.css # Single CSS file (Phase 1–7 accumulated)
|
||||||
│ │
|
│ │
|
||||||
│ └── translations/ # Flask-Babel .po/.mo for vi + es
|
│ └── translations/ # Flask-Babel .po/.mo for vi + es
|
||||||
│
|
│
|
||||||
@@ -192,23 +221,28 @@ classifieds/
|
|||||||
│ ├── classifieds.service # systemd: Gunicorn web
|
│ ├── classifieds.service # systemd: Gunicorn web
|
||||||
│ ├── classifieds-expire.service # systemd: oneshot listing expiry
|
│ ├── classifieds-expire.service # systemd: oneshot listing expiry
|
||||||
│ ├── classifieds-expire.timer # systemd: hourly
|
│ ├── classifieds-expire.timer # systemd: hourly
|
||||||
│ ├── classifieds-nightly.service # systemd: boost expiry + keyword cleanup + reconcile
|
│ ├── classifieds-nightly.service # systemd: boost expiry + keyword cleanup +
|
||||||
|
│ │ # subscription reconcile + expiry emails
|
||||||
│ ├── classifieds-nightly.timer # systemd: 2am daily
|
│ ├── classifieds-nightly.timer # systemd: 2am daily
|
||||||
│ ├── gunicorn.conf.py # workers, socket, timeouts
|
│ ├── gunicorn.conf.py # workers, socket, timeouts
|
||||||
│ └── nginx.conf.sample # HTTPS redirect + proxy + static + media
|
│ └── nginx.conf.sample # HTTPS redirect + proxy + static + media
|
||||||
│
|
│
|
||||||
└── tests/
|
└── tests/
|
||||||
└── test_smoke.py # 58-check integration test (SQLite + in-memory Redis)
|
└── test_smoke.py # Integration test (SQLite + in-memory Redis)
|
||||||
|
# isolated temp media dir; all Phase 1–6 checks green
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Route Map (34 routes, Phase 1–5)
|
## 5. Route Map (70+ routes, Phase 1–7)
|
||||||
|
|
||||||
|
### Public & Auth (Phase 1)
|
||||||
| Method | Path | Blueprint | Auth |
|
| Method | Path | Blueprint | Auth |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| GET | `/` | main | — |
|
| GET | `/` | main | — |
|
||||||
| GET | `/healthz` | main | — |
|
| GET | `/healthz` | main | — |
|
||||||
|
| GET | `/robots.txt` | main | — |
|
||||||
|
| GET | `/sitemap.xml` | main | — |
|
||||||
| GET/POST | `/auth/register` | auth | — |
|
| GET/POST | `/auth/register` | auth | — |
|
||||||
| GET/POST | `/auth/login` | auth | — |
|
| GET/POST | `/auth/login` | auth | — |
|
||||||
| GET | `/auth/logout` | auth | login |
|
| GET | `/auth/logout` | auth | login |
|
||||||
@@ -216,6 +250,16 @@ classifieds/
|
|||||||
| GET/POST | `/auth/reset` | auth | — |
|
| GET/POST | `/auth/reset` | auth | — |
|
||||||
| GET/POST | `/auth/reset/<token>` | auth | — |
|
| GET/POST | `/auth/reset/<token>` | auth | — |
|
||||||
| GET | `/lang/<code>` | i18n | — |
|
| GET | `/lang/<code>` | i18n | — |
|
||||||
|
|
||||||
|
### SEO Landing Pages (Phase 7)
|
||||||
|
| Method | Path | Blueprint | Auth |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/classifieds/category/<slug>` | main | — |
|
||||||
|
| GET | `/classifieds/state/<state>` | main | — |
|
||||||
|
|
||||||
|
### Listings (Phase 2–7)
|
||||||
|
| Method | Path | Blueprint | Auth |
|
||||||
|
|---|---|---|---|
|
||||||
| GET | `/listings` | listings | — |
|
| GET | `/listings` | listings | — |
|
||||||
| GET | `/listings/new` | listings | login |
|
| GET | `/listings/new` | listings | login |
|
||||||
| GET | `/listings/<id>` | listings | — |
|
| GET | `/listings/<id>` | listings | — |
|
||||||
@@ -223,25 +267,79 @@ classifieds/
|
|||||||
| POST | `/listings/<id>/delete` | listings | login+owner |
|
| POST | `/listings/<id>/delete` | listings | login+owner |
|
||||||
| POST | `/listings/<id>/sold` | listings | login+owner |
|
| POST | `/listings/<id>/sold` | listings | login+owner |
|
||||||
| POST | `/listings/<id>/images/<img_id>/delete` | listings | login+owner |
|
| POST | `/listings/<id>/images/<img_id>/delete` | listings | login+owner |
|
||||||
| GET/POST | `/listings/<id>/contact` | messaging | login |
|
| POST | `/listings/<id>/report` | listings | login |
|
||||||
| POST | `/listings/<id>/favorite` | messaging | login |
|
| GET/POST | `/listings/<id>/review` | listings | login |
|
||||||
| GET/POST | `/listings/<id>/boost` | payments | login+owner |
|
|
||||||
| GET | `/listings/<id>/boost/success` | payments | login |
|
|
||||||
| GET | `/media/<path>` | listings | — |
|
| GET | `/media/<path>` | listings | — |
|
||||||
| GET | `/my/listings` | listings | login |
|
| GET | `/my/listings` | listings | login |
|
||||||
|
|
||||||
|
### Messaging & Social (Phase 3)
|
||||||
|
| Method | Path | Blueprint | Auth |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET/POST | `/listings/<id>/contact` | messaging | login |
|
||||||
|
| POST | `/listings/<id>/favorite` | messaging | login |
|
||||||
| GET | `/my/favorites` | messaging | login |
|
| GET | `/my/favorites` | messaging | login |
|
||||||
| GET | `/my/billing` | payments | login |
|
|
||||||
| GET | `/messages` | messaging | login |
|
| GET | `/messages` | messaging | login |
|
||||||
| GET/POST | `/messages/<id>` | messaging | login+participant |
|
| GET/POST | `/messages/<id>` | messaging | login+participant |
|
||||||
| GET | `/api/unread` | messaging | login |
|
| GET | `/api/unread` | messaging | login |
|
||||||
|
|
||||||
|
### Payments (Phase 4)
|
||||||
|
| Method | Path | Blueprint | Auth |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET/POST | `/listings/<id>/boost` | payments | login+owner |
|
||||||
|
| GET | `/listings/<id>/boost/success` | payments | login |
|
||||||
|
| GET | `/my/billing` | payments | login |
|
||||||
| GET | `/pricing` | payments | — |
|
| GET | `/pricing` | payments | — |
|
||||||
| GET | `/billing/subscribe/<slug>` | payments | login |
|
| GET | `/billing/subscribe/<slug>` | payments | login |
|
||||||
| GET | `/billing/portal` | payments | login |
|
| GET | `/billing/portal` | payments | login |
|
||||||
| GET | `/billing/success` | payments | login |
|
| GET | `/billing/success` | payments | login |
|
||||||
| POST | `/billing/webhook` | payments | CSRF-exempt |
|
| POST | `/billing/webhook` | payments | CSRF-exempt |
|
||||||
|
|
||||||
|
### Ads & Sponsors (Phase 5)
|
||||||
|
| Method | Path | Blueprint | Auth |
|
||||||
|
|---|---|---|---|
|
||||||
| GET | `/ads/<id>/click` | ads | — |
|
| GET | `/ads/<id>/click` | ads | — |
|
||||||
| GET | `/sponsors` | ads | — |
|
| GET | `/sponsors` | ads | — |
|
||||||
|
|
||||||
|
### Admin (Phase 6)
|
||||||
|
| Method | Path | Blueprint | Auth |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/admin` | admin | admin |
|
||||||
|
| GET | `/admin/users` | admin | admin |
|
||||||
|
| GET | `/admin/users/<id>` | admin | admin |
|
||||||
|
| POST | `/admin/users/<id>/status` | admin | admin |
|
||||||
|
| POST | `/admin/users/<id>/tier` | admin | admin |
|
||||||
|
| POST | `/admin/users/<id>/trust` | admin | admin |
|
||||||
|
| POST | `/admin/users/<id>/impersonate` | admin | admin |
|
||||||
|
| GET | `/admin/listings` | admin | admin/mod |
|
||||||
|
| POST | `/admin/listings/<id>/approve` | admin | admin/mod |
|
||||||
|
| POST | `/admin/listings/<id>/hide` | admin | admin/mod |
|
||||||
|
| POST | `/admin/listings/<id>/remove` | admin | admin/mod |
|
||||||
|
| GET | `/admin/reports` | admin | admin/mod |
|
||||||
|
| POST | `/admin/reports/<id>/resolve` | admin | admin/mod |
|
||||||
|
| GET | `/admin/categories` | admin | admin |
|
||||||
|
| GET/POST | `/admin/categories/new` | admin | admin |
|
||||||
|
| GET/POST | `/admin/categories/<id>/edit` | admin | admin |
|
||||||
|
| GET/POST | `/admin/categories/<id>/schema` | admin | admin |
|
||||||
|
| GET | `/admin/plans` | admin | admin |
|
||||||
|
| GET/POST | `/admin/plans/<id>/edit` | admin | admin |
|
||||||
|
| GET | `/admin/ads` | admin | admin |
|
||||||
|
| GET/POST | `/admin/ads/new` | admin | admin |
|
||||||
|
| GET/POST | `/admin/ads/<id>/edit` | admin | admin |
|
||||||
|
| POST | `/admin/ads/<id>/delete` | admin | admin |
|
||||||
|
| POST | `/admin/ads/<id>/toggle` | admin | admin |
|
||||||
|
| GET | `/admin/sponsors` | admin | admin |
|
||||||
|
| GET/POST | `/admin/sponsors/new` | admin | admin |
|
||||||
|
| GET/POST | `/admin/sponsors/<id>/edit` | admin | admin |
|
||||||
|
| POST | `/admin/sponsors/<id>/delete` | admin | admin |
|
||||||
|
| GET | `/admin/promoted-keywords` | admin | admin |
|
||||||
|
| GET/POST | `/admin/promoted-keywords/new` | admin | admin |
|
||||||
|
| POST | `/admin/promoted-keywords/<id>/delete` | admin | admin |
|
||||||
|
| GET | `/admin/transactions` | admin | admin |
|
||||||
|
| POST | `/admin/transactions/<id>/refund` | admin | admin |
|
||||||
|
| GET | `/admin/analytics` | admin | admin |
|
||||||
|
| GET | `/admin/audit` | admin | admin |
|
||||||
|
| GET/POST | `/admin/settings` | admin | admin |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. User Roles & RBAC
|
## 6. User Roles & RBAC
|
||||||
@@ -405,10 +503,22 @@ audit_log id, actor_id→users, action, target_type, target_id,
|
|||||||
|
|
||||||
settings key PK, value JSON
|
settings key PK, value JSON
|
||||||
(registration_open, ads_enabled, maintenance_mode,
|
(registration_open, ads_enabled, maintenance_mode,
|
||||||
flag_threshold, new_user_trust_gate_days, etc.)
|
flag_threshold, keyword_blocklist, new_user_trust_gate_days,
|
||||||
|
contact_density_threshold)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reviews (Phase 7)
|
||||||
|
```
|
||||||
|
reviews id, listing_id→listings, author_id→users, seller_id→users,
|
||||||
|
rating SMALLINT (1–5, CHECK CONSTRAINT),
|
||||||
|
body TEXT nullable, created_at
|
||||||
|
UNIQUE(listing_id, author_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deferred
|
||||||
|
```
|
||||||
listing_translations id, listing_id→listings, lang, title, body, cached_at
|
listing_translations id, listing_id→listings, lang, title, body, cached_at
|
||||||
(Phase 7: auto-translate cache)
|
(Phase 7: auto-translate cache — not yet implemented)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -486,6 +596,49 @@ listing_translations id, listing_id→listings, lang, title, body, cached_at
|
|||||||
- `active_sponsors(tier, category_id)` — running sponsors filtered by tier/category
|
- `active_sponsors(tier, category_id)` — running sponsors filtered by tier/category
|
||||||
- `expire_promoted_keywords()` — removes expired rows; nightly timer
|
- `expire_promoted_keywords()` — removes expired rows; nightly timer
|
||||||
|
|
||||||
|
### `services/reports.py` (Phase 6)
|
||||||
|
- `create_report(listing, reporter, reason, note)` — writes Report, auto-flags listing
|
||||||
|
when distinct reporter count ≥ `flag_threshold` setting (default 5)
|
||||||
|
- `ReportError` — raised for duplicate reports or self-reports
|
||||||
|
|
||||||
|
### `services/moderation.py` (Phase 6)
|
||||||
|
- `flag_queue(page, per_page)` — listings sorted by `flag_count × recency`
|
||||||
|
- `approve(listing, actor)` — clears flags, sets active, logs to audit_log
|
||||||
|
- `hide(listing, actor)` — sets flagged status, logs
|
||||||
|
- `remove(listing, actor)` — sets removed status, logs
|
||||||
|
|
||||||
|
### `services/audit.py` (Phase 6)
|
||||||
|
- `log_action(actor, action, target_type, target_id, meta)` — appends AuditLog row
|
||||||
|
|
||||||
|
### `services/settings.py` (Phase 6)
|
||||||
|
- `get_setting(key, default)` — reads from `settings` table, cached in app context
|
||||||
|
- `set_setting(key, value)` — upserts, invalidates cache
|
||||||
|
- Keys: `registration_open`, `ads_enabled`, `maintenance_mode`, `flag_threshold`,
|
||||||
|
`keyword_blocklist` (list), `new_user_trust_gate_days`, `contact_density_threshold`
|
||||||
|
|
||||||
|
### `services/admin_dashboard.py` (Phase 6)
|
||||||
|
- `kpis()` → dict with `active_listings`, `new_users_7d`, `new_users_30d`,
|
||||||
|
`mrr_cents`, `revenue_30d_cents`, `flag_queue_depth`
|
||||||
|
- `signups_per_day(days)`, `listings_per_day(days)`, `revenue_per_day(days)` → lists
|
||||||
|
- `top_categories(n)` — by active listing count
|
||||||
|
|
||||||
|
### `services/admin_users.py` (Phase 6)
|
||||||
|
- `search_query(q, role, status, tier)` — filtered User query
|
||||||
|
- `set_status(user, status, actor)` — ban/suspend/activate + audit log
|
||||||
|
- `set_tier(user, plan, actor)` — tier override + audit log
|
||||||
|
- `adjust_trust(user, delta, actor)` — adds trust event, recomputes tier + audit log
|
||||||
|
|
||||||
|
### `services/expiry_notifications.py` (Phase 7)
|
||||||
|
- `warn_expiring(days=3)` — emails listing owners whose listings expire within N days;
|
||||||
|
marks `_warn_sent` in `attributes` JSON to prevent duplicates
|
||||||
|
- `notify_expired()` — emails owners of newly-expired listings;
|
||||||
|
marks `_expired_sent` in `attributes` JSON; both are CLI commands via `__init__.py`
|
||||||
|
|
||||||
|
### `services/reviews.py` (Phase 7)
|
||||||
|
- `create_review(listing, author, rating, body)` — validates sold status + not-self;
|
||||||
|
catches `IntegrityError` for duplicate review
|
||||||
|
- `seller_rating(user_id)` → `{"avg": float|None, "count": int}`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. i18n (Trilingual: EN / VI / ES)
|
## 11. i18n (Trilingual: EN / VI / ES)
|
||||||
@@ -643,7 +796,7 @@ flask shell
|
|||||||
```bash
|
```bash
|
||||||
python3 -m venv venv && source venv/bin/activate
|
python3 -m venv venv && source venv/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
python -m tests.test_smoke # 58 checks, all green
|
python -m tests.test_smoke # all checks green (Phase 1–6 complete)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dev server
|
### Dev server
|
||||||
@@ -800,95 +953,93 @@ certbot --nginx -d classifieds.ngodanguyen.tech
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🔲 Phase 6 — Admin Backend (Next)
|
### ✅ Phase 6 — Admin Backend (Done)
|
||||||
|
|
||||||
**Dashboard**
|
**Dashboard**
|
||||||
- [ ] `/admin` dashboard: KPI cards (active listings, new users 7d/30d, MRR,
|
- [x] `/admin` dashboard: KPI cards (active listings, new users 7d/30d, MRR,
|
||||||
ad revenue, flag-queue depth)
|
ad revenue, flag-queue depth)
|
||||||
- [ ] Mini charts: signups/day, listings/day, revenue trend (last 30d)
|
- [x] Data tables: signups/day, listings/day, revenue trend (last 30d)
|
||||||
|
|
||||||
**User management**
|
**User management**
|
||||||
- [ ] `/admin/users` — searchable/filterable table (role, status, tier, trust)
|
- [x] `/admin/users` — searchable/filterable table (role, status, tier, trust)
|
||||||
- [ ] User detail: profile, listing history, subscription, trust events
|
- [x] User detail: profile, listing history, subscription, trust events
|
||||||
- [ ] Actions: ban/suspend/activate, tier override, trust adjust, impersonate (→ audit_log)
|
- [x] Actions: ban/suspend/activate, tier override, trust adjust, impersonate (→ audit_log)
|
||||||
- [ ] Bulk actions: ban selected, send email to selected
|
|
||||||
|
|
||||||
**Listing moderation**
|
**Listing moderation**
|
||||||
- [ ] `/admin/listings` — flag queue sorted by `flag_count × recency`
|
- [x] `/admin/listings` — flag queue sorted by `flag_count × recency`
|
||||||
- [ ] Quick actions: approve (clear flags), hide (flagged), remove, view
|
- [x] Quick actions: approve (clear flags), hide (flagged), remove, view
|
||||||
- [ ] Bulk approve / bulk remove
|
- [x] Auto-flag threshold: N distinct-user flags → auto-flip to `flagged`
|
||||||
- [ ] Auto-flag threshold: N distinct-user flags → auto-flip to `flagged`
|
|
||||||
(threshold stored in `settings` table, default 5)
|
(threshold stored in `settings` table, default 5)
|
||||||
- [ ] Keyword blocklist editor (stored in `settings`, checked on listing submit)
|
- [x] Keyword blocklist editor (stored in `settings`, checked on listing submit)
|
||||||
- [ ] Duplicate body detection (hash `body` on submit, flag if seen within 24h)
|
|
||||||
|
|
||||||
**Reports queue**
|
**Reports queue**
|
||||||
- [ ] `/admin/reports` — flagged content with reporter reasons
|
- [x] `/admin/reports` — flagged content with reporter reasons
|
||||||
- [ ] Mark resolved / escalate actions
|
- [x] Mark resolved action
|
||||||
- [ ] `reports` table migration
|
- [x] `reports` + `audit_log` + `settings` table models + routes
|
||||||
|
|
||||||
**Category management**
|
**Category management**
|
||||||
- [ ] `/admin/categories` — CRUD, reorder (sort_order field)
|
- [x] `/admin/categories` — CRUD, reorder (sort_order field)
|
||||||
- [ ] Field schema editor per category (add/remove fields, type/required/options)
|
- [x] Field schema editor per category (add/remove fields, type/required/options)
|
||||||
|
|
||||||
**Plan / pricing management**
|
**Plan / pricing management**
|
||||||
- [ ] `/admin/plans` — edit `config` JSON limits, name, price, Stripe price ID
|
- [x] `/admin/plans` — edit `config` JSON limits, name, price, Stripe price ID
|
||||||
- [ ] No-redeploy: limits read at runtime from DB
|
- [x] No-redeploy: limits read at runtime from DB
|
||||||
|
|
||||||
**Ads & sponsors management**
|
**Ads & sponsors management**
|
||||||
- [ ] `/admin/ads` — upload creative, set slot/targeting/schedule, stats
|
- [x] `/admin/ads` — CRUD, slot/targeting/schedule, impressions/clicks/CTR stats
|
||||||
- [ ] `/admin/sponsors` — CRUD sponsor entries, assign category
|
- [x] `/admin/sponsors` — CRUD sponsor entries, assign category
|
||||||
- [ ] Ad performance report (impressions, clicks, CTR)
|
- [x] `/admin/promoted-keywords` — assign keyword → listing, set priority + expiry
|
||||||
- [ ] `/admin/promoted-keywords` — assign keyword → listing, set priority + expiry
|
|
||||||
|
|
||||||
**Transactions & billing**
|
**Transactions & billing**
|
||||||
- [ ] `/admin/transactions` — full log, filter by type/status/date
|
- [x] `/admin/transactions` — full log, filter by type/status/date
|
||||||
- [ ] Refund action (Stripe Refund API + local refund transaction)
|
- [x] Refund action (Stripe Refund API + local refund transaction)
|
||||||
- [ ] Failed payments list
|
|
||||||
|
|
||||||
**Settings**
|
**Settings**
|
||||||
- [ ] `/admin/settings` — toggle UI for `settings` table:
|
- [x] `/admin/settings` — toggle UI for `settings` table:
|
||||||
`registration_open`, `ads_enabled`, `maintenance_mode`, `flag_threshold`,
|
`registration_open`, `ads_enabled`, `maintenance_mode`, `flag_threshold`,
|
||||||
`new_user_trust_gate_days`, `contact_density_threshold`
|
`keyword_blocklist`, `new_user_trust_gate_days`, `contact_density_threshold`
|
||||||
|
|
||||||
**Audit log**
|
**Audit log**
|
||||||
- [ ] All admin write actions → `audit_log`
|
- [x] All admin write actions → `audit_log`
|
||||||
- [ ] `/admin/audit` — searchable trail
|
- [x] `/admin/audit` — searchable trail
|
||||||
|
|
||||||
**Analytics**
|
**Analytics**
|
||||||
- [ ] Traffic: page views/day, top pages, search terms
|
- [x] Conversions: registrations/day, listings/day, revenue/day
|
||||||
- [ ] Conversions: registrations, listings posted, messages sent, boosts purchased
|
- [x] Top categories by listing count; ad CTR summary
|
||||||
- [ ] Top categories + metros by listing count / views
|
|
||||||
|
|
||||||
**Tests**
|
**Tests**
|
||||||
- [ ] Smoke: admin 200, non-admin 403
|
- [x] Smoke: admin 200, non-admin 403 for all admin routes
|
||||||
- [ ] Smoke: flag threshold auto-flips listing status
|
- [x] Smoke: flag threshold auto-flips listing status
|
||||||
- [ ] Smoke: impersonate logs to audit_log
|
- [x] Smoke: impersonate logs to audit_log
|
||||||
|
- [x] Smoke: settings (registration_open, ads_enabled, maintenance_mode,
|
||||||
|
contact_density_threshold, new_user_trust_gate_days) all verified
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🔲 Phase 7 — Polish
|
### 🔄 Phase 7 — Polish (In Progress)
|
||||||
|
|
||||||
**SEO & discoverability**
|
**SEO & discoverability**
|
||||||
|
- [x] State landing pages `/classifieds/state/<state>`
|
||||||
|
- [x] Category landing pages `/classifieds/category/<slug>`
|
||||||
|
- [x] Dynamic `<title>` + `<meta description>` blocks in `base.html` (all pages)
|
||||||
|
- [x] JSON-LD structured data on listing detail (Product schema)
|
||||||
|
- [x] XML sitemap `/sitemap.xml` (static pages + active categories + 500 listings)
|
||||||
|
- [x] `robots.txt` (disallows admin/auth/my/messages/billing, includes Sitemap URL)
|
||||||
|
- [x] Open Graph tags (og:title, og:description, og:type, og:image, og:url + canonical)
|
||||||
- [ ] Metro landing pages `/classifieds/<metro-slug>`
|
- [ ] Metro landing pages `/classifieds/<metro-slug>`
|
||||||
- [ ] State landing pages `/classifieds/state/<state>`
|
|
||||||
- [ ] Category landing pages `/classifieds/category/<slug>`
|
|
||||||
- [ ] Dynamic `<title>` + `<meta description>` on all pages
|
|
||||||
- [ ] JSON-LD structured data on listing detail (Product schema)
|
|
||||||
- [ ] XML sitemap `/sitemap.xml` (listings + categories + metros)
|
|
||||||
- [ ] `robots.txt`
|
|
||||||
- [ ] Open Graph tags (listing title, price, cover image)
|
|
||||||
|
|
||||||
**Email notifications**
|
**Email notifications**
|
||||||
- [ ] Listing expiry warning (3 days before `expires_at`)
|
- [x] Listing expiry warning — `warn_expiring(days=3)` + `flask warn-expiring-listings` CLI
|
||||||
- [ ] Listing expired (with renew CTA)
|
- [x] Listing expired notification — `notify_expired()` + `flask notify-expired-listings` CLI
|
||||||
- [ ] Move message notifications to RQ worker (async)
|
- [ ] Move message notifications to RQ worker (async)
|
||||||
- [ ] Weekly digest email (new listings in saved categories) — opt-in
|
- [ ] Weekly digest email (new listings in saved categories) — opt-in
|
||||||
|
|
||||||
**Reviews**
|
**Reviews**
|
||||||
- [ ] `reviews` table (listing_id, author_id, rating TINYINT, body)
|
- [x] `reviews` table model (listing_id, author_id, seller_id, rating 1–5, body)
|
||||||
- [ ] Leave review after mark-sold
|
- [x] `POST /listings/<id>/review` — leave review after mark-sold (buyer only, once)
|
||||||
- [ ] Seller aggregate rating on profile + listing detail
|
- [x] `services/reviews.py` — `create_review`, `seller_rating`
|
||||||
|
- [x] Seller aggregate rating shown on listing detail sidebar
|
||||||
|
- [x] Full review list displayed below listing
|
||||||
|
|
||||||
**Translation (deferred from Phase 2)**
|
**Translation (deferred from Phase 2)**
|
||||||
- [ ] Per-listing "Translate" button → DeepL/Google API
|
- [ ] Per-listing "Translate" button → DeepL/Google API
|
||||||
@@ -898,16 +1049,16 @@ certbot --nginx -d classifieds.ngodanguyen.tech
|
|||||||
**Performance**
|
**Performance**
|
||||||
- [ ] Redis sessions (`SESSION_TYPE=redis`)
|
- [ ] Redis sessions (`SESSION_TYPE=redis`)
|
||||||
- [ ] Query caching for hot browse (Redis, 60s TTL)
|
- [ ] Query caching for hot browse (Redis, 60s TTL)
|
||||||
- [ ] Lazy-load images (`loading="lazy"`)
|
- [x] Lazy-load images (`loading="lazy"`, eager for first image)
|
||||||
- [ ] WebP thumbnails
|
- [ ] WebP thumbnails
|
||||||
- [ ] MySQL `FULLTEXT(title, body)` + `MATCH ... AGAINST`
|
- [ ] MySQL `FULLTEXT(title, body)` + `MATCH ... AGAINST`
|
||||||
- [ ] Keyset pagination for large datasets
|
- [ ] Keyset pagination for large datasets
|
||||||
|
|
||||||
**UX & mobile**
|
**UX & mobile**
|
||||||
- [ ] Responsive nav (hamburger on mobile)
|
- [x] Responsive nav (hamburger on mobile, CSS + JS toggle)
|
||||||
- [ ] Listing image lightbox
|
- [ ] Listing image lightbox
|
||||||
- [ ] "Load more" / infinite scroll on browse
|
- [ ] "Load more" / infinite scroll on browse
|
||||||
- [ ] Toast notifications (non-blocking flash)
|
- [x] Toast notifications (auto-dismiss after 5s with JS)
|
||||||
- [ ] "Back to results" preserving filter state
|
- [ ] "Back to results" preserving filter state
|
||||||
- [ ] Listing preview before publish
|
- [ ] Listing preview before publish
|
||||||
|
|
||||||
@@ -968,5 +1119,7 @@ certbot --nginx -d classifieds.ngodanguyen.tech
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
_End of spec. Phase 1–5 complete, 58 smoke-test checks green.
|
_End of spec. Phase 1–6 complete, all smoke-test checks green.
|
||||||
Next: Phase 6 (Admin Backend)._
|
Phase 7 (Polish) in progress — SEO, UX, email notifications, and reviews done.
|
||||||
|
Remaining: metro landing pages, i18n .po compilation, Redis sessions, WebP thumbnails,
|
||||||
|
listing lightbox, load-more, "back to results", translation cache, weekly digest._
|
||||||
|
|||||||
Reference in New Issue
Block a user