feat: publish CodexPi contest snapshot
Some checks are pending
Qualité / tests (push) Waiting to run

This commit is contained in:
CodexPi
2026-11-14 20:23:59 +01:00
commit 995b17a6e2
295 changed files with 36803 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -0,0 +1,7 @@
export function relativePointerPosition(pointer,dx,dy,scaleX,scaleY,speed,width,height){
const clamp=(value,min,max)=>Math.min(max,Math.max(min,value))
return {
x:clamp(pointer.x+dx*scaleX*speed,0,Math.max(0,width-1)),
y:clamp(pointer.y+dy*scaleY*speed,0,Math.max(0,height-1)),
}
}

View File

@@ -0,0 +1,154 @@
import UI from '/browser/app/ui.js'
import {relativePointerPosition} from '/browser-touchpad-math.js'
const POINTER_SPEED=1.35
const MOVE_THRESHOLD=6
const LONG_PRESS_DELAY=560
const DOUBLE_TAP_DELAY=320
const SCROLL_STEP=18
let installedCanvas=null
let touchSurface=null
let pointer={x:0,y:0}
let primaryTouch=null
let origin=null
let lastPoint=null
let moved=false
let longPressed=false
let dragging=false
let longPressTimer=null
let lastTapAt=0
let secondTap=false
let scrollY=null
let scrollRemainder=0
function rfb(){return UI.rfb}
function canvas(){return rfb()?._canvas||null}
function bounds(){const target=canvas();return target?{target,rect:target.getBoundingClientRect()}:null}
function syncPointer(){
const remote=rfb()?._mousePos
const target=canvas()
if(Number.isFinite(remote?.x)&&Number.isFinite(remote?.y)){pointer={x:remote.x,y:remote.y}}
else if(target){pointer={x:target.width/2,y:target.height/2}}
}
function movePointer(dx,dy){
const current=rfb(),geometry=bounds()
if(!current||!geometry||!geometry.rect.width||!geometry.rect.height)return
const scaleX=geometry.target.width/geometry.rect.width
const scaleY=geometry.target.height/geometry.rect.height
pointer=relativePointerPosition(pointer,dx,dy,scaleX,scaleY,POINTER_SPEED,geometry.target.width,geometry.target.height)
current._handleMouseMove(pointer.x,pointer.y)
current._cursor?.move(geometry.rect.left+pointer.x/scaleX,geometry.rect.top+pointer.y/scaleY)
}
function click(mask=1){
const current=rfb()
if(!current)return
syncPointer()
current._handleMouseButton(pointer.x,pointer.y,mask)
current._handleMouseButton(pointer.x,pointer.y,0)
}
function beginDrag(){
if(dragging||!rfb())return
dragging=true
rfb()._handleMouseButton(pointer.x,pointer.y,1)
}
function endDrag(){
if(!dragging||!rfb())return
rfb()._handleMouseButton(pointer.x,pointer.y,0)
dragging=false
}
function clearLongPress(){if(longPressTimer!==null){clearTimeout(longPressTimer);longPressTimer=null}}
function touchAverage(touches){let total=0;for(const touch of touches)total+=touch.clientY;return touches.length?total/touches.length:0}
function wheel(direction){
const current=rfb()
if(!current)return
const mask=direction>0?8:16
current._handleMouseButton(pointer.x,pointer.y,mask)
current._handleMouseButton(pointer.x,pointer.y,0)
}
function consume(event){event.preventDefault();event.stopImmediatePropagation()}
function onTouchStart(event){
consume(event)
canvas()?.focus()
if(event.touches.length>=2){
clearLongPress();endDrag();primaryTouch=null;scrollY=touchAverage(event.touches);scrollRemainder=0;return
}
const touch=event.changedTouches[0]
if(!touch)return
syncPointer()
primaryTouch=touch.identifier
origin={x:touch.clientX,y:touch.clientY}
lastPoint={...origin}
moved=false
longPressed=false
secondTap=Date.now()-lastTapAt<DOUBLE_TAP_DELAY
clearLongPress()
if(!secondTap)longPressTimer=setTimeout(()=>{longPressed=true;click(4);navigator.vibrate?.(25)},LONG_PRESS_DELAY)
}
function onTouchMove(event){
consume(event)
if(event.touches.length>=2){
clearLongPress()
const next=touchAverage(event.touches)
if(scrollY!==null){
scrollRemainder+=next-scrollY
while(Math.abs(scrollRemainder)>=SCROLL_STEP){const direction=Math.sign(scrollRemainder);wheel(direction);scrollRemainder-=direction*SCROLL_STEP}
}
scrollY=next
return
}
const touch=[...event.touches].find(item=>item.identifier===primaryTouch)
if(!touch||!lastPoint||!origin)return
const total=Math.hypot(touch.clientX-origin.x,touch.clientY-origin.y)
if(total>MOVE_THRESHOLD){moved=true;clearLongPress();if(secondTap)beginDrag()}
movePointer(touch.clientX-lastPoint.x,touch.clientY-lastPoint.y)
lastPoint={x:touch.clientX,y:touch.clientY}
}
function onTouchEnd(event){
consume(event)
if(event.touches.length>=2){scrollY=touchAverage(event.touches);return}
if(event.touches.length===1){
scrollY=null
const touch=event.touches[0]
primaryTouch=touch.identifier;origin={x:touch.clientX,y:touch.clientY};lastPoint={...origin};return
}
clearLongPress();endDrag()
if(!moved&&!longPressed){click(1);lastTapAt=Date.now()}else if(moved){lastTapAt=0}
primaryTouch=null;origin=null;lastPoint=null;scrollY=null;scrollRemainder=0;secondTap=false
}
function onTouchCancel(event){
consume(event);clearLongPress();endDrag()
primaryTouch=null;origin=null;lastPoint=null;scrollY=null;scrollRemainder=0;secondTap=false;moved=false
}
function installTouchSurface(target){
if(!touchSurface){
touchSurface=document.createElement('div')
touchSurface.id='codexpi-relative-touch-surface'
touchSurface.setAttribute('aria-label','Pavé tactile relatif')
// noVNC draws its software cursor at z-index 65535. Stay immediately
// below it so touches hit this surface while the pointer remains visible.
Object.assign(touchSurface.style,{position:'fixed',zIndex:'65534',background:'transparent',touchAction:'none',userSelect:'none'})
touchSurface.addEventListener('touchstart',onTouchStart,{capture:true,passive:false})
touchSurface.addEventListener('touchmove',onTouchMove,{capture:true,passive:false})
touchSurface.addEventListener('touchend',onTouchEnd,{capture:true,passive:false})
touchSurface.addEventListener('touchcancel',onTouchCancel,{capture:true,passive:false})
document.body.appendChild(touchSurface)
}
const rect=target.getBoundingClientRect()
Object.assign(touchSurface.style,{left:`${rect.left}px`,top:`${rect.top}px`,width:`${rect.width}px`,height:`${rect.height}px`,display:rect.width&&rect.height?'block':'none'})
}
function install(){
const target=canvas()
if(!target){if(touchSurface)touchSurface.style.display='none';return}
target.style.touchAction='none'
target.style.userSelect='none'
installTouchSurface(target)
if(target!==installedCanvas){
installedCanvas=target
syncPointer()
}
}
window.codexpiTouchpad={click,install}
window.setInterval(install,500)
install()

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect width="512" height="512" rx="112" fill="#111316"/>
<rect x="74" y="74" width="364" height="364" rx="92" fill="#e8e9e4"/>
<path d="M173 286c0-61 36-101 88-101 34 0 62 16 78 44l-45 26c-8-15-19-22-34-22-23 0-39 20-39 53 0 34 16 54 40 54 16 0 28-8 35-24l46 24c-16 31-46 49-82 49-52 0-87-40-87-103Z" fill="#111316"/>
<circle cx="354" cy="167" r="22" fill="#5ab978"/>
</svg>

