diff --git a/CHANGELOG.md b/CHANGELOG.md
index d82bddb6f..d6ae39665 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,8 @@
# AmpliPi Software Releases
# Future Release
+* Web App
+ * Add warning for older versions of the webapp running on newer backends
# 0.4.11
@@ -9,7 +11,6 @@
* Web App
* Changed caching rules to ensure that users don't get stuck with old versions of the webapp post update
-
## 0.4.10
* Web App
diff --git a/NEW_RELEASE.md b/NEW_RELEASE.md
index 2365ae2ce..45fa84ed5 100644
--- a/NEW_RELEASE.md
+++ b/NEW_RELEASE.md
@@ -31,7 +31,8 @@ This project follows [Semantic Versioning](https://semver.org/). Here are some e
- [ ] Update the API by running `scripts/create_spec` script.
- [ ] Create & merge a branch/PR off `main` to bump the version in the CHANGELOG and also using `poetry version ${VERSION}`
- [ ] Checkout main & create a detached HEAD: `git checkout main; git pull; git checkout --detach`
-- [ ] Build the webapp in `web` with `npm run build` and force add the changes with `git add -f web/dist; git commit -m "Build web app for release"`
+- [ ] CD into `web`, Run `echo "VITE_BACKEND_VERSION=$(poetry version -s)" > .env`
+- [ ] Still in `web`, build the webapp with `npm run build` and force add the changes with `git add -f web/dist; git commit -m "Build web app for release"`
- [ ] Tag the changes so we can make a release on GitHub: `git tag -as ${VERSION} -m '' && git push origin ${VERSION}`
- [ ] Make a release using the GitHub interface
- [ ] Use the AmpliPi updater to update to the release
diff --git a/scripts/deploy b/scripts/deploy
index 4361d20aa..5d60fbefb 100755
--- a/scripts/deploy
+++ b/scripts/deploy
@@ -159,6 +159,7 @@ poetry version ${git_info}
echo "Building web app"
pushd ../web # Change to web directory
npm install # Install nodejs dependencies
+echo "VITE_BACKEND_VERSION=$(poetry version -s)" > .env # Collect the version number and bake it into the frontend
npm run build # Build the web app
popd
diff --git a/web/src/App.jsx b/web/src/App.jsx
index 4e28a7d3c..229f8988e 100644
--- a/web/src/App.jsx
+++ b/web/src/App.jsx
@@ -13,6 +13,7 @@ import DisconnectedIcon from "./components/DisconnectedIcon/DisconnectedIcon";
import Browse from "@/pages/Browse/Browse";
import PropTypes from "prop-types";
+import AlertBar from "./components/StatusBars/AlertBar";
// holds onto the selectedSource state so that it persists between refreshes
export const usePersistentStore = create(
@@ -40,6 +41,7 @@ export const useStatusStore = create((set, get) => ({
skipUpdate: false,
loaded: false, // using this instead of (status === null) because it fixes the re-rendering issue
disconnected: true,
+ alert: {"open": false, "text": "", "onClose": () => {}},
skipNextUpdate: () => {
set({ skipUpdate: true });
},
@@ -122,6 +124,9 @@ export const useStatusStore = create((set, get) => ({
}
});
},
+ setAlert: (text, onClose) => {
+ set({alert: {"open": true, "text": text, "onClose": onClose()}});
+ },
getSystemState: () => {
fetch("/api")
@@ -133,6 +138,9 @@ export const useStatusStore = create((set, get) => ({
set({ skipUpdate: false });
} else {
set({ status: s, loaded: true, disconnected: false });
+ if(s.info.version != import.meta.env.VITE_BACKEND_VERSION){
+ set({alert: {"open": true, "text": "Your webapp is out of date, closing this message will refresh the page. If this message persists post-refresh, clear your browser cache and try again.", "onClose": () => {window.location.reload();}}});
+ }
}
});
} else if (res.status == 401) {
@@ -225,15 +233,19 @@ Page.propTypes = {
};
const App = ({ selectedPage }) => {
+ const alert = useStatusStore((s) => s.alert);
return (
-
+
-
{/* Used to make sure the background doesn't stretch or stop prematurely on scrollable pages */}
-
-
+
{/* Used to make sure the background doesn't stretch or stop prematurely on scrollable pages */}
+
+
{alert["open"] == false; alert["onClose"]();}}/>
+
+
+
+
);
};
App.propTypes = {
diff --git a/web/src/App.scss b/web/src/App.scss
index 6a7bfb696..b99b6ca38 100644
--- a/web/src/App.scss
+++ b/web/src/App.scss
@@ -20,6 +20,7 @@
}
// unused
+
.app-container {
display: flex;
width: 100%;
@@ -31,10 +32,25 @@
padding-bottom: general.$navbar-height;
}
+
+.alert{
+ position: fixed;
+ top: 10px;
+ left: 10px;
+ right: 10px;
+ z-index: 2;
+}
+
@media (general.$is-landscape){
- // Themed scrollbar is overridden by 90% of mobile apps and browsers, but only partially overridden by one (andorid app)
+ // Themed scrollbar is overridden by 90% of mobile apps and browsers, but only partially overridden by one (android app)
// This still looks better on desktop, so gate it to only being on desktop without running the risk for the weird android app situation
+ .alert{
+ left: 25vw;
+ right: 25vw;
+ top: 10px;
+ }
+
.pill-scrollbar {
max-height: inherit;
overflow-y: auto;
diff --git a/web/src/components/CreateStreamModal/StreamModal/StreamModal.jsx b/web/src/components/CreateStreamModal/StreamModal/StreamModal.jsx
index 2d27fbb8f..685131d22 100644
--- a/web/src/components/CreateStreamModal/StreamModal/StreamModal.jsx
+++ b/web/src/components/CreateStreamModal/StreamModal/StreamModal.jsx
@@ -251,7 +251,6 @@ const StreamModal = ({ stream, onClose, apply, del }) => {
renderAnimationState={renderAlertAnimation}
open={hasError}
onClose={() => {setHasError(false);}}
- status={false}
text={errorMessage}
/>
}
diff --git a/web/src/components/StatusBars/AlertBar.jsx b/web/src/components/StatusBars/AlertBar.jsx
index 8d5aaa682..2ae5e6706 100644
--- a/web/src/components/StatusBars/AlertBar.jsx
+++ b/web/src/components/StatusBars/AlertBar.jsx
@@ -7,7 +7,7 @@ import Alert from "@mui/material/Alert";
export default function AlertBar(props) {
const {
open,
- status,
+ success,
text,
onClose,
renderAnimationState,
@@ -19,34 +19,37 @@ export default function AlertBar(props) {
if(alertRef.current != null){
const alertComp = alertRef.current;
alertComp.classList.remove("error");
- if(status == false){
+ if(!success){
alertComp.offsetWidth;
alertComp.classList.add("error");
}
}
- }, [status, renderAnimationState]);
+ }, [success, renderAnimationState]);
- if(open){
+ const [closedText, setClosedText] = React.useState(""); // If a user has closed a given message, don't show it again until another message tries to appear
+
+ if(open && text != closedText){
return(
{onClose(); setClosedText(text);}}
+ severity={success ? "success" : "error"}
variant="filled"
style={{width: "100%",}}
>
{text}
- )
+ );
}
-};
+}
AlertBar.propTypes = {
open: PropTypes.bool.isRequired,
- status: PropTypes.bool.isRequired,
+ success: PropTypes.bool,
text: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
renderAnimationState: PropTypes.number,
};
AlertBar.defaultProps = {
+ success: false,
renderAnimationState: 1,
-}
+};
diff --git a/web/src/components/StatusBars/ResponseBar.jsx b/web/src/components/StatusBars/ResponseBar.jsx
index 337bfac29..d4c94f8a2 100644
--- a/web/src/components/StatusBars/ResponseBar.jsx
+++ b/web/src/components/StatusBars/ResponseBar.jsx
@@ -41,7 +41,7 @@ export default function ResponseBar(props) {
return(
{setOpen(false);}}
/>
diff --git a/web/src/components/StatusBars/StatusBar.jsx b/web/src/components/StatusBars/StatusBar.jsx
index 0f20c8f76..c54326513 100644
--- a/web/src/components/StatusBars/StatusBar.jsx
+++ b/web/src/components/StatusBars/StatusBar.jsx
@@ -3,38 +3,50 @@ import PropTypes from "prop-types";
import "./StatusBars.scss";
import Snackbar from "@mui/material/Snackbar";
-import Alert from "@mui/material/Alert"
+import Alert from "@mui/material/Alert";
export default function StatusBar(props) {
const {
open,
- status,
+ success,
text,
onClose,
+ anchorOrigin,
+ autoHideDuration,
} = props;
+ const [closedText, setClosedText] = React.useState(""); // If a user has closed a given message, don't show it again until another message tries to appear
+
return(
{onClose(); setClosedText(text);}}
>
{onClose(); setClosedText(text);}}
+ severity={success ? "success" : "error"}
variant="filled"
style={{width: "100%"}}
>
{text}
- )
-};
+ );
+}
StatusBar.propTypes = {
open: PropTypes.bool.isRequired,
- status: PropTypes.bool.isRequired,
+ success: PropTypes.bool,
text: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
+ autoHideDuration: PropTypes.number,
+ anchorOrigin: PropTypes.object,
};
+StatusBar.defaultProps = {
+ success: false,
+ autoHideDuration: 3000,
+ anchorOrigin: {vertical: "bottom", horizontal: "left"},
+};
+
diff --git a/web/src/pages/Browse/Browse.jsx b/web/src/pages/Browse/Browse.jsx
index 3c8495175..cef246723 100644
--- a/web/src/pages/Browse/Browse.jsx
+++ b/web/src/pages/Browse/Browse.jsx
@@ -113,7 +113,6 @@ const Browse = () => {
{setErrorOpen(false);}}
/>
diff --git a/web/src/pages/Home/Home.jsx b/web/src/pages/Home/Home.jsx
index 0f28f48bc..6a91fd8ae 100644
--- a/web/src/pages/Home/Home.jsx
+++ b/web/src/pages/Home/Home.jsx
@@ -70,7 +70,7 @@ const Home = () => {
const [showStatus, setShowStatus] = React.useState(false);
const statusText = React.useRef("");
- const status = React.useRef(true);
+ const success = React.useRef(true);
let nextAvailableSource = null;
let cards = [];
@@ -145,10 +145,10 @@ const Home = () => {
)}
{presetsModalOpen && (
{status.current = state; statusText.current = text; setShowStatus(true);}}
+ onApply={(state, text) => {success.current = state; statusText.current = text; setShowStatus(true);}}
onClose={() => setPresetsModalOpen(false)} />
)}
- {setShowStatus(false);}} />
+ {setShowStatus(false);}} />
);
};