Timothy Pomeroy hace 2 años
padre
commit
cba6915fbd

+ 0 - 4
.env

@@ -1,4 +0,0 @@
-CLIENTS='["10.10.10.100","10.10.10.101","10.10.10.102","10.10.10.103"]'
-NAMES='["Stage","Yellow Tower","Blue Tower","Red Tower"]'
-# CLIENTS='["192.168.1.122","192.168.1.113"]'
-# NAMES='["POC1","POC2"]'

+ 107 - 7
app/admin/page.js

@@ -5,10 +5,10 @@ import { useEffect, useState } from "react";
 import { Collapse, Loader } from "tiny-ui";
 import { WLEDClient } from "wled-client";
 
-import { Layout, Management } from "components";
+import { Layout, Management, Notification } from "components";
 
-const CLIENTS = process.env.CLIENTS || [];
-const NAMES = process.env.NAMES || [];
+import config from "data/config.json";
+import defaults from "data/presets.json";
 
 export default function Home() {
   const [names, setNames] = useState(null);
@@ -19,20 +19,52 @@ export default function Home() {
     let t = [];
     let c = [];
     let n = [];
-    for (let client of CLIENTS) {
+    for (let client of Object.keys(config)) {
       c.push(client);
+      n.push(config[client]);
       t.push(new WLEDClient(client));
     }
-    for (let name of NAMES) {
-      n.push(name);
-    }
     setClients(c);
     setConnections(t);
     setNames(n);
   }, []);
 
+  const handleDefaults = async (payload) => {
+    if (payload?.clients) {
+      Notification({
+        type: "loading",
+        title: "Default presets installing",
+        description: `Sending data to ${payload?.clients?.join(", ")}`
+      });
+      for (let client of payload.clients) {
+        let indx = clients.findIndex((o) => o === client);
+        if (connections[indx]) {
+          let wled = connections[indx];
+          for (let n of Object.keys(defaults)) {
+            if (n && defaults?.[n]) {
+              await wled.savePreset(n, defaults?.[n]);
+            }
+          }
+          await wled.reinit();
+        }
+        Notification({
+          type: "success",
+          title: "Default presets installed",
+          description: `Default presets installed on ${payload?.clients?.join(
+            ", "
+          )}`
+        });
+      }
+    }
+  };
+
   const handleCopy = async (payload) => {
     if (payload?.clients && payload?.value) {
+      Notification({
+        type: "loading",
+        title: "Copying presets",
+        description: `Sending data to ${payload?.clients?.join(", ")}`
+      });
       for (let client of payload.clients) {
         let indx = clients.findIndex((o) => o === client);
         if (connections[indx]) {
@@ -46,11 +78,25 @@ export default function Home() {
           await wled.reinit();
         }
       }
+      Notification({
+        type: "success",
+        title: "Presets copied",
+        description: `Presets ${Object.keys(payload?.value)?.join(
+          ", "
+        )} copied to ${payload?.clients?.join(", ")}`
+      });
     }
   };
 
   const handleDelete = async (payload) => {
     if (payload?.clients && payload?.value) {
+      Notification({
+        type: "loading",
+        title: "Deleting presets",
+        description: `Sending delete requests to ${payload?.clients?.join(
+          ", "
+        )}`
+      });
       for (let client of payload?.clients) {
         let indx = clients.findIndex((o) => o === client);
         if (connections[indx]) {
@@ -61,11 +107,23 @@ export default function Home() {
           await wled.reinit();
         }
       }
+      Notification({
+        type: "success",
+        title: "Deleted presets",
+        description: `Presets ${Object.keys(payload?.value)?.join(
+          ", "
+        )} deleted from ${payload?.clients?.join(", ")}`
+      });
     }
   };
 
   const handleSync = async (payload) => {
     if (payload?.clients) {
+      Notification({
+        type: "loading",
+        title: "Sync update",
+        description: `Sending sync updates to ${payload?.clients?.join(", ")}`
+      });
       for (let client of payload?.clients) {
         let indx = clients.findIndex((o) => o === client);
         if (connections[indx]) {
@@ -81,11 +139,21 @@ export default function Home() {
           await wled.reinit();
         }
       }
+      Notification({
+        type: "success",
+        title: `Sync ${payload?.value}`,
+        description: `Sync ${payload?.value} on ${payload?.clients?.join(", ")}`
+      });
     }
   };
 
   const handlePower = async (payload) => {
     if (payload?.clients && payload?.value) {
+      Notification({
+        type: "loading",
+        title: "Power updates",
+        description: `Sending power updates to ${payload?.clients?.join(", ")}`
+      });
       for (let client of payload?.clients) {
         let indx = clients.findIndex((o) => o === client);
         if (connections[indx]) {
@@ -112,11 +180,23 @@ export default function Home() {
           }
         }
       }
+      Notification({
+        type: "success",
+        title: `Power ${payload?.value}`,
+        description: `Power ${payload?.value} run on ${payload?.clients?.join(
+          ", "
+        )}`
+      });
     }
   };
 
   const handleBrightness = async (payload) => {
     if (payload?.clients && payload?.value) {
+      Notification({
+        type: "loading",
+        title: "Adjusting brightness",
+        description: `Adjusting brightness on ${payload?.clients?.join(", ")}`
+      });
       for (let client of payload?.clients) {
         let indx = clients.findIndex((o) => o === client);
         if (connections[indx]) {
@@ -125,12 +205,24 @@ export default function Home() {
           await wled.reinit();
         }
       }
+      Notification({
+        type: "success",
+        title: `Brightness set`,
+        description: `Brightness set to ${
+          payload?.value
+        } on ${payload?.clients?.join(", ")}`
+      });
     }
   };
   const debounceHandleBrightness = debounce(handleBrightness, 500);
 
   const handlePreset = async (payload) => {
     if (payload?.clients && payload?.value) {
+      Notification({
+        type: "loading",
+        title: "Setting preset",
+        description: `Setting preset on ${payload?.clients?.join(", ")}`
+      });
       for (let client of payload?.clients) {
         let indx = clients.findIndex((o) => o === client);
         if (connections[indx]) {
@@ -139,6 +231,13 @@ export default function Home() {
           await wled.reinit();
         }
       }
+      Notification({
+        type: "success",
+        title: `Preset set`,
+        description: `Preset set to ${
+          payload?.value
+        } on ${payload?.clients?.join(", ")}`
+      });
     }
   };
 
@@ -160,6 +259,7 @@ export default function Home() {
               onPower={handlePower}
               onBrightness={debounceHandleBrightness}
               onPreset={handlePreset}
+              onDefaults={handleDefaults}
             />
           ))}
         </Collapse>

+ 22 - 8
app/page.js

@@ -4,14 +4,12 @@ import { useEffect, useState } from "react";
 import { Col, Loader, Row } from "tiny-ui";
 import { WLEDClient } from "wled-client";
 
-import { Automation, Layout } from "components";
+import { Automation, Layout, Notification } from "components";
 
 import automation from "data/automation.json";
+import config from "data/config.json";
 import presets from "data/presets.json";
 
-const CLIENTS = process.env.CLIENTS || [];
-const NAMES = process.env.NAMES || [];
-
 export default function Home() {
   const [names, setNames] = useState(null);
   const [clients, setClients] = useState(null);
@@ -21,13 +19,11 @@ export default function Home() {
     let t = [];
     let c = [];
     let n = [];
-    for (let client of CLIENTS) {
+    for (let client of Object.keys(config)) {
       c.push(client);
+      n.push(config[client]);
       t.push(new WLEDClient(client));
     }
-    for (let name of NAMES) {
-      n.push(name);
-    }
     setClients(c);
     setConnections(t);
     setNames(n);
@@ -49,6 +45,12 @@ export default function Home() {
           }
         }
       }
+      Notification({
+        title: `Sync ${payload?.value}`,
+        description: `Sync ${
+          payload?.value
+        } complete for ${payload?.clients?.join(", ")}`
+      });
     }
   };
 
@@ -68,6 +70,12 @@ export default function Home() {
           }
         }
       }
+      Notification({
+        title: `Power ${payload?.value}`,
+        description: `Power ${
+          payload?.value
+        } complete for ${payload?.clients?.join(", ")}`
+      });
     }
   };
 