After

Width:  |  Height:  |  Size: 446 B

View File

@@ -0,0 +1,16 @@
{
"name": "CodexPi",
"short_name": "CodexPi",
"description": "Atelier Codex privé hébergé sur Raspberry Pi",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#0b0c0e",
"theme_color": "#0b0c0e",
"orientation": "any",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
{ "src": "/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}

58
frontend/public/sw.js Normal file
View File

@@ -0,0 +1,58 @@
const CACHE = 'codexpi-shell-v18'
const SHELL = ['/', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/icon-512.png', '/icons/apple-touch-icon.png']
self.addEventListener('install', event => {
event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(SHELL)).then(() => self.skipWaiting()))
})
self.addEventListener('activate', event => {
event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(key => key !== CACHE).map(key => caches.delete(key)))).then(() => self.clients.claim()))
})
self.addEventListener('fetch', event => {
const request = event.request
const url = new URL(request.url)
if (request.method !== 'GET' || url.origin !== location.origin || url.pathname.startsWith('/api/') || url.pathname.startsWith('/ws/')) return
if (url.pathname === '/version.json' || url.pathname === '/sw.js') {
event.respondWith(fetch(request, {cache: 'no-store'}))
return
}
if (request.mode === 'navigate') {
event.respondWith(fetch(request).then(response => {
const copy = response.clone()
caches.open(CACHE).then(cache => cache.put('/', copy))
return response
}).catch(() => caches.match('/')))
return
}
event.respondWith(caches.match(request).then(cached => cached || fetch(request).then(response => {
if (response.ok) caches.open(CACHE).then(cache => cache.put(request, response.clone()))
return response
})))
})
self.addEventListener('push', event => {
let message = {}
try { message = event.data ? event.data.json() : {} } catch {}
event.waitUntil(self.registration.showNotification(message.title || 'Tâche CodexPi terminée', {
body: message.body || 'Ouvrez CodexPi pour consulter le résultat.',
icon: '/icons/icon-192.png', badge: '/icons/icon-192.png',
tag: message.tag || 'codexpi-task', data: {url: message.url || '/'},
}))
})
self.addEventListener('notificationclick', event => {
event.notification.close()
event.waitUntil((async () => {
const target = new URL(event.notification.data?.url || '/', self.location.origin)
if (target.origin !== self.location.origin) return
const windows = await self.clients.matchAll({type: 'window', includeUncontrolled: true})
for (const client of windows) {
if (new URL(client.url).origin === target.origin && 'navigate' in client) {
const navigated = await client.navigate(target.href)
if (navigated) { await navigated.focus(); return }
}
}
await self.clients.openWindow(target.href)
})())
})