Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44fb456405 | |||
| 8f9b9b4945 | |||
| cae8f3cbf6 | |||
| e4e48b13d8 | |||
| 8a78b2bb2e |
@@ -1,44 +0,0 @@
|
|||||||
name: Claude Code Review
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, ready_for_review, reopened]
|
|
||||||
# Optional: Only run on specific file changes
|
|
||||||
# paths:
|
|
||||||
# - "src/**/*.ts"
|
|
||||||
# - "src/**/*.tsx"
|
|
||||||
# - "src/**/*.js"
|
|
||||||
# - "src/**/*.jsx"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
claude-review:
|
|
||||||
# Optional: Filter by PR author
|
|
||||||
# if: |
|
|
||||||
# github.event.pull_request.user.login == 'external-contributor' ||
|
|
||||||
# github.event.pull_request.user.login == 'new-developer' ||
|
|
||||||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
issues: read
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Run Claude Code Review
|
|
||||||
id: claude-review
|
|
||||||
uses: anthropics/claude-code-action@v1
|
|
||||||
with:
|
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
||||||
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
|
|
||||||
plugins: 'code-review@claude-code-plugins'
|
|
||||||
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
|
|
||||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
|
||||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
|
||||||
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
name: Claude Code
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
pull_request_review_comment:
|
|
||||||
types: [created]
|
|
||||||
issues:
|
|
||||||
types: [opened, assigned]
|
|
||||||
pull_request_review:
|
|
||||||
types: [submitted]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
claude:
|
|
||||||
if: |
|
|
||||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
||||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
||||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
|
||||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
issues: read
|
|
||||||
id-token: write
|
|
||||||
actions: read # Required for Claude to read CI results on PRs
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Run Claude Code
|
|
||||||
id: claude
|
|
||||||
uses: anthropics/claude-code-action@v1
|
|
||||||
with:
|
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
||||||
|
|
||||||
# This is an optional setting that allows Claude to read CI results on PRs
|
|
||||||
additional_permissions: |
|
|
||||||
actions: read
|
|
||||||
|
|
||||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
|
||||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
|
||||||
|
|
||||||
# Optional: Add claude_args to customize behavior and configuration
|
|
||||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
|
||||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
|
||||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<img src="logoExcaliDash.png" alt="ExcaliDash Logo" width="80" height="88">
|
<img src="logoExcaliDash.png" alt="ExcaliDash Logo" width="80" height="88">
|
||||||
|
|
||||||
# ExcaliDash
|
# ExcaliDash v0.1.8
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|||||||
-14
@@ -27,17 +27,3 @@ CSRF Protection (8a78b2b)
|
|||||||
- Updated docker-compose configurations with new environment variables
|
- Updated docker-compose configurations with new environment variables
|
||||||
- E2E test suite improvements and reliability fixes
|
- E2E test suite improvements and reliability fixes
|
||||||
- Added Kubernetes deployment note in README
|
- Added Kubernetes deployment note in README
|
||||||
|
|
||||||
### Kubernetes
|
|
||||||
|
|
||||||
A `CSRF_SECRET` environment variable is now required for CSRF protection. Generate a secure 32+ character random string:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openssl rand -base64 32
|
|
||||||
|
|
||||||
Add it to your deployment:
|
|
||||||
- Docker Compose: Add CSRF_SECRET=<your-secret> to the backend service environment
|
|
||||||
- Kubernetes: Add to your ConfigMap/Secret and reference in the backend deployment
|
|
||||||
|
|
||||||
If not set, the backend will refuse to start.
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "0.3.0",
|
"version": "0.2.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -532,6 +532,7 @@ export const validateImportedDrawing = (data: any): boolean => {
|
|||||||
// CSRF Protection
|
// CSRF Protection
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
const CSRF_TOKEN_LENGTH = 32;
|
||||||
const CSRF_TOKEN_HEADER = "x-csrf-token";
|
const CSRF_TOKEN_HEADER = "x-csrf-token";
|
||||||
const CSRF_TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
|
const CSRF_TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||||
const CSRF_TOKEN_FUTURE_SKEW_MS = 5 * 60 * 1000; // 5 minutes clock skew tolerance
|
const CSRF_TOKEN_FUTURE_SKEW_MS = 5 * 60 * 1000; // 5 minutes clock skew tolerance
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.0",
|
"version": "0.2.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const UploadProvider: React.FC<{ children: ReactNode }> = ({ children })
|
|||||||
|
|
||||||
const uploadFiles = useCallback(async (files: File[], targetCollectionId: string | null) => {
|
const uploadFiles = useCallback(async (files: File[], targetCollectionId: string | null) => {
|
||||||
const newTasks: UploadTask[] = files.map(f => ({
|
const newTasks: UploadTask[] = files.map(f => ({
|
||||||
id: crypto.randomUUID(),
|
id: Math.random().toString(36).substring(2, 11),
|
||||||
fileName: f.name,
|
fileName: f.name,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
progress: 0
|
progress: 0
|
||||||
@@ -56,12 +56,12 @@ export const UploadProvider: React.FC<{ children: ReactNode }> = ({ children })
|
|||||||
|
|
||||||
setTasks(prev => [...newTasks, ...prev]);
|
setTasks(prev => [...newTasks, ...prev]);
|
||||||
|
|
||||||
// Map file index to task ID for progress callbacks (handles duplicate filenames)
|
// Map file names to task IDs for progress callbacks
|
||||||
const indexToTaskId = new Map<number, string>();
|
const fileTaskMap = new Map<string, string>();
|
||||||
newTasks.forEach((t, index) => indexToTaskId.set(index, t.id));
|
newTasks.forEach(t => fileTaskMap.set(t.fileName, t.id));
|
||||||
|
|
||||||
const handleProgress = (fileIndex: number, status: UploadStatus, progress: number, error?: string) => {
|
const handleProgress = (fileName: string, status: UploadStatus, progress: number, error?: string) => {
|
||||||
const taskId = indexToTaskId.get(fileIndex);
|
const taskId = fileTaskMap.get(fileName);
|
||||||
if (taskId) {
|
if (taskId) {
|
||||||
updateTask(taskId, { status, progress, error });
|
updateTask(taskId, { status, progress, error });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const importDrawings = async (
|
|||||||
targetCollectionId: string | null,
|
targetCollectionId: string | null,
|
||||||
onSuccess?: () => void | Promise<void>,
|
onSuccess?: () => void | Promise<void>,
|
||||||
onProgress?: (
|
onProgress?: (
|
||||||
fileIndex: number,
|
fileName: string,
|
||||||
status: UploadStatus,
|
status: UploadStatus,
|
||||||
progress: number,
|
progress: number,
|
||||||
error?: string
|
error?: string
|
||||||
@@ -25,20 +25,12 @@ export const importDrawings = async (
|
|||||||
let failCount = 0;
|
let failCount = 0;
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
// Build a map from drawingFile index to original file index for progress reporting
|
|
||||||
const originalIndexMap = new Map<number, number>();
|
|
||||||
drawingFiles.forEach((df, i) => {
|
|
||||||
const originalIndex = files.indexOf(df);
|
|
||||||
originalIndexMap.set(i, originalIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
// We process files in parallel (Promise.all) but we could limit concurrency if needed.
|
// We process files in parallel (Promise.all) but we could limit concurrency if needed.
|
||||||
// For now, full parallel is fine as browser limits connection count anyway.
|
// For now, full parallel is fine as browser limits connection count anyway.
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
drawingFiles.map(async (file, drawingIndex) => {
|
drawingFiles.map(async (file) => {
|
||||||
const fileIndex = originalIndexMap.get(drawingIndex) ?? drawingIndex;
|
|
||||||
try {
|
try {
|
||||||
if (onProgress) onProgress(fileIndex, 'processing', 0); // Parsing phase
|
if (onProgress) onProgress(file.name, 'processing', 0); // Parsing phase
|
||||||
|
|
||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
const data = JSON.parse(text);
|
const data = JSON.parse(text);
|
||||||
@@ -69,7 +61,7 @@ export const importDrawings = async (
|
|||||||
preview: svg.outerHTML,
|
preview: svg.outerHTML,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (onProgress) onProgress(fileIndex, 'uploading', 0);
|
if (onProgress) onProgress(file.name, 'uploading', 0);
|
||||||
|
|
||||||
await api.post("/drawings", payload, {
|
await api.post("/drawings", payload, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -81,12 +73,12 @@ export const importDrawings = async (
|
|||||||
const percentCompleted = Math.round(
|
const percentCompleted = Math.round(
|
||||||
(progressEvent.loaded * 100) / progressEvent.total
|
(progressEvent.loaded * 100) / progressEvent.total
|
||||||
);
|
);
|
||||||
onProgress(fileIndex, 'uploading', percentCompleted);
|
onProgress(file.name, 'uploading', percentCompleted);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (onProgress) onProgress(fileIndex, 'success', 100);
|
if (onProgress) onProgress(file.name, 'success', 100);
|
||||||
successCount++;
|
successCount++;
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -98,7 +90,7 @@ export const importDrawings = async (
|
|||||||
err?.message ||
|
err?.message ||
|
||||||
"Upload failed";
|
"Upload failed";
|
||||||
errors.push(`${file.name}: ${errorMessage}`);
|
errors.push(`${file.name}: ${errorMessage}`);
|
||||||
if (onProgress) onProgress(fileIndex, 'error', 0, errorMessage);
|
if (onProgress) onProgress(file.name, 'error', 0, errorMessage);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,16 +15,19 @@ try {
|
|||||||
console.warn("Unable to read VERSION file:", error);
|
console.warn("Unable to read VERSION file:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const appVersion = process.env.VITE_APP_VERSION?.trim() || versionFromFile;
|
if (
|
||||||
const buildLabel = process.env.VITE_APP_BUILD_LABEL?.trim() || "local development build";
|
!process.env.VITE_APP_VERSION ||
|
||||||
|
process.env.VITE_APP_VERSION.trim().length === 0
|
||||||
|
) {
|
||||||
|
process.env.VITE_APP_VERSION = versionFromFile;
|
||||||
|
if (!process.env.VITE_APP_BUILD_LABEL) {
|
||||||
|
process.env.VITE_APP_BUILD_LABEL = "local development build";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
define: {
|
|
||||||
'import.meta.env.VITE_APP_VERSION': JSON.stringify(appVersion),
|
|
||||||
'import.meta.env.VITE_APP_BUILD_LABEL': JSON.stringify(buildLabel),
|
|
||||||
},
|
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
|
|||||||
Reference in New Issue
Block a user