init project

This commit is contained in:
root
2026-04-23 00:21:02 -04:00
parent 23826c5528
commit 9191fd32f0
64 changed files with 10229 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Run the Vite dev container from WSL (Docker Desktop with WSL2 backend, or Docker Engine in WSL).
#
# Usage (from repo root):
# chmod +x scripts/docker-up-wsl.sh
# ./scripts/docker-up-wsl.sh
#
# Or from anywhere:
# bash /mnt/d/Test/alrahma/client_laravel/scripts/docker-up-wsl.sh
#
# Windows D: drive in WSL is usually: /mnt/d/Test/alrahma/client_laravel
#
# Laravel API proxy: defaults to host port 8000. Override if requests fail:
# export VITE_PROXY_API=http://172.17.0.1:8000
# ./scripts/docker-up-wsl.sh
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT"
if [[ "$(pwd)" == /mnt/* ]]; then
echo "Note: Project is on a Windows mount (/mnt/...). File sync can be slower; Vite uses polling." >&2
fi
export VITE_PROXY_API="${VITE_PROXY_API:-http://host.docker.internal:8000}"
echo "Using VITE_PROXY_API=${VITE_PROXY_API}" >&2
echo "App → http://localhost:5173 (ensure Laravel is reachable from the container at the proxy target)" >&2
exec docker compose up --build client
+92
View File
@@ -0,0 +1,92 @@
/**
* Generates src/lib/ciRouteMeta.generated.ts from CodeIgniter app/Views/*.php files.
* Run: node scripts/gen-ci-route-meta.mjs
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const viewsRoot = path.resolve(__dirname, '../../app_codeigniter/app/Views')
const outFile = path.resolve(__dirname, '../src/lib/ciRouteMeta.generated.ts')
const SKIP_DIRS = new Set([
'layout',
'partials',
'errors',
'pagination',
'docs',
])
function humanize(slug) {
const s = slug.replace(/\.php$/i, '').replace(/_/g, ' ')
return s.replace(/\b\w/g, (c) => c.toUpperCase())
}
function walk(dir, baseRel) {
const out = []
let entries = []
try {
entries = fs.readdirSync(dir, { withFileTypes: true })
} catch {
return out
}
for (const e of entries) {
const rel = path.join(baseRel, e.name)
const full = path.join(dir, e.name)
if (e.isDirectory()) {
if (SKIP_DIRS.has(e.name)) continue
out.push(...walk(full, rel))
} else if (e.isFile() && e.name.endsWith('.php')) {
out.push(rel.replace(/\\/g, '/'))
}
}
return out
}
function main() {
if (!fs.existsSync(viewsRoot)) {
console.error('Missing Views folder:', viewsRoot)
process.exit(1)
}
const files = walk(viewsRoot, '')
.filter((f) => !f.includes('/layout/'))
.sort()
const entries = []
for (const f of files) {
const parts = f.split('/')
const base = parts.pop().replace(/\.php$/i, '')
const section = parts.join('/') || '(root)'
const routeKey = [...parts, base].filter(Boolean).join('/')
const ciViewPath = f
const title = humanize(base)
entries.push({ routeKey, title, ciViewPath })
}
const lines = []
lines.push(`/** Auto-generated by scripts/gen-ci-route-meta.mjs — do not edit by hand. */`)
lines.push(``)
lines.push(`export type CiRouteMetaEntry = {`)
lines.push(` title: string`)
lines.push(` ciViewPath: string`)
lines.push(`}`)
lines.push(``)
lines.push(`export const ciRouteMeta: Record<string, CiRouteMetaEntry> = {`)
for (const { routeKey, title, ciViewPath } of entries) {
const safeTitle = title.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
const safeView = ciViewPath.replace(/\\/g, '/')
lines.push(` '${routeKey}': { title: '${safeTitle}', ciViewPath: '${safeView}' },`)
}
lines.push(`}`)
lines.push(``)
fs.mkdirSync(path.dirname(outFile), { recursive: true })
fs.writeFileSync(outFile, lines.join('\n'), 'utf8')
console.log(`Wrote ${entries.length} entries → ${path.relative(process.cwd(), outFile)}`)
}
main()
+85
View File
@@ -0,0 +1,85 @@
/**
* Smoke-test the same API routes the Vite client uses.
* Usage: node scripts/test-api.mjs
* $env:API_BASE="http://127.0.0.1:5173"; node scripts/test-api.mjs # via Vite proxy
*/
const base = (process.env.API_BASE ?? 'http://127.0.0.1:8000').replace(/\/$/, '')
const tests = [
['GET', '/up', null],
['GET', '/api/v1/health', null],
['GET', '/api/v1/auth/register/captcha', null],
['GET', '/api/v1/policies/school', null],
['GET', '/api/docs/public', null],
[
'POST',
'/api/v1/auth/login',
JSON.stringify({ email: 'nosuch@example.org', password: 'wrong-password' }),
],
]
async function main() {
console.log(`API_BASE=${base}\n`)
for (const [method, path, body] of tests) {
const url = `${base}${path}`
try {
const init = {
method,
headers: {
Accept: 'application/json',
...(body ? { 'Content-Type': 'application/json' } : {}),
},
...(body ? { body } : {}),
}
const res = await fetch(url, init)
const text = await res.text()
let snippet = text.slice(0, 280).replace(/\s+/g, ' ')
let fatal = false
let dbError = false
try {
const j = JSON.parse(text)
const msg =
typeof j.message === 'string'
? j.message
: typeof j.exception === 'string'
? j.exception
: ''
if (j.exception?.includes?.('FatalError') || /FatalError/i.test(j.message ?? '')) {
fatal = true
snippet = `PHP FatalError: ${(j.message ?? '').slice(0, 100)}`
} else if (j.message && typeof j.message === 'string') {
snippet = j.message.slice(0, 120)
}
if (/SQLSTATE/i.test(msg) || /SQLSTATE/i.test(text)) {
dbError = true
snippet = `${snippet.slice(0, 80)}… (DB unavailable?)`
}
if (j.ok === true && path.includes('captcha')) {
snippet = `ok=true captcha=<${String(j.captcha ?? '').length} chars>`
}
if (j.status === true && path.includes('docs/public')) {
snippet = `title=${j.title ?? 'n/a'} openapi=${!!j.openapi_url}`
}
if (j.data?.policy && path.includes('policies/school')) {
snippet = `policy content len=${String(j.data.policy.content ?? '').length}`
}
} catch {
/* not JSON */
}
const ok =
!fatal &&
!dbError &&
(res.ok ||
(path.endsWith('/login') && (res.status === 401 || res.status === 400)) ||
res.status === 422)
console.log(
`${method} ${path}${res.status} ${ok ? '✓' : '✗'} ${snippet}`,
)
} catch (e) {
console.log(`${method} ${path} → ERROR ${e instanceof Error ? e.message : e}`)
}
}
}
main()