feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,4 @@
export * from './serializer';
export * from './parser';
export * from './types';
export { Token } from './token';

View File

@@ -0,0 +1,443 @@
import {
Dictionary,
List,
Item,
BareItem,
Parameters,
InnerList,
ByteSequence
} from './types';
import { Token } from './token';
import { isAscii } from './util';
export function parseDictionary(input: string): Dictionary {
const parser = new Parser(input);
return parser.parseDictionary();
}
export function parseList(input: string): List {
const parser = new Parser(input);
return parser.parseList();
}
export function parseItem(input: string): Item {
const parser = new Parser(input);
return parser.parseItem();
}
export class ParseError extends Error {
constructor(position: number, message:string) {
super(`Parse error: ${message} at offset ${position}`);
}
}
export default class Parser {
input: string;
pos: number;
constructor(input: string) {
this.input = input;
this.pos = 0;
}
parseDictionary(): Dictionary {
this.skipWS();
const dictionary = new Map();
while(!this.eof()) {
const thisKey = this.parseKey();
let member;
if (this.lookChar()==='=') {
this.pos++;
member = this.parseItemOrInnerList();
} else {
member = [true, this.parseParameters()];
}
dictionary.set(thisKey, member);
this.skipOWS();
if (this.eof()) {
return dictionary;
}
this.expectChar(',');
this.pos++;
this.skipOWS();
if (this.eof()) {
throw new ParseError(this.pos, 'Dictionary contained a trailing comma');
}
}
return dictionary;
}
parseList(): List {
this.skipWS();
const members: List = [];
while(!this.eof()) {
members.push(
this.parseItemOrInnerList()
);
this.skipOWS();
if (this.eof()) {
return members;
}
this.expectChar(',');
this.pos++;
this.skipOWS();
if (this.eof()) {
throw new ParseError(this.pos, 'A list may not end with a trailing comma');
}
}
return members;
}
parseItem(standaloneItem: boolean = true): Item {
if (standaloneItem) this.skipWS();
const result: Item = [
this.parseBareItem(),
this.parseParameters()
];
if (standaloneItem) this.checkTrail();
return result;
}
private parseItemOrInnerList(): Item|InnerList {
if (this.lookChar()==='(') {
return this.parseInnerList();
} else {
return this.parseItem(false);
}
}
private parseInnerList(): InnerList {
this.expectChar('(');
this.pos++;
const innerList: Item[] = [];
while(!this.eof()) {
this.skipWS();
if (this.lookChar() === ')') {
this.pos++;
return [
innerList,
this.parseParameters()
];
}
innerList.push(this.parseItem(false));
const nextChar = this.lookChar();
if (nextChar!==' ' && nextChar !== ')') {
throw new ParseError(this.pos, 'Expected a whitespace or ) after every item in an inner list');
}
}
throw new ParseError(this.pos, 'Could not find end of inner list');
}
private parseBareItem(): BareItem {
const char = this.lookChar();
if (char.match(/^[-0-9]/)) {
return this.parseIntegerOrDecimal();
}
if (char === '"') {
return this.parseString();
}
if (char.match(/^[A-Za-z*]/)) {
return this.parseToken();
}
if (char === ':' ) {
return this.parseByteSequence();
}
if (char === '?') {
return this.parseBoolean();
}
throw new ParseError(this.pos, 'Unexpected input');
}
private parseParameters(): Parameters {
const parameters = new Map();
while(!this.eof()) {
const char = this.lookChar();
if (char!==';') {
break;
}
this.pos++;
this.skipWS();
const key = this.parseKey();
let value: BareItem = true;
if (this.lookChar() === '=') {
this.pos++;
value = this.parseBareItem();
}
parameters.set(key, value);
}
return parameters;
}
private parseIntegerOrDecimal(): number {
let type: 'integer' | 'decimal' = 'integer';
let sign = 1;
let inputNumber = '';
if (this.lookChar()==='-') {
sign = -1;
this.pos++;
}
// The spec wants this check but it's unreachable code.
//if (this.eof()) {
// throw new ParseError(this.pos, 'Empty integer');
//}
if (!isDigit(this.lookChar())) {
throw new ParseError(this.pos, 'Expected a digit (0-9)');
}
while(!this.eof()) {
const char = this.getChar();
if (isDigit(char)) {
inputNumber+=char;
} else if (type === 'integer' && char === '.') {
if (inputNumber.length>12) {
throw new ParseError(this.pos, 'Exceeded maximum decimal length');
}
inputNumber+='.';
type = 'decimal';
} else {
// We need to 'prepend' the character, so it's just a rewind
this.pos--;
break;
}
if (type === 'integer' && inputNumber.length>15) {
throw new ParseError(this.pos, 'Exceeded maximum integer length');
}
if (type === 'decimal' && inputNumber.length>16) {
throw new ParseError(this.pos, 'Exceeded maximum decimal length');
}
}
if (type === 'integer') {
return parseInt(inputNumber, 10) * sign;
} else {
if (inputNumber.endsWith('.')) {
throw new ParseError(this.pos, 'Decimal cannot end on a period');
}
if (inputNumber.split('.')[1].length>3) {
throw new ParseError(this.pos, 'Number of digits after the decimal point cannot exceed 3');
}
return parseFloat(inputNumber) * sign;
}
}
private parseString(): string {
let outputString = '';
this.expectChar('"');
this.pos++;
while(!this.eof()) {
const char = this.getChar();
if (char==='\\') {
if (this.eof()) {
throw new ParseError(this.pos, 'Unexpected end of input');
}
const nextChar = this.getChar();
if (nextChar!=='\\' && nextChar !== '"') {
throw new ParseError(this.pos, 'A backslash must be followed by another backslash or double quote');
}
outputString+=nextChar;
} else if (char === '"') {
return outputString;
} else if (!isAscii(char)) {
throw new Error('Strings must be in the ASCII range');
} else {
outputString += char;
}
}
throw new ParseError(this.pos, 'Unexpected end of input');
}
private parseToken(): Token {
// The specification wants this check, but it's an unreachable code block.
// if (!/^[A-Za-z*]/.test(this.lookChar())) {
// throw new ParseError(this.pos, 'A token must begin with an asterisk or letter (A-Z, a-z)');
//}
let outputString = '';
while(!this.eof()) {
const char = this.lookChar();
if (!/^[:/!#$%&'*+\-.^_`|~A-Za-z0-9]$/.test(char)) {
return new Token(outputString);
}
outputString += this.getChar();
}
return new Token(outputString);
}
private parseByteSequence(): ByteSequence {
this.expectChar(':');
this.pos++;
const endPos = this.input.indexOf(':', this.pos);
if (endPos === -1) {
throw new ParseError(this.pos, 'Could not find a closing ":" character to mark end of Byte Sequence');
}
const b64Content = this.input.substring(this.pos, endPos);
this.pos += b64Content.length+1;
if (!/^[A-Za-z0-9+/=]*$/.test(b64Content)) {
throw new ParseError(this.pos, 'ByteSequence does not contain a valid base64 string');
}
return new ByteSequence(b64Content);
}
private parseBoolean(): boolean {
this.expectChar('?');
this.pos++;
const char = this.getChar();
if (char === '1') {
return true;
}
if (char === '0') {
return false;
}
throw new ParseError(this.pos, 'Unexpected character. Expected a "1" or a "0"');
}
private parseKey(): string {
if (!this.lookChar().match(/^[a-z*]/)) {
throw new ParseError(this.pos, 'A key must begin with an asterisk or letter (a-z)');
}
let outputString = '';
while(!this.eof()) {
const char = this.lookChar();
if (!/^[a-z0-9_\-.*]$/.test(char)) {
return outputString;
}
outputString += this.getChar();
}
return outputString;
}
/**
* Looks at the next character without advancing the cursor.
*/
private lookChar():string {
return this.input[this.pos];
}
/**
* Checks if the next character is 'char', and fail otherwise.
*/
private expectChar(char: string): void {
if (this.lookChar()!==char) {
throw new ParseError(this.pos, `Expected ${char}`);
}
}
private getChar(): string {
return this.input[this.pos++];
}
private eof():boolean {
return this.pos>=this.input.length;
}
// Advances the pointer to skip all whitespace.
private skipOWS(): void {
while (true) {
const c = this.input.substr(this.pos, 1);
if (c === ' ' || c === '\t') {
this.pos++;
} else {
break;
}
}
}
// Advances the pointer to skip all spaces
private skipWS(): void {
while(this.lookChar()===' ') {
this.pos++;
}
}
// At the end of parsing, we need to make sure there are no bytes after the
// header except whitespace.
private checkTrail(): void {
this.skipWS();
if (!this.eof()) {
throw new ParseError(this.pos, 'Unexpected characters at end of input');
}
}
}
const isDigitRegex = /^[0-9]$/;
function isDigit(char: string): boolean {
return isDigitRegex.test(char);
}

View File

@@ -0,0 +1,147 @@
import {
BareItem,
ByteSequence,
Dictionary,
InnerList,
Item,
List,
Parameters,
} from './types';
import { Token } from './token';
import { isAscii, isInnerList, isValidKeyStr } from './util';
export class SerializeError extends Error {}
export function serializeList(input: List): string {
return input.map(value => {
if (isInnerList(value)) {
return serializeInnerList(value);
} else {
return serializeItem(value);
}
}).join(', ');
}
export function serializeDictionary(input: Dictionary): string {
return Array.from(
input.entries()
).map(([key, value]) => {
let out = serializeKey(key);
if (value[0]===true) {
out += serializeParameters(value[1]);
} else {
out += '=';
if (isInnerList(value)) {
out += serializeInnerList(value);
} else {
out += serializeItem(value);
}
}
return out;
}).join(', ');
}
export function serializeItem(input: Item): string {
return serializeBareItem(input[0]) + serializeParameters(input[1]);
}
function serializeInnerList(input: InnerList): string {
return `(${input[0].map(value => serializeItem(value)).join(' ')})${serializeParameters(input[1])}`;
}
function serializeBareItem(input: BareItem): string {
if (typeof input === 'number') {
if (Number.isInteger(input)) {
return serializeInteger(input);
}
return serializeDecimal(input);
}
if (typeof input === 'string') {
return serializeString(input);
}
if (input instanceof Token) {
return serializeToken(input);
}
if (input instanceof ByteSequence) {
return serializeByteSequence(input);
}
if (typeof input === 'boolean') {
return serializeBoolean(input);
}
throw new SerializeError(`Cannot serialize values of type ${typeof input}`);
}
function serializeInteger(input: number): string {
if (input < -999_999_999_999_999 || input > 999_999_999_999_999) {
throw new SerializeError('Structured headers can only encode integers in the range range of -999,999,999,999,999 to 999,999,999,999,999 inclusive');
}
return input.toString();
}
function serializeDecimal(input: number): string {
const out = input.toFixed(3).replace(/0+$/,'');
const signifantDigits = out.split('.')[0].replace('-','').length;
if (signifantDigits > 12) {
throw new SerializeError('Fractional numbers are not allowed to have more than 12 significant digits before the decimal point');
}
return out;
}
function serializeString(input: string): string {
if (!isAscii(input)) {
throw new SerializeError('Only ASCII strings may be serialized');
}
return `"${input.replace(/("|\\)/g, (v) => '\\' + v)}"`;
}
function serializeBoolean(input: boolean): string {
return input ? '?1' : '?0';
}
function serializeByteSequence(input: ByteSequence): string {
return `:${input.toBase64()}:`;
}
function serializeToken(input: Token): string {
return input.toString();
}
function serializeParameters(input: Parameters): string {
return Array.from(input).map(([key, value]) => {
let out = ';' + serializeKey(key);
if (value!==true) {
out+='=' + serializeBareItem(value);
}
return out;
}).join('');
}
function serializeKey(input: string): string {
if (!isValidKeyStr(input)) {
throw new SerializeError('Keys in dictionaries must only contain lowercase letter, numbers, _-*. and must start with a letter or *');
}
return input;
}

View File

@@ -0,0 +1,21 @@
import { isValidTokenStr } from './util';
export class Token {
private value: string;
constructor(value: string) {
if (!isValidTokenStr(value)) {
throw new TypeError('Invalid character in Token string. Tokens must start with *, A-Z and the rest of the string may only contain a-z, A-Z, 0-9, :/!#$%&\'*+-.^_`|~');
}
this.value = value;
}
toString(): string {
return this.value;
}
}

View File

@@ -0,0 +1,52 @@
import { Token } from './token';
/**
* Lists are arrays of zero or more members, each of which can be an Item
* or an Inner List, both of which can be Parameterized
*/
export type List = (InnerList|Item)[];
/**
* An Inner List is an array of zero or more Items. Both the individual Items
* and the Inner List itself can be Parameterized.
*/
export type InnerList = [Item[], Parameters];
/**
* Parameters are an ordered map of key-value pairs that are associated with
* an Item or Inner List. The keys are unique within the scope of the
* Parameters they occur within, and the values are bare items (i.e., they
* themselves cannot be parameterized
*/
export type Parameters = Map<string, BareItem>;
/**
* Dictionaries are ordered maps of key-value pairs, where the keys are short
* textual strings and the values are Items or arrays of Items, both of which
* can be Parameterized.
*
* There can be zero or more members, and their keys are unique in the scope
* of the Dictionary they occur within.
*/
export type Dictionary = Map<string, Item|InnerList>;
export class ByteSequence {
base64Value: string;
constructor(base64Value: string) {
this.base64Value = base64Value;
}
toBase64(): string {
return this.base64Value;
}
}
export type BareItem = number | string | Token | ByteSequence | boolean;
export type Item = [BareItem, Parameters];

View File

@@ -0,0 +1,30 @@
import { Item, InnerList } from './types';
const asciiRe = /^[\x20-\x7E]*$/;
const tokenRe = /^[a-zA-Z*][:/!#$%&'*+\-.^_`|~A-Za-z0-9]*$/;
const keyRe = /^[a-z*][*\-_.a-z0-9]*$/;
export function isAscii(str: string): boolean {
return asciiRe.test(str);
}
export function isValidTokenStr(str: string): boolean {
return tokenRe.test(str);
}
export function isValidKeyStr(str: string): boolean {
return keyRe.test(str);
}
export function isInnerList(input: Item | InnerList): input is InnerList {
return Array.isArray(input[0]);
}