Skip to content

Commit edfd662

Browse files
feat(dashboard): complete data platform - API + UI + collection
Dashboard (Next.js 15 + Vercel Postgres + Drizzle): - 7 API routes: skills CRUD, stats overview, collection endpoint, events management, dynamic recommendations - 4 dashboard pages: overview with charts, skill detail with 30-day curves, competitor comparison, events management - Components: stat cards, install charts (Recharts), milestone progress bar, skill table, event form - shadcn/ui + Tailwind for clean internal tool UI Data collection: - GitHub Actions workflow (hourly cron) to scrape skills.sh via npx skills find, parse output, POST to API - Parser for skills.sh CLI output (ANSI-stripped) Database: - Drizzle schema: skills, daily_stats, events - Seed script with 6 own skills + 2 competitors + 30-day synthetic data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 507e380 commit edfd662

56 files changed

Lines changed: 15015 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
name: Collect Skill Stats
2+
3+
on:
4+
schedule:
5+
- cron: '17 * * * *' # Every hour at :17
6+
workflow_dispatch: {}
7+
8+
jobs:
9+
collect:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- uses: actions/setup-node@v4
15+
with:
16+
node-version: '20'
17+
18+
- name: Install skills CLI
19+
run: npm install -g @anthropic-ai/skills 2>/dev/null || true
20+
21+
- name: Collect our skills data
22+
id: our_skills
23+
run: |
24+
OUTPUT=$(npx skills find "second-me-01" 2>/dev/null || echo "")
25+
echo "output<<EOF" >> $GITHUB_OUTPUT
26+
echo "$OUTPUT" >> $GITHUB_OUTPUT
27+
echo "EOF" >> $GITHUB_OUTPUT
28+
29+
- name: Collect competitor data
30+
id: competitors
31+
run: |
32+
XHS=$(npx skills find "xhs" 2>/dev/null || echo "")
33+
BRAINSTORM=$(npx skills find "brainstorm" 2>/dev/null || echo "")
34+
echo "xhs<<EOF" >> $GITHUB_OUTPUT
35+
echo "$XHS" >> $GITHUB_OUTPUT
36+
echo "EOF" >> $GITHUB_OUTPUT
37+
echo "brainstorm<<EOF" >> $GITHUB_OUTPUT
38+
echo "$BRAINSTORM" >> $GITHUB_OUTPUT
39+
echo "EOF" >> $GITHUB_OUTPUT
40+
41+
- name: Parse and submit data
42+
env:
43+
API_URL: ${{ secrets.API_URL }}
44+
API_SECRET: ${{ secrets.API_SECRET }}
45+
run: |
46+
node << 'SCRIPT'
47+
const output = `${{ steps.our_skills.outputs.output }}`;
48+
const xhsOutput = `${{ steps.competitors.outputs.xhs }}`;
49+
const brainstormOutput = `${{ steps.competitors.outputs.brainstorm }}`;
50+
51+
function parseInstalls(text) {
52+
const results = [];
53+
const clean = text.replace(/\x1b\[[0-9;]*m/g, '');
54+
const lines = clean.split('\n');
55+
for (const line of lines) {
56+
const match = line.trim().match(/^(.+?)@(.+?)\s+([\d,.]+K?)\s+installs?$/);
57+
if (match) {
58+
let installs = match[3].replace(/,/g, '');
59+
if (installs.endsWith('K')) installs = String(parseFloat(installs) * 1000);
60+
results.push({ repo: match[1], name: match[2], installs: Math.round(parseFloat(installs)) });
61+
}
62+
}
63+
return results;
64+
}
65+
66+
const ours = parseInstalls(output);
67+
const xhs = parseInstalls(xhsOutput);
68+
const brainstorm = parseInstalls(brainstormOutput);
69+
70+
// Map our skills
71+
const data = [];
72+
const ourMap = {
73+
'skill-hub': 'skill-hub',
74+
'smart-brainstorm': 'smart-brainstorm',
75+
'ui-design-system': 'ui-design-system',
76+
'xhs-writer': 'xhs-writer',
77+
'video-script': 'video-script',
78+
'feishu-kit': 'feishu-kit',
79+
};
80+
81+
for (const s of ours) {
82+
if (ourMap[s.name]) {
83+
data.push({ skill_id: ourMap[s.name], installs: s.installs, platform: 'skills.sh' });
84+
}
85+
}
86+
87+
// Find specific competitors
88+
const daqiXhs = xhs.find(s => s.repo.includes('daqi') && s.name.includes('xhs'));
89+
if (daqiXhs) data.push({ skill_id: 'comp-daqi-xhs', installs: daqiXhs.installs, platform: 'skills.sh' });
90+
91+
const compBrainstorm = brainstorm.find(s => s.name === 'brainstorming' || s.name === 'brainstorm');
92+
if (compBrainstorm) data.push({ skill_id: 'comp-brainstorming', installs: compBrainstorm.installs, platform: 'skills.sh' });
93+
94+
if (data.length === 0) {
95+
console.log('No data collected, skipping API call');
96+
process.exit(0);
97+
}
98+
99+
console.log('Submitting data:', JSON.stringify(data, null, 2));
100+
101+
fetch(`${process.env.API_URL}/api/stats/collect`, {
102+
method: 'POST',
103+
headers: {
104+
'Content-Type': 'application/json',
105+
'x-api-secret': process.env.API_SECRET,
106+
},
107+
body: JSON.stringify({ data }),
108+
})
109+
.then(r => r.json())
110+
.then(r => console.log('API response:', r))
111+
.catch(e => console.error('API error:', e));
112+
SCRIPT

dashboard/.gitignore

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
# testing
14+
/coverage
15+
16+
# next.js
17+
/.next/
18+
/out/
19+
20+
# production
21+
/build
22+
23+
# misc
24+
.DS_Store
25+
*.pem
26+
27+
# debug
28+
npm-debug.log*
29+
yarn-debug.log*
30+
yarn-error.log*
31+
.pnpm-debug.log*
32+
33+
# env files (can opt-in for committing if needed)
34+
.env*
35+
36+
# vercel
37+
.vercel
38+
39+
# typescript
40+
*.tsbuildinfo
41+
next-env.d.ts

dashboard/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2+
3+
## Getting Started
4+
5+
First, run the development server:
6+
7+
```bash
8+
npm run dev
9+
# or
10+
yarn dev
11+
# or
12+
pnpm dev
13+
# or
14+
bun dev
15+
```
16+
17+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18+
19+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20+
21+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22+
23+
## Learn More
24+
25+
To learn more about Next.js, take a look at the following resources:
26+
27+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29+
30+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31+
32+
## Deploy on Vercel
33+
34+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35+
36+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

dashboard/components.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema.json",
3+
"style": "base-nova",
4+
"rsc": true,
5+
"tsx": true,
6+
"tailwind": {
7+
"config": "",
8+
"css": "src/app/globals.css",
9+
"baseColor": "neutral",
10+
"cssVariables": true,
11+
"prefix": ""
12+
},
13+
"iconLibrary": "lucide",
14+
"rtl": false,
15+
"aliases": {
16+
"components": "@/components",
17+
"utils": "@/lib/utils",
18+
"ui": "@/components/ui",
19+
"lib": "@/lib",
20+
"hooks": "@/hooks"
21+
},
22+
"menuColor": "default",
23+
"menuAccent": "subtle",
24+
"registries": {}
25+
}

dashboard/drizzle.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { defineConfig } from "drizzle-kit";
2+
3+
export default defineConfig({
4+
schema: "./src/db/schema.ts",
5+
out: "./drizzle",
6+
dialect: "postgresql",
7+
dbCredentials: {
8+
url: process.env.POSTGRES_URL!,
9+
},
10+
});

dashboard/eslint.config.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { defineConfig, globalIgnores } from "eslint/config";
2+
import nextVitals from "eslint-config-next/core-web-vitals";
3+
import nextTs from "eslint-config-next/typescript";
4+
5+
const eslintConfig = defineConfig([
6+
...nextVitals,
7+
...nextTs,
8+
// Override default ignores of eslint-config-next.
9+
globalIgnores([
10+
// Default ignores of eslint-config-next:
11+
".next/**",
12+
"out/**",
13+
"build/**",
14+
"next-env.d.ts",
15+
]),
16+
]);
17+
18+
export default eslintConfig;

dashboard/next.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { NextConfig } from "next";
2+
3+
const nextConfig: NextConfig = {
4+
/* config options here */
5+
};
6+
7+
export default nextConfig;

0 commit comments

Comments
 (0)