Browse Source

Merge pull request #241 from meienberger/release/0.7.1

Release/0.7.1
Nicolas Meienberger 2 years ago
parent
commit
83180709b6

+ 5 - 2
.dockerignore

@@ -5,5 +5,8 @@
 node_modules
 node_modules
 .next
 .next
 dist/
 dist/
-**/dist/
-**/next/
+
+# all docker-compose files
+docker-compose*.yml
+Dockerfile*
+.dockerignore

+ 8 - 8
Dockerfile

@@ -1,4 +1,4 @@
-FROM node:18 AS build
+FROM node:18 AS builder
 
 
 RUN npm install node-gyp -g
 RUN npm install node-gyp -g
 
 
@@ -35,13 +35,13 @@ WORKDIR /api
 COPY ./packages/system-api/package*.json /api/
 COPY ./packages/system-api/package*.json /api/
 RUN npm install --omit=dev
 RUN npm install --omit=dev
 
 
-WORKDIR /dashboard
-COPY ./packages/dashboard/package*.json /dashboard/
-RUN npm install --omit=dev
-
-COPY --from=build /api/dist /api/dist
+COPY --from=builder /api/dist /api/dist
 
 
-COPY --from=build /dashboard/.next /dashboard/.next
-COPY ./packages/dashboard /dashboard
+WORKDIR /dashboard
+COPY --from=builder /dashboard/next.config.js ./
+COPY --from=builder /dashboard/public ./public
+COPY --from=builder /dashboard/package.json ./package.json
+COPY --from=builder --chown=node:node /dashboard/.next/standalone ./
+COPY --from=builder --chown=node:node /dashboard/.next/static ./.next/static
 
 
 WORKDIR /
 WORKDIR /

+ 1 - 0
docker-compose.dev.yml

@@ -71,6 +71,7 @@ services:
       APPS_REPO_ID: ${APPS_REPO_ID}
       APPS_REPO_ID: ${APPS_REPO_ID}
       APPS_REPO_URL: ${APPS_REPO_URL}
       APPS_REPO_URL: ${APPS_REPO_URL}
       DOMAIN: ${DOMAIN}
       DOMAIN: ${DOMAIN}
+      ARCHITECTURE: ${ARCHITECTURE}
     networks:
     networks:
       - tipi_main_network
       - tipi_main_network
     labels:
     labels:

+ 2 - 1
docker-compose.rc.yml

@@ -63,6 +63,7 @@ services:
       APPS_REPO_ID: ${APPS_REPO_ID}
       APPS_REPO_ID: ${APPS_REPO_ID}
       APPS_REPO_URL: ${APPS_REPO_URL}
       APPS_REPO_URL: ${APPS_REPO_URL}
       DOMAIN: ${DOMAIN}
       DOMAIN: ${DOMAIN}
+      ARCHITECTURE: ${ARCHITECTURE}
     networks:
     networks:
       - tipi_main_network
       - tipi_main_network
     labels:
     labels:
@@ -85,7 +86,7 @@ services:
 
 
   dashboard:
   dashboard:
     image: meienberger/runtipi:rc-${TIPI_VERSION}
     image: meienberger/runtipi:rc-${TIPI_VERSION}
-    command: /bin/sh -c "cd /dashboard && npm run start"
+    command: /bin/sh -c "cd /dashboard && node server.js"
     container_name: dashboard
     container_name: dashboard
     networks:
     networks:
       - tipi_main_network
       - tipi_main_network

+ 2 - 1
docker-compose.yml

@@ -63,6 +63,7 @@ services:
       APPS_REPO_ID: ${APPS_REPO_ID}
       APPS_REPO_ID: ${APPS_REPO_ID}
       APPS_REPO_URL: ${APPS_REPO_URL}
       APPS_REPO_URL: ${APPS_REPO_URL}
       DOMAIN: ${DOMAIN}
       DOMAIN: ${DOMAIN}
+      ARCHITECTURE: ${ARCHITECTURE}
     networks:
     networks:
       - tipi_main_network
       - tipi_main_network
     labels:
     labels:
@@ -85,7 +86,7 @@ services:
 
 
   dashboard:
   dashboard:
     image: meienberger/runtipi:${TIPI_VERSION}
     image: meienberger/runtipi:${TIPI_VERSION}
-    command: /bin/sh -c "cd /dashboard && npm run start"
+    command: /bin/sh -c "cd /dashboard && node server.js"
     restart: unless-stopped
     restart: unless-stopped
     container_name: dashboard
     container_name: dashboard
     networks:
     networks:

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "runtipi",
   "name": "runtipi",
