Adminoverblik
Brugerstyring
| Brugernavn | Visningsnavn | Rolle | Hold | Oprettet | ||
|---|---|---|---|---|---|---|
Databaseoverblik
| Tabel | Rækker | Størrelse | Sidst opdateret |
|---|---|---|---|
Databaseskema
Holdværktøj
Brugervejledning
TeamApp er en digital platform til sportshold, der samler coaching, spillerudvikling, træningsplanlægning og taktik på ét sted. Platformen styrker kommunikationen mellem trænere og spillere og giver bedre overblik over holdets udvikling.
Systemet består af to primære brugerflader: Coach-siden (browser-baseret) til trænere og administratorer, og Team Play (PWA), spillernes app, der er optimeret til mobilbrug.
Rollen er pr. hold: den gemmes på medlemskabet (hold_brugere.rolle), så samme person kan være cheftræner på ét hold og spiller på et andet. Admin er den eneste globale rolle: admin-panelet når alle hold, men i selve appen når admin kun de hold, admin er medlem af. Kontoens type (admin, træner, spiller) bruges kun ved oprettelse; adgangen afgøres af rollen på det aktive hold. Rollen assistent findes ikke længere: tidligere assistenter er blevet trænere, og admin-panelet kan ikke oprette den (kontotyperne er admin, træner og spiller).
| Rolle | Adgang | Typisk bruger |
|---|---|---|
| Admin | Global. Admin-panelet når alle hold, brugeradministration, roller på hold og systemindstillinger; i selve appen kun de hold, admin er medlem af | Systemadministrator |
| Cheftræner | Alt, en træner kan, plus holdadministration: omdøbe og slette holdet, rette holdets sæson (årgange, sæsonnavn og datoer) og kampprogram-links, lukke og genåbne holdet, starte en ny sæson, invitere (som træner eller spiller) og fjerne medlemmer, oprette spiller-logins og give en spiller med login et nyt kodeord (Coaching → Trup). Er holdet lukket, er cheftræneren den eneste, der kan se det. Holdet har én cheftræner, som ikke kan fjernes. Den, der opretter et hold, bliver cheftræner | Holdets hovedansvarlige træner |
| Træner | Fuld adgang til de daglige værktøjer på holdet: Coaching, kampnoter, kampstatistik, spiller-indhold, træning, øvelsesark, tegning, sæsonplan, modstander-scouting, styrke/skadeforebyggelse og sit eget kørselsregnskab (udlæg, kørsel og afregning) samt link-biblioteket (opretter links og retter egne). Har desuden sine egne personlige biblioteker: taktikbiblioteket (åbninger), Beach-noter og træningsskabeloner. Ingen holdadministration | Træner |
| Spiller | Team Play (PWA): forside, åbninger, kampe, udvikling. Ingen adgang til Coaching, kampnoter, kampstatistik, spiller-indhold, træning, øvelsesark, tegning, sæsonplan, modstander-scouting, Beach eller skabeloner — ser kun de åbninger, en træner har delt med holdet, og kan læse, kommentere og like dem. Kan læse øvelsesbiblioteket og de styrkeprogrammer, der er sat på holdet, samt link-biblioteket. I kørselsregnskabet kræver det et hold, hvor man er træner, at oprette en post; en tidligere træner beholder og kan stadig rette, slette og afregne sine egne poster | Holdspiller |
Rettighederne følger rollen på det aktive hold. Træner og cheftræner har samme adgang til de daglige coach-værktøjer; kun holdadministration (omdøbe/slette hold, invitere/fjerne medlemmer, oprette spiller-logins) kræver cheftræner. Omkostningssatserne er fælles for alle klubber og ændres kun af admin
(requireAdmin()), og det fælles styrke/skade-bibliotek (øvelser og programmer) kan kun rettes/slettes af den, der oprettede det, eller af admin; et program sættes på et hold af enhver træner på holdet. Spiller har slet ingen adgang til coach-værktøjerne (kun til Team Play med de åbninger, træneren har delt,
læsning af de styrkeprogrammer, der er sat på holdet, og link-bibliotek). Taktikbiblioteket, Beach og træningsskabelonerne er personlige: de ligger uden for hold-gaten og scopes på ejer_id = den indloggede, så en andens åbning, note, fil eller skabelon giver 403/404 uanset rolle (admin kan dog rette og slette en andens åbning). At oprette dem kræver admin eller træner på mindst ét hold. Håndhæves to steder: i api.php sætter gaten for data-endpoints rollen pr. hold (requireHoldAccess() returnerer cheftraener, traener eller spiller),
og requireTraener() blokerer skrivekald server-side; holdadministration kræver kraevCheftraener(). Endpoints uden hold (fx de fælles biblioteker) kræver, at brugeren er cheftræner eller træner på mindst ét hold (traenerEtSted()). Et separat
rolle-tjek blokerer al adgang (læsning og skrivning) for spiller til de coach-specifikke endpoints. Siderne selv
omdirigerer en spiller væk med det samme (til spiller.html), og skjuler eller deaktiverer holdadministration for andre end cheftræneren. Et lukket hold (arkiv) kan kun holdets cheftræner (og admin, der er medlem) se, og kun læse: requireHoldAccess() svarer 403 "Holdet er lukket" til alle andre, også på læsning, og kraevAabentHold() svarer 403 "Holdet er lukket. Genåbn det for at rette." på al skrivning. Undtagelserne er genåbning, sletning af et tomt hold, guiden «Start ny sæson» (læser fra holdet), kørselsregnskabet og gemning af en træning som skabelon.
| Område | Admin | Cheftræner | Træner | Spiller |
|---|---|---|---|---|
| Admin-panel (admin.html) — inkl. at rette en brugers rolle på et hold | Fuld adgang | — | — | — |
| Coaching, kampnoter, spiller-indhold, træning, øvelsesark, tegning, sæsonplan, modstander-scouting | Fuld adgang | Fuld adgang | Fuld adgang | Ingen adgang |
| Kampstatistik — start, del/luk/nyt link og se tallene (Coaching og kampnoter); ret hændelser, halvlegsstart og resultat (kun Coaching) | Fuld adgang | Fuld adgang | Fuld adgang | Ingen adgang |
| Kampstatistik — tastesiden (kampstatistik.html#nøgle) | Ingen login: den, der har linket, kan taste for den ene kamp og intet andet | |||
| Sæsonplan — kopiér fra et andet hold | Hold, admin er medlem af | Kun hold, hvor man er cheftræner eller træner | Ingen adgang | |
| Taktikbibliotek (coaching.html) — egne åbninger: opret, ret, slet (slet skjuler en åbning, der bruges), videolinks | Egne; kan også rette og slette andres | Kun egne | Kun egne | Ingen adgang |
| Åbninger på et hold — koble egne åbninger på, fjerne en kobling, ændre deling med spillerne (enhver kobling på holdet) | Fuld adgang | Fuld adgang | Fuld adgang | Ingen adgang |
| Åbninger på et hold — rette eller slette en andens åbning (kun ejeren; en åbning uden ejer kun admin) | Fuld adgang | — | — | — |
| Åbninger i Team Play — læse, kommentere, like og markere læst (kun koblinger delt med spillerne) | Fuld adgang | Fuld adgang | Fuld adgang | Kun delte åbninger |
| Beach håndbold — egne noter og filer (andres er usynlige) | Kun egne | Kun egne | Kun egne | Ingen adgang |
| Træningsskabeloner — gem fra en træning, omdøb, slet og indsæt i en træning på et af ens hold (kun egne) | Kun egne | Kun egne | Kun egne | Ingen adgang |
| Styrke & skadeforebyggelse — oprette øvelser og programmer, rette og slette egne (slet skjuler en øvelse eller et program, der er i brug) | Fuld adgang (kan også rette og slette andres) | Opret; ret og slet kun egne | Opret; ret og slet kun egne | Ingen adgang |
| Styrke & skadeforebyggelse — sætte et program på holdet og fjerne det igen (ethvert ikke-skjult program, også andres) | Fuld adgang | Fuld adgang | Fuld adgang | Ingen adgang |
| Styrke & skadeforebyggelse — se holdets programmer | Fuld adgang | Fuld adgang | Fuld adgang | Kun læsning (programmer sat på holdet) |
| Hold — oprette hold (opretteren bliver cheftræner) | Fuld adgang | Ja | Ja | Ja |
| Hold — omdøbe/slette hold, invitere (som træner eller spiller) og fjerne medlemmer, oprette spiller-logins og give en spiller med login et nyt kodeord | Fuld adgang | Fuld adgang (cheftræneren kan ikke fjernes) | — | — |
| Hold — rette årgange, sæsonnavn, sæsondatoer og kampprogram-links (kun mens holdet er åbent) | Hold, admin er medlem af | Fuld adgang | Kun læsning | — |
| Hold — lukke og genåbne holdet; starte en ny sæson (guiden kan også køres fra et lukket hold) | Hold, admin er medlem af | Fuld adgang | — | — |
| Lukket hold (arkiv) — se og læse holdets data. Holdet forsvinder fra holdvælgeren og login-svaret for alle andre end cheftræneren | Hold, admin er medlem af | Kun læsning | Ingen adgang (403 «Holdet er lukket») | Ingen adgang (403 «Holdet er lukket») |
| Lukket hold (arkiv) — skrive (kampe, noter, trup, træning, kommentarer, reaktioner, taktik- og styrkekoblinger m.m.) | Genåbn først (403) | Genåbn først (403) | Ingen adgang | Ingen adgang |
| Lukket hold (arkiv) — registrere kørsel og gemme en træning som skabelon (egne data) | Egne data | Kørsel og skabeloner | Kun kørsel | — |
| Link-bibliotek (links-admin.html) — opret links; ret og slet egne (nye links venter på admins godkendelse) | Fuld adgang (godkendt med det samme, kan rette alle) | Opret og ret egne | Opret og ret egne | Kun læsning (den offentlige side) |
| Link-bibliotek — godkend/afvis links, kategorier, sortering, CSV-import og linktjek | Fuld adgang | — | — | — |
| Udlæg og afregning (kørselsregnskab) — kun egne: poster, afregninger og udlægsprofil. En post oprettes på et hold, hvor man er cheftræner eller træner | Kun egne | Kun egne | Kun egne | — |
| Udlæg og afregning — andres poster og afregninger (ingen rolle ser dem; en andens id giver 404) | — | — | — | — |
| Omkostninger — satser (fælles for alle klubber) | Fuld adgang | Kun læsning | Kun læsning | — |
| Kommentarer og reaktioner (like) | Alle + slet andres | Alle + slet andres | Alle + slet andres | Kun egne kan slettes |
| Team Play (spiller.html) | Adgang | Adgang | Adgang | Målgruppen |
Alle brugere logger ind via login.html med brugernavn og kodeord. Systemet gemmer en JWT-token lokalt og omdirigerer automatisk til den korrekte side baseret på brugerens rolle.
- Gå til login.html og indtast brugernavn + kodeord
- Systemet validerer og udsteder en JWT-token
- Cheftrænere, trænere og admins videresendes til forsiden index.html (eller tilbage til den side, der sendte dem til login)
- Spillere videresendes til spiller.html (PWA)
- Login huskes i 30 dage og forlænges automatisk, når appen bruges (glidende login) — først efter 30 dage uden brug kræves nyt login. Kodeordsskift logger alle andre enheder ud
- Efter 5 forkerte forsøg på samme brugernavn låses login i 15 minutter
- Lukkede hold (arkiv) er kun med i holdlisten for holdets cheftræner. Har brugeren både åbne og lukkede hold, starter login på et åbent hold; en træner eller spiller på et hold, der lukkes, mister adgangen med det samme
- Glemt kodeord (træner): tryk på "Glemt kodeord?" under login-formularen på login.html, skriv brugernavn eller e-mail og tryk "Send link". Svaret er altid det samme ("Hvis vi kender dig, har vi sendt en mail med et link. Tjek også spam."), uanset om kontoen findes. Mailen kommer fra "TeamApp" <kontakt@teamapp.dk> og har et link, der gælder i 1 time og kun kan bruges én gang; beder du om et nyt link, virker det forrige ikke mere. Linket åbner login.html med "Vælg nyt kodeord" (to felter, mindst 8 tegn); bagefter er du logget ind, og dine andre enheder er logget ud. Har du ikke fået en mail, så tjek spam eller bed om et nyt link (højst 3 forespørgsler pr. konto i 15 min). Admin kan stadig nulstille under Brugere i admin.html
- Glemt kodeord (spiller): en spiller har ingen e-mail og kan ikke bruge "Glemt kodeord". Holdets cheftræner åbner Coaching → Trup, trykker "Nyt kodeord" ved spilleren (knappen findes kun ved spillere med login og kun for cheftræneren på et åbent hold) og bekræfter. Et nyt midlertidigt kodeord på 10 tegn vises nu én gang i et vindue med "Kopiér" (det lukkes kun med "Luk" eller Escape), og cheftræneren giver det videre til spilleren. Spillerens andre enheder logges ud, og spilleren vælger selv et nyt kodeord under profil-knappen i Team Play
- Skift eget kodeord: brugermenuen øverst til højre i topmenuen (computersiderne) eller profil-knappen med dine initialer i Team Play
Forsiden ("Klubhuset") er indgangen til alle moduler og kræver ikke login. Hvert kort har to små ikoner: om det er lavet til mobil eller computer, og om det kræver login (åben lås = intet login, eller du er allerede logget ind). Er du logget ind som spiller, er trænermodulerne nedtonet, mærket "Kun trænere" og kan ikke åbnes.
- Hurtigstart i hallen (øverst) og Værktøjskassen (længere nede): Interval Timer, Sekvens Timer, Teamgenerator og Mini Turnering samt de eksterne apps Duel og Score — ingen login; Noter (under Hurtigstart i hallen) kræver træner-login
- Fire områder: Team Management, Team Play, Trænerressourcer og Beach Håndbold
- Handball Playmaker Studio (til computer, kræver login — kun trænere). Under den ligger de eksterne apps CoachClip (klip og tegn på kampvideo — intet login) og KlubHub (klubbens interne værktøj — har sit eget login, TeamApp-login gælder ikke)
- Forsiden har sin egen menu øverst (ikke den fælles topmenu): genveje til Områder, Værktøjskassen og Playmaker Studio samt Installér apps. Til højre skifter du tema og logger ind — er du logget ind, står der "Hej {navn}" og Log ud. Nederst på siden er der links til Links Admin og TeamApp Admin
- Trykker du på et computerkort på en telefon, får du først en besked om, at siden er lavet til computer ("Åbn alligevel" / "Bliv på forsiden")
På computersiderne (Coaching, træning, modstandere, styrke-skade, spiller-admin, links-admin, admin og afregning — samt link-biblioteket, når det åbnes i en browser på computer) står en fælles topmenu øverst. Den bruges til at skifte mellem moduler; sidens egen sidebjælke er kun navigation inde i det aktuelle modul.
- Klik på TeamApp-logoet for at komme til forsiden
- Åbn et rum — Team Management, Team Play, Ressourcer (med undergruppen Værktøjer) eller Beach — og vælg modulet. Du ser kun de moduler, din rolle har adgang til
- Hold: Skift aktivt hold i holdvælgeren — siden genindlæses med det valgte holds data. Vælgeren viser din rolle på hvert hold (cheftræner, træner eller spiller), og dine rettigheder følger det hold, du har valgt. Lukkede hold står nederst under overskriften «Tidligere hold» (kun cheftræneren ser dem), og er det valgte hold lukket, står banneret «Lukket hold — kun læsning» under topmenuen. Under Hold & indstillinger i Coaching ser du holdets medlemmer med rolle; cheftræneren kan invitere (som træner eller spiller) og fjerne medlemmer. Som spiller på flere hold skifter du hold fra profilen i Team Play
- Skift mellem mørkt og lyst tema med temaknappen
- Brugermenuen (dit navn) giver adgang til Admin (kun admin), Links-admin, Installér apps og Log ud
- På små skærme samles rummene bag en menuknap, og "Sektioner ▸" åbner sidens egen sidebjælke
Navigation og beskeder fungerer ens på tværs af siderne.
- "‹ TeamApp" øverst i småapperne (Beach, Interval Timer, Sekvens Timer, Teamgenerator, Mini Turnering, Udgifter og Noter) fører til forsiden — også når appen er installeret og åbnet fra hjemmeskærmen
- I Team Play, Coaching, træning og modstandere står den aktuelle visning i adressen (fx
#kampeeller#kamp/…). Telefonens og browserens tilbage-knap går derfor et skridt tilbage i appen i stedet for at forlade siden, og et genindlæst eller delt link åbner samme visning - Øvelsesarket åbnes for én bestemt træning, og "← Træning" fører tilbage til samme træning
- Har du ugemte ændringer i et modstander-ark, spørger siden, før du går videre — "Bliv" beholder ændringerne
- Fejl vises som en kort besked nederst på skærmen — flere steder også "Gemt ✓", når noget er gemt (fx modstandere og styrke-skade) — og sletning bekræftes i en dialogboks. Træningens note viser "Gemmer… / Gemt ✓ / Ikke gemt · Prøv igen"
- Er dit login udløbet, sendes du til login og bagefter tilbage til samme side og visning
Trænerens hovedside. Her oprettes og styres kampe, kampnoter, taktiske åbninger og spillerindhold. Forsiden Overblik viser nøgletal, næste kamp, seneste kampe og spillernoter samt kommende kampe; sidebjælken har modulets egne sider. Titelbjælken viser holdet og sæsonnavnet («hold · sæsonnavn») og har knappen til opdatering, og holdet vælges i topmenuen. Der er ingen sæsonvælger: siden viser altid hele holdets kampe, og Statistik dækker hele holdet.
- Log ind som træner og åbn coaching.html
- Vælg en kamp i overblikket eller under Kampoverblik — eller opret en med + Ny kamp
- Tilføj taktiske noter og spillerobservationer, og vælg åbninger til kampforberedelsen fra holdets åbninger (se afsnittet "Taktikbibliotek")
- Del indhold med spillere via Team Play Coach (spiller-admin.html)
- Se spillernes reaktioner og kommentarer på delt indhold
- Under kampen finder du kortet Kampstatistik: start statistik, del eller luk linket, se de fulde tal og ret dem bagefter (se afsnittet "Live kampstatistik" nedenfor)
Et hold er én sæson. Når en ny sæson begynder, opretter cheftræneren et nyt hold ud fra det gamle med guiden Start ny sæson under Hold & indstillinger i Coaching. Guiden kan køres fra et åbent eller et lukket hold, og hvis noget fejler undervejs, oprettes intet, og det gamle hold er uændret.
- Holddata: navn, klub, årgange, sæsonnavn (guiden foreslår næste sæson, fx 2027/28) og sæsonens start og slut. Du bliver cheftræner på det nye hold, og holdets kampprogram-links følger med
- Spillere: vælg, hvilke spillere der fortsætter. En fortsættende spiller får en ny række på det nye hold med din note, udvikling og styrker; har spilleren et login, flyttes det til den nye række, så spilleren logger ind og ser det nye hold i Team Play. Kampe og statistik på det gamle hold viser stadig spilleren. Tilføj desuden nye spillere med navn og evt. rygnummer
- Kopiér: vælg, om holdets sæsonplan (perioder og fokuspunkter; datoerne flyttes, og fokuspunkterne nulstilles), de åbninger, der er sat på holdet (med deres deling), og de styrkeprogrammer, der er sat på holdet, skal kopieres med. Skjulte åbninger og programmer kopieres ikke
- Luk det gamle hold: er valget slået til, lukkes det gamle hold, når det nye er oprettet
- Kampe, kampnoter, statistik, træninger, modstanderark, kampforberedelser, kommentarer og andre trænere kopieres aldrig. Inviter dine trænere til det nye hold, når det er oprettet
Et hold, der ikke bruges mere, kan lukkes, så det bliver et arkiv. Det gør cheftræneren under Hold & indstillinger med knappen Luk holdet.
- Et lukket hold kan kun ses af cheftræneren, og kun læses. Trænere og spillere mister adgangen med det samme, og holdet forsvinder fra deres holdvælger
- Cheftræneren finder holdet under «Tidligere hold» i holdvælgeren. Et banner under topmenuen minder om, at holdet er lukket
- Kørselsregnskabet kan stadig bruges: trænere kan registrere kørsel på et lukket hold, og cheftræneren kan gemme en træning som skabelon
- Genåbn giver trænere og spillere adgangen tilbage, og holdet kan rettes igen. Man kan også starte en ny sæson fra det lukkede hold
- Sletning kræver et helt tomt hold. Et hold med kampe, spillere, træninger, modstanderark, kampforberedelser, spillerudvikling, egne øvelser eller en sæsonplan kan ikke slettes: appen afviser det og viser, hvad holdet indeholder. Luk holdet i stedet, så bliver det et arkiv. Kun et hold, der er tømt for indhold, kan slettes: Administrér hold → blyanten → Slet (også det aktive eller eneste hold; appen spørger først). Admin kan slette et tomt hold under Hold-værktøj → Slet hold uden at være medlem
Åbningerne er dit eget bibliotek som træner, ikke et holds. Du sætter dem på de hold, du er træner på, og kan genbruge samme åbning på flere hold. Siden har to dele: Holdets åbninger (dem, der er sat på det aktive hold, også andre trænere) og Mit bibliotek (dine egne åbninger, som ikke er på dette hold).
- Åbn Taktikbibliotek i Coaching og tryk + Ny åbning: giv den navn, beskrivelse, hvilken forsvarsformation den er bedst mod. Åbningen oprettes i dit bibliotek og sættes samtidig på det aktive hold
- Tryk Tegn bane på åbningens kort (Ret tegning, når den allerede har en tegning) for at tegne åbningen som en række trin, som små baner efter hinanden på papiret. Tegn spillere, bold og kegler som i øvelsernes tegneflade. Pilene på et trin viser bevægelsen til næste trin: løb, aflevering, skud, skærm og dribling. Det sidste trin kan have pile for afslutningen
- + Næste trin sætter et nyt trin ind efter det aktuelle med spillere, bold og kegler kopieret (ingen pile, tekst eller note), så du kun flytter dem, der har ændret sig, og tegner trinets nye pile. Åbningen kan have op til 12 trin; Slet trin fjerner det aktuelle. Skriv en note til hvert trin (højst 300 tegn), fx hvem der løber hvorhen
- Tryk ▶ Afspil for at se åbningen: spillerne glider fra trin til trin, mens pilene skifter. Vælg hastighed 0,5×, 1× eller 2×. Tryk på banen eller ■ Stop for at standse. ◀ og ▶ eller piletasterne skifter trin. Et tryk på åbningens lille billede (i biblioteket, kampforberedelsen og kampvisningen) viser trinene i et vindue
- Under Mit bibliotek trykker du + Kobl på holdet for at sætte en af dine øvrige åbninger på det aktive hold. Du kan kun koble dine egne åbninger
- ✕ Fjern fra holdet fjerner koblingen (åbningen bliver i dit bibliotek). Det kan ikke lade sig gøre, hvis åbningen bruges i en kampforberedelse på holdet. Fjernes koblingen, forsvinder dens kommentarer og likes fra holdet
- Åbninger fra en anden træner på holdet ses med "Lavet af …". Du kan bruge dem i kampforberedelsen, men kun ejeren (eller admin) kan rette eller slette dem
- ✕ Slet (under Mit bibliotek) sletter åbningen. Bruges den stadig på et hold eller i en kampforberedelse, skjules den i stedet: den forsvinder fra dit bibliotek, men bliver på holdene og i kampforberedelserne, og ryddes væk, når den ikke bruges mere
- I Team Play Coach (spiller-admin.html, fanen Åbninger) vælger enhver træner på holdet pr. åbning, om spillerne må se den. Videolinks tilføjes kun af åbningens ejer. Knappen Se viser åbningens trin eller billede i et vindue (kun læsning; tegningen rettes i Taktikbibliotek). Deling og kommentarer hører til holdet, ikke til åbningen: deles samme åbning på to hold, har hvert hold sine egne kommentarer og sin egen deling
- Slettes en træners konto, bliver hans åbninger stående på holdene, men kun admin kan rette dem
Trænerne kan ikke selv føre statistik under kampen, så den tastes af en anden på bænken (fx en holdleder eller forælder) via et delt link — uden login. Statistikken gælder én kamp ad gangen og kan kun ses af trænere; spillere har ingen adgang.
- Start og del: åbn kampen i Coaching (kortet Kampstatistik) eller i kampnoters fane Statistik og tryk Start statistik. Vælg halvlegslængde (30, 25 eller 20 min) og om positioner og modstanderens angreb skal tælles. Tryk derefter Del link — på telefonen åbnes delemenuen, ellers kopieres linket
- Tastning: den, der har linket, åbner
kampstatistik.html#…på sin telefon. Vælg trøjenummer (eller ? hvis skytten er ukendt), evt. hvor skuddet kom fra, og resultatet: Mål, Reddet, Forbi eller Blokeret. Der er også knapper til 2 min, teknisk fejl, aktiv målmand og Start 1. halvleg / Start 2. halvleg / Slut. Fortryd sletter det seneste tryk. Skærmen holdes tændt, og tryk lægges i kø på telefonen, hvis nettet falder ud, og sendes, når der er forbindelse igen - Prøv først:
kampstatistik.html#demoviser tastesiden uden net og uden at gemme noget - Ved pausen: fanen Statistik i kampnoter viser stilling, mål/skud pr. spiller og målmændenes redninger. Tallene opdateres hvert 20. sekund eller med Opdatér
- Efter kampen: kortet Kampstatistik i Coaching viser de fulde tabeller. Under Ret hændelser kan en hændelse rettes eller slettes, og en glemt halvlegsstart kan rettes (alle minutter i halvlegen flyttes)
- Resultat: når kampen sluttes, skrives statistikkens stilling i kampens resultat — men kun hvis resultatet var tomt. Er resultatet et andet, tilbyder Coaching Brug som resultat
- Luk eller lav nyt link: Luk link stopper tastningen — det samme link holder op med at virke, og telefonen viser, at linket er lukket (det, der allerede er sendt, er gemt). Nyt link laver et nyt link, som du skal dele igen; det gamle bliver ukendt. Linket udløber også af sig selv (48 timer efter, at det er lavet, eller til udgangen af dagen efter kampdagen, hvis det er senere). Kun én telefon ad gangen kan taste — åbnes linket på en anden telefon, spørger siden, om den skal overtage
Team Play er spillernes mobilvenlige app, implementeret som en PWA, og kan installeres på spillerens telefon direkte fra browseren uden app store.
- Spilleren åbner spiller.html i mobilbrowseren og installerer appen
- Forside: Overblik over næste kamp og seneste opslag fra træneren
- Åbninger: Taktiske angrebsåbninger med video og tegning — kun dem, træneren har delt med holdet. En tegnet åbning vises trin for trin: bladr med Forrige/Næste, prikkerne eller et stryg til siden, læs trinets note, og tryk ▶ Afspil for at se spillerne bevæge sig (hastighed 0,5×/1×/2×). ↻ Højkant/Liggende vender banen; på en smal telefonskærm starter den i højkant, og valget huskes
- Kampe: Kampforberedelse og evaluering med noter og video
- Udvikling: Personlig udviklingsindhold opdelt i kategorier
- Refleksion: Personlige refleksioner efter kamp/træning — guidede spørgsmål med hurtigsvar, gemt lokalt på telefonen pr. bruger (ingen serveradgang), så en anden spiller på samme telefon ikke ser dem
- Spilleren kan like og kommentere på alt delt indhold
- Log ud sidder i topbaren; refleksionsloggen bliver liggende på telefonen til næste login
Banerne tegnes direkte på planlægningsarket (oevelsesark.html) og i bibliotek-fanen under Træning, i en fælles tegneflade (tegneflade.js). Hver bane gemmes som en lille tegning på øvelsen. Den gamle adresse tegning.html sender videre til biblioteket.
- Tryk + Bane på en øvelse – på planlægningsarket eller i en egen øvelse i Øvelsesbibliotek – så åbner tegnefladen
- Vælg en opstilling, eller placer spillere, målvogter, bold og kegler på banen
- Spillerne er tre grupper, og formen viser gruppen: Angreb ▲, Forsvar ● og Joker ■ (fx en joker i overtal eller et tredje hold til stafetter). Hver gruppe får sine egne numre. En spiller skifter gruppe ved at blive slettet og sat igen
- Tryk på den lille farvefirkant ved Angreb, Forsvar eller Joker for at vælge gruppens farve (blå, rød, grøn, gul, lilla, orange, hvid eller sort). Farven gælder alle gruppens spillere på banen. Målvogteren er altid grøn
- Tryk på farvefirkanten ved Kegle for at vælge farven på de næste kegler, du sætter (hvid, gul, rød, blå eller orange). Er en kegle markeret med Vælg, skifter den farve
- Farverne gemmes på banen. En ny bane (+ Bane) starter med den forrige banes farver, og den første bane med standardfarverne (angreb blå, forsvar rød, joker gul). Udskriften er sort/hvid: angreb sort, forsvar hvid og joker grå
- Tegn løb, afleveringer, skud, skærme og dribling som streger mellem spillerne, og skriv tekst på banen
- Giv banen en mærkat, og brug fortryd/gentag, hvis noget skal rettes
- Tryk Gem – banen følger øvelsen med ind i træningerne (en øvelse kan have op til 8 baner)
Et scouting-ark pr. modstanderhold, hvor trænerteamet samler observationer før en kamp. Arket er koblet til Coaching, hvor den aktuelle modstander får et link til sit ark, og noterne kan foldes ud i kampnoter.html under den aktuelle kamp.
- Søg i listen efter modstanderholdet, eller opret et nyt ark med holdets navn
- Vælg typisk forsvarsformation (6:0, 5:1, 5:1 skævt, 4:2, 3:3 eller 3:2:1) og noter angrebsstil
- Beskriv farlige spillere og målvogter-tendenser
- Tilføj generelle noter til resten af holdet
- Arket kan tilgås igen fra Coaching, når holdet igen møder modstanderen
Træningsmodulet bruges til at planlægge og dokumentere træningssessioner med et struktureret øvelsesbibliotek.
- Opret en ny træningssession med dato og titel
- Tilføj øvelser fra biblioteket eller opret nye øvelser
- Angiv starttid og varighed (minutter) for hver øvelse i øvelsesarket
- Gem sessionen – den er synlig for hele coachingteamet
Fanen Sæsonplan viser holdets perioder som kolonner og emnerne som rækker. Fokuspunkterne i cellerne har status og note; på telefon kan planen kun læses. På planlægningsarket kan hvert punkt kobles til periodens fokuspunkter, og sæsonplanen viser så, hvor mange gange hvert fokuspunkt er trænet. Sæsonplanen hører til holdet (holdet er sæsonen). En tom plan kan starte som en kopi af et tidligere holds perioder og fokuspunkter, også fra et andet af dine hold, hvor du er cheftræner eller træner (under «Eller kopiér fra et tidligere hold»); datoerne flyttes efter de to holds sæsonstart, og status nulstilles.
Dine noter fra Noter-appen har deres eget punkt, Træning → Noter (også i topmenuen under Team Management), med antallet af åbne noter ved punktet: dine egne noter på det aktive hold, nyeste først, med de brugte og afkrydsede samlet i en lukket liste nedenunder (se «Noter fra træningen»). Sæsonplanens titelbjælke har en genvej, «Mine noter (N åbne) →».
En skabelon er en gemt række af træningspunkter, du kan bruge igen. Skabelonerne er dine egne og følger dig på tværs af hold, så en skabelon fra U15 kan bruges på U17. En skabelon gemmer hvert punkts navn, emne, varighed, beskrivelse og baner — ikke kobling til sæsonplanens fokuspunkter eller til øvelsesbiblioteket, og ikke billeder.
- Åbn en træning på planlægningsarket og tryk Gem som skabelon. Giv den et navn (unikt blandt dine skabeloner, uden forskel på store og små bogstaver) og evt. et tema
- Når du opretter en ny træning, kan du vælge en skabelon i feltet Start fra skabelon (kun hvis du har nogen)
- På en eksisterende træning tilføjer Fra skabelon skabelonens punkter efter de punkter, der allerede er. Det sker alt eller intet
- I traening.html kan du omdøbe og slette dine skabeloner. En skabelon består, selv om den træning, den blev gemt fra, slettes
Noter er en lille app til telefonen, så du kan få en idé ned i hallen uden at forlade træningen. Noterne er dine egne og hører til ét hold: andre trænere kan ikke se dem, heller ikke på samme hold, og spillere har ingen adgang. Appen kræver, at du er cheftræner eller træner på et åbent hold. Er holdet lukket, kan noterne kun læses.
- Åbn Noter (fra forsiden under Hurtigstart i hallen eller fra Installér apps) og log ind. Har du flere hold, vælger du holdet i feltet øverst; valget huskes kun i Noter og ændrer ikke det aktive hold i resten af TeamApp
- Vælg Husk (noget, du skal huske) eller Øvelsesidé, skriv noten (højst 500 tegn) og tryk Gem note. Feltet tømmes og står klar til næste note. Noten får dagens dato og kobles til holdets første træning i dag, hvis der er en
- Under feltet står Mine åbne noter på holdet, nyeste først. Her kan du allerede på telefonen rette en note (tekst og type), krydse den af (den forsvinder herfra og kan genåbnes på computeren) eller slette den efter en bekræftelse. Telefonen viser de 10 nyeste; har holdet flere åbne noter, står der, hvor mange ældre der ligger på computeren
- Vælg en træning under Træning → Træninger: under Øvelsesark står Noter fra hallen med dine nyeste åbne noter, og Sæt i denne træning lægger en øvelsesidé som et punkt sidst på træningen med ét klik. En Husk-note har i stedet Sæt i noten, der føjer den til Note til træningen (ingen minutter på tidslinjen); på Noter-siden hedder den Sæt i træningens note
- Åbn Træning → Noter på computeren. Under Mine noter kan du for hver åben note vælge Sæt i træning (vælg en kommende eller tidligere træning; noten bliver et nyt punkt sidst på træningen, 10 minutter, med notens første linje som navn og hele teksten som beskrivelse) eller Gør til øvelse (noten bliver en øvelse i dit eget øvelsesbibliotek)
- Kryds af lægger en note, du er færdig med, i den lukkede liste. Ret ændrer tekst og type. Slet fjerner noten efter en bekræftelse
- En note kan kun bruges én gang. Brugte og afkrydsede noter står under «Brugte og afkrydsede noter» (en brugt note viser, hvor den blev brugt) og kan genåbnes, så de igen er åbne. Er punktet eller øvelsen slettet bagefter, står noten som «Brugt (slettet)»
- Noterne følger holdet: slettes holdet, slettes noterne. De kopieres ikke med, når du starter en ny sæson
Beach-noterne (angreb, forsvar, øvelser med tekst, YouTube-link og vedhæftede filer) er trænerens egne. De hører ikke til et hold, og andre trænere — også på samme hold — kan ikke se dem. Spillere har ingen adgang. Noterne overlever, at et hold slettes. Slettes ejerens konto, bliver noterne ejerløse og usynlige.
- Åbn Beach og log ind som træner
- Vælg kategori og opret en note med titel, tekst og evt. YouTube-link (kun http/https) samt PDF, Word-fil eller billede/video (højst 16 MB pr. fil)
- Ret eller slet noten fra listen; en slettet note tager sine filer med
Modulet har to faner: et fælles øvelsesbibliotek og programmer. Både øvelser og programmer er fælles for alle trænere (på tværs af hold og klubber); et program sættes på de hold, der skal bruge det.
- Bibliotek: Opret styrke-/skadeforebyggelsesøvelser med kategori, søg og filtrér i det fælles bibliotek. Kun den, der oprettede en øvelse (eller admin), kan rette eller slette den. Bruges øvelsen i et program, skjules den i stedet for at blive slettet: den forsvinder fra biblioteket, men står stadig i programmerne
- Programmer: Fanen viser Holdets programmer og Alle programmer (de fælles programmer, der endnu ikke er på holdet). Opret et program og tilføj øvelser fra biblioteket; et nyt program kommer på det aktive hold, når du gemmer det
- Kobl på holdet: Enhver træner på holdet kan sætte et fælles program på holdet ("Kobl på holdet") og fjerne det igen ("Fjern fra holdet"), uanset hvem der oprettede det. Kun opretteren (eller admin) kan rette programmet og dets øvelser eller slette det. En træner på mindst ét hold kan redigere det fælles bibliotek, selv om det aktive hold er et spillerhold, men programmer kan kun kobles på hold, hvor du er træner
- Slet et program: Er programmet på et hold, skjules det ("Fjernet fra biblioteket"): holdene beholder det, men det kan ikke sættes på flere hold. Det slettes helt, når det ikke længere er på noget hold
- Spillere ser kun de programmer, der er sat på deres hold, og kan kun læse dem
En kurateret linksamling til håndbold-ressourcer (taktik, Instagram-profiler, regler, videokanaler m.m.), organiseret i op til to niveauer af kategorier. Den offentlige side links.html kræver ikke login — alle (spillere, forældre, trænere) kan browse og søge frit. Administrationen sker via links-admin.html (kræver login som admin eller som cheftræner/træner på mindst ét hold — også når det aktive hold er et spillerhold; en ren spiller kan kun læse). Alle trænere kan foreslå links, men et link fra en træner vises først offentligt, når admin har godkendt det.
- Log ind som træner/admin og åbn links-admin.html
- Admin opretter hovedkategorier (evt. med billede) og underkategorier under dem
- Opret enkelte links manuelt via "+ Nyt link". Et link fra admin er offentligt med det samme. Et link fra en træner får mærket "Venter på godkendelse" og vises ikke offentligt, før admin har godkendt det. Man kan rette og slette sine egne links; ændrer man et links URL, skal admin godkende det igen
- Admin ser øverst sektionen "Til godkendelse" med linkets opretter og knapperne Godkend og Afvis (afvisning sletter linket). Admin kan også importere mange links på én gang via "⬆ Importer links": vælg en mål-kategori, upload en CSV-fil med kolonnerne
titel,urlogbeskrivelse(en skabelon kan downloades i dialogen), gennemse forhåndsvisningen — rækker med manglende felter, ugyldige URL'er eller dubletter (i filen eller allerede i biblioteket) markeres og springes automatisk over — og bekræft importen - Admin bruger "Tjek alle links" periodisk for at finde og rette døde links
- Se klik-statistik pr. link for at identificere de mest brugte ressourcer
Trænernes personlige kørselsregnskab: udlæg, kørsel, fortæring og fast godtgørelse (telefon, administration, tøj). Posterne er kun dine egne — hverken admin eller cheftræneren ser dem. Appen Udgifter (index.html i mappen Omkostningsgodtgørelse) er til telefonen; afregningsrapporten (afregning.html) er til computeren.
- Ny post: vælg hold for hver post (kun hold, hvor du er cheftræner eller træner). Holdets klubnavn følger med posten; har holdet intet klubnavn, skal cheftræneren sætte det under Hold & indstillinger. Beløbet udregnes af serveren efter statens satser for året (kørsel: km × antal × kilometersats, hvor antal er 1 eller 2 ved retur; fortæring: antal × sats). Kun beløbet på et udlæg skriver du selv
- Fast godtgørelse vælges som procent af årets maksimum pr. type; for samme klub, år og type kan du i alt højst bruge 100 %
- Historik: ret eller slet egne åbne poster. En post, der er med i en afregning, er låst; flyt en åben post til et andet hold ved at vælge holdet igen
- Oversigt: vælg klub (i stedet for sæson) for at se summer pr. kategori. Klubber med forskellig stavemåde af store/små bogstaver er samme klub. Ældre poster uden klub står under "Uden klub"
- Afregning: åbn afregningsrapporten, vælg klub og afregn. Alle dine åbne poster i klubben (på tværs af hold) markeres som afregnet og låses. Rapporten kan udskrives eller hentes som Excel med klubbens navn. Genåbn afregning frigiver posterne igen
- Slettes et hold, bliver dine poster og afregninger stående (poster fra holdet vises som "Slettet hold") og kan stadig afregnes. Slettes din bruger, slettes dine poster, afregninger og udlægsprofil
Admin-panelet bruges til systemadministration og er kun tilgængeligt for brugere med admin-rollen.
- Overblik: Statistik over alle hold, brugere og data på tværs af systemet
- Brugere: Opret, ret visningsnavn og e-mail, nulstil kodeord, spær/genaktivér og slet brugere og spillere — samt importér en eksisterende spiller som login-bruger. E-mail er påkrævet for admin og trænerkonti og aldrig for spillere. Admin retter den under Brugere → Rediger; man retter selv sin e-mail under brugermenuen → "Min e-mail"
- Database: Se diskforbrug og tabelstørrelser for alle tabeller
- Skema: Visualiser databaseskemaet — tabeller grupperet i domæner med kolonner og relationer
- Roller på hold: Under Brugere og Hold-værktøj kan admin se og rette en brugers rolle på hvert hold (cheftræner, træner, spiller) og tilføje en bruger til et hold med en rolle. Hvert hold har én cheftræner; vælges en ny, bliver den gamle træner
- Hold-værktøj: Se alt tilknyttet data for ét hold i dybden — roster, adgang, sæson, historik og sundhedsadvarsler — ret roller og slet et tomt hold. At genbruge åbninger, øvelser, sæsonplan og styrkeprogrammer på et nyt hold gør træneren selv (Taktikbibliotek, eget øvelsesbibliotek, Sæsonplan → kopiér fra et tidligere hold, Styrke & skade og guiden "Start ny sæson")
Tekniskspecifikation
TeamApp er en serverless-lean webapplikation baseret på statiske HTML/CSS/JS-sider der kommunikerer med en PHP-backend via REST API. Der er ingen build-pipeline – alt deployes direkte som statiske filer.
| Lag | Teknologi | Detaljer |
|---|---|---|
| Frontend | HTML5 · CSS3 · Vanilla JS | Ingen frameworks. Statiske filer på CDN/webhotel |
| Backend | PHP 8+ | api.php og admin2.php – REST-endpoints |
| Database | MySQL / MariaDB | Relationel database med JOINs og indeksering |
| Auth | JWT (JSON Web Tokens) | HS256-signerede tokens, gemt i localStorage |
| Hosting | teamapp.dk | Webhotel med PHP og MySQL support |
| PWA | Service Worker + Web App Manifest | Team Play kan installeres og køre offline |
| Typografi | Google Fonts | Outfit · Space Grotesk · DM Mono (design.css) · IBM Plex Sans (forside og login) |
Systemet anvender JWT-baseret autentificering. Login-flowet er:
- Brugeren sender
brugernavn+kodeordvia POST tilapi.php?action=login - Backend validerer credentials mod databasen (bcrypt-hashet kodeord). Maks. 5 forkerte forsøg pr. brugernavn i 15 min (
LOGIN_MAX_FORSOEG/LOGIN_VINDUE, tabellenlogin_forsoeg) — derefter429 - Returnerer JWT-token med payload:
{ sub, navn, rolle, tv, iat, exp }(ingenhold_id: holdet følger med i hvert kald; gamle tokens medhold_idvirker, feltet ignoreres) - Frontend gemmer token i
localStorage.kn_token - Alle efterfølgende API-kald sender token i
Authorization: Bearer <token> - Backend validerer signaturen og udløbsdatoen på hvert kald og slår rolle,
aktivogtoken_versionop ibrugere— rollen i tokenet bruges ikke
Glemt kodeord (skema 21): tabellen kodeord_nulstillinger (bruger_id med CASCADE, kun sha256-hashen af tokenet, udloeber, brugt). glemt_kodeord svarer altid 200 med samme tekst og tager mindst 1 s; mailen sendes inden for sekundet, før svaret, via sendMail() i mail.php (PHPMailer i lib/PHPMailer/, SMTP-oplysningerne ligger i config.local.php). Linket bygges af APP_URL og gælder 1 time og én gang. Grænser i 15 min via login_forsoeg: 3 pr. indtastning, 10 pr. IP og 3 pr. konto. nulstil_kodeord hæver token_version og rydder login-låsen. spiller_nyt_kodeord er kun for cheftræneren og kun for rene spillerkonti.
Token-levetid: 30 dage (TOKEN_TTL) med glidende login: er et token over et døgn gammelt, sender api.php et nyt i headeren X-Nyt-Token, som api-config.js gemmer for alle sider. Kodeordsskift/-nulstilling hæver token_version, så alle tidligere udstedte tokens bliver ugyldige.
Lukkede hold (skema 17): hold.status er aaben eller lukket. requireHoldAccess() afviser alle andre end holdets cheftræner (og admin, der er medlem) med 403 "Holdet er lukket", også på læsning. Gaten for $dataEndpoints kalder desuden kraevAabentHold() på alt andet end GET (403 "Holdet er lukket. Genåbn det for at rette."), så også kommentarer, reaktioner og laest_status afvises. Undtagelser, der ligger uden for gaten og tjekker selv: PUT hold med kun status, GET hold, GET hold_brugere, DELETE hold, POST hold_ny_saeson, POST/PUT omkostninger (cheftræner og træner) og POST traening_skabeloner (cheftræner). POST taktik og POST styrkeprogrammer med hold_id, hold_inviter, DELETE hold_brugere og kampstatistikkens tastesider (stat_link*, 403 ved skrivning) afviser også et lukket hold. Lister over hold gennem medlemskaber (login, GET hold, hold-listerne i taktik og styrkeprogrammer, saesonplan_kildehold) viser kun lukkede hold for cheftræneren (HOLD_SYNLIGT_SQL); traenerEtSted() tæller cheftræner-medlemskaber på lukkede hold med, men ikke træner- og spiller-medlemskaber.
Der er to API-filer: api.php til generel brug og admin2.php til admin-funktioner (kræver admin-rolle i JWT).
| Endpoint | Metode | Beskrivelse |
|---|---|---|
api.php?action=login | POST | Login – returnerer JWT token |
api.php?action=skift_kodeord | POST | Skift eget kodeord – logger andre enheder ud og returnerer nyt token (alle roller; begrænset antal forkerte forsøg) |
api.php?action=glemt_kodeord | POST | Glemt kodeord (offentlig, uden login). Body: {login} (brugernavn eller e-mail). Svarer altid 200 med samme tekst, også for ukendt konto, spiller, spærret konto, over grænsen og ved mailfejl; svaret tager mindst 1 s. Sender et engangslink (1 time) til kontoens e-mail. Højst 3 pr. indtastning, 10 pr. IP og 3 pr. konto i 15 min |
api.php?action=nulstil_tjek | POST | Tjekker et link fra mailen uden at bruge det (offentlig). Body: {token}. 200 {success}, ellers 400 med samme tekst som nulstil_kodeord. login.html kalder det, når linket åbnes. |
api.php?action=nulstil_kodeord | POST | Nyt kodeord med linket fra mailen (offentlig). Body: {token, nyt_kodeord}. 400 ved kodeord under 8 tegn eller et ukendt, udløbet eller brugt link; ellers 200 med samme svar som login (brugeren er logget ind, andre enheder logges ud) |
api.php?action=spiller_nyt_kodeord | POST | Nyt midlertidigt kodeord til en spiller med login på holdet. Body: {hold_id, spiller_id}. Kun holdets cheftræner (admin, hvis medlem) og kun på et åbent hold; kun rene spillerkonti (403 ellers); 404, hvis spilleren ikke har et login på holdet. Svar: {success, kodeord} (10 tegn, vises kun én gang) |
api.php?action=hold | GET | Brugerens egne hold med rollen på hvert hold (rolle; bruges bl.a. af topmenuens holdvælger) og holdets felter: aargange, saeson_navn, saeson_start, saeson_slut, status (aaben/lukket), forgaenger_id og kampprogram_links (liste af {titel, url, sub}, null hvis holdet aldrig har fået links). Åbne hold først; lukkede hold er kun med for holdets cheftræner (og admin). POST opretter et hold, og opretteren bliver cheftræner (kun admin, trænerkonti og trænere på et hold; en spillerkonto får 403). PUT {id, …} (kun cheftræner) retter navn (påkrævet, ≤ 100), klub (≤ 100), aargange (≤ 50), saeson_navn (≤ 20), saeson_start/saeson_slut (gyldige datoer, start ≤ slut), kampprogram_links (højst 10 med http(s)-adresse) og status; på et lukket hold accepteres kun status (genåbning). DELETE kræver holdets cheftræner (admin også uden at være medlem; Hold-værktøj → Slet hold) og et helt tomt hold: holdet låses, og kampe, spillere, traeninger, modstandere, kamprum, spillerudvikling, su_kategorier, oevelser og saesonplan_perioder tælles i samme transaktion. Har holdet indhold, svarer serveren 409 {error, indhold:{tabel:antal}} (kun tabeller med antal over 0), og intet slettes |
api.php?action=hold_ny_saeson | POST | Guiden «Start ny sæson» (kun kildeholdets cheftræner; kildeholdet må være åbent eller lukket). Body: {fra_hold_id, navn, klub, aargange, saeson_navn, saeson_start, saeson_slut, fortsaetter: [spiller_id…], nye_spillere: [{navn, nummer}], kopier: {saesonplan, taktik, styrke}, luk_gammelt}. Alt sker i én transaktion: det nye hold får forgaenger_id = kildeholdet, brugeren som cheftræner og kildeholdets kampprogram-links (sæsonnavn og -datoer står på holdet; der oprettes ingen sæsonrække). En fortsættende spiller (skal tilhøre kildeholdet, ellers 400) får en ny spillere-række, og et login følger med: det nye holds medlemskab får hold_brugere.spiller_id = den nye række, mens det gamle medlemskab beholder sin kobling (arkivet); kopierne dækker sæsonplanen (kopierSaesonplan()), koblinger til ikke-skjulte åbninger og programmer. Kampe, noter, statistik, træninger, modstanderark, kommentarer og andre trænere kopieres aldrig. Svar: {success, hold_id, spillere, perioder, aabninger, programmer} |
api.php?action=hold_brugere | GET/DELETE | Holdets medlemmer med rolle (alle på holdet). DELETE fjerner et medlem (cheftræner/admin); holdets cheftræner kan ikke fjernes (409). Læsning følger reglen for lukkede hold; fjernelse kræver et åbent hold |
api.php?action=hold_inviter | POST | Tilføj en eksisterende bruger til holdet via brugernavn med rolle traener eller spiller (cheftræner/admin) |
api.php?action=kampe | GET | Hent kampe for holdet |
api.php?action=saesonplan · saesonplan_periode · saesonplan_fokuspunkt · saesonplan_periode_for_dato | GET/POST/PUT/DELETE | Sæsonplanen hører til holdet: kun hold_id, ingen sæson-id (hold-gatet, spiller blokeret, skrivning kræver træner). GET saesonplan giver holdets perioder og fokuspunkter; POST saesonplan_periode tager navn, fra_dato, til_dato m.fl. og afviser overlap med holdets øvrige perioder (400) |
api.php?action=saesonplan_kildehold · saesonplan_kopier | GET · POST | «Kopiér fra et tidligere hold» (erstatter saesonplan_kildesaesoner). GET: de hold med mindst én periode, hvor brugeren er cheftræner eller træner (hold_id, hold_navn, saeson_navn, saeson_start, antal_perioder; nyeste sæsonstart først; det valgte hold er med, og siden skjuler det). POST {fra_hold_id}: kopierer perioder og fokuspunkter ind i det valgte hold, som skal være uden perioder (409); et fremmed eller ukendt kildehold giver 404, et uden perioder 400. Datoerne flyttes efter de to holds saeson_start; fokuspunkterne starter med status ikke |
api.php?action=taktik | GET/POST/PUT/DELETE | Trænerens eget taktikbibliotek, uden hold_id (uden for hold-gaten, scopet på aabninger.ejer_id). GET: egne, ikke-skjulte åbninger med deres koblinger (hold: link_id, hold_id, delt_med_spillere). POST: opret (admin eller træner på mindst ét hold; med hold_id kobles den samtidig, kræver træner dér). PUT/DELETE: kun ejer eller admin, ellers 403. DELETE sætter skjult=1 (svar skjult: true), hvis åbningen bruges af en kobling eller kampforberedelse, ellers slettes den. Feltet billede (kolonnen tegning) er et billede (URL eller data:image) eller en trin-tegning, se "Åbningens trin" |
api.php?action=aabninger | GET | Holdets åbninger = de koblede (også skjulte): åbningens felter (id = åbningens id), link_id, delt_med_spillere, ejer_id, ejer_navn og kan_rette. Kun læsning; spiller blokeret |
api.php?action=hold_aabninger | POST/PUT/DELETE | Koblinger mellem åbninger og hold (hold_aabninger, unik pr. hold og åbning). POST {aabning_id}: kun egne, ikke-skjulte åbninger (403/409). PUT {id, delt_med_spillere}: enhver træner på holdet. DELETE {id}: 409 hvis åbningen bruges i en kampforberedelse på holdet; fjerner koblingens kommentarer, likes og læst-status |
api.php?action=kamp_aabninger | GET/POST/DELETE | Åbninger i en kampforberedelse. POST giver 403, når kampen ikke er holdets, eller åbningen ikke er koblet på holdet. Serveren laver id'et og svarer med det; et id fra klienten ignoreres |
api.php?action=aabning_videoer | GET/POST/DELETE | Videolinks hører til selve åbningen: POST/DELETE kun ejer (eller admin), GET kræver, at åbningen er koblet på holdet. POST: serveren laver id'et og svarer med det |
api.php?action=spillere | GET | Hent spillerliste med har_bruger_adgang (ud fra spillerloginnets kobling hold_brugere.spiller_id). DELETE med fjern_login (cheftræner/admin) finder kontoen via koblingen, fjerner spillerens medlemskab og spærrer kontoen (aktiv=0), medmindre den er admin, har rollen cheftræner/træner på et hold eller er medlem af et andet åbent hold (et lukket hold tæller ikke, så loginet ikke holdes i live af et gammelt arkivhold). Svaret har konto_aktiv (true, når loginet blev bevaret) |
api.php?action=traeninger | GET | Hent træningssessioner |
api.php?action=opret_spiller | POST | Opret ny spillerbruger (cheftræner/admin). Valgfri spiller_id kobler kontoen til en trupspiller på holdet (404 hvis spilleren ikke er på holdet, 409 hvis den allerede har et login); koblingen ligger på medlemskabet (hold_brugere.spiller_id, unik). Konto og medlemskab oprettes i én transaktion: et tabt kapløb giver 409, og et spiller_id, der ikke er en streng, giver 400 |
api.php?action=spiller_brugere | GET | Holdets spiller-logins (kræver træner/cheftræner/admin) |
api.php?action=spiller_forside · spiller_aabninger · spiller_kampe | GET | Team Plays data: næste kamp, seneste udviklingsopslag og ulæst-tal · delte koblinger (id = koblingens id, aabning_id = åbningen) · kampe med kamprum (alle holdmedlemmer, også spiller) |
api.php?action=kamprum | GET/POST/PUT/DELETE | Kampforberedelse/-evaluering pr. kamp (Team Play Coach). Skrivning: træner/cheftræner/admin; spiller blokeret – læser via spiller_kampe. POST: kamp_id skal høre til holdet (ellers 404), type er forberedelse eller evaluering (ellers 400), en dublet giver 409, og serveren laver id'et og svarer {success, id} |
api.php?action=spillerudvikling · su_kategorier | GET/POST/PUT/DELETE | Udviklingsopslag i kategorier. Læses også af spiller; skrivning: træner/cheftræner/admin. kategori_id skal være en af holdets kategorier (ellers 404); null er tilladt |
api.php?action=kommentarer | GET/POST/DELETE | Kommentarer på åbninger, kamprum og udviklingsopslag (alle roller; træner/admin kan slette andres). For indholdstype=aabning er indholds_id koblingens id, og en spiller kan kun røre delte koblinger. Kommentarens id laves på serveren |
api.php?action=reaktioner · laest_status | GET/POST | Like (slå til/fra) og læst-markering til ulæst-badges (alle roller; for åbninger hører de til koblingen) |
admin2.php (alle handlinger) | — | Kræver login og admin (requireAuth() og requireAdmin()), før noget andet kører, også kolonne-sikringerne (ingen DDL for en anonym). En ukendt handling giver 404 «Ukendt admin-endpoint»; flyt_spillere og kopier_indhold er fjernet, og sæsonskiftet klares af guiden «Start ny sæson» |
admin2.php?action=overblik | GET | Alle hold med statistik (admin) |
admin2.php?action=brugere | GET | Alle brugere (admin) |
admin2.php?action=opret_bruger | POST | Opret ny bruger (admin). E-mail er påkrævet for admin og trænerkonti, aldrig for spillere. Kontotypen (admin, traener eller spiller) er ikke en rolle på et hold; kontoen tilknyttes hold bagefter. Kontotypen assistent findes ikke og giver 400 |
admin2.php?action=opdater_bruger | PUT | Ret visningsnavn og e-mail (admin) |
api.php?action=min_email | GET/PUT | Egen e-mail (admin og trænerkonti) |
admin2.php?action=reset_kodeord | POST | Nulstil kodeord (admin) |
admin2.php?action=slet_bruger | DELETE | Slet bruger (admin). Sletningerne kører i én transaktion |
admin2.php?action=hold_spillere | GET | Spillere-roster for ét hold (admin); har_bruger ud fra hold_brugere.spiller_id |
admin2.php?action=hold_detalje | GET | Fuldt røntgenbillede af ét hold på tværs af alle moduler, inkl. medlemmer med rolle (admin). hold har klub, saeson_navn, saeson_start, saeson_slut og status (ingen sæsonliste); traeninger har antal, forste og sidste. Åbninger tælles via koblingerne; beach returneres stadig som nuller, men vises ikke |
admin2.php?action=tilfoej_hold · saet_holdrolle | POST | Tilføj en bruger til et hold med en rolle (cheftraener/traener/spiller) · ret en brugers rolle på ét hold. En ny cheftræner flytter den gamle til træner. fjern_hold kan ikke fjerne cheftræneren, før en ny er valgt |
admin2.php?action=tabel_storrelse | GET | Database-statistik (admin) |
api.php?action=link_kategorier | GET | Alle link-kategorier, hierarkisk (offentligt, ingen auth) |
api.php?action=links | GET | Godkendte links, evt. filtreret på kategori/sortering (offentligt, uden interne felter) |
api.php?action=link | POST | Opret nyt link (alle trænere; admins er godkendt med det samme, andres venter). CSV-import i links-admin bruger samme endpoint |
api.php?action=link_admin_liste | GET | Linkliste til links-admin: admin ser alle inkl. ventende og opretterens navn, andre ser godkendte plus egne ventende (træner/admin) |
api.php?action=link_godkend | POST | Godkend eller afvis et ventende link (handling: godkend/afvis; afvis sletter linket) (admin) |
api.php?action=valider_links | GET | HTTP HEAD-tjek af alle links – finder døde links (kun admin). Kun http/https; DNS-svaret tjekkes for hvert hop, og private, reserverede, link-local, CGNAT- og NAT64-adresser blokeres (et blokeret link får http_status 0 og fejl «Blokeret adresse» og kaldes aldrig). Forbindelsen bindes til den tjekkede adresse, og højst 5 omdirigeringer følges, hvor hvert hop tjekkes igen. Tidsgrænser: 3 s til forbindelse, 5 s pr. forespørgsel, 10 s pr. link |
api.php?action=omkostninger | GET/POST/PUT/DELETE | Brugerens egne poster (udlæg, kørsel, fortæring, fast godtgørelse); uden hold_id i gaten, kun bruger_id = den indloggede (andres id giver 404). GET kan filtreres med klub, open=1 og afregning_id. POST kræver et hold_id, hvor brugeren er cheftræner/træner (403 ellers; 409 uden klubnavn på holdet). På PUT er hold_id valgfrit: udeladt (eller tomt) beholder posten det gemte hold og klubnavn; et nyt hold valideres som ved POST. Serveren beregner km_sats og belob ud fra omk_satser (kun beløbet på udlæg kommer fra klienten); fast godtgørelse højst 100 % pr. bruger, klub, år og type (409). Afregnede poster kan ikke rettes eller slettes (409) |
api.php?action=omk_klubber | GET | Brugerens hold som cheftræner/træner (id, navn, klub, status; også lukkede hold, fordi kørsel kan registreres dér) og brugerens klubber fra hold, poster og afregninger (én pr. klub; '' = uden klub) |
api.php?action=afregninger | GET/POST/DELETE | Brugerens egne afregninger, pr. klub (GET kan filtreres med klub). POST {klub, dato, titel, noter} afregner alle egne åbne poster i klubben, også fra slettede hold (409 uden åbne poster); DELETE genåbner og frigiver kun egne poster |
api.php?action=omk_satser · omk_profil | GET/POST/PUT/DELETE | Statens satser pr. år (alle læser; kun admin ændrer; ingen hold_id) · udlægsprofilen (navn, adresse, bank), kun egen række |
api.php?action=beregn_km | GET | Afstand via Google Directions (fra, til, via); kræver admin eller trænerrettighed på mindst ét hold. (omk_kategori_config er fjernet i skema 18) |
api.php?action=beach_noter | GET/POST/PUT/DELETE | Trænerens egne Beach-noter, uden hold_id (scopet på beach_noter.ejer_id; en andens note giver 404). GET kan filtreres med kategori. POST kræver admin eller træner på mindst ét hold; note-id laves på serveren; filerne (pdf, docx, medie, højst 16 MB) gemmes i samme transaktion, først når noten er fundet og ejet |
api.php?action=beach_filer | GET | Hent en fil (note_id, filtype) på en note, brugeren ejer; ellers 404 |
api.php?action=traening_skabeloner · traening_skabelon | GET/POST/PUT/DELETE | Trænerens egne træningsskabeloner, uden hold_id (scopet på traening_skabeloner.ejer_id). GET: liste med antal punkter og samlet varighed; traening_skabelon?id=: én skabelon med punkter. POST {navn, tema?, hold_id, traening_id} gemmer en træning som skabelon (træner på holdet; højst 60 punkter). PUT omdøber/skifter tema. Navnet er unikt pr. ejer (409) |
api.php?action=traening_fra_skabelon | POST | Indsætter en skabelons punkter efter træningens punkter: {hold_id, traening_id, skabelon_id} (hold-gated, træner på holdet; alt eller intet). Punkterne får nye id'er og ingen øvelse eller fokuspunkt |
api.php?action=traening_noter | GET/POST/PUT/DELETE | Trænerens egne noter fra træningen på holdet (traening_noter, skema 19). Hold-gated, spiller blokeret, kræver træner på holdet; på et lukket hold kun GET. Alle kald scopes på ejer_id = den indloggede og hold_id; en andens eller ukendt note giver 404. GET: egne noter nyeste først med traening_navn og brugt_navn (null, hvis målet er slettet). POST {hold_id, type, tekst}: type er husk eller oevelse, teksten er trimmet og 1–500 tegn (400 ellers). Serveren laver id'et, sætter dato til i dag i dansk tid og traening_id til holdets første træning den dag (ellers null). PUT {id, tekst?, type?, status?}: status kan kun sættes til afkrydset eller aaben (genåbning nulstiller henvisningen); brugt sættes kun af serveren. DELETE {id} |
api.php?action=traening_note_brug | POST | Bruger en åben note én gang: {hold_id, id, til, traening_id?}. til: 'traening' lægger et punkt sidst på en af holdets træninger (404 for en fremmed træning; 10 minutter, intet emne, navn = notens første linje, hele teksten som beskrivelse), til: 'oevelse' lægger en øvelse i trænerens eget bibliotek (oevelser, uden hold_id), og til: 'note' (skema 20) føjer teksten til træningens note på en ny linje (ingen punkter; svaret har den samlede note, brugt_id = træningens id). Kører i én transaktion med noten låst (FOR UPDATE): er noten ikke åben, svarer den 409 «Noten er allerede brugt», så to samtidige kald giver én 200 og én 409. Noten bliver brugt med brugt_type/brugt_id |
api.php?action=modstandere | GET | Hent modstander-scouting-ark for holdet |
api.php?action=kampstatistik · kampstatistik_haendelse | GET/POST/PUT/DELETE | Live kampstatistik pr. kamp: start, indstillinger, nyt/luk link, halvlegsstart, og ret/slet af enkelte hændelser (træner/cheftræner/admin; spiller blokeret via $spillerBlokeret) |
api.php?action=stat_link · stat_link_overtag · stat_link_haendelser · stat_link_halvleg | GET/POST/DELETE | Tastesiden kampstatistik.html: uden JWT, men med link-nøglen i headeren X-Stat-Noegle (+ X-Stat-Enhed). Kun den ene kamps data — plus holdets spillerliste (numre, navne og positioner, til knappen "Alle"); forkerte nøgler tælles pr. IP. Spillerne er aliasser lavet med statAliasNoegle() (afledt af statistikkens id og JWT_SECRET, ikke af link-nøglen); ved gem godtages kun disse, og et rigtigt spiller-id eller et alias afledt af link-nøglen giver 400 for hele batchen |
api.php?action=fysiske_oevelser · upload_fysisk_oevelse_billede | GET/POST/PUT/DELETE | Det fælles øvelsesbibliotek (styrke/skade), uden hold_id (ikke i hold-gaten). GET viser kun ikke-skjulte øvelser (kan filtreres med kategori/maalgruppe). POST: træner/admin. PUT, DELETE og billedupload: kun opretteren (oprettet_af) eller admin, ellers 403. DELETE: bruges øvelsen i en programlinje, sættes skjult=1 (svar skjult: true, billedet bevares, programmernes linjer viser den stadig); ellers slettes øvelsen med billedet |
api.php?action=styrkeprogrammer | GET/POST/PUT/DELETE | Programbiblioteket (fælles for alle trænere), uden hold_id i gaten; kun træner/admin. GET: alle ikke-skjulte programmer med linjer, opretter_navn, kan_rette og de af brugerens hold, programmet er koblet på. POST {navn, beskrivelse?, hold_id?} opretter programmet (med hold_id kobles det også på holdet; kræver træner dér). PUT/DELETE: kun opretteren eller admin (403). DELETE: er programmet koblet på et hold, sættes skjult=1 (svar skjult: true; holdene beholder det); ellers slettes det med linjerne |
api.php?action=fysiske_programmer | GET | Holdets koblede styrke-/skadeprogrammer med linjer (hold-gatet, også for spiller). Viser også skjulte programmer; id er programmets id, link_id koblingens. Kun GET: programmer oprettes og rettes via styrkeprogrammer |
api.php?action=hold_styrkeprogrammer | POST/DELETE | Kobling mellem hold og program (hold-gatet, spiller blokeret). POST {hold_id, program_id}: cheftræner/træner på holdet (admin, hvis medlem) kobler ethvert ikke-skjult program (409 "Programmet er skjult" eller "Programmet er allerede på holdet"). DELETE {hold_id, id} (id = koblingens id) fjerner koblingen; et skjult program uden flere koblinger ryddes |
api.php?action=fysiske_program_oevelser | POST/PUT/DELETE | Et programs linjer (ikke i hold-gaten): kun programmets opretter eller admin (403). POST {program_id, oevelse_id, saet_reps_override?, sortering?} (oevelse_id er påkrævet; en skjult øvelse kan ikke tilføjes, 409). PUT {id, sortering?, saet_reps_override?}. DELETE {id}; en skjult øvelse, der ikke længere bruges, ryddes |
Databasen er relationel MySQL med hold som central entitet. Data er hold-isoleret via hold_id-fremmednøgler — undtagen det, der bevidst er fælles for alle klubber: link-biblioteket, styrke/skade-biblioteket (øvelserne fysiske_oevelser og programmerne fysiske_programmer med linjerne i fysiske_program_oevelser) og omkostningssatserne (omk_satser). Et styrkeprogram sættes på et hold med en kobling i hold_styrkeprogrammer (skema 16; CASCADE fra både hold og program); fysiske_programmer har ingen hold_id (fjernet i skema 18). Programmer og øvelser har skjult og skjules i stedet for at slettes, når de er i brug. Slettes et hold, forsvinder kun koblingerne; programmerne består. Udlægsprofilen (omk_profil: navn, adresse, bank) hører til brugeren og ikke holdet. Et hold er én sæson (skema 17): hold har aargange, saeson_navn, saeson_start, saeson_slut, status (aaben/lukket), forgaenger_id (forrige sæsons hold, uden fremmednøgle) og kampprogram_links (JSON). Holdet er sæsonen: tabellen saesoner og alle saeson_id-kolonner er fjernet (skema 18), og sæsonplanen hører direkte til holdet. En spillers login er hold_brugere.spiller_id (unik, FK til spillere med ON DELETE SET NULL); brugere har hverken hold_id eller spiller_id. De personlige biblioteker (skema 15) ejes af træneren (ejer_id): åbninger (aabninger, sat på hold med koblingerne i hold_aabninger, som har den enkelte deling med spillerne), Beach-noter og -filer (beach_noter, beach_filer) og træningsskabeloner (traening_skabeloner). Slettes et hold, forsvinder kun koblingerne; åbninger og Beach-noter består. Trænerens noter fra træningen (traening_noter, skema 19) ejes af træneren (ejer_id, FK CASCADE) og hører til et hold (hold_id, FK CASCADE); traening_id er ON DELETE SET NULL, og brugt_id har ingen fremmednøgle. Noterne slettes med holdet og med brugeren, tælles ikke med i DELETE holds tjek af indhold og kopieres ikke af hold_ny_saeson. Kørselsregnskabet (omkostninger og afregninger) ejes af brugeren (bruger_id) og grupperes efter klub; hold_id er kun oprindelsen og bliver NULL (ON DELETE SET NULL), når holdet slettes, så økonomien overlever (skema 14). Hvilke data der hører til platform, bruger og hold (holdet er sæsonen), står i docs/dataejerskab.md. Skemaet migreres af setupDB(), som kun kører, når databasens skema_version er ældre end SKEMA_VERSION i api.php. Tabellisten herunder hentes live fra databasen (samme kilde som "Database"-siden), så den altid matcher den faktiske struktur.
| Tabel | Rækker | Sidst opdateret |
|---|---|---|
Team Play (spiller.html) er implementeret som en PWA og kan installeres på iOS og Android uden en app store.
| Feature | Implementering |
|---|---|
| Installation | Web App Manifest – ikon, navn, display: standalone |
| Offline support | Service Worker cacher statiske assets |
| Ikoner | TA-logoet i appens farve (Icons/teamplay/ for Team Play) – apple-touch-icon 180px, 192/512px PNG (any) og 192/512px maskable; versioneret med ?v=2. Vektor-mastere i Icons/ta/<farve>/ |
| Push-notifikationer | Ikke implementeret (planlagt fremtidig feature) |
To tabeller: kampstatistik (én pr. kamp: halvlegslængde, indstillinger, halvlegsstart/slut, link-nøgle og udløb, aktiv telefon) og kampstatistik_haendelser (de enkelte tryk). Begge slettes sammen med kampen (ON DELETE CASCADE). Tabellerne oprettes i setupDB() i skema-version 3.
| Mekanisme | Implementering |
|---|---|
| Adgang via link | Linket er kampstatistik.html#<64 hex>. Nøglen sendes i headeren X-Stat-Noegle, og stat_link*-endpoints håndteres før den globale requireAuth(). Nøglen giver kun adgang til den ene kamp. Forkerte nøgler tælles pr. IP (maks. 10 i 15 min). Tastesiden får aldrig rigtige spiller-id'er, men et alias pr. statistik (statAlias(), afledt af statistikkens id og serverens hemmelighed), som serveren oversætter tilbage, når hændelser gemmes. Aliasset er det samme, når træneren laver et nyt link, så telefonens kø af tryk stadig kan sendes |
| Udløb | Linket udløber efter 48 timer eller til udgangen af dagen efter kampdagen (det seneste). Luk link fjerner udløbet, men beholder nøglen: det samme link holder op med at virke, og telefonen viser "Linket er lukket" (410) og beholder sin kø. Nyt link laver en ny nøgle: det gamle link bliver ukendt (404), og træneren skal dele det nye |
| Én aktiv telefon | aktiv_enhed (headeren X-Stat-Enhed): kun den aktive telefon må skrive; en anden får 409, indtil den kalder stat_link_overtag |
| Offline-kø | Hvert tryk får et id på telefonen og lægges i en kø i localStorage; køen sendes i ét kald, og serveren ignorerer id'er, den allerede har (INSERT IGNORE). Skærmen holdes tændt med Wake Lock |
| Beregning | kampstatistik.js er ren beregning uden DOM (beregn(), caMinut(), resultatTekst()) og testes med node --test "tests/*.test.js" |
| Trænervisning | kampstatistik-traener.js (KampstatTraener) tegner styring, delt link, kort udgave (kampnoter) og fuld udgave med rettelser (Coaching) |
| Resultat | Ved Slut skrives stillingen i kampe.resultat, men kun hvis feltet er tomt; ellers tilbyder Coaching "Brug som resultat" |
| Adgang (trænere) | requireTraener() på skrivning (cheftræner og træner på holdet); kampstatistik og kampstatistik_haendelse står i både $dataEndpoints og $spillerBlokeret |
En åbning tegnes som en række trin, som papirets små baner efter hinanden. Tegningen gemmes i aabninger.tegning (API-feltet billede på taktik) som JSON-tekst. En tekst, der ikke starter med {, er stadig et uploadet billede.
| Del | Implementering |
|---|---|
| Format | {"type":"trin","trin":[{"bane":{"maerkat":string|null,"elementer":[…],"farver"?:{…}},"note":string}]}. Banen er den samme som i øvelserne (Handball Playmaker Studios DrawElement). Pilene på trin N er bevægelsen fra trin N til N+1; et nyt trin kopierer kun spillere, bold og kegler (og banens farver) |
| Grupper og farver | En spillers team er gruppen og giver formen: home = Angreb ▲ (hold 1), away = Forsvar ● (hold 2), hold3 = Joker ■ (hold 3); den gamle hold4 godkendes og tegnes som ■ (Traeningsark.gruppeNoegle()). role følger med for bagudkompatibilitet. En bane (også øvelsernes) kan have farver med nøglerne hold1–hold3 (de gamle hold4, mv og kegle godkendes stadig), hver en palet-nøgle: blaa, roed, groen, gul, lilla, orange, hvid, sort. En manglende nøgle er standarden (Traeningsark.STANDARD_FARVER: blå, rød, gul). En kegle kan have sin egen data.farve fra KEGLE_PALET (hvid, gul, roed, blaa, orange); uden den bruges banens gamle farver.kegle, ellers hvid. Serveren (gyldigtSpiller(), gyldigeFarver(), gyldigtPunktElement()) afviser et ukendt team, en ukendt nøgle eller værdi, en ukendt keglefarve, en bold med farve og et tomt eller ugyldigt farver (et tomt objekt {}, en liste eller noget, der ikke er et objekt) med 400; gamle tegninger uden farver og tegninger fra før grupperne godkendes. Traeningsark.baneSvg() giver brikkerne klasserne bt-f-<palet>/bt-t-<palet>, som tegneflade.js farver med design.css-tokens; målvogteren er altid en grøn cirkel, og udskriften er sort/hvid (▲ sort, ● hvid, ■ grå). I en åbning har alle trin samme gruppefarver, og en kegle har samme farve på alle trin |
| Validering | aabningTrinTilJson() i api.php, kaldt af aabningFelter() på POST/PUT taktik: 1–12 trin, noten højst 300 tegn, mærkaten højst 20 tegn, hver bane via banerTilJson() (højst 300 elementer). Ukendte felter afvises, ikke fjernes, og serveren gemmer den rensede JSON (højst 2 MB). Ugyldig tegning giver 400 "Ugyldig tegning". Testes af docker/aabningstest.mjs |
| Logik | aabningstrin.js (Aabningstrin) er ren logik uden DOM: nyAabning(), nytTrin(), rens(), laes(), tilTekst() og animationen (billede(), overgang(), ramme(): stilling med pile, glid, pilene skiftes; brikker følges ad på id; svgBillede() holder et afspilningsbillede på højst 300 elementer). Testes med node --test "tests/*.test.js" |
| Editor | Tegneflade.aabn({aabning, titel, gem}) i tegneflade.js er trin-tilstanden: notefelt, trinrække, + Næste trin, Slet trin og Afspil. Hvert trin har sin egen fortryd; et farveskift (en gruppes eller en kegles) lægges på alle trin. Bruges af coaching.html. Værktøjslinjen har delte knapper til Angreb, Forsvar, Joker og Kegle (brikdelen vælger værktøjet, farvefirkanten åbner paletten); Målvogter har ingen farvefirkant. Til én bane er det Tegneflade.aabn({bane, titel, gem, farverStart}): en ny, tom bane starter med farverStart (den forrige banes farver ved + Bane) eller, uden valget eller med null, med standardfarverne |
| Visning | Aabningstrin.visning(beholder, aabning, {kompakt}) (bladr, swipe, afspil, hastighed, liggende/højkant) og miniature() (trin 1 som lille billede). Bruges af coaching.html (vindue), spiller.html (Team Play) og spiller-admin.html (knappen Se). Sidene indlæser traeningsark.js, aabningstrin.js og tegneflade.js; spiller.html cacher dem i sw.js. Hastighed og retning huskes i localStorage (ta_afspil_hastighed, ta_aabning_retning) |
Globalt modul uden hold_id – to tabeller: link_kategorier (hierarkisk via selvreferentiel parent_id, max to niveauer) og links (med klik-tæller, manuel sortering, oprettet_af og status = venter/godkendt; skema-version 12).
| Mekanisme | Implementering |
|---|---|
| Thumbnail-fallback | Manuelt uploadet billede → auto-screenshot via thum.io → favicon (onerror-kæde i frontend) |
| Klik-tæller | Fire-and-forget POST action=link_klik ved hvert klik på et link |
| Sortering | Manuel / mest populære / nyeste / alfabetisk – valgt via sort-parameter |
| Link-validering | Sekventielle HTTP HEAD-requests server-side (cURL; 3 s til forbindelsen, 5 s pr. forespørgsel, 10 s pr. link). Kun admin. Kun http/https, interne adresser blokeres (også via omdirigeringer), og forbindelsen bindes til den tjekkede adresse (CURLOPT_CONNECT_TO) |
| CSV-import | Ren client-side parsing/validering i links-admin.html (selvskrevet CSV-parser, ingen bibliotek) – dublet-tjek mod eksisterende links, preview før commit, sekventielle kald til POST action=link. Knappen vises kun for admin, så importerede links er godkendt med det samme. Håndhæves ikke af serveren, men et link fra en anden end admin bliver alligevel venter |
| Godkendelse | Eksisterende links er godkendt uden opretter (= admins). Nye links fra admin er godkendt; fra andre trænere venter de, til admin godkender via link_godkend. Afvisning sletter linket og dets billede. En ikke-admin, der ændrer et links URL, sender det tilbage til venter |
| Offentlig visning | GET links, GET link_kategorier og link_klik (bruges af links.html og links-embed.html på madsdam.dk) viser kun godkendte links og fjerner interne felter som oprettet_af |
| Adgang | requireTraener() til at oprette links (admin, cheftræner og træner). Opretteren kan rette og slette sine egne links, admin alle. Kategorier, sortering og valider_links kræver requireAdmin(). CSV-import findes kun for admin i links-admin (knappen skjules for andre), og serveren sætter alligevel andres links til venter |
| Område | Implementering |
|---|---|
| Kodeord | bcrypt-hashing i PHP (password_hash() / password_verify()) |
| SQL Injection | PDO prepared statements i alle database-queries |
| XSS | Alle brugerinput escaped med htmlspecialchars() og JS esc() |
| Auth-guard | Alle sider validerer JWT lokalt + backend validerer på hvert API-kald |
| Rolle-check | Admin-endpoints kræver rolle = 'admin' — rollen slås op i databasen ved hvert kald (både api.php og admin2.php), ikke læst fra JWT-payload |
| CORS | Begrænset til teamapp.dk-domænet |
Designmanual
TeamApp følger et dark-first designprincip med et markant accent-orange der skaber energi og sportsfølelse. Interfacet er funktionelt frem for dekorativt – data er i centrum.
Kerneprincipper: Klarhed · Hastighed · Fokus · Sportsenergisk
Kilde til sandhed: alle farver og fonte er samlet i design.css (roden). Siderne bruger var(--token) frem for hardcodede værdier. Mørkt er standard (:root); lyst aktiveres via klassen .light på <html> og huskes i localStorage['kn_theme']. Fuld reference: docs/design-system.md.
Bevidste undtagelser: forsiden (index.html, "Klubhuset") og login.html linker ikke design.css — de har lime accent og IBM Plex Sans via lokale --fh-*-variabler (mørk + html.light); login har desuden baggrundsfoto (Icons/login-baggrund*.jpg). Se docs/design-system.md §8.
Bløde flader (badges, hover, alerts): --accent-soft, --green-dim, --red-soft. Domæne-tokens (kampnoter/coaching): --taktik, --spiller, --timeout m.fl. — aliaser oven på grundfarverne.
Kategorifarver (palet B): --cat-coaching (rød), --cat-teamplay (blå), --cat-ressourcer (teal) og --cat-beach (gul) — samme farve for et område på forsiden, i topmenuen, i sidebarerne (--sb-accent) og i apperne; hver har en -soft-variant. --cat-oekonomi er ikke et forsiderum og bruges kun af admin.
Kontrast og tekststørrelse: al tekst skal have mindst 4,5:1 mod sin flade i begge temaer, og mindste tekststørrelse er 12px. Tekst på en fyldt farve (knapper, badges, toasts) bruger altid var(--paa-farve) — mørk i mørkt tema, hvid i lyst — aldrig #fff/#000. Tjek en side med docs/kontrast-tjek.js (kontrastTjek(), smaaTekst(), klippet(), knapper()).
Farverne herunder læses live fra design.css via CSS-variablerne — ændres et token dér, opdateres denne side automatisk.
| Font | Rolle | Vægte | Eksempel |
|---|---|---|---|
| Outfit | Overskrifter, logoer, tal | 400–800 | TeamApp |
| Space Grotesk | Brødtekst, navigation, labels | 400–700 | Coaching platform |
| DM Mono | Kode, tal, tekniske data | 400, 500 | api.php · JWT |
| Størrelse | Brug |
|---|---|
| 26–36px · Outfit 800 | Sideoverskrifter og store tal (stat-num) |
| 18–20px · Outfit 700 | Kortoverskrifter og modaltitler |
| 15–16px · Space Grotesk 600–700 | Primær tekst, kortnavne |
| 13px · Space Grotesk 400–500 | Brødtekst, tabelindhold |
| 12px · Space Grotesk 600 | Labels, badges, uppercase metadata (mindste tekststørrelse er 12px) |
| 12px · DM Mono | Tekniske værdier, diskforbrug, datoer |
Knapper
Komponentlaget i design.css: alle knapper har samme form — kun farven fortæller betydningen (orange = hovedhandling, grøn = gem, rød = fare). Formen kommer fra --knap-padding/--knap-font (40px høj; -lille-varianterne 32px) og --radius; på touchskærme (@media (pointer: coarse)) er normale knapper 44px. Eksisterende sider beholder deres klassenavne (.knap-primaer m.fl.) men henter formen fra tokens; nye sider bruger klasserne direkte:
Modaler: .modal-baggrund > .modal-boks med .modal-titel, .modal-tekst og .modal-knapper — overlay --modal-baggrund, boks med --radius-stor, --modal-padding og --modal-skygge. Forbilledet er den fælles appConfirm-dialog i tilbagemelding.js. Fuld reference: docs/design-system.md §5–5b.
Badges og tags
Alerts
| Element | Specifikation |
|---|---|
| Topmenu | 56px, sticky øverst på computersiderne (topmenu.js) — global navigation; sætter html.har-topmenu og --tm-hoejde, så fuldhøjde-sider bruger calc(100vh - var(--tm-hoejde, 0px)). Er det aktive hold lukket (kn_hold_lukket_id, sat ved hvert holdvalg (topmenuen eller saetAktivtHold)), tilføjes en 28px bjælke «Lukket hold — kun læsning» under menuen (html.har-lukket-hold), og --tm-hoejde bliver 84px. Holdvælgeren grupperer lukkede hold under «Tidligere hold» |
| Sidebar bredde | 240px fast på computer · 260px som skuffe under 700px (design.css) — sidebaren er lokal navigation i modulet |
| Computersider | Samme opbygning overalt: topmenu → sidebar (modulets sider, undertitel = områdets navn) → titelbjælke (.titelbjaelke med todelt titel) → indhold, ingen footer. .knap-primaer i områdets farve (--sb-accent), .knap-sekundaer neutral — se docs/design-system.md §7e |
| Main padding | 36px 32px (desktop) · 20px 16px (mobil <700px) |
| Radius | Skala i design.css: --radius-lille 6px (mærker, små felter) · --radius 10px (knapper, felter, kort) · --radius-stor 16px (modaler, store kort) · --radius-pille 999px (piller/chips) |
| Kortpadding | 18–28px afhængig af kontekst |
| Gitter | CSS Grid med auto-fill og minmax() |
| Responsiv breakpoint | 700px – sidebar skjules, layout skifter til single-column |
| Transitions | background/border/color: .2s ease · transform: .15s |
| Baggrundsgradient | Subtil radial gradient øverst og nederst – accent og blue toner |
Ikonerne er linje-baserede SVG'er tegnet på et 24×24-grid med ensartet streg. De arver farve fra teksten (stroke: currentColor) og skalerer frit. De defineres som <symbol>-sprites og bruges via <use href="#icon-…">; stregattributterne sættes af container-klassen (fx .nav-icon i design.css), ikke på selve symbolet.
| Egenskab | Værdi |
|---|---|
| Grid | 24 × 24 |
| Stregtykkelse | 1.8 |
| Linecap / linejoin | round |
| Farve | currentColor – arver fra tekst/kontekst |
| Fyld | ingen (fill: none) |
| Brug | <svg><use href="#icon-navn"></svg> |
Admin-modulets ikonsæt (eksempel på husstilen)
homelogoutusertheme-suntheme-moonoverblikbrugeredatabaseskematestprojektvejledningtekniskdesignclaudearkitekturmodulerroentgenIkonerne defineres i dag pr. side som inline-sprites (ca. 122 distinkte på tværs af appen; genbrugte ikoner deler bevidst identisk path-data for pixel-perfekt konsistens). En genereret reference med style-spec ligger under docs/icon-sets/. En samlet, central ikon-fil er et muligt næste skridt.
App-ikoner: alle apps bruger samme TA-logo med håndbold på mørk baggrund; kun skråstregen og bolden har appens farve — TeamApp lime, Team Play og Teamgenerator blå, Link-bibliotek teal, Beach gul, Interval Timer grøn, Sekvens Timer lilla, Mini Turnering accent-orange, Udgifter gul, Noter rød. Vektor-mastere ligger i Icons/ta/<farve>/ (icon.svg + icon-maskable.svg); manifesterne har både any- og maskable-ikoner, og alle ikon-referencer er versioneret med ?v=N. Fuld tabel: docs/design-system.md §"App-ikoner".
Claude-værktøjer
Projektet har en .claude/-mappe, der udvider Claude Code (og Cowork) med projekt-specifikke værktøjer til udvikling af TeamApp. Der er tre slags — de bruges på hver sin måde:
| Type | Hvad det er | Hvordan det startes |
|---|---|---|
| Skill | Viden / fremgangsmåde Claude trækker ind | Automatisk, når opgaven matcher |
| Subagent | En separat Claude, der løser en delopgave | Automatisk eller ved at nævne navnet |
| Kommando | Genvej til en arbejdsgang | Du skriver /navn |
En skill er en mappe med en SKILL.md, der giver Claude specialiseret viden eller en arbejdsgang. Claude læser den automatisk ind, når din besked matcher skillens triggere — du aktiverer den ikke selv.
Sådan bruger du dem: beskriv bare opgaven. Fx udløser "fejlsøg et PHP-endpoint" web-udvikler, og "prioritér backloggen" udløser projektleder.
En subagent er en selvstændig Claude, der kører en afgrænset opgave i sit eget vindue og kun sender resultatet tilbage. Det sparer kontekst og giver den en snæver rolle. Projektets agenter ligger i .claude/agents/:
| Agent | Formål |
|---|---|
frontend-review | Gennemgår og optimerer frontend-design på en side |
backend-review | Reviewer api.php / admin2.php — sikkerhed først |
pwa-deploy | Tjekker service worker, manifest, cache og deploy |
web-tester | Statisk sundhedstjek (brudte links, syntaks, PWA) |
e2e-tester | Skriver og vedligeholder tværgående testcases på tværs af moduler (docs/testcases.md) |
ide-generator | Brainstormer nye features + håndboldindhold |
design-system | Vedligeholder det fælles designsystem (tokens) |
docs-sync | Synkroniserer dokumentationen i admin.html/CLAUDE.md med koden |
Sådan bruger du dem: beskriv opgaven ("reviewer kampnoter.html"), eller nævn agenten direkte ("brug web-tester på hele repoet"). Vil du kun have forslag, så sig "foreslå kun, lav ikke ændringer".
En slash-kommando er en genvej, du selv starter ved at skrive /navn. Den kører en forudskrevet arbejdsgang — ofte ved at kalde en eller flere agenter. De ligger i .claude/commands/:
| Kommando | Gør |
|---|---|
/pre-deploy | Fuldt review af ændringer + GO/NO-GO før deploy |
/test | Teknisk sundhedstjek af repoet (kalder web-tester) |
/design | Vedligehold designsystemet (audit / migrér en side til tokens) |
/ny-side | Stil en ny rod-HTML-side op efter projektets mønster |
/ny-pwa | Stil en ny undermappe-PWA op (manifest, sw, ikoner, design.css) |
/docs-sync | Sammenlign og synkroniser dokumentationen med koden (kalder docs-sync) |
Sådan bruger du dem: skriv kommandoen, evt. med et argument — fx /test kampnoter.html eller /ny-side statistik.html. Uden argument kører de deres standard.
.claude/agents/ | Subagenter (én .md pr. agent) |
.claude/commands/ | Slash-kommandoer |
.claude/skills/ | Projekt-skills (SKILL.md) |
Sådan hænger det sammen: en skill er passiv viden, en subagent er en aktiv proces (kan bruge en skill undervejs), og en kommando er knappen du selv trykker på for at sætte det i gang. Alle tre er værktøjer til at udvikle TeamApp — ikke funktioner i selve appen for trænere og spillere.
Projektoverblik
En samling statiske HTML-sider (vanilla JS, ingen build-step) med én fælles PHP-backend mod MySQL. Flere sider er installerbare PWA'er.
Dokumentation
Kampnoter — Brugervejledning v3
Kampnoter — Teknisk spec v3
TeamApp Designmanual
docs/design-system.md
docs/linkbibliotek-spec.md
Infrastruktur
GitHub Actions → FTP deploy
.htaccess (Apache/UnoEuro)
MySQL 8.0 · UnoEuro hosting
JWT (HS256) auth
design.css designsystem
Claude skills · 6
Fra .claude/skills/. .agents/skills/ (Codex) har 11 — de samme 6 plus agent-browser, design-taste, find-skills, frontend-design og remotion.
Arkitektur
Sådan hænger TeamApp sammen — fra bruger til database. Statiske sider taler med én PHP-backend, beskyttet af JWT, og deployes via FTP. Diagrammet følger appens tema.
hentHoldListe() · aktivt hold og kn_hold_lukket_id) · nav.js (tema · log ud) · sikker.js (esc · sikre URL'er) · tilbagemelding.js (beskeder · bekræftelse · "Gemt ✓") · skift-kodeord.js (skift eget kodeord fra brugermenuen) · ruter.js (tilbage-knap via #visning) · computer-hint.js (computerside på telefon) · install-hint.js (læg på hjemmeskærmen)Ingen staging — main er produktion. config.local.php ligger kun på serveren og overskrives ikke ved deploy. HTML serveres med no-cache, så nye versioner hentes straks.
Moduloversigt
TeamApp består af 17 HTML-moduler der tilsammen dækker hele løsningen – fra spillerens mobil-app til administratorens systempanel. Alle moduler bruger samme API-backend, og de fleste deler designsystemet i design.css. Forsiden og login har bevidst deres eget "Klubhuset"-look, og API-testsiden har egne styles.
tegneflade.js · Kræver login| Fra modul | → Til modul | Datatype |
|---|---|---|
| spiller-admin.html | spiller.html | Åbninger, kampanalyser og udviklingsopslag deles med spillerne |
| tegneflade.js | oevelsesark.html / traening.html | Baner tegnes på arket og i bibliotek-fanen og gemmes på øvelsen |
| coaching.html (tegneflade.js, aabningstrin.js) | spiller.html / spiller-admin.html | Åbningens trin tegnes i Taktikbibliotek og vises som trinvisning i Team Play og i Team Play Coach (Se) |
| traening.html | coaching.html | Træningssessioner ses i coaching-overblikket |
| Noter/ | traening.html (Noter, #noter) | Noter fra hallen bliver punkter på en træning eller øvelser i trænerens bibliotek |
| kampstatistik.html | coaching.html / kampnoter.html | Tastede hændelser vises som statistik i kampvisningen (fuld) og i kampnoters Statistik-fane (kort) |
| admin.html | Alle moduler | Brugere og hold administreres centralt – påvirker adgang overalt |
| login.html | coaching.html / spiller.html | JWT-token bruges på tværs af alle moduler til auth |
| Modul | API | PWA | Canvas | Tema |
|---|---|---|---|---|
| admin.html | admin2.php | — | — | Mørkt + Lyst |
| coaching.html | api.php | — | ✓ | Mørkt + Lyst |
| spiller-admin.html | api.php | — | — | Mørkt + Lyst |
| spiller.html | api.php | ✓ | — | Mørkt + Lyst |
| tegneflade.js | — (gemmer via siden, der bruger den) | — | — | Mørkt + Lyst |
| traening.html | api.php | — | — | Mørkt + Lyst |
| kampnoter.html | api.php | — | — | Mørkt + Lyst |
| kampstatistik.html | api.php (stat_link*) | — | — | Mørkt + Lyst |
| oevelsesark.html | api.php | — | — | Mørkt + Lyst |
| modstandere.html | api.php | — | — | Mørkt + Lyst |
| styrke-skade.html | api.php | — | — | Mørkt + Lyst |
| links.html | api.php | ✓ (manifest-links.json) | — | Mørkt + Lyst |
| links-admin.html | api.php | — | — | Mørkt + Lyst |
| test.html | admin2.php + api.php | — | — | Mørkt |
| login.html | api.php (login) | — | — | Mørkt + Lyst (eget "Klubhuset"-look, ikke design.css) |
| links-embed.html | api.php | — | — | Fast lyst |
| apps.html | — | — | — | Mørkt + Lyst |