feat: portal SAM - Next.js 15 + Bootstrap, integración con deployer API
This commit is contained in:
1 parent
229cbd074a
commit
7691c01ff1
9 files changed
+996
-98
No files matched your search
+24
@@ -0,0 +1,24 @@
|
|||||||
|
FROM node:22-alpine AS base
|
||||||
|
|
||||||
|
FROM base AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
USER nextjs
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "server.js"]
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
output: "standalone",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
Generated
+262
-5
@@ -8,6 +8,8 @@
|
|||||||
"name": "sam-portal",
|
"name": "sam-portal",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bootstrap": "^5.3.8",
|
||||||
|
"boxicons": "^2.1.4",
|
||||||
"next": "16.3.4",
|
"next": "16.3.4",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8"
|
"react-dom": "19.2.8"
|
||||||
@@ -228,6 +230,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||||
@@ -1377,6 +1388,17 @@
|
|||||||
"node": ">=12.4.0"
|
"node": ">=12.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@popperjs/core": {
|
||||||
|
"version": "2.11.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
|
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/popperjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rtsao/scc": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||||
@@ -2387,6 +2409,12 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@webcomponents/webcomponentsjs": {
|
||||||
|
"version": "2.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.8.0.tgz",
|
||||||
|
"integrity": "sha512-loGD63sacRzOzSJgQnB9ZAhaQGkN7wl2Zuw7tsphI5Isa0irijrRo6EnJii/GgjGefIFO8AIO7UivzRhFaEk9w==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.18.0",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
|
||||||
@@ -2692,6 +2720,78 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bootstrap": {
|
||||||
|
"version": "5.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
|
||||||
|
"integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/twbs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/bootstrap"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@popperjs/core": "^2.11.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/boxicons": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/boxicons/-/boxicons-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-BvJNfYfnE4g9WQ7GL91fftxMOTwAleWlPFwvQJPYb/Ju7aLjlQ/Eu55AH9JLNk/OR82z+ZSq4TbKzbV/e5Rr0A==",
|
||||||
|
"license": "(CC-BY-4.0 OR OFL-1.1 OR MIT)",
|
||||||
|
"dependencies": {
|
||||||
|
"@webcomponents/webcomponentsjs": "^2.0.2",
|
||||||
|
"prop-types": "^15.6.0",
|
||||||
|
"react": "^16.0.0",
|
||||||
|
"react-dom": "^16.0.0",
|
||||||
|
"react-interactive": "^0.8.1",
|
||||||
|
"react-router-dom": "^4.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/boxicons/node_modules/react": {
|
||||||
|
"version": "16.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
|
||||||
|
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/boxicons/node_modules/react-dom": {
|
||||||
|
"version": "16.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
|
||||||
|
"integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"prop-types": "^15.6.2",
|
||||||
|
"scheduler": "^0.19.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/boxicons/node_modules/scheduler": {
|
||||||
|
"version": "0.19.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
|
||||||
|
"integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"object-assign": "^4.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.18",
|
"version": "1.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||||
@@ -3031,6 +3131,24 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-hover": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-hover/-/detect-hover-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-HtLoY+tClgYucJNiovNICGWFp9nOGVmHY44s7L62iPqORXM9vujeWFaVcqtA7XRvp/2Y+4RBUfHbDKFGN+xxZQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/detect-it": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-it/-/detect-it-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-RxpgcdbatUX6epJE09K16iJqF7x6iEcEdoL18FR2zpBO4JhnL7aMOAUoUEyexdtbWOSfTmoDWmeD6mwRBQyRXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-hover": "^1.0.3",
|
||||||
|
"detect-passive-events": "^1.0.5",
|
||||||
|
"detect-pointer": "^1.0.3",
|
||||||
|
"detect-touch-events": "^2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
@@ -3041,6 +3159,24 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-passive-events": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-passive-events/-/detect-passive-events-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-foW7Q35wwOCxVzW0xLf5XeB5Fhe7oyRgvkBYdiP9IWgLMzjqUqTvsJv9ymuEWGjY6AoDXD3OC294+Z9iuOw0QA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/detect-pointer": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-pointer/-/detect-pointer-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-d0o/Puo3fiGSCXy6H039h9Kwz+mmYCGKZ/qtPFnpN3WfsumjC1C9b5KKvRu+aYnfdI8peqN/iAe7dPd85qIt2g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/detect-touch-events": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-touch-events/-/detect-touch-events-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-g8GWBkJLiIDRJfRXEdrd1wMXpNyGId2DkbfuwFahSb4OCvn717hyRJtAcEDISfp3zkwEhZ4Y4woHPA6DeyB3Fw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/doctrine": {
|
"node_modules/doctrine": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||||
@@ -4175,6 +4311,26 @@
|
|||||||
"hermes-estree": "0.25.1"
|
"hermes-estree": "0.25.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/history": {
|
||||||
|
"version": "4.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||||
|
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.1.2",
|
||||||
|
"loose-envify": "^1.2.0",
|
||||||
|
"resolve-pathname": "^3.0.0",
|
||||||
|
"tiny-invariant": "^1.0.2",
|
||||||
|
"tiny-warning": "^1.0.0",
|
||||||
|
"value-equal": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hoist-non-react-statics": {
|
||||||
|
"version": "2.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
|
||||||
|
"integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -4227,6 +4383,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/invariant": {
|
||||||
|
"version": "2.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||||
|
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -4704,7 +4869,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
@@ -5137,7 +5301,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
@@ -5385,7 +5548,6 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -5613,6 +5775,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/path-to-regexp": {
|
||||||
|
"version": "1.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
|
||||||
|
"integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"isarray": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-to-regexp/node_modules/isarray": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -5685,7 +5862,6 @@
|
|||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.4.0",
|
"loose-envify": "^1.4.0",
|
||||||
@@ -5745,13 +5921,61 @@
|
|||||||
"react": "^19.2.8"
|
"react": "^19.2.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-interactive": {
|
||||||
|
"version": "0.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-interactive/-/react-interactive-0.8.3.tgz",
|
||||||
|
"integrity": "sha512-mmRvA9aKP7zu9kVfP1AX1egX8tFlnE3DDXq92z0JTZezfOpmeQBzr77O1+mTV54OOmn+M2t6c5kFD5VnuFoM7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-it": "^3.0.3",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"prop-types": "^15.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"history": "^4.7.2",
|
||||||
|
"hoist-non-react-statics": "^2.5.0",
|
||||||
|
"invariant": "^2.2.4",
|
||||||
|
"loose-envify": "^1.3.1",
|
||||||
|
"path-to-regexp": "^1.7.0",
|
||||||
|
"prop-types": "^15.6.1",
|
||||||
|
"warning": "^4.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=15"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"history": "^4.7.2",
|
||||||
|
"invariant": "^2.2.4",
|
||||||
|
"loose-envify": "^1.3.1",
|
||||||
|
"prop-types": "^15.6.1",
|
||||||
|
"react-router": "^4.3.1",
|
||||||
|
"warning": "^4.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
@@ -5830,6 +6054,12 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resolve-pathname": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve-pkg-maps": {
|
"node_modules/resolve-pkg-maps": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
@@ -6394,6 +6624,18 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-invariant": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tiny-warning": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
@@ -6734,6 +6976,21 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/value-equal": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/warning": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bootstrap": "^5.3.8",
|
||||||
|
"boxicons": "^2.1.4",
|
||||||
"next": "16.3.4",
|
"next": "16.3.4",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8"
|
"react-dom": "19.2.8"
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
const DEPLOYER_URL = process.env.DEPLOYER_URL || "http://127.0.0.1:3010";
|
||||||
|
const DEPLOYER_TOKEN = process.env.DEPLOYER_TOKEN || "";
|
||||||
|
|
||||||
|
function headers() {
|
||||||
|
const h: Record<string, string> = { "Content-Type": "application/json" };
|
||||||
|
if (DEPLOYER_TOKEN) h["X-API-Token"] = DEPLOYER_TOKEN;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${DEPLOYER_URL}/api/modules`, {
|
||||||
|
headers: headers(),
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
return NextResponse.json(data, { status: res.status });
|
||||||
|
} catch (e) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: e instanceof Error ? e.message : "Error al conectar con deployer" },
|
||||||
|
{ status: 502 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const res = await fetch(`${DEPLOYER_URL}/api/modules`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: headers(),
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
return NextResponse.json(data, { status: res.status });
|
||||||
|
} catch (e) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: e instanceof Error ? e.message : "Error al registrar módulo" },
|
||||||
|
{ status: 502 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+141
-21
@@ -1,26 +1,146 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--primary: #00285d;
|
||||||
--foreground: #171717;
|
--primary-light: #1a3f7a;
|
||||||
}
|
--primary-gradient: linear-gradient(135deg, #00285d 0%, #1a4a8a 100%);
|
||||||
|
--card-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
@theme inline {
|
--card-shadow-hover: 0 6px 24px rgba(0, 0, 0, 0.14);
|
||||||
--color-background: var(--background);
|
--radius: 12px;
|
||||||
--color-foreground: var(--foreground);
|
--transition: 0.25s ease;
|
||||||
--font-sans: var(--font-geist-sans);
|
|
||||||
--font-mono: var(--font-geist-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--background: #0a0a0a;
|
|
||||||
--foreground: #ededed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
font-family: "Fira Sans", sans-serif;
|
||||||
color: var(--foreground);
|
background-color: #f0f2f5;
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-top: 20px;
|
||||||
|
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-main-text {
|
||||||
|
padding: 60px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-main-text img {
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 767px) {
|
||||||
|
.sidenav {
|
||||||
|
padding: 20px 0 10px;
|
||||||
|
}
|
||||||
|
.login-main-text {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.login-main-text h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.main {
|
||||||
|
margin-left: 40%;
|
||||||
|
}
|
||||||
|
.sidenav {
|
||||||
|
width: 40%;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border-left: 4px solid var(--primary);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
transition: transform var(--transition), box-shadow var(--transition),
|
||||||
|
background-color var(--transition);
|
||||||
|
border: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: var(--card-shadow-hover);
|
||||||
|
background-color: #f8faff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card a {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stretched-link ~ * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card table {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card-sm table {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card .bx-border {
|
||||||
|
border-radius: 8px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-respaldo {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 500;
|
||||||
|
vertical-align: middle;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-cards > [class*="col-"] {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-healthy {
|
||||||
|
background: #198754;
|
||||||
|
box-shadow: 0 0 6px rgba(25, 135, 84, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-unhealthy {
|
||||||
|
background: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-starting {
|
||||||
|
background: #ffc107;
|
||||||
}
|
}
|
||||||
+20
-19
@@ -1,29 +1,30 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
|
||||||
variable: "--font-geist-sans",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "SAM | ICHN - Portal de Módulos",
|
||||||
description: "Generated by create next app",
|
description: "Sistema de Administración de Matrículas - Portal de integración de módulos",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<html
|
<html lang="es">
|
||||||
lang="en"
|
<head>
|
||||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
<link
|
||||||
>
|
href="https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css"
|
||||||
<body className="min-h-full flex flex-col">{children}</body>
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css?family=Fira+Sans:400,500,600,700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>{children}</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+271
-52
@@ -1,69 +1,288 @@
|
|||||||
import Image from "next/image";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface ModuleState {
|
||||||
|
name: string;
|
||||||
|
repoUrl: string;
|
||||||
|
branch: string;
|
||||||
|
subdomain: string;
|
||||||
|
port: number;
|
||||||
|
active: boolean;
|
||||||
|
lastDeploy?: {
|
||||||
|
sha: string;
|
||||||
|
time: string;
|
||||||
|
ok: boolean;
|
||||||
|
log: string;
|
||||||
|
};
|
||||||
|
container?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse {
|
||||||
|
backend: {
|
||||||
|
repoUrl: string;
|
||||||
|
branch: string;
|
||||||
|
container: string | null;
|
||||||
|
lastDeploy: ModuleState["lastDeploy"] | null;
|
||||||
|
};
|
||||||
|
modules: ModuleState[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconMap: Record<string, { icon: string; bg: string }> = {
|
||||||
|
ventas: { icon: "bx-money", bg: "bg-success" },
|
||||||
|
comercial: { icon: "bx-money", bg: "bg-success" },
|
||||||
|
finanzas: { icon: "bx-detail", bg: "bg-danger" },
|
||||||
|
contabilidad: { icon: "bx-detail", bg: "bg-danger" },
|
||||||
|
academico: { icon: "bx-user", bg: "bg-primary" },
|
||||||
|
"academico-empresas": { icon: "bx-briefcase", bg: "bg-primary" },
|
||||||
|
profesor: { icon: "bx-pencil", bg: "bg-warning text-dark" },
|
||||||
|
profesoras: { icon: "bx-pencil", bg: "bg-warning text-dark" },
|
||||||
|
empresas: { icon: "bx-building", bg: "bg-info" },
|
||||||
|
pagos: { icon: "bx-credit-card", bg: "bg-success" },
|
||||||
|
default: { icon: "bx-cube", bg: "bg-secondary" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function getModuleMeta(name: string) {
|
||||||
|
const key = name.toLowerCase();
|
||||||
|
return iconMap[key] || iconMap.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusInfo(m: ModuleState) {
|
||||||
|
if (!m.container) return { label: "Detenido", cls: "status-unhealthy" };
|
||||||
|
if (m.container.includes("healthy")) return { label: "Activo", cls: "status-healthy" };
|
||||||
|
if (m.container.includes("starting")) return { label: "Iniciando", cls: "status-starting" };
|
||||||
|
return { label: "Activo", cls: "status-healthy" };
|
||||||
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const [data, setData] = useState<ApiResponse | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchModules = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await fetch("/api/modules", { cache: "no-store" });
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const json = await res.json();
|
||||||
|
setData(json);
|
||||||
|
setError(null);
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : "Error al cargar módulos");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchModules();
|
||||||
|
const id = setInterval(fetchModules, 15000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const modules = data?.modules ?? [];
|
||||||
|
const backend = data?.backend;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<>
|
||||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
<div className="sidenav">
|
||||||
<Image
|
<div className="row text-light login-main-text">
|
||||||
className="dark:invert h-5 w-[100px]"
|
<div className="col-12 text-center">
|
||||||
src="/next.svg"
|
<div
|
||||||
alt="Next.js logo"
|
className="mx-auto d-flex align-items-center justify-content-center bg-white rounded-3"
|
||||||
width={100}
|
style={{ width: 80, height: 80 }}
|
||||||
height={20}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
||||||
To get started, edit the{" "}
|
|
||||||
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
|
|
||||||
page.tsx
|
|
||||||
</code>{" "}
|
|
||||||
file.
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
>
|
||||||
Templates
|
<i className="bx bx-layer text-primary" style={{ fontSize: 48 }} />
|
||||||
</a>{" "}
|
</div>
|
||||||
or the{" "}
|
</div>
|
||||||
<a
|
<div className="col-12 text-center mt-3">
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<h1 className="fw-bold fs-2">Sistema SAM</h1>
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
</div>
|
||||||
|
<div className="col-12 text-center">
|
||||||
|
<h4 className="fw-semibold opacity-75">Aplicaciones</h4>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 text-center mt-4">
|
||||||
|
<Link href="/upload" className="btn btn-light btn-sm px-4">
|
||||||
|
<i className="bx bx-plus me-1" /> Nuevo Módulo
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 text-center mt-3">
|
||||||
|
<small className="opacity-50">Portal de integración</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="main">
|
||||||
|
<div className="row" id="modulos">
|
||||||
|
<div className="col-lg-1" />
|
||||||
|
<div className="col-lg-10 mt-4">
|
||||||
|
{loading && (
|
||||||
|
<div className="text-center py-5">
|
||||||
|
<div className="spinner-border text-primary" role="status" />
|
||||||
|
<p className="mt-2 text-muted">Cargando módulos...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-danger d-flex align-items-center">
|
||||||
|
<i className="bx bx-error-circle me-2 fs-5" />
|
||||||
|
<div>
|
||||||
|
<strong>Error:</strong> {error}
|
||||||
|
<button
|
||||||
|
onClick={fetchModules}
|
||||||
|
className="btn btn-sm btn-outline-danger ms-3"
|
||||||
>
|
>
|
||||||
Learning
|
Reintentar
|
||||||
</a>{" "}
|
</button>
|
||||||
center.
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && !error && (
|
||||||
|
<div className="row row-cards">
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="alert alert-light section-header mb-3 py-2 px-3 d-flex justify-content-between align-items-center">
|
||||||
|
<span>
|
||||||
|
<i className="bx bx-grid-alt me-1" /> Selección de Aplicación
|
||||||
|
</span>
|
||||||
|
<span className="badge bg-primary rounded-pill">
|
||||||
|
{modules.length} módulo{modules.length !== 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{modules.length === 0 ? (
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="app-card p-4 text-center">
|
||||||
|
<i className="bx bx-inbox fs-1 text-muted" />
|
||||||
|
<p className="text-muted mt-2 mb-3">
|
||||||
|
No hay módulos integrados. ¡Sube tu primer proyecto!
|
||||||
</p>
|
</p>
|
||||||
|
<Link href="/upload" className="btn btn-primary btn-sm">
|
||||||
|
<i className="bx bx-upload me-1" /> Subir Proyecto
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
</div>
|
||||||
<a
|
) : (
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
modules.map((m) => {
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
const meta = getModuleMeta(m.name);
|
||||||
target="_blank"
|
const status = getStatusInfo(m);
|
||||||
rel="noopener noreferrer"
|
const url = `https://${m.subdomain}.devops.norteamericano.cl`;
|
||||||
>
|
const displayName =
|
||||||
<Image
|
m.name.charAt(0).toUpperCase() + m.name.slice(1).replace(/-/g, " ");
|
||||||
className="dark:invert h-[14px] w-4"
|
return (
|
||||||
src="/vercel.svg"
|
<div className="col-md-6" key={m.name}>
|
||||||
alt="Vercel logomark"
|
<div className="app-card p-2">
|
||||||
width={16}
|
<table className="table table-borderless mb-0 align-middle">
|
||||||
height={14}
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td className="ps-3" style={{ width: 56 }}>
|
||||||
|
<i
|
||||||
|
className={`bx ${meta.icon} bx-sm bx-border text-light ${meta.bg}`}
|
||||||
/>
|
/>
|
||||||
Deploy Now
|
</td>
|
||||||
</a>
|
<td>
|
||||||
<a
|
<a
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
href={url}
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
className="text-decoration-none text-dark d-block py-2 stretched-link"
|
||||||
>
|
>
|
||||||
Documentation
|
{displayName}
|
||||||
</a>
|
</a>
|
||||||
|
<div className="d-flex align-items-center gap-2">
|
||||||
|
<span className={`status-dot ${status.cls}`} />
|
||||||
|
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
|
||||||
|
{status.label} · :{m.port} · {m.branch}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="text-end pe-3" style={{ width: 40 }}>
|
||||||
|
<i className="bx bx-chevron-right text-muted" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
|
||||||
|
{backend && (
|
||||||
|
<>
|
||||||
|
<div className="col-12 mt-4 mb-2">
|
||||||
|
<div className="alert alert-light section-header mb-3 py-2 px-3">
|
||||||
|
<i className="bx bx-server me-1" /> Backend & Sistema
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<div className="app-card p-2">
|
||||||
|
<table className="table table-borderless mb-0 align-middle">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td className="ps-3" style={{ width: 56 }}>
|
||||||
|
<i className="bx bx-code-alt bx-sm bx-border text-light bg-dark" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href="https://api.devops.norteamericano.cl/health"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-decoration-none text-dark d-block py-2 stretched-link"
|
||||||
|
>
|
||||||
|
API Backend
|
||||||
|
</a>
|
||||||
|
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
|
||||||
|
{backend.container ? "Activo" : "Detenido"} · {backend.branch}
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<div className="app-card p-2">
|
||||||
|
<table className="table table-borderless mb-0 align-middle">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td className="ps-3" style={{ width: 56 }}>
|
||||||
|
<i className="bx bx-cog bx-sm bx-border text-light bg-secondary" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Link
|
||||||
|
href="/upload"
|
||||||
|
className="text-decoration-none text-dark d-block py-2 stretched-link"
|
||||||
|
>
|
||||||
|
Gestionar Módulos
|
||||||
|
</Link>
|
||||||
|
<small className="text-muted" style={{ fontSize: "0.75rem" }}>
|
||||||
|
Subir / actualizar proyectos
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="col-12 mt-4 mb-4 text-center">
|
||||||
|
<small className="text-muted">
|
||||||
|
Auto-actualizado cada 15s ·{" "}
|
||||||
|
<a href="#" onClick={(e) => { e.preventDefault(); fetchModules(); }}>
|
||||||
|
actualizar ahora
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function UploadPage() {
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
name: "",
|
||||||
|
repoUrl: "",
|
||||||
|
branch: "main",
|
||||||
|
port: "",
|
||||||
|
subdomain: "",
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setForm({ ...form, [e.target.name]: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setResult(null);
|
||||||
|
|
||||||
|
const payload: Record<string, unknown> = {
|
||||||
|
name: form.name.trim().toLowerCase(),
|
||||||
|
repoUrl: form.repoUrl.trim(),
|
||||||
|
branch: form.branch.trim() || "main",
|
||||||
|
};
|
||||||
|
if (form.port) payload.port = parseInt(form.port, 10);
|
||||||
|
if (form.subdomain) payload.subdomain = form.subdomain.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/modules", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
setResult({
|
||||||
|
ok: true,
|
||||||
|
message: `Módulo "${payload.name}" registrado. Deploy: ${data.webhook ? "webhook creado" : "sin webhook"}${data.deployLog ? " · " + data.deployLog.slice(0, 80) : ""}`,
|
||||||
|
});
|
||||||
|
setForm({ name: "", repoUrl: "", branch: "main", port: "", subdomain: "" });
|
||||||
|
} else {
|
||||||
|
setResult({ ok: false, message: data.error || `Error HTTP ${res.status}` });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setResult({ ok: false, message: err instanceof Error ? err.message : "Error de red" });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="sidenav">
|
||||||
|
<div className="row text-light login-main-text">
|
||||||
|
<div className="col-12 text-center">
|
||||||
|
<div
|
||||||
|
className="mx-auto d-flex align-items-center justify-content-center bg-white rounded-3"
|
||||||
|
style={{ width: 80, height: 80 }}
|
||||||
|
>
|
||||||
|
<i className="bx bx-upload text-primary" style={{ fontSize: 48 }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 text-center mt-3">
|
||||||
|
<h1 className="fw-bold fs-2">Sistema SAM</h1>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 text-center">
|
||||||
|
<h4 className="fw-semibold opacity-75">Subir Proyecto</h4>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 text-center mt-4">
|
||||||
|
<Link href="/" className="btn btn-outline-light btn-sm px-4">
|
||||||
|
<i className="bx bx-arrow-back me-1" /> Volver
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="main">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-lg-1" />
|
||||||
|
<div className="col-lg-10 mt-4">
|
||||||
|
<div className="alert alert-light section-header mb-3 py-2 px-3">
|
||||||
|
<i className="bx bx-cloud-upload me-1" /> Integrar Nuevo Módulo
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="app-card p-4">
|
||||||
|
<p className="text-muted mb-4">
|
||||||
|
Registra un proyecto <strong>Next.js 15 + Bootstrap</strong> desde un repositorio
|
||||||
|
Gitea. Se clonará, desplegará automáticamente y quedará disponible en{" "}
|
||||||
|
<code>https://{"{subdominio}"}.devops.norteamericano.cl</code>. Cada push futuro
|
||||||
|
actualiza el módulo vía webhook.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<div className={`alert ${result.ok ? "alert-success" : "alert-danger"}`}>
|
||||||
|
<i className={`bx ${result.ok ? "bx-check-circle" : "bx-error-circle"} me-2`} />
|
||||||
|
{result.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="row g-3">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label fw-semibold">
|
||||||
|
Nombre del módulo <span className="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="name"
|
||||||
|
value={form.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="ej: ventas"
|
||||||
|
pattern="^[a-z][a-z0-9-]+$"
|
||||||
|
required
|
||||||
|
className="form-control"
|
||||||
|
/>
|
||||||
|
<div className="form-text">Minúsculas, guiones, empieza con letra.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label fw-semibold">
|
||||||
|
Subdominio <small className="text-muted">(opcional)</small>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="subdomain"
|
||||||
|
value={form.subdomain}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="ej: ventas (default: nombre)"
|
||||||
|
className="form-control"
|
||||||
|
/>
|
||||||
|
<div className="form-text">https://{form.subdomain || form.name || "subdominio"}.devops.norteamericano.cl</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12">
|
||||||
|
<label className="form-label fw-semibold">
|
||||||
|
URL del repositorio Gitea <span className="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="repoUrl"
|
||||||
|
value={form.repoUrl}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="https://git.norteamericano.cl/Fimm/mi-frontend.git"
|
||||||
|
required
|
||||||
|
className="form-control"
|
||||||
|
/>
|
||||||
|
<div className="form-text">Debe ser accesible desde el deployer (https://git.norteamericano.cl).</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label fw-semibold">Rama</label>
|
||||||
|
<input
|
||||||
|
name="branch"
|
||||||
|
value={form.branch}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="main"
|
||||||
|
className="form-control"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label fw-semibold">
|
||||||
|
Puerto <small className="text-muted">(opcional)</small>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="port"
|
||||||
|
type="number"
|
||||||
|
value={form.port}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="auto (3011+)"
|
||||||
|
min={3000}
|
||||||
|
max={4000}
|
||||||
|
className="form-control"
|
||||||
|
/>
|
||||||
|
<div className="form-text">Se asigna automáticamente si se deja vacío.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 mt-2">
|
||||||
|
<div className="alert alert-info py-2 small mb-0">
|
||||||
|
<i className="bx bx-info-circle me-1" />
|
||||||
|
El repositorio debe ser un proyecto <strong>Next.js 15</strong> (con{" "}
|
||||||
|
<code>package.json</code> en raíz o en <code>frontend/</code>) y usar{" "}
|
||||||
|
<strong>Bootstrap</strong>. Si no trae <code>Dockerfile</code>, se genera uno automáticamente.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 mt-3 d-flex gap-2">
|
||||||
|
<button type="submit" disabled={loading} className="btn btn-primary px-4">
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<span className="spinner-border spinner-border-sm me-2" /> Desplegando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<i className="bx bx-rocket me-1" /> Registrar y Desplegar
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<Link href="/" className="btn btn-outline-secondary">
|
||||||
|
Cancelar
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="app-card p-3 mt-3">
|
||||||
|
<h6 className="fw-semibold mb-2">
|
||||||
|
<i className="bx bx-book-open me-1" /> Requisitos del repositorio
|
||||||
|
</h6>
|
||||||
|
<ul className="small text-muted mb-0">
|
||||||
|
<li>
|
||||||
|
Proyecto Next.js 15 con <code>next build</code> / <code>next start</code>
|
||||||
|
</li>
|
||||||
|
<li>Bootstrap 5 (via CDN o npm) + Boxicons opcional</li>
|
||||||
|
<li>
|
||||||
|
Variable <code>NEXT_PUBLIC_API_URL=https://api.devops.norteamericano.cl</code>{" "}
|
||||||
|
para consumir el backend
|
||||||
|
</li>
|
||||||
|
<li>Branch por defecto: main (configurable)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user