The basic idea for this project is a landing page that presents to you the latest info for a select set of gauges selected by the user to track the flows (or other measured parameters). Features the app will include: ### Features - [ ] Time series visualization of sensor data - [ ] User profiles allow users to subscribe to data they want on their dashboard - [ ] Per location metrics and thresholds to communicate critical levels for sensors - [ ] Notification system alerting user of extreme conditions ### Main TODO - [ ] Basic time series for flow data - [ ] tooltip - [ ] color changes - [x] Seed all source data to get this project started - [x] create the basic lambda function to populate database with all flows and river stage - [x] populate source table with all the queries to start out with - [x] locations - [x] sensors - [x] cdec_queries - its hard to tell what are the important ones for now. I will add - [x] handle the data overlaps, that is when this is run every hour/day handle overlap of data correctly - [x] Create users - [x] back-end support for creating users - [x] UI for creating and displaying users ## Sunday, September 29th 2024 Got the basic plot working. I want to try and make a Svelte component out of this and then work on the rest of the ui. ![[dashboard-plot-prototype.gif]] ## Monday, September 23rd 2024 Focus now switches over to the dashboard, the user profile is clearly not done but moving over to some different stuff to get that dopamine hit. Here is the basic implementation of what these plots can look like, literally just one of these for each of the stations+sensors they are subscribed to ![[Pasted image 20240923223706.png]] Here is another idea this one focuses more on a station, ![[Pasted image 20240923225547.png]] It could just be that when the user clicks on a plot we present this more detail view?? ## Monday, September 16th 2024 Today I want to add where I left off on Sunday, disable the buttons for "Add" when the user is already subbed to the station+sensor combo. Ok well I was able to do it with this ugly looking piece of code, but whatever. ``` {#if subscriptions.find((s) => s.sensor_id === sensor.sensor_id && s.station_name === selected_value)} <Table.Cell><Button disabled>Add</Button></Table.Cell> {:else} <Table.Cell><Button>Add</Button></Table.Cell> {/if} ``` And here is the result: ![[Pasted image 20240916233248.png]] ## Sunday, September 15th 2024 Working on this today, this entry is actually Saturday night at 1 am, but just wanted to share that I have the results showing after a user selects a station. Here is what this looks like: ![[Peek 2024-09-15 01-07.gif]] The only hard part for this was building the query for the list of sensors, I use knex, that query looks like this: ```ts const data = await db("cdec_query") .join("sensors", function () { this.on("sensor_number", "=", db.raw("sensors.parameter_code::int")); }) .join("stations", "cdec_query.location", "=", "stations.code") .select( "stations.id as station_id", "cdec_query.location as station_code", "sensor_number", "sensors.parameter_name as sensor_name", "duration_code" ) .where("stations.id", id); ``` Its actually pretty hacky as you can tell by the `::int` cast, I need to go back and make this better at some point. The next thing to tackle is populating the sensors subscriptions list, and also disabling the button when a sensor+station combo is already in the users subscription list. Lets start by working on the disabling of the button since that *seems* to be the easier one. ## Friday, September 6th 2024 It's been a while since I created an entry here, but I've only been able to work on this about an hour or two each night these past two weeks. My main focus these two weeks have been on the profile page for the logged in user. This means I implemented logging in and out, so that is working just fine.The profile page looks like this at the start today. ![[Pasted image 20240906225705.png]] So what I am going to work on today is show the list of current Station+Sensor subscriptions in the profile, once that is done I will start on adding more elements to this list. **Goal** Add a table to list out the current sensor subscriptions - [ ] Show list of current subscriptions - [ ] delete a subscription Ok I finished the list current subs, I of course hard-coded these subs in via insert statements since I don't have functionality worked out yet, but, yeah I can have a list of subbed sensors + station combos. Here is what it looks like now (super ugly, need to turn it into a table). ![[Pasted image 20240907001903.png]] I am using sveltekit and honestly it was really straightforward, here is the route I created for getting a users subs (chatgpt built the knex query for me): ```ts import knex from "knex"; import config from "$lib/server/db/knexfile"; import jwt from "jsonwebtoken"; import { json } from "@sveltejs/kit"; import { check_auth } from "$lib/server/auth/auth"; const db = knex(config); export async function GET({ cookies }) { try { const user = check_auth(cookies); const data = await db("user_sensor_subscriptions") .where({ "user_sensor_subscriptions.user_id": user.id }) .leftJoin("stations", "user_sensor_subscriptions.station_id", "stations.id") .leftJoin("sensors", "user_sensor_subscriptions.sensor_id", "sensors.id") .select( "user_sensor_subscriptions.*", "stations.code as station_code", "sensors.parameter_name as sensor_name" ); return json(data, { status: 200 }); } catch (error) { console.error("ERROR:", error); return json({ error: "internal server error" }, { status: 500 }); } } ``` I created a `check_auth` function since I am going to be doing the check for token in the cookies. ```ts import jwt from "jsonwebtoken"; const JWT_SECRET = "this-is-top-secret"; export function check_auth(cookies) { const token = cookies.get("session"); if (!token) { throw new Error("Unauthorized"); } try { const user = jwt.verify(token, JWT_SECRET); return user; } catch (error) { throw new Error("Unauthorized"); } } ``` Here is quick update of what the subs data looks like in a table, I will work using the `DataTable` shadcn component since it will give me buttons for handling delete and other possible options. ![[Pasted image 20240907004140.png]] Ok probably the last update of the night added some icons to the table that will then allow me to add the delete feature. ![[Pasted image 20240907005102.png]] ## Creating users So I am just learning Sveltekit, but just learned that you can create api routes within the routes folder. So I just created a route `src/rutes/api/users/+server.ts` and in this function I can define GET, POST and other method functions to create these endpoints. Two days and a few hours of being able to work on this and the user creation, log in, log out have all been implemented, lets discuss a bit here. Lets start with the definition of the api for creating a user. Here is the table ```sql create table users ( id serial primary key, username varchar(50) not null unique constraint unique_username unique, email varchar(255) not null unique, password_hash varchar(255) not null, created_at timestamp with time zone default CURRENT_TIMESTAMP, updated_at timestamp with time zone default CURRENT_TIMESTAMP ); ``` so a super basic table, users will need an email and password to log in. The username is just for displaying around the site. To create a route that will create a user I first create a file in `src/routes/api/users/+server.ts`. In this file I create a `POST` function to post data from a form. Here is what that looks like. ```ts import { json } from '@sveltejs/kit'; import bcrypt from 'bcrypt'; import knex from 'knex'; import config from '$lib/server/db/knexfile'; const db = knex(config); export async function POST({ request }) { const { username, email, password } = await request.json(); if (!username || !email || !password) { return json({ error: 'Missing required fields' }, { status: 400 }); } try { const existing_user = await db('users').where({ email }).first(); if (existing_user) { return json({ error: 'User already exists' }, { status: 400 }); } const salt_arounds = 10; const hashed_password = await bcrypt.hash(password, salt_arounds); const [user_id] = await db('users') .insert({ username, email, password_hash: hashed_password }) .returning('id'); return json({ message: 'user created succesfully' }, { status: 201 }); } catch (error) { console.error('Error creating uesr', error); return json({ error: 'internal server error' }, { status: 500 }); } } ``` a couple important things to point out. - i am using knex as a query builder, so I import and create the db object at the top of the file -