fix(qc-dash): v0.2.1 — hot-reconfig + cartes depuis api/latest
BUG1: GET /api/config retourne config courante (host/port/path/url/interval)
POST /api/config update à chaud + log "cible changée"
Page pré-remplit les inputs au chargement via loadConfig()
Toast "Appliqué" après POST réussi
BUG2: JS reécrit pour lire UNIQUEMENT api/latest (relatif)
Tous les champs mappés sur json tags Go minuscules:
gps.valid/sats/lat/lon/hdg/speed_kn, rssi, power.voltage/current_mA,
seaker.angle/dist/rx_freq, targetf.lat/lon/r95_m, deviceId, firmware.version, ip
Sparklines alimentées depuis rows[] ring-buffer backend
Plus aucun fetch direct vers ESP32
This commit is contained in:
150
main.go
150
main.go
@@ -18,7 +18,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Version = "0.1.0"
|
const Version = "0.2.1"
|
||||||
|
|
||||||
// ─── QC Thresholds (edit here) ───────────────────────────────────────────────
|
// ─── QC Thresholds (edit here) ───────────────────────────────────────────────
|
||||||
const (
|
const (
|
||||||
@@ -36,6 +36,7 @@ type Config struct {
|
|||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
Path string
|
Path string
|
||||||
|
URL string // full URL override (http or https), replaces host:port/path
|
||||||
Interval int // seconds
|
Interval int // seconds
|
||||||
CSV string
|
CSV string
|
||||||
Web int
|
Web int
|
||||||
@@ -93,6 +94,8 @@ func loadIni(path string, cfg *Config) {
|
|||||||
}
|
}
|
||||||
case "mode":
|
case "mode":
|
||||||
cfg.Mode = v
|
cfg.Mode = v
|
||||||
|
case "url":
|
||||||
|
cfg.URL = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,6 +139,8 @@ func parseFlags(cfg *Config) {
|
|||||||
}
|
}
|
||||||
case "mode":
|
case "mode":
|
||||||
cfg.Mode = val
|
cfg.Mode = val
|
||||||
|
case "url":
|
||||||
|
cfg.URL = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,8 +196,8 @@ type Telemetry struct {
|
|||||||
|
|
||||||
// Row enriched with PC timestamp
|
// Row enriched with PC timestamp
|
||||||
type Row struct {
|
type Row struct {
|
||||||
Timestamp string
|
Timestamp string `json:"Timestamp"`
|
||||||
T Telemetry
|
T Telemetry `json:"T"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Ring buffer ─────────────────────────────────────────────────────────────
|
// ─── Ring buffer ─────────────────────────────────────────────────────────────
|
||||||
@@ -368,6 +373,7 @@ type App struct {
|
|||||||
espHost string
|
espHost string
|
||||||
espPort int
|
espPort int
|
||||||
espPath string
|
espPath string
|
||||||
|
espURL string // full URL override
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) poll() {
|
func (a *App) poll() {
|
||||||
@@ -376,8 +382,14 @@ func (a *App) poll() {
|
|||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
a.mu.RLock()
|
a.mu.RLock()
|
||||||
url := fmt.Sprintf("http://%s:%d%s", a.espHost, a.espPort, a.espPath)
|
var pollURL string
|
||||||
|
if a.espURL != "" {
|
||||||
|
pollURL = a.espURL
|
||||||
|
} else {
|
||||||
|
pollURL = fmt.Sprintf("http://%s:%d%s", a.espHost, a.espPort, a.espPath)
|
||||||
|
}
|
||||||
a.mu.RUnlock()
|
a.mu.RUnlock()
|
||||||
|
url := pollURL
|
||||||
|
|
||||||
resp, err := client.Get(url)
|
resp, err := client.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -460,15 +472,19 @@ func (a *App) apiLatest(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) apiConfig(w http.ResponseWriter, r *http.Request) {
|
func (a *App) apiConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
a.mu.RLock()
|
a.mu.RLock()
|
||||||
type Out struct {
|
type Out struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Interval int `json:"interval"`
|
||||||
}
|
}
|
||||||
json.NewEncoder(w).Encode(Out{Host: a.espHost, Port: a.espPort, Path: a.espPath})
|
out := Out{Host: a.espHost, Port: a.espPort, Path: a.espPath, URL: a.espURL, Interval: a.cfg.Interval}
|
||||||
a.mu.RUnlock()
|
a.mu.RUnlock()
|
||||||
|
json.NewEncoder(w).Encode(out)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
@@ -476,11 +492,17 @@ func (a *App) apiConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Interval int `json:"interval"`
|
||||||
}
|
}
|
||||||
json.NewDecoder(r.Body).Decode(&in)
|
json.NewDecoder(r.Body).Decode(&in)
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
if in.Host != "" {
|
if in.Host != "" {
|
||||||
a.espHost = in.Host
|
a.espHost = in.Host
|
||||||
|
// switching to host mode: clear url override
|
||||||
|
if in.URL == "" {
|
||||||
|
a.espURL = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if in.Port != 0 {
|
if in.Port != 0 {
|
||||||
a.espPort = in.Port
|
a.espPort = in.Port
|
||||||
@@ -488,8 +510,14 @@ func (a *App) apiConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
if in.Path != "" {
|
if in.Path != "" {
|
||||||
a.espPath = in.Path
|
a.espPath = in.Path
|
||||||
}
|
}
|
||||||
|
if in.URL != "" {
|
||||||
|
a.espURL = in.URL
|
||||||
|
}
|
||||||
|
if in.Interval > 0 {
|
||||||
|
a.cfg.Interval = in.Interval
|
||||||
|
}
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
log.Printf("[CFG] ESP32 → %s:%d%s", a.espHost, a.espPort, a.espPath)
|
log.Printf("[CFG] cible changée → %s:%d%s (url=%q)", a.espHost, a.espPort, a.espPath, a.espURL)
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
w.Write([]byte(`{"ok":true}`))
|
w.Write([]byte(`{"ok":true}`))
|
||||||
return
|
return
|
||||||
@@ -539,6 +567,7 @@ footer{font-size:0.7rem;color:#2a4060;margin-top:8px}
|
|||||||
<input id="cfg-port" placeholder="80" style="width:60px"/>
|
<input id="cfg-port" placeholder="80" style="width:60px"/>
|
||||||
<input id="cfg-path" placeholder="/api/telemetry" style="width:140px"/>
|
<input id="cfg-path" placeholder="/api/telemetry" style="width:140px"/>
|
||||||
<button onclick="saveConfig()">Appliquer</button>
|
<button onclick="saveConfig()">Appliquer</button>
|
||||||
|
<span id="cfg-status" style="font-size:0.8rem;color:#4ade80;display:none">Appliqué</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid" id="cards">
|
<div class="grid" id="cards">
|
||||||
@@ -589,10 +618,11 @@ function cls(v,ok,warn){
|
|||||||
}
|
}
|
||||||
function setCard(id,text,cls_name){
|
function setCard(id,text,cls_name){
|
||||||
const el=document.getElementById(id);
|
const el=document.getElementById(id);
|
||||||
|
if(!el)return;
|
||||||
el.textContent=text;
|
el.textContent=text;
|
||||||
el.className='value '+cls_name;
|
el.className='value '+cls_name;
|
||||||
}
|
}
|
||||||
function fmt(v,d=1){return v==null?'–':Number(v).toFixed(d);}
|
function fmt(v,d=1){return(v==null||v===undefined)?'–':Number(v).toFixed(d);}
|
||||||
|
|
||||||
function drawSpark(id,data,color,minV,maxV){
|
function drawSpark(id,data,color,minV,maxV){
|
||||||
const canvas=document.getElementById(id);
|
const canvas=document.getElementById(id);
|
||||||
@@ -616,11 +646,20 @@ function drawSpark(id,data,color,minV,maxV){
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
function push(arr,v){arr.push(v);if(arr.length>SPARK_N)arr.shift();}
|
// Pré-chargement config au démarrage (BUG1 fix)
|
||||||
|
async function loadConfig(){
|
||||||
|
try{
|
||||||
|
const resp=await fetch('api/config');
|
||||||
|
const cfg=await resp.json();
|
||||||
|
if(cfg.host)document.getElementById('cfg-host').value=cfg.host;
|
||||||
|
if(cfg.port)document.getElementById('cfg-port').value=cfg.port;
|
||||||
|
if(cfg.path)document.getElementById('cfg-path').value=cfg.path;
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchAndUpdate(){
|
async function fetchAndUpdate(){
|
||||||
try{
|
try{
|
||||||
const resp=await fetch('/api/latest?n='+SPARK_N);
|
const resp=await fetch('api/latest?n='+SPARK_N);
|
||||||
const data=await resp.json();
|
const data=await resp.json();
|
||||||
const linked=data.linked;
|
const linked=data.linked;
|
||||||
const age=data.age_sec;
|
const age=data.age_sec;
|
||||||
@@ -637,51 +676,58 @@ async function fetchAndUpdate(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(rows.length===0)return;
|
if(rows.length===0)return;
|
||||||
|
// JSON sérialisé par Go : Row.Timestamp="Timestamp", Row.T="T"
|
||||||
|
// Telemetry fields utilisent les json tags (minuscules)
|
||||||
const r=rows[rows.length-1];
|
const r=rows[rows.length-1];
|
||||||
const t=r.T;
|
const t=r.T||{};
|
||||||
|
|
||||||
// GPS
|
// GPS — json tags: gps.valid, gps.sats, gps.lat, gps.lon, gps.hdg, gps.speed_kn, gps.status
|
||||||
const gv=t.GPS&&t.GPS.Valid===1;
|
const gps=t.gps||{};
|
||||||
setCard('c-gps-valid',gv?'FIX':'NO FIX',gv?'ok':'err');
|
const gv=gps.valid===1;
|
||||||
const sats=t.GPS?t.GPS.Sats:0;
|
setCard('c-gps-valid',gv?(gps.status||'FIX'):'NO FIX',gv?'ok':'err');
|
||||||
|
const sats=gps.sats!=null?gps.sats:0;
|
||||||
setCard('c-gps-sats',sats,sats>=THRESH_SATS_OK?'ok':sats>0?'warn':'err');
|
setCard('c-gps-sats',sats,sats>=THRESH_SATS_OK?'ok':sats>0?'warn':'err');
|
||||||
setCard('c-lat',fmt(t.GPS&&t.GPS.Lat,7),'na');
|
setCard('c-lat',fmt(gps.lat,7),'na');
|
||||||
setCard('c-lon',fmt(t.GPS&&t.GPS.Lon,7),'na');
|
setCard('c-lon',fmt(gps.lon,7),'na');
|
||||||
setCard('c-hdg',fmt(t.GPS&&t.GPS.Hdg,1),'na');
|
setCard('c-hdg',fmt(gps.hdg,1),'na');
|
||||||
const spd=t.GPS?t.GPS.SpeedKn:0;
|
setCard('c-speed',fmt(gps.speed_kn,2),'na');
|
||||||
setCard('c-speed',fmt(spd,2),'na');
|
|
||||||
|
|
||||||
// RSSI
|
// RSSI — json tag: rssi
|
||||||
const rssi=t.RSSI||0;
|
const rssi=t.rssi!=null?t.rssi:0;
|
||||||
setCard('c-rssi',rssi,cls(rssi,THRESH_RSSI_OK,THRESH_RSSI_WARN));
|
setCard('c-rssi',rssi,cls(rssi,THRESH_RSSI_OK,THRESH_RSSI_WARN));
|
||||||
|
|
||||||
// Power
|
// Power — json tags: power.voltage, power.current_mA
|
||||||
const volt=t.Power?t.Power.Voltage:0;
|
const pwr=t.power||{};
|
||||||
|
const volt=pwr.voltage!=null?pwr.voltage:0;
|
||||||
setCard('c-volt',fmt(volt,2),cls(volt,THRESH_BATT_OK,THRESH_BATT_WARN));
|
setCard('c-volt',fmt(volt,2),cls(volt,THRESH_BATT_OK,THRESH_BATT_WARN));
|
||||||
setCard('c-amp',fmt(t.Power&&t.Power.CurrentMA,0),'na');
|
setCard('c-amp',fmt(pwr.current_mA,0),'na');
|
||||||
|
|
||||||
// Seaker
|
// Seaker — json tags: seaker.angle, seaker.dist, seaker.rx_freq
|
||||||
setCard('c-sk-ang',fmt(t.Seaker&&t.Seaker.Angle,1),'na');
|
const sk=t.seaker||{};
|
||||||
setCard('c-sk-dist',fmt(t.Seaker&&t.Seaker.Dist,1),'na');
|
setCard('c-sk-ang',fmt(sk.angle,1),'na');
|
||||||
setCard('c-sk-freq',fmt(t.Seaker&&t.Seaker.RxFreq,1),'na');
|
setCard('c-sk-dist',fmt(sk.dist,1),'na');
|
||||||
|
setCard('c-sk-freq',fmt(sk.rx_freq,1),'na');
|
||||||
|
|
||||||
// Target / ROV
|
// Target / ROV — json tags: targetf.lat, targetf.lon, targetf.r95_m
|
||||||
setCard('c-rov-lat',fmt(t.TargetF&&t.TargetF.Lat,7),'na');
|
const tf=t.targetf||{};
|
||||||
setCard('c-rov-lon',fmt(t.TargetF&&t.TargetF.Lon,7),'na');
|
setCard('c-rov-lat',fmt(tf.lat,7),'na');
|
||||||
setCard('c-rov-r95',fmt(t.TargetF&&t.TargetF.R95M,2),'na');
|
setCard('c-rov-lon',fmt(tf.lon,7),'na');
|
||||||
|
setCard('c-rov-r95',fmt(tf.r95_m,2),'na');
|
||||||
|
|
||||||
setCard('c-devid',t.DeviceID||'–','na');
|
// json tags: deviceId, firmware.version, ip
|
||||||
setCard('c-fw',(t.Firmware&&t.Firmware.Version)||'–','na');
|
setCard('c-devid',t.deviceId||'–','na');
|
||||||
setCard('c-ip',t.IP||'–','na');
|
const fw=t.firmware||{};
|
||||||
|
setCard('c-fw',fw.version||'–','na');
|
||||||
|
setCard('c-ip',t.ip||'–','na');
|
||||||
|
|
||||||
// Sparklines — fill from all rows
|
// Sparklines — alimentées depuis rows (historique ring-buffer)
|
||||||
sparkData.volt.length=0;sparkData.rssi.length=0;sparkData.dist.length=0;sparkData.speed.length=0;
|
sparkData.volt.length=0;sparkData.rssi.length=0;sparkData.dist.length=0;sparkData.speed.length=0;
|
||||||
rows.forEach(row=>{
|
rows.forEach(row=>{
|
||||||
const tt=row.T;
|
const tt=row.T||{};
|
||||||
sparkData.volt.push(tt.Power?tt.Power.Voltage:0);
|
sparkData.volt.push((tt.power||{}).voltage||0);
|
||||||
sparkData.rssi.push(tt.RSSI||0);
|
sparkData.rssi.push(tt.rssi||0);
|
||||||
sparkData.dist.push(tt.Seaker?tt.Seaker.Dist:0);
|
sparkData.dist.push((tt.seaker||{}).dist||0);
|
||||||
sparkData.speed.push(tt.GPS?tt.GPS.SpeedKn:0);
|
sparkData.speed.push((tt.gps||{}).speed_kn||0);
|
||||||
});
|
});
|
||||||
|
|
||||||
drawSpark('sp-volt',sparkData.volt,'#4ade80');
|
drawSpark('sp-volt',sparkData.volt,'#4ade80');
|
||||||
@@ -696,12 +742,27 @@ async function fetchAndUpdate(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveConfig(){
|
async function saveConfig(){
|
||||||
const host=document.getElementById('cfg-host').value;
|
const host=document.getElementById('cfg-host').value.trim();
|
||||||
const port=parseInt(document.getElementById('cfg-port').value)||0;
|
const port=parseInt(document.getElementById('cfg-port').value)||0;
|
||||||
const path=document.getElementById('cfg-path').value;
|
const path=document.getElementById('cfg-path').value.trim();
|
||||||
await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({host,port,path})});
|
try{
|
||||||
|
const resp=await fetch('api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({host,port,path})});
|
||||||
|
const ok=await resp.json();
|
||||||
|
const st=document.getElementById('cfg-status');
|
||||||
|
if(ok.ok){
|
||||||
|
st.textContent='Appliqué';st.style.color='#4ade80';
|
||||||
|
}else{
|
||||||
|
st.textContent='Erreur';st.style.color='#f87171';
|
||||||
|
}
|
||||||
|
st.style.display='inline';
|
||||||
|
setTimeout(()=>{st.style.display='none';},3000);
|
||||||
|
}catch(e){
|
||||||
|
const st=document.getElementById('cfg-status');
|
||||||
|
st.textContent='Erreur: '+e.message;st.style.color='#f87171';st.style.display='inline';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadConfig();
|
||||||
setInterval(fetchAndUpdate,1000);
|
setInterval(fetchAndUpdate,1000);
|
||||||
fetchAndUpdate();
|
fetchAndUpdate();
|
||||||
</script>
|
</script>
|
||||||
@@ -734,6 +795,7 @@ func main() {
|
|||||||
espHost: cfg.Host,
|
espHost: cfg.Host,
|
||||||
espPort: cfg.Port,
|
espPort: cfg.Port,
|
||||||
espPath: cfg.Path,
|
espPath: cfg.Path,
|
||||||
|
espURL: cfg.URL,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start poller or receiver
|
// Start poller or receiver
|
||||||
|
|||||||
Reference in New Issue
Block a user