fix(nosignal): hardware-test feedback round 2 — weather, dashboard, tray

From the 2026-06-15 on-box hardware test (all 6 prior fixes confirmed; these
are the new findings, baked into the fork source rather than shipped as
pacman-hook patches, per the Phase-2 "we own the shell" approach):

- F-W1 (Weather.qml): fetch at startup. Stock only called reload() on a config
  CHANGE, so a saved location blanked after reboot and an empty/auto location
  never populated on a fresh boot. Add Component.onCompleted: reload() and
  switch the hourly Timer to reload() (re-detects auto/IP too).
- F-W3 (Weather.qml): 7-day forecast showed "undefined°" in °F mode — only
  maxTempC/minTempC were stored. Store rounded maxTempF/minTempF (and round °C).
- F-W2 (Weather.qml): empty/auto-IP location never populated — the in-shell
  ipinfo.io fetch didn't set loc (provider-specific; the city path works via
  the same Requests.get). Repair attempt: defensive JSON parse + error callback
  + fallback to geojs.io. Compiled Requests module not inspectable, so this is
  best-effort; a saved city still works regardless.
- F-D1a (modules/dashboard/*.qml): square the 25 card radii bound to the
  compiled Tokens.rounding.* Material defaults (which ignore shell.json
  rounding.scale=0) so the dashboard matches the square redesign.
- F-D1b (Glyphs.qml): remap the weather glyphs to the Material-weather family
  (every codepoint verified present in JetBrainsMono Nerd Font Mono); the old
  Font-Awesome codepoints were absent (rendered "?") and thunderstorm /
  snowing_heavy were unmapped.
- F-T1 (NsBar.qml): gate the system-tray pill on SystemTray.items count so it
  only shows when an app registers a tray icon (was opening an empty panel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
28allday 2026-06-15 21:04:28 +01:00
parent f21e5b5eaf
commit 53c5833de0
16 changed files with 85 additions and 42 deletions

View file

@ -75,7 +75,7 @@ Item {
anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.large
radius: Tokens.rounding.large
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: "transparent"
Flickable {

View file

@ -21,7 +21,7 @@ GridLayout {
Layout.preferredWidth: Tokens.sizes.dashboard.userWidth
Layout.fillHeight: true
radius: Tokens.rounding.extraLarge
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
User {
id: user
@ -37,7 +37,7 @@ GridLayout {
Layout.preferredWidth: Tokens.sizes.dashboard.weatherWidth
Layout.preferredHeight: weather.implicitHeight
radius: Tokens.rounding.extraLarge * 1.5
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
SmallWeather {
id: weather
@ -49,7 +49,7 @@ GridLayout {
Layout.preferredWidth: dateTime.implicitWidth
Layout.fillHeight: true
radius: Tokens.rounding.large
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
DateTime {
id: dateTime
@ -63,7 +63,7 @@ GridLayout {
Layout.fillWidth: true
Layout.preferredHeight: calendar.implicitHeight
radius: Tokens.rounding.extraLarge
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
Calendar {
id: calendar
@ -78,7 +78,7 @@ GridLayout {
Layout.preferredWidth: resources.implicitWidth
Layout.fillHeight: true
radius: Tokens.rounding.large
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
Resources {
id: resources
@ -92,7 +92,7 @@ GridLayout {
Layout.preferredWidth: media.implicitWidth
Layout.fillHeight: true
radius: Tokens.rounding.extraLarge * 2
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
Media {
id: media

View file

@ -86,7 +86,7 @@ Item {
implicitHeight: parent.implicitHeight * 2
color: Colours.palette.m3primary
radius: Tokens.rounding.full
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
}
Behavior on x {
@ -148,7 +148,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
implicitHeight: parent.height + Tokens.sizes.dashboard.tabIndicatorSpacing * 2
radius: Tokens.rounding.medium
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface
onClicked: root.dashState.currentTab = tab.TabBar.index
}

View file

@ -67,7 +67,7 @@ Item {
Layout.fillWidth: true
implicitHeight: bigInfoRow.implicitHeight + Tokens.padding.small
radius: Tokens.rounding.extraLarge * 2
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.tPalette.m3surfaceContainer
RowLayout {
@ -155,7 +155,7 @@ Item {
Layout.fillWidth: true
implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.medium * 2
radius: Tokens.rounding.large
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.tPalette.m3surfaceContainer
ColumnLayout {
@ -209,7 +209,7 @@ Item {
Layout.fillWidth: true
Layout.preferredHeight: 60
radius: Tokens.rounding.medium
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.tPalette.m3surfaceContainer
Row {

View file

@ -62,7 +62,7 @@ CustomMouseArea {
StateLayer {
color: Colours.palette.m3primary
radius: pressed ? Tokens.rounding.small : Tokens.rounding.large
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
disabled: {
const now = new Date();
return root.currMonth === now.getMonth() && root.currYear === now.getFullYear();

View file

@ -216,7 +216,7 @@ Item {
implicitWidth: 10
implicitHeight: 10
radius: Tokens.rounding.full
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.palette.m3secondaryContainer
}
@ -229,7 +229,7 @@ Item {
implicitWidth: 15
implicitHeight: 15
radius: Tokens.rounding.full
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.palette.m3secondaryContainer
}
@ -240,7 +240,7 @@ Item {
anchors.leftMargin: -Tokens.padding.medium
y: Tokens.padding.extraSmall
radius: Tokens.rounding.largeIncreased
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.palette.m3secondaryContainer
implicitWidth: wmLabel.implicitWidth + Tokens.padding.medium * 2
implicitHeight: wmLabel.implicitHeight + Tokens.padding.small * 2

View file

@ -176,7 +176,7 @@ Item {
implicitWidth: shape.implicitSize + Tokens.padding.medium * 2
implicitHeight: shape.implicitSize + Tokens.padding.medium * 2
color: Colours.palette.m3primaryContainer
radius: Tokens.rounding.full
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
LoadingIndicator {
id: shape

View file

@ -33,7 +33,7 @@ Item {
anchors.fill: parent
anchors.margins: !btn.pressed && btn.containsMouse ? -Tokens.padding.extraSmall : 0
group: blobGroup
radius: Tokens.rounding.medium
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
Behavior on anchors.margins {
Anim {}
@ -50,7 +50,7 @@ Item {
implicitHeight: parent.height
group: blobGroup
radius: Tokens.rounding.medium
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
deformScale: 0.00001
states: State {

View file

@ -12,7 +12,7 @@ StyledClippingRect {
property real animPerc: UPower.displayDevice.percentage
color: Colours.palette.m3secondaryContainer
radius: Tokens.rounding.large
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
implicitWidth: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None) || Config.dashboard.performance.showStorage || Config.dashboard.performance.showMemory ? Tokens.sizes.dashboard.perfBattWidth : Tokens.sizes.dashboard.perfBattWidthSingle
implicitHeight: Tokens.sizes.dashboard.perfBattHeight
@ -39,7 +39,7 @@ StyledClippingRect {
implicitHeight: parent.height * root.animPerc
color: Colours.palette.m3secondary
radius: Tokens.rounding.extraSmall
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
clip: true
Contents {

View file

@ -17,7 +17,7 @@ StyledRect {
required property real temperature
color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.extraLarge
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
implicitWidth: Tokens.sizes.dashboard.perfHeroCardWidth
implicitHeight: Math.max(tempProg.implicitHeight + detailsRow.implicitHeight + Tokens.spacing.large, usageShape.implicitHeight + usageLabel.implicitHeight) + Tokens.padding.large * 2

View file

@ -12,7 +12,7 @@ StyledRect {
readonly property color accent: Colours.palette.m3tertiary
color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.medium
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
implicitWidth: layout.implicitWidth + Tokens.padding.extraLargeIncreased * 2
implicitHeight: layout.implicitHeight + Tokens.padding.large * 2

View file

@ -10,7 +10,7 @@ StyledRect {
id: root
color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.extraLarge
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
implicitWidth: Tokens.sizes.dashboard.perfNetworkCardWidth
implicitHeight: Tokens.sizes.dashboard.perfNetworkCardHeight

View file

@ -14,7 +14,7 @@ StyledRect {
readonly property real percentage: Storage.primaryDisk?.perc ?? 0
color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.extraExtraLarge
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
implicitHeight: layout.implicitHeight + Tokens.padding.large * 2

View file

@ -16,6 +16,7 @@ import Quickshell
import Quickshell.Wayland
import Quickshell.Bluetooth
import Quickshell.Services.UPower
import Quickshell.Services.SystemTray
import qs.services
import qs.utils
import qs.components
@ -139,10 +140,12 @@ Variants {
}
}
// System tray
// System tray only shown when an app actually registers a tray
// icon, otherwise the pill just opened an empty panel (F-T1).
IconPill {
icon: "widgets"
panel: "tray"
visible: SystemTray.items.values.length > 0
}
// Notifications bell (with unread dot overlaid on the icon)

View file

@ -145,16 +145,21 @@ Singleton {
"warning": root.cp(0xf071),
"web_asset_off": root.cp(0xf2d0),
"wifi_find": root.cp(0xf1eb),
// weather (Icons.getWeatherIcon)
"clear_day": root.cp(0xf185),
"clear_night": root.cp(0xf186),
"partly_cloudy_day": root.cp(0xf6c4),
"cloudy": root.cp(0xf0c2),
"cloud": root.cp(0xf0c2),
"rainy": root.cp(0xf73d),
"cloudy_snowing": root.cp(0xf2dc),
"foggy": root.cp(0xf75f),
"air": root.cp(0xf72e),
// weather (Icons.getWeatherIcon) Material-weather family, every
// codepoint verified present in JetBrainsMono Nerd Font Mono. The old
// Font-Awesome mapping had several glyphs absent from the font (rendered
// as "?" boxes) and was missing thunderstorm/snowing_heavy (F-D1).
"clear_day": root.cp(0xf0599),
"clear_night": root.cp(0xf0594),
"partly_cloudy_day": root.cp(0xf0595),
"cloudy": root.cp(0xf0590),
"cloud": root.cp(0xf0590),
"rainy": root.cp(0xf0597),
"cloudy_snowing": root.cp(0xf0598),
"snowing_heavy": root.cp(0xf0f37),
"thunderstorm": root.cp(0xf0593),
"foggy": root.cp(0xf0591),
"air": root.cp(0xf059d),
// apps / devices (Icons.get*)
"code": root.cp(0xf121),
"forum": root.cp(0xf086),

View file

@ -41,15 +41,43 @@ Singleton {
fetchCoordsFromCity(configLocation);
}
} else if (!loc || timer.elapsed() > 900) {
fetchLocationFromIp();
}
}
// Auto-detect location from the public IP (used when weatherLocation is
// empty). Stock caelestia only queried ipinfo.io with no error handling;
// on the 2026-06-15 hardware test that call never populated `loc` (the
// city path works via the same Requests.get, so it is provider-specific
// finding F-W2). Repair: wrap parsing defensively and fall back to a second
// provider (geojs.io, which returns separate latitude/longitude fields) on
// any error or empty response.
function fetchLocationFromIp(): void {
const useGeoJs = () => {
Requests.get("https://get.geojs.io/v1/ip/geo.json", text => {
try {
const g = JSON.parse(text);
if (g.latitude && g.longitude) {
loc = g.latitude + "," + g.longitude;
city = g.city ?? "";
timer.restart();
}
} catch (e) {}
});
};
Requests.get("https://ipinfo.io/json", text => {
try {
const response = JSON.parse(text);
if (response.loc) {
loc = response.loc;
city = response.city ?? "";
timer.restart();
return;
}
});
}
} catch (e) {}
useGeoJs();
}, useGeoJs);
}
function fetchCityFromCoords(coords: string): void {
@ -131,8 +159,10 @@ Singleton {
for (let i = 0; i < json.daily.time.length; i++)
forecastList.push({
date: json.daily.time[i].replace(/-/g, "/"),
maxTempC: json.daily.temperature_2m_max[i],
minTempC: json.daily.temperature_2m_min[i],
maxTempC: Math.round(json.daily.temperature_2m_max[i]),
minTempC: Math.round(json.daily.temperature_2m_min[i]),
maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])),
minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])),
weatherCode: json.daily.weather_code[i],
icon: Icons.getWeatherIcon(json.daily.weather_code[i])
});
@ -210,6 +240,11 @@ Singleton {
onLocChanged: fetchWeatherData()
// Stock caelestia only called reload() on a config CHANGE, never at startup,
// so a saved location blanked after a reboot and an empty (auto/IP) location
// never populated on a fresh boot (finding F-W1). Fetch once on load.
Component.onCompleted: reload()
Connections {
function onWeatherLocationChanged(): void {
root.reload();
@ -222,7 +257,7 @@ Singleton {
interval: 3600000 // 1 hour
running: true
repeat: true
onTriggered: fetchWeatherData()
onTriggered: reload() // re-detect an auto/IP location too, not just refetch a known one
}
ElapsedTimer {