@loghead/core 0.1.18 ā 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/server.js +134 -2
- package/dist/cli_main.js +24 -5
- package/dist/public/assets/index-CEQopy59.css +1 -0
- package/dist/public/assets/index-JSs_QfLO.js +95 -0
- package/dist/public/index.html +13 -0
- package/dist/ui/main.js +1 -1
- package/package.json +20 -13
- package/build/loggerhead +0 -0
- package/build/loghead +0 -0
- package/deno.lock +0 -1003
- package/loggerhead.db +0 -0
- package/loghead.db +0 -0
- package/src/api/server.ts +0 -154
- package/src/cli_main.ts +0 -83
- package/src/db/client.ts +0 -20
- package/src/db/migrate.ts +0 -67
- package/src/services/auth.ts +0 -74
- package/src/services/db.ts +0 -173
- package/src/services/ollama.ts +0 -39
- package/src/tests/db.test.ts +0 -63
- package/src/types.ts +0 -31
- package/src/ui/main.ts +0 -197
- package/src/utils/startup.ts +0 -52
- package/tsconfig.json +0 -15
package/dist/api/server.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.startApiServer = startApiServer;
|
|
7
7
|
const express_1 = __importDefault(require("express"));
|
|
8
8
|
const cors_1 = __importDefault(require("cors"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
9
10
|
const auth_1 = require("../services/auth");
|
|
10
11
|
const chalk_1 = __importDefault(require("chalk"));
|
|
11
12
|
const auth = new auth_1.AuthService();
|
|
@@ -14,9 +15,106 @@ async function startApiServer(db) {
|
|
|
14
15
|
const port = process.env.PORT || 4567;
|
|
15
16
|
app.use((0, cors_1.default)());
|
|
16
17
|
app.use(express_1.default.json());
|
|
18
|
+
// Serve static frontend files
|
|
19
|
+
// Determine path based on whether we are running in src (dev) or dist (prod)
|
|
20
|
+
let publicPath = path_1.default.join(__dirname, "../public");
|
|
21
|
+
if (!require("fs").existsSync(publicPath)) {
|
|
22
|
+
// Try looking in dist/public if we are in src
|
|
23
|
+
publicPath = path_1.default.join(__dirname, "../../dist/public");
|
|
24
|
+
}
|
|
25
|
+
if (require("fs").existsSync(publicPath)) {
|
|
26
|
+
app.use(express_1.default.static(publicPath));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.warn(chalk_1.default.yellow("Frontend build not found. Run 'npm run build' in packages/core/frontend to build the UI."));
|
|
30
|
+
}
|
|
17
31
|
await auth.initialize();
|
|
18
|
-
console.log(
|
|
19
|
-
console.log(
|
|
32
|
+
// console.log(chalk.bold.green(`\nš» API server running on:`));
|
|
33
|
+
// console.log(chalk.green(`http://localhost:${port}`));
|
|
34
|
+
// Helper to parse OTLP attributes
|
|
35
|
+
const parseOtlpAttributes = (attributes) => {
|
|
36
|
+
if (!Array.isArray(attributes))
|
|
37
|
+
return {};
|
|
38
|
+
const result = {};
|
|
39
|
+
for (const attr of attributes) {
|
|
40
|
+
if (attr.key && attr.value) {
|
|
41
|
+
// Extract value based on type (stringValue, intValue, boolValue, etc.)
|
|
42
|
+
const val = attr.value;
|
|
43
|
+
if (val.stringValue !== undefined)
|
|
44
|
+
result[attr.key] = val.stringValue;
|
|
45
|
+
else if (val.intValue !== undefined)
|
|
46
|
+
result[attr.key] = parseInt(val.intValue);
|
|
47
|
+
else if (val.doubleValue !== undefined)
|
|
48
|
+
result[attr.key] = val.doubleValue;
|
|
49
|
+
else if (val.boolValue !== undefined)
|
|
50
|
+
result[attr.key] = val.boolValue;
|
|
51
|
+
else if (val.arrayValue !== undefined)
|
|
52
|
+
result[attr.key] = val.arrayValue; // Simplified
|
|
53
|
+
else if (val.kvlistValue !== undefined)
|
|
54
|
+
result[attr.key] = val.kvlistValue; // Simplified
|
|
55
|
+
else
|
|
56
|
+
result[attr.key] = val;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
};
|
|
61
|
+
// OTLP Logs Ingestion Endpoint
|
|
62
|
+
app.post("/v1/logs", async (req, res) => {
|
|
63
|
+
try {
|
|
64
|
+
const authHeader = req.headers.authorization;
|
|
65
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
66
|
+
return res.status(401).json({ code: 16, message: "Unauthenticated" });
|
|
67
|
+
}
|
|
68
|
+
const token = authHeader.split(" ")[1];
|
|
69
|
+
const payload = await auth.verifyToken(token);
|
|
70
|
+
if (!payload || !payload.streamId) {
|
|
71
|
+
return res.status(401).json({ code: 16, message: "Invalid token" });
|
|
72
|
+
}
|
|
73
|
+
const streamId = payload.streamId;
|
|
74
|
+
const { resourceLogs } = req.body;
|
|
75
|
+
if (!resourceLogs || !Array.isArray(resourceLogs)) {
|
|
76
|
+
return res.status(400).json({ code: 3, message: "Invalid payload" });
|
|
77
|
+
}
|
|
78
|
+
let count = 0;
|
|
79
|
+
for (const resourceLog of resourceLogs) {
|
|
80
|
+
const resourceAttrs = parseOtlpAttributes(resourceLog.resource?.attributes);
|
|
81
|
+
if (resourceLog.scopeLogs) {
|
|
82
|
+
for (const scopeLog of resourceLog.scopeLogs) {
|
|
83
|
+
const scopeName = scopeLog.scope?.name;
|
|
84
|
+
if (scopeLog.logRecords) {
|
|
85
|
+
for (const log of scopeLog.logRecords) {
|
|
86
|
+
let content = "";
|
|
87
|
+
if (log.body?.stringValue)
|
|
88
|
+
content = log.body.stringValue;
|
|
89
|
+
else if (log.body?.kvlistValue)
|
|
90
|
+
content = JSON.stringify(log.body.kvlistValue);
|
|
91
|
+
else if (typeof log.body === 'string')
|
|
92
|
+
content = log.body; // Fallback
|
|
93
|
+
const logAttrs = parseOtlpAttributes(log.attributes);
|
|
94
|
+
// Merge attributes: Resource > Scope (if any) > Log
|
|
95
|
+
const metadata = {
|
|
96
|
+
...resourceAttrs,
|
|
97
|
+
...logAttrs,
|
|
98
|
+
severity: log.severityText || log.severityNumber,
|
|
99
|
+
scope: scopeName,
|
|
100
|
+
timestamp: log.timeUnixNano
|
|
101
|
+
};
|
|
102
|
+
if (content) {
|
|
103
|
+
await db.addLog(streamId, content, metadata);
|
|
104
|
+
count++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
res.json({ partialSuccess: {}, logsIngested: count });
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
console.error("OTLP Ingest error:", e);
|
|
115
|
+
res.status(500).json({ code: 13, message: String(e) });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
20
118
|
app.post("/api/ingest", async (req, res) => {
|
|
21
119
|
try {
|
|
22
120
|
const authHeader = req.headers.authorization;
|
|
@@ -57,6 +155,18 @@ async function startApiServer(db) {
|
|
|
57
155
|
res.status(500).json({ error: String(e) });
|
|
58
156
|
}
|
|
59
157
|
});
|
|
158
|
+
app.get("/api/connection", async (req, res) => {
|
|
159
|
+
try {
|
|
160
|
+
const token = await auth.getOrCreateMcpToken();
|
|
161
|
+
res.json({
|
|
162
|
+
token,
|
|
163
|
+
mcpUrl: `http://localhost:${port}/sse` // Assuming default MCP behavior or just provide base URL
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
res.status(500).json({ error: String(e) });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
60
170
|
app.get("/api/projects", (req, res) => {
|
|
61
171
|
const projects = db.listProjects();
|
|
62
172
|
res.json(projects);
|
|
@@ -88,6 +198,16 @@ async function startApiServer(db) {
|
|
|
88
198
|
db.deleteStream(id);
|
|
89
199
|
res.json({ success: true });
|
|
90
200
|
});
|
|
201
|
+
app.get("/api/streams/:id/token", async (req, res) => {
|
|
202
|
+
const { id } = req.params;
|
|
203
|
+
try {
|
|
204
|
+
const token = await auth.createStreamToken(id);
|
|
205
|
+
res.json({ token });
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
res.status(500).json({ error: String(e) });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
91
211
|
app.post("/api/streams", (req, res) => {
|
|
92
212
|
// Deprecated or just listing? The previous code had this returning listStreams for POST?
|
|
93
213
|
// I'll remove it or keep it if CLI uses it?
|
|
@@ -132,6 +252,18 @@ async function startApiServer(db) {
|
|
|
132
252
|
}
|
|
133
253
|
res.json(logs);
|
|
134
254
|
});
|
|
255
|
+
// SPA fallback
|
|
256
|
+
app.get("*", (req, res) => {
|
|
257
|
+
if (req.path.startsWith("/api")) {
|
|
258
|
+
return res.status(404).json({ error: "Not Found" });
|
|
259
|
+
}
|
|
260
|
+
if (require("fs").existsSync(path_1.default.join(publicPath, "index.html"))) {
|
|
261
|
+
res.sendFile(path_1.default.join(publicPath, "index.html"));
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
res.status(404).send("Dashboard not found. Please build the frontend.");
|
|
265
|
+
}
|
|
266
|
+
});
|
|
135
267
|
app.listen(port, () => {
|
|
136
268
|
// listening
|
|
137
269
|
});
|
package/dist/cli_main.js
CHANGED
|
@@ -10,21 +10,40 @@ const db_1 = require("./services/db");
|
|
|
10
10
|
const server_1 = require("./api/server");
|
|
11
11
|
const migrate_1 = require("./db/migrate");
|
|
12
12
|
// import { ensureInfrastructure } from "./utils/startup"; // Might need adjustment
|
|
13
|
-
const main_1 = require("./ui/main");
|
|
14
13
|
const auth_1 = require("./services/auth");
|
|
14
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
15
|
+
const open_1 = __importDefault(require("open"));
|
|
15
16
|
const db = new db_1.DbService();
|
|
16
17
|
const auth = new auth_1.AuthService();
|
|
17
18
|
async function main() {
|
|
18
19
|
const argv = await (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
|
|
19
20
|
.command(["start", "$0"], "Start API Server", {}, async () => {
|
|
20
|
-
console.log("Ensuring database is initialized...");
|
|
21
|
+
// console.log("Ensuring database is initialized...");
|
|
21
22
|
await (0, migrate_1.migrate)(false); // Run migrations silently
|
|
22
23
|
const token = await auth.getOrCreateMcpToken();
|
|
23
24
|
// Start API Server (this sets up express listen)
|
|
24
25
|
await (0, server_1.startApiServer)(db);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
console.clear();
|
|
27
|
+
console.log(chalk_1.default.bold.green(`
|
|
28
|
+
__ __ __
|
|
29
|
+
/ / ___ ___ ____ / / ___ ___ ____ ___/ /
|
|
30
|
+
/ /__/ _ \\/ _ \`/ _ \\/ _ \\/ -_) _ \`/ _ \\/ _ /
|
|
31
|
+
/____/\\___/\\_, /_//_/_//_/\\__/\\_,_/\\___/\\_,_/
|
|
32
|
+
/___/
|
|
33
|
+
`));
|
|
34
|
+
console.log(chalk_1.default.gray("--------------------------------------------------"));
|
|
35
|
+
console.log(chalk_1.default.bold(" š¢ Loghead is running"));
|
|
36
|
+
console.log(chalk_1.default.gray("--------------------------------------------------"));
|
|
37
|
+
console.log("");
|
|
38
|
+
console.log(chalk_1.default.bold(" š„ļø Dashboard : ") + chalk_1.default.cyan("http://localhost:4567"));
|
|
39
|
+
console.log(chalk_1.default.bold(" š MCP Server: ") + chalk_1.default.cyan("http://localhost:4567/sse"));
|
|
40
|
+
console.log("");
|
|
41
|
+
console.log(chalk_1.default.bold(" š MCP Token : "));
|
|
42
|
+
console.log(chalk_1.default.yellow(token));
|
|
43
|
+
console.log("");
|
|
44
|
+
console.log(chalk_1.default.gray("--------------------------------------------------"));
|
|
45
|
+
console.log(chalk_1.default.gray(" Press Ctrl+C to stop"));
|
|
46
|
+
(0, open_1.default)("http://localhost:4567");
|
|
28
47
|
})
|
|
29
48
|
.command("projects <cmd> [name]", "Manage projects", (yargs) => {
|
|
30
49
|
yargs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.left-0{left:0}.right-2{right:.5rem}.top-0{top:0}.top-2{top:.5rem}.top-full{top:100%}.z-50{z-index:50}.z-\[100\]{z-index:100}.col-span-full{grid-column:1 / -1}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.block{display:block}.flex{display:flex}.grid{display:grid}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-\[50vh\]{height:50vh}.min-h-\[60px\]{min-height:60px}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-3{width:.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-64{width:16rem}.w-8{width:2rem}.w-full{width:100%}.max-w-7xl{max-width:80rem}.max-w-lg{max-width:32rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.select-all{-webkit-user-select:all;-moz-user-select:all;user-select:all}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-4{gap:1rem}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-current{border-color:currentColor}.border-gray-800{--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-black\/50{background-color:#00000080}.bg-black\/80{background-color:#000c}.bg-blue-500\/20{background-color:#3b82f633}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-gray-900\/30{background-color:#1118274d}.bg-gray-900\/50{background-color:#11182780}.bg-gray-950{--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.bg-gray-950\/50{background-color:#03071280}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-purple-500\/20{background-color:#a855f733}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-gradient-to-tr{background-image:linear-gradient(to top right,var(--tw-gradient-stops))}.from-green-500{--tw-gradient-from: #22c55e var(--tw-gradient-from-position);--tw-gradient-to: rgb(34 197 94 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-emerald-600{--tw-gradient-to: #059669 var(--tw-gradient-to-position)}.p-1\.5{padding:.375rem}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pb-4{padding-bottom:1rem}.pr-12{padding-right:3rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.opacity-20{opacity:.2}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-200{transition-duration:.2s}.hover\:border-gray-700:hover{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-800:hover{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-900:hover{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.hover\:text-gray-200:hover{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.group:hover .group-hover\:text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}@media (min-width: 640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}
|