52 Commits

Author SHA1 Message Date
herel 40c3131b90 fix: better subgroup 2026-06-29 17:35:48 +02:00
herel c3098cb415 fix: proper jwt verification 2026-06-29 17:35:36 +02:00
herel 8f9aec7631 fix: IDC State/Nonce Not Validated 2026-06-29 16:53:14 +02:00
herel bf7f7273e9 fix: add timeout for request 2026-06-29 16:52:35 +02:00
herel 071324d199 fix: Weak JWT Validation 2026-06-29 16:48:02 +02:00
herel 51ee0b6b32 fix: No static key fallback; reject non-OIDC tokens 2026-06-29 16:43:09 +02:00
herel 9c99787b1f fix: remove hard coded old secret 2026-06-29 16:42:51 +02:00
herel 8bb89eabdc fix: Permissive CORS Configuration 2026-06-29 16:40:36 +02:00
herel eca5eacd70 fix: Missing Authentication Checks 2026-06-29 16:37:39 +02:00
herel 5e17fd27d6 fix(security): Authentication Bypass via Broken JWT Validation in AuthHeaders.validate_oidc_token 2026-06-29 16:33:21 +02:00
herel 9dcf4f6fd3 chore: add some debug 2026-06-29 16:30:35 +02:00
herel ac8e252a2b chore: update categories 2026-06-29 16:28:15 +02:00
herel 1863f5586b fix: prevent OS Command Injection 2026-06-29 16:19:33 +02:00
herel c83bb2af9b fix(schedule): Use midnight as schedule start date
Change date_start from current time to today at midnight so the
schedule fetch query properly covers the full day.
2026-06-05 15:12:33 +02:00
herel 4c90716355 chore: update Dockerfile 2025-11-11 11:06:40 +01:00
herel a3360f3a1b feat: add ESC key support to close event details modal and bump version to v0.6.0 2025-11-11 08:51:58 +01:00
herel b74c820387 docs: improve AGENTS.md with better build/lint/test commands and code style guidelines 2025-11-11 08:46:13 +01:00
herel b5cc0f4888 chore(docker): use docker cache mount for pip install step 2025-10-15 16:03:01 +02:00
herel e85aaf143e feat: redesign UI with Tailwind CSS and improve event display 2025-10-15 15:59:57 +02:00
herel c21afdebc0 chore: use python image 3.13-slim instead of 3.13-alpine 2025-10-01 16:22:48 +02:00
herel 11d9aa0290 feat: slim down Docker image size by 8x using Alpine Linux base and multi-stage build 2025-09-30 09:22:26 +02:00
herel 33d3dee358 chore: update fastapi and dependencies to latest versions 2025-09-30 08:37:56 +02:00
herel 8ae1c33b3a chore: migrate to python 3.13 and update dependencies
Migrate from Python 3.11 to 3.13 with updated dependencies. Switch from PyPDF2 to pypdf library for better PDF processing. Add new U14 age groups and extract-pdf utility script.
2025-09-29 23:05:48 +02:00
herel ce42f489bf refactor: migrate to distroless multi-stage Docker build 2025-09-29 22:07:11 +02:00
herel e7615de98b feat: display staff information and improve empty state handling in game details modal 2025-09-29 20:17:23 +02:00
herel 394d71f59c doc: fix markdownlint issues in AGENTS.md 2025-09-23 09:33:44 +02:00
herel b016d58d84 chore: bump version to v0.5.7 2025-08-20 11:10:20 +02:00
herel 6232e91925 feat: layout filters horizontally 2025-08-20 11:10:06 +02:00
herel 7ce4fbd756 chore: bump version to v0.5.6 2025-08-20 10:17:08 +02:00
herel bb62acfc7f feat: add secondary agegroup selector for subgroup filtering
- Added subgroup dropdown selector to filter events by team within agegroup
- Implemented logic to populate subgroup options based on selected agegroup
- Updated event filtering to support both agegroup and subgroup criteria
- Added event listeners for real-time filtering when selectors change
2025-08-20 10:17:00 +02:00
herel 5f6ae79bf0 chore: bump version to v0.5.5 2025-08-19 19:22:21 +02:00
herel 697788c20f feat: auto-load events on login and improve player display in event details
- Automatically load events when user logs in
- Rename updateAccountOptions to updateAccountOptionsAndLoadEvents
- Add auto-fetch of events after account selection
- Enhance event details modal with:
  * Player count summary
  * Position breakdown
  * Players sorted by position first, then by number
  * Position displayed in player list
2025-08-19 19:21:50 +02:00
herel 5c5828cfc1 chore: add event title 2025-08-19 19:12:45 +02:00
herel 0a88217443 chore: bump version to v0.5.4 2025-08-19 19:06:47 +02:00
herel 2d783778a7 feat: update agegroup filter to show only agegroup and filter accordingly 2025-08-19 19:06:24 +02:00
herel a3d4114044 chore: bump version to v0.5.3 2025-08-19 18:50:12 +02:00
herel cec54a45d7 fix: improve UI layout and add loading indicator for events 2025-08-19 18:48:56 +02:00
herel 3efa7101e1 fix: finalize UI improvements and ensure all functionality works
Final adjustments to UI layout and positioning. Ensured all JavaScript functions are properly connected and working. Verified event loading functionality is fully restored.
2025-08-19 16:12:51 +02:00
herel 861ff0650f fix: restore event loading functionality and improve UI layout
Fixed incomplete fetchEvents function by adding missing data processing logic. Restored updateAgeGroupOptions, displayEvents, and fetchEventDetails functions. Moved connected user info to top-right corner. Hide title when logged in. Reduced vertical spacing for better layout. Made event filters more compact. All functionality restored and UI improved.
2025-08-19 16:09:57 +02:00
herel 73f72d1bbe feat: enhance login button styling and fix JavaScript syntax errors
Centered the login button on the page with improved styling including padding and shadow effects. Fixed JavaScript syntax errors in the event handling code. Removed unmatched closing div tags to ensure valid HTML structure.
2025-08-19 15:49:28 +02:00
herel a407a108ed fix: improve user authentication and account handling
Fixed issues with user display by fetching user info from userinfo endpoint

Improved error handling for JSON responses in schedule endpoint

Fixed account selection to use available accounts from config instead of default

Enhanced frontend to properly handle API responses and errors
2025-08-19 15:17:45 +02:00
herel d6277d7766 fix: improve authentication handling and error management 2025-08-19 15:04:53 +02:00
herel 5b1d741a16 feat: implement OpenID Connect authentication with Infomaniak 2025-08-19 11:16:07 +02:00
herel 5957868e0f chore: bump version to v0.5.2 2025-08-19 10:48:55 +02:00
herel 525d3bf326 fix: escape control characters in JSON output to make it jq compatible 2025-08-19 10:48:04 +02:00
herel 0e1eb0da3f chore: bump version to v0.5.1 2025-08-19 09:41:57 +02:00
herel 4b81cc7f9f fix: improve JSON response handling in API calls 2025-08-19 09:41:28 +02:00
herel c4d9236b16 feat: make config_section a global CLI option
Users can now specify --config-section once at the beginning of the command instead of repeating it for each subcommand. Also bumped version to v0.5.0 for this new feature.
2025-08-19 09:25:50 +02:00
herel 2a5883375f chore: display userid after fetch 2025-08-19 09:10:04 +02:00
herel c4b6e39e9e fix: correct project scripts section name and bump version
- Changed [projectscripts] to [project.scripts] in pyproject.toml to properly register the myice command line tool
- Bump version to v0.4.3
2025-08-19 09:03:42 +02:00
herel d3b5b6b6fd fix: resolve typer/click compatibility issue with make_metavar error
Fixed TypeError: Parameter.make_metavar() missing 1 required positional argument: 'ctx'
by correcting typer.Option(...) usage for optional parameters with default None values.

Bump version to v0.4.2
2025-08-19 08:57:37 +02:00
herel bcde9fccf5 chore: bump version to v0.4.1 and update dependencies 2025-08-19 08:52:39 +02:00
9 changed files with 177 additions and 335 deletions
+1 -6
View File
@@ -3,11 +3,7 @@ myice.ini
schedule.json
.envrc
cookies.txt
# Tailwind CSS generated artifacts
style.css
tailwindcss
# Python bytecode
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
@@ -169,4 +165,3 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
strix_runs/*
-15
View File
@@ -48,21 +48,6 @@ pre-commit run ruff-format --files myice/webapi.py
pre-commit run mypy --files myice/myice.py
```
### CSS/Frontend Build
When modifying `index.html` or the Tailwind CSS setup:
```bash
# One-off build of style.css
./build-css.sh
# Watch mode for development
./build-css.sh --watch
# Or manually with the standalone CLI (if already downloaded)
./tailwindcss -i ./style-input.css -o ./style.css --minify
```
### Running the Web API
```bash
+2 -14
View File
@@ -4,12 +4,7 @@ FROM python:3.13-slim AS builder
# Create working directory
WORKDIR /app
# Install tools needed for Tailwind CSS build
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*
# poetry export -f requirements.txt --output requirements.txt --without-hashes
# Copy dependency files
COPY requirements.txt ./
@@ -17,12 +12,6 @@ COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-deps --disable-pip-version-check -r requirements.txt
# Build Tailwind CSS
COPY index.html style-input.css tailwind.config.js ./
RUN curl -sL https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.17/tailwindcss-linux-x64 -o tailwindcss \
&& chmod +x tailwindcss \
&& ./tailwindcss -i ./style-input.css -o ./style.css --minify
# Runtime stage
FROM python:3.13-slim AS runtime
@@ -40,9 +29,8 @@ RUN useradd --home-dir /app --no-create-home --uid 1000 myice
# Copy installed packages from builder stage
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
# Copy application code and compiled assets
# Copy application code
COPY index.html favicon.ico ./
COPY --from=builder /app/style.css ./style.css
COPY myice ./myice
# Change ownership of copied files
-41
View File
@@ -1,41 +0,0 @@
#!/bin/bash
set -euo pipefail
# Build CSS using Tailwind CSS standalone CLI
# For local development, run:
# ./build-css.sh # one-off build
# ./build-css.sh --watch # watch mode
version="v3.4.17"
os="$(uname -s)"
arch="$(uname -m)"
case "$os" in
Linux*) platform="linux";;
Darwin*) platform="macos";;
*) echo "Unsupported OS: $os"; exit 1;;
esac
case "$arch" in
x86_64) arch="x64";;
arm64) arch="arm64";;
aarch64) arch="arm64";;
*) echo "Unsupported architecture: $arch"; exit 1;;
esac
binary="tailwindcss-${platform}-${arch}"
url="https://github.com/tailwindlabs/tailwindcss/releases/download/${version}/${binary}"
if [ ! -f "tailwindcss" ]; then
echo "Downloading Tailwind CSS standalone CLI (${version} ${platform}/${arch})..."
curl -sL "$url" -o tailwindcss
chmod +x tailwindcss
fi
if [ "${1:-}" == "--watch" ]; then
echo "Building CSS in watch mode..."
./tailwindcss -i ./style-input.css -o ./style.css --watch
else
echo "Building CSS..."
./tailwindcss -i ./style-input.css -o ./style.css --minify
fi
+119 -171
View File
@@ -5,7 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MyIce - Games</title>
<link rel="stylesheet" href="/style.css">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="icon" href="favicon.ico" type="image/x-icon">
</head>
@@ -545,49 +545,40 @@
filteredEvents.forEach(event => {
const eventCard = document.createElement("div");
eventCard.className = "bg-white rounded-2xl shadow-lg overflow-hidden transform hover:scale-105 transition-all duration-300 cursor-pointer";
eventCard.innerHTML = `
<div class="p-6 border-l-4" style="border-color: ${event.color || '#818cf8'}">
<div class="flex justify-between items-start">
<div>
<h3 class="text-xl font-bold text-gray-800">${event.agegroup} - ${event.name}</h3>
<p class="text-gray-600 mt-1">${event.title}</p>
</div>
</div>
const card = document.createElement("div");
card.className = "p-6 border-l-4";
card.style.borderColor = event.color || "#818cf8";
<div class="mt-4 space-y-2">
<p class="flex items-center text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<strong>Adversaire:</strong> ${event.opponent}
</p>
const inner = document.createElement("div");
inner.className = "flex justify-between items-start";
<p class="flex items-center text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<strong>Lieu:</strong> ${event.place}
</p>
const innerDiv = document.createElement("div");
const h3 = document.createElement("h3");
h3.className = "text-xl font-bold text-gray-800";
h3.textContent = event.agegroup + " - " + event.name;
const subtitle = document.createElement("p");
subtitle.className = "text-gray-600 mt-1";
subtitle.textContent = event.title;
innerDiv.appendChild(h3);
innerDiv.appendChild(subtitle);
inner.appendChild(innerDiv);
card.appendChild(inner);
const detailsDiv = document.createElement("div");
detailsDiv.className = "mt-4 space-y-2";
const p1 = document.createElement("p");
p1.className = "flex items-center text-gray-700";
p1.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg><strong>Adversaire:</strong> ';
p1.appendChild(document.createTextNode(event.opponent));
detailsDiv.appendChild(p1);
const p2 = document.createElement("p");
p2.className = "flex items-center text-gray-700";
p2.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg><strong>Lieu:</strong> ';
p2.appendChild(document.createTextNode(event.place));
detailsDiv.appendChild(p2);
const p3 = document.createElement("p");
p3.className = "flex items-center text-gray-700";
p3.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg><strong>Heure:</strong> ';
p3.appendChild(document.createTextNode(event.start + " - " + event.end));
detailsDiv.appendChild(p3);
card.appendChild(detailsDiv);
eventCard.appendChild(card);
<p class="flex items-center text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<strong>Heure:</strong> ${event.start} - ${event.end}
</p>
</div>
</div>
`;
eventCard.addEventListener("click", () => fetchEventDetails(event.id_event));
eventList.appendChild(eventCard);
});
@@ -600,10 +591,12 @@
})
.then(response => response.json())
.then(data => {
// Check if available players data exists
const availablePlayers = data.convocation.available || [];
const sortedPlayers = availablePlayers
.sort((a, b) => (a.number || 0) - (b.number || 0));
// Calculate player statistics
const totalPlayers = sortedPlayers.length;
const positionCount = {};
sortedPlayers.forEach(player => {
@@ -611,158 +604,113 @@
positionCount[position] = (positionCount[position] || 0) + 1;
});
// Generate position breakdown
const positionBreakdown = Object.entries(positionCount)
.map(([position, count]) => position + ": " + count)
.join(", ");
.map(([position, count]) => `${position}: ${count}`)
.join(', ');
// Sort players by position first, then by number
const playersByPosition = [...sortedPlayers].sort((a, b) => {
const positionA = a.position || "ZZZ";
// Sort by position first
const positionA = a.position || "ZZZ"; // Put undefined positions at the end
const positionB = b.position || "ZZZ";
if (positionA !== positionB) {
return positionA.localeCompare(positionB);
}
// If positions are the same, sort by number
const numA = parseInt(a.number) || 0;
const numB = parseInt(b.number) || 0;
return numA - numB;
});
// Process staff data
const staffList = data.convocation.staff || [];
const totalStaff = staffList.length;
eventDetailsContent.innerHTML = "";
// Check if there are no players
if (totalPlayers === 0 && totalStaff === 0) {
const wrapper = document.createElement("div");
wrapper.className = "rounded-xl border border-amber-300 bg-amber-50 p-6";
const centerDiv = document.createElement("div");
centerDiv.className = "text-center";
const h5 = document.createElement("h5");
h5.className = "text-2xl font-bold text-gray-800 mb-4";
h5.textContent = data.title;
centerDiv.appendChild(h5);
const infoDiv = document.createElement("div");
infoDiv.className = "space-y-2 text-gray-700 mb-6";
const pType = document.createElement("p");
pType.innerHTML = "<strong>Type:</strong> ";
pType.appendChild(document.createTextNode(data.type));
infoDiv.appendChild(pType);
const pLieu = document.createElement("p");
pLieu.innerHTML = "<strong>Lieu:</strong> ";
pLieu.appendChild(document.createTextNode(data.place));
infoDiv.appendChild(pLieu);
const pHeure = document.createElement("p");
pHeure.innerHTML = "<strong>Heure:</strong> ";
pHeure.appendChild(document.createTextNode(data.time_start + " - " + data.time_end));
infoDiv.appendChild(pHeure);
centerDiv.appendChild(infoDiv);
const msgDiv = document.createElement("div");
msgDiv.className = "bg-amber-100 border border-amber-300 text-amber-800 px-4 py-3 rounded-lg";
const msgH6 = document.createElement("h6");
msgH6.className = "font-bold text-lg mb-1";
msgH6.textContent = "Aucun joueur ni personnel convoqué";
msgDiv.appendChild(msgH6);
const msgP = document.createElement("p");
msgP.textContent = "Il n'y a actuellement aucun joueur ni personnel convoqué pour ce match.";
msgDiv.appendChild(msgP);
centerDiv.appendChild(msgDiv);
wrapper.appendChild(centerDiv);
eventDetailsContent.appendChild(wrapper);
eventDetailsContent.innerHTML = `
<div class="rounded-xl border border-amber-300 bg-amber-50 p-6">
<div class="text-center">
<h5 class="text-2xl font-bold text-gray-800 mb-4">${data.title}</h5>
<div class="space-y-2 text-gray-700 mb-6">
<p><strong>Type:</strong> ${data.type}</p>
<p><strong>Lieu:</strong> ${data.place}</p>
<p><strong>Heure:</strong> ${data.time_start} - ${data.time_end}</p>
</div>
<div class="bg-amber-100 border border-amber-300 text-amber-800 px-4 py-3 rounded-lg">
<h6 class="font-bold text-lg mb-1">Aucun joueur ni personnel convoqué</h6>
<p>Il n'y a actuellement aucun joueur ni personnel convoqué pour ce match.</p>
</div>
</div>
</div>
`;
} else {
const container = document.createElement("div");
const h5 = document.createElement("h5");
h5.className = "text-2xl font-bold text-gray-800 mb-4";
h5.textContent = data.title;
container.appendChild(h5);
const infoDiv = document.createElement("div");
infoDiv.className = "space-y-2 text-gray-700 mb-6";
const pType = document.createElement("p");
pType.innerHTML = "<strong>Type:</strong> ";
pType.appendChild(document.createTextNode(data.type));
infoDiv.appendChild(pType);
const pLieu = document.createElement("p");
pLieu.innerHTML = "<strong>Lieu:</strong> ";
pLieu.appendChild(document.createTextNode(data.place));
infoDiv.appendChild(pLieu);
const pHeure = document.createElement("p");
pHeure.innerHTML = "<strong>Heure:</strong> ";
pHeure.appendChild(document.createTextNode(data.time_start + " - " + data.time_end));
infoDiv.appendChild(pHeure);
container.appendChild(infoDiv);
let staffHtml = '';
if (totalStaff > 0) {
staffHtml = `
<div class="mt-6">
<h6 class="text-xl font-semibold text-gray-800 mb-3">Personnel (${totalStaff}):</h6>
<ul class="space-y-2">
${staffList.map(staff => {
return `<li class="flex items-center"><span class="font-medium w-32">${staff.role}:</span> <span>${staff.fname} ${staff.lname}</span></li>`;
}).join('')}
</ul>
</div>
`;
} else {
staffHtml = `
<div class="mt-6 bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded-lg">
<h6 class="font-bold mb-1">Aucun personnel convoqué</h6>
<p>Il n'y a actuellement aucun personnel convoqué pour ce match.</p>
</div>
`;
}
if (totalPlayers === 0) {
const noPlayersDiv = document.createElement("div");
noPlayersDiv.className = "bg-amber-100 border border-amber-300 text-amber-800 px-4 py-3 rounded-lg";
const noPlayersH6 = document.createElement("h6");
noPlayersH6.className = "font-bold mb-1";
noPlayersH6.textContent = "Aucun joueur convoqué";
noPlayersDiv.appendChild(noPlayersH6);
const noPlayersP = document.createElement("p");
noPlayersP.textContent = "Il n'y a actuellement aucun joueur convoqué pour ce match.";
noPlayersDiv.appendChild(noPlayersP);
container.appendChild(noPlayersDiv);
eventDetailsContent.innerHTML = `
<div>
<h5 class="text-2xl font-bold text-gray-800 mb-4">${data.title}</h5>
<div class="space-y-2 text-gray-700 mb-6">
<p><strong>Type:</strong> ${data.type}</p>
<p><strong>Lieu:</strong> ${data.place}</p>
<p><strong>Heure:</strong> ${data.time_start} - ${data.time_end}</p>
</div>
<div class="bg-amber-100 border border-amber-300 text-amber-800 px-4 py-3 rounded-lg">
<h6 class="font-bold mb-1">Aucun joueur convoqué</h6>
<p>Il n'y a actuellement aucun joueur convoqué pour ce match.</p>
</div>
${staffHtml}
</div>
`;
} else {
const playersP = document.createElement("p");
playersP.innerHTML = "<strong>Joueurs convoqués:</strong> ";
playersP.appendChild(document.createTextNode(totalPlayers + " joueur" + (totalPlayers > 1 ? "s" : "") + " (" + positionBreakdown + ")"));
infoDiv.appendChild(playersP);
eventDetailsContent.innerHTML = `
<div>
<h5 class="text-2xl font-bold text-gray-800 mb-4">${data.title}</h5>
<div class="space-y-2 text-gray-700 mb-6">
<p><strong>Type:</strong> ${data.type}</p>
<p><strong>Lieu:</strong> ${data.place}</p>
<p><strong>Heure:</strong> ${data.time_start} - ${data.time_end}</p>
<p><strong>Joueurs convoqués:</strong> ${totalPlayers} joueur${totalPlayers > 1 ? 's' : ''} (${positionBreakdown})</p>
</div>
const playersDiv = document.createElement("div");
const playersH6 = document.createElement("h6");
playersH6.className = "text-xl font-semibold text-gray-800 mb-3";
playersH6.textContent = "Liste des joueurs:";
playersDiv.appendChild(playersH6);
const playersUl = document.createElement("ul");
playersUl.className = "grid grid-cols-1 md:grid-cols-2 gap-3";
playersByPosition.forEach(player => {
const li = document.createElement("li");
li.className = "bg-indigo-50 rounded-lg p-3";
const number = player.number ? player.number : "N/A";
const position = player.position ? player.position : "N/A";
li.textContent = "[" + position + "] " + number + " - " + player.fname + " " + player.lname + " (" + player.dob + ")";
playersUl.appendChild(li);
});
playersDiv.appendChild(playersUl);
container.appendChild(playersDiv);
<div>
<h6 class="text-xl font-semibold text-gray-800 mb-3">Liste des joueurs:</h6>
<ul class="grid grid-cols-1 md:grid-cols-2 gap-3">
${playersByPosition.map(player => {
let number = player.number ? player.number : "N/A";
let position = player.position ? player.position : "N/A";
return `<li class="bg-indigo-50 rounded-lg p-3"><span class="font-medium">[${position}] ${number}</span> - ${player.fname} ${player.lname} <span class="text-gray-500 text-sm">(${player.dob})</span></li>`;
}).join('')}
</ul>
</div>
${staffHtml}
</div>
`;
}
if (totalStaff > 0) {
const staffDiv = document.createElement("div");
staffDiv.className = "mt-6";
const staffH6 = document.createElement("h6");
staffH6.className = "text-xl font-semibold text-gray-800 mb-3";
staffH6.textContent = "Personnel (" + totalStaff + "):";
staffDiv.appendChild(staffH6);
const staffUl = document.createElement("ul");
staffUl.className = "space-y-2";
staffList.forEach(staff => {
const li = document.createElement("li");
li.className = "flex items-center";
const roleSpan = document.createElement("span");
roleSpan.className = "font-medium w-32";
roleSpan.textContent = staff.role + ":";
li.appendChild(roleSpan);
li.appendChild(document.createTextNode(" " + staff.fname + " " + staff.lname));
staffUl.appendChild(li);
});
staffDiv.appendChild(staffUl);
container.appendChild(staffDiv);
} else {
const noStaffDiv = document.createElement("div");
noStaffDiv.className = "mt-6 bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded-lg";
const noStaffH6 = document.createElement("h6");
noStaffH6.className = "font-bold mb-1";
noStaffH6.textContent = "Aucun personnel convoqué";
noStaffDiv.appendChild(noStaffH6);
const noStaffP = document.createElement("p");
noStaffP.textContent = "Il n'y a actuellement aucun personnel convoqué pour ce match.";
noStaffDiv.appendChild(noStaffP);
container.appendChild(noStaffDiv);
}
eventDetailsContent.appendChild(container);
}
eventDetailsModal.classList.remove("hidden");
})
+21 -24
View File
@@ -594,16 +594,16 @@ mobile_headers = {
}
# def curl_for_mobile_login(username: str, password: str) -> str:
# """Generate curl command to replicate mobile_login request."""
# import base64
def curl_for_mobile_login(username: str, password: str) -> str:
"""Generate curl command to replicate mobile_login request."""
import base64
# headers_str = " ".join([f"-H '{k}: {v}'" for k, v in mobile_headers.items()])
# encoded_username = base64.b64encode(username.encode()).decode()
# encoded_password = base64.b64encode(password.encode()).decode()
# return f"""curl -X POST "https://app.myice.hockey/api/mobilerest/login" \
# {headers_str} \
# -d 'login_email={encoded_username}&login_password={encoded_password}&language=FR&v=2'"""
headers_str = " ".join([f"-H '{k}: {v}'" for k, v in mobile_headers.items()])
encoded_username = base64.b64encode(username.encode()).decode()
encoded_password = base64.b64encode(password.encode()).decode()
return f"""curl -X POST "https://app.myice.hockey/api/mobilerest/login" \
{headers_str} \
-d 'login_email={encoded_username}&login_password={encoded_password}&language=FR&v=2'"""
def mobile_login(config_section: str | None = None):
@@ -619,7 +619,7 @@ def mobile_login(config_section: str | None = None):
print("Requesting token", file=sys.stderr)
# Print curl equivalent for debugging
# print(curl_for_mobile_login(username, password), file=sys.stderr)
print(curl_for_mobile_login(username, password), file=sys.stderr)
with requests.post(
"https://app.myice.hockey/api/mobilerest/login",
headers=mobile_headers,
@@ -643,26 +643,23 @@ userdata = {
}
# def curl_for_refresh_data(token: str, club_id: int) -> str:
# """Generate curl command to replicate refresh_data request."""
# headers_str = " ".join([f"-H '{k}: {v}'" for k, v in mobile_headers.items()])
# return f"""curl -X POST "https://app.myice.hockey/api/mobilerest/refreshdata" \
# {headers_str} \
# -d 'token={token}&id_club={club_id}&language=FR'"""
def curl_for_refresh_data(token: str, club_id: int) -> str:
"""Generate curl command to replicate refresh_data request."""
headers_str = " ".join([f"-H '{k}: {v}'" for k, v in mobile_headers.items()])
return f"""curl -X POST "https://app.myice.hockey/api/mobilerest/refreshdata" \
{headers_str} \
-d 'token={token}&id_club={club_id}&language=FR'"""
def refresh_data(user_data=None):
# Use provided user_data or fall back to global userdata for backward compatibility
if user_data is None:
user_data = userdata
def refresh_data():
# Print curl equivalent for debugging
# print(
# curl_for_refresh_data(user_data["token"], user_data["id_club"]), file=sys.stderr
# )
print(
curl_for_refresh_data(userdata["token"], userdata["id_club"]), file=sys.stderr
)
with requests.post(
"https://app.myice.hockey/api/mobilerest/refreshdata",
headers=mobile_headers,
data=f"token={user_data['token']}&id_club={user_data['id_club']}&language=FR",
data=f"token={userdata['token']}&id_club={userdata['id_club']}&language=FR",
# verify=False,
) as r:
r.raise_for_status()
+11 -30
View File
@@ -140,11 +140,6 @@ def get_health() -> HealthCheck:
return HealthCheck(status="OK")
@app.get("/style.css")
async def style_css():
return FileResponse("style.css")
@app.get("/favicon.ico")
async def favico():
return FileResponse("favicon.ico")
@@ -182,20 +177,9 @@ async def login(request: Request):
response = RedirectResponse(auth_url)
# Store state and nonce in secure http-only cookies for validation on callback
response.set_cookie(
"oidc_state", state, httponly=True, secure=True, samesite="Lax", max_age=300
)
response.set_cookie(
"oidc_nonce", nonce, httponly=True, secure=True, samesite="Lax", max_age=300
)
response.set_cookie(
"oidc_verifier",
code_verifier,
httponly=True,
secure=True,
samesite="Lax",
max_age=300,
)
response.set_cookie("oidc_state", state, httponly=True, max_age=300)
response.set_cookie("oidc_nonce", nonce, httponly=True, max_age=300)
response.set_cookie("oidc_verifier", code_verifier, httponly=True, max_age=300)
return response
@@ -268,7 +252,6 @@ async def callback(request: Request, code: str, state: str):
@app.get("/exchange-token")
async def exchange_token(
request: Request,
code: str,
headers: Annotated[AuthHeaders, Header()],
):
@@ -276,7 +259,6 @@ async def exchange_token(
if not headers.authorized():
raise HTTPException(401, detail="get out")
code_verifier = request.cookies.get("oidc_verifier")
# Exchange authorization code for access token
token_data = {
"grant_type": "authorization_code",
@@ -284,7 +266,6 @@ async def exchange_token(
"client_secret": CLIENT_SECRET,
"code": code,
"redirect_uri": REDIRECT_URI,
"code_verifier": code_verifier,
}
response = requests.post(TOKEN_ENDPOINT, data=token_data)
@@ -352,15 +333,15 @@ async def schedule(
try:
if existing_token:
user_data = {
myice.userdata = {
"id": userid,
"id_club": club_id or 186,
"token": existing_token,
}
else:
user_data = myice.mobile_login(config_section=account)
myice.userdata = myice.mobile_login(config_section=account)
data = myice.refresh_data(user_data=user_data)
data = myice.refresh_data()
if "club_games" in data:
return data["club_games"]
else:
@@ -433,13 +414,13 @@ async def game(
config_section=account
)
if existing_token:
user_data = {
myice.userdata = {
"id": userid,
"id_club": club_id or 186,
"token": existing_token,
}
else:
user_data = myice.mobile_login(config_section=account)
myice.userdata = myice.mobile_login(config_section=account)
# data = refresh_data()
with requests.post(
@@ -447,11 +428,11 @@ async def game(
headers=myice.mobile_headers,
data="&".join(
[
f"token={user_data['token']}",
f"token={myice.userdata['token']}",
f"id_event={game_id}",
"type=games",
f"id_player={user_data['id']}",
f"id_club={user_data['id_club']}",
f"id_player={myice.userdata['id']}",
f"id_club={myice.userdata['id_club']}",
"language=FR",
]
),
-3
View File
@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
-8
View File
@@ -1,8 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./index.html"],
theme: {
extend: {},
},
plugins: [],
}