@@ -80,6 +88,12 @@ export default function Home() {
           await wled.setPreset(payload?.value);
         }
       }
+      Notification({
+        title: `Preset set`,
+        description: `Preset ${payload?.value} set on ${payload?.clients?.join(
+          ", "
+        )}`
+      });
     }
   };
 

+ 100 - 20
components/Automation.js

@@ -1,12 +1,33 @@
 import { useEffect, useState } from "react";
-import { Button, Drawer, Icon, Steps, Typography } from "tiny-ui";
+import { Button, Collapse, Drawer, Icon, Steps, Typography } from "tiny-ui";
+import WLED from "./WLED.js";
 
 import { useLocalStorage } from "lib/state";
 import { isBlank } from "lib/utils";
 
 const { Step } = Steps;
+const { Panel } = Collapse;
 const { Heading, Paragraph, Text } = Typography;
 
+const Client = ({ name, client, connection, ...rest }) => {
+  const normalize = (v) => JSON.stringify(v, null, 2);
+  return (
+    <Panel header={name} {...rest}>
+      <p>IP address: {client}</p>
+      <div style={{ overflowY: "scroll", height: 200 }}>
+        {Object.keys(connection?.state)?.map((o, i) => {
+          return (
+            <div>
+              <strong>{o}:</strong> {normalize(connection?.state?.[o])}
+            </div>
+          );
+        })}
+      </div>
+    </Panel>
+  );
+};
+const WLEDClient = WLED(Client);
+
 const Automation = ({
   automation,
   presets,
@@ -15,11 +36,13 @@ const Automation = ({
   onPreset,
   onSync,
   onPower,
+  connections,
   ...props
 }) => {
-  const [current, setCurrent] = useLocalStorage(-1);
+  const [current, setCurrent] = useLocalStorage("preset", -1, 3600);
   const [total, setTotal] = useState();
-  const [visible, setVisible] = useState(false);
+  const [help, setHelp] = useState(false);
+  const [info, setInfo] = useState(false);
 
   const getTitle = (indx) => presets?.[indx]?.name || indx;
   const getName = (v) => {
@@ -48,21 +71,32 @@ const Automation = ({
       if (automation?.[indx]?.[key]?.sync === true) sync.enable.push(key);
       else sync.disable.push(key);
     }
-    if (!isBlank(power?.on)) onPower({ clients: power?.on, value: "on" }); // power on
-    if (!isBlank(power?.off)) onPower({ clients: power?.off, value: "off" }); // power off
     if (!isBlank(sync?.enable))
       onSync({ clients: sync?.enable, value: "enable" }); // enable sync
     if (!isBlank(sync?.disable))
       onSync({ clients: sync?.disable, value: "disable" }); // disable sync
-    onPreset(preset); // apply the preset
+    setTimeout(() => {
+      onPreset(preset); // apply the preset
+    }, 100);
+    setTimeout(() => {
+      if (!isBlank(power?.on)) onPower({ clients: power?.on, value: "on" }); // power on
+      if (!isBlank(power?.off)) onPower({ clients: power?.off, value: "off" }); // power off
+    }, 200);
+  };
+
+  const handlePower = (value) => {
+    let keys = Object.keys(automation?.[1]);
+    onPower({ clients: keys, value: value });
   };
 
   useEffect(() => {
-    setTotal((automation && Object.keys(automation)) || 0);
-  }, [automation]);
+    setTotal((automation && Object.keys(automation)?.length) || 0);
+  }, []);
 
   useEffect(() => {
-    if (current > -1 && current < total) handleChange(current + 1);
+    if (current > -1 && current < total) {
+      handleChange(current + 1);
+    }
   }, [current]);
 
   let steps = Object.keys(automation).map((o, i) => {
@@ -70,10 +104,10 @@ const Automation = ({
       <Step
         key={i}
         title={
-          <>
-            {getTitle(o)}
+          <div style={{ display: "flex", flexDirection: "row" }}>
+            <div>{getTitle(o)}</div>
             {current === o - 1 ? (
-              <div style={{ float: "right" }}>
+              <div style={{ marginLeft: "3rem" }}>
                 <Button
                   title="Reapply"
                   round
@@ -84,7 +118,7 @@ const Automation = ({
                 />
               </div>
             ) : null}
-          </>
+          </div>
         }
         description={getDescription(o)}
       ></Step>
@@ -94,7 +128,7 @@ const Automation = ({
   return (
     <>
       <div style={{ position: "sticky", top: "60px", marginBottom: "2rem" }}>
-        <Button.Group size="lg" round>
+        <Button.Group size="md" round>
           <Button
             title="Previous"
             icon={<Icon name="arrow-left" />}
@@ -108,7 +142,7 @@ const Automation = ({
             title="Reapply"
             icon={<Icon name="loader-circle" />}
             onClick={() => {
-              handleChange(current);
+              handleChange(current + 1);
             }}
           />
           <Button
@@ -121,12 +155,39 @@ const Automation = ({
             }}
           />
         </Button.Group>
-        <Button.Group size="lg" round>
+        <Button.Group size="md" round>
+          <Button
+            title="Power on"
+            onClick={() => {
+              handlePower("on");
+            }}
+          >
+            On
+          </Button>
+          <Button
+            title="Power off"
+            onClick={() => {
+              handlePower("off");
+            }}
+          >
+            Off
+          </Button>
+        </Button.Group>
+        <Button.Group size="md" round>
           <Button
             title="Help"
             icon={<Icon name="question-fill" />}
             onClick={() => {
-              setVisible(true);
+              setHelp(true);
+            }}
+          />
+        </Button.Group>
+        <Button.Group size="md" round>
+          <Button
+            title="Information"
+            icon={<Icon name="info" />}
+            onClick={() => {
+              setInfo(true);
             }}
           />
         </Button.Group>
@@ -139,11 +200,30 @@ const Automation = ({
         {steps}
       </Steps>
       <Drawer
-        header="Introduction"
+        header="Client connections"
+        placement="right"
+        size={480}
+        onClose={() => setInfo(false)}
+        visible={info}
+      >
+        <Collapse accordion bordered={false}>
+          {connections?.map((o, i) => (
+            <WLEDClient
+              key={i}
+              itemKey={i}
+              connection={o}
+              client={clients[i]}
+              name={names[i]}
+            />
+          ))}
+        </Collapse>
+      </Drawer>
+      <Drawer
+        header="Help"
         placement="right"
         size={480}
-        onClose={() => setVisible(false)}
-        visible={visible}
+        onClose={() => setHelp(false)}
+        visible={help}
       >
         <>
           <Paragraph>

+ 47 - 32
components/Management.js

@@ -12,7 +12,7 @@ import {
   ScrollIndicator,
   Slider,
   SplitButton,
-  Transfer,
+  Transfer
 } from "tiny-ui";
 import WLED from "./WLED.js";
 
@@ -79,7 +79,7 @@ const UploadModal = ({ data, client, onConfirm, ...rest }) => {
       if (e?.target?.result) {
         console.log("payload", {
           ...payload,
-          value: JSON.parse(e?.target?.result),
+          value: JSON.parse(e?.target?.result)
         });
         setPayload({ ...payload, value: JSON.parse(e?.target?.result) });
       }
@@ -184,7 +184,7 @@ const InstanceManagement = ({
   presets,
   onCopy,
   onPreset,
-  onDelete,
+  onDelete
 }) => {
   if (!presets) return null;
   return Object.keys(presets)?.map((o, i) => (
@@ -193,6 +193,7 @@ const InstanceManagement = ({
       <Col span={8} className="action">
         <SplitButton
           btnType="primary"
+          title="Apply"
           overlay={
             <StyledMenu>
               <Menu.Item
@@ -228,7 +229,6 @@ const InstanceManagement = ({
           }}
         >
           <Icon name="checkmark" />
-          Apply
         </SplitButton>
       </Col>
     </StyledRow>
@@ -244,6 +244,7 @@ const StateMangement = ({
   onSync,
   onBrightness,
   onPreset,
+  onDefaults,
   current,
   ...props
 }) => {
@@ -254,36 +255,40 @@ const StateMangement = ({
         <Button
           btnType={current?.on ? "primary" : "outline"}
           icon={<Icon name="switch" />}
+          title={`Lights ${current?.on ? "on" : "off"}`}
           round
           onClick={() =>
             onPower({ clients: [client], value: current?.on ? "off" : "on" })
           }
-        >
-          Lights {current?.on ? "on" : "off"}
-        </Button>
-        <PopConfirm
-          title="Are you sure you want to make enable sync?"
-          onConfirm={() => onSync({ clients: [client], value: "enable" })}
-        >
-          <Button btnType="outline" round disabled={current?.udpSync?.send}>
-            Enable sync
-          </Button>
-        </PopConfirm>
-        <PopConfirm
-          title="Are you sure you want to disable sync?"
-          onConfirm={() => onSync({ clients: [client], value: "disable" })}
-        >
-          <Button btnType="outline" round disabled={!current?.udpSync?.send}>
-            Disable sync
-          </Button>
-        </PopConfirm>
+        />
+        {current?.udpSync?.send ? (
+          <Button
+            onClick={() => onSync({ clients: [client], value: "disable" })}
+            title="Disable sync"
+            icon={<Icon name="broadcast" />}
+            btnType="primary"
+            round
+          />
+        ) : (
+          <Button
+            onClick={() => onSync({ clients: [client], value: "enable" })}
+            btnType="outline"
+            title="Enable sync"
+            icon={<Icon name="broadcast" />}
+            round
+          />
+        )}
+
         <PopConfirm
           title="Are you sure you want to reboot?"
           onConfirm={() => onPower({ clients: [client], value: "reboot" })}
         >
-          <Button icon={<Icon name="loader" />} btnType="danger" round>
-            Reboot
-          </Button>
+          <Button
+            title="Reboot"
+            icon={<Icon name="loader" />}
+            btnType="danger"
+            round
+          />
         </PopConfirm>
       </Col>
       <Col span={12} style={{ textAlign: "right" }}>
@@ -307,7 +312,6 @@ const StateMangement = ({
             }}
           />
         </Button.Group>
-
         <SplitButton
           onClick={(e) => {
             e.preventDefault();
@@ -326,6 +330,7 @@ const StateMangement = ({
                 <Icon name="eye" />
                 Open
               </Menu.Item>
+              <Menu.Divider />
               <Menu.Item
                 onClick={(e) => {
                   e.preventDefault();
@@ -337,7 +342,7 @@ const StateMangement = ({
                 }}
               >
                 <Icon name="download" />
-                Save all
+                Download
               </Menu.Item>
               <Menu.Item
                 onClick={(e) => {
@@ -347,7 +352,17 @@ const StateMangement = ({
                 }}
               >
                 <Icon name="upload" />
-                Restore all
+                Upload
+              </Menu.Item>
+              <Menu.Item
+                onClick={(e) => {
+                  e.preventDefault();
+                  e.stopPropagation();
+                  onDefaults({ clients: [client] });
+                }}
+              >
+                <Icon name="branch" />
+                Defaults
               </Menu.Item>
               <Menu.Item
                 onClick={(e) => {
@@ -357,7 +372,7 @@ const StateMangement = ({
                 }}
               >
                 <Icon name="inspection" />
-                Copy all
+                Copy
               </Menu.Item>
               <Menu.Divider />
               <Menu.Item
@@ -366,12 +381,12 @@ const StateMangement = ({
                   e.stopPropagation();
                   setPurge({
                     clients: [client],
-                    value: Object.keys(props?.presets),
+                    value: Object.keys(props?.presets)
                   });
                 }}
               >
                 <Icon name="trash" />
-                Delete all
+                Delete
               </Menu.Item>
             </StyledMenu>
           }

+ 1 - 1
components/Menu.js

@@ -3,7 +3,7 @@ import Link from "next/link";
 import { Image, Menu as _Menu } from "tiny-ui";
 
 const MENU = [
-  { url: "/", text: "Hawkband WLED" },
+  { url: "/", text: "Powerline" },
   { url: "/", text: "Automation" },
   { url: "/admin", text: "Administration" }
 ];

+ 23 - 0
components/Notification.js

@@ -0,0 +1,23 @@
+import { Icon, Notification as _Notification } from "tiny-ui";
+
+const Notification = ({ type, ...props }) => {
+  let payload = { ...props, placement: "bottomRight" };
+  if (type && ["info", "success", "warning", "error"].includes(type)) {
+    _Notification[type](payload);
+  } else if (type && ["loader", "loading"].includes(type)) {
+    payload.duration = 1500;
+    payload.icon = (
+      <Icon name="loader" size={32} style={{ marginRight: "1rem" }} spin />
+    );
+    _Notification.open(payload);
+  } else if (type && ["announce", "announcement"].includes(type)) {
+    payload.icon = (
+      <Icon name="broadcast" size={32} style={{ marginRight: "1rem" }} />
+    );
+    _Notification.open(payload);
+  } else {
+    _Notification.open(payload);
+  }
+};
+
+export default Notification;

+ 1 - 0
components/index.js

@@ -2,4 +2,5 @@ export { default as Automation } from "./Automation";
 export { default as Layout } from "./Layout";
 export { default as Management } from "./Management";
 export { default as Menu } from "./Menu";
+export { default as Notification } from "./Notification";
 export { default as WLED } from "./WLED";

+ 70 - 0
data/development/automation.json

@@ -0,0 +1,70 @@
+{
+  "1": {
+    "192.168.1.113": { "on": true, "sync": false },
+    "192.168.1.122": { "on": false, "sync": false }
+  },
+  "2": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  },
+  "3": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  },
+  "4": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  },
+  "5": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  },
+  "6": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  },
+  "7": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  },
+  "8": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  },
+  "9": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  },
+  "10": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  },
+  "11": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  },
+  "12": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  },
+  "13": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  },
+  "14": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  },
+  "15": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  },
+  "16": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  },
+  "17": {
+    "192.168.1.113": { "on": true, "sync": true },
+    "192.168.1.122": { "on": true, "sync": true }
+  }
+}

+ 4 - 0
data/development/config.json

@@ -0,0 +1,4 @@
+{
+  "192.168.1.113": "POC1",
+  "192.168.1.122": "POC2"
+}

+ 530 - 57
data/presets.json → data/development/presets.json

@@ -18,9 +18,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [0, 30, 255],
-          [0, 0, 0],
-          [0, 0, 0]
+          [
+            0,
+            30,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 57,
         "effectSpeed": 145,
@@ -145,9 +157,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [0, 0, 255],
-          [0, 0, 0],
-          [0, 0, 0]
+          [
+            0,
+            0,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 57,
         "effectSpeed": 255,
@@ -272,9 +296,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [0, 0, 255],
-          [0, 0, 0],
-          [0, 0, 0]
+          [
+            0,
+            0,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 57,
         "effectSpeed": 255,
@@ -399,9 +435,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [0, 0, 255],
-          [0, 0, 0],
-          [0, 0, 0]
+          [
+            0,
+            0,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 0,
         "effectSpeed": 255,
@@ -526,9 +574,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [0, 0, 255],
-          [0, 0, 0],
-          [0, 0, 0]
+          [
+            0,
+            0,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 87,
         "effectSpeed": 255,
@@ -653,9 +713,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [255, 0, 255],
-          [0, 0, 0],
-          [0, 0, 0]
+          [
+            255,
+            0,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 87,
         "effectSpeed": 255,
@@ -780,9 +852,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [0, 0, 255],
-          [183, 38, 255],
-          [0, 0, 0]
+          [
+            0,
+            0,
+            255
+          ],
+          [
+            183,
+            38,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 87,
         "effectSpeed": 255,
@@ -907,9 +991,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [0, 0, 255],
-          [0, 0, 0],
-          [0, 0, 0]
+          [
+            0,
+            0,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 57,
         "effectSpeed": 255,
@@ -1034,9 +1130,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [0, 30, 255],
-          [0, 0, 0],
-          [0, 0, 0]
+          [
+            0,
+            30,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 18,
         "effectSpeed": 255,
@@ -1161,9 +1269,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [0, 0, 255],
-          [0, 0, 0],
-          [0, 0, 0]
+          [
+            0,
+            0,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 96,
         "effectSpeed": 255,
@@ -1288,9 +1408,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [255, 0, 0],
-          [0, 0, 0],
-          [0, 0, 0]
+          [
+            255,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 88,
         "effectSpeed": 255,
@@ -1397,6 +1529,145 @@
     "name": "10 part 2 letter F"
   },
   "12": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "1 Powerline",
+        "colors": [
+          [
+            0,
+            30,
+            255
+          ],
+          [
+            255,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 45,
+        "effectSpeed": 255,
+        "effectIntensity": 255,
+        "paletteId": 3,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "10A crazy 8s"
+  },
+  "13": {
     "on": true,
     "brightness": 255,
     "transition": 7,
@@ -1415,9 +1686,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [0, 0, 0],
-          [0, 0, 0],
-          [0, 0, 0]
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 87,
         "effectSpeed": 83,
@@ -1523,7 +1806,7 @@
     ],
     "name": "11 part 2 ending "
   },
-  "13": {
+  "14": {
     "on": true,
     "brightness": 255,
     "transition": 7,
@@ -1542,9 +1825,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [8, 255, 0],
-          [255, 200, 0],
-          [0, 0, 0]
+          [
+            8,
+            255,
+            0
+          ],
+          [
+            255,
+            200,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 74,
         "effectSpeed": 22,
@@ -1650,7 +1945,7 @@
     ],
     "name": "12 part 3 opening "
   },
-  "14": {
+  "15": {
     "on": true,
     "brightness": 255,
     "transition": 7,
@@ -1669,9 +1964,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [8, 255, 0],
-          [255, 200, 0],
-          [0, 0, 0]
+          [
+            8,
+            255,
+            0
+          ],
+          [
+            255,
+            200,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 7,
         "effectSpeed": 255,
@@ -1777,7 +2084,7 @@
     ],
     "name": "13 part 3 bolt hit"
   },
-  "15": {
+  "16": {
     "on": true,
     "brightness": 255,
     "transition": 7,
@@ -1796,9 +2103,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [8, 255, 0],
-          [0, 0, 0],
-          [0, 0, 0]
+          [
+            8,
+            255,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 2,
         "effectSpeed": 0,
@@ -1904,7 +2223,7 @@
     ],
     "name": "14 part 3 ending "
   },
-  "16": {
+  "17": {
     "on": true,
     "brightness": 255,
     "transition": 7,
@@ -1923,9 +2242,21 @@
         "cct": 127,
         "name": "Powerline",
         "colors": [
-          [8, 255, 0],
-          [0, 0, 0],
-          [0, 0, 0]
+          [
+            8,
+            255,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
         ],
         "effectId": 2,
         "effectSpeed": 0,
@@ -2031,12 +2362,154 @@
     ],
     "name": "15 end of part 3"
   },
+  "200": {
+    "name": "888_Poweroff",
+    "label": "1",
+    "on": true,
+    "brightness": 1,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 0,
+        "effectSpeed": 255,
+        "effectIntensity": 255,
+        "paletteId": 2,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ]
+  },
   "249": {
-    "win": "win&P1=1&P2=16PL=~-",
-    "name": "999_Previous"
+    "win": "win&P1=1&P2=17PL=~-",
+    "name": "999_Previous",
+    "label": "3"
   },
   "250": {
-    "win": "win&P1=1&P2=16PL=~",
-    "name": "999_Next"
+    "win": "win&P1=1&P2=17PL=~",
+    "name": "999_Next",
+    "label": "2"
   }
-}
+}

+ 6 - 0
data/automation.json → data/production/automation.json

@@ -94,5 +94,11 @@
     "10.10.10.101": { "on": true, "sync": true },
     "10.10.10.102": { "on": true, "sync": true },
     "10.10.10.103": { "on": true, "sync": true }
+  },
+  "17": {
+    "10.10.10.100": { "on": true, "sync": true },
+    "10.10.10.101": { "on": true, "sync": true },
+    "10.10.10.102": { "on": true, "sync": true },
+    "10.10.10.103": { "on": true, "sync": true }
   }
 }

+ 6 - 0
data/production/config.json

@@ -0,0 +1,6 @@
+{
+  "10.10.10.100": "Stage",
+  "10.10.10.101": "Yellow tower",
+  "10.10.10.102": "Blue tower",
+  "10.10.10.103": "Red tower"
+}

+ 2515 - 0
data/production/presets.json

@@ -0,0 +1,2515 @@
+{
+  "1": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            0,
+            30,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 57,
+        "effectSpeed": 145,
+        "effectIntensity": 255,
+        "paletteId": 2,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "1 part 1 opening "
+  },
+  "2": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            0,
+            0,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 57,
+        "effectSpeed": 255,
+        "effectIntensity": 255,
+        "paletteId": 2,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "2 part 1 3/4"
+  },
+  "3": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            0,
+            0,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 57,
+        "effectSpeed": 255,
+        "effectIntensity": 255,
+        "paletteId": 2,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "3 part 1 set 19"
+  },
+  "4": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            0,
+            0,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 0,
+        "effectSpeed": 255,
+        "effectIntensity": 255,
+        "paletteId": 2,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "4 part 1 end"
+  },
+  "5": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            0,
+            0,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 87,
+        "effectSpeed": 255,
+        "effectIntensity": 255,
+        "paletteId": 2,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "5 part 2 opening "
+  },
+  "6": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            255,
+            0,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 87,
+        "effectSpeed": 255,
+        "effectIntensity": 255,
+        "paletteId": 2,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "6 part 2 ww feature"
+  },
+  "7": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            0,
+            0,
+            255
+          ],
+          [
+            183,
+            38,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 87,
+        "effectSpeed": 255,
+        "effectIntensity": 255,
+        "paletteId": 3,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "7 part 2 set 41"
+  },
+  "8": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            0,
+            0,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 57,
+        "effectSpeed": 255,
+        "effectIntensity": 255,
+        "paletteId": 2,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "7A part 2 going to circles"
+  },
+  "9": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            0,
+            30,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 18,
+        "effectSpeed": 255,
+        "effectIntensity": 255,
+        "paletteId": 2,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "8 part 2 set 50 circles"
+  },
+  "10": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            0,
+            0,
+            255
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 96,
+        "effectSpeed": 255,
+        "effectIntensity": 255,
+        "paletteId": 2,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "9 part 2 riffle toss"
+  },
+  "11": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            255,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 88,
+        "effectSpeed": 255,
+        "effectIntensity": 255,
+        "paletteId": 2,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "10 part 2 letter F"
+  },
+  "12": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "1 Powerline",
+        "colors": [
+          [
+            0,
+            30,
+            255
+          ],
+          [
+            255,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 45,
+        "effectSpeed": 255,
+        "effectIntensity": 255,
+        "paletteId": 3,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "10A crazy 8s"
+  },
+  "13": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 87,
+        "effectSpeed": 83,
+        "effectIntensity": 91,
+        "paletteId": 2,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "11 part 2 ending "
+  },
+  "14": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            8,
+            255,
+            0
+          ],
+          [
+            255,
+            200,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 74,
+        "effectSpeed": 22,
+        "effectIntensity": 46,
+        "paletteId": 3,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "12 part 3 opening "
+  },
+  "15": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            8,
+            255,
+            0
+          ],
+          [
+            255,
+            200,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 7,
+        "effectSpeed": 255,
+        "effectIntensity": 255,
+        "paletteId": 3,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "13 part 3 bolt hit"
+  },
+  "16": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            8,
+            255,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 2,
+        "effectSpeed": 0,
+        "effectIntensity": 0,
+        "paletteId": 2,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "14 part 3 ending "
+  },
+  "17": {
+    "on": true,
+    "brightness": 255,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            8,
+            255,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 2,
+        "effectSpeed": 0,
+        "effectIntensity": 0,
+        "paletteId": 2,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ],
+    "name": "15 end of part 3"
+  },
+  "200": {
+    "name": "888_Poweroff",
+    "label": "1",
+    "on": true,
+    "brightness": 1,
+    "transition": 7,
+    "mainSegment": 0,
+    "segments": [
+      {
+        "id": 0,
+        "start": 0,
+        "stop": 500,
+        "grouping": 1,
+        "spacing": 0,
+        "of": 0,
+        "on": true,
+        "freeze": false,
+        "brightness": 255,
+        "cct": 127,
+        "name": "Powerline",
+        "colors": [
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ],
+          [
+            0,
+            0,
+            0
+          ]
+        ],
+        "effectId": 0,
+        "effectSpeed": 255,
+        "effectIntensity": 255,
+        "paletteId": 2,
+        "selected": true,
+        "reverse": false,
+        "mirror": false
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      },
+      {
+        "stop": 0
+      }
+    ]
+  },
+  "249": {
+    "win": "win&P1=1&P2=17PL=~-",
+    "name": "999_Previous",
+    "label": "3"
+  },
+  "250": {
+    "win": "win&P1=1&P2=17PL=~",
+    "name": "999_Next",
+    "label": "2"
+  }
+}

+ 26 - 11
lib/state.js

@@ -1,6 +1,6 @@
 import { useEffect, useRef, useState } from "react";
 
-import { isFunction, parse, stringify } from "lib/utils";
+import { isFunction, stringify } from "lib/utils";
 
 const hasWindow = () => {
   return typeof window !== "undefined" ? true : false;
@@ -10,24 +10,40 @@ const hasLocalStorage = () => {
   return hasWindow() && window.localStorage ? true : false;
 };
 
-const getItem = (key) => {
-  return hasLocalStorage() ? window.localStorage.getItem(key) : null;
+const getItem = (key, ttl = 0) => {
+  if (!hasLocalStorage()) return null;
+  let value = window.localStorage.getItem(key);
+  if (!value) return null;
+  const now = new Date()?.getTime();
+  const payload = JSON.parse(value);
+  if (payload?.expiry > 0 && now > payload?.expiry) {
+    removeItem(key);
+    return null;
+  }
+  return payload?.value || null;
 };
 
-const setItem = (key, value) => {
-  return hasLocalStorage() ? window.localStorage.setItem(key, value) : false;
+const setItem = (key, value, ttl = 0) => {
+  if (!hasLocalStorage()) return false;
+  const now = new Date()?.getTime();
+  const payload = {
+    value: value,
+    expiry: (ttl && now + ttl) || 0
+  };
+  return window.localStorage.setItem(key, JSON.stringify(payload));
 };
 
 const removeItem = (key) => {
-  return hasLocalStorage() ? window.localStorage.removeItem(key) : false;
+  if (!hasLocalStorage()) return null;
+  return window.localStorage.removeItem(key);
 };
 
 // use local storage to store state values for persistent values
-export function useLocalStorage(key, initialValue) {
+export function useLocalStorage(key, initialValue, ttl = 0) {
   const [storedValue, setStoredValue] = useState(() => {
     try {
-      const item = getItem(key);
-      return item ? parse(item) : initialValue;
+      const item = getItem(key, ttl);
+      return (item && JSON.parse(item)) || initialValue;
     } catch (err) {
       log.error("useLocalStorage useState error", err.message || err);
       return initialValue;
@@ -38,12 +54,11 @@ export function useLocalStorage(key, initialValue) {
     try {
       const item = isFunction(value) ? value(storedValue) : value;
       setStoredValue(item);
-      setItem(key, stringify(item));
+      setItem(key, stringify(item), ttl);
     } catch (err) {
       log.error("useLocalStorage setValue error", err.message || err);
     }
   };
-
   return [storedValue, setValue];
 }
 

+ 8 - 8
next.config.js

@@ -1,13 +1,13 @@
+const path = require("path");
+
+const NODE_ENV = process.env.NODE_ENV || "development";
+
 /** @type {import('next').NextConfig} */
 const nextConfig = {
-  // output: "export",
-  env: {
-    CLIENTS: process.env.CLIENTS
-      ? JSON.parse(process.env.CLIENTS)
-      : ["10.10.10.100", "10.10.10.101", "10.10.10.102", "10.10.10.103"],
-    NAMES: process.env.NAMES
-      ? JSON.parse(process.env.NAMES)
-      : ["Stage", "Yellow Tower", "Blue Tower", "Red Tower"]
+  // output: "standalone",
+  webpack: (config, options) => {
+    config.resolve.alias.data = path.resolve(__dirname, `data/${NODE_ENV}`);
+    return config;
   }
 };
 

+ 6 - 1
package.json

@@ -4,8 +4,13 @@
   "private": true,
   "scripts": {
     "dev": "next dev",
+    "compile": "next build",
+    "copy": "cp -rf public .next/standalone/. && cp -rf .next/static .next/standalone/. && cp -rf data .next/standalone/.",
+    "relocate": "rm -rf build && mv .next/standalone build",
+    "standalone": "yarn compile && yarn copy && yarn relocate",
     "build": "next build",
-    "start": "next start -p 80",
+    "start": "NODE_ENV=production next start -p 80",
+    "start:dev": "next start",
     "lint": "next lint"
   },
   "dependencies": {