Initial commit

This commit is contained in:
Dsupanta
2026-09-06 18:39:10 -03:00
commit e5caf1c462
10 changed files with 425 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
# Example environment variables - do NOT commit secrets
VITE_COGNITO_USER_POOL_ID=
VITE_COGNITO_CLIENT_ID=
VITE_API_URL=https://your-api-id.execute-api.<region>.amazonaws.com
# Optional: if you have a Cognito Hosted UI domain you can add it to enable redirects
# VITE_COGNITO_HOSTED_UI_DOMAIN=your-domain.auth.<region>.amazoncognito.com
+47
View File
@@ -0,0 +1,47 @@
# Node
node_modules/
package-lock.json
yarn.lock
pnpm-lock.yaml
# Environment
.env
.env.*
!.env.example
# Build outputs
dist/
build/
out/
.cache/
# Vite/Next/CRA
.next/
.cache/
# Static analysis
coverage/
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# AWS / Amplify
aws-exports.js
aws-exports.ts
amplify/
# Credentials and keys
*.pem
*.key
credentials
aws-credentials
# Editor / OS
.vscode/
.idea/
.DS_Store
# Misc
*.local
+108
View File
@@ -0,0 +1,108 @@
iot-front-dashboard
====================
Resumen
------
Aplicación web sencilla (Vue 3 + Vite + TypeScript) para visualizar las mediciones IoT. Está diseñada como una primera versión didáctica, deliberadamente simple, de modo que los alumnos puedan luego refactorizarla.
Tecnologías
----------
- Vue 3
- Vite
- TypeScript
- HTML / CSS
- Amazon Cognito (autenticación)
Estructura
---------
```
iot-front-dashboard/
├── package.json
├── vite.config.ts
├── tsconfig.json
├── .env.example
├── index.html
└── src/
├── main.ts
├── App.vue
└── style.css
```
Variables de entorno
-------------------
Copiar `.env.example` a `.env` y ajustar valores:
- VITE_COGNITO_USER_POOL_ID: (no obligatorio para esta primera versión)
- VITE_COGNITO_CLIENT_ID: (no obligatorio para esta primera version)
- VITE_API_URL: URL base de la API (por ejemplo https://<api-id>.execute-api.<region>.amazonaws.com)
No pongas credenciales reales en el repositorio.
Login y obtención del JWT
-------------------------
Esta primera versión implementa un login simple: el usuario debe obtener un JWT (id o access token) desde Cognito y pegarlo en la aplicación. El token se almacena en localStorage como `iot_token`.
Cómo obtener un token desde Cognito (sugerencias):
- Usar el Hosted UI de Cognito (si el App Client está configurado con callback y dominio) y copiar el token desde la URL o desde las herramientas de desarrollador.
- Obtener un token con herramientas administrativas o scripts (ej. usar AWS CLI/AdminInitiateAuth si se dispone de permisos).
Nota: automatizar el flujo de login (Hosted UI redirect / Authorization Code) requiere configurar callback URLs y dominio en el App Client y puede ser añadido en versiones posteriores.
Ejecución local
---------------
1. Instalar dependencias:
```bash
npm install
```
2. Crear `.env` a partir de `.env.example` y ajustar VITE_API_URL.
3. Ejecutar la app en modo desarrollo:
```bash
npm run dev
```
Uso
---
1. Obtener un JWT de Cognito (ver sección anterior).
2. Pegar el token en el textarea y hacer click en "Usar token (Login)".
3. La app llamará a `GET ${VITE_API_URL}/readings` enviando el header `Authorization: Bearer <TOKEN>`.
4. Verás indicadores sencillos (temperatura/humedad actuales) y una tabla con las lecturas.
API esperada
-----------
El endpoint `/readings` debe devolver JSON con la forma:
```json
{
"data": [
{
"deviceId": "ARDUINO-01",
"timestamp": "2026-09-02T18:00:00Z",
"temperature": 24.7,
"humidity": 58.2
}
]
}
```
Estados iniciales y manejo de errores
-----------------------------------
- Cargando: "Cargando datos..."
- Error simple: "No fue posible obtener los datos"
- 401: mostrado como "No autorizado (401)" (API Gateway/Cognito normalmente maneja la validación)
Refactorización posterior
-------------------------
Esta versión concentra gran parte de la lógica en `App.vue` intencionalmente. En tareas posteriores se pedirá a los alumnos que separen responsabilidades (componentes, servicios HTTP, manejo de estado) y mejoren la experiencia y seguridad.
Relación con Cognito y la API
----------------------------
- Cognito emite el JWT que la app usará como bearer token.
- La API (API Gateway + Cognito Authorizer + Lambda) protege `/readings` y delega la autenticación a Cognito.
Contacto
--------
Proyecto educativo — implementación simple para fines didácticos.
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IoT Front Dashboard</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+19
View File
@@ -0,0 +1,19 @@
{
"name": "iot-front-dashboard",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.0.0",
"vite": "^5.0.0",
"typescript": "^5.0.0",
"vue-tsc": "^1.0.0"
}
}
+201
View File
@@ -0,0 +1,201 @@
<template>
<div class="container">
<h1>IoT Dashboard</h1>
<div v-if="!token" class="card">
<h2>Iniciar sesión</h2>
<p>La aplicación usa Amazon Cognito para autenticación. Obtén un JWT (id o access token) desde Cognito y pégalo aquí.</p>
<textarea v-model="tokenInput" placeholder="Pega tu JWT aquí" rows="6"></textarea>
<div class="row">
<button @click="doLogin">Usar token (Login)</button>
<button @click="clearToken" class="secondary">Limpiar</button>
</div>
<p class="hint">Opcional: si tienes configurado Hosted UI, genera el JWT mediante Cognito (ver README).</p>
</div>
<div v-else>
<div class="row space-between">
<div>
<strong>Autenticado</strong>
</div>
<div>
<button @click="logout" class="secondary">Cerrar sesión</button>
</div>
</div>
<div class="card">
<h2>Indicadores</h2>
<div v-if="loading">Cargando datos...</div>
<div v-else-if="error">No fue posible obtener los datos: {{ error }}</div>
<div v-else>
<div class="indicators">
<div class="indicator">
<div class="label">Temperatura actual</div>
<div class="value">{{ latest?.temperature ?? '—' }} °C</div>
</div>
<div class="indicator">
<div class="label">Humedad actual</div>
<div class="value">{{ latest?.humidity ?? '—' }} %</div>
</div>
</div>
<h3>Lecturas</h3>
<table>
<thead>
<tr>
<th>Dispositivo</th>
<th>Fecha</th>
<th>Temperatura (°C)</th>
<th>Humedad (%)</th>
</tr>
</thead>
<tbody>
<tr v-for="r in readings" :key="r.deviceId + r.timestamp">
<td>{{ r.deviceId }}</td>
<td>{{ r.timestamp }}</td>
<td>{{ r.temperature }}</td>
<td>{{ r.humidity }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { ref, computed, onMounted } from 'vue'
type Reading = {
deviceId: string
timestamp: string
temperature: number
humidity: number
}
export default {
setup() {
const API_URL = (import.meta.env.VITE_API_URL as string) || ''
const token = ref<string | null>(localStorage.getItem('iot_token'))
const tokenInput = ref('')
const readings = ref<Reading[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const latest = computed(() => {
if (!readings.value.length) return null
// try to find the latest by timestamp (ISO strings assumed)
return readings.value.slice().sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1))[0]
})
async function fetchReadings() {
if (!API_URL) {
error.value = 'VITE_API_URL no está configurada. Revisa .env'
return
}
if (!token.value) {
error.value = 'No autenticado'
return
}
loading.value = true
error.value = null
try {
const res = await fetch(`${API_URL.replace(/\/+$/, '')}/readings`, {
headers: {
Authorization: `Bearer ${token.value}`
}
})
if (res.status === 401) {
error.value = 'No autorizado (401)'
readings.value = []
loading.value = false
return
}
if (!res.ok) {
error.value = `HTTP ${res.status}`
readings.value = []
loading.value = false
return
}
const data = await res.json()
readings.value = Array.isArray(data?.data) ? data.data : []
} catch (e: any) {
error.value = e?.message || String(e)
readings.value = []
} finally {
loading.value = false
}
}
function doLogin() {
if (!tokenInput.value) return
token.value = tokenInput.value.trim()
localStorage.setItem('iot_token', token.value)
// immediately fetch
fetchReadings()
}
function logout() {
token.value = null
localStorage.removeItem('iot_token')
readings.value = []
}
function clearToken() {
tokenInput.value = ''
}
// If token already present, fetch on mount
onMounted(() => {
if (token.value) {
fetchReadings()
}
})
return {
token,
tokenInput,
doLogin,
clearToken,
logout,
readings,
latest,
loading,
error
}
}
}
</script>
<style scoped>
.container {
max-width: 900px;
margin: 32px auto;
padding: 0 16px;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
}
.card {
background: #fff;
border: 1px solid #e6e6e6;
padding: 16px;
margin-top: 16px;
border-radius: 6px;
}
.row { display:flex; gap:8px; align-items:center; }
.space-between { justify-content:space-between; }
textarea { width:100%; font-family: monospace; }
button { padding:8px 12px; cursor:pointer; }
button.secondary { background:#eee; }
.indicators { display:flex; gap:16px; margin-bottom:12px; }
.indicator { padding:8px; background:#f7f7f7; border-radius:4px; }
.label { font-size:0.9rem; color:#666; }
.value { font-size:1.5rem; font-weight:600; }
table { width:100%; border-collapse:collapse; margin-top:8px; }
th, td { text-align:left; padding:8px; border-bottom:1px solid #eee; }
.hint { color:#666; font-size:0.9rem; }
</style>
+5
View File
@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'
createApp(App).mount('#app')
+2
View File
@@ -0,0 +1,2 @@
html, body, #app { height: 100%; margin: 0; }
body { background: #f0f2f5; }
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 5173
}
})