From e84e9e659455ee708aad7f5934d57d31d6e98783 Mon Sep 17 00:00:00 2001 From: "D. Scott Boggs" Date: Sun, 25 Jun 2023 11:30:45 -0400 Subject: [PATCH 1/6] Add README --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..1282a0e --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Kalkutago + +A multi-user Tickmate clone for the web + +## Development +### Before you start +The backend uses a postgres server which is not tracked as a part of the +repository, and neither is the password for it. You'll need to generate a +password by doing something like + +```console +openssl rand -base64 36 > server/postgres.pw +``` + +The development environment uses Traefik to reverse-proxy the API and dev +server. In production, the API will serve the client directly, but this system +allows you to run the dev server behind a reverse proxy so that changes are +rendered quickly without having to rebuild the container. Using the vite dev +server also gets us better debugging in the browser. + +However, that means that Traefik needs a hostname to listen for. The way I've +done this is by adding the following entry to my `/etc/hosts` file: + +``` +127.0.0.1 kalkutago +``` + +### Server +The server is written in Rust, using Rocket and SeaOrm. It uses Rust nightly, so +you'll need to `rustup override set nightly`, though it gets built in docker so +you really only need this for IDE support. + +### Client + +The client is a Vue application. It has a central state in +[`state.ts`](client/src/state.ts). On application load, the state is initialized +by fetching the current tracks and ticks via the API, and then subscribes to +updates from the server by opening an EventSource at `/api/v1/updates`. + +The basic flow is: + - a user interaction happens which should trigger a state change + - the action is dispatched to the server (e.g. by making a PATCH request to + `/api/v1/tracks//ticked`) + - the server makes the appropriate change to the database + - an event is dispatched to the event sources subscribed via `/api/v1/updates` + - The client receives the event, the reactive state is updated, and the UI + changes to match the expected state. + +### Running for development + +`docker-compose_dev.yml` is symlinked to `docker-compose.yml` for now, so once +the hostname is set up Just run `docker-compose up --build` and visit +http://kalkutago/ to open the app + +### Populating the database +In order to see the UI components I add a track to the database like + +```console +curl --json '{"name": "test", "description": "test track", "icon": "❓", "enabled": 1}' kalkutago/api/v1/tracks +``` \ No newline at end of file From 741429630bfcb3ad2867b7e7c4953b62ad19a702 Mon Sep 17 00:00:00 2001 From: "D. Scott Boggs" Date: Tue, 20 Jun 2023 11:47:20 -0400 Subject: [PATCH 2/6] Build custom image for client dev server --- client/.dockerignore | 2 ++ client/Dockerfile | 10 ++++++++++ docker-compose_dev.yml | 6 ++---- 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 client/.dockerignore create mode 100644 client/Dockerfile diff --git a/client/.dockerignore b/client/.dockerignore new file mode 100644 index 0000000..04c01ba --- /dev/null +++ b/client/.dockerignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ \ No newline at end of file diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000..1a44d67 --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,10 @@ +FROM node +# user node has UID 1000 in the container +USER node +EXPOSE 5173 +VOLUME /client +WORKDIR /client +ADD package.json yarn.lock tsconfig.json tsconfig.node.json vite.config.ts /client/ +RUN yarn +ADD public/ src/ index.html /client/ + diff --git a/docker-compose_dev.yml b/docker-compose_dev.yml index 780b9be..93d4bf1 100644 --- a/docker-compose_dev.yml +++ b/docker-compose_dev.yml @@ -37,11 +37,9 @@ services: - ./db.mount:/var/lib/postgresql/data client_devserver: - image: node + build: ./client volumes: [ ./client:/client/ ] - working_dir: /client - command: [ "sh", "-c", "yarn && yarn dev --host 0.0.0.0" ] - expose: [ 5173 ] + command: "yarn dev --host 0.0.0.0" networks: [ web ] labels: traefik.enable: true From 9e16306c615dc8e96aed5258badf1b9ab68a6379 Mon Sep 17 00:00:00 2001 From: "D. Scott Boggs" Date: Tue, 20 Jun 2023 11:48:02 -0400 Subject: [PATCH 3/6] Remove a bit of cruft --- client/src/state.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/client/src/state.ts b/client/src/state.ts index e0ad4ad..d9c00e7 100644 --- a/client/src/state.ts +++ b/client/src/state.ts @@ -18,12 +18,6 @@ export const state = reactive({ source.addEventListener('TickAdded', event => { console.log(event) const tick: Tick = JSON.parse(event.data) - // for (const track of this.tracks) { - // if (track.id === tick.track_id) { - // console.debug('pushing tick') - // track.ticks?.push(tick) - // } - // } const tracks = this.tracks.map(track => { if (track.id === tick.track_id) { const ticks = track.ticks ?? [] @@ -33,22 +27,17 @@ export const state = reactive({ } return track }) - console.debug(tracks) this.tracks = tracks }) source.addEventListener('TickDropped', event => { console.log(event) const tick: Tick = JSON.parse(event.data) - // for (const track of this.tracks) - // if (track.id === tick.track_id) - // track.ticks = track.ticks?.filter($tick => $tick.id === tick.id) const tracks = this.tracks.map(track => { if (track.id === tick.track_id) { track.ticks = track.ticks?.filter($tick => $tick.id !== tick.id) } return track }) - console.debug(tracks) this.tracks = tracks }) source.addEventListener('Lagged', event => { From b81485a3d5690a11bca49685af68cb5daf592ec2 Mon Sep 17 00:00:00 2001 From: "D. Scott Boggs" Date: Wed, 21 Jun 2023 06:23:36 -0400 Subject: [PATCH 4/6] Cleanly disconnect from the update stream if possible --- client/src/state.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/state.ts b/client/src/state.ts index d9c00e7..cc140cd 100644 --- a/client/src/state.ts +++ b/client/src/state.ts @@ -49,6 +49,7 @@ export const state = reactive({ error(event) window.location = window.location }) + window.addEventListener('beforeunload', () => source.close()) }, async repopulate() { this.state = State.Fetching From ac6a5e786c7e562bd9cea63039e6cc0ce4567914 Mon Sep 17 00:00:00 2001 From: "D. Scott Boggs" Date: Wed, 21 Jun 2023 06:26:52 -0400 Subject: [PATCH 5/6] Move state initialization from App.vue to main.ts --- client/src/App.vue | 3 --- client/src/main.ts | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/App.vue b/client/src/App.vue index b7adb44..a7b1c04 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,8 +1,5 @@