Working Prototype

This commit is contained in:
blaisadmin
2026-03-26 00:24:33 -04:00
parent 04c74de25a
commit 4cdfaa822e
9 changed files with 2760 additions and 1051 deletions
+1
View File
@@ -4,6 +4,7 @@ POSTGRES_PASSWORD=change_me
NODE_ENV=development NODE_ENV=development
FRONTEND_URL=http://localhost:3000 FRONTEND_URL=http://localhost:3000
VITE_API_BASE_URL=http://localhost:5000/api VITE_API_BASE_URL=http://localhost:5000/api
ALLOW_REGISTRATION=true
# Production-only Traefik settings # Production-only Traefik settings
TRAEFIK_NETWORK=traefik_proxy TRAEFIK_NETWORK=traefik_proxy
TRAEFIK_ENTRYPOINT=websecure TRAEFIK_ENTRYPOINT=websecure
+6
View File
@@ -78,6 +78,12 @@ VITE_API_BASE_URL=https://api.arsenal.example.com/api
FRONTEND_URL=https://arsenal.example.com FRONTEND_URL=https://arsenal.example.com
``` ```
To disable self-service account creation and allow only existing users or SSO sign-in, set:
```env
ALLOW_REGISTRATION=false
```
## API routes ## API routes
- `GET /health` - `GET /health`
+129 -20
View File
@@ -1,46 +1,155 @@
CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255),
name VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE TABLE IF NOT EXISTS profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE (user_id, name)
);
CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON profiles(user_id);
CREATE TABLE IF NOT EXISTS auth_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
active_profile_id UUID REFERENCES profiles(id) ON DELETE SET NULL,
token_hash VARCHAR(255) NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_auth_sessions_user_id ON auth_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_auth_sessions_expires_at ON auth_sessions(expires_at);
CREATE TABLE IF NOT EXISTS auth_provider_configs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
provider_key VARCHAR(100) NOT NULL UNIQUE,
display_name VARCHAR(255) NOT NULL,
protocol VARCHAR(50) NOT NULL DEFAULT 'oidc',
client_id TEXT,
client_secret TEXT,
authorization_endpoint TEXT,
token_endpoint TEXT,
userinfo_endpoint TEXT,
issuer TEXT,
scopes TEXT NOT NULL DEFAULT 'openid profile email',
enabled BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS auth_identities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider_key VARCHAR(100) NOT NULL REFERENCES auth_provider_configs(provider_key) ON DELETE CASCADE,
provider_subject TEXT NOT NULL,
email VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE (provider_key, provider_subject)
);
CREATE TABLE IF NOT EXISTS oauth_states (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
provider_key VARCHAR(100) NOT NULL REFERENCES auth_provider_configs(provider_key) ON DELETE CASCADE,
state_code VARCHAR(255) NOT NULL UNIQUE,
redirect_uri TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS calibers ( CREATE TABLE IF NOT EXISTS calibers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(40) NOT NULL UNIQUE, profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
name VARCHAR(40) NOT NULL,
is_default BOOLEAN NOT NULL DEFAULT FALSE, is_default BOOLEAN NOT NULL DEFAULT FALSE,
is_active BOOLEAN NOT NULL DEFAULT TRUE, is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE (profile_id, name)
); );
CREATE INDEX IF NOT EXISTS idx_calibers_profile_id ON calibers(profile_id);
CREATE TABLE IF NOT EXISTS firearms ( CREATE TABLE IF NOT EXISTS firearms (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
manufacturer VARCHAR(120) NOT NULL, manufacturer VARCHAR(120) NOT NULL,
model VARCHAR(120) NOT NULL, model VARCHAR(120) NOT NULL,
category VARCHAR(80) NOT NULL, category VARCHAR(80) NOT NULL,
caliber VARCHAR(40) NOT NULL, caliber VARCHAR(40) NOT NULL,
serial_number VARCHAR(120) NOT NULL UNIQUE, serial_number VARCHAR(120) NOT NULL,
purchase_price NUMERIC(10, 2) NOT NULL DEFAULT 0, purchase_price NUMERIC(10, 2) NOT NULL DEFAULT 0,
acquired_on DATE, acquired_on DATE,
image_url TEXT, image_url TEXT,
notes TEXT, notes TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX IF NOT EXISTS idx_firearms_profile_id ON firearms(profile_id);
CREATE TABLE IF NOT EXISTS ammo_inventory ( CREATE TABLE IF NOT EXISTS ammo_inventory (
caliber_id UUID PRIMARY KEY REFERENCES calibers(id) ON DELETE CASCADE, profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
caliber_id UUID NOT NULL REFERENCES calibers(id) ON DELETE CASCADE,
rounds_on_hand INT NOT NULL DEFAULT 0 CHECK (rounds_on_hand >= 0), rounds_on_hand INT NOT NULL DEFAULT 0 CHECK (rounds_on_hand >= 0),
cost_per_round NUMERIC(10, 2) NOT NULL DEFAULT 0, cost_per_round NUMERIC(10, 2) NOT NULL DEFAULT 0,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (profile_id, caliber_id)
); );
INSERT INTO calibers (name, is_default, is_active) INSERT INTO auth_provider_configs (
provider_key,
display_name,
protocol,
authorization_endpoint,
token_endpoint,
userinfo_endpoint,
issuer,
scopes,
enabled
)
VALUES VALUES
('9mm', TRUE, TRUE), (
('.22 LR', TRUE, TRUE), 'google',
('5.56 NATO', TRUE, TRUE), 'Google',
('.308 Win', TRUE, TRUE), 'oidc',
('12 Gauge', TRUE, TRUE), 'https://accounts.google.com/o/oauth2/v2/auth',
('.45 ACP', TRUE, TRUE) 'https://oauth2.googleapis.com/token',
ON CONFLICT (name) DO NOTHING; 'https://openidconnect.googleapis.com/v1/userinfo',
'https://accounts.google.com',
INSERT INTO ammo_inventory (caliber_id, rounds_on_hand, cost_per_round) 'openid profile email',
SELECT id, 0, 0 FALSE
FROM calibers ),
ON CONFLICT (caliber_id) DO NOTHING; (
'entra',
'Microsoft Entra ID',
'oidc',
'',
'',
'https://graph.microsoft.com/oidc/userinfo',
'',
'openid profile email',
FALSE
),
(
'oidc',
'Custom OIDC',
'oidc',
'',
'',
'',
'',
'openid profile email',
FALSE
)
ON CONFLICT (provider_key) DO NOTHING;
+225
View File
@@ -8,6 +8,8 @@
"name": "arsenal-iq-backend", "name": "arsenal-iq-backend",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"axios": "1.6.2",
"bcryptjs": "2.4.3",
"cors": "2.8.5", "cors": "2.8.5",
"dotenv": "16.4.5", "dotenv": "16.4.5",
"express": "4.18.2", "express": "4.18.2",
@@ -17,10 +19,12 @@
"pg": "8.11.3" "pg": "8.11.3"
}, },
"devDependencies": { "devDependencies": {
"@types/bcryptjs": "2.4.6",
"@types/cors": "2.8.17", "@types/cors": "2.8.17",
"@types/express": "4.17.21", "@types/express": "4.17.21",
"@types/morgan": "1.9.9", "@types/morgan": "1.9.9",
"@types/node": "20.10.6", "@types/node": "20.10.6",
"@types/pg": "8.10.9",
"tsx": "4.7.0", "tsx": "4.7.0",
"typescript": "5.3.3" "typescript": "5.3.3"
} }
@@ -416,6 +420,13 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@types/bcryptjs": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/body-parser": { "node_modules/@types/body-parser": {
"version": "1.19.6", "version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
@@ -500,6 +511,80 @@
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"node_modules/@types/pg": {
"version": "8.10.9",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.9.tgz",
"integrity": "sha512-UksbANNE/f8w0wOMxVKKIrLCbEMV+oM1uKejmwXr39olg4xqcfBDbXxObJAt6XxHbDa4XTKOlUEcEltXDX+XLQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^4.0.1"
}
},
"node_modules/@types/pg/node_modules/pg-types": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.1.0.tgz",
"integrity": "sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"pg-numeric": "1.0.2",
"postgres-array": "~3.0.1",
"postgres-bytea": "~3.0.0",
"postgres-date": "~2.1.0",
"postgres-interval": "^3.0.0",
"postgres-range": "^1.1.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@types/pg/node_modules/postgres-array": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
"integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/@types/pg/node_modules/postgres-bytea": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz",
"integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==",
"dev": true,
"license": "MIT",
"dependencies": {
"obuf": "~1.1.2"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@types/pg/node_modules/postgres-date": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz",
"integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/@types/pg/node_modules/postgres-interval": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz",
"integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/@types/qs": { "node_modules/@types/qs": {
"version": "6.15.0", "version": "6.15.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
@@ -554,6 +639,23 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/basic-auth": { "node_modules/basic-auth": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
@@ -572,6 +674,12 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
"license": "MIT"
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.1", "version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
@@ -643,6 +751,18 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -701,6 +821,15 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -791,6 +920,21 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.19.12", "version": "0.19.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
@@ -920,6 +1064,42 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1036,6 +1216,21 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -1230,6 +1425,13 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/obuf": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
"dev": true,
"license": "MIT"
},
"node_modules/on-finished": { "node_modules/on-finished": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -1323,6 +1525,16 @@
"node": ">=4.0.0" "node": ">=4.0.0"
} }
}, },
"node_modules/pg-numeric": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz",
"integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=4"
}
},
"node_modules/pg-pool": { "node_modules/pg-pool": {
"version": "3.13.0", "version": "3.13.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
@@ -1402,6 +1614,13 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/postgres-range": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz",
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
"dev": true,
"license": "MIT"
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -1415,6 +1634,12 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.11.0", "version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+4
View File
@@ -10,6 +10,8 @@
"start": "node dist/app.js" "start": "node dist/app.js"
}, },
"dependencies": { "dependencies": {
"axios": "1.6.2",
"bcryptjs": "2.4.3",
"cors": "2.8.5", "cors": "2.8.5",
"dotenv": "16.4.5", "dotenv": "16.4.5",
"express": "4.18.2", "express": "4.18.2",
@@ -19,10 +21,12 @@
"pg": "8.11.3" "pg": "8.11.3"
}, },
"devDependencies": { "devDependencies": {
"@types/bcryptjs": "2.4.6",
"@types/cors": "2.8.17", "@types/cors": "2.8.17",
"@types/express": "4.17.21", "@types/express": "4.17.21",
"@types/morgan": "1.9.9", "@types/morgan": "1.9.9",
"@types/node": "20.10.6", "@types/node": "20.10.6",
"@types/pg": "8.10.9",
"tsx": "4.7.0", "tsx": "4.7.0",
"typescript": "5.3.3" "typescript": "5.3.3"
} }
+958 -256
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -29,6 +29,7 @@ services:
NODE_ENV: ${NODE_ENV:-development} NODE_ENV: ${NODE_ENV:-development}
DATABASE_URL: postgresql://${POSTGRES_USER:-arsenal}:${POSTGRES_PASSWORD:-arsenal_dev_password}@postgres:5432/${POSTGRES_DB:-arsenal_iq} DATABASE_URL: postgresql://${POSTGRES_USER:-arsenal}:${POSTGRES_PASSWORD:-arsenal_dev_password}@postgres:5432/${POSTGRES_DB:-arsenal_iq}
FRONTEND_URL: ${FRONTEND_URL:-http://localhost:3000} FRONTEND_URL: ${FRONTEND_URL:-http://localhost:3000}
ALLOW_REGISTRATION: ${ALLOW_REGISTRATION:-true}
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
@@ -50,6 +51,7 @@ services:
container_name: arsenaliq-frontend container_name: arsenaliq-frontend
environment: environment:
VITE_API_BASE_URL: ${VITE_API_BASE_URL:-http://localhost:5000/api} VITE_API_BASE_URL: ${VITE_API_BASE_URL:-http://localhost:5000/api}
VITE_ALLOW_REGISTRATION: ${ALLOW_REGISTRATION:-true}
depends_on: depends_on:
- backend - backend
ports: ports:
+448 -278
View File
@@ -3,49 +3,29 @@
@tailwind utilities; @tailwind utilities;
:root { :root {
color-scheme: dark; font-family: "Segoe UI", "Inter", sans-serif;
--bg: #0d1216; color: #e8eadf;
--panel: rgba(20, 28, 34, 0.92); background:
--panel-soft: rgba(28, 38, 45, 0.84); radial-gradient(circle at top left, rgba(94, 112, 71, 0.28), transparent 32%),
--line: rgba(255, 255, 255, 0.08); radial-gradient(circle at bottom right, rgba(67, 80, 51, 0.26), transparent 30%),
--text: #edf3ef; linear-gradient(160deg, #10130f 0%, #171b15 45%, #0d100c 100%);
--muted: #97a8a5;
--gold: #d8b36a;
--accent: #78b8a4;
--shadow: 0 24px 70px rgba(0, 0, 0, 0.34);
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
html { html,
background: body,
radial-gradient(circle at top left, rgba(216, 179, 106, 0.16), transparent 22%), #root {
radial-gradient(circle at bottom right, rgba(120, 184, 164, 0.14), transparent 24%), min-height: 100%;
linear-gradient(180deg, #11181d 0%, #0a0f12 100%); margin: 0;
} }
body { body {
margin: 0;
min-width: 320px;
min-height: 100vh; min-height: 100vh;
color: var(--text); color: #e8eadf;
font-family: "Avenir Next", "Segoe UI", sans-serif; background: transparent;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#root {
min-height: 100vh;
}
h1,
h2,
h3,
.eyebrow,
.panel-kicker {
font-family: "Iowan Old Style", "Palatino Linotype", Georgia, serif;
} }
button, button,
@@ -55,233 +35,456 @@ textarea {
font: inherit; font: inherit;
} }
button {
cursor: pointer;
}
input, input,
select, select,
textarea { textarea {
width: 100%; width: 100%;
margin-top: 8px; border: 1px solid rgba(171, 180, 140, 0.18);
padding: 12px 14px;
color: var(--text);
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px; border-radius: 14px;
appearance: none; background: rgba(12, 16, 11, 0.72);
-webkit-appearance: none; color: #eef1e5;
-moz-appearance: none; padding: 0.85rem 1rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
} }
select { input:focus,
background: select:focus,
linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.03)), textarea:focus {
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M5 7.5L10 12.5L15 7.5' stroke='%23d8b36a' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") outline: none;
no-repeat right 14px center; border-color: rgba(140, 158, 101, 0.74);
padding-right: 42px; box-shadow: 0 0 0 3px rgba(109, 127, 73, 0.2);
}
select option {
color: var(--text);
background: #162027;
} }
textarea { textarea {
resize: vertical; resize: vertical;
} }
.app-shell {
display: grid;
grid-template-columns: 290px minmax(0, 1fr);
gap: 22px;
width: min(1440px, calc(100% - 28px));
margin: 0 auto;
padding: 20px 0 36px;
}
.sidebar,
.panel,
.summary-card,
.error-banner {
border: 1px solid var(--line);
background: var(--panel);
backdrop-filter: blur(16px);
box-shadow: var(--shadow);
}
.sidebar {
display: flex;
flex-direction: column;
gap: 18px;
padding: 22px;
border-radius: 28px;
position: sticky;
top: 20px;
height: fit-content;
}
.brand-block h1,
.stage-header h2 {
margin: 8px 0 12px;
}
.brand-block p,
.summary-card p,
.placeholder-copy,
.settings-row p,
.ammo-card p,
.firearm-card p,
.mini-stat span,
.card-footer span {
color: var(--muted);
}
.eyebrow, .eyebrow,
.panel-kicker { .panel-kicker {
color: var(--gold);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.16em; letter-spacing: 0.16em;
font-size: 0.76rem; font-size: 0.72rem;
color: #aab37d;
} }
.nav-stack { .loading-shell,
.auth-shell {
min-height: 100vh;
display: grid; display: grid;
gap: 10px; gap: 2rem;
align-items: center;
padding: 3rem;
} }
.nav-button, .loading-shell {
.primary-button, place-items: center;
.secondary-button, }
.chip-button {
.loading-card,
.auth-card,
.auth-hero,
.panel,
.sidebar {
border: 1px solid rgba(171, 180, 140, 0.12);
background: rgba(17, 22, 16, 0.76);
backdrop-filter: blur(16px);
box-shadow: 0 28px 60px rgba(0, 0, 0, 0.28);
}
.loading-card,
.auth-card,
.auth-hero {
border-radius: 28px;
padding: 2rem;
}
.auth-shell {
grid-template-columns: 1.1fr 0.9fr;
}
.auth-hero {
min-height: 520px;
display: flex;
flex-direction: column;
justify-content: center;
background:
linear-gradient(135deg, rgba(103, 120, 68, 0.2), transparent 55%),
rgba(17, 22, 16, 0.82);
}
.auth-brand {
display: flex;
align-items: center;
gap: 1rem;
}
.brand-mark {
width: 72px;
height: 72px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 10px;
border-radius: 14px;
border: 1px solid transparent;
cursor: pointer;
}
.nav-button {
width: 100%;
justify-content: flex-start;
padding: 14px 16px;
background: rgba(255, 255, 255, 0.03);
color: var(--text);
}
.nav-button.active {
background: linear-gradient(135deg, rgba(216, 179, 106, 0.18), rgba(120, 184, 164, 0.12));
border-color: rgba(216, 179, 106, 0.18);
}
.summary-card {
padding: 18px;
border-radius: 22px; border-radius: 22px;
background: linear-gradient(180deg, rgba(216, 179, 106, 0.12), rgba(255, 255, 255, 0.02)); color: #dfe6c8;
background: linear-gradient(145deg, rgba(104, 121, 68, 0.34), rgba(47, 56, 36, 0.76));
border: 1px solid rgba(171, 180, 140, 0.18);
} }
.summary-card strong { .auth-hero h1 {
display: block; font-size: clamp(2.4rem, 5vw, 4.2rem);
margin-top: 10px; line-height: 1.04;
font-size: 2rem; margin: 0.35rem 0 0;
max-width: none;
} }
.main-stage { .auth-hero p,
display: grid; .muted-copy,
gap: 18px; .header-copy,
.card-footer span,
.settings-row p,
.provider-header p {
color: #b8c0af;
} }
.stage-header { .hero-tags,
.chip-row,
.button-row {
display: flex; display: flex;
align-items: flex-end; gap: 0.75rem;
justify-content: space-between; flex-wrap: wrap;
gap: 18px;
padding: 8px 4px;
} }
.stage-stats { .hero-tags span,
display: flex; .profile-chip,
gap: 14px; .status-pill {
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
border-radius: 999px;
padding: 0.55rem 0.95rem;
background: rgba(171, 180, 140, 0.08);
border: 1px solid rgba(171, 180, 140, 0.12);
color: inherit;
} }
.mini-stat { .auth-card {
min-width: 160px; max-width: 520px;
padding: 14px 16px; width: 100%;
border-radius: 18px; justify-self: center;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.05);
} }
.mini-stat strong { .auth-tabs,
display: block; .settings-inline,
margin-top: 8px; .header-tools,
font-size: 1.2rem; .provider-header,
} .toggle-row,
.panel {
padding: 22px;
border-radius: 26px;
}
.panel-heading,
.card-footer,
.ammo-card-top, .ammo-card-top,
.settings-row { .panel-heading,
.settings-row,
.card-footer,
.stage-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 1rem;
} }
.panel-heading { .auth-tabs {
margin-bottom: 18px; margin-bottom: 1.25rem;
} }
.tab-button,
.nav-button,
.profile-chip,
.secondary-button,
.primary-button {
border: 0;
transition: transform 0.2s ease, background 0.2s ease, border-color 0.2s ease;
}
.tab-button {
flex: 1;
padding: 0.95rem 1rem;
border-radius: 16px;
background: rgba(171, 180, 140, 0.06);
color: #dce2d2;
}
.tab-button.active,
.nav-button.active,
.primary-button,
.profile-chip.active {
background: linear-gradient(135deg, #89985f, #4c5736);
color: #eef3e4;
}
.form-stack,
.settings-block,
.settings-list,
.provider-config-grid,
.nav-stack,
.main-stage,
.view-grid,
.firearm-grid,
.ammo-grid,
.settings-grid {
display: grid;
gap: 1rem;
}
.auth-divider {
margin: 1.5rem 0 1rem;
text-align: center;
color: #8d9586;
position: relative;
}
.auth-divider span {
position: relative;
padding: 0 0.75rem;
background: rgba(16, 20, 27, 0.92);
}
.auth-divider::before {
content: "";
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: rgba(171, 180, 140, 0.12);
}
.provider-list {
display: grid;
gap: 0.75rem;
}
.sso-button {
width: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 14px;
padding: 0.9rem 1rem;
border: 1px solid rgba(171, 180, 140, 0.12);
background: rgba(171, 180, 140, 0.06);
color: #edf0e3;
transition: transform 0.2s ease, background 0.2s ease, border-color 0.2s ease;
}
.sso-button:hover {
transform: translateY(-1px);
background: rgba(171, 180, 140, 0.12);
}
.full-width {
width: 100%;
}
.error-banner,
.success-banner {
margin: 0;
padding: 0.95rem 1rem;
border-radius: 16px;
}
.error-banner {
background: rgba(146, 49, 49, 0.24);
border: 1px solid rgba(222, 96, 96, 0.28);
color: #ffd0d0;
}
.success-banner {
background: rgba(43, 97, 76, 0.24);
border: 1px solid rgba(88, 180, 143, 0.28);
color: #d3ffe8;
}
.toast-banner {
position: fixed;
top: 1rem;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
min-width: min(520px, calc(100vw - 2rem));
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28);
}
.primary-button,
.secondary-button,
.nav-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.55rem;
border-radius: 14px;
padding: 0.85rem 1.1rem;
}
.primary-button {
font-weight: 700;
}
.secondary-button,
.nav-button,
.profile-chip {
background: rgba(171, 180, 140, 0.06);
color: #edf0e3;
border: 1px solid rgba(171, 180, 140, 0.12);
}
.nav-button {
justify-content: flex-start;
}
.primary-button:hover,
.secondary-button:hover,
.tab-button:hover,
.nav-button:hover,
.profile-chip:hover {
transform: translateY(-1px);
}
.app-shell {
min-height: 100vh;
display: grid;
grid-template-columns: 300px 1fr;
gap: 1.5rem;
padding: 1.5rem;
}
.sidebar,
.panel {
border-radius: 28px;
}
.sidebar {
padding: 1.5rem;
position: sticky;
top: 1.5rem;
height: calc(100vh - 3rem);
}
.mobile-sidebar {
display: none;
}
.brand-block h1,
.stage-header h2,
.panel h3 {
margin: 0.55rem 0 0.35rem;
}
.main-stage {
align-content: start;
}
.stage-header {
padding: 0.5rem 0;
}
.profile-picker {
min-width: 220px;
}
.profile-picker span,
label span {
display: block;
margin-bottom: 0.45rem;
color: #d9e0e6;
font-size: 0.92rem;
}
.stage-stats,
.view-grid, .view-grid,
.settings-grid { .settings-grid {
display: grid; display: grid;
gap: 18px; gap: 1rem;
}
.stage-stats {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.mini-stat {
border-radius: 22px;
padding: 1.2rem 1.35rem;
background: rgba(17, 22, 16, 0.72);
border: 1px solid rgba(171, 180, 140, 0.12);
}
.mini-stat span {
color: #b7bead;
display: block;
margin-bottom: 0.45rem;
}
.mini-stat strong {
font-size: 1.65rem;
} }
.view-grid { .view-grid {
grid-template-columns: minmax(0, 1.4fr) minmax(340px, 0.8fr); grid-template-columns: minmax(0, 1.35fr) minmax(360px, 0.8fr);
}
.settings-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.auth-settings-panel,
.settings-menu-panel {
grid-column: 1 / -1;
}
.panel {
padding: 1.4rem;
} }
.firearm-grid, .firearm-grid,
.ammo-grid, .ammo-grid,
.settings-list, .provider-config-grid {
.chip-grid { margin-top: 1rem;
display: grid;
gap: 14px;
} }
.ammo-toolbar { .firearm-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
gap: 14px; }
margin-bottom: 18px;
.ammo-grid,
.provider-config-grid {
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
} }
.firearm-card, .firearm-card,
.ammo-card { .ammo-card,
padding: 18px; .provider-card {
border-radius: 22px; border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.06); padding: 1rem;
background: var(--panel-soft); background: rgba(12, 16, 11, 0.74);
} border: 1px solid rgba(171, 180, 140, 0.1);
.firearm-card {
display: grid;
gap: 16px;
} }
.firearm-visual { .firearm-visual {
overflow: hidden; min-height: 180px;
border-radius: 18px; border-radius: 20px;
aspect-ratio: 16 / 7; padding: 1rem;
background: rgba(255, 255, 255, 0.04); display: flex;
align-items: center;
justify-content: center;
background: rgba(171, 180, 140, 0.04);
} }
.firearm-visual img { .firearm-photo,
.firearm-silhouette {
width: 100%; width: 100%;
height: 100%; max-height: 180px;
border-radius: 18px;
} }
.firearm-photo { .firearm-photo {
@@ -297,134 +500,101 @@ textarea {
.form-grid { .form-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px; gap: 0.9rem;
margin-top: 1rem;
} }
.form-grid.compact { .form-grid.compact {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: minmax(0, 1fr);
} }
.full-width { .full-width {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
label span {
display: block;
color: var(--muted);
font-size: 0.84rem;
}
.card-footer { .card-footer {
margin-top: 4px; margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid rgba(171, 180, 140, 0.08);
} }
.primary-button, .placeholder-copy {
.secondary-button, margin: 0;
.chip-button { color: #abb4a1;
padding: 12px 16px; padding: 1rem 0;
}
.button-row {
display: flex;
gap: 10px;
}
.primary-button {
background: var(--gold);
color: #16120d;
}
.secondary-button {
background: rgba(255, 255, 255, 0.05);
color: var(--text);
border-color: rgba(255, 255, 255, 0.08);
}
.chip-grid {
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
}
.chip-button {
background: rgba(255, 255, 255, 0.04);
color: var(--text);
border-color: rgba(255, 255, 255, 0.08);
}
.chip-button.disabled {
opacity: 0.45;
cursor: not-allowed;
}
.settings-inline {
display: flex;
gap: 12px;
}
.settings-inline input {
margin-top: 0;
} }
.settings-row { .settings-row {
padding: 14px 0; padding: 0.95rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05); border-top: 1px solid rgba(171, 180, 140, 0.08);
} }
.settings-row:last-child { .settings-row:first-child {
border-bottom: 0; border-top: 0;
padding-top: 0;
} }
.badge { .settings-row.static {
display: inline-flex; padding-bottom: 0.4rem;
align-items: center;
border-radius: 999px;
padding: 8px 12px;
background: rgba(120, 184, 164, 0.14);
border: 1px solid rgba(120, 184, 164, 0.18);
color: #c9efe4;
} }
.error-banner { .status-pill {
padding: 14px 16px; color: #eef3e4;
border-radius: 18px; background: linear-gradient(135deg, #829455, #59693d);
background: rgba(201, 83, 83, 0.16); border: 0;
border-color: rgba(201, 83, 83, 0.3);
} }
@media (max-width: 1120px) { .toggle-row {
gap: 0.65rem;
color: #d7ddc8;
}
.toggle-row input {
width: 18px;
height: 18px;
padding: 0;
}
@media (max-width: 1100px) {
.app-shell, .app-shell,
.view-grid { .auth-shell,
.view-grid,
.settings-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.desktop-sidebar {
display: none;
}
.mobile-sidebar {
display: block;
position: static;
height: auto;
}
.sidebar { .sidebar {
position: static; position: static;
height: auto;
} }
} }
@media (max-width: 720px) { @media (max-width: 720px) {
.app-shell { .app-shell,
width: min(100% - 16px, 1440px); .auth-shell {
padding-top: 12px; padding: 1rem;
} }
.stage-header, .stage-header,
.panel-heading, .header-tools,
.card-footer, .card-footer,
.button-row, .settings-inline {
.settings-inline,
.settings-row,
.ammo-card-top {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: stretch;
} }
.stage-stats, .stage-stats,
.form-grid, .form-grid {
.form-grid.compact {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.mini-stat {
width: 100%;
}
} }
File diff suppressed because it is too large Load Diff