-  "version": "0.7.0",
+  "version": "0.7.1",
   "description": "A homeserver for everyone",
   "description": "A homeserver for everyone",
   "scripts": {
   "scripts": {
     "prepare": "husky install",
     "prepare": "husky install",

+ 1 - 2
packages/dashboard/next.config.js

@@ -1,7 +1,6 @@
 /** @type {import('next').NextConfig} */
 /** @type {import('next').NextConfig} */
-const { INTERNAL_IP, DOMAIN, NGINX_PORT } = process.env;
-
 const nextConfig = {
 const nextConfig = {
+  output: 'standalone',
   webpackDevMiddleware: (config) => {
   webpackDevMiddleware: (config) => {
     config.watchOptions = {
     config.watchOptions = {
       poll: 1000,
       poll: 1000,

+ 2 - 2
packages/dashboard/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "dashboard",
   "name": "dashboard",
-  "version": "0.7.0",
+  "version": "0.7.1",
   "private": true,
   "private": true,
   "scripts": {
   "scripts": {
     "test": "jest --colors",
     "test": "jest --colors",
@@ -22,7 +22,7 @@
     "framer-motion": "^6",
     "framer-motion": "^6",
     "graphql": "^15.8.0",
     "graphql": "^15.8.0",
     "graphql-tag": "^2.12.6",
     "graphql-tag": "^2.12.6",
-    "next": "12.1.6",
+    "next": "12.3.1",
     "react": "18.1.0",
     "react": "18.1.0",
     "react-dom": "18.1.0",
     "react-dom": "18.1.0",
     "react-final-form": "^6.5.9",
     "react-final-form": "^6.5.9",

+ 1 - 1
packages/system-api/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "system-api",
   "name": "system-api",
-  "version": "0.7.0",
+  "version": "0.7.1",
   "description": "",
   "description": "",
   "exports": "./dist/server.js",
   "exports": "./dist/server.js",
   "type": "module",
   "type": "module",

+ 4 - 0
packages/system-api/src/core/config/TipiConfig.ts

@@ -2,6 +2,7 @@ import { z } from 'zod';
 import * as dotenv from 'dotenv';
 import * as dotenv from 'dotenv';
 import fs from 'fs-extra';
 import fs from 'fs-extra';
 import { readJsonFile } from '../../modules/fs/fs.helpers';
 import { readJsonFile } from '../../modules/fs/fs.helpers';
+import { AppSupportedArchitecturesEnum } from '../../modules/apps/apps.types';
 
 
 if (process.env.NODE_ENV !== 'production') {
 if (process.env.NODE_ENV !== 'production') {
   dotenv.config({ path: '.env.dev' });
   dotenv.config({ path: '.env.dev' });
@@ -21,11 +22,13 @@ const {
   APPS_REPO_URL = '',
   APPS_REPO_URL = '',
   DOMAIN = '',
   DOMAIN = '',
   STORAGE_PATH = '/runtipi',
   STORAGE_PATH = '/runtipi',
+  ARCHITECTURE = 'amd64',
 } = process.env;
 } = process.env;
 
 
 const configSchema = z.object({
 const configSchema = z.object({
   NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
   NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
   status: z.union([z.literal('RUNNING'), z.literal('UPDATING'), z.literal('RESTARTING')]),
   status: z.union([z.literal('RUNNING'), z.literal('UPDATING'), z.literal('RESTARTING')]),
+  architecture: z.nativeEnum(AppSupportedArchitecturesEnum),
   logs: z.object({
   logs: z.object({
     LOGS_FOLDER: z.string(),
     LOGS_FOLDER: z.string(),
     LOGS_APP: z.string(),
     LOGS_APP: z.string(),
@@ -56,6 +59,7 @@ class Config {
         LOGS_ERROR,
         LOGS_ERROR,
       },
       },
       NODE_ENV: NODE_ENV as z.infer<typeof configSchema>['NODE_ENV'],
       NODE_ENV: NODE_ENV as z.infer<typeof configSchema>['NODE_ENV'],
+      architecture: ARCHITECTURE as z.infer<typeof configSchema>['architecture'],
       rootFolder: '/runtipi',
       rootFolder: '/runtipi',
       internalIp: INTERNAL_IP,
       internalIp: INTERNAL_IP,
       version: TIPI_VERSION,
       version: TIPI_VERSION,

+ 5 - 2
packages/system-api/src/modules/apps/__tests__/apps.factory.ts

@@ -1,5 +1,5 @@
 import { faker } from '@faker-js/faker';
 import { faker } from '@faker-js/faker';
-import { AppCategoriesEnum, AppInfo, AppStatusEnum, FieldTypes } from '../apps.types';
+import { AppCategoriesEnum, AppInfo, AppStatusEnum, AppSupportedArchitecturesEnum, FieldTypes } from '../apps.types';
 import App from '../app.entity';
 import App from '../app.entity';
 
 
 interface IProps {
 interface IProps {
@@ -10,10 +10,11 @@ interface IProps {
   exposed?: boolean;
   exposed?: boolean;
   domain?: string;
   domain?: string;
   exposable?: boolean;
   exposable?: boolean;
+  supportedArchitectures?: AppSupportedArchitecturesEnum[];
 }
 }
 
 
 const createApp = async (props: IProps) => {
 const createApp = async (props: IProps) => {
-  const { installed = false, status = AppStatusEnum.RUNNING, requiredPort, randomField = false, exposed = false, domain = '', exposable = false } = props;
+  const { installed = false, status = AppStatusEnum.RUNNING, requiredPort, randomField = false, exposed = false, domain = '', exposable = false, supportedArchitectures } = props;
 
 
   const categories = Object.values(AppCategoriesEnum);
   const categories = Object.values(AppCategoriesEnum);
 
 
@@ -29,6 +30,7 @@ const createApp = async (props: IProps) => {
         env_variable: 'TEST_FIELD',
         env_variable: 'TEST_FIELD',
       },
       },
     ],
     ],
+
     name: faker.random.word(),
     name: faker.random.word(),
     description: faker.random.words(),
     description: faker.random.words(),
     tipi_version: faker.datatype.number({ min: 1, max: 10 }),
     tipi_version: faker.datatype.number({ min: 1, max: 10 }),
@@ -37,6 +39,7 @@ const createApp = async (props: IProps) => {
     source: faker.internet.url(),
     source: faker.internet.url(),
     categories: [categories[faker.datatype.number({ min: 0, max: categories.length - 1 })]],
     categories: [categories[faker.datatype.number({ min: 0, max: categories.length - 1 })]],
     exposable,
     exposable,
+    supported_architectures: supportedArchitectures,
   };
   };
 
 
   if (randomField) {
   if (randomField) {

+ 78 - 1
packages/system-api/src/modules/apps/__tests__/apps.service.test.ts

@@ -1,12 +1,13 @@
 import AppsService from '../apps.service';
 import AppsService from '../apps.service';
 import fs from 'fs-extra';
 import fs from 'fs-extra';
-import { AppInfo, AppStatusEnum } from '../apps.types';
+import { AppInfo, AppStatusEnum, AppSupportedArchitecturesEnum } from '../apps.types';
 import App from '../app.entity';
 import App from '../app.entity';
 import { createApp } from './apps.factory';
 import { createApp } from './apps.factory';
 import { setupConnection, teardownConnection } from '../../../test/connection';
 import { setupConnection, teardownConnection } from '../../../test/connection';
 import { DataSource } from 'typeorm';
 import { DataSource } from 'typeorm';
 import { getEnvMap } from '../apps.helpers';
 import { getEnvMap } from '../apps.helpers';
 import EventDispatcher, { eventDispatcher, EventTypes } from '../../../core/config/EventDispatcher';
 import EventDispatcher, { eventDispatcher, EventTypes } from '../../../core/config/EventDispatcher';
+import { setConfig } from '../../../core/config/TipiConfig';
 
 
 jest.mock('fs-extra');
 jest.mock('fs-extra');
 jest.mock('child_process');
 jest.mock('child_process');
@@ -152,6 +153,38 @@ describe('Install app', () => {
 
 
     await expect(AppsService.installApp(app3.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com')).rejects.toThrowError(`Domain test.com already in use by app ${app2.appInfo.id}`);
     await expect(AppsService.installApp(app3.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com')).rejects.toThrowError(`Domain test.com already in use by app ${app2.appInfo.id}`);
   });
   });
+
+  it('Should throw if architecure is not supported', async () => {
+    const { MockFiles, appInfo } = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM] });
+    // @ts-ignore
+    fs.__createMockFiles(MockFiles);
+
+    await expect(AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' })).rejects.toThrowError(`App ${appInfo.id} is not supported on this architecture`);
+  });
+
+  it('Can install if architecture is supported', async () => {
+    setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
+    const { MockFiles, appInfo } = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM, AppSupportedArchitecturesEnum.ARM64] });
+    // @ts-ignore
+    fs.__createMockFiles(MockFiles);
+
+    await AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' });
+    const app = await App.findOne({ where: { id: appInfo.id } });
+
+    expect(app).toBeDefined();
+  });
+
+  it('Can install if no architecture is specified', async () => {
+    setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
+    const { MockFiles, appInfo } = await createApp({ supportedArchitectures: undefined });
+    // @ts-ignore
+    fs.__createMockFiles(MockFiles);
+
+    await AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' });
+    const app = await App.findOne({ where: { id: appInfo.id } });
+
+    expect(app).toBeDefined();
+  });
 });
 });
 
 
 describe('Uninstall app', () => {
 describe('Uninstall app', () => {
@@ -431,6 +464,50 @@ describe('List apps', () => {
     expect(apps[1].id).toBe(sortedApps[1].id);
     expect(apps[1].id).toBe(sortedApps[1].id);
     expect(apps[0].description).toBe('md desc');
     expect(apps[0].description).toBe('md desc');
   });
   });
+
+  it('Should not list apps that have supportedArchitectures and are not supported', async () => {
+    // Arrange
+    setConfig('architecture', AppSupportedArchitecturesEnum.ARM64);
+    const app3 = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM] });
+    // @ts-ignore
+    fs.__createMockFiles(Object.assign(app3.MockFiles));
+
+    // Act
+    const { apps } = await AppsService.listApps();
+
+    // Assert
+    expect(apps).toBeDefined();
+    expect(apps.length).toBe(0);
+  });
+
+  it('Should list apps that have supportedArchitectures and are supported', async () => {
+    // Arrange
+    setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
+    const app3 = await createApp({ supportedArchitectures: [AppSupportedArchitecturesEnum.ARM] });
+    // @ts-ignore
+    fs.__createMockFiles(Object.assign(app3.MockFiles));
+    // Act
+    const { apps } = await AppsService.listApps();
+
+    // Assert
+    expect(apps).toBeDefined();
+    expect(apps.length).toBe(1);
+  });
+
+  it('Should list apps that have no supportedArchitectures specified', async () => {
+    // Arrange
+    setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
+    const app3 = await createApp({});
+    // @ts-ignore
+    fs.__createMockFiles(Object.assign(app3.MockFiles));
+
+    // Act
+    const { apps } = await AppsService.listApps();
+
+    // Assert
+    expect(apps).toBeDefined();
+    expect(apps.length).toBe(1);
+  });
 });
 });
 
 
 describe('Start all apps', () => {
 describe('Start all apps', () => {

+ 4 - 0
packages/system-api/src/modules/apps/apps.helpers.ts

@@ -26,6 +26,10 @@ export const checkAppRequirements = async (appName: string) => {
     }
     }
   }
   }
 
 
+  if (configFile?.supported_architectures && !configFile.supported_architectures.includes(getConfig().architecture)) {
+    throw new Error(`App ${appName} is not supported on this architecture`);
+  }
+
   return valid;
   return valid;
 };
 };
 
 

+ 13 - 1
packages/system-api/src/modules/apps/apps.service.ts

@@ -9,6 +9,18 @@ import { getConfig } from '../../core/config/TipiConfig';
 import { eventDispatcher, EventTypes } from '../../core/config/EventDispatcher';
 import { eventDispatcher, EventTypes } from '../../core/config/EventDispatcher';
 
 
 const sortApps = (a: AppInfo, b: AppInfo) => a.name.localeCompare(b.name);
 const sortApps = (a: AppInfo, b: AppInfo) => a.name.localeCompare(b.name);
+const filterApp = (app: AppInfo): boolean => {
+  if (!app.supported_architectures) {
+    return true;
+  }
+
+  const arch = getConfig().architecture;
+  return app.supported_architectures.includes(arch);
+};
+
+const filterApps = (apps: AppInfo[]): AppInfo[] => {
+  return apps.sort(sortApps).filter(filterApp);
+};
 
 
 /**
 /**
  * Start all apps which had the status RUNNING in the database
  * Start all apps which had the status RUNNING in the database
@@ -159,7 +171,7 @@ const listApps = async (): Promise<ListAppsResonse> => {
     app.description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app.id}/metadata/description.md`);
     app.description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app.id}/metadata/description.md`);
   });
   });
 
 
-  return { apps: apps.sort(sortApps), total: apps.length };
+  return { apps: filterApps(apps), total: apps.length };
 };
 };
 
 
 /**
 /**

+ 13 - 0
packages/system-api/src/modules/apps/apps.types.ts

@@ -41,6 +41,12 @@ export enum AppStatusEnum {
   UPDATING = 'updating',
   UPDATING = 'updating',
 }
 }
 
 
+export enum AppSupportedArchitecturesEnum {
+  ARM = 'arm',
+  ARM64 = 'arm64',
+  AMD64 = 'amd64',
+}
+
 registerEnumType(AppCategoriesEnum, {
 registerEnumType(AppCategoriesEnum, {
   name: 'AppCategoriesEnum',
   name: 'AppCategoriesEnum',
 });
 });
@@ -49,6 +55,10 @@ registerEnumType(FieldTypes, {
   name: 'FieldTypesEnum',
   name: 'FieldTypesEnum',
 });
 });
 
 
+registerEnumType(AppSupportedArchitecturesEnum, {
+  name: 'AppSupportedArchitecturesEnum',
+});
+
 @ObjectType()
 @ObjectType()
 class FormField {
 class FormField {
   @Field(() => FieldTypes)
   @Field(() => FieldTypes)
@@ -128,6 +138,9 @@ class AppInfo {
 
 
   @Field(() => Boolean, { nullable: true })
   @Field(() => Boolean, { nullable: true })
   exposable?: boolean;
   exposable?: boolean;
+
+  @Field(() => [AppSupportedArchitecturesEnum], { nullable: true })
+  supported_architectures?: AppSupportedArchitecturesEnum[];
 }
 }
 
 
 @ObjectType()
 @ObjectType()

+ 6 - 1
packages/system-api/src/server.ts

@@ -27,12 +27,16 @@ let corsOptions = {
       return callback(null, true);
       return callback(null, true);
     }
     }
     // disallow requests with no origin
     // disallow requests with no origin
-    if (!origin) return callback(new Error('Not allowed by CORS'), false);
+    if (!origin) {
+      logger.error('No origin');
+      return callback(new Error('Not allowed by CORS'), false);
+    }
 
 
     if (getConfig().clientUrls.includes(origin)) {
     if (getConfig().clientUrls.includes(origin)) {
       return callback(null, true);
       return callback(null, true);
     }
     }
 
 
+    logger.error(`Origin ${origin} not allowed by CORS`);
     const message = "The CORS policy for this origin doesn't allow access from the particular origin.";
     const message = "The CORS policy for this origin doesn't allow access from the particular origin.";
     return callback(new Error(message), false);
     return callback(new Error(message), false);
   },
   },
@@ -103,6 +107,7 @@ const main = async () => {
       // Start apps
       // Start apps
       appsService.startAllApps();
       appsService.startAllApps();
       logger.info(`Server running on port ${port} 🚀 Production => ${__prod__}`);
       logger.info(`Server running on port ${port} 🚀 Production => ${__prod__}`);
+      logger.info(`Config: ${JSON.stringify(getConfig(), null, 2)}`);
     });
     });
   } catch (error) {
   } catch (error) {
     logger.error(error);
     logger.error(error);

+ 84 - 53
pnpm-lock.yaml

@@ -49,7 +49,7 @@ importers:
       graphql: ^15.8.0
       graphql: ^15.8.0
       graphql-tag: ^2.12.6
       graphql-tag: ^2.12.6
       jest: ^28.1.0
       jest: ^28.1.0
-      next: 12.1.6
+      next: 12.3.1
       postcss: ^8.4.12
       postcss: ^8.4.12
       react: 18.1.0
       react: 18.1.0
       react-dom: 18.1.0
       react-dom: 18.1.0
@@ -78,7 +78,7 @@ importers:
       framer-motion: 6.3.3_ef5jwxihqo6n7gxfmzogljlgcm
       framer-motion: 6.3.3_ef5jwxihqo6n7gxfmzogljlgcm
       graphql: 15.8.0
       graphql: 15.8.0
       graphql-tag: 2.12.6_graphql@15.8.0
       graphql-tag: 2.12.6_graphql@15.8.0
-      next: 12.1.6_talmm3uuvp6ssixt2qevhfgvue
+      next: 12.3.1_talmm3uuvp6ssixt2qevhfgvue
       react: 18.1.0
       react: 18.1.0
       react-dom: 18.1.0_react@18.1.0
       react-dom: 18.1.0_react@18.1.0
       react-final-form: 6.5.9_bnxchjdfy45cdln7bu7hnhf37u
       react-final-form: 6.5.9_bnxchjdfy45cdln7bu7hnhf37u
@@ -109,7 +109,7 @@ importers:
       autoprefixer: 10.4.7_postcss@8.4.13
       autoprefixer: 10.4.7_postcss@8.4.13
       eslint: 8.12.0
       eslint: 8.12.0
       eslint-config-airbnb-typescript: 17.0.0_r46exuh3jlhq2wmrnqx2ufqspa
       eslint-config-airbnb-typescript: 17.0.0_r46exuh3jlhq2wmrnqx2ufqspa
-      eslint-config-next: 12.1.4_e6a2zi6fqdwfehht5cxvkmo3zu
+      eslint-config-next: 12.1.4_c2ous3fcmy6f3xlzfnkf2jzbvm
       eslint-plugin-import: 2.26.0_hhyjdrupy4c2vgtpytri6cjwoy
       eslint-plugin-import: 2.26.0_hhyjdrupy4c2vgtpytri6cjwoy
       jest: 28.1.0_@types+node@17.0.31
       jest: 28.1.0_@types+node@17.0.31
       postcss: 8.4.13
       postcss: 8.4.13
@@ -3169,8 +3169,8 @@ packages:
       graphql: 15.8.0
       graphql: 15.8.0
     dev: true
     dev: true
 
 
-  /@next/env/12.1.6:
-    resolution: {integrity: sha512-Te/OBDXFSodPU6jlXYPAXpmZr/AkG6DCATAxttQxqOWaq6eDFX25Db3dK0120GZrSZmv4QCe9KsZmJKDbWs4OA==}
+  /@next/env/12.3.1:
+    resolution: {integrity: sha512-9P9THmRFVKGKt9DYqeC2aKIxm8rlvkK38V1P1sRE7qyoPBIs8l9oo79QoSdPtOWfzkbDAVUqvbQGgTMsb8BtJg==}
     dev: false
     dev: false
 
 
   /@next/eslint-plugin-next/12.1.4:
   /@next/eslint-plugin-next/12.1.4:
@@ -3179,8 +3179,8 @@ packages:
       glob: 7.1.7
       glob: 7.1.7
     dev: true
     dev: true
 
 
-  /@next/swc-android-arm-eabi/12.1.6:
-    resolution: {integrity: sha512-BxBr3QAAAXWgk/K7EedvzxJr2dE014mghBSA9iOEAv0bMgF+MRq4PoASjuHi15M2zfowpcRG8XQhMFtxftCleQ==}
+  /@next/swc-android-arm-eabi/12.3.1:
+    resolution: {integrity: sha512-i+BvKA8tB//srVPPQxIQN5lvfROcfv4OB23/L1nXznP+N/TyKL8lql3l7oo2LNhnH66zWhfoemg3Q4VJZSruzQ==}
     engines: {node: '>= 10'}
     engines: {node: '>= 10'}
     cpu: [arm]
     cpu: [arm]
     os: [android]
     os: [android]
@@ -3188,8 +3188,8 @@ packages:
     dev: false
     dev: false
     optional: true
     optional: true
 
 
-  /@next/swc-android-arm64/12.1.6:
-    resolution: {integrity: sha512-EboEk3ROYY7U6WA2RrMt/cXXMokUTXXfnxe2+CU+DOahvbrO8QSWhlBl9I9ZbFzJx28AGB9Yo3oQHCvph/4Lew==}
+  /@next/swc-android-arm64/12.3.1:
+    resolution: {integrity: sha512-CmgU2ZNyBP0rkugOOqLnjl3+eRpXBzB/I2sjwcGZ7/Z6RcUJXK5Evz+N0ucOxqE4cZ3gkTeXtSzRrMK2mGYV8Q==}
     engines: {node: '>= 10'}
     engines: {node: '>= 10'}
     cpu: [arm64]
     cpu: [arm64]
     os: [android]
     os: [android]
@@ -3197,8 +3197,8 @@ packages:
     dev: false
     dev: false
     optional: true
     optional: true
 
 
-  /@next/swc-darwin-arm64/12.1.6:
-    resolution: {integrity: sha512-P0EXU12BMSdNj1F7vdkP/VrYDuCNwBExtRPDYawgSUakzi6qP0iKJpya2BuLvNzXx+XPU49GFuDC5X+SvY0mOw==}
+  /@next/swc-darwin-arm64/12.3.1:
+    resolution: {integrity: sha512-hT/EBGNcu0ITiuWDYU9ur57Oa4LybD5DOQp4f22T6zLfpoBMfBibPtR8XktXmOyFHrL/6FC2p9ojdLZhWhvBHg==}
     engines: {node: '>= 10'}
     engines: {node: '>= 10'}
     cpu: [arm64]
     cpu: [arm64]
     os: [darwin]
     os: [darwin]
@@ -3206,8 +3206,8 @@ packages:
     dev: false
     dev: false
     optional: true
     optional: true
 
 
-  /@next/swc-darwin-x64/12.1.6:
-    resolution: {integrity: sha512-9FptMnbgHJK3dRDzfTpexs9S2hGpzOQxSQbe8omz6Pcl7rnEp9x4uSEKY51ho85JCjL4d0tDLBcXEJZKKLzxNg==}
+  /@next/swc-darwin-x64/12.3.1:
+    resolution: {integrity: sha512-9S6EVueCVCyGf2vuiLiGEHZCJcPAxglyckTZcEwLdJwozLqN0gtS0Eq0bQlGS3dH49Py/rQYpZ3KVWZ9BUf/WA==}
     engines: {node: '>= 10'}
     engines: {node: '>= 10'}
     cpu: [x64]
     cpu: [x64]
     os: [darwin]
     os: [darwin]
@@ -3215,8 +3215,17 @@ packages:
     dev: false
     dev: false
     optional: true
     optional: true
 
 
-  /@next/swc-linux-arm-gnueabihf/12.1.6:
-    resolution: {integrity: sha512-PvfEa1RR55dsik/IDkCKSFkk6ODNGJqPY3ysVUZqmnWMDSuqFtf7BPWHFa/53znpvVB5XaJ5Z1/6aR5CTIqxPw==}
+  /@next/swc-freebsd-x64/12.3.1:
+    resolution: {integrity: sha512-qcuUQkaBZWqzM0F1N4AkAh88lLzzpfE6ImOcI1P6YeyJSsBmpBIV8o70zV+Wxpc26yV9vpzb+e5gCyxNjKJg5Q==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [freebsd]
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /@next/swc-linux-arm-gnueabihf/12.3.1:
+    resolution: {integrity: sha512-diL9MSYrEI5nY2wc/h/DBewEDUzr/DqBjIgHJ3RUNtETAOB3spMNHvJk2XKUDjnQuluLmFMloet9tpEqU2TT9w==}
     engines: {node: '>= 10'}
     engines: {node: '>= 10'}
     cpu: [arm]
     cpu: [arm]
     os: [linux]
     os: [linux]
@@ -3224,8 +3233,8 @@ packages:
     dev: false
     dev: false
     optional: true
     optional: true
 
 
-  /@next/swc-linux-arm64-gnu/12.1.6:
-    resolution: {integrity: sha512-53QOvX1jBbC2ctnmWHyRhMajGq7QZfl974WYlwclXarVV418X7ed7o/EzGY+YVAEKzIVaAB9JFFWGXn8WWo0gQ==}
+  /@next/swc-linux-arm64-gnu/12.3.1:
+    resolution: {integrity: sha512-o/xB2nztoaC7jnXU3Q36vGgOolJpsGG8ETNjxM1VAPxRwM7FyGCPHOMk1XavG88QZSQf+1r+POBW0tLxQOJ9DQ==}
     engines: {node: '>= 10'}
     engines: {node: '>= 10'}
     cpu: [arm64]
     cpu: [arm64]
     os: [linux]
     os: [linux]
@@ -3233,8 +3242,8 @@ packages:
     dev: false
     dev: false
     optional: true
     optional: true
 
 
-  /@next/swc-linux-arm64-musl/12.1.6:
-    resolution: {integrity: sha512-CMWAkYqfGdQCS+uuMA1A2UhOfcUYeoqnTW7msLr2RyYAys15pD960hlDfq7QAi8BCAKk0sQ2rjsl0iqMyziohQ==}
+  /@next/swc-linux-arm64-musl/12.3.1:
+    resolution: {integrity: sha512-2WEasRxJzgAmP43glFNhADpe8zB7kJofhEAVNbDJZANp+H4+wq+/cW1CdDi8DqjkShPEA6/ejJw+xnEyDID2jg==}
     engines: {node: '>= 10'}
     engines: {node: '>= 10'}
     cpu: [arm64]
     cpu: [arm64]
     os: [linux]
     os: [linux]
@@ -3242,8 +3251,8 @@ packages:
     dev: false
     dev: false
     optional: true
     optional: true
 
 
-  /@next/swc-linux-x64-gnu/12.1.6:
-    resolution: {integrity: sha512-AC7jE4Fxpn0s3ujngClIDTiEM/CQiB2N2vkcyWWn6734AmGT03Duq6RYtPMymFobDdAtZGFZd5nR95WjPzbZAQ==}
+  /@next/swc-linux-x64-gnu/12.3.1:
+    resolution: {integrity: sha512-JWEaMyvNrXuM3dyy9Pp5cFPuSSvG82+yABqsWugjWlvfmnlnx9HOQZY23bFq3cNghy5V/t0iPb6cffzRWylgsA==}
     engines: {node: '>= 10'}
     engines: {node: '>= 10'}
     cpu: [x64]
     cpu: [x64]
     os: [linux]
     os: [linux]
@@ -3251,8 +3260,8 @@ packages:
     dev: false
     dev: false
     optional: true
     optional: true
 
 
-  /@next/swc-linux-x64-musl/12.1.6:
-    resolution: {integrity: sha512-c9Vjmi0EVk0Kou2qbrynskVarnFwfYIi+wKufR9Ad7/IKKuP6aEhOdZiIIdKsYWRtK2IWRF3h3YmdnEa2WLUag==}
+  /@next/swc-linux-x64-musl/12.3.1:
+    resolution: {integrity: sha512-xoEWQQ71waWc4BZcOjmatuvPUXKTv6MbIFzpm4LFeCHsg2iwai0ILmNXf81rJR+L1Wb9ifEke2sQpZSPNz1Iyg==}
     engines: {node: '>= 10'}
     engines: {node: '>= 10'}
     cpu: [x64]
     cpu: [x64]
     os: [linux]
     os: [linux]
@@ -3260,8 +3269,8 @@ packages:
     dev: false
     dev: false
     optional: true
     optional: true
 
 
-  /@next/swc-win32-arm64-msvc/12.1.6:
-    resolution: {integrity: sha512-3UTOL/5XZSKFelM7qN0it35o3Cegm6LsyuERR3/OoqEExyj3aCk7F025b54/707HTMAnjlvQK3DzLhPu/xxO4g==}
+  /@next/swc-win32-arm64-msvc/12.3.1:
+    resolution: {integrity: sha512-hswVFYQYIeGHE2JYaBVtvqmBQ1CppplQbZJS/JgrVI3x2CurNhEkmds/yqvDONfwfbttTtH4+q9Dzf/WVl3Opw==}
     engines: {node: '>= 10'}
     engines: {node: '>= 10'}
     cpu: [arm64]
     cpu: [arm64]
     os: [win32]
     os: [win32]
@@ -3269,8 +3278,8 @@ packages:
     dev: false
     dev: false
     optional: true
     optional: true
 
 
-  /@next/swc-win32-ia32-msvc/12.1.6:
-    resolution: {integrity: sha512-8ZWoj6nCq6fI1yCzKq6oK0jE6Mxlz4MrEsRyu0TwDztWQWe7rh4XXGLAa2YVPatYcHhMcUL+fQQbqd1MsgaSDA==}
+  /@next/swc-win32-ia32-msvc/12.3.1:
+    resolution: {integrity: sha512-Kny5JBehkTbKPmqulr5i+iKntO5YMP+bVM8Hf8UAmjSMVo3wehyLVc9IZkNmcbxi+vwETnQvJaT5ynYBkJ9dWA==}
     engines: {node: '>= 10'}
     engines: {node: '>= 10'}
     cpu: [ia32]
     cpu: [ia32]
     os: [win32]
     os: [win32]
@@ -3278,8 +3287,8 @@ packages:
     dev: false
     dev: false
     optional: true
     optional: true
 
 
-  /@next/swc-win32-x64-msvc/12.1.6:
-    resolution: {integrity: sha512-4ZEwiRuZEicXhXqmhw3+de8Z4EpOLQj/gp+D9fFWo6ii6W1kBkNNvvEx4A90ugppu+74pT1lIJnOuz3A9oQeJA==}
+  /@next/swc-win32-x64-msvc/12.3.1:
+    resolution: {integrity: sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA==}
     engines: {node: '>= 10'}
     engines: {node: '>= 10'}
     cpu: [x64]
     cpu: [x64]
     os: [win32]
     os: [win32]
@@ -3562,6 +3571,12 @@ packages:
       '@swc/core-win32-x64-msvc': 1.2.210
       '@swc/core-win32-x64-msvc': 1.2.210
     dev: true
     dev: true
 
 
+  /@swc/helpers/0.4.11:
+    resolution: {integrity: sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==}
+    dependencies:
+      tslib: 2.4.0
+    dev: false
+
   /@szmarczak/http-timer/1.1.2:
   /@szmarczak/http-timer/1.1.2:
     resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==}
     resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==}
     engines: {node: '>=6'}
     engines: {node: '>=6'}
@@ -5047,6 +5062,11 @@ packages:
 
 
   /caniuse-lite/1.0.30001338:
   /caniuse-lite/1.0.30001338:
     resolution: {integrity: sha512-1gLHWyfVoRDsHieO+CaeYe7jSo/MT7D7lhaXUiwwbuR5BwQxORs0f1tAwUSQr3YbxRXJvxHM/PA5FfPQRnsPeQ==}
     resolution: {integrity: sha512-1gLHWyfVoRDsHieO+CaeYe7jSo/MT7D7lhaXUiwwbuR5BwQxORs0f1tAwUSQr3YbxRXJvxHM/PA5FfPQRnsPeQ==}
+    dev: true
+
+  /caniuse-lite/1.0.30001419:
+    resolution: {integrity: sha512-aFO1r+g6R7TW+PNQxKzjITwLOyDhVRLjW0LcwS/HCZGUUKTGNp9+IwLC4xyDSZBygVL/mxaFR3HIV6wEKQuSzw==}
+    dev: false
 
 
   /capital-case/1.0.4:
   /capital-case/1.0.4:
     resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
     resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
@@ -6119,7 +6139,7 @@ packages:
       eslint-plugin-import: 2.26.0_hhyjdrupy4c2vgtpytri6cjwoy
       eslint-plugin-import: 2.26.0_hhyjdrupy4c2vgtpytri6cjwoy
     dev: true
     dev: true
 
 
-  /eslint-config-next/12.1.4_e6a2zi6fqdwfehht5cxvkmo3zu:
+  /eslint-config-next/12.1.4_c2ous3fcmy6f3xlzfnkf2jzbvm:
     resolution: {integrity: sha512-Uj0jrVjoQbg9qerxRjSHoOOv3PEzoZxpb8G9LYct25fsflP8xIiUq0l4WEu2KSB5owuLv5hie7wSMqPEsHj+bQ==}
     resolution: {integrity: sha512-Uj0jrVjoQbg9qerxRjSHoOOv3PEzoZxpb8G9LYct25fsflP8xIiUq0l4WEu2KSB5owuLv5hie7wSMqPEsHj+bQ==}
     peerDependencies:
     peerDependencies:
       eslint: ^7.23.0 || ^8.0.0
       eslint: ^7.23.0 || ^8.0.0
@@ -6139,7 +6159,7 @@ packages:
       eslint-plugin-jsx-a11y: 6.5.1_eslint@8.12.0
       eslint-plugin-jsx-a11y: 6.5.1_eslint@8.12.0
       eslint-plugin-react: 7.29.1_eslint@8.12.0
       eslint-plugin-react: 7.29.1_eslint@8.12.0
       eslint-plugin-react-hooks: 4.3.0_eslint@8.12.0
       eslint-plugin-react-hooks: 4.3.0_eslint@8.12.0
-      next: 12.1.6_talmm3uuvp6ssixt2qevhfgvue
+      next: 12.3.1_talmm3uuvp6ssixt2qevhfgvue
       typescript: 4.6.4
       typescript: 4.6.4
     transitivePeerDependencies:
     transitivePeerDependencies:
       - eslint-import-resolver-webpack
       - eslint-import-resolver-webpack
@@ -9686,8 +9706,8 @@ packages:
     engines: {node: '>= 0.6'}
     engines: {node: '>= 0.6'}
     dev: false
     dev: false
 
 
-  /next/12.1.6_talmm3uuvp6ssixt2qevhfgvue:
-    resolution: {integrity: sha512-cebwKxL3/DhNKfg9tPZDQmbRKjueqykHHbgaoG4VBRH3AHQJ2HO0dbKFiS1hPhe1/qgc2d/hFeadsbPicmLD+A==}
+  /next/12.3.1_talmm3uuvp6ssixt2qevhfgvue:
+    resolution: {integrity: sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw==}
     engines: {node: '>=12.22.0'}
     engines: {node: '>=12.22.0'}
     hasBin: true
     hasBin: true
     peerDependencies:
     peerDependencies:
@@ -9704,25 +9724,28 @@ packages:
       sass:
       sass:
         optional: true
         optional: true
     dependencies:
     dependencies:
-      '@next/env': 12.1.6
-      caniuse-lite: 1.0.30001338
-      postcss: 8.4.5
+      '@next/env': 12.3.1
+      '@swc/helpers': 0.4.11
+      caniuse-lite: 1.0.30001419
+      postcss: 8.4.14
       react: 18.1.0
       react: 18.1.0
       react-dom: 18.1.0_react@18.1.0
       react-dom: 18.1.0_react@18.1.0
-      styled-jsx: 5.0.2_vm2wkhzl5f5eyl7hfuywll6uzq
+      styled-jsx: 5.0.7_vm2wkhzl5f5eyl7hfuywll6uzq
+      use-sync-external-store: 1.2.0_react@18.1.0
     optionalDependencies:
     optionalDependencies:
-      '@next/swc-android-arm-eabi': 12.1.6
-      '@next/swc-android-arm64': 12.1.6
-      '@next/swc-darwin-arm64': 12.1.6
-      '@next/swc-darwin-x64': 12.1.6
-      '@next/swc-linux-arm-gnueabihf': 12.1.6
-      '@next/swc-linux-arm64-gnu': 12.1.6
-      '@next/swc-linux-arm64-musl': 12.1.6
-      '@next/swc-linux-x64-gnu': 12.1.6
-      '@next/swc-linux-x64-musl': 12.1.6
-      '@next/swc-win32-arm64-msvc': 12.1.6
-      '@next/swc-win32-ia32-msvc': 12.1.6
-      '@next/swc-win32-x64-msvc': 12.1.6
+      '@next/swc-android-arm-eabi': 12.3.1
+      '@next/swc-android-arm64': 12.3.1
+      '@next/swc-darwin-arm64': 12.3.1
+      '@next/swc-darwin-x64': 12.3.1
+      '@next/swc-freebsd-x64': 12.3.1
+      '@next/swc-linux-arm-gnueabihf': 12.3.1
+      '@next/swc-linux-arm64-gnu': 12.3.1
+      '@next/swc-linux-arm64-musl': 12.3.1
+      '@next/swc-linux-x64-gnu': 12.3.1
+      '@next/swc-linux-x64-musl': 12.3.1
+      '@next/swc-win32-arm64-msvc': 12.3.1
+      '@next/swc-win32-ia32-msvc': 12.3.1
+      '@next/swc-win32-x64-msvc': 12.3.1
     transitivePeerDependencies:
     transitivePeerDependencies:
       - '@babel/core'
       - '@babel/core'
       - babel-plugin-macros
       - babel-plugin-macros
@@ -10371,8 +10394,8 @@ packages:
       source-map-js: 1.0.2
       source-map-js: 1.0.2
     dev: true
     dev: true
 
 
-  /postcss/8.4.5:
-    resolution: {integrity: sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==}
+  /postcss/8.4.14:
+    resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==}
     engines: {node: ^10 || ^12 || >=14}
     engines: {node: ^10 || ^12 || >=14}
     dependencies:
     dependencies:
       nanoid: 3.3.4
       nanoid: 3.3.4
@@ -11453,8 +11476,8 @@ packages:
       tslib: 2.4.0
       tslib: 2.4.0
     dev: false
     dev: false
 
 
-  /styled-jsx/5.0.2_vm2wkhzl5f5eyl7hfuywll6uzq:
-    resolution: {integrity: sha512-LqPQrbBh3egD57NBcHET4qcgshPks+yblyhPlH2GY8oaDgKs8SK4C3dBh3oSJjgzJ3G5t1SYEZGHkP+QEpX9EQ==}
+  /styled-jsx/5.0.7_vm2wkhzl5f5eyl7hfuywll6uzq:
+    resolution: {integrity: sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==}
     engines: {node: '>= 12.0.0'}
     engines: {node: '>= 12.0.0'}
     peerDependencies:
     peerDependencies:
       '@babel/core': '*'
       '@babel/core': '*'
@@ -12289,6 +12312,14 @@ packages:
       tslib: 2.4.0
       tslib: 2.4.0
     dev: false
     dev: false
 
 
+  /use-sync-external-store/1.2.0_react@18.1.0:
+    resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+    dependencies:
+      react: 18.1.0
+    dev: false
+
   /util-deprecate/1.0.2:
   /util-deprecate/1.0.2:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
 
 

+ 19 - 4
scripts/app.sh

@@ -141,9 +141,13 @@ if [[ "$command" = "uninstall" ]]; then
     write_log "Failed to uninstall app ${app}"
     write_log "Failed to uninstall app ${app}"
     exit 1
     exit 1
   fi
   fi
+
   if ! compose "${app}" down --rmi all --remove-orphans; then
   if ! compose "${app}" down --rmi all --remove-orphans; then
-    write_log "Failed to uninstall app ${app}"
-    exit 1
+    # just stop it if we can't remove the images
+    if ! compose "${app}" rm --force --stop; then
+      write_log "Failed to uninstall app ${app}"
+      exit 1
+    fi
   fi
   fi
 
 
   write_log "Deleting app data for app ${app}..."
   write_log "Deleting app data for app ${app}..."
@@ -167,8 +171,11 @@ if [[ "$command" = "update" ]]; then
   fi
   fi
 
 
   if ! compose "${app}" down --rmi all --remove-orphans; then
   if ! compose "${app}" down --rmi all --remove-orphans; then
-    write_log "Failed to update app ${app}"
-    exit 1
+    # just stop it if we can't remove the images
+    if ! compose "${app}" rm --force --stop; then
+      write_log "Failed to uninstall app ${app}"
+      exit 1
+    fi
   fi
   fi
 
 
   # Remove app
   # Remove app
@@ -214,4 +221,12 @@ if [[ "$command" = "compose" ]]; then
   exit 0
   exit 0
 fi
 fi
 
 
+if [[ "$command" = "clean" ]]; then
+  # Remove all stopped containers and unused images
+  write_log "Cleaning up..."
+  docker system prune --all --force
+
+  exit 0
+fi
+
 exit 1
 exit 1

+ 1 - 1
scripts/git.sh

@@ -55,7 +55,7 @@ if [[ "$command" = "update" ]]; then
     write_log "Updating ${repo} in ${repo_hash}"
     write_log "Updating ${repo} in ${repo_hash}"
     cd "${repo_dir}" || exit
     cd "${repo_dir}" || exit
 
 
-    if ! git pull origin master; then
+    if ! git pull origin "$(git rev-parse --abbrev-ref HEAD)"; then
         cd "${ROOT_FOLDER}" || exit
         cd "${ROOT_FOLDER}" || exit
         write_log "Failed to update repo"
         write_log "Failed to update repo"
         exit 1
         exit 1

+ 13 - 1
scripts/start-dev.sh

@@ -12,7 +12,7 @@ SED_ROOT_FOLDER="$(echo "$ROOT_FOLDER" | sed 's/\//\\\//g')"
 NGINX_PORT=80
 NGINX_PORT=80
 NGINX_PORT_SSL=443
 NGINX_PORT_SSL=443
 DOMAIN=tipi.localhost
 DOMAIN=tipi.localhost
-DNS_IP=9.9.9.9 # Default to Quad9 DNS
+DNS_IP="9.9.9.9" # Default to Quad9 DNS
 ARCHITECTURE="$(uname -m)"
 ARCHITECTURE="$(uname -m)"
 TZ="UTC"
 TZ="UTC"
 JWT_SECRET=secret
 JWT_SECRET=secret
@@ -21,6 +21,18 @@ TIPI_VERSION=$(get_json_field "${ROOT_FOLDER}/package.json" version)
 INTERNAL_IP=localhost
 INTERNAL_IP=localhost
 storage_path="${ROOT_FOLDER}"
 storage_path="${ROOT_FOLDER}"
 STORAGE_PATH_ESCAPED="$(echo "${storage_path}" | sed 's/\//\\\//g')"
 STORAGE_PATH_ESCAPED="$(echo "${storage_path}" | sed 's/\//\\\//g')"
+if [[ "$ARCHITECTURE" == "aarch64" ]]; then
+    ARCHITECTURE="arm64"
+elif [[ "$ARCHITECTURE" == "armv7l" ]]; then
+    ARCHITECTURE="arm"
+elif [[ "$ARCHITECTURE" == "x86_64" ]]; then
+    ARCHITECTURE="amd64"
+fi
+# If none of the above conditions are met, the architecture is not supported
+if [[ "$ARCHITECTURE" != "arm64" ]] && [[ "$ARCHITECTURE" != "arm" ]] && [[ "$ARCHITECTURE" != "amd64" ]]; then
+    echo "Architecture not supported!"
+    exit 1
+fi
 
 
 ### --------------------------------
 ### --------------------------------
 ### Apps repository configuration
 ### Apps repository configuration

+ 26 - 13
scripts/start.sh

@@ -12,7 +12,9 @@ ensure_pwd
 ensure_root
 ensure_root
 clean_logs
 clean_logs
 
 
-# Configure Tipi
+### --------------------------------
+### Pre-configuration
+### --------------------------------
 "${ROOT_FOLDER}/scripts/configure.sh"
 "${ROOT_FOLDER}/scripts/configure.sh"
 
 
 STATE_FOLDER="${ROOT_FOLDER}/state"
 STATE_FOLDER="${ROOT_FOLDER}/state"
@@ -22,12 +24,14 @@ if [[ ! -f "${STATE_FOLDER}/seed" ]]; then
   tr </dev/urandom -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1 >"${STATE_FOLDER}/seed"
   tr </dev/urandom -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1 >"${STATE_FOLDER}/seed"
 fi
 fi
 
 
-# Default variables
+### --------------------------------
+### General variables
+### --------------------------------
 NGINX_PORT=80
 NGINX_PORT=80
 NGINX_PORT_SSL=443
 NGINX_PORT_SSL=443
 DOMAIN=tipi.localhost
 DOMAIN=tipi.localhost
 SED_ROOT_FOLDER="$(echo "$ROOT_FOLDER" | sed 's/\//\\\//g')"
 SED_ROOT_FOLDER="$(echo "$ROOT_FOLDER" | sed 's/\//\\\//g')"
-DNS_IP=9.9.9.9 # Default to Quad9 DNS
+DNS_IP="9.9.9.9" # Default to Quad9 DNS
 ARCHITECTURE="$(uname -m)"
 ARCHITECTURE="$(uname -m)"
 TZ="$(timedatectl | grep "Time zone" | awk '{print $3}' | sed 's/\//\\\//g' || Europe\/Berlin)"
 TZ="$(timedatectl | grep "Time zone" | awk '{print $3}' | sed 's/\//\\\//g' || Europe\/Berlin)"
 apps_repository="https://github.com/meienberger/runtipi-appstore"
 apps_repository="https://github.com/meienberger/runtipi-appstore"
@@ -61,6 +65,16 @@ INTERNAL_IP="$(ip addr show "${NETWORK_INTERFACE}" | grep "inet " | awk '{print
 
 
 if [[ "$ARCHITECTURE" == "aarch64" ]]; then
 if [[ "$ARCHITECTURE" == "aarch64" ]]; then
   ARCHITECTURE="arm64"
   ARCHITECTURE="arm64"
+elif [[ "$ARCHITECTURE" == "armv7"* || "$ARCHITECTURE" == "armv8"* ]]; then
+  ARCHITECTURE="arm"
+elif [[ "$ARCHITECTURE" == "x86_64" ]]; then
+  ARCHITECTURE="amd64"
+fi
+
+# If none of the above conditions are met, the architecture is not supported
+if [[ "$ARCHITECTURE" != "arm64" ]] && [[ "$ARCHITECTURE" != "arm" ]] && [[ "$ARCHITECTURE" != "amd64" ]]; then
+  echo "Architecture ${ARCHITECTURE} not supported!"
+  exit 1
 fi
 fi
 
 
 ### --------------------------------
 ### --------------------------------
@@ -129,21 +143,18 @@ if [[ "${NGINX_PORT}" != "80" ]] && [[ "${DOMAIN}" != "tipi.localhost" ]]; then
   exit 1
   exit 1
 fi
 fi
 
 
-# Run system-info.sh
+### --------------------------------
+### Watcher and system-info
+### --------------------------------
 echo "Running system-info.sh..."
 echo "Running system-info.sh..."
 "${ROOT_FOLDER}/scripts/system-info.sh"
 "${ROOT_FOLDER}/scripts/system-info.sh"
 
 
 kill_watcher
 kill_watcher
 "${ROOT_FOLDER}/scripts/watcher.sh" &
 "${ROOT_FOLDER}/scripts/watcher.sh" &
 
 
-# Copy the config sample if it isn't here
-if [[ ! -f "${STATE_FOLDER}/apps.json" ]]; then
-  cp "${ROOT_FOLDER}/templates/config-sample.json" "${STATE_FOLDER}/config.json"
-fi
-
-export DOCKER_CLIENT_TIMEOUT=240
-export COMPOSE_HTTP_TIMEOUT=240
-
+### --------------------------------
+### settings.json overrides
+### --------------------------------
 echo "Generating config files..."
 echo "Generating config files..."
 # Override vars with values from settings.json
 # Override vars with values from settings.json
 if [[ -f "${STATE_FOLDER}/settings.json" ]]; then
 if [[ -f "${STATE_FOLDER}/settings.json" ]]; then
@@ -216,7 +227,9 @@ done
 
 
 mv -f "$ENV_FILE" "$ROOT_FOLDER/.env"
 mv -f "$ENV_FILE" "$ROOT_FOLDER/.env"
 
 
-## Don't run if config-only
+### --------------------------------
+### Start the project
+### --------------------------------
 if [[ ! $ci == "true" ]]; then
 if [[ ! $ci == "true" ]]; then
 
 
   if [[ $rc == "true" ]]; then
   if [[ $rc == "true" ]]; then

+ 1 - 1
scripts/system.sh

@@ -27,7 +27,7 @@ if [[ "$command" = "update" ]]; then
 
 
     scripts/stop.sh
     scripts/stop.sh
     git config --global --add safe.directory "${ROOT_FOLDER}"
     git config --global --add safe.directory "${ROOT_FOLDER}"
-    git pull origin master
+    git pull origin "$(git rev-parse --abbrev-ref HEAD)"
     scripts/start.sh
     scripts/start.sh
     exit
     exit
 fi
 fi