diff --git a/pigway_frame_flow/web/app.js b/pigway_frame_flow/web/app.js
index c870690..a40dfaa 100644
--- a/pigway_frame_flow/web/app.js
+++ b/pigway_frame_flow/web/app.js
@@ -118,7 +118,7 @@ async function templateEditor(selected){
const reset=document.createElement('button');reset.id='reset-template';reset.textContent=t('resetTemplate');$('#save-template').after(reset);bind('reset-template',()=>confirmDialog(t('resetConfirm'),async()=>{await api('/api/v1/templates/'+selected.id+'/config',{method:'DELETE'});await navigate();}));
const save=$('#save-template');const oldSave=save.onclick;save.onclick=async e=>{if([...panel.querySelectorAll('textarea'),raw].some(x=>!x.checkValidity())){toast(t('INVALID_REQUEST'));return;}await oldSave(e);};
}
-function accountSettings(){const section=$('#logout').parentElement;const form=document.createElement('form');form.id='password-form';form.innerHTML=`
${t('changePassword')} ${t('currentPassword')}
${t('newPassword')}
${t('changePassword')} `;section.append(form);form.onsubmit=busy(async e=>{e.preventDefault();await api('/api/v1/auth/password',{method:'PUT',body:{current_password:$('#current-password').value,new_password:$('#new-password').value}});forgetLogin();shell();login();toast(t('passwordChanged'));});}
+function accountSettings(){const section=$('#logout').parentElement;const form=document.createElement('form');form.id='password-form';form.innerHTML=`${t('changePassword')} ${t('changePassword')} `;section.append(form);form.onsubmit=busy(async e=>{e.preventDefault();await api('/api/v1/auth/password',{method:'PUT',body:{current_password:$('#current-password').value,new_password:$('#new-password').value}});forgetLogin();shell();login();toast(t('passwordChanged'));});}
const views={
studio:()=>layoutStudioPage(),
connections:()=>connectionsPage(),
@@ -144,7 +144,7 @@ const views={
async templates(){state.templates=await api('/api/v1/templates');const selected=state.templates.find(x=>x.id===state.selectedTemplate)||state.templates[0];state.selectedTemplate=selected.id;if(state.pendingTemplateData?.template===selected.id){selected.defaults=state.pendingTemplateData.data;state.initialProfile=state.pendingTemplateData.profile;state.pendingTemplateData=null;}state.templatePreview=null;$('#main').innerHTML=notice()+`${t('templateHint')}
${t('templateSteps')}
${state.templates.map((x,i)=>`
${{'daily-poetry':'詩','calendar':'▦','daily-dashboard':'☀','photo-text':'▣','quote':'❝','system-alert':'◒','weather':'☁'}[x.id]||'▦'}
${esc(x.name.split(' / ')[state.lang==='zh'?1:0]||x.name)} 1200 × 1600 · ${esc(x.id)} `).join('')}
${t('templateData')} ${t('templateGuideTitle')}
${t({'photo-text':'guidePhotoText','daily-dashboard':'guideDashboard',weather:'guideWeather',calendar:'guideCalendar',quote:'guideQuote','daily-poetry':'guidePoetry','system-alert':'guideAlert'}[selected.id]||'templateContentHelp')}
${['weather','daily-dashboard'].includes(selected.id)?`${t('templateWeatherHelp')}
`:''}${t('profile')} ${t('color')} ${t('grayscale')} ${t('helpProfile')}
${t('templateActionHelp')}
${t('templateAutomationHelp')}
${buttons('render-template','render',true)}${buttons('save-template','saveTemplate')}
${t('preview')} ${t('templatePreviewHint')}
${t('display')} `;document.querySelectorAll('[data-template]').forEach(b=>{b.onclick=()=>{state.selectedTemplate=b.dataset.template;navigate(['collage','poster'].includes(b.dataset.template)?'studio':undefined);};b.onkeydown=e=>{if(e.key==='Enter')b.click();};});bind('render-template',async()=>{const content={type:'template',template:selected.id,data:JSON.parse($('#template-data').value),profile:$('#template-profile').value};if([...document.querySelectorAll('#template-fields textarea'),$('#template-data')].some(x=>!x.checkValidity())){toast(t('INVALID_REQUEST'));return;}const id=await accepted('/api/v1/render',content);$('#template-preview').textContent=t('previewLoading');const op=await waitOperation(id);state.templatePreview=op.result.asset_id;$('#template-preview').className='';$('#template-preview').innerHTML=' ';await blobImage($('#template-image'),op.result.url);$('#display-template').disabled=!liveWriteAllowed();});bind('save-template',async()=>{await api('/api/v1/templates/'+selected.id+'/config',{method:'PUT',body:{data:JSON.parse($('#template-data').value)}});toast(t('done'));});$('#template-data').oninput=()=>$('#display-template').disabled=true;$('#template-profile').onchange=()=>$('#display-template').disabled=true;bind('display-template',()=>displayDialog({type:'image',asset_id:state.templatePreview}));await templateEditor(selected);if(state.initialProfile){$('#template-profile').value=state.initialProfile;state.initialProfile=null;}},
async playlists(epoch){const playlists=await api('/api/v1/playlists');if(epoch!==state.epoch)return;$('#main').innerHTML=notice()+`${t('playlistNotice')}
${t('playlists')} ${buttons('new-playlist','newPlaylist',true)}${playlists.map((p,i)=>`
${t(p.enabled?'enabled':'disabled')} ${esc(p.name)} ${p.image_ids.length} ${t('count')} · ${p.interval_minutes} ${t('minutes')} · ${t(p.mode)}
${esc(p.image_ids.join(' → '))}
${t('lastScheduled')}: ${p.position>=0?esc(p.image_ids[p.position]):'—'} ${t('edit')} ${t('remove')}
`).join('')||`
${t('noTasks')}
`}
`;bind('new-playlist',()=>playlistEditor());document.querySelectorAll('[data-pl-edit]').forEach(b=>b.onclick=()=>playlistEditor(playlists[b.dataset.plEdit]));document.querySelectorAll('[data-pl-delete]').forEach(b=>b.onclick=()=>confirmDialog(t('removeConfirm'),async()=>{await api('/api/v1/playlists/'+playlists[b.dataset.plDelete].id,{method:'DELETE'});await navigate();}));},
async history(epoch){const ops=await api('/api/v1/operations');if(epoch!==state.epoch)return;$('#main').innerHTML=`
${t('history')} ${buttons('history-refresh','refresh')}${ops.some(x=>x.status==='UNKNOWN')?`${t('unknown')}
`:''}${ops.length?`
${['created','action','status','result','client','requestId'].map(x=>`${t(x)} `).join('')} ${ops.map((o,i)=>`${fmt(o.created_at)} ${esc(operationActionLabel(o.action))}${esc(deviceNameForId(o.device_id))} ${t(o.status)} ${o.status==='UNKNOWN'?`${t('reconcile')}
`:''}${esc(o.result?.image_id||o.result?.asset_id||operationErrorLabel(o.error)||'—')} ${esc(operationClientLabel(o.client_id))} ${esc(o.request_id)} ${esc(o.operation_id)} `).join('')}
`:`
${t('noHistory')}
`}
`;bind('history-refresh',()=>navigate());document.querySelectorAll('[data-reconcile]').forEach(b=>b.onclick=busy(async()=>{const o=ops[b.dataset.reconcile];const r=await api('/api/v1/operations/'+o.operation_id+'/reconcile',{method:'POST',body:{acknowledge:false}});showModal(`${t('reconcile')} ${t('current')}: ${esc(r.current_image_id)}
${t('result')}: ${esc(r.expected_image_id)}
${t('unknown')}
${buttons('modal-cancel','cancel')}${r.resolved?'':buttons('acknowledge','acknowledge')}
`);bind('acknowledge',async()=>{await api('/api/v1/operations/'+o.operation_id+'/reconcile',{method:'POST',body:{acknowledge:true}});$('#modal').close();await navigate();});}));},
- async settings(){state.settings=await api('/api/v1/settings');const s=state.settings;$('#main').innerHTML=notice()+`${t('settings')} ${t('theme')} ${['system','light','dark'].map(x=>`${t(x==='system'?'systemTheme':x)} `).join('')}
${t('language')} 简体中文 English
${t('tokenHint')}
${buttons('logout','logout')}${t('auth')} ${detail('version',s.version)}${detail('writeProtection',t(!s.write_enabled?'yes':'no'))}${detail('keyConfigured',t(s.api_key_configured?'yes':'no'))}${detail('timezone',s.timezone)}
${t('refreshInterval')}
${t('apiDocs')} → GET /openapi.json · /api/v1
`;$('#theme-select').onchange=()=>{state.theme=$('#theme-select').value;localStorage.setItem('ff-theme',state.theme);applyTheme();};$('#language-select').onchange=()=>{state.lang=$('#language-select').value;localStorage.setItem('ff-language',state.lang);navigate();};bind('logout',async()=>{await api('/api/v1/auth/logout',{method:'POST'});forgetLogin();shell();login();});accountSettings();}
+ async settings(){state.settings=await api('/api/v1/settings');const s=state.settings;$('#main').innerHTML=notice()+`${t('settings')} ${t('tokenHint')}
${buttons('logout','logout')}${t('auth')} ${detail('version',s.version)}${detail('writeProtection',t(!s.write_enabled?'yes':'no'))}${detail('keyConfigured',t(s.api_key_configured?'yes':'no'))}${detail('timezone',s.timezone)}
${t('refreshInterval')}
${t('apiDocs')} → GET /openapi.json · /api/v1
`;$('#theme-select').onchange=()=>{state.theme=$('#theme-select').value;localStorage.setItem('ff-theme',state.theme);applyTheme();};$('#language-select').onchange=()=>{state.lang=$('#language-select').value;localStorage.setItem('ff-language',state.lang);navigate();};bind('logout',async()=>{await api('/api/v1/auth/logout',{method:'POST'});forgetLogin();shell();login();});accountSettings();}
};
function jobPayload(j,changes={}){return {name:j.name,enabled:j.enabled,device_id:j.device_id,schedule:j.schedule,source:j.source,display_duration:j.display_duration,restore_previous:j.restore_previous,priority:j.priority,cleanup_unstarred:j.cleanup_unstarred||false,...changes};}
async function playlistEditor(p){p=p||{name:'',image_ids:[],interval_minutes:60,mode:'queue',enabled:false};showModal(`${t('playlists')} `);$('#playlist-form').onsubmit=busy(async e=>{e.preventDefault();await api('/api/v1/playlists'+(p.id?'/'+p.id:''),{method:p.id?'PUT':'POST',body:{name:$('#playlist-name').value,device_id:p.device_id||state.device,image_ids:$('#playlist-images').value.split(',').map(x=>Number(x.trim())),interval_minutes:Number($('#playlist-interval').value),mode:$('#playlist-mode').value,enabled:$('#playlist-enabled').checked}});$('#modal').close();await navigate();});await playlistImagePicker(p);}
diff --git a/pigway_frame_flow/web/features.js b/pigway_frame_flow/web/features.js
index b5cfa40..09bbde6 100644
--- a/pigway_frame_flow/web/features.js
+++ b/pigway_frame_flow/web/features.js
@@ -40,7 +40,7 @@ function enhanceGallery(){
$('#gallery-search').value=state.galleryQuery||'';$('#gallery-sort').value=state.galleryOrder||'newest';$('#gallery-search').oninput();$('#gallery-sort').onchange();
}
async function weatherSettings(){
- const weather=await api('/api/v1/providers/weather');const box=document.createElement('section');box.className='card';box.innerHTML=`${t('weatherSettings')}
`;$('#main').append(box);$('#weather-provider').value=weather.config.provider;
+ const weather=await api('/api/v1/providers/weather');const box=document.createElement('section');box.className='card';box.innerHTML=`${t('weatherSettings')}
`;$('#main').append(box);$('#weather-provider').value=weather.config.provider;
function result(w){$('#weather-result').innerHTML=`${esc(w.current.temperature)} · ${esc(w.current.summary)}
${w.current.mock?t('weatherMock'):t('weatherAttribution')}${w.current.stale?' · '+t('stale'):''}
${t('forecast')} ${w.forecast.map(x=>`${esc(x.date)} · ${esc(x.summary)} · ${x.minimum_c}–${x.maximum_c}°C · ${x.rain_probability}%
`).join('')}`;}
result(weather);$('#weather-form').onsubmit=busy(async e=>{e.preventDefault();await api('/api/v1/settings/weather',{method:'PUT',body:{provider:$('#weather-provider').value,location:$('#weather-location').value,latitude:Number($('#weather-latitude').value),longitude:Number($('#weather-longitude').value)}});result(await api('/api/v1/providers/weather'));toast(t('done'));});
}
diff --git a/pigway_frame_flow/web/style.css b/pigway_frame_flow/web/style.css
index 88101dc..ca059ab 100644
--- a/pigway_frame_flow/web/style.css
+++ b/pigway_frame_flow/web/style.css
@@ -186,3 +186,15 @@ body.login-mode header,body.login-mode footer{flex-shrink:0}
.history-table td:nth-child(2){white-space:nowrap;overflow-wrap:normal}
.history-status{color:var(--accent)}
.history-status.bad{color:var(--bad)}
+
+.settings-columns{align-items:start}
+.settings-preferences .field,.password-fields .field,#weather-form .field{min-width:0;margin-bottom:0}
+#password-form>button{align-self:flex-start}
+form#weather-form{display:grid;grid-template-columns:minmax(160px,1fr) minmax(220px,2fr) repeat(2,minmax(100px,1fr)) auto;gap:18px;align-items:end}
+#weather-form .weather-coordinates{display:contents}
+#weather-form>button{justify-self:start}
+@media(max-width:1050px){form#weather-form{grid-template-columns:repeat(2,minmax(0,1fr))}}
+@media(max-width:720px){form#weather-form{grid-template-columns:minmax(0,1fr)}}
+
+#weather-form input,#weather-form select{height:46px}
+#image-host-form>button{align-self:flex-start}