Dugout Value
투구·타격·예측 연구와 선수·계약·트레이드 가치를 비교하는 도구 제공
Ifriqiya Soccer Star는 청소년 축구 훈련·기술 개발·성장을 목표로 하는 엘리트 아카데미입니다
Ifriqiya Soccer Star is an elite football academy devoted to training, technical development and the growth of young talent. A verified profile, match footage, scouting days and direct contact with re
스포츠·피트니스TypeScript · PLpgSQLGitHub @ifriqiyastar온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Ifriqiya Soccer Star is an elite football academy devoted to training, technical development and the growth of young talent. A verified profile, match footage, scouting days and direct contact with recruiters — all from your phone.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Interface d'administration web (§12 du cahier des charges) pour la plateforme Ifriqiya Star. Elle s'appuie sur le meme projet Supabase que l'application mobile (~/ifriqiyastar) : memes utilisateurs auth.users, memes tables, memes policies RLS. Ce depot n'a donc aucun schema propre — la source de verite reste 00_ALL_IN_ONE.sql cote mobile. Stack : Next.js 16 (App Router, Turbopack) · React 19 · Tailwind CSS v4 · shadcn/ui (style base-sera, sur Base UI) · @supabase/ssr.README 전문 보기 ↗
Interface d'administration web (§12 du cahier des charges) pour la plateforme
Ifriqiya Star. Elle s'appuie sur le meme projet Supabase que l'application
mobile (~/ifriqiyastar) : memes utilisateurs auth.users, memes tables,
memes policies RLS. Ce depot n'a donc aucun schema propre — la source de verite
reste 00_ALL_IN_ONE.sql cote mobile.
Stack : Next.js 16 (App Router, Turbopack) · React 19 · Tailwind CSS v4 ·
shadcn/ui (style base-sera, sur Base UI) · @supabase/ssr.
npm install
npm run dev # http://localhost:3000 -> redirige vers /connexion
npm run build
npm run lint
Le .env de ce depot a ete copie de l'application Expo et porte encore des
noms EXPO_PUBLIC_*. Next.js n'expose au navigateur que NEXT_PUBLIC_*, d'ou
les deux jeux de variables :
| Variable | Requis | Role |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | oui | URL du projet Supabase |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY | oui | Cle publique (le RLS fait le reste) |
SUPABASE_SERVICE_ROLE_KEY | non (voir ci-dessous) | Suppression definitive d'un compte, webhook de paiement, envoi d'un code de reinitialisation de mot de passe depuis une fiche, et repli du masquage si la migration 0042 n'est pas appliquee |
PAYMENT_WEBHOOK_SECRET | pour les paiements en ligne | Signature HMAC-SHA256 de /api/webhooks/payment-provider |
NEXT_PUBLIC_APP_STORE_URL | non — a renseigner le jour de la publication | Fiche App Store, pas encore publiee ; des qu'elle existe, le badge Apple du site devient un vrai lien et le QR de l'entete redirige un iPhone qui le scanne directement vers elle (lib/store-urls.ts) |
NEXT_PUBLIC_PLAY_STORE_URL | non — inutile | Surcharge de la fiche Google Play, utile seulement pour pointer une fiche de test. La fiche publiee est ecrite dans lib/store-urls.ts et non dans l'environnement : posee dans le seul .env, elle etait perdue au deploiement et les badges redevenaient desactives en ligne sans que rien echoue. L'URL se batit sur l'identifiant d'application (android.package dans ~/ifriqiyastar/app.json), jamais sur le nom affiche |
NEXT_PUBLIC_TURNSTILE_SITE_KEY | oui tant que la protection anti-robot est active sur le projet Supabase | Cle publique du widget Cloudflare Turnstile, la meme que l'app mobile. Voir « Protection anti-robot » ci-dessous |
RESEND_API_KEY | oui | Envoi des e-mails via Resend : formulaire /contact (lib/actions/contact.ts) et canal email des campagnes (lib/email/campaign.ts) |
NEXT_PUBLIC_SITE_URL | oui des que le canal email sert | Racine publique du site. Les metadonnees Open Graph l'utilisent, et surtout les URL absolues des courriels : un client de messagerie ne resout pas un chemin relatif. A defaut, le domaine de production est utilise |
EMAIL_UNSUBSCRIBE_SECRET | recommande | Signature du lien de desabonnement des campagnes. A defaut, SUPABASE_SERVICE_ROLE_KEY sert de secret ; sans ni l'un ni l'autre, aucun lien n'est produit et le courriel ne porte que l'en-tete mailto:. ⚠️ Le changer invalide les liens deja envoyes |
RESEND_TOPIC_ID | non | Thematique Resend sous laquelle les annonces partent. A defaut elle est retrouvee par son nom, puis creee au premier envoi |
Voir et modifier le gabarit. npm run email ouvre le serveur de
previsualisation React Email sur http://localhost:3001 : rendu en direct,
bascule bureau/mobile, vue du HTML, du texte brut, et des controles de
delivrabilite (linter, compatibilite clients, score spam). Rien n'est envoye.
| Fichier | Role |
|---|---|
emails/campaign-email.tsx | le gabarit reel, celui que l'envoi rend |
emails/copy.ts | l'habillage (entete, pied, desabonnement) en fr / en / ar |
emails/apercus/ | variantes de previsualisation seulement — jamais importees par l'application |
Le gabarit se modifie a la main, dans le code : il n'y a pas d'editeur de
gabarits dans le back-office, et rien n'en stocke en base. Ce que
l'administrateur compose sur /admin/notifications, c'est le titre et le
message ; l'habillage autour est le meme pour toutes les campagnes.
⚠️ Le logo des courriels est public/brand/ifriqiya-star-mark.png — le signe
seul, rogne, et non l'icone d'application dont la plaque noire disparait sur
le bandeau noir en laissant un point lime minuscule. Il est charge par son URL
absolue, donc il n'apparait dans les courriels reels qu'une fois le site
deploye ; l'apercu du back-office, lui, charge ses images depuis le serveur
qu'on interroge et les montre tout de suite. Sans image, le nom de la marque
ecrit a cote reste lisible — c'est aussi ce que voient les destinataires qui
bloquent les images, soit la plupart par defaut.
Notification, e-mail, ou les deux. Le composeur ouvre sur ce choix. En
mode « un e-mail », aucune notification n'est ecrite : rien dans la cloche,
rien sur le telephone, seulement le courriel — et l'intitule devient « objet »,
avec 120 caracteres au lieu des 64 d'un titre d'ecran verrouille. En mode
« les deux », une case permet de rediger un objet et un corps propres au
courriel plutot que de reprendre ceux de la notification. Demande la migration
202609300007_campaign_email_subject.sql.
⚠️ Le push reste indissociable de l'in-app — c'est l'ecriture de la notification qui le declenche. C'est le couple qui est facultatif.
Avant d'envoyer. Le bouton « Envoi test » envoie la notification et le vrai courriel a votre propre compte, par le meme chemin que la diffusion : meme modele, meme mise en forme, meme gabarit. Rien n'est enregistre dans l'historique. Le panneau de droite previsualise les deux rendus — ecran verrouille et e-mail — dans la langue de votre choix.
Composer le corps d'un modele. L'ecran de modele ouvre sur un
compositeur : on ajoute des blocs (texte, image, bouton, deux colonnes,
trait, espace), on les glisse pour les reordonner, on les supprime. Un bloc
special, « Message de la campagne », marque l'endroit ou se posera le
texte ecrit au moment de l'envoi. La mise en page est commune aux trois
langues ; seuls les textes changent d'un onglet a l'autre. Demande la
migration 202609300006_email_blocks.sql, qui cree aussi le bucket public
email-media (images de courriel, 2 Mo maximum, ecriture super admin).
Ce n'est volontairement pas un editeur libre : chaque bloc est un composant, donc tout agencement reste lisible chez Outlook, se replie sur un telephone et respecte la charte. Un courriel part chez des milliers de personnes et personne ne le relira dans dix clients de messagerie.
Couleurs et message mis en forme. L'ecran de modele permet de choisir
cinq couleurs (fond d'entete, fond du message, texte, fond et texte du
bouton) ; une couleur laissee vide reprend celle de la charte, et le nom de
la marque bascule tout seul entre blanc et noir pour rester lisible sur le
fond choisi. Dans le composeur, le message s'ecrit avec le meme editeur que
le blog (Tiptap) : gras, italique, intertitre, listes, liens et couleur.
⚠️ La mise en forme ne part que dans l'e-mail : la notification in-app et
le push recoivent le meme texte sans balisage, parce qu'un ecran verrouille
n'en rend aucun. Le texte brut est derive de la saisie, jamais tape a part.
Demande la migration 202609300005_email_colors_and_rich_body.sql.
Modeles d'e-mail (/admin/notifications/modele). Un super
administrateur y gere plusieurs habillages : une galerie ou chaque carte
montre le courriel reel, et un editeur ou l'on modifie, langue par langue, le
nom d'expediteur, l'adresse de reponse, l'accueil, le bouton (libelle,
adresse, ou pas de bouton), la signature, le pied de page et la mention de
desabonnement. Un apercu rendu par le meme code que l'envoi est affiche a
cote, avec une bascule bureau / mobile. Demande les migrations
202609300003_email_template.sql et
202609300004_email_template_catalogue.sql.
A l'envoi, le composeur propose de choisir le modele — uniquement quand le canal email est coche, puisque l'in-app et le push n'ont pas d'habillage. Sans choix, c'est le modele marque « par defaut » qui part ; sans modele du tout, les textes livres. Le journal retient lequel a servi, et « Reessayer » rejoue celui de la campagne d'origine plutot que le defaut du moment.
Ce qui n'est pas modifiable, et pourquoi : les couleurs, les polices, le logo et la structure suivent la charte graphique du client ; le sens de lecture decoule de la langue (l'arabe reste de droite a gauche) ; l'adresse d'expedition reste sur le domaine verifie, sinon Resend refuse l'envoi. Il n'y a pas d'editeur HTML libre : chaque champ est du texte, echappe au rendu — un balisage saisi a la main ne survit pas a Outlook et un courriel part chez des milliers de personnes. Un champ laisse vide reprend le texte livre.
Choix des canaux (/admin/notifications). Un super administrateur
peut decocher le push mobile et cocher l'e-mail ; un administrateur ordinaire
envoie sur les canaux par defaut. L'in-app, lui, n'est jamais decochable — la
ligne de notification est a la fois l'element de la cloche et le declencheur du
push. Demande la migration
supabase/migrations/202609300002_push_channel_choice.sql, qui apporte aussi
admin_push_reach() : sans elle, « comptes joignables par push » affiche
« — » plutot qu'un zero trompeur, car push_tokens n'est volontairement
pas lisible par l'administration (migration mobile 0023).
Canal email des campagnes (/admin/notifications). Deux conditions, et
l'ecran nomme celle qui manque : RESEND_API_KEY, et la migration
supabase/migrations/202609300001_notification_email_channel.sql appliquee sur
le projet Supabase (elle apporte la table des desabonnements et la fonction
admin_broadcast_recipients()). Tant qu'il en manque une, la case « Email »
reste desactivee dans le composeur, avec sa raison — la notification in-app et
le push partent normalement. Le courriel est redige dans la langue du
destinataire (profiles.locale, les trois langues du site, arabe compris
avec dir="rtl"), part une adresse par message — jamais un to collectif
— et porte un lien de desabonnement signe plus les en-tetes List-Unsubscribe
et List-Unsubscribe-Post exiges des expediteurs de masse. Un desabonnement
exclut le compte des campagnes suivantes et de celles-la seulement : les
messages lies au compte (validation, securite) continuent de partir.
⚠️ Une diffusion email est plafonnee a 2 000 destinataires par envoi
(MAX_EMAIL_RECIPIENTS) — au-dela, l'in-app et le push partent quand meme a
tout le monde et l'ecran dit combien de courriels n'ont pas ete servis.
Formulaire de contact (/contact). Envoi uniquement par le serveur, via
Resend — pas de repli mailto:. Le domaine d'expedition
(ifriqiya-soccer.com, distinct du nom du site ifriqiya-soccer-star.com)
doit rester verifie dans le tableau de bord Resend (SPF/DKIM) : sans
RESEND_API_KEY, ou si Resend refuse l'envoi (domaine desverifie, panne du
service), le formulaire affiche une erreur au visiteur au lieu de l'envoyer.
⚠️ ifriqiya-soccer.com n'a pas d'enregistrement MX : le domaine est
verifie pour l'envoi, mais personne ne peut recevoir de courrier a
[email protected] tant qu'une messagerie n'y est pas branchee
(Google Workspace, Zoho Mail, ou une redirection Cloudflare Email Routing vers
une boite existante). En attendant, DELIVERY_ADDRESS dans
lib/actions/contact.ts route les messages vers la boite Gmail deja utilisee
par l'app ([email protected]) — a remplacer par l'adresse du domaine
des que sa reception sera configuree. L'adresse affichee sur le site
(pied de page, page /contact, messages d'erreur) est la meme
[email protected], pour ne jamais montrer une adresse que personne ne
lit — a remettre a jour avec DELIVERY_ADDRESS le jour ou les deux changent.
Protection anti-robot (Cloudflare Turnstile). Activee le 2026-09-17 a la
demande du client, c'est la fonctionnalite captcha de Supabase Auth
(Authentication -> Attack Protection) et non un controle de cette application.
⚠️ C'est un reglage de projet : il s'applique d'un coup a tous les points
d'entree d'authentification du projet partage, donc l'ecran /connexion doit
envoyer un jeton exactement comme l'app mobile — sans quoi Supabase repond
captcha protection: request disallowed (no captcha_token found). Le jeton ne
prouve rien par lui-meme : c'est GoTrue qui l'echange contre la cle secrete,
laquelle ne vit que dans le tableau de bord Supabase.
⚠️ Le nom d'hote doit etre declare sur la cle, sinon le widget refuse de se
charger (code 110200), et cela ne se corrige pas dans le code : ajouter
localhost et le domaine de production dans le tableau de bord Cloudflare
(Turnstile -> le widget -> Hostname Management). L'origine reellement declaree
est journalisee a cote du code dans la console, parce que c'est la seule
question utile face a un 110200.
Reinitialisation du mot de passe (§4.1). Elle se fait par code a six chiffres, jamais par lien : le gabarit d'e-mail est commun a l'app mobile et a ce back-office (un seul projet Supabase), et l'app mobile n'a pas de site vers lequel un lien reviendrait. Deux portes y menent :
/connexion/mot-de-passe-oublie, ouvert a qui connait son adresse — adresse,
puis code, puis nouveau mot de passe, sur un seul ecran. Le code est consomme
a la verification, d'ou l'absence de retour arriere possible ; la session
ouverte par le code est fermee apres l'ecriture, pour que l'entree dans le
back-office reste /connexion et sa garde requireAdmin() ;/admin/utilisateurs/[id],
reserve au super administrateur (is_super_admin()). Il declenche le meme
e-mail vers l'adresse du compte : l'administrateur ne choisit aucun mot de
passe et n'en apprend aucun. ⚠️ Cet envoi-la exige
SUPABASE_SERVICE_ROLE_KEY — la protection anti-robot couvre /recover, un
serveur n'a pas de defi a resoudre, et GoTrue n'en dispense que les appels
porteurs d'identifiants d'administration.⚠️ Deux reglages du tableau de bord conditionnent tout cela, et aucun ne se
detecte depuis le code : Authentication -> Emails -> Reset Password doit
contenir {{ .Token }} (le gabarit livre par defaut ne porte que le lien, et
l'e-mail arrive alors sans code), et Authentication -> SMTP Settings doit
porter un fournisseur a nous (le serveur integre de Supabase ne delivre qu'aux
membres du projet, ce que l'ecran nomme au lieu de l'afficher comme une panne).
Masquage d'un contenu. Les migrations 0033 et 0035 ont retire le droit
d'ecrire is_hidden aux sessions authenticated — session administrateur
comprise, un privilege de colonne ne regardant que le role Postgres — pour
qu'un auteur ne puisse pas annuler une decision de moderation sur son propre
contenu. Le back-office passe donc par la RPC admin_set_content_hidden
(migration 0042, depot mobile), security definer, qui verifie is_admin()
elle-meme : aucune cle supplementaire n'est necessaire, et l'autorisation reste
dans Postgres. Tant que 0042 n'est pas appliquee, le masquage retombe sur
SUPABASE_SERVICE_ROLE_KEY si elle est renseignee, et affiche sinon un message
nommant les deux remedes.
Desactivation, reactivation et changement de role. Meme histoire : la
migration 0025 a revoque update (is_active, deactivated_at, role) sur
profiles au role authenticated — pour qu'aucun utilisateur ne se promeuve
administrateur — et la session du back-office est une session authenticated
comme les autres. Ces gestes passent donc par les RPC de la migration 0044
(admin_set_account_active, admin_set_account_role,
admin_request_account_deletion). Sans elle, le back-office echoue en
42501 permission denied for table profiles.
Sans SUPABASE_SERVICE_ROLE_KEY, l'action « Supprimer » desactive le compte et
horodate deletion_requested_at au lieu de supprimer la ligne auth.users : la
cle publique ne peut pas appeler l'API Auth Admin, et supprimer seulement
public.profiles laisserait le JWT deja emis valide sur l'appareil.
Le back-office n'est accessible qu'aux comptes dont profiles.role = 'admin'
— c'est la colonne que lit public.is_admin(), dont dependent toutes les
policies RLS d'administration.
L'application mobile ne cree que des comptes player et professional : il
faut donc promouvoir un compte existant depuis le SQL Editor de Supabase.
Attention, un simple UPDATE echoue : le trigger
trg_prevent_self_role_escalation refuse tout changement de role quand
is_admin() est faux, et auth.uid() est nul dans l'editeur SQL. Il faut le
desactiver le temps de la requete :
alter table public.profiles disable trigger trg_prevent_self_role_escalation;
update public.profiles
set role = 'admin'
where email = '[email protected]';
alter table public.profiles enable trigger trg_prevent_self_role_escalation;
Une fois un premier admin en place, les changements de role suivants passent par la fiche compte du back-office (le trigger est alors satisfait).
Le role RBAC editeur (202609230004_admin_editor_role.sql,
dashboard.read retire depuis 202609230006_editeur_blog_only.sql)
ne porte que blog.manage : la personne ne voit que l'ecran Blog, rien
d'autre — ni tableau de bord, ni validations, ni moderation, ni Scout Days,
ni finances. A la connexion, elle est envoyee directement sur /admin/blog
(firstAccessiblePath(), components/admin/nav-items.ts) plutot que sur
/admin, qui lui est ferme.
Deux etapes, dans cet ordre :
profiles.role = 'admin' — obligatoire, c'est la garde de
requireAdmin() (lib/auth.ts), verifiee avant meme de regarder le role
RBAC. Si au moins un admin existe deja, faites-le depuis la fiche compte
du back-office (/admin/utilisateurs/[id]) plutot que par SQL — le
trigger trg_prevent_self_role_escalation n'a alors pas besoin d'etre
desactive.editeur — comme tous les roles RBAC, il n'y a
pas d'ecran pour ca (voir le commentaire de
202608240001_admin_platform.sql) : ca passe par le SQL Editor de
Supabase.insert into public.admin_user_roles (admin_id, role_id)
select p.id, r.id
from public.profiles p, public.admin_roles r
where p.email = '[email protected]' and r.code = 'editeur'
on conflict (admin_id) do update set role_id = excluded.role_id;
on conflict ... do update fait aussi office de changement de role : relancer
cette requete avec un autre r.code (support, moderator...) reassigne un
compte qui avait deja un role RBAC.
| Section | Ecran | Ce qui est couvert |
|---|---|---|
| — | / | Site public : page vitrine d'Ifriqiya Star (charte graphique Wii Studio — noir #000000, vert neon #aff70f, Nunito Sans / Poppins), captures reelles de l'application mobile. La racine ne redirige plus vers /admin. |
| — | Page 404 | Page introuvable aux couleurs du site public, dans la langue du visiteur (prefixe d'URL, puis cookie, puis Accept-Language). Servie par app/global-not-found.tsx — voir « Choix d'implementation notables ». Une adresse /admin erronee reste, elle, dans le chassis d'administration. |
| §4.1 | /connexion/mot-de-passe-oublie | Mot de passe oublie : envoi d'un code a six chiffres a l'adresse du compte, verification, puis nouveau mot de passe. Meme mecanisme que l'app mobile, meme gabarit d'e-mail. |
| §12.1 | /admin/validations | Files d'attente : profils joueurs, comptes professionnels, justificatifs pro, pieces d'identite. Validation / refus motive. Export CSV de chaque file, avec l'etat affiche et la recherche de l'ecran. |
| §12.1 | /admin/utilisateurs + /admin/utilisateurs/[id] | Annuaire filtrable (type, statut, actif, demande de suppression) ; consultation et modification (fiche compte, profil sportif, fiche pro) ; suspension, reactivation, suppression ; suivi du statut de verification ; visibilite du profil joueur. |
| §12.2 | /admin/moderation | Signalements : un moderateur propose un retrait motive, un super administrateur le valide ou le refuse. Publications, commentaires, medias joueurs (videos, photos), comptes et messages. Masquage, suppression, suspension d'un utilisateur. |
| §12.2 | /admin/moderation/signalements/[id] | Le dossier d'un signalement : la cible, le motif, les blocages deja recus par le compte, la provenance (fil d'actualite ou messagerie), la frise des decisions, et les gestes de decision a cote. Le contenu des messages prives n'y est pas affiche : l'instruction se fait sur le motif. |
| §12.3 | /admin/scout-days + /admin/scout-days/[id] | File d'attente de des evenements soumis par les professionnels (super admin), publication / depublication / annulation / cloture / suppression, inscriptions et leurs statuts, paiements par inscription, rapports de scouting. avec les filtres de l'ecran, organisateur et nombre d'inscriptions. |
Dans cet ordre, sur le projet Supabase partage. Toutes sont idempotentes.
| Fichier | Depot | Role |
|---|---|---|
202608240001_admin_platform.sql | back-office | Roles et permissions fines, file de notifications, historique immuable des evaluations. Attribue super_admin aux administrateurs existants. |
202608240002_admin_authoring_policies.sql | back-office | Autorise l'administration a creer un Scout Day et une evaluation (le schema d'origine les reservait au professionnel concerne). |
0040_scout_day_admin_validation.sql | mobile (~/ifriqiyastar) | Soumission a validation des Scout Days : en_attente_validation, is_super_admin(), trigger de transition, notification de l'organisateur. |
202608240003_scout_day_validation_permission.sql | back-office | Permission events.validate, attribuee au seul super_admin. A appliquer apres 0040. |
0041_report_removal_validation.sql | mobile (~/ifriqiyastar) | Retrait d'un contenu signale : statut a_valider, machine a etats, garde-fous de suppression, notification du signaleur. Depend de 0040. |
0042_admin_hide_content.sql | mobile (~/ifriqiyastar) | RPC admin_set_content_hidden : rend au back-office le droit de masquer, retire par 0033/0035, sans cle service_role. |
202608240004_report_validation_permission.sql |
Un utilisateur signale une publication depuis l'application mobile
(report_content(), migration 0033). Cote back-office, le retrait se decide
en deux temps depuis la migration 0041 :
Postgres a le dernier mot : supprimer une publication, un commentaire ou un
media joueur appartenant a quelqu'un d'autre est reserve a
is_super_admin(). Sans ces garde-fous, un moderateur contournerait la
validation en supprimant le contenu directement. L'auteur, lui, reste libre de
retirer ce qui lui appartient.
Depuis la migration 0040, un professionnel ne publie plus son evenement
lui-meme : il le soumet, et seul un super administrateur peut le passer a
publie. Postgres a le dernier mot (is_super_admin() dans le trigger
trg_enforce_scout_day_validation) ; events.validate ne fait que cacher le
geste aux administrateurs qui ne l'obtiendraient pas. La file d'attente est en
tete de /admin/scout-days, avec le compteur dans la navigation. Un refus est
obligatoirement motive : le motif part en notification a l'organisateur et
s'affiche sur sa fiche.
La page Notifications envoie reellement. L'envoi passe par
admin_broadcast_notification() (migration 0046, depot mobile), qui insere
une ligne par destinataire dans public.notifications — ce qui declenche le
push Expo deja pose par la migration 0023 et remplit la boite in-app. Il n'y a
donc pas de worker a ecrire : la logique d'envoi existait, elle n'etait
simplement pas appelee.
La cloche du bandeau montre la file de travail de l'administration, pas les
notifications des utilisateurs. Elle listait les vingt dernieres lignes de
public.notifications — la boite de reception des membres, avec le nom du
destinataire : le RLS l'autorise, la regle du client non, comme pour les
conversations privees. Elle liste desormais ce qui attend une decision
(dossiers a valider, signalements, retraits, Scout Days, demandes de
suppression), filtre par les droits de l'administrateur connecte, chaque ligne
renvoyant a son ecran. Meme lecture que les pastilles de la navigation
(lib/queries/admin-queue.ts), donc les deux chiffres ne peuvent pas diverger.
Il n'y a plus d'etat « lu » : une tache disparait quand elle est traitee — la
table de la migration 202608240005 n'est donc plus lue.
Le canal email a ete retire de l'interface : aucun fournisseur n'est
configure, la case n'envoyait donc rien. Les colonnes email_required /
email_sent_at de notifications restent en place pour le jour ou un
fournisseur sera choisi.
Ce qui reste hors perimetre : la remise effective d'un push (les
« receipts » Expo, qui demandent une seconde requete differee).
recipient_count dit combien de personnes ont ete servies ;
delivered_count reste a zero tant que personne ne relit les receipts, et
admin_notification_deliveries attend toujours ce suivi fin.
L'autorisation est verifiee a trois niveaux, volontairement redondants :
proxy.ts (l'ancien « middleware », renomme en Next 16) rafraichit le token
Supabase. Il ne fait pas l'autorisation — la doc Next le deconseille.requireAdmin() (lib/auth.ts) est appele par le layout /admin et par
chaque Server Action : une Server Action est joignable par un POST direct,
sans passer par l'interface.public.is_admin() bloquerait la requete.Les buckets prives (identity-documents, professional-documents,
guardian-documents) n'ont pas de lecture publique. La route
/admin/documents?bucket=…&path=… genere une URL signee (5 min) avec la session
administrateur, sur une liste blanche de buckets.
Les couleurs et la typographie sont reprises a l'identique de l'application
mobile (src/constants/theme.ts d'~/ifriqiyastar) : fond noir #000000,
surfaces #212225, bordures #333333, accent lime #aff70f, titres en Nunito
Sans, corps de texte en Poppins, cartes arrondies a 16 px et pastilles de statut
en petites majuscules interlettrees. Le theme clair equivalent
(Colors.light) est defini dans app/globals.css mais l'interface est rendue
en sombre, comme l'application.
Les couleurs de series des graphiques (--viz-1, --viz-2) sont distinctes
de l'accent de marque : l'accent est une couleur d'etat (bouton, onglet actif),
pas une identite de serie. Le couple retenu est valide sur la surface sombre
pour la bande de clarte, le plancher de chroma, la separation daltonisme et le
contraste ; legende et valeurs chiffrees restent affichees, la couleur ne porte
jamais seule l'information.
profiles. player_profiles
et professional_profiles ont deux cles etrangeres vers profiles (id et
status_updated_by), ce qui rend l'embed ambigu. Les identites sont donc
chargees par fetchProfilesByIds().profiles n'a pas de colonne avatar_url sur ce projet Supabase, malgre
ce que suggere 00_ALL_IN_ONE.sql (verifie contre la base reelle). La photo
vient des tables metier : player_profiles.profile_photo_url pour un joueur,
professional_profiles.photo_url pour un professionnel (migration mobile
0039). fetchProfilesByIds() fusionne l'une ou l'autre sous le nom
avatar_url ; seuls les comptes administrateurs restent sans vignette.player_profiles.status (player_profile_status) et
identity_verifications.status (identity_verification_status) sont deux
enums distincts ; seul le premier est lu par le resolveur d'onboarding mobile.
Les deux gestes sont donc proposes separement.is_hidden / is_deleted, pas de DELETE :
le schema n'ouvre pas de policy DELETE admin sur posts / post_comments, ce
qui est voulu — un contenu retire reste consultable par l'administration pour
instruire un signalement. Videos et photos joueurs, elles, sont bien
supprimees (ligne + fichier de stockage).app/global-not-found.tsx et le drapeau
experimental.globalNotFound. La mise en page racine de ce depot est un
segment dynamique (app/[locale]/layout.tsx) : Next n'a donc aucune mise en
page ou composer une 404 globale, et not-found.tsx seul rendait un document
d'erreur nu — corps vide cote serveur, pas de dir="rtl" en arabe, pas de
polices de marque. Ce fichier court-circuitant la mise en page, il importe
lui-meme globals.css et lib/fonts.ts, et recoit la langue deja resolue par
proxy.ts via l'en-tete x-ifriqiya-locale.| §12.4 | /admin | Utilisateurs par type de compte, profils actifs et valides, comptes actifs sur la periode (avec le rappel 7 j / 30 j), abonnements actifs, revenus et paiements, encaissements en attente, nombre de Scout Days et d'inscriptions. Le selecteur de periode (7 / 30 / 90 jours, 12 mois) filtre reellement les revenus, a la journee et en base ; l'ecran annonce la fenetre qu'il a mesuree, pas la duree cliquee. Un panneau liste a part les files qu'un super administrateur est seul a pouvoir vider. |
| §12.3 / §12.4 | /admin/finances | Paiements (dont activation manuelle des encaissements hors ligne, §10.3), abonnements, catalogue d'offres, revenus par mois. |
| Evaluations | /admin/evaluations | Creation, calcul serveur du score global, publication/masquage, suppression et historique SQL. Export CSV : les six axes de la migration mobile 0091 et les quatre d'avant, en colonnes separees — aucune conversion n'existe entre les deux, et une colonne vide dit « non note » la ou un zero dirait « nul ». |
| Notifications | /admin/notifications | Ciblage individuel, role, Scout Day ou plateforme ; notification in-app et push Expo ; historique et renvoi. |
| back-office |
Permission moderation.validate, attribuee au seul super_admin. A appliquer apres 0041. |
202608240005_admin_notification_reads.sql | back-office | Plus utilisee. Etat « lu » par administrateur, du temps ou la cloche listait les notifications des utilisateurs. La table reste en place, plus rien ne la lit. |
202608240006_drop_admins_manage.sql | back-office | Retire la permission admins.manage et la policy d'ecriture sur admin_user_roles : l'ecran d'attribution des roles ne figure pas au cahier des charges. L'attribution se fait desormais dans l'editeur SQL. |
0047_messaging_block_report.sql | mobile (~/ifriqiyastar) | Blocage et signalement d'un utilisateur depuis la messagerie : reports.context_conversation_id, report_conversation_user(). Le back-office lit le signalement, jamais les messages du fil. |
202609090001_realtime_admin_queue.sql | back-office | Publie en temps reel les sept tables comptees par la file d'attente, pour que la cloche et les pastilles reagissent a l'instant plutot qu'au sondage suivant. Facultative : sans elle le back-office fonctionne, avec une latence de dix secondes sur les compteurs. |
202609210001_blog.sql | back-office | Table blog_posts, entierement propre a ce depot (n'existe pas cote mobile) : articles rediges en Tiptap depuis /admin/blog, publies sur /blog. Bucket public blog-media, permission blog.manage accordee a tous les roles. |
202609230001_blog_author_name.sql | back-office | blog_posts.author_name : copie figee du nom d'auteur a la creation, pour l'afficher sans ouvrir profiles a une session anonyme. |
202609230002_blog_scheduling_seo.sql | back-office | blog_posts.meta_title / meta_description (repli sur title/excerpt quand vides) et scheduled_at avec le statut programme : publication reellement differee, sans tache planifiee — les deux pages du blog etant force-dynamic, la regle RLS suffit a rendre l'article visible a l'heure dite. |
202609230003_blog_category.sql | back-office | blog_posts.category : texte libre choisi par l'administration, pas une liste fermee. Alimente le filtre « Categories » de /blog. |
202609230004_admin_editor_role.sql | back-office | Role RBAC editeur : dashboard.read + blog.manage uniquement. Voir « Creer un compte editeur ». |
202609230005_admin_roles_grants.sql | back-office | grant select manquant depuis 202608240001 sur admin_roles/admin_user_roles pour authenticated : sans lui, la colonne « role RBAC » de /admin/utilisateurs echoue en silence (42501) et retombe sur le libelle generique « Administrateur » — invisible pour super_admin (libelle quasi identique), flagrant pour editeur. |
202609230006_editeur_blog_only.sql | back-office | Retire dashboard.read du role editeur : ne garde que blog.manage. A appliquer avec le changement de code qui redirige un compte sans dashboard.read vers sa premiere section accessible au lieu de /admin/acces-refuse (firstAccessiblePath()). |
202609240001_content_validation_permission.sql | back-office | Permission content.validate : l'equivalent applicatif du trigger de la migration mobile 0089, qui reserve au super administrateur la validation d'une publication ou d'un commentaire. |
202609300001_notification_email_channel.sql | back-office | Le canal e-mail des campagnes (§11, §12) : propre au back-office, l'application mobile n'envoyant aucun courriel. Preference de desabonnement et colonnes de suivi. |
202609300002_push_channel_choice.sql | back-office | Choix des canaux d'une diffusion, et correction d'un chiffre faux : « X comptes joignables par push » affichait toujours zero, push_tokens n'etant lisible que par son proprietaire. |
202609300003_email_template.sql | back-office | L'habillage des courriels, modifiable : les phrases qui entourent le message (accueil, bouton, pied, signature), une ligne par langue. |
202609300004_email_template_catalogue.sql | back-office | Plusieurs modeles d'e-mail, et le choix du modele au moment de la diffusion : une annonce de Scout Day ne se redige pas comme un rappel d'inscription. |
202609300005_email_colors_and_rich_body.sql | back-office | Cinq couleurs verifiees par contrainte (#rrggbb) et le message mis en forme. Pas de theme libre : une valeur libre finirait dans un attribut style de courriel. |
202609300006_email_blocks.sql | back-office | Le corps d'un modele se compose par blocs : le super administrateur ajoute une image ou deplace un bouton sans pouvoir produire un courriel casse. |
202609300007_campaign_email_subject.sql | back-office | L'e-mail porte son propre objet et son propre corps : un push dit « venez », un courriel explique. |
202610050001_admin_profile_status.sql | back-office | ⚠️ Un grant, pas une policy. « Valider le compte » repondait 42501 permission denied for table player_profiles : la migration mobile 0050 avait revoque les colonnes de statut a authenticated, et un privilege de colonne est verifie avant le RLS. |
202610060001_sync_profile_email.sql | back-office | profiles.email suit l'adresse de connexion : changer son e-mail depuis « Mon profil » ne touchait que auth.users, et le rail affichait l'ancienne adresse indefiniment. |
202610070001_scout_day_min_notice.sql | back-office | Le preavis minimum d'un Scout Day (demande client du 2026-10-07) : un evenement depose pour le lendemain ne laissait aucun joueur s'inscrire. Reglage pose par le super administrateur, applique par la base donc aussi a l'application mobile. |
202610140001_admin_fast_reads.sql | back-office | admin_access() et admin_queue_counts() : les lectures du layout en un aller-retour au lieu de quinze. Facultative, les deux appelants retombent sur leurs requetes d'origine. |
202610190001_queue_payments_to_collect.sql | back-office | La cloche annonce l'argent qui attend : une inscription payante reglee en especes n'a aucune ligne dans payments, donc l'ecran « Paiements » paraissait a jour. Demande client. |
202610210001_admin_guard.sql | back-office | admin_guard() : requireAdmin() + requirePermission() + getAdminAccess() en une seule lecture, le preambule paye par chaque page. Facultative au meme titre que 202610140001. |
202610220001_admin_dashboard_counts.sql | back-office | admin_dashboard_counts() : les lectures que /admin posait a cote de get_admin_dashboard(), rendues en une. Facultative — sans elle le tableau de bord retombe sur ces requetes, affiche les memes chiffres, et se charge plus lentement. |
202610230001_admin_dashboard_period.sql | back-office | La periode du tableau de bord atteint les revenus. Ils venaient d'une vue groupee par mois et la page y appliquait une fenetre en jours : « 7 derniers jours » consulte apres le 7 du mois ecartait le mois en cours et affichait zero. Le fenetrage descend dans la requete, au pas qui convient (jour / semaine / mois), et la fonction renvoie la borne reelle de la fenetre. Facultative — sans elle l'ecran montre les douze derniers mois et l'annonce. |
can_message()