1+ <!DOCTYPE html>
2+ < html lang ="en ">
3+ < head >
4+ < meta charset ="UTF-8 ">
5+ < meta name ="viewport " content ="width=device-width, initial-scale=1.0 ">
6+ < title > Mazduino Firmware — INI Files</ title >
7+ < style >
8+ : root {
9+ --bg : # 0d1117 ;
10+ --surface : # 161b22 ;
11+ --border : # 30363d ;
12+ --text : # c9d1d9 ;
13+ --muted : # 8b949e ;
14+ --accent : # 58a6ff ;
15+ --green : # 3fb950 ;
16+ --yellow : # d29922 ;
17+ }
18+ * { box-sizing : border-box; margin : 0 ; padding : 0 ; }
19+ body {
20+ font-family : -apple-system, BlinkMacSystemFont, 'Segoe UI' , Helvetica, Arial, sans-serif;
21+ background : var (--bg );
22+ color : var (--text );
23+ line-height : 1.6 ;
24+ min-height : 100vh ;
25+ }
26+ .container { max-width : 860px ; margin : 0 auto; padding : 32px 20px ; }
27+
28+ header { margin-bottom : 32px ; }
29+ header h1 { font-size : 1.5rem ; font-weight : 600 ; margin-bottom : 4px ; }
30+ header p { color : var (--muted ); font-size : 0.9rem ; }
31+ header a { color : var (--accent ); text-decoration : none; }
32+ header a : hover { text-decoration : underline; }
33+
34+ .filters {
35+ display : flex; gap : 8px ; flex-wrap : wrap;
36+ margin-bottom : 24px ;
37+ }
38+ .filter-btn {
39+ padding : 4px 14px ; border-radius : 20px ;
40+ border : 1px solid var (--border );
41+ background : transparent; color : var (--text );
42+ cursor : pointer; font-size : 0.82rem ;
43+ transition : background 0.1s , border-color 0.1s ;
44+ }
45+ .filter-btn : hover { border-color : var (--accent ); }
46+ .filter-btn .active {
47+ background : var (--accent ); border-color : var (--accent );
48+ color : # 0d1117 ; font-weight : 600 ;
49+ }
50+
51+ # status { color : var (--muted ); font-size : 0.85rem ; margin-bottom : 20px ; min-height : 20px ; }
52+
53+ .board-section { margin-bottom : 32px ; }
54+ .board-header {
55+ display : flex; align-items : baseline; gap : 10px ;
56+ padding-bottom : 8px ; margin-bottom : 0 ;
57+ border-bottom : 1px solid var (--border );
58+ }
59+ .board-name { font-size : 1rem ; font-weight : 600 ; }
60+ .board-count { font-size : 0.8rem ; color : var (--muted ); }
61+
62+ table { width : 100% ; border-collapse : collapse; font-size : 0.855rem ; }
63+ th {
64+ text-align : left; color : var (--muted ); font-weight : 500 ;
65+ padding : 8px 10px 6px 0 ; white-space : nowrap;
66+ }
67+ td { padding : 9px 10px 9px 0 ; border-top : 1px solid var (--border ); vertical-align : middle; }
68+ tr : last-child td { border-bottom : 1px solid var (--border ); }
69+
70+ .badge {
71+ display : inline-block; padding : 1px 8px ; border-radius : 10px ;
72+ font-size : 0.75rem ; border : 1px solid var (--border );
73+ background : var (--surface ); color : var (--muted );
74+ white-space : nowrap;
75+ }
76+ .badge-main { border-color : var (--green ); color : var (--green ); }
77+ .badge-feature { border-color : var (--yellow ); color : var (--yellow ); }
78+
79+ .hash { font-family : 'SFMono-Regular' , Consolas, monospace; color : var (--muted ); font-size : 0.8rem ; }
80+ .latest-label {
81+ font-size : 0.72rem ; padding : 1px 6px ; border-radius : 8px ;
82+ background : rgba (63 , 185 , 80 , 0.1 ); border : 1px solid var (--green );
83+ color : var (--green ); margin-left : 6px ; vertical-align : middle;
84+ }
85+
86+ a { color : var (--accent ); text-decoration : none; }
87+ a : hover { text-decoration : underline; }
88+
89+ .error { color : # f85149 ; font-size : 0.9rem ; }
90+ .empty { color : var (--muted ); font-size : 0.9rem ; padding : 16px 0 ; }
91+
92+ footer {
93+ margin-top : 48px ; padding-top : 20px ;
94+ border-top : 1px solid var (--border );
95+ color : var (--muted ); font-size : 0.8rem ;
96+ display : flex; justify-content : space-between; flex-wrap : wrap; gap : 8px ;
97+ }
98+ </ style >
99+ </ head >
100+ < body >
101+ < div class ="container ">
102+ < header >
103+ < h1 > Mazduino Firmware</ h1 >
104+ < p >
105+ TunerStudio INI configuration files —
106+ < a href ="https://github.com/mazduino/mazduino-fw/releases " target ="_blank "> Releases & firmware binaries →</ a >
107+ </ p >
108+ </ header >
109+
110+ < div id ="filters " class ="filters "> </ div >
111+ < div id ="status "> Loading…</ div >
112+ < div id ="content "> </ div >
113+
114+ < footer >
115+ < span >
116+ < a href ="https://github.com/mazduino/mazduino-fw " target ="_blank "> mazduino/mazduino-fw</ a >
117+ ·
118+ < a href ="https://www.mazduino.com/ " target ="_blank "> mazduino.com</ a >
119+ </ span >
120+ < span id ="last-updated "> </ span >
121+ </ footer >
122+ </ div >
123+
124+ < script >
125+ const REPO = 'mazduino/mazduino-fw' ;
126+ const BASE_URL = 'https://mazduino.github.io/mazduino-fw' ;
127+
128+ let allFiles = [ ] ;
129+ let activeBoard = 'all' ;
130+
131+ async function fetchFiles ( ) {
132+ const url = `https://api.github.com/repos/${ REPO } /git/trees/gh-pages?recursive=1` ;
133+ const res = await fetch ( url , { headers : { Accept : 'application/vnd.github+json' } } ) ;
134+ if ( ! res . ok ) throw new Error ( `GitHub API responded with ${ res . status } ` ) ;
135+ const data = await res . json ( ) ;
136+
137+ if ( data . truncated ) {
138+ console . warn ( 'GitHub tree response truncated — some files may be missing' ) ;
139+ }
140+
141+ return data . tree
142+ . filter ( item => item . type === 'blob' && item . path . startsWith ( 'ini/' ) && item . path . endsWith ( '.ini' ) )
143+ . map ( item => {
144+ // ini/<branch>/<year>/<month>/<day>/<target>/<hash>.ini
145+ const parts = item . path . replace ( / ^ i n i \/ / , '' ) . split ( '/' ) ;
146+ if ( parts . length < 6 ) return null ;
147+ const [ branch , year , month , day , target , filename ] = parts ;
148+ const hash = filename . replace ( '.ini' , '' ) ;
149+ return { path : item . path , branch, year, month, day, target, hash } ;
150+ } )
151+ . filter ( Boolean ) ;
152+ }
153+
154+ function groupByBoard ( files ) {
155+ const groups = { } ;
156+ for ( const f of files ) {
157+ if ( ! groups [ f . target ] ) groups [ f . target ] = [ ] ;
158+ groups [ f . target ] . push ( f ) ;
159+ }
160+ for ( const target of Object . keys ( groups ) ) {
161+ groups [ target ] . sort ( ( a , b ) => {
162+ const da = `${ a . year } ${ a . month . padStart ( 2 , '0' ) } ${ a . day . padStart ( 2 , '0' ) } ` ;
163+ const db = `${ b . year } ${ b . month . padStart ( 2 , '0' ) } ${ b . day . padStart ( 2 , '0' ) } ` ;
164+ if ( db !== da ) return db . localeCompare ( da ) ;
165+ // same date: main branch first
166+ if ( a . branch === 'main' && b . branch !== 'main' ) return - 1 ;
167+ if ( b . branch === 'main' && a . branch !== 'main' ) return 1 ;
168+ return a . branch . localeCompare ( b . branch ) ;
169+ } ) ;
170+ }
171+ return groups ;
172+ }
173+
174+ function branchBadge ( branch ) {
175+ const cls = branch === 'main' ? 'badge badge-main'
176+ : branch . startsWith ( 'feature' ) ? 'badge badge-feature'
177+ : 'badge' ;
178+ return `<span class="${ cls } ">${ escHtml ( branch ) } </span>` ;
179+ }
180+
181+ function escHtml ( s ) {
182+ return s . replace ( / & / g, '&' ) . replace ( / < / g, '<' ) . replace ( / > / g, '>' ) ;
183+ }
184+
185+ function renderFilters ( boards ) {
186+ const div = document . getElementById ( 'filters' ) ;
187+ div . innerHTML = '' ;
188+
189+ const all = document . createElement ( 'button' ) ;
190+ all . className = 'filter-btn' + ( activeBoard === 'all' ? ' active' : '' ) ;
191+ all . textContent = 'All boards' ;
192+ all . onclick = ( ) => setBoard ( 'all' ) ;
193+ div . appendChild ( all ) ;
194+
195+ for ( const board of boards ) {
196+ const btn = document . createElement ( 'button' ) ;
197+ btn . className = 'filter-btn' + ( activeBoard === board ? ' active' : '' ) ;
198+ btn . textContent = board ;
199+ btn . onclick = ( ) => setBoard ( board ) ;
200+ div . appendChild ( btn ) ;
201+ }
202+ }
203+
204+ function setBoard ( board ) {
205+ activeBoard = board ;
206+ render ( ) ;
207+ }
208+
209+ function render ( ) {
210+ const groups = groupByBoard ( allFiles ) ;
211+ const boards = Object . keys ( groups ) . sort ( ) ;
212+ renderFilters ( boards ) ;
213+
214+ const content = document . getElementById ( 'content' ) ;
215+ content . innerHTML = '' ;
216+
217+ const visible = activeBoard === 'all' ? boards : boards . filter ( b => b === activeBoard ) ;
218+
219+ if ( visible . length === 0 ) {
220+ content . innerHTML = '<p class="empty">No INI files found.</p>' ;
221+ return ;
222+ }
223+
224+ for ( const board of visible ) {
225+ const files = groups [ board ] ;
226+ const section = document . createElement ( 'div' ) ;
227+ section . className = 'board-section' ;
228+
229+ let rows = '' ;
230+ files . forEach ( ( f , idx ) => {
231+ const date = `${ f . year } -${ f . month . padStart ( 2 , '0' ) } -${ f . day . padStart ( 2 , '0' ) } ` ;
232+ const url = `${ BASE_URL } /${ f . path } ` ;
233+ const filename = `${ f . hash } .ini` ;
234+ const latestTag = idx === 0 ? '<span class="latest-label">latest</span>' : '' ;
235+ rows += `<tr>
236+ <td>${ date } ${ latestTag } </td>
237+ <td>${ branchBadge ( f . branch ) } </td>
238+ <td><span class="hash">${ f . hash . slice ( 0 , 8 ) } </span></td>
239+ <td><a href="${ url } ">${ escHtml ( filename ) } </a></td>
240+ </tr>` ;
241+ } ) ;
242+
243+ section . innerHTML = `
244+ <div class="board-header">
245+ <span class="board-name">${ escHtml ( board ) } </span>
246+ <span class="board-count">${ files . length } build${ files . length !== 1 ? 's' : '' } </span>
247+ </div>
248+ <table>
249+ <thead><tr><th>Date</th><th>Branch</th><th>Build hash</th><th>Download</th></tr></thead>
250+ <tbody>${ rows } </tbody>
251+ </table>` ;
252+ content . appendChild ( section ) ;
253+ }
254+ }
255+
256+ ( async ( ) => {
257+ try {
258+ allFiles = await fetchFiles ( ) ;
259+ const count = allFiles . length ;
260+ const latestDate = allFiles . length
261+ ? [ ...allFiles ] . sort ( ( a , b ) => {
262+ const da = `${ a . year } ${ a . month } ${ a . day } ` ;
263+ const db = `${ b . year } ${ b . month } ${ b . day } ` ;
264+ return db . localeCompare ( da ) ;
265+ } ) [ 0 ]
266+ : null ;
267+
268+ document . getElementById ( 'status' ) . textContent =
269+ `${ count } INI file${ count !== 1 ? 's' : '' } available` ;
270+
271+ if ( latestDate ) {
272+ document . getElementById ( 'last-updated' ) . textContent =
273+ `Last build: ${ latestDate . year } -${ latestDate . month . padStart ( 2 , '0' ) } -${ latestDate . day . padStart ( 2 , '0' ) } ` ;
274+ }
275+
276+ render ( ) ;
277+ } catch ( e ) {
278+ document . getElementById ( 'status' ) . textContent = '' ;
279+ document . getElementById ( 'content' ) . innerHTML =
280+ `<p class="error">Could not load file list: ${ e . message } </p>
281+ <p style="color:var(--muted);font-size:0.85rem;margin-top:8px;">
282+ You can browse INI files directly at
283+ <a href="https://github.com/mazduino/mazduino-fw/releases">GitHub Releases</a>.
284+ </p>` ;
285+ }
286+ } ) ( ) ;
287+ </ script >
288+ </ body >
289+ </ html >
0 commit comments