—
Hold
—
Brugere
—
Kampe
—
Spillere
—
Træninger
—
Noter
—
Åbninger
—
Øvelser
Brugere
Database
Hold
Indlæser…
Coaching Team Play Coach Træning Kør API tests
Alle brugere
Brugernavn Visningsnavn E-mail Rolle Hold Oprettet
Tabeller · diskforbrug
Tabel Rækker Størrelse Sidst opdateret
Vælg et hold i listen for at se detaljer.
🏆 Hvad er TeamApp?

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.

👥 Roller i systemet

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).

RolleAdgangTypisk 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
🛡️ Rettighedsmatrix — hvem må hvad

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ådeAdminCheftrænerTrænerSpiller
Admin-panel (admin.html) — inkl. at rette en brugers rolle på et holdFuld adgang———
Coaching, kampnoter, spiller-indhold, træning, øvelsesark, tegning, sæsonplan, modstander-scoutingFuld adgangFuld adgangFuld adgangIngen adgang
Kampstatistik — start, del/luk/nyt link og se tallene (Coaching og kampnoter); ret hændelser, halvlegsstart og resultat (kun Coaching)Fuld adgangFuld adgangFuld adgangIngen 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 holdHold, admin er medlem afKun hold, hvor man er cheftræner eller trænerIngen adgang
Taktikbibliotek (coaching.html) — egne åbninger: opret, ret, slet (slet skjuler en åbning, der bruges), videolinksEgne; kan også rette og slette andresKun egneKun egneIngen adgang
Åbninger på et hold — koble egne åbninger på, fjerne en kobling, ændre deling med spillerne (enhver kobling på holdet)Fuld adgangFuld adgangFuld adgangIngen 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 adgangFuld adgangFuld adgangKun delte åbninger
Beach håndbold — egne noter og filer (andres er usynlige)Kun egneKun egneKun egneIngen 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 egneKun egneKun egneIngen 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 egneOpret; ret og slet kun egneIngen adgang
Styrke & skadeforebyggelse — sætte et program på holdet og fjerne det igen (ethvert ikke-skjult program, også andres)Fuld adgangFuld adgangFuld adgangIngen adgang
Styrke & skadeforebyggelse — se holdets programmerFuld adgangFuld adgangFuld adgangKun læsning (programmer sat på holdet)
Hold — oprette hold (opretteren bliver cheftræner)Fuld adgangJaJaJa
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 kodeordFuld adgangFuld 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 afFuld adgangKun 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 afFuld adgang——
Lukket hold (arkiv) — se og læse holdets data. Holdet forsvinder fra holdvælgeren og login-svaret for alle andre end cheftrænerenHold, admin er medlem afKun læsningIngen 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 adgangIngen adgang
Lukket hold (arkiv) — registrere kørsel og gemme en træning som skabelon (egne data)Egne dataKørsel og skabelonerKun 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 egneOpret og ret egneKun læsning (den offentlige side)
Link-bibliotek — godkend/afvis links, kategorier, sortering, CSV-import og linktjekFuld 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ænerKun egneKun egneKun 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 adgangKun læsningKun læsning—
Kommentarer og reaktioner (like)Alle + slet andresAlle + slet andresAlle + slet andresKun egne kan slettes
Team Play (spiller.html)AdgangAdgangAdgangMålgruppen
🔐 Login og adgang

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.

  1. Gå til login.html og indtast brugernavn + kodeord
  2. Systemet validerer og udsteder en JWT-token
  3. Cheftrænere, trænere og admins videresendes til forsiden index.html (eller tilbage til den side, der sendte dem til login)
  4. Spillere videresendes til spiller.html (PWA)
  5. 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
  6. Efter 5 forkerte forsøg på samme brugernavn låses login i 15 minutter
  7. 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
  8. 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
  9. 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
  10. Skift eget kodeord: brugermenuen øverst til højre i topmenuen (computersiderne) eller profil-knappen med dine initialer i Team Play
🏠 Forsiden (index.html)

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.

  1. 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
  2. Fire områder: Team Management, Team Play, Trænerressourcer og Beach Håndbold
  3. 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)
  4. 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
  5. 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")
🧭 Navigation — topmenuen

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.

  1. Klik på TeamApp-logoet for at komme til forsiden
  2. Å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
  3. 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
  4. Skift mellem mørkt og lyst tema med temaknappen
  5. Brugermenuen (dit navn) giver adgang til Admin (kun admin), Links-admin, Installér apps og Log ud
  6. På små skærme samles rummene bag en menuknap, og "Sektioner ▸" åbner sidens egen sidebjælke
↩️ Tilbage-knap og beskeder

Navigation og beskeder fungerer ens på tværs af siderne.

  1. "‹ 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
  2. I Team Play, Coaching, træning og modstandere står den aktuelle visning i adressen (fx #kampe eller #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
  3. Øvelsesarket åbnes for én bestemt træning, og "← Træning" fører tilbage til samme træning
  4. Har du ugemte ændringer i et modstander-ark, spørger siden, før du går videre — "Bliv" beholder ændringerne
  5. 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"
  6. Er dit login udløbet, sendes du til login og bagefter tilbage til samme side og visning
📱 Coaching (coaching.html)

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.

  1. Log ind som træner og åbn coaching.html
  2. Vælg en kamp i overblikket eller under Kampoverblik — eller opret en med + Ny kamp
  3. Tilføj taktiske noter og spillerobservationer, og vælg åbninger til kampforberedelsen fra holdets åbninger (se afsnittet "Taktikbibliotek")
  4. Del indhold med spillere via Team Play Coach (spiller-admin.html)
  5. Se spillernes reaktioner og kommentarer på delt indhold
  6. 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)
🆕 Start ny sæson (coaching.html)

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.

  1. 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
  2. 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
  3. 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
  4. Luk det gamle hold: er valget slået til, lukkes det gamle hold, når det nye er oprettet
  5. 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
🗄️ Luk holdet (coaching.html)

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.

  1. 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
  2. Cheftræneren finder holdet under «Tidligere hold» i holdvælgeren. Et banner under topmenuen minder om, at holdet er lukket
  3. 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
  4. 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
  5. 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
📚 Taktikbibliotek (coaching.html)

Å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).

  1. Å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
  2. 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
  3. + 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
  4. 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
  5. 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
  6. ✕ 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
  7. Å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
  8. ✕ 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
  9. 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
  10. Slettes en træners konto, bliver hans åbninger stående på holdene, men kun admin kan rette dem
📊 Live kampstatistik (kampstatistik.html)

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.

  1. 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
  2. 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
  3. Prøv først: kampstatistik.html#demo viser tastesiden uden net og uden at gemme noget
  4. 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
  5. 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)
  6. 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
  7. 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 (spiller.html)

Team Play er spillernes mobilvenlige app, implementeret som en PWA, og kan installeres på spillerens telefon direkte fra browseren uden app store.

  1. Spilleren åbner spiller.html i mobilbrowseren og installerer appen
  2. Forside: Overblik over næste kamp og seneste opslag fra træneren
  3. Å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
  4. Kampe: Kampforberedelse og evaluering med noter og video
  5. Udvikling: Personlig udviklingsindhold opdelt i kategorier
  6. 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
  7. Spilleren kan like og kommentere på alt delt indhold
  8. Log ud sidder i topbaren; refleksionsloggen bliver liggende på telefonen til næste login
✏️ Tegneflade (planlægningsark og øvelsesbibliotek)

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.

  1. Tryk + Bane på en øvelse – på planlægningsarket eller i en egen øvelse i Øvelsesbibliotek – så åbner tegnefladen
  2. Vælg en opstilling, eller placer spillere, målvogter, bold og kegler på banen
  3. 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
  4. 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
  5. 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
  6. 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å
  7. Tegn løb, afleveringer, skud, skærme og dribling som streger mellem spillerne, og skriv tekst på banen
  8. Giv banen en mærkat, og brug fortryd/gentag, hvis noget skal rettes
  9. Tryk Gem – banen følger øvelsen med ind i træningerne (en øvelse kan have op til 8 baner)
🔍 Modstander-scouting (modstandere.html)

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.

  1. Søg i listen efter modstanderholdet, eller opret et nyt ark med holdets navn
  2. Vælg typisk forsvarsformation (6:0, 5:1, 5:1 skævt, 4:2, 3:3 eller 3:2:1) og noter angrebsstil
  3. Beskriv farlige spillere og målvogter-tendenser
  4. Tilføj generelle noter til resten af holdet
  5. Arket kan tilgås igen fra Coaching, når holdet igen møder modstanderen
🏋️ Træningsplanlægning (traening.html)

Træningsmodulet bruges til at planlægge og dokumentere træningssessioner med et struktureret øvelsesbibliotek.

  1. Opret en ny træningssession med dato og titel
  2. Tilføj øvelser fra biblioteket eller opret nye øvelser
  3. Angiv starttid og varighed (minutter) for hver øvelse i øvelsesarket
  4. 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) →».

📋 Træningsskabeloner

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.

  1. Å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
  2. Når du opretter en ny træning, kan du vælge en skabelon i feltet Start fra skabelon (kun hvis du har nogen)
  3. På en eksisterende træning tilføjer Fra skabelon skabelonens punkter efter de punkter, der allerede er. Det sker alt eller intet
  4. 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 fra træningen (Noter/)

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.

  1. Å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
  2. 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
  3. 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
  4. 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
  5. Å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)
  6. 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
  7. 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)»
  8. Noterne følger holdet: slettes holdet, slettes noterne. De kopieres ikke med, når du starter en ny sæson
🏖️ Beach (Beach/)

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.

  1. Åbn Beach og log ind som træner
  2. 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)
  3. Ret eller slet noten fra listen; en slettet note tager sine filer med
💪 Styrke & skadeforebyggelse (styrke-skade.html)

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.

  1. 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
  2. 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
  3. 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
  4. 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
  5. Spillere ser kun de programmer, der er sat på deres hold, og kan kun læse dem
🔗 Link-bibliotek (links.html / links-admin.html)

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.

  1. Log ind som træner/admin og åbn links-admin.html
  2. Admin opretter hovedkategorier (evt. med billede) og underkategorier under dem
  3. 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
  4. 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, url og beskrivelse (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
  5. Admin bruger "Tjek alle links" periodisk for at finde og rette døde links
  6. Se klik-statistik pr. link for at identificere de mest brugte ressourcer
🧾 Udgifter og afregning (Omkostningsgodtgørelse)

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.

  1. 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
  2. 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 %
  3. 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
  4. 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"
  5. 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
  6. 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-panel (admin.html)

Admin-panelet bruges til systemadministration og er kun tilgængeligt for brugere med admin-rollen.

  1. Overblik: Statistik over alle hold, brugere og data på tværs af systemet
  2. 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"
  3. Database: Se diskforbrug og tabelstørrelser for alle tabeller
  4. Skema: Visualiser databaseskemaet — tabeller grupperet i domæner med kolonner og relationer
  5. 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
  6. 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")
🏗️ Systemarkitektur

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.

LagTeknologiDetaljer
FrontendHTML5 · CSS3 · Vanilla JSIngen frameworks. Statiske filer på CDN/webhotel
BackendPHP 8+api.php og admin2.php – REST-endpoints
DatabaseMySQL / MariaDBRelationel database med JOINs og indeksering
AuthJWT (JSON Web Tokens)HS256-signerede tokens, gemt i localStorage
Hostingteamapp.dkWebhotel med PHP og MySQL support
PWAService Worker + Web App ManifestTeam Play kan installeres og køre offline
TypografiGoogle FontsOutfit · Space Grotesk · DM Mono (design.css) · IBM Plex Sans (forside og login)
🔑 Autentificering og autorisation

Systemet anvender JWT-baseret autentificering. Login-flowet er:

  1. Brugeren sender brugernavn + kodeord via POST til api.php?action=login
  2. Backend validerer credentials mod databasen (bcrypt-hashet kodeord). Maks. 5 forkerte forsøg pr. brugernavn i 15 min (LOGIN_MAX_FORSOEG/LOGIN_VINDUE, tabellen login_forsoeg) — derefter 429
  3. Returnerer JWT-token med payload: { sub, navn, rolle, tv, iat, exp } (ingen hold_id: holdet følger med i hvert kald; gamle tokens med hold_id virker, feltet ignoreres)
  4. Frontend gemmer token i localStorage.kn_token
  5. Alle efterfølgende API-kald sender token i Authorization: Bearer <token>
  6. Backend validerer signaturen og udløbsdatoen på hvert kald og slår rolle, aktiv og token_version op i brugere — 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.

🔌 API-endpoints

Der er to API-filer: api.php til generel brug og admin2.php til admin-funktioner (kræver admin-rolle i JWT).

EndpointMetodeBeskrivelse
api.php?action=loginPOSTLogin – returnerer JWT token
api.php?action=skift_kodeordPOSTSkift eget kodeord – logger andre enheder ud og returnerer nyt token (alle roller; begrænset antal forkerte forsøg)
api.php?action=glemt_kodeordPOSTGlemt 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_tjekPOSTTjekker 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_kodeordPOSTNyt 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_kodeordPOSTNyt 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=holdGETBrugerens 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_saesonPOSTGuiden «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_brugereGET/DELETEHoldets 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_inviterPOSTTilføj en eksisterende bruger til holdet via brugernavn med rolle traener eller spiller (cheftræner/admin)
api.php?action=kampeGETHent kampe for holdet
api.php?action=saesonplan · saesonplan_periode · saesonplan_fokuspunkt · saesonplan_periode_for_datoGET/POST/PUT/DELETESæ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_kopierGET · 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=taktikGET/POST/PUT/DELETETræ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=aabningerGETHoldets å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_aabningerPOST/PUT/DELETEKoblinger 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_aabningerGET/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_videoerGET/POST/DELETEVideolinks 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=spillereGETHent 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=traeningerGETHent træningssessioner
api.php?action=opret_spillerPOSTOpret 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_brugereGETHoldets spiller-logins (kræver træner/cheftræner/admin)
api.php?action=spiller_forside · spiller_aabninger · spiller_kampeGETTeam 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=kamprumGET/POST/PUT/DELETEKampforberedelse/-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_kategorierGET/POST/PUT/DELETEUdviklingsopslag 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=kommentarerGET/POST/DELETEKommentarer 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_statusGET/POSTLike (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=overblikGETAlle hold med statistik (admin)
admin2.php?action=brugereGETAlle brugere (admin)
admin2.php?action=opret_brugerPOSTOpret 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_brugerPUTRet visningsnavn og e-mail (admin)
api.php?action=min_emailGET/PUTEgen e-mail (admin og trænerkonti)
admin2.php?action=reset_kodeordPOSTNulstil kodeord (admin)
admin2.php?action=slet_brugerDELETESlet bruger (admin). Sletningerne kører i én transaktion
admin2.php?action=hold_spillereGETSpillere-roster for ét hold (admin); har_bruger ud fra hold_brugere.spiller_id
admin2.php?action=hold_detaljeGETFuldt 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_holdrollePOSTTilfø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_storrelseGETDatabase-statistik (admin)
api.php?action=link_kategorierGETAlle link-kategorier, hierarkisk (offentligt, ingen auth)
api.php?action=linksGETGodkendte links, evt. filtreret på kategori/sortering (offentligt, uden interne felter)
api.php?action=linkPOSTOpret 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_listeGETLinkliste til links-admin: admin ser alle inkl. ventende og opretterens navn, andre ser godkendte plus egne ventende (træner/admin)
api.php?action=link_godkendPOSTGodkend eller afvis et ventende link (handling: godkend/afvis; afvis sletter linket) (admin)
api.php?action=valider_linksGETHTTP 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=omkostningerGET/POST/PUT/DELETEBrugerens 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_klubberGETBrugerens 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=afregningerGET/POST/DELETEBrugerens 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_profilGET/POST/PUT/DELETEStatens 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_kmGETAfstand 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_noterGET/POST/PUT/DELETETræ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_filerGETHent en fil (note_id, filtype) på en note, brugeren ejer; ellers 404
api.php?action=traening_skabeloner · traening_skabelonGET/POST/PUT/DELETETræ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_skabelonPOSTIndsæ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_noterGET/POST/PUT/DELETETræ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_brugPOSTBruger 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=modstandereGETHent modstander-scouting-ark for holdet
api.php?action=kampstatistik · kampstatistik_haendelseGET/POST/PUT/DELETELive 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_halvlegGET/POST/DELETETastesiden 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_billedeGET/POST/PUT/DELETEDet 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=styrkeprogrammerGET/POST/PUT/DELETEProgrambiblioteket (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_programmerGETHoldets 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_styrkeprogrammerPOST/DELETEKobling 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_oevelserPOST/PUT/DELETEEt 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
🗄️ Databasestruktur

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.

TabelRækkerSidst opdateret
📱 PWA (Progressive Web App)

Team Play (spiller.html) er implementeret som en PWA og kan installeres på iOS og Android uden en app store.

FeatureImplementering
InstallationWeb App Manifest – ikon, navn, display: standalone
Offline supportService Worker cacher statiske assets
IkonerTA-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-notifikationerIkke implementeret (planlagt fremtidig feature)
📊 Live kampstatistik

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.

MekanismeImplementering
Adgang via linkLinket 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øbLinket 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 telefonaktiv_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
Beregningkampstatistik.js er ren beregning uden DOM (beregn(), caMinut(), resultatTekst()) og testes med node --test "tests/*.test.js"
Trænervisningkampstatistik-traener.js (KampstatTraener) tegner styring, delt link, kort udgave (kampnoter) og fuld udgave med rettelser (Coaching)
ResultatVed 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
🎞️ Åbningens trin (aabningstrin.js)

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.

DelImplementering
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 farverEn 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
ValideringaabningTrinTilJson() 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
Logikaabningstrin.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"
EditorTegneflade.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
VisningAabningstrin.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)
🔗 Link-bibliotek

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).

MekanismeImplementering
Thumbnail-fallbackManuelt uploadet billede → auto-screenshot via thum.io → favicon (onerror-kæde i frontend)
Klik-tællerFire-and-forget POST action=link_klik ved hvert klik på et link
SorteringManuel / mest populære / nyeste / alfabetisk – valgt via sort-parameter
Link-valideringSekventielle 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-importRen 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
GodkendelseEksisterende 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 visningGET 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
AdgangrequireTraener() 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
🛡️ Sikkerhed
OmrådeImplementering
Kodeordbcrypt-hashing i PHP (password_hash() / password_verify())
SQL InjectionPDO prepared statements i alle database-queries
XSSAlle brugerinput escaped med htmlspecialchars() og JS esc()
Auth-guardAlle sider validerer JWT lokalt + backend validerer på hvert API-kald
Rolle-checkAdmin-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
CORSBegrænset til teamapp.dk-domænet
🎯 Brand og designprincipper

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()).

🌙 Farvepalette – Mørkt tema (standard)

Farverne herunder læses live fra design.css via CSS-variablerne — ændres et token dér, opdateres denne side automatisk.

☀️ Farvepalette – Lyst tema
✍️ Typografi
FontRolleVægteEksempel
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ørrelseBrug
26–36px · Outfit 800Sideoverskrifter og store tal (stat-num)
18–20px · Outfit 700Kortoverskrifter og modaltitler
15–16px · Space Grotesk 600–700Primær tekst, kortnavne
13px · Space Grotesk 400–500Brødtekst, tabelindhold
12px · Space Grotesk 600Labels, badges, uppercase metadata (mindste tekststørrelse er 12px)
12px · DM MonoTekniske værdier, diskforbrug, datoer
🧱 UI-komponenter

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

Admin Cheftræner Træner Spiller Taktik Video Udvikling

Alerts

✓ Handling gennemført korrekt
✕ Der opstod en fejl – prøv igen
ℹ Informationsbesked til brugeren
📐 Layout og spacing
ElementSpecifikation
Topmenu56px, 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 bredde240px fast på computer · 260px som skuffe under 700px (design.css) — sidebaren er lokal navigation i modulet
ComputersiderSamme 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 padding36px 32px (desktop) · 20px 16px (mobil <700px)
RadiusSkala 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)
Kortpadding18–28px afhængig af kontekst
GitterCSS Grid med auto-fill og minmax()
Responsiv breakpoint700px – sidebar skjules, layout skifter til single-column
Transitionsbackground/border/color: .2s ease · transform: .15s
BaggrundsgradientSubtil radial gradient øverst og nederst – accent og blue toner
🖼️ Ikonografi

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.

EgenskabVærdi
Grid24 × 24
Stregtykkelse1.8
Linecap / linejoinround
FarvecurrentColor – arver fra tekst/kontekst
Fyldingen (fill: none)
Brug<svg><use href="#icon-navn"></svg>

Admin-modulets ikonsæt (eksempel på husstilen)

Ikonerne 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".

🤖 Oversigt

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:

TypeHvad det erHvordan det startes
SkillViden / fremgangsmåde Claude trækker indAutomatisk, når opgaven matcher
SubagentEn separat Claude, der løser en delopgaveAutomatisk eller ved at nævne navnet
KommandoGenvej til en arbejdsgangDu skriver /navn
🧠 Skills

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.

🕵️ Subagenter

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/:

AgentFormål
frontend-reviewGennemgår og optimerer frontend-design på en side
backend-reviewReviewer api.php / admin2.php — sikkerhed først
pwa-deployTjekker service worker, manifest, cache og deploy
web-testerStatisk sundhedstjek (brudte links, syntaks, PWA)
e2e-testerSkriver og vedligeholder tværgående testcases på tværs af moduler (docs/testcases.md)
ide-generatorBrainstormer nye features + håndboldindhold
design-systemVedligeholder det fælles designsystem (tokens)
docs-syncSynkroniserer 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".

⌨️ Kommandoer

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/:

KommandoGør
/pre-deployFuldt review af ændringer + GO/NO-GO før deploy
/testTeknisk sundhedstjek af repoet (kalder web-tester)
/designVedligehold designsystemet (audit / migrér en side til tokens)
/ny-sideStil en ny rod-HTML-side op efter projektets mønster
/ny-pwaStil en ny undermappe-PWA op (manifest, sw, ikoner, design.css)
/docs-syncSammenlign 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.

📁 Hvor de ligger
.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.

🗂️ TeamApp.dk — arkitektur på ét blik

En samling statiske HTML-sider (vanilla JS, ingen build-step) med én fælles PHP-backend mod MySQL. Flere sider er installerbare PWA'er.

21
HTML-sider
2
PHP API'er
6
Standalone apps
8
Claude-agenter
Hoved-app teamapp.dk
api.php
PHP + MySQL backend · JWT · setupDB()
admin2.php
Admin-API
login.html
JWT auth
index.html
Forside "Klubhuset" — indgang til alle moduler
kampnoter
Live kampnoter
kampstatistik
Tasteside til live kampstatistik via delt link
traening
Træningsplanlægning + personligt øvelsesbibliotek (emner, miniaturer, tegneflade)
oevelsesark
Øvelsesark til én træning (rækkefølge, tider)
spiller
Team Play — spillernes app: forside, åbninger, kampe, udvikling, refleksion
coaching
Coaching — trænerens overblik (Team Management)
dashboard
Redirect-stub → coaching
tegning
Viderestilling til Træning (bibliotek)
modstandere
Scouting-ark
styrke-skade
Styrke & skadeforebyg.
links
Offentligt link-bibliotek
links-admin
Links-admin
links-embed
Link-biblioteket på madsdam.dk (iframe)
mini-turnering
Redirect-stub → turnering/
spiller-admin
Team Play Coach — indhold til spillerne
admin
Admin-panel + dokumentation
apps
Installér apps
test
API-testside
404
Fejlside
Standalone egne PWA'er
Beach
Beach håndbold-app
Timer
Interval-timer til træning
Teamgenerator
Generér hold fra spillerliste
Sekvens
Sekvens Timer
Omkostningsgodtgørelse
Udgifter + køreseddel · afregning
turnering
Mini-turnering (PWA-version)
Noter
Trænerens noter fra træningen

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

web-udviklerux-ekspertnavigation projektledericon-setweb-design

Fra .claude/skills/. .agents/skills/ (Codex) har 11 — de samme 6 plus agent-browser, design-taste, find-skills, frontend-design og remotion.

ℹ️ mini-turnering.html i roden er en redirect-stub til turnering/mini-turnering.html (den vedligeholdte PWA-version) — bevaret for gamle bogmærker, ikke et duplikat.
🗺️ Systemoverblik

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.

👥Brugereroller
CheftrænerTrænerSpillerAdmin
↓login.html → JWT-token i localStorage
🖥️Frontend — modulerstatisk HTML/JS/CSS · ingen build-step
coachingkampnoterkampstatistikspillerspiller-admintraeningoevelsesarkmodstanderestyrke-skadeadminlinkslinks-adminlinks-embedapps
BeachTimerTeamgeneratorSekvensturneringOmkostningsgodtgørelseNoter
🎨 design.css — fælles tokens (farver · fonte · mørkt/lyst tema) og komponenter (knapper · dialoger) på næsten alle sider · undtagelser: forside og login (Klubhuset-look) og test
🧭 topmenu.js — global navigation på computersiderne (+ links.html på computer) · rollefiltrering · holdvælger (bruger den delte holdliste) · banneret «Lukket hold»
🧩 Fælles scripts — api-config.js (API-base · glidende login · authFetch · delt holdliste 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)
↓PWA-lag
📲PWAinstallerbar · offline
sw.js — network-first, cacher aldrig api.phpmanifest.json
↓fetch · Authorization: Bearer <JWT> · hold-scoping
🔌API — api.phpPHP · routing via ?action=
Auth: JWT (HS256) · roller: admin (global) · cheftræner / træner / spiller (pr. hold) admin2.php — separat admin-API
↓SQL (prepared statements)
🗄️Database — MySQL 8 (UnoEuro)auto-migreret via setupDB()
holdbrugerespillerekampenoteraabningeroevelsertraeningermodstanderekampstatistikfysiske_programmeromkostningerlinks
🚀 Deploy-flow
Push til main→ GitHub Actions→ FTP-upload→ UnoEuro /public_html/→ teamapp.dk (live)

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.

🗺️ Systemoversigt

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.

📊
Admin Panel
admin.html · Kræver admin-rolle
Systemadministration: brugerstyring, databaseoverblik, hold-statistik på tværs af alle hold og Hold-værktøj (roller, sletning af tomme hold).
Admin
🏆
Coaching
coaching.html · Kræver login
Primært trænerpanel. Opret og administrer kampe, taktiske åbninger, kampnoter og spilanalyser med overblik over holdets aktiviteter.
Træner
📋
Team Play Coach
spiller-admin.html · Kræver login
Opret og rediger indhold til spillerne: åbninger, kampanalyser og udviklingsopslag. Kontroller synlighed og deling med spillerne.
Træner
📱
Team Play (PWA)
spiller.html · Kræver login (alle roller)
Mobiloptimeret PWA til spillere med fem faneblade: Forside, Åbninger, Kampe, Udvikling og Refleksion. Spillere kan like og kommentere indhold. Trænere kan også åbne appen for at se, hvad spillerne ser.
Spiller
✏️
Tegneflade
traening.html · tegneflade.js · Kræver login
Tegneflade til baner på planlægningsarket og i øvelsesbiblioteket. Opstillinger, streger og tekst; banerne gemmes på øvelsen.
Træner
🏋️
Træningsplanlægning
traening.html · Kræver login
Planlæg og dokumenter træningssessioner med et struktureret øvelsesbibliotek. Organiser øvelser i sessioner med starttid og varighed.
Træner
📝
Kampnoter
kampnoter.html · Kræver login
Live kampnoter under kampen: taktik-, spiller- og timeout-noter i realtid. Noterne samles på kampen og kan deles med Team Play.
Træner
📊
Kampstatistik
kampstatistik.html · Via delt link (intet login)
Tasteside til live kampstatistik: mål, skud, 2 min og tekniske fejl pr. spiller samt målmændenes redninger. Trænere starter og deler linket i Coaching eller kampnoter og ser tallene bagefter; spiller har ingen adgang.
Træner
📄
Øvelsesark
oevelsesark.html · Kræver login
Sammensæt øvelsesarket til én træning ud fra øvelsesbiblioteket eller med nye øvelser – med starttid, varighed og beskrivelse for hver øvelse.
Træner
🔍
Modstander-scouting
modstandere.html · Kræver login
Scouting-ark pr. modstanderhold: forsvarsformation, angrebsstil, farlige spillere og målvogter-tendenser. Kobles til Coaching og kampnoter.
Træner
💪
Styrke & skadeforebyggelse
styrke-skade.html · Kræver login
Fælles bibliotek af øvelser og programmer til styrke og skadeforebyggelse. Programmerne er fælles for trænerne (kun opretteren retter dem) og sættes på de hold, der skal bruge dem. Spillere kan læse holdets programmer.
Træner Spiller (læs)
🔗
Link-bibliotek
links.html · Offentlig
Kurateret linksamling til håndbold-ressourcer i op til to niveauer af kategorier, med søgning, favoritter (localStorage) og "Nyt"-badges. Kræver ikke login – installerbar som egen PWA.
Alle
🧩
Link-bibliotek Embed
links-embed.html · Offentlig
Iframe-venlig udgave af link-biblioteket med fast lyst tema og automatisk højde. Vises på madsdam.dk, så links kun vedligeholdes ét sted (links-admin).
Alle
📥
Link-bibliotek Admin
links-admin.html · Kræver login
Linkstyring for alle trænere (opret og ret egne links; nye links venter på godkendelse) og godkendelsesliste for admin. Kun admin styrer kategorier, CSV-import og link-validering (finder døde links). Klikstatistikken ses af alle med adgang. Spiller har ingen adgang.
Træner
🧪
API Test Panel
test.html · Kræver admin-login
Udviklingsværktøj til at teste API-endpoints direkte i browseren. Send forespørgsler og se råt JSON-svar med syntaksfremhævning.
Admin
🔐
Login
login.html · Offentlig
Fælles loginside for alle brugertyper. JWT-baseret autentificering med automatisk omdirigering til korrekt side baseret på brugerrolle.
Alle
📲
Installér apps
apps.html · Offentlig
Installationsside med guide og kort til de 9 mobil-apps, så de kan lægges på hjemmeskærmen. Nås fra forsiden og fra topmenuens brugermenu.
Alle
🔗 Modulrelationer og dataflow
Fra modul→ Til modulDatatype
spiller-admin.htmlspiller.htmlÅbninger, kampanalyser og udviklingsopslag deles med spillerne
tegneflade.jsoevelsesark.html / traening.htmlBaner 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.htmlcoaching.htmlTræ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.htmlcoaching.html / kampnoter.htmlTastede hændelser vises som statistik i kampvisningen (fuld) og i kampnoters Statistik-fane (kort)
admin.htmlAlle modulerBrugere og hold administreres centralt – påvirker adgang overalt
login.htmlcoaching.html / spiller.htmlJWT-token bruges på tværs af alle moduler til auth
⚙️ Teknisk profil per modul
ModulAPIPWACanvasTema
admin.htmladmin2.php——Mørkt + Lyst
coaching.htmlapi.php—✓Mørkt + Lyst
spiller-admin.htmlapi.php——Mørkt + Lyst
spiller.htmlapi.php✓—Mørkt + Lyst
tegneflade.js— (gemmer via siden, der bruger den)——Mørkt + Lyst
traening.htmlapi.php——Mørkt + Lyst
kampnoter.htmlapi.php——Mørkt + Lyst
kampstatistik.htmlapi.php (stat_link*)——Mørkt + Lyst
oevelsesark.htmlapi.php——Mørkt + Lyst
modstandere.htmlapi.php——Mørkt + Lyst
styrke-skade.htmlapi.php——Mørkt + Lyst
links.htmlapi.php✓ (manifest-links.json)—Mørkt + Lyst
links-admin.htmlapi.php——Mørkt + Lyst
test.htmladmin2.php + api.php——Mørkt
login.htmlapi.php (login)——Mørkt + Lyst (eget "Klubhuset"-look, ikke design.css)
links-embed.htmlapi.php——Fast lyst
apps.html———Mørkt + Lyst