16 Commits
Author SHA1 Message Date
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
4 changed files with 37 additions and 222 deletions

No files matched your search

+2 -30
View File
@@ -26,22 +26,11 @@ yamllint . # YAML linting
markdownlint . # Markdown linting markdownlint . # Markdown linting
``` ```
### Running Tests ### Testing
```bash ```bash
# No formal test framework configured # Run tests (no specific test framework configured)
# Project uses manual testing with example PDF files in repository # Project uses manual testing with example PDF files in repository
# To test individual functions, run the CLI commands directly:
# myice schedule --days 7
# myice mobile-login
# myice search --help
```
### Running the Web API
```bash
# Or with poetry
poetry run fastapi run myice/webapi.py --host 127.0.0.1
``` ```
## Code Style Guidelines ## Code Style Guidelines
@@ -86,20 +75,3 @@ poetry run fastapi run myice/webapi.py --host 127.0.0.1
- requests for HTTP requests - requests for HTTP requests
- PyPDF2 for PDF processing - PyPDF2 for PDF processing
- Use rich for enhanced console output - Use rich for enhanced console output
- Custom rl_ai_tools package for AI functionalities
### Git Commit Messages
- Use conventional commits format
- Never mention Claude in commit messages
- Be descriptive but concise
- Use present tense ("add feature" not "added feature")
### Additional Rules
- Always use ddg-mcp to perform Web Search functionality
- Follow the existing code patterns in myice/myice.py and myice/webapi.py
- Maintain backward compatibility when modifying existing APIs
- Document new features in README.md
- Always run ruff format and ruff check after editing a python file
- use conventional commit messages
+33 -190
View File
@@ -32,28 +32,16 @@
<div class="container mt-2" id="mainContent" style="display: none;"> <div class="container mt-2" id="mainContent" style="display: none;">
<div id="eventFilters" style="display: none;"> <div id="eventFilters" style="display: none;">
<div class="mb-3 row"> <div class="mb-3">
<div class="col-md-3"> <label for="account" class="form-label">Compte</label>
<label for="account" class="form-label">Compte</label> <select id="account" class="form-select">
<select id="account" class="form-select"> <option value="default">Défaut</option>
<option value="default">Défaut</option> </select>
</select> <label for="agegroup" class="form-label">Âge</label>
</div> <select id="agegroup" class="form-select">
<div class="col-md-3"> <option value="">Tous</option>
<label for="agegroup" class="form-label">Âge</label> </select>
<select id="agegroup" class="form-select"> <button id="fetchEvents" class="btn btn-primary" style="margin-top: 1.2rem;">Charger</button>
<option value="">Tous</option>
</select>
</div>
<div class="col-md-3">
<label for="subgroup" class="form-label">Sous-groupe</label>
<select id="subgroup" class="form-select">
<option value="">Tous</option>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<button id="fetchEvents" class="btn btn-primary">Charger</button>
</div>
</div> </div>
</div> </div>
@@ -89,7 +77,6 @@
const eventFilters = document.getElementById("eventFilters"); const eventFilters = document.getElementById("eventFilters");
const accountSelect = document.getElementById("account"); const accountSelect = document.getElementById("account");
const agegroupSelect = document.getElementById("agegroup"); const agegroupSelect = document.getElementById("agegroup");
const subgroupSelect = document.getElementById("subgroup");
const eventList = document.getElementById("eventList"); const eventList = document.getElementById("eventList");
const fetchButton = document.getElementById("fetchEvents"); const fetchButton = document.getElementById("fetchEvents");
const eventDetailsContent = document.getElementById("eventDetailsContent"); const eventDetailsContent = document.getElementById("eventDetailsContent");
@@ -223,7 +210,9 @@
} }
eventFilters.style.display = "block"; eventFilters.style.display = "block";
updateAccountOptionsAndLoadEvents(); updateAccountOptions();
// Don't automatically fetch events on page load
// Wait for user to explicitly select an account or click fetch
} else { } else {
// User is not logged in // User is not logged in
loginView.style.display = "flex"; loginView.style.display = "flex";
@@ -266,7 +255,7 @@
location.reload(); location.reload();
} }
function updateAccountOptionsAndLoadEvents() { function updateAccountOptions() {
// Fetch available accounts from the server // Fetch available accounts from the server
fetch(`${apiBaseUrl}/accounts`, { fetch(`${apiBaseUrl}/accounts`, {
headers: { "Authorization": `Bearer ${storedApiKey}` } headers: { "Authorization": `Bearer ${storedApiKey}` }
@@ -317,9 +306,6 @@
// Set the selected account in the dropdown // Set the selected account in the dropdown
accountSelect.value = accountToSelect; accountSelect.value = accountToSelect;
// Automatically fetch events for the selected account
fetchEvents(storedApiKey, accountToSelect);
}) })
.catch(error => { .catch(error => {
console.error("Erreur lors du chargement des comptes:", error); console.error("Erreur lors du chargement des comptes:", error);
@@ -348,12 +334,6 @@
}); });
agegroupSelect.addEventListener("change", () => { agegroupSelect.addEventListener("change", () => {
// Update subgroup options based on selected agegroup
updateSubgroupOptions(agegroupSelect.value, lastFetchedEvents);
displayEvents(lastFetchedEvents);
});
subgroupSelect.addEventListener("change", () => {
displayEvents(lastFetchedEvents); displayEvents(lastFetchedEvents);
}); });
@@ -424,69 +404,20 @@
} }
function updateAgeGroupOptions(events) { function updateAgeGroupOptions(events) {
let agegroups = new Set(events.map(event => event.agegroup)); let agegroups = new Set(events.map(event => `${event.agegroup} ${event.name}`.trim()));
agegroupSelect.innerHTML = '<option value="">Tous</option>'; agegroupSelect.innerHTML = '<option value="">Tous</option>';
Array.from(agegroups).sort().forEach(group => { agegroups.forEach(group => {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = group; option.value = group;
option.textContent = group; option.textContent = group;
agegroupSelect.appendChild(option); agegroupSelect.appendChild(option);
}); });
// Reset subgroup selector
subgroupSelect.innerHTML = '<option value="">Tous</option>';
}
function updateSubgroupOptions(selectedAgegroup, events) {
// Reset subgroup options
subgroupSelect.innerHTML = '<option value="">Tous</option>';
if (selectedAgegroup === "") {
// If no agegroup is selected, disable subgroup selector
subgroupSelect.disabled = true;
return;
}
// Enable subgroup selector
subgroupSelect.disabled = false;
// Extract subgroups from events matching the selected agegroup
let subgroups = new Set();
events
.filter(event => event.agegroup === selectedAgegroup)
.forEach(event => {
// Extract subgroup from event.name or event.title
// This assumes the subgroup is part of the name field
if (event.name && event.name !== selectedAgegroup) {
subgroups.add(event.name);
}
});
// Add subgroups to the selector
Array.from(subgroups).sort().forEach(subgroup => {
const option = document.createElement("option");
option.value = subgroup;
option.textContent = subgroup;
subgroupSelect.appendChild(option);
});
} }
function displayEvents(events) { function displayEvents(events) {
eventList.innerHTML = ""; eventList.innerHTML = "";
let selectedAgegroup = agegroupSelect.value; let selectedAgegroup = agegroupSelect.value;
let selectedSubgroup = subgroupSelect.value; let filteredEvents = events.filter(event => event.event === "Jeu" && (selectedAgegroup === "" || `${event.agegroup} ${event.name}` === selectedAgegroup));
let filteredEvents = events.filter(event => {
// Filter by event type
if (event.event !== "Jeu") return false;
// Filter by agegroup
if (selectedAgegroup !== "" && event.agegroup !== selectedAgegroup) return false;
// Filter by subgroup
if (selectedSubgroup !== "" && event.name !== selectedSubgroup) return false;
return true;
});
if (filteredEvents.length === 0) { if (filteredEvents.length === 0) {
eventList.innerHTML = "<p class='text-muted'>Aucun événement 'Jeu' trouvé.</p>"; eventList.innerHTML = "<p class='text-muted'>Aucun événement 'Jeu' trouvé.</p>";
@@ -499,9 +430,7 @@
eventCard.innerHTML = ` eventCard.innerHTML = `
<div class="card" style="border-left: 5px solid ${event.color}" data-id="${event.id_event}"> <div class="card" style="border-left: 5px solid ${event.color}" data-id="${event.id_event}">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">${event.agegroup} - ${event.name}</h5> <h5 class="card-title">${event.title}</h5>
<p class="card-text">${event.title}</p>
<p class="card-text"><strong>Adversaire:</strong> ${event.opponent}</p>
<p class="card-text"><strong>Lieu:</strong> ${event.place}</p> <p class="card-text"><strong>Lieu:</strong> ${event.place}</p>
<p class="card-text"><strong>Heure:</strong> ${event.start} - ${event.end}</p> <p class="card-text"><strong>Heure:</strong> ${event.start} - ${event.end}</p>
</div> </div>
@@ -518,108 +447,22 @@
headers: { "Authorization": `Bearer ${storedApiKey}` } headers: { "Authorization": `Bearer ${storedApiKey}` }
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
// Check if available players data exists const sortedPlayers = data.convocation.available
const availablePlayers = data.convocation.available || []; .sort((a, b) => (a.number || 0) - (b.number || 0));
const sortedPlayers = availablePlayers
.sort((a, b) => (a.number || 0) - (b.number || 0));
// Calculate player statistics eventDetailsContent.innerHTML = `
const totalPlayers = sortedPlayers.length; <h5>${data.title}</h5>
const positionCount = {}; <p><strong>Type:</strong> ${data.type}</p>
sortedPlayers.forEach(player => { <p><strong>Lieu:</strong> ${data.place}</p>
const position = player.position || "N/A"; <p><strong>Heure:</strong> ${data.time_start} - ${data.time_end}</p>
positionCount[position] = (positionCount[position] || 0) + 1; <h6>Joueurs convoqués:</h6>
}); <ul>${sortedPlayers.map(player => {
let number = player.number ? player.number : "N/A";
// Generate position breakdown let position = player.position ? player.position : "N/A";
const positionBreakdown = Object.entries(positionCount) return `<li>${number} - ${player.fname} ${player.lname} (${position}, ${player.dob})</li>`;
.map(([position, count]) => `${position}: ${count}`) }).join('')}</ul>
.join(', '); `;
// Sort players by position first, then by number
const playersByPosition = [...sortedPlayers].sort((a, b) => {
// 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;
// Check if there are no players
if (totalPlayers === 0 && totalStaff === 0) {
eventDetailsContent.innerHTML = `
<div class="card border-warning">
<div class="card-body text-center">
<h5 class="card-title">${data.title}</h5>
<p class="card-text"><strong>Type:</strong> ${data.type}</p>
<p class="card-text"><strong>Lieu:</strong> ${data.place}</p>
<p class="card-text"><strong>Heure:</strong> ${data.time_start} - ${data.time_end}</p>
<div class="alert alert-warning" role="alert">
<h6 class="alert-heading">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 {
let staffHtml = '';
if (totalStaff > 0) {
staffHtml = `
<h6>Personnel (${totalStaff}):</h6>
<ul>${staffList.map(staff => {
return `<li><strong>${staff.role}:</strong> ${staff.fname} ${staff.lname}</li>`;
}).join('')}</ul>
`;
} else {
staffHtml = `
<div class="alert alert-info" role="alert">
<h6>Aucun personnel convoqué</h6>
<p>Il n'y a actuellement aucun personnel convoqué pour ce match.</p>
</div>
`;
}
if (totalPlayers === 0) {
eventDetailsContent.innerHTML = `
<h5>${data.title}</h5>
<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 class="alert alert-warning" role="alert">
<h6 class="alert-heading">Aucun joueur convoqué</h6>
<p>Il n'y a actuellement aucun joueur convoqué pour ce match.</p>
</div>
${staffHtml}
`;
} else {
eventDetailsContent.innerHTML = `
<h5>${data.title}</h5>
<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>
<h6>Liste des joueurs:</h6>
<ul>${playersByPosition.map(player => {
let number = player.number ? player.number : "N/A";
let position = player.position ? player.position : "N/A";
return `<li>[${position}] ${number} - ${player.fname} ${player.lname} (${player.dob})</li>`;
}).join('')}</ul>
${staffHtml}
`;
}
}
new bootstrap.Modal(document.getElementById('eventDetailsModal')).show(); new bootstrap.Modal(document.getElementById('eventDetailsModal')).show();
}) })
.catch(error => console.error("Erreur lors du chargement des détails de l'événement:", error)); .catch(error => console.error("Erreur lors du chargement des détails de l'événement:", error));
+1 -1
View File
@@ -11,7 +11,7 @@ from . import myice
# OIDC Configuration # OIDC Configuration
CLIENT_ID = os.environ.get("CLIENT_ID", "8ea04fbb-4237-4b1d-a895-0b3575a3af3f") CLIENT_ID = os.environ.get("CLIENT_ID", "8ea04fbb-4237-4b1d-a895-0b3575a3af3f")
CLIENT_SECRET = os.environ.get( CLIENT_SECRET = os.environ.get(
"CLIENT_SECRET", "REDACTED_CLIENT_SECRET" "CLIENT_SECRET", "5iXycu7aU6o9e17NNTOUeetQkRkBqQlomoD2hTyLGLTAcwj0dwkkLH3mz1IrZSmJ"
) )
REDIRECT_URI = os.environ.get("REDIRECT_URI", "http://localhost:8000/callback") REDIRECT_URI = os.environ.get("REDIRECT_URI", "http://localhost:8000/callback")
AUTHORIZATION_ENDPOINT = "https://login.infomaniak.com/authorize" AUTHORIZATION_ENDPOINT = "https://login.infomaniak.com/authorize"
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "myice" name = "myice"
version = "v0.5.7" version = "v0.5.2"
description = "myice parsing" description = "myice parsing"
authors = [ authors = [
{ name = "Rene Luria", "email" = "<rene@luria.ch>"}, { name = "Rene Luria", "email" = "<rene@luria.ch>"},