LAMΓ Fencing Tournament System
DASHBOARD
TOURNAMENT OVERVIEW
Google Sheets: optional
SHEETS: NOT SET
0
Fencers
0
Teams
0
Events
0
Bouts Done
0%
Complete
TOURNAMENT INFO
No tournament loaded.
Click + Tournament to begin.
Click + Tournament to begin.
EVENTS SUMMARY
No events yet.
UPCOMING BOUTS
| Pool | Event | Cat | Left Fencer | Right Fencer | Status | Action |
|---|
FENCERS
ATHLETE REGISTRY
π€Ί ADD NEW FENCER
All Ages
U14
U17
U19
Open
All Genders
Girls
Boys
All Weapons
Foil
ΓpΓ©e
Sabre
| # | Name | Nat | Club / School / College | Age Cat | Gender | Weapon(s) | Rating | DOB | Actions |
|---|
EVENTS
INDIVIDUAL & TEAM EVENTS
⚔️ ADD NEW EVENT
⚙️ Event Options
All Types
Individual
Team
All Ages
U14
U17
U19
Open
All Genders
Girls
Boys
TEAMS
TEAM ROSTER MANAGEMENT
π₯ ADD NEW TEAM
⚔️ Event Assignment
π€Ί Team Roster — 3 players compete, 1 optional substitute
REGISTERED TEAMS
No teams registered.
TEAM FORMAT RULES
| Touch Target | Relay Bouts | Touches/Bout | Members |
|---|---|---|---|
| 45 touches | 9 bouts | 5 each | 3 + 1 sub |
| 15 touches | 3 bouts | 5 each | 3 + 1 sub |
| 10 touches | 2 bouts | 5 each | 3 + 1 sub |
| 5 touches | 1 bout | 5 each | 3 + 1 sub |
Select touch target when creating a Team Event. Winner is first to reach target, or highest score after time limit.
TEAM STANDINGS
No results yet.
⚔️ TEAM BOUTS
Select a team event to view fixtures.
π TEAM KNOCKOUT BRACKET
Select a team event and generate the knockout bracket.
POOLS
ROUND-ROBIN PHASE
Select an individual event and generate pools.
KNOCKOUT
DIRECT ELIMINATION
π― SEEDING ORDER
Seed 1 = strongest · Drag or ↑↓ to reorder · Match-ups: 1 vs N, 2 vs N−1 (sum = bracket+1)
✂️ POOL CUT
Eliminate the bottom % of fencers after pools before they enter the knockout. Top seeds always advance.
π― DE TOUCHES PER ROUND
Set touch limit for each knockout stage. Overrides the event default.
π₯ BRONZE MEDAL FORMAT
Bracket Preview
LIVE SCORE
BOUT SCOREBOARD
SELECT A MATCH
—
3:00
PAUSED
—
0
0π¨
0π₯
VS
⚡ TIED
PRIORITY
PRIORITY
—
0
0π¨
0π₯
NO MATCH LOADED
LIVE SCORING
BOUT CONTROL PANEL
SELECT BOUT
POOL
LEFT
0
03:00
VS
RIGHT
0
PENALTY CARDS
| Fencer | π¨ Yellow | π₯ Red | ⬛ Black |
|---|---|---|---|
| — | 0 | 0 | |
| — | 0 | 0 |
BOUT LOG
Waiting for bout...
RESULTS
FINAL RANKINGS & REPORTS
FINAL RANKINGS
| Rank | Name | Club | Nat | Age | Gender | V | TS | TR | Ind |
|---|
BOUT RESULTS
| Round | Left | Score | Right | Winner |
|---|
ROUND ROBIN RANKINGS
| Rank | Team | Club | Age | Gender | W | L | PF | PA | Diff |
|---|
KNOCKOUT RANKINGS
| Rank | Team | Club | Age | Gender | Round reached |
|---|
MEDAL TABLE
| Club / Nation | π₯ Gold | π₯ Silver | π₯ Bronze | Total |
|---|
ALL EVENT RESULTS
SCHEDULE
BOUT ORDER & PISTE ASSIGNMENT
0
Total Bouts
0
Completed
0
Pending
0
Active Pistes
Select an event and click ⚡ Generate Order to create a bout schedule.
PROFILES
FENCER STATISTICS & HEAD-TO-HEAD
π₯ FIREBASE LIVE SYNC SETUP
Setup steps:
1. Go to console.firebase.google.com
2. Create a project → Add Web App
3. Enable Firestore Database (test mode)
4. Copy your config values below
5. Share your app URL with all 6 referees — scores sync instantly ✅
1. Go to console.firebase.google.com
2. Create a project → Add Web App
3. Enable Firestore Database (test mode)
4. Copy your config values below
5. Share your app URL with all 6 referees — scores sync instantly ✅
π‘ Once connected, all 15 referee devices sync in real-time.
Each referee selects their Piste (1–15) in Live Scoring → each piste is fully independent.
Main display screen can watch any piste using πΊ Display Mode.
Each referee selects their Piste (1–15) in Live Scoring → each piste is fully independent.
Main display screen can watch any piste using πΊ Display Mode.
πΊ DISPLAY MODE
Watch a piste live on TV or projector. Scores update automatically.
π TOURNAMENTS
+ NEW TOURNAMENT
π€ USERS & ACCESS CONTROL
π SIGN IN WITH PIN
π
Chief Referee
Full access · Create tournaments · Manage users
⚔️
Operator
Score entry · View all · Cannot edit setup
π️
Viewer
Read-only · Results & standings only
+ ADD USER
π NEW TOURNAMENT
π€Ί ADD FENCER
π€Ί Weapon & Rating
π€Ί EDIT FENCER
⚔️ ADD EVENT
⚙️ Event Options
EDIT EVENT
⚠️ Changing weapon, age, or gender after pools are generated will not auto-update existing pool assignments.
✏️ EDIT TEAM
Editing Team
⚔️ Event Assignment
π€Ί Team Roster — 3 players compete, 1 optional substitute
ENTER BOUT SCORE
⚔️ DE BOUT SCORE ENTRY
3:00
FENCER A
0
VS
FENCER B
0
⚡ SCORES TIED — Priority Draw Required
Time expired with equal scores. Draw priority by coin toss. Priority holder wins if scores remain tied after 1-min overtime.
Priority:
LAMΓ FENCING
—
0
VS
3:00
PAUSED
⚡ TIED — PRIORITY
0
0
0
LAMΓ FENCING TOURNAMENT SYSTEM
WINS
⚔️ TEAM MATCH
TEAM A
VS
TEAM B
Active Roster — Team A
Active Roster — Team B
π Substitution Log
⚔️ TEAM KO MATCH
0
0 bouts won
VS
0
0 bouts won
Manual result override:
π MATCH RULES
π RESET SCORES
Select what to reset. This cannot be undone.
⚠ Scores will be permanently deleted. Fencers, events and bracket structures are kept.
⚠️ MARK AS ABSENT
π«
Fencer Name
will be marked ABSENT. Their opponent advances automatically with a score of 15–0.
⚠ This cannot be undone without resetting the match.
π₯ 3RD PLACE MATCH
Select the second team for the 3rd place match
TEAM 1 (already set)
TEAM 2 — SELECT
⚔️ TEAM BOUT SHEET
TEAM A
0
VS
TEAM B
0
RELAY BOUTS — Click a bout to enter score
π€Ί BOUT SCORE
FENCER A
0
VS
FENCER B
0
π SUBSTITUTION
UPLOAD RESULT
π BOUT ENTRY
BOUT
0
VS
0
✅ ALL BOUTS COMPLETE!
⌨ ← → navigate · ↑↓ adjust left score · Enter save
LAMΓ
OFFICIAL SCORE ENTRY
SCORE ENTRY ONLY · READ ONLY ACCESS
Enter your PIN to submit pool scores
Received this file? Add #mobile to the URL
π SCORE ENTRY
π OFFICIAL · SCORE ENTRY ONLY
Checking sync...
0 pending
SELECT EVENT
SELECT POOL
SELECT BOUT
BOUT
0
VS
0
Scores saved directly to tournament
✅ ALL BOUTS IN THIS POOL COMPLETE!
localStorage: ready
π± MOBILE SCORE ENTRY SETUP
π‘ Officials need: this file + add
No app download, no internet required.
#mobile to URL + this PIN.No app download, no internet required.
π
Google Sheets Sync (optional)
Every score saved by officials is also sent to a Google Sheet in real-time
π Google Sheets Setup (one-time, 5 minutes)
- Go to sheets.google.com → create a new sheet
- Click Extensions → Apps Script
- Paste this code and click Save:
Creates 3 sheets: Scores · Pools · Standings
// ═══════════════════════════════════════════════════════════════
// LAMΓ FENCING — APPS SCRIPT BACKEND v3
// Handles: score entry · full sync · live scores · results
//
// SETUP:
// 1. Open script.google.com → New project → paste this code
// 2. Deploy → New Deployment → Web App
// 3. Execute as: Me | Who has access: Anyone
// 4. Copy the Web App URL → paste into LAMΓ app (π± Mobile Setup)
// ═══════════════════════════════════════════════════════════════
const PROP = PropertiesService.getScriptProperties();
// ── ROUTER ───────────────────────────────────────────────────────
function doPost(e) {
try {
const d = JSON.parse(e.postData.contents);
const ss = SpreadsheetApp.getActiveSpreadsheet();
switch (d.action) {
case 'score': return handleScore(ss, d);
case 'pushdata': return handlePushData(d);
case 'fullsync': return handleFullSync(ss, d);
case 'livescores': return handleLiveScores(d);
case 'publishresults': return handlePublishResults(ss, d);
case 'blogpost': return handleBlogPost(ss, d);
default: return ok('unknown action: ' + d.action);
}
} catch (err) {
return err_(err.message);
}
}
function doGet(e) {
const action = (e.parameter && e.parameter.action) || 'ping';
if (action === 'getdata') {
const data = PROP.getProperty('tournamentData') || '{}';
return jsonOut(data);
}
if (action === 'getlivescores') {
const html = PROP.getProperty('liveScoresHTML') || defaultLiveHTML();
return ContentService.createTextOutput(html)
.setMimeType(ContentService.MimeType.TEXT);
}
if (action === 'getresults') {
const html = PROP.getProperty('resultsHTML') || '<p>No results published yet.</p>';
return ContentService.createTextOutput(html)
.setMimeType(ContentService.MimeType.TEXT);
}
// ping / health check
return jsonOut(JSON.stringify({
ok: true,
msg: 'LAMΓ Backend v3 · ' + new Date().toLocaleString('en-GB')
}));
}
// ── PUSH DATA (officials load this on their phones) ──────────────
// Payload: { action:'pushdata', payload:{ tournament, events, pools, fencers } }
function handlePushData(d) {
const payload = d.payload || d;
PROP.setProperty('tournamentData', JSON.stringify(payload));
PROP.setProperty('tournamentDataUpdated', new Date().toISOString());
return ok('data stored · ' + new Date().toLocaleTimeString('en-GB'));
}
// ── SCORE (single bout result from an official's phone) ──────────
// Payload: { action:'score', tournament, event, pool, bout,
// nameA, clubA, sa, nameB, clubB, sb, winner, status, timestamp }
function handleScore(ss, d) {
var sh = ss.getSheetByName('Scores') || ss.insertSheet('Scores');
const HEADERS = [
'Key', 'Timestamp', 'Tournament', 'Event', 'Pool', 'Bout#',
'Fencer A', 'Club A', 'Score A', 'Score B', 'Club B', 'Fencer B',
'Winner', 'Status'
];
if (sh.getLastRow() === 0) sh.appendRow(HEADERS);
const key = [d.tournament, d.event, 'P' + (d.pool + 1), 'B' + (d.bout + 1)].join('|');
const ts = new Date().toLocaleString('en-GB');
const row = [
key, ts, d.tournament || '', d.event || '',
'Pool ' + (d.pool + 1), 'Bout ' + (d.bout + 1),
d.nameA || '', d.clubA || '', d.sa, d.sb,
d.clubB || '', d.nameB || '', d.winner || '', d.status || 'Scored'
];
// Update existing row or append
const vals = sh.getDataRange().getValues();
var found = -1;
for (var i = 1; i < vals.length; i++) {
if (vals[i][0] === key) { found = i + 1; break; }
}
if (found > 0) {
sh.getRange(found, 1, 1, row.length).setValues([row]);
colorScoreRow(sh, found, d.sa, d.sb);
} else {
sh.appendRow(row);
colorScoreRow(sh, sh.getLastRow(), d.sa, d.sb);
}
autoResize(sh);
return ok('score saved');
}
function colorScoreRow(sh, rowNum, sa, sb) {
sh.getRange(rowNum, 1, 1, 14).setBackground('#ffffff');
if (sa > sb) sh.getRange(rowNum, 7).setBackground('#d9ead3'); // A wins → green col 7
else if (sb > sa) sh.getRange(rowNum, 12).setBackground('#d9ead3'); // B wins → green col 12
else sh.getRange(rowNum, 7, 1, 6).setBackground('#fff2cc'); // draw → yellow
}
// ── FULL SYNC (chief pushes complete tournament snapshot) ────────
// Payload: { action:'fullsync', tournament, events, pools:[],
// standings:[{event, fencers:[{rank,name,club,V,TS,TR}]}],
// eventCount, poolCount }
function handleFullSync(ss, d) {
handlePushData(d); // store for officials
buildPoolsSheet(ss, d);
buildStandingsSheet(ss, d);
logAction(ss, 'Full Sync',
'Tournament: ' + d.tournament +
' | Events: ' + (d.eventCount || 0) +
' | Pools: ' + (d.poolCount || 0));
return ok('full sync done');
}
function buildPoolsSheet(ss, d) {
var sh = ss.getSheetByName('Pools') || ss.insertSheet('Pools');
sh.clearContents(); sh.clearFormats();
var rows = [
['LAMΓ FENCING — POOL RESULTS', '', '', '', '', '', '', '', '', ''],
['Tournament: ' + (d.tournament || ''), '', 'Date: ' + new Date().toLocaleDateString('en-GB'), '', '', '', '', '', '', ''],
[]
];
(d.pools || []).forEach(function (p) {
var fencers = p.fencers || [];
var header = ['#', 'Fencer', 'Club'];
fencers.forEach(function (_, i) { header.push(String.fromCharCode(65 + i)); });
header.push('V', 'TS', 'TR', 'Ind', 'Rank');
rows.push(['═══ POOL ' + (p.poolNum + 1) + ' — ' + p.event + ' ═══',
'', '', '', '', '', '', '', '', '']);
rows.push(header);
fencers.forEach(function (f, fi) {
var row = [fi + 1, f.name || '', f.club || ''];
fencers.forEach(function (_, gi) {
if (fi === gi) { row.push('—'); return; }
var bkey = Math.min(fi, gi) + '_' + Math.max(fi, gi);
var b = p.bouts && p.bouts[bkey];
if (b) row.push(fi < gi ? b.sa + '/' + b.sb : b.sb + '/' + b.sa);
else row.push('');
});
row.push(f.V || 0, f.TS || 0, f.TR || 0, (f.TS || 0) - (f.TR || 0), f.rank || '');
rows.push(row);
});
rows.push([]);
});
var maxCols = Math.max.apply(null, rows.map(function (r) { return r.length; })) || 10;
sh.getRange(1, 1, rows.length, maxCols).setValues(
rows.map(function (r) {
while (r.length < maxCols) r.push('');
return r.slice(0, maxCols);
})
);
// Style header rows
sh.getRange(1, 1, 1, maxCols).setFontWeight('bold')
.setBackground('#1a1a2e').setFontColor('#c8a84b');
sh.getRange(2, 1, 1, maxCols).setFontStyle('italic')
.setBackground('#0d0d18').setFontColor('#666666');
autoResize(sh);
}
function buildStandingsSheet(ss, d) {
var sh = ss.getSheetByName('Standings') || ss.insertSheet('Standings');
sh.clearContents(); sh.clearFormats();
var rows = [
['LAMΓ — POOL STANDINGS', '', '', '', '', '', ''],
['Tournament: ' + (d.tournament || ''), '', 'As of: ' + new Date().toLocaleString('en-GB'), '', '', '', ''],
[]
];
(d.standings || []).forEach(function (ev) {
rows.push(['── ' + ev.event + ' ──', '', '', '', '', '', '']);
rows.push(['Rank', 'Fencer', 'Club', 'V', 'TS', 'TR', 'Ind']);
(ev.fencers || []).forEach(function (f) {
rows.push([
f.rank || '', f.name || '', f.club || '',
f.V || 0, f.TS || 0, f.TR || 0, (f.TS || 0) - (f.TR || 0)
]);
});
rows.push([]);
});
sh.getRange(1, 1, rows.length, 7).setValues(
rows.map(function (r) {
while (r.length < 7) r.push('');
return r.slice(0, 7);
})
);
sh.getRange(1, 1, 1, 7).setFontWeight('bold')
.setBackground('#1a1a2e').setFontColor('#c8a84b');
autoResize(sh);
}
// ── LIVE SCORES HTML (for Blogspot live scores page) ─────────────
// Payload: { action:'livescores', html, title }
function handleLiveScores(d) {
PROP.setProperty('liveScoresHTML', d.html || '');
PROP.setProperty('liveScoresTitle', d.title || 'Live Scores');
PROP.setProperty('liveScoresUpdated', new Date().toISOString());
return ok('live scores updated · ' + new Date().toLocaleTimeString('en-GB'));
}
// ── PUBLISH RESULTS HTML (for Blogspot results page) ─────────────
// Payload: { action:'publishresults', html, title, tournament }
function handlePublishResults(ss, d) {
PROP.setProperty('resultsHTML', d.html || '');
PROP.setProperty('resultsTitle', d.title || 'Results');
PROP.setProperty('resultsUpdated', new Date().toISOString());
logAction(ss, 'Results Published', d.tournament || '');
return ok('results published');
}
// ── BLOGPOST proxy (legacy / fallback) ───────────────────────────
function handleBlogPost(ss, d) {
if (d.type === 'live-scores') return handleLiveScores(d);
return handlePublishResults(ss, d);
}
// ── SHARED HELPERS ───────────────────────────────────────────────
function logAction(ss, action, details) {
try {
var sh = ss.getSheetByName('Log') || ss.insertSheet('Log');
if (sh.getLastRow() === 0)
sh.appendRow(['Timestamp', 'Action', 'Details']);
sh.appendRow([new Date().toLocaleString('en-GB'), action, details]);
} catch (e) {}
}
function defaultLiveHTML() {
return '<p style="font-family:sans-serif;text-align:center;padding:40px;color:#888">' +
'No live scores yet. Check back soon!</p>';
}
function autoResize(sh) {
try { sh.autoResizeColumns(1, sh.getLastColumn()); } catch (e) {}
}
function ok(msg) {
return jsonOut(JSON.stringify({ ok: true, msg: msg }));
}
function err_(msg) {
return jsonOut(JSON.stringify({ ok: false, error: msg }));
}
function jsonOut(s) {
return ContentService.createTextOutput(s)
.setMimeType(ContentService.MimeType.JSON);
}
π Click code to copy
- Click Deploy → New Deployment → Web App
- Set Execute as: Me · Who has access: Anyone
- Click Deploy → copy the Web App URL → paste above
- From the main app: π Sync to Sheets button updates all pool data any time
π 3 sheets auto-created:
• Scores — every bout with update-in-place (no duplicates)
• Pools — full grid view with V/TS/TR/rankings
• Standings — ranked fencer list per event
• Scores — every bout with update-in-place (no duplicates)
• Pools — full grid view with V/TS/TR/rankings
• Standings — ranked fencer list per event
π± HOW OFFICIALS SCORE ON THEIR PHONE
1
No app download needed
This is a self-contained HTML file. Officials just need a phone browser — Chrome, Safari, Firefox all work. No internet required after the file is shared.
2
Share the file with officials
Send via WhatsApp, email, or AirDrop. Use the π€ Share button on the Dashboard. Officials receive the same file you're using right now.
π¬ WhatsApp
π§ Email
π‘ AirDrop
π USB / Drive
3
Official opens file → adds
#mobile to the URLIn their browser address bar, the URL ends with
.html#mobile — this activates score-entry-only mode automatically.file:///...lame-fencing.html#mobile
4
Enter PIN → Select pool → Score bouts
Officials see only their score entry screen. No access to tournament setup, fencer lists, or bracket management. Scores save instantly to localStorage.
5
Add to Home Screen (optional but recommended)
On iPhone: Safari → Share → Add to Home Screen. On Android: Chrome → ⋮ → Add to Home Screen. Launches fullscreen like a real app — no browser chrome.
π iOS Safari
π€ Android Chrome
QUICK SHARE TO OFFICIALS
π
Chief Referee PIN
Locks full app — only chief referee can access tournament management
π
Blogspot Integration
Host app · Live scores · Auto-publish results
π‘ How it works:
• π‘ Push Live Scores — Updates a live-scores post on your Blogspot every time you sync. Spectators visit your Blogspot to see current standings.
• π° Publish Results — Creates a formatted results post with medal table, pool standings, and knockout results.
• Blogger API Key — Free from Google Cloud Console. Needed only for auto-publishing.
• π‘ Push Live Scores — Updates a live-scores post on your Blogspot every time you sync. Spectators visit your Blogspot to see current standings.
• π° Publish Results — Creates a formatted results post with medal table, pool standings, and knockout results.
• Blogger API Key — Free from Google Cloud Console. Needed only for auto-publishing.
π€ SHARE & EXPORT
SCAN TO OPEN ON MOBILE
⚡ Opens the app directly — works over local Wi-Fi or when file is on a server
π Download Full App + Data
Complete HTML file with all tournament data baked in. Send via WhatsApp, Drive, email or USB — opens offline in any browser.
πΎ Export Data Only (JSON)
Lightweight data backup — import into any LAMΓ installation to restore.
π₯ Import Data
Load a previously exported JSON file to restore or continue a tournament.
π¨️
Print Results as PDF
Open Results tab → Print → Save as PDF. Share the PDF via WhatsApp, email or Drive instantly.
LAMΓ
CHIEF REFEREE ACCESS
Enter Chief Referee PIN to access full tournament management
π Officials use #mobile mode for score entry only
π₯️ This screen is for Chief Referee full access
π₯️ This screen is for Chief Referee full access
π SET CHIEF REFEREE PIN
This PIN protects full tournament management from mobile officials. Set it once and keep it with the chief referee only.