Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CodeQL

on:
pull_request:
branches: ["develop"]
push:
branches: ["develop", "master"]
schedule:
- cron: "15 2 * * 6"

permissions:
contents: read

concurrency:
group: codeql-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: false

jobs:
analyze:
name: Analyze (javascript-typescript)
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: javascript-typescript

- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: "/language:javascript-typescript"
11 changes: 9 additions & 2 deletions .github/workflows/osvscanner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
name: OSV-Scanner

'on':
pull_request:
branches: ["develop"]
push:
branches: ["develop", "master"]
schedule:
- cron: '20 19 * * 5'

permissions:
# Require writing security events to upload SARIF file to security tab
security-events: write
# Read commit contents
contents: read
# Read PR metadata for comparison of introduced vulnerabilities
Expand All @@ -28,6 +30,11 @@ permissions:
jobs:
scan:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
actions: read
security-events: write
concurrency:
# Avoid canceling this workflow due to other workflows' concurrency queues (e.g., CodeQL).
# Key off PR number for PR/merge_group, otherwise ref.
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/scorecard-analysis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
name: Scorecard analysis

on:
push:
pull_request:
branches: ["develop"]
push:
branches: ["develop", "master"]
schedule:
- cron: "30 1 * * 6"

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/trivy.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: Trivy Security Scan

on:
pull_request:
branches:
- develop
push:
branches:
- develop
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM nginx:1.25-alpine
FROM nginx:1.25-alpine@sha256:516475cc129da42866742567714ddc681e5eed7b9ee0b9e9c015e464b4221a00
COPY infra/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY index.html 404.html app.js styles.css wbs.json /usr/share/nginx/html/
COPY docs/user-guide.md /usr/share/nginx/html/docs/
Expand All @@ -10,4 +10,5 @@ RUN touch /var/run/nginx.pid && \

USER nginx
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD wget -qO- http://127.0.0.1:8080/ >/dev/null || exit 1
CMD ["nginx", "-g", "daemon off;"]
50 changes: 44 additions & 6 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,27 @@ const EDITOR_FIELD_TEST_IDS = Object.freeze(Object.assign(Object.create(null), {
}));

const LEGACY_PLANNED_END_FIELD = 'plannedEnd' + 'Ddate';
const TASK_STORAGE_FIELDS = Object.freeze([
'id',
'parentId',
'depth',
'expanded',
'pendingDelete',
'isSynthetic',
'phase',
'activity',
'task',
'categoryLarge',
'categoryMedium',
'documentName',
'owner',
'supportTeam',
'plannedStartDate',
'plannedEndDate',
'actualProgressStatus',
'actualStartDate',
'actualEndDate'
]);

const DEFAULT_EDITOR_STATE = {
mode: null,
Expand Down Expand Up @@ -187,6 +208,7 @@ async function bootstrap() {
const savedState = loadLocalState();
if (savedState) {
hydrateState(savedState);
persistState();
} else {
const seedData = await loadSeedTasks();
state.tasks = normalizeImportedTasks(seedData);
Expand Down Expand Up @@ -479,6 +501,7 @@ function setTableBodyRows(rows) {
function createEmptyStateRow() {
const row = document.createElement('tr');
const cell = document.createElement('td');
cell.className = 'empty-state-cell';
Comment thread
seonghobae marked this conversation as resolved.
cell.colSpan = 21;

const emptyState = document.createElement('div');
Expand Down Expand Up @@ -1420,11 +1443,10 @@ function findTask(taskId) {
}

function persistState() {
// ⚡ Bolt: Remove redundant O(N) object cloning before JSON.stringify to prevent massive memory allocations on every keystroke
const payload = {
projectName: state.projectName,
baseDate: state.baseDate,
tasks: state.tasks
tasks: state.tasks.map(createPersistableTask)
};
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
Expand Down Expand Up @@ -1460,13 +1482,26 @@ function hydrateState(savedState) {

function normalizeStoredTask(task) {
const safeTask = isTaskRecord(task) ? task : {};
const normalizedTask = {
return createPersistableTask({
...safeTask,
plannedEndDate: getPlannedEndDateValue(safeTask),
expanded: safeTask.expanded !== false
};
delete normalizedTask[LEGACY_PLANNED_END_FIELD];
return normalizedTask;
});
}

function createPersistableTask(task) {
const safeTask = isTaskRecord(task) ? task : {};
const persistableTask = Object.create(null);
for (const field of TASK_STORAGE_FIELDS) {
if (safeTask[field] !== undefined) {
persistableTask[field] = safeTask[field];
}
}
persistableTask.plannedEndDate = getPlannedEndDateValue(safeTask);
persistableTask.expanded = safeTask.expanded !== false;
persistableTask.pendingDelete = Boolean(safeTask.pendingDelete);
persistableTask.isSynthetic = Boolean(safeTask.isSynthetic);
return persistableTask;
}

async function loadSeedTasks() {
Expand Down Expand Up @@ -2483,6 +2518,9 @@ function debounce(callback, wait) {
// Export for testing
if (typeof window !== 'undefined') {
window.validateDraft = validateDraft;
window.sanitizeCsvFormulaValue = sanitizeCsvFormulaValue;
window.csvEscape = csvEscape;
window.createTextCellContent = createTextCellContent;
}

bootstrap();
19 changes: 16 additions & 3 deletions infra/k8s/deployment.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
apiVersion: v1
kind: Namespace
metadata:
name: scopeweave
labels:
app: scopeweave
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: scopeweave-deployment
namespace: scopeweave
labels:
app: scopeweave
spec:
Expand All @@ -16,16 +24,20 @@ spec:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: scopeweave
image: scopeweave:1.0.0
imagePullPolicy: IfNotPresent
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
Expand Down Expand Up @@ -72,6 +84,7 @@ apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: scopeweave-pdb
namespace: scopeweave
spec:
minAvailable: 1
selector:
Expand Down
1 change: 1 addition & 0 deletions infra/k8s/service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ apiVersion: v1
kind: Service
metadata:
name: scopeweave-service
namespace: scopeweave
spec:
selector:
app: scopeweave
Expand Down
43 changes: 42 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
"description": "Production-grade pure HTML/CSS/JS WBS planner",
"scripts": {
"check:python-docstrings": "node scripts/ci/static_coverage_evidence.mjs docstrings",
"coverage": "node scripts/ci/static_coverage_evidence.mjs coverage",
"coverage": "node scripts/ci/static_coverage_evidence.mjs coverage && npm run test:fuzz",
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed"
"test:e2e:headed": "playwright test --headed",
"test:fuzz": "playwright install chromium && playwright test tests/e2e/csv_formula_fuzz.spec.js"
},
"devDependencies": {
"@playwright/test": "^1.61.1"
"@playwright/test": "1.61.1",
"fast-check": "4.9.0"
}
}
14 changes: 14 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ button {
}

.app-shell {
--table-empty-page-gutter: 98px; /* 2 * 48px shell padding + 2px table-section border */
display: flex;
min-height: 100vh;
flex-direction: column;
Expand Down Expand Up @@ -801,6 +802,14 @@ select[data-inline-progress]:focus {
font-weight: 500;
}

.empty-state-cell {
padding: 0 !important;
}

.empty-state-cell > .table-empty {
width: min(100%, calc(100vw - var(--table-empty-page-gutter)));
}

@media (max-width: 1400px) {
.meta-grid-primary,
.meta-grid-secondary,
Expand All @@ -817,6 +826,7 @@ select[data-inline-progress]:focus {

@media (max-width: 800px) {
.app-shell {
--table-empty-page-gutter: 34px; /* 2 * 16px shell padding + 2px table-section border */
padding: 16px;
gap: 16px;
}
Expand All @@ -840,6 +850,10 @@ select[data-inline-progress]:focus {
border-radius: var(--radius-sm);
padding: 16px;
}

.empty-state-cell > .table-empty {
padding: 48px 24px;
}
}

@media (prefers-reduced-motion: reduce) {
Expand Down
Loading