Compare commits
1 Commits
codex/webs
...
c75d03a548
| Author | SHA1 | Date | |
|---|---|---|---|
| c75d03a548 |
@@ -1,390 +0,0 @@
|
||||
---
|
||||
name: ci-cd-and-automation
|
||||
description: Automates CI/CD pipeline setup. Use when setting up or modifying build and deployment pipelines. Use when you need to automate quality gates, configure test runners in CI, or establish deployment strategies.
|
||||
---
|
||||
|
||||
# CI/CD and Automation
|
||||
|
||||
## Overview
|
||||
|
||||
Automate quality gates so that no change reaches production without passing tests, lint, type checking, and build. CI/CD is the enforcement mechanism for every other skill — it catches what humans and agents miss, and it does so consistently on every single change.
|
||||
|
||||
**Shift Left:** Catch problems as early in the pipeline as possible. A bug caught in linting costs minutes; the same bug caught in production costs hours. Move checks upstream — static analysis before tests, tests before staging, staging before production.
|
||||
|
||||
**Faster is Safer:** Smaller batches and more frequent releases reduce risk, not increase it. A deployment with 3 changes is easier to debug than one with 30. Frequent releases build confidence in the release process itself.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Setting up a new project's CI pipeline
|
||||
- Adding or modifying automated checks
|
||||
- Configuring deployment pipelines
|
||||
- When a change should trigger automated verification
|
||||
- Debugging CI failures
|
||||
|
||||
## The Quality Gate Pipeline
|
||||
|
||||
Every change goes through these gates before merge:
|
||||
|
||||
```
|
||||
Pull Request Opened
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ LINT CHECK │ eslint, prettier
|
||||
│ ↓ pass │
|
||||
│ TYPE CHECK │ tsc --noEmit
|
||||
│ ↓ pass │
|
||||
│ UNIT TESTS │ jest/vitest
|
||||
│ ↓ pass │
|
||||
│ BUILD │ npm run build
|
||||
│ ↓ pass │
|
||||
│ INTEGRATION │ API/DB tests
|
||||
│ ↓ pass │
|
||||
│ E2E (optional) │ Playwright/Cypress
|
||||
│ ↓ pass │
|
||||
│ SECURITY AUDIT │ npm audit
|
||||
│ ↓ pass │
|
||||
│ BUNDLE SIZE │ bundlesize check
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
Ready for review
|
||||
```
|
||||
|
||||
**No gate can be skipped.** If lint fails, fix lint — don't disable the rule. If a test fails, fix the code — don't skip the test.
|
||||
|
||||
## GitHub Actions Configuration
|
||||
|
||||
### Basic CI Pipeline
|
||||
|
||||
```yaml
|
||||
# .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Type check
|
||||
run: npx tsc --noEmit
|
||||
|
||||
- name: Test
|
||||
run: npm test -- --coverage
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Security audit
|
||||
run: npm audit --audit-level=high
|
||||
```
|
||||
|
||||
### With Database Integration Tests
|
||||
|
||||
```yaml
|
||||
integration:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_DB: testdb
|
||||
POSTGRES_USER: ci_user
|
||||
POSTGRES_PASSWORD: ${{ secrets.CI_DB_PASSWORD }}
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- name: Run migrations
|
||||
run: npx prisma migrate deploy
|
||||
env:
|
||||
DATABASE_URL: postgresql://ci_user:${{ secrets.CI_DB_PASSWORD }}@localhost:5432/testdb
|
||||
- name: Integration tests
|
||||
run: npm run test:integration
|
||||
env:
|
||||
DATABASE_URL: postgresql://ci_user:${{ secrets.CI_DB_PASSWORD }}@localhost:5432/testdb
|
||||
```
|
||||
|
||||
> **Note:** Even for CI-only test databases, use GitHub Secrets for credentials rather than hardcoding values. This builds good habits and prevents accidental reuse of test credentials in other contexts.
|
||||
|
||||
### E2E Tests
|
||||
|
||||
```yaml
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- name: Install Playwright
|
||||
run: npx playwright install --with-deps chromium
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Run E2E tests
|
||||
run: npx playwright test
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
```
|
||||
|
||||
## Feeding CI Failures Back to Agents
|
||||
|
||||
The power of CI with AI agents is the feedback loop. When CI fails:
|
||||
|
||||
```
|
||||
CI fails
|
||||
│
|
||||
▼
|
||||
Copy the failure output
|
||||
│
|
||||
▼
|
||||
Feed it to the agent:
|
||||
"The CI pipeline failed with this error:
|
||||
[paste specific error]
|
||||
Fix the issue and verify locally before pushing again."
|
||||
│
|
||||
▼
|
||||
Agent fixes → pushes → CI runs again
|
||||
```
|
||||
|
||||
**Key patterns:**
|
||||
|
||||
```
|
||||
Lint failure → Agent runs `npm run lint --fix` and commits
|
||||
Type error → Agent reads the error location and fixes the type
|
||||
Test failure → Agent follows debugging-and-error-recovery skill
|
||||
Build error → Agent checks config and dependencies
|
||||
```
|
||||
|
||||
## Deployment Strategies
|
||||
|
||||
### Preview Deployments
|
||||
|
||||
Every PR gets a preview deployment for manual testing:
|
||||
|
||||
```yaml
|
||||
# Deploy preview on PR (Vercel/Netlify/etc.)
|
||||
deploy-preview:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Deploy preview
|
||||
run: npx vercel --token=${{ secrets.VERCEL_TOKEN }}
|
||||
```
|
||||
|
||||
### Feature Flags
|
||||
|
||||
Feature flags decouple deployment from release. Deploy incomplete or risky features behind flags so you can:
|
||||
|
||||
- **Ship code without enabling it.** Merge to main early, enable when ready.
|
||||
- **Roll back without redeploying.** Disable the flag instead of reverting code.
|
||||
- **Canary new features.** Enable for 1% of users, then 10%, then 100%.
|
||||
- **Run A/B tests.** Compare behavior with and without the feature.
|
||||
|
||||
```typescript
|
||||
// Simple feature flag pattern
|
||||
if (featureFlags.isEnabled('new-checkout-flow', { userId })) {
|
||||
return renderNewCheckout();
|
||||
}
|
||||
return renderLegacyCheckout();
|
||||
```
|
||||
|
||||
**Flag lifecycle:** Create → Enable for testing → Canary → Full rollout → Remove the flag and dead code. Flags that live forever become technical debt — set a cleanup date when you create them.
|
||||
|
||||
### Staged Rollouts
|
||||
|
||||
```
|
||||
PR merged to main
|
||||
│
|
||||
▼
|
||||
Staging deployment (auto)
|
||||
│ Manual verification
|
||||
▼
|
||||
Production deployment (manual trigger or auto after staging)
|
||||
│
|
||||
▼
|
||||
Monitor for errors (15-minute window)
|
||||
│
|
||||
├── Errors detected → Rollback
|
||||
└── Clean → Done
|
||||
```
|
||||
|
||||
### Rollback Plan
|
||||
|
||||
Every deployment should be reversible:
|
||||
|
||||
```yaml
|
||||
# Manual rollback workflow
|
||||
name: Rollback
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to rollback to'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
rollback:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Rollback deployment
|
||||
run: |
|
||||
# Deploy the specified previous version
|
||||
npx vercel rollback ${{ inputs.version }}
|
||||
```
|
||||
|
||||
## Environment Management
|
||||
|
||||
```
|
||||
.env.example → Committed (template for developers)
|
||||
.env → NOT committed (local development)
|
||||
.env.test → Committed (test environment, no real secrets)
|
||||
CI secrets → Stored in GitHub Secrets / vault
|
||||
Production secrets → Stored in deployment platform / vault
|
||||
```
|
||||
|
||||
CI should never have production secrets. Use separate secrets for CI testing.
|
||||
|
||||
## Automation Beyond CI
|
||||
|
||||
### Dependabot / Renovate
|
||||
|
||||
```yaml
|
||||
# .github/dependabot.yml
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
```
|
||||
|
||||
### Build Cop Role
|
||||
|
||||
Designate someone responsible for keeping CI green. When the build breaks, the Build Cop's job is to fix or revert — not the person whose change caused the break. This prevents broken builds from accumulating while everyone assumes someone else will fix it.
|
||||
|
||||
### PR Checks
|
||||
|
||||
- **Required reviews:** At least 1 approval before merge
|
||||
- **Required status checks:** CI must pass before merge
|
||||
- **Branch protection:** No force-pushes to main
|
||||
- **Auto-merge:** If all checks pass and approved, merge automatically
|
||||
|
||||
## CI Optimization
|
||||
|
||||
When the pipeline exceeds 10 minutes, apply these strategies in order of impact:
|
||||
|
||||
```
|
||||
Slow CI pipeline?
|
||||
├── Cache dependencies
|
||||
│ └── Use actions/cache or setup-node cache option for node_modules
|
||||
├── Run jobs in parallel
|
||||
│ └── Split lint, typecheck, test, build into separate parallel jobs
|
||||
├── Only run what changed
|
||||
│ └── Use path filters to skip unrelated jobs (e.g., skip e2e for docs-only PRs)
|
||||
├── Use matrix builds
|
||||
│ └── Shard test suites across multiple runners
|
||||
├── Optimize the test suite
|
||||
│ └── Remove slow tests from the critical path, run them on a schedule instead
|
||||
└── Use larger runners
|
||||
└── GitHub-hosted larger runners or self-hosted for CPU-heavy builds
|
||||
```
|
||||
|
||||
**Example: caching and parallelism**
|
||||
```yaml
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: '22', cache: 'npm' }
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: '22', cache: 'npm' }
|
||||
- run: npm ci
|
||||
- run: npx tsc --noEmit
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: '22', cache: 'npm' }
|
||||
- run: npm ci
|
||||
- run: npm test -- --coverage
|
||||
```
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "CI is too slow" | Optimize the pipeline (see CI Optimization below), don't skip it. A 5-minute pipeline prevents hours of debugging. |
|
||||
| "This change is trivial, skip CI" | Trivial changes break builds. CI is fast for trivial changes anyway. |
|
||||
| "The test is flaky, just re-run" | Flaky tests mask real bugs and waste everyone's time. Fix the flakiness. |
|
||||
| "We'll add CI later" | Projects without CI accumulate broken states. Set it up on day one. |
|
||||
| "Manual testing is enough" | Manual testing doesn't scale and isn't repeatable. Automate what you can. |
|
||||
|
||||
## Red Flags
|
||||
|
||||
- No CI pipeline in the project
|
||||
- CI failures ignored or silenced
|
||||
- Tests disabled in CI to make the pipeline pass
|
||||
- Production deploys without staging verification
|
||||
- No rollback mechanism
|
||||
- Secrets stored in code or CI config files (not secrets manager)
|
||||
- Long CI times with no optimization effort
|
||||
|
||||
## Verification
|
||||
|
||||
After setting up or modifying CI:
|
||||
|
||||
- [ ] All quality gates are present (lint, types, tests, build, audit)
|
||||
- [ ] Pipeline runs on every PR and push to main
|
||||
- [ ] Failures block merge (branch protection configured)
|
||||
- [ ] CI results feed back into the development loop
|
||||
- [ ] Secrets are stored in the secrets manager, not in code
|
||||
- [ ] Deployment has a rollback mechanism
|
||||
- [ ] Pipeline runs in under 10 minutes for the test suite
|
||||
@@ -1,288 +0,0 @@
|
||||
---
|
||||
name: documentation-and-adrs
|
||||
description: Records decisions and documentation. Use when making architectural decisions, changing public APIs, shipping features, or when you need to record context that future engineers and agents will need to understand the codebase.
|
||||
---
|
||||
|
||||
# Documentation and ADRs
|
||||
|
||||
## Overview
|
||||
|
||||
Document decisions, not just code. The most valuable documentation captures the *why* — the context, constraints, and trade-offs that led to a decision. Code shows *what* was built; documentation explains *why it was built this way* and *what alternatives were considered*. This context is essential for future humans and agents working in the codebase.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Making a significant architectural decision
|
||||
- Choosing between competing approaches
|
||||
- Adding or changing a public API
|
||||
- Shipping a feature that changes user-facing behavior
|
||||
- Onboarding new team members (or agents) to the project
|
||||
- When you find yourself explaining the same thing repeatedly
|
||||
|
||||
**When NOT to use:** Don't document obvious code. Don't add comments that restate what the code already says. Don't write docs for throwaway prototypes.
|
||||
|
||||
## Architecture Decision Records (ADRs)
|
||||
|
||||
ADRs capture the reasoning behind significant technical decisions. They're the highest-value documentation you can write.
|
||||
|
||||
### When to Write an ADR
|
||||
|
||||
- Choosing a framework, library, or major dependency
|
||||
- Designing a data model or database schema
|
||||
- Selecting an authentication strategy
|
||||
- Deciding on an API architecture (REST vs. GraphQL vs. tRPC)
|
||||
- Choosing between build tools, hosting platforms, or infrastructure
|
||||
- Any decision that would be expensive to reverse
|
||||
|
||||
### Match the existing convention first
|
||||
|
||||
Before creating an ADR, inspect the available repository context for an established convention — existing ADRs, project instructions, and ADR-related configuration or tooling (e.g. an `.adr-dir` file). An established convention overrides the defaults below. Match:
|
||||
|
||||
- **Location and format** — e.g. `docs/adr/*.md`, `Documentation/Decisions/*.rst`, a MADR layout, or an `adr-tools` setup. Match the existing directory, file extension, and markup (Markdown vs reStructuredText).
|
||||
- **Numbering and naming** — continue the existing sequence and filename pattern (`ADR-004-Title.rst`, `0004-title.md`, …); don't restart at 001 or introduce a second scheme.
|
||||
- **Section headings** — reuse the project's heading set rather than imposing this template's.
|
||||
|
||||
If the available evidence conflicts, surface the conflict rather than silently introducing another scheme. Only when no convention can be established do you apply the default below.
|
||||
|
||||
### ADR Template
|
||||
|
||||
Store ADRs in `docs/decisions/` with sequential numbering (unless the project already uses another location — see above):
|
||||
|
||||
```markdown
|
||||
# ADR-001: Use PostgreSQL for primary database
|
||||
|
||||
## Status
|
||||
Accepted | Superseded by ADR-XXX | Deprecated
|
||||
|
||||
## Date
|
||||
2025-01-15
|
||||
|
||||
## Context
|
||||
We need a primary database for the task management application. Key requirements:
|
||||
- Relational data model (users, tasks, teams with relationships)
|
||||
- ACID transactions for task state changes
|
||||
- Support for full-text search on task content
|
||||
- Managed hosting available (for small team, limited ops capacity)
|
||||
|
||||
## Decision
|
||||
Use PostgreSQL with Prisma ORM.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### MongoDB
|
||||
- Pros: Flexible schema, easy to start with
|
||||
- Cons: Our data is inherently relational; would need to manage relationships manually
|
||||
- Rejected: Relational data in a document store leads to complex joins or data duplication
|
||||
|
||||
### SQLite
|
||||
- Pros: Zero configuration, embedded, fast for reads
|
||||
- Cons: Limited concurrent write support, no managed hosting for production
|
||||
- Rejected: Not suitable for multi-user web application in production
|
||||
|
||||
### MySQL
|
||||
- Pros: Mature, widely supported
|
||||
- Cons: PostgreSQL has better JSON support, full-text search, and ecosystem tooling
|
||||
- Rejected: PostgreSQL is the better fit for our feature requirements
|
||||
|
||||
## Consequences
|
||||
- Prisma provides type-safe database access and migration management
|
||||
- We can use PostgreSQL's full-text search instead of adding Elasticsearch
|
||||
- Team needs PostgreSQL knowledge (standard skill, low risk)
|
||||
- Hosting on managed service (Supabase, Neon, or RDS)
|
||||
```
|
||||
|
||||
### ADR Lifecycle
|
||||
|
||||
```
|
||||
PROPOSED → ACCEPTED → (SUPERSEDED or DEPRECATED)
|
||||
```
|
||||
|
||||
- **Don't delete old ADRs.** They capture historical context.
|
||||
- When a decision changes, write a new ADR that references and supersedes the old one.
|
||||
|
||||
## Inline Documentation
|
||||
|
||||
### When to Comment
|
||||
|
||||
Comment the *why*, not the *what*:
|
||||
|
||||
```typescript
|
||||
// BAD: Restates the code
|
||||
// Increment counter by 1
|
||||
counter += 1;
|
||||
|
||||
// GOOD: Explains non-obvious intent
|
||||
// Rate limit uses a sliding window — reset counter at window boundary,
|
||||
// not on a fixed schedule, to prevent burst attacks at window edges
|
||||
if (now - windowStart > WINDOW_SIZE_MS) {
|
||||
counter = 0;
|
||||
windowStart = now;
|
||||
}
|
||||
```
|
||||
|
||||
### When NOT to Comment
|
||||
|
||||
```typescript
|
||||
// Don't comment self-explanatory code
|
||||
function calculateTotal(items: CartItem[]): number {
|
||||
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
||||
}
|
||||
|
||||
// Don't leave TODO comments for things you should just do now
|
||||
// TODO: add error handling ← Just add it
|
||||
|
||||
// Don't leave commented-out code
|
||||
// const oldImplementation = () => { ... } ← Delete it, git has history
|
||||
```
|
||||
|
||||
### Document Known Gotchas
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* IMPORTANT: This function must be called before the first render.
|
||||
* If called after hydration, it causes a flash of unstyled content
|
||||
* because the theme context isn't available during SSR.
|
||||
*
|
||||
* See ADR-003 for the full design rationale.
|
||||
*/
|
||||
export function initializeTheme(theme: Theme): void {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
For public APIs (REST, GraphQL, library interfaces):
|
||||
|
||||
### Inline with Types (Preferred for TypeScript)
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Creates a new task.
|
||||
*
|
||||
* @param input - Task creation data (title required, description optional)
|
||||
* @returns The created task with server-generated ID and timestamps
|
||||
* @throws {ValidationError} If title is empty or exceeds 200 characters
|
||||
* @throws {AuthenticationError} If the user is not authenticated
|
||||
*
|
||||
* @example
|
||||
* const task = await createTask({ title: 'Buy groceries' });
|
||||
* console.log(task.id); // "task_abc123"
|
||||
*/
|
||||
export async function createTask(input: CreateTaskInput): Promise<Task> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAPI / Swagger for REST APIs
|
||||
|
||||
```yaml
|
||||
paths:
|
||||
/api/tasks:
|
||||
post:
|
||||
summary: Create a task
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateTaskInput'
|
||||
responses:
|
||||
'201':
|
||||
description: Task created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Task'
|
||||
'422':
|
||||
description: Validation error
|
||||
```
|
||||
|
||||
## README Structure
|
||||
|
||||
Every project should have a README that covers:
|
||||
|
||||
```markdown
|
||||
# Project Name
|
||||
|
||||
One-paragraph description of what this project does.
|
||||
|
||||
## Quick Start
|
||||
1. Clone the repo
|
||||
2. Install dependencies: `npm install`
|
||||
3. Set up environment: `cp .env.example .env`
|
||||
4. Run the dev server: `npm run dev`
|
||||
|
||||
## Commands
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `npm run dev` | Start development server |
|
||||
| `npm test` | Run tests |
|
||||
| `npm run build` | Production build |
|
||||
| `npm run lint` | Run linter |
|
||||
|
||||
## Architecture
|
||||
Brief overview of the project structure and key design decisions.
|
||||
Link to ADRs for details.
|
||||
|
||||
## Contributing
|
||||
How to contribute, coding standards, PR process.
|
||||
```
|
||||
|
||||
## Changelog Maintenance
|
||||
|
||||
For shipped features:
|
||||
|
||||
```markdown
|
||||
# Changelog
|
||||
|
||||
## [1.2.0] - 2025-01-20
|
||||
### Added
|
||||
- Task sharing: users can share tasks with team members (#123)
|
||||
- Email notifications for task assignments (#124)
|
||||
|
||||
### Fixed
|
||||
- Duplicate tasks appearing when rapidly clicking create button (#125)
|
||||
|
||||
### Changed
|
||||
- Task list now loads 50 items per page (was 20) for better UX (#126)
|
||||
```
|
||||
|
||||
## Documentation for Agents
|
||||
|
||||
Special consideration for AI agent context:
|
||||
|
||||
- **CLAUDE.md / rules files** — Document project conventions so agents follow them
|
||||
- **Spec files** — Keep specs updated so agents build the right thing
|
||||
- **ADRs** — Help agents understand why past decisions were made (prevents re-deciding)
|
||||
- **Inline gotchas** — Prevent agents from falling into known traps
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "The code is self-documenting" | Code shows what. It doesn't show why, what alternatives were rejected, or what constraints apply. |
|
||||
| "We'll write docs when the API stabilizes" | APIs stabilize faster when you document them. The doc is the first test of the design. |
|
||||
| "Nobody reads docs" | Agents do. Future engineers do. Your 3-months-later self does. |
|
||||
| "ADRs are overhead" | A 10-minute ADR prevents a 2-hour debate about the same decision six months later. |
|
||||
| "Comments get outdated" | Comments on *why* are stable. Comments on *what* get outdated — that's why you only write the former. |
|
||||
|
||||
## Red Flags
|
||||
|
||||
- Architectural decisions with no written rationale
|
||||
- Public APIs with no documentation or types
|
||||
- README that doesn't explain how to run the project
|
||||
- Commented-out code instead of deletion
|
||||
- TODO comments that have been there for weeks
|
||||
- No ADRs in a project with significant architectural choices
|
||||
- Documentation that restates the code instead of explaining intent
|
||||
|
||||
## Verification
|
||||
|
||||
After documenting:
|
||||
|
||||
- [ ] ADRs exist for all significant architectural decisions
|
||||
- [ ] README covers quick start, commands, and architecture overview
|
||||
- [ ] API functions have parameter and return type documentation
|
||||
- [ ] Known gotchas are documented inline where they matter
|
||||
- [ ] No commented-out code remains
|
||||
- [ ] Rules files (CLAUDE.md etc.) are current and accurate
|
||||
@@ -1,355 +0,0 @@
|
||||
---
|
||||
name: git-workflow-and-versioning
|
||||
description: Structures git workflow practices. Use when making any code change. Use when committing, branching, resolving conflicts, or when you need to organize work across multiple parallel streams. Use when cutting a release, choosing a semantic version bump, tagging, or writing a changelog.
|
||||
---
|
||||
|
||||
# Git Workflow and Versioning
|
||||
|
||||
## Overview
|
||||
|
||||
Git is your safety net. Treat commits as save points, branches as sandboxes, and history as documentation. With AI agents generating code at high speed, disciplined version control is the mechanism that keeps changes manageable, reviewable, and reversible.
|
||||
|
||||
## When to Use
|
||||
|
||||
Always. Every code change flows through git.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### Trunk-Based Development (Recommended)
|
||||
|
||||
Keep `main` always deployable. Work in short-lived feature branches that merge back within 1-3 days. Long-lived development branches are hidden costs — they diverge, create merge conflicts, and delay integration. DORA research consistently shows trunk-based development correlates with high-performing engineering teams.
|
||||
|
||||
```
|
||||
main ──●──●──●──●──●──●──●──●──●── (always deployable)
|
||||
╲ ╱ ╲ ╱
|
||||
●──●─╱ ●──╱ ← short-lived feature branches (1-3 days)
|
||||
```
|
||||
|
||||
This is the recommended default. Teams using gitflow or long-lived branches can adapt the principles (atomic commits, small changes, descriptive messages) to their branching model — the commit discipline matters more than the specific branching strategy.
|
||||
|
||||
- **Dev branches are costs.** Every day a branch lives, it accumulates merge risk.
|
||||
- **Release branches are acceptable.** When you need to stabilize a release while main moves forward.
|
||||
- **Feature flags > long branches.** Prefer deploying incomplete work behind flags rather than keeping it on a branch for weeks.
|
||||
|
||||
### 1. Commit Early, Commit Often
|
||||
|
||||
Each successful increment gets its own commit. Don't accumulate large uncommitted changes.
|
||||
|
||||
```
|
||||
Work pattern:
|
||||
Implement slice → Test → Verify → Commit → Next slice
|
||||
|
||||
Not this:
|
||||
Implement everything → Hope it works → Giant commit
|
||||
```
|
||||
|
||||
Commits are save points. If the next change breaks something, you can revert to the last known-good state instantly.
|
||||
|
||||
### 2. Atomic Commits
|
||||
|
||||
Each commit does one logical thing:
|
||||
|
||||
```
|
||||
# Good: Each commit is self-contained
|
||||
git log --oneline
|
||||
a1b2c3d Add task creation endpoint with validation
|
||||
d4e5f6g Add task creation form component
|
||||
h7i8j9k Connect form to API and add loading state
|
||||
m1n2o3p Add task creation tests (unit + integration)
|
||||
|
||||
# Bad: Everything mixed together
|
||||
git log --oneline
|
||||
x1y2z3a Add task feature, fix sidebar, update deps, refactor utils
|
||||
```
|
||||
|
||||
### 3. Descriptive Messages
|
||||
|
||||
Commit messages explain the *why*, not just the *what*:
|
||||
|
||||
```
|
||||
# Good: Explains intent
|
||||
feat: add email validation to registration endpoint
|
||||
|
||||
Prevents invalid email formats from reaching the database.
|
||||
Uses Zod schema validation at the route handler level,
|
||||
consistent with existing validation patterns in auth.ts.
|
||||
|
||||
# Bad: Describes what's obvious from the diff
|
||||
update auth.ts
|
||||
```
|
||||
|
||||
**Format:**
|
||||
```
|
||||
<type>: <short description>
|
||||
|
||||
<optional body explaining why, not what>
|
||||
```
|
||||
|
||||
**Types:**
|
||||
- `feat` — New feature
|
||||
- `fix` — Bug fix
|
||||
- `refactor` — Code change that neither fixes a bug nor adds a feature
|
||||
- `test` — Adding or updating tests
|
||||
- `docs` — Documentation only
|
||||
- `chore` — Tooling, dependencies, config
|
||||
|
||||
### 4. Keep Concerns Separate
|
||||
|
||||
Don't combine formatting changes with behavior changes. Don't combine refactors with features. Each type of change should be a separate commit — and ideally a separate PR:
|
||||
|
||||
```
|
||||
# Good: Separate concerns
|
||||
git commit -m "refactor: extract validation logic to shared utility"
|
||||
git commit -m "feat: add phone number validation to registration"
|
||||
|
||||
# Bad: Mixed concerns
|
||||
git commit -m "refactor validation and add phone number field"
|
||||
```
|
||||
|
||||
**Separate refactoring from feature work.** A refactoring change and a feature change are two different changes — submit them separately. This makes each change easier to review, revert, and understand in history. Small cleanups (renaming a variable) can be included in a feature commit at reviewer discretion.
|
||||
|
||||
### 5. Size Your Changes
|
||||
|
||||
Target ~100 lines per commit/PR. Changes over ~1000 lines should be split. See the splitting strategies in `code-review-and-quality` for how to break down large changes.
|
||||
|
||||
```
|
||||
~100 lines → Easy to review, easy to revert
|
||||
~300 lines → Acceptable for a single logical change
|
||||
~1000 lines → Split into smaller changes
|
||||
```
|
||||
|
||||
## Branching Strategy
|
||||
|
||||
### Feature Branches
|
||||
|
||||
```
|
||||
main (always deployable)
|
||||
│
|
||||
├── feature/task-creation ← One feature per branch
|
||||
├── feature/user-settings ← Parallel work
|
||||
└── fix/duplicate-tasks ← Bug fixes
|
||||
```
|
||||
|
||||
- Branch from `main` (or the team's default branch)
|
||||
- Keep branches short-lived (merge within 1-3 days) — long-lived branches are hidden costs
|
||||
- Delete branches after merge
|
||||
- Prefer feature flags over long-lived branches for incomplete features
|
||||
|
||||
### Branch Naming
|
||||
|
||||
```
|
||||
feature/<short-description> → feature/task-creation
|
||||
fix/<short-description> → fix/duplicate-tasks
|
||||
chore/<short-description> → chore/update-deps
|
||||
refactor/<short-description> → refactor/auth-module
|
||||
```
|
||||
|
||||
## Working with Worktrees
|
||||
|
||||
For parallel AI agent work, use git worktrees to run multiple branches simultaneously:
|
||||
|
||||
```bash
|
||||
# Create a worktree for a feature branch
|
||||
git worktree add ../project-feature-a feature/task-creation
|
||||
git worktree add ../project-feature-b feature/user-settings
|
||||
|
||||
# Each worktree is a separate directory with its own branch
|
||||
# Agents can work in parallel without interfering
|
||||
ls ../
|
||||
project/ ← main branch
|
||||
project-feature-a/ ← task-creation branch
|
||||
project-feature-b/ ← user-settings branch
|
||||
|
||||
# When done, merge and clean up
|
||||
git worktree remove ../project-feature-a
|
||||
```
|
||||
|
||||
Benefits:
|
||||
- Multiple agents can work on different features simultaneously
|
||||
- No branch switching needed (each directory has its own branch)
|
||||
- If one experiment fails, delete the worktree — nothing is lost
|
||||
- Changes are isolated until explicitly merged
|
||||
|
||||
## The Save Point Pattern
|
||||
|
||||
```
|
||||
Agent starts work
|
||||
│
|
||||
├── Makes a change
|
||||
│ ├── Test passes? → Commit → Continue
|
||||
│ └── Test fails? → Revert to last commit → Investigate
|
||||
│
|
||||
├── Makes another change
|
||||
│ ├── Test passes? → Commit → Continue
|
||||
│ └── Test fails? → Revert to last commit → Investigate
|
||||
│
|
||||
└── Feature complete → All commits form a clean history
|
||||
```
|
||||
|
||||
This pattern means you never lose more than one increment of work. If an agent goes off the rails, `git reset --hard HEAD` takes you back to the last successful state.
|
||||
|
||||
## Change Summaries
|
||||
|
||||
After any modification, provide a structured summary. This makes review easier, documents scope discipline, and surfaces unintended changes:
|
||||
|
||||
```
|
||||
CHANGES MADE:
|
||||
- src/routes/tasks.ts: Added validation middleware to POST endpoint
|
||||
- src/lib/validation.ts: Added TaskCreateSchema using Zod
|
||||
|
||||
THINGS I DIDN'T TOUCH (intentionally):
|
||||
- src/routes/auth.ts: Has similar validation gap but out of scope
|
||||
- src/middleware/error.ts: Error format could be improved (separate task)
|
||||
|
||||
POTENTIAL CONCERNS:
|
||||
- The Zod schema is strict — rejects extra fields. Confirm this is desired.
|
||||
- Added zod as a dependency (72KB gzipped) — already in package.json
|
||||
```
|
||||
|
||||
This pattern catches wrong assumptions early and gives reviewers a clear map of the change. The "DIDN'T TOUCH" section is especially important — it shows you exercised scope discipline and didn't go on an unsolicited renovation.
|
||||
|
||||
## Pre-Commit Hygiene
|
||||
|
||||
Before every commit:
|
||||
|
||||
```bash
|
||||
# 1. Check what you're about to commit
|
||||
git diff --staged
|
||||
|
||||
# 2. Ensure no secrets
|
||||
git diff --staged | grep -i "password\|secret\|api_key\|token"
|
||||
|
||||
# 3. Run tests
|
||||
npm test
|
||||
|
||||
# 4. Run linting
|
||||
npm run lint
|
||||
|
||||
# 5. Run type checking
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
Automate this with git hooks:
|
||||
|
||||
```json
|
||||
// package.json (using lint-staged + husky)
|
||||
{
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
|
||||
"*.{json,md}": ["prettier --write"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Handling Generated Files
|
||||
|
||||
- **Commit generated files** only if the project expects them (e.g., `package-lock.json`, Prisma migrations)
|
||||
- **Don't commit** build output (`dist/`, `.next/`), environment files (`.env`), or IDE config (`.vscode/settings.json` unless shared)
|
||||
- **Have a `.gitignore`** that covers: `node_modules/`, `dist/`, `.env`, `.env.local`, `*.pem`
|
||||
|
||||
## Using Git for Debugging
|
||||
|
||||
```bash
|
||||
# Find which commit introduced a bug
|
||||
git bisect start
|
||||
git bisect bad HEAD
|
||||
git bisect good <known-good-commit>
|
||||
# Git checkouts midpoints; run your test at each to narrow down
|
||||
|
||||
# View what changed recently
|
||||
git log --oneline -20
|
||||
git diff HEAD~5..HEAD -- src/
|
||||
|
||||
# Find who last changed a specific line
|
||||
git blame src/services/task.ts
|
||||
|
||||
# Search commit messages for a keyword
|
||||
git log --grep="validation" --oneline
|
||||
```
|
||||
|
||||
## Release & Versioning
|
||||
|
||||
Commits are how *you* track change; a **version** is how your *consumers* track it. The moment anything else depends on your code — another team, a published package, a deployed client — "latest on main" stops being a sufficient answer to "what am I running, and is it safe to upgrade?" A version number and a changelog are the contract that answers it.
|
||||
|
||||
### Semantic Versioning
|
||||
|
||||
For anything with consumers, version `MAJOR.MINOR.PATCH` and let the number carry meaning:
|
||||
|
||||
```
|
||||
MAJOR breaking change — consumers must change their code to upgrade
|
||||
MINOR new functionality, backward-compatible — safe to upgrade
|
||||
PATCH bug fix, backward-compatible — safe to upgrade
|
||||
```
|
||||
|
||||
The number is a promise, so make the code match it. A "patch" that changes behavior consumers relied on is a major change wearing a disguise (Hyrum's Law — see the `api-and-interface-design` skill). When unsure whether a change is breaking, assume it is; a surprise major is far cheaper than a broken consumer.
|
||||
|
||||
### Tag the release, and let the tag be the source of truth
|
||||
|
||||
A release is an immutable point in history, not a moving branch. Tag it so it can always be reproduced:
|
||||
|
||||
```bash
|
||||
git tag -a v1.4.0 -m "Release 1.4.0"
|
||||
git push origin v1.4.0
|
||||
```
|
||||
|
||||
Derive the version from the tag rather than hand-editing it in scattered files, so the artifact, the tag, and the changelog can never disagree.
|
||||
|
||||
### Keep a changelog written for humans
|
||||
|
||||
A changelog is not `git log`. It's the curated, consumer-facing answer to "what changed and do I care?" — grouped by `Added / Changed / Fixed / Deprecated / Removed / Security`, newest on top, every entry phrased around user impact, not internal mechanics.
|
||||
|
||||
```markdown
|
||||
## [1.4.0] - 2025-06-12
|
||||
### Added
|
||||
- Bulk task import via CSV
|
||||
### Fixed
|
||||
- Timezone drift in recurring task due dates
|
||||
### Deprecated
|
||||
- `GET /v1/tasks/all` — use the paginated `GET /v1/tasks` (removal in 2.0)
|
||||
```
|
||||
|
||||
Write the entry in the same change that makes the change, while the impact is fresh — not reconstructed from commit archaeology at release time. Breaking changes get a migration note and a deprecation window (follow the `deprecation-and-migration` skill); shipping the actual release is the `shipping-and-launch` skill's job — this section is the versioning contract that feeds it.
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "I'll commit when the feature is done" | One giant commit is impossible to review, debug, or revert. Commit each slice. |
|
||||
| "The message doesn't matter" | Messages are documentation. Future you (and future agents) will need to understand what changed and why. |
|
||||
| "I'll squash it all later" | Squashing destroys the development narrative. Prefer clean incremental commits from the start. |
|
||||
| "Branches add overhead" | Short-lived branches are free and prevent conflicting work from colliding. Long-lived branches are the problem — merge within 1-3 days. |
|
||||
| "I'll split this change later" | Large changes are harder to review, riskier to deploy, and harder to revert. Split before submitting, not after. |
|
||||
| "I don't need a .gitignore" | Until `.env` with production secrets gets committed. Set it up immediately. |
|
||||
| "It's just a small fix, bump the patch" | Check what consumers can observe. A behavior change they relied on is a major, whatever the diff size. |
|
||||
| "The changelog is just the commit log" | Commits are for you; the changelog is for consumers, curated by impact. Generating one from raw commits buries what matters. |
|
||||
| "We'll write the changelog at release time" | By then the impact is reconstructed from memory and half of it is missing. Write the entry with the change. |
|
||||
|
||||
## Red Flags
|
||||
|
||||
- Large uncommitted changes accumulating
|
||||
- Commit messages like "fix", "update", "misc"
|
||||
- Formatting changes mixed with behavior changes
|
||||
- No `.gitignore` in the project
|
||||
- Committing `node_modules/`, `.env`, or build artifacts
|
||||
- Long-lived branches that diverge significantly from main
|
||||
- Force-pushing to shared branches
|
||||
- A breaking change shipped under a minor or patch version bump
|
||||
- A release with no tag, or a version number hand-edited out of sync with the tag
|
||||
- A user-facing release with no changelog entry, or a changelog that's just dumped commit messages
|
||||
|
||||
## Verification
|
||||
|
||||
For every commit:
|
||||
|
||||
- [ ] Commit does one logical thing
|
||||
- [ ] Message explains the why, follows type conventions
|
||||
- [ ] Tests pass before committing
|
||||
- [ ] No secrets in the diff
|
||||
- [ ] No formatting-only changes mixed with behavior changes
|
||||
- [ ] `.gitignore` covers standard exclusions
|
||||
|
||||
For every release (anything with consumers):
|
||||
|
||||
- [ ] The version bump matches the change: breaking → major, additive → minor, fix → patch
|
||||
- [ ] The release is tagged, and the version is derived from the tag, not hand-edited out of sync
|
||||
- [ ] The changelog has a curated, human-readable entry grouped by impact for this version
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
name: grill-me
|
||||
description: A relentless interview to sharpen a plan or design.
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
Call the Skill tool with "grilling".
|
||||
@@ -1,5 +0,0 @@
|
||||
interface:
|
||||
display_name: "Grill Me"
|
||||
short_description: "Sharpen a plan through interview"
|
||||
policy:
|
||||
allow_implicit_invocation: false
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: grilling
|
||||
description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
|
||||
---
|
||||
|
||||
Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.
|
||||
|
||||
Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled: the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round.
|
||||
|
||||
Each question should be formatted like so:
|
||||
|
||||
```
|
||||
❓ **Q1** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
|
||||
|
||||
➡️ <your recommended answer>
|
||||
```
|
||||
|
||||
Each round the user answers reshapes the tree: settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one.
|
||||
|
||||
Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it; don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report; ask the rest of the frontier now. The _decisions_ are the user's: put each to them and wait.
|
||||
|
||||
The session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding.
|
||||
@@ -1,3 +0,0 @@
|
||||
interface:
|
||||
display_name: "Grilling"
|
||||
short_description: "Stress-test thinking a round of questions at a time"
|
||||
@@ -1,248 +0,0 @@
|
||||
---
|
||||
name: planning-and-task-breakdown
|
||||
description: Breaks work into ordered tasks. Use when you have a spec or clear requirements and need to break work into implementable tasks. Use when a task feels too large to start, when you need to estimate scope, or when parallel work is possible.
|
||||
---
|
||||
|
||||
# Planning and Task Breakdown
|
||||
|
||||
## Overview
|
||||
|
||||
Decompose work into small, verifiable tasks with explicit acceptance criteria. Good task breakdown is the difference between an agent that completes work reliably and one that produces a tangled mess. Every task should be small enough to implement, test, and verify in a single focused session.
|
||||
|
||||
## When to Use
|
||||
|
||||
- You have a spec and need to break it into implementable units
|
||||
- A task feels too large or vague to start
|
||||
- Work needs to be parallelized across multiple agents or sessions
|
||||
- You need to communicate scope to a human
|
||||
- The implementation order isn't obvious
|
||||
|
||||
**When NOT to use:** Single-file changes with obvious scope, or when the spec already contains well-defined tasks.
|
||||
|
||||
## The Planning Process
|
||||
|
||||
### Step 1: Enter Plan Mode
|
||||
|
||||
Before writing any code, operate in read-only mode:
|
||||
|
||||
- Read the spec and relevant codebase sections
|
||||
- Identify existing patterns and conventions
|
||||
- Map dependencies between components
|
||||
- Note risks and unknowns
|
||||
|
||||
**Do NOT write code during planning.** The output is a plan document saved to `tasks/plan.md` and a task list recorded in the task list target (see Output Files; default `tasks/todo.md`), not implementation.
|
||||
|
||||
### Step 2: Identify the Dependency Graph
|
||||
|
||||
Map what depends on what:
|
||||
|
||||
```
|
||||
Database schema
|
||||
│
|
||||
├── API models/types
|
||||
│ │
|
||||
│ ├── API endpoints
|
||||
│ │ │
|
||||
│ │ └── Frontend API client
|
||||
│ │ │
|
||||
│ │ └── UI components
|
||||
│ │
|
||||
│ └── Validation logic
|
||||
│
|
||||
└── Seed data / migrations
|
||||
```
|
||||
|
||||
Implementation order follows the dependency graph bottom-up: build foundations first.
|
||||
|
||||
### Step 3: Slice Vertically
|
||||
|
||||
Instead of building all the database, then all the API, then all the UI — build one complete feature path at a time:
|
||||
|
||||
**Bad (horizontal slicing):**
|
||||
```
|
||||
Task 1: Build entire database schema
|
||||
Task 2: Build all API endpoints
|
||||
Task 3: Build all UI components
|
||||
Task 4: Connect everything
|
||||
```
|
||||
|
||||
**Good (vertical slicing):**
|
||||
```
|
||||
Task 1: User can create an account (schema + API + UI for registration)
|
||||
Task 2: User can log in (auth schema + API + UI for login)
|
||||
Task 3: User can create a task (task schema + API + UI for creation)
|
||||
Task 4: User can view task list (query + API + UI for list view)
|
||||
```
|
||||
|
||||
Each vertical slice delivers working, testable functionality.
|
||||
|
||||
### Step 4: Write Tasks
|
||||
|
||||
Each task follows this structure, whether it lands in the markdown task list or as an item in an external tracker (see Output Files):
|
||||
|
||||
```markdown
|
||||
## Task [N]: [Short descriptive title]
|
||||
|
||||
**Description:** One paragraph explaining what this task accomplishes.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] [Specific, testable condition]
|
||||
- [ ] [Specific, testable condition]
|
||||
|
||||
**Verification:**
|
||||
- [ ] Tests pass: [the repository's focused-test command]
|
||||
- [ ] Build succeeds: [the repository's build command]
|
||||
- [ ] Manual check: [description of what to verify]
|
||||
|
||||
**Dependencies:** [Task numbers this depends on, or "None"]
|
||||
|
||||
**Files likely touched:**
|
||||
- `src/path/to/file.ts`
|
||||
- `tests/path/to/test.ts`
|
||||
|
||||
**Estimated scope:** [Small: 1-2 files | Medium: 3-5 files | Large: 5+ files]
|
||||
```
|
||||
|
||||
### Step 5: Order and Checkpoint
|
||||
|
||||
Arrange tasks so that:
|
||||
|
||||
1. Dependencies are satisfied (build foundation first)
|
||||
2. Each task leaves the system in a working state
|
||||
3. Verification checkpoints occur after every 2-3 tasks
|
||||
4. High-risk tasks are early (fail fast)
|
||||
|
||||
Add explicit checkpoints to the task list target:
|
||||
|
||||
```markdown
|
||||
## Checkpoint: After Tasks 1-3
|
||||
- [ ] All tests pass
|
||||
- [ ] Application builds without errors
|
||||
- [ ] Core user flow works end-to-end
|
||||
- [ ] Review with human before proceeding
|
||||
```
|
||||
|
||||
## Task Sizing Guidelines
|
||||
|
||||
| Size | Files | Scope | Example |
|
||||
|------|-------|-------|---------|
|
||||
| **XS** | 1 | Single function or config change | Add a validation rule |
|
||||
| **S** | 1-2 | One component or endpoint | Add a new API endpoint |
|
||||
| **M** | 3-5 | One feature slice | User registration flow |
|
||||
| **L** | 5-8 | Multi-component feature | Search with filtering and pagination |
|
||||
| **XL** | 8+ | **Too large — break it down further** | — |
|
||||
|
||||
If a task is L or larger, it should be broken into smaller tasks. An agent performs best on S and M tasks.
|
||||
|
||||
**When to break a task down further:**
|
||||
- It would take more than one focused session (roughly 2+ hours of agent work)
|
||||
- You cannot describe the acceptance criteria in 3 or fewer bullet points
|
||||
- It touches two or more independent subsystems (e.g., auth and billing)
|
||||
- You find yourself writing "and" in the task title (a sign it is two tasks)
|
||||
|
||||
## Output Files
|
||||
|
||||
- **Plan document:** Save the implementation plan to `tasks/plan.md`. This is always a markdown file — design decisions, risks, and open questions don't map cleanly onto individual tracker issues.
|
||||
- **Task list:** Record each task in the **task list target** (defined below).
|
||||
|
||||
Create the `tasks/` directory if it does not exist.
|
||||
|
||||
### Task List Target
|
||||
|
||||
The task list target is where tasks and checkpoints are recorded. It is defined once, here; every other reference in this skill defers to it.
|
||||
|
||||
- **Default: a checklist-style markdown file at `tasks/todo.md`.** This is the convention the `/build` command and other downstream tooling expect. Use it unless the project says otherwise.
|
||||
- **External tracker:** if the project's agent rules (`CLAUDE.md`, `AGENTS.md`, etc.) or the user designate an issue tracker (e.g. GitHub Issues, Jira, Linear, `bd`/beads), create one tracker item per task instead of writing `tasks/todo.md`. Map the Step 4 structure onto the tracker's fields: acceptance criteria and verification steps in the item body, dependencies via the tracker's linking mechanism (`bd dep add`, "blocked by", etc.). Record Step 5 checkpoints as tracker items too, or as a checklist in the plan document if the tracker has no natural equivalent.
|
||||
|
||||
When using an external tracker, note it in `tasks/plan.md` (e.g. "Tasks tracked in Linear project FOO") so downstream steps and future sessions know where to look, and keep the plan document's Task List section as an ordered index of tracker item IDs or links rather than a duplicate checklist.
|
||||
|
||||
## Plan Document Template
|
||||
|
||||
```markdown
|
||||
# Implementation Plan: [Feature/Project Name]
|
||||
|
||||
## Overview
|
||||
[One paragraph summary of what we're building]
|
||||
|
||||
## Architecture Decisions
|
||||
- [Key decision 1 and rationale]
|
||||
- [Key decision 2 and rationale]
|
||||
|
||||
## Task List
|
||||
|
||||
### Phase 1: Foundation
|
||||
- [ ] Task 1: ...
|
||||
- [ ] Task 2: ...
|
||||
|
||||
### Checkpoint: Foundation
|
||||
- [ ] Tests pass, builds clean
|
||||
|
||||
### Phase 2: Core Features
|
||||
- [ ] Task 3: ...
|
||||
- [ ] Task 4: ...
|
||||
|
||||
### Checkpoint: Core Features
|
||||
- [ ] End-to-end flow works
|
||||
|
||||
### Phase 3: Polish
|
||||
- [ ] Task 5: ...
|
||||
- [ ] Task 6: ...
|
||||
|
||||
### Checkpoint: Complete
|
||||
- [ ] All acceptance criteria met
|
||||
- [ ] Ready for review
|
||||
|
||||
## Risks and Mitigations
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|------------|
|
||||
| [Risk] | [High/Med/Low] | [Strategy] |
|
||||
|
||||
## Open Questions
|
||||
- [Question needing human input]
|
||||
```
|
||||
|
||||
When tasks live in an external tracker, keep the Task List section above as an ordered index of tracker item IDs or links instead of a duplicate checklist.
|
||||
|
||||
## Parallelization Opportunities
|
||||
|
||||
When multiple agents or sessions are available:
|
||||
|
||||
- **Safe to parallelize:** Independent feature slices, tests for already-implemented features, documentation
|
||||
- **Must be sequential:** Database migrations, shared state changes, dependency chains
|
||||
- **Needs coordination:** Features that share an API contract (define the contract first, then parallelize)
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "I'll figure it out as I go" | That's how you end up with a tangled mess and rework. 10 minutes of planning saves hours. |
|
||||
| "The tasks are obvious" | Write them down anyway. Explicit tasks surface hidden dependencies and forgotten edge cases. |
|
||||
| "Planning is overhead" | Planning is the task. Implementation without a plan is just typing. |
|
||||
| "I can hold it all in my head" | Context windows are finite. Written plans survive session boundaries and compaction. |
|
||||
|
||||
## Red Flags
|
||||
|
||||
- Starting implementation without a written task list
|
||||
- Writing `tasks/todo.md` when the project has designated an external tracker (or scattering tasks across both)
|
||||
- Tasks that say "implement the feature" without acceptance criteria
|
||||
- No verification steps in the plan
|
||||
- All tasks are XL-sized
|
||||
- No checkpoints between tasks
|
||||
- Dependency order isn't considered
|
||||
|
||||
## Verification
|
||||
|
||||
Before starting implementation, confirm:
|
||||
|
||||
- [ ] Every task has acceptance criteria
|
||||
- [ ] Every task has a verification step
|
||||
- [ ] Task dependencies are identified and ordered correctly
|
||||
- [ ] Tasks are recorded in the task list target (default `tasks/todo.md`)
|
||||
- [ ] No task touches more than ~5 files
|
||||
- [ ] Checkpoints exist between major phases
|
||||
- [ ] The human has reviewed and approved the plan
|
||||
|
||||
## See Also
|
||||
|
||||
Acceptance criteria are per-task and answer "did we build the right thing?". They sit on top of the project-wide Definition of Done, the standing bar every task clears before it counts as done. See `references/definition-of-done.md`.
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# Definition of Done
|
||||
|
||||
A standing, project-wide bar that every change must clear before it counts as done. Unlike acceptance criteria, which vary per task and answer "did we build the right thing?", the Definition of Done is the same every time and answers "is this finished to our standard?". Use it as the final gate in `planning-and-task-breakdown`, `incremental-implementation`, and `shipping-and-launch`.
|
||||
|
||||
## Definition of Done vs. Acceptance Criteria
|
||||
|
||||
| | Acceptance Criteria | Definition of Done |
|
||||
|---|---|---|
|
||||
| Scope | Specific to one task or spec | Applies to every increment |
|
||||
| Changes | Different for each item | Fixed and reused |
|
||||
| Answers | "Did we build *this thing*?" | "Is it *ready*?" |
|
||||
| Owner | Defined when planning the task | Defined once for the project |
|
||||
| Example | "User can reset password via email link" | "Tests pass, no regressions, docs updated" |
|
||||
|
||||
The two are complementary. A task is done only when **its** acceptance criteria are met **and** the standing Definition of Done is satisfied. Skipping either leaves work that looks finished but is not.
|
||||
|
||||
## The Standing Checklist
|
||||
|
||||
Apply this to every change before declaring it done.
|
||||
|
||||
### Correctness
|
||||
- [ ] All acceptance criteria for the task are met
|
||||
- [ ] Code runs and behaves as intended, verified at runtime, not just compiled or typechecked
|
||||
- [ ] New behavior is covered by tests that fail without the change and pass with it
|
||||
- [ ] Existing tests still pass; no regressions introduced
|
||||
- [ ] Edge cases and error paths are handled, not just the happy path
|
||||
|
||||
### Quality
|
||||
- [ ] Code reveals intent through naming and structure; no comments needed to explain *what* it does
|
||||
- [ ] No duplicated business logic
|
||||
- [ ] No dead code, debug output, or commented-out blocks left behind
|
||||
- [ ] Changes are scoped to the task; no unrelated refactors snuck in
|
||||
- [ ] Linting and formatting pass
|
||||
|
||||
The depth behind these items lives in `code-review-and-quality` (the five-axis review) and `code-simplification` (reducing complexity without changing behavior).
|
||||
|
||||
### Integration
|
||||
- [ ] Change works with the rest of the system, not just in isolation
|
||||
- [ ] Database migrations, config changes, and feature flags are accounted for
|
||||
- [ ] Backward compatibility considered for any public interface or API change
|
||||
|
||||
### Documentation
|
||||
- [ ] Public interfaces, APIs, and user-facing behavior are documented
|
||||
- [ ] Architectural decisions worth preserving are recorded (see `documentation-and-adrs`)
|
||||
- [ ] Documentation describes the current state in timeless language, not the change history
|
||||
|
||||
### Ship-readiness
|
||||
- [ ] Security implications reviewed for any untrusted input, auth, or data handling (see `security-and-hardening`)
|
||||
- [ ] Observability in place for new critical paths (logs, metrics, traces) (see `observability-and-instrumentation`)
|
||||
- [ ] Rollback path exists for anything risky (see `shipping-and-launch`)
|
||||
- [ ] The human has reviewed and approved before merge or deploy
|
||||
|
||||
## How to Apply
|
||||
|
||||
- **Per task**: confirm the Correctness and Quality sections before checking the task off.
|
||||
- **Per feature**: confirm Integration and Documentation before considering the feature complete.
|
||||
- **Per release**: the full checklist is the floor; `shipping-and-launch` adds the deploy-specific gates on top.
|
||||
|
||||
Tailor the list to the project once, then reuse it unchanged. A Definition of Done that is renegotiated every sprint is not a Definition of Done.
|
||||
|
||||
## Red Flags
|
||||
|
||||
- "It's done, I just haven't run it yet": unverified work is not done.
|
||||
- "Tests pass" used as a synonym for done while docs, regressions, or runtime verification are skipped.
|
||||
- A different bar applied depending on deadline pressure.
|
||||
- Acceptance criteria treated as the whole bar, with no standing quality floor.
|
||||
- "Done" declared before human review on changes that need it.
|
||||
11
.env.example
@@ -1,13 +1,2 @@
|
||||
NEXT_HOST=east-guilan-ce.ir
|
||||
LETSENCRYPT_EMAIL=admin@east-guilan-ce.ir
|
||||
|
||||
# Backups
|
||||
S3_BACKUP_BUCKET=replace-with-bucket
|
||||
S3_BACKUP_PREFIX=backup/guilan-ace
|
||||
S3_BACKUP_REGION=us-east-1
|
||||
S3_BACKUP_ENDPOINT_URL=https://replace-with-s3-endpoint.example.com
|
||||
S3_BACKUP_ACCESS_KEY_ID=replace-with-access-key
|
||||
S3_BACKUP_SECRET_ACCESS_KEY=replace-with-secret-key
|
||||
BACKUP_ENCRYPTION_PASSPHRASE=replace-with-long-random-passphrase
|
||||
BACKUP_LOCAL_KEEP_LATEST=3
|
||||
BACKUP_REMOTE_KEEP_LATEST=10
|
||||
|
||||
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
*.sh text eol=lf
|
||||
8
.github/workflows/deployment.yml
vendored
@@ -37,12 +37,20 @@ jobs:
|
||||
REDIS_URL=redis://:password@redis:6379/0
|
||||
CELERY_BROKER_URL=redis://:password@redis:6379/0
|
||||
CELERY_RESULT_BACKEND=redis://:password@redis:6379/1
|
||||
EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
|
||||
EMAIL_HOST=localhost
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USE_TLS=False
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
DEFAULT_FROM_EMAIL=noreply@example.com
|
||||
JWT_SECRET_KEY=validate
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_ACCESS_TOKEN_LIFETIME=3600
|
||||
JWT_REFRESH_TOKEN_LIFETIME=86400
|
||||
CORS_ALLOWED_ORIGINS=https://frontend.example.com
|
||||
FRONTEND_ROOT=https://frontend.example.com
|
||||
FRONTEND_PASSWORD_RESET_PAGE=https://frontend.example.com/reset-password
|
||||
FRONTEND_CALLBACK_URL=https://frontend.example.com/payments/result
|
||||
ZARINPAL_MERCHANT_ID=test
|
||||
ZARINPAL_USE_SANDBOX=True
|
||||
|
||||
1
.gitignore
vendored
@@ -1,6 +1,5 @@
|
||||
.env
|
||||
data/
|
||||
backups/
|
||||
certs/fullchain.pem
|
||||
certs/privateKey.pem
|
||||
.vscode/
|
||||
|
||||
100
README.md
@@ -32,103 +32,3 @@ parent-directory/
|
||||
- The frontend is a Next.js standalone Node runtime behind Traefik on internal port `3000`.
|
||||
- Alertmanager has been removed from this stack. Prometheus scraping and Grafana provisioning remain intact.
|
||||
- Dockerfiles live only in this deployment repository, and compose is intentionally wired only for the nested production layout.
|
||||
|
||||
## Backups and restores
|
||||
The deployment repo owns backup automation in `scripts/`. Backups include:
|
||||
- PostgreSQL dump from the `db` service.
|
||||
- Django media files from the `django_media` Docker volume.
|
||||
- Deployment, backend, and frontend `.env` files when present.
|
||||
|
||||
The scripts expect this production layout:
|
||||
```text
|
||||
guilan-ace-deployment/
|
||||
.env
|
||||
backend/guilan-ace-backend/.env
|
||||
frontend/guilan-ace-frontend/.env
|
||||
docker-compose.yml
|
||||
```
|
||||
|
||||
Add these backup variables to `guilan-ace-deployment/.env`:
|
||||
```env
|
||||
S3_BACKUP_BUCKET=c284984
|
||||
S3_BACKUP_PREFIX=backup/guilan-ace
|
||||
S3_BACKUP_REGION=us-east-1
|
||||
S3_BACKUP_ENDPOINT_URL=https://c284984.parspack.net
|
||||
S3_BACKUP_ACCESS_KEY_ID=replace-with-access-key
|
||||
S3_BACKUP_SECRET_ACCESS_KEY=replace-with-secret-key
|
||||
BACKUP_ENCRYPTION_PASSPHRASE=replace-with-long-random-passphrase
|
||||
BACKUP_LOCAL_KEEP_LATEST=3
|
||||
BACKUP_REMOTE_KEEP_LATEST=10
|
||||
```
|
||||
|
||||
Install script dependencies on the server:
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y docker-compose-plugin rclone openssl
|
||||
chmod +x ~/guilan-ace-deployment/scripts/*.sh
|
||||
```
|
||||
|
||||
Create a manual local backup:
|
||||
```bash
|
||||
cd ~/guilan-ace-deployment
|
||||
./scripts/backup.sh
|
||||
```
|
||||
|
||||
Create, encrypt, upload, and rotate an S3 backup:
|
||||
```bash
|
||||
cd ~/guilan-ace-deployment
|
||||
./scripts/backup-upload-s3.sh
|
||||
```
|
||||
|
||||
Restore from a local backup archive:
|
||||
```bash
|
||||
cd ~/guilan-ace-deployment
|
||||
./scripts/restore.sh backups/guilan-ace-backup-YYYYMMDD-HHMMSS.tar.gz
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Restore the latest encrypted S3 backup:
|
||||
```bash
|
||||
cd ~/guilan-ace-deployment
|
||||
./scripts/restore-from-s3.sh latest
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
If a new server fails with `Database is uninitialized and superuser password is not specified`,
|
||||
make sure `backend/guilan-ace-backend/.env` contains `POSTGRES_DB`, `POSTGRES_USER`, and
|
||||
`POSTGRES_PASSWORD` matching `DB_NAME`, `DB_USER`, and `DB_PASSWORD`. Then remove the failed
|
||||
empty database volume and rerun the restore:
|
||||
```bash
|
||||
cd ~/guilan-ace-deployment
|
||||
docker compose down
|
||||
docker volume rm east-guilan_postgres_data
|
||||
./scripts/restore-from-s3.sh latest
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Restore only selected parts by setting skip flags:
|
||||
```bash
|
||||
RESTORE_SKIP_ENV=1 ./scripts/restore.sh backups/guilan-ace-backup-YYYYMMDD-HHMMSS.tar.gz
|
||||
RESTORE_SKIP_DB=1 RESTORE_SKIP_MEDIA=1 ./scripts/restore-from-s3.sh latest
|
||||
```
|
||||
|
||||
Database restore recreates the configured database and is destructive unless `RESTORE_SKIP_DB=1` is set.
|
||||
|
||||
Run encrypted S3 backups periodically with cron:
|
||||
```bash
|
||||
crontab -e
|
||||
```
|
||||
|
||||
Example: run every day at `03:30` server time and write logs to `backups/backup.log`:
|
||||
```cron
|
||||
30 3 * * * cd /home/ubuntu/guilan-ace-deployment && ./scripts/backup-upload-s3.sh >> /home/ubuntu/guilan-ace-deployment/backups/backup.log 2>&1
|
||||
```
|
||||
|
||||
Example: run every 6 hours:
|
||||
```cron
|
||||
0 */6 * * * cd /home/ubuntu/guilan-ace-deployment && ./scripts/backup-upload-s3.sh >> /home/ubuntu/guilan-ace-deployment/backups/backup.log 2>&1
|
||||
```
|
||||
|
||||
## Project-local planning and deployment skills
|
||||
|
||||
Cross-repository planning and deployment guidance is vendored in `.agents/skills/`: `grill-me`, `grilling`, `planning-and-task-breakdown`, `documentation-and-adrs`, `ci-cd-and-automation`, and `git-workflow-and-versioning`. The redesign and content-management implementation plan lives in `tasks/` in this repository.
|
||||
|
||||
@@ -7,6 +7,14 @@ ENV PYTHONUNBUFFERED=1
|
||||
# Set work directory
|
||||
WORKDIR /app
|
||||
|
||||
RUN rm -f /etc/apt/sources.list.d/debian.sources && \
|
||||
printf '%s\n' \
|
||||
'deb http://mirror-linux.runflare.com/debian trixie main' \
|
||||
'deb http://mirror-linux.runflare.com/debian-security trixie-security main' \
|
||||
> /etc/apt/sources.list && \
|
||||
echo 'Acquire::Check-Valid-Until "false";' > /etc/apt/apt.conf.d/99no-check-valid
|
||||
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
|
||||
@@ -1,6 +1,64 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
...
|
||||
MIIGEzCCBPugAwIBAgISBZf+U3m3Aftq6nyZTh1KZD6tMA0GCSqGSIb3DQEBCwUA
|
||||
MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD
|
||||
EwNSMTIwHhcNMjYwNTE4MTQyOTI0WhcNMjYwODE2MTQyOTIzWjAeMRwwGgYDVQQD
|
||||
DBMqLmVhc3QtZ3VpbGFuLWNlLmlyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
|
||||
CgKCAgEAx0eyNPO1YVzOZGiC19l5IEedFeMe1Yf2T1srJUr9MSpDKKiE8n3AY0Jq
|
||||
9WgUou9E8ZViu3cYjE8UEuP6s4W+U1iXYWMwqc6hAkSejn8mb4vSdIO1dVEW9BNM
|
||||
spDgXZvbwSs6UWm+sUpxwot4hV9RlzIdSoZ4nrLRJnu7OSW/fO7xU8UNPAgPHarH
|
||||
e/xBPPTeYKq+CcDb7HJUdJMYxDd1oRtZQm/Uz5rDrqf0R4DxAhUBcZXgp8zn4yH1
|
||||
nPjIR+2XUCB2n4QlOfNqhiPa9JwD6ZVrUImaFBdDTZjenE/HHVJ94k1LzMTGgHRv
|
||||
Cp/avx5fw4lYlY8J72eKKFK77fHQpUUk7F7klZ+CHDlrLi+RDyqiJezhVJlPuXXn
|
||||
ivEWvrrVN0EZ+1pO+Xn1xqBDjJ2KP+VbGzSveGgxsO5XHy2BaGumYx7nQwhqynXk
|
||||
BMKC5PCufu8zfvsd2ODSGUvIhihgsXbFRaWEWOQiLqFq3z/4GORnn09DvBqnIsKQ
|
||||
LGI4xDrnb/JYmkCs5zxLey1kDWOtqRjzZ/aVzSUM8h1UOmofXKPGg/ucM1SMEqe5
|
||||
qLyifgYsxZ/OUnrx6OZt1xW4eROGxovyehscaCIIMeIAhKZ4oiJhWfZ4mgb9teiF
|
||||
RylJST+YXqN9/DydVe6AyIUInKAQZL5PsY36B/QK6Fckb2GnOK8CAwEAAaOCAjQw
|
||||
ggIwMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMB
|
||||
Af8EAjAAMB0GA1UdDgQWBBTU6TZIKc65xcaDLlCRk8padrq7yjAfBgNVHSMEGDAW
|
||||
gBQAtSnyLY5vMeibTK14Pvrc6QzR0jAzBggrBgEFBQcBAQQnMCUwIwYIKwYBBQUH
|
||||
MAKGF2h0dHA6Ly9yMTIuaS5sZW5jci5vcmcvMDEGA1UdEQQqMCiCEyouZWFzdC1n
|
||||
dWlsYW4tY2UuaXKCEWVhc3QtZ3VpbGFuLWNlLmlyMBMGA1UdIAQMMAowCAYGZ4EM
|
||||
AQIBMC4GA1UdHwQnMCUwI6AhoB+GHWh0dHA6Ly9yMTIuYy5sZW5jci5vcmcvNDYu
|
||||
Y3JsMIIBDAYKKwYBBAHWeQIEAgSB/QSB+gD4AHcAwjF+V0UZo0XufzjespBB68fC
|
||||
IVoiv3/Vta12mtkOUs0AAAGeO7NpVgAABAMASDBGAiEAmxz+oc1QeAR1J/yEe1jZ
|
||||
W2hT/U3XF+5q63O+kRjQWO0CIQCGI+xwY/hhPjJr9HkPRTI5NXGt9EeVe0vMAu1s
|
||||
+TMtxgB9ABqLnWsP/r+BtHk5xtIxCobW0QLU8EbiGCyd419eJiXvAAABnjuza0wA
|
||||
CAAABQAUIto1BAMARjBEAiAPAyeQN4zkD/vUAAqo/8sF5uKicaul9fS9y0bUv+8d
|
||||
hgIgQxClI5FycnYL6GTwBxpNWS0uWbDhoTAtj+Mw6NOGOD8wDQYJKoZIhvcNAQEL
|
||||
BQADggEBAHt+R9Da16AbLPZqC5BelK6prKdmeaqkDIDO6aE1aZyuS0xxK228fPAr
|
||||
zcopyWI4Onm29bAYxaeFtUwZurDyqb+jHf0AD8PC2zOFxQCvDyEO9l28yQ51hSR2
|
||||
6KlTNfsUaBAdZctSseZI1wcqJ3cwZLGAJjrTXaMDDDc83UeG/bJ8syU+iGlUhhWu
|
||||
e/hEFTtIQ14U6lvNLwgtsC3Ptwe5gGIauJ+wtTIkavulGrWpGLgEUQPqNFz/NmwM
|
||||
+Nis+oB2qKMJfQBoQgNIVFahsnOvUTJZbx3nEKQfI/OAGQHH2fr6xpg09zbi0j/5
|
||||
Y8q58R3hzVyBdJHytdcI5W/pEJmYzSE=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
...
|
||||
MIIFBjCCAu6gAwIBAgIRAMISMktwqbSRcdxA9+KFJjwwDQYJKoZIhvcNAQELBQAw
|
||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
|
||||
WhcNMjcwMzEyMjM1OTU5WjAzMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
|
||||
RW5jcnlwdDEMMAoGA1UEAxMDUjEyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEA2pgodK2+lP474B7i5Ut1qywSf+2nAzJ+Npfs6DGPpRONC5kuHs0BUT1M
|
||||
5ShuCVUxqqUiXXL0LQfCTUA83wEjuXg39RplMjTmhnGdBO+ECFu9AhqZ66YBAJpz
|
||||
kG2Pogeg0JfT2kVhgTU9FPnEwF9q3AuWGrCf4yrqvSrWmMebcas7dA8827JgvlpL
|
||||
Thjp2ypzXIlhZZ7+7Tymy05v5J75AEaz/xlNKmOzjmbGGIVwx1Blbzt05UiDDwhY
|
||||
XS0jnV6j/ujbAKHS9OMZTfLuevYnnuXNnC2i8n+cF63vEzc50bTILEHWhsDp7CH4
|
||||
WRt/uTp8n1wBnWIEwii9Cq08yhDsGwIDAQABo4H4MIH1MA4GA1UdDwEB/wQEAwIB
|
||||
hjAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwEgYDVR0TAQH/BAgwBgEB
|
||||
/wIBADAdBgNVHQ4EFgQUALUp8i2ObzHom0yteD763OkM0dIwHwYDVR0jBBgwFoAU
|
||||
ebRZ5nu25eQBc4AIiMgaWPbpm24wMgYIKwYBBQUHAQEEJjAkMCIGCCsGAQUFBzAC
|
||||
hhZodHRwOi8veDEuaS5sZW5jci5vcmcvMBMGA1UdIAQMMAowCAYGZ4EMAQIBMCcG
|
||||
A1UdHwQgMB4wHKAaoBiGFmh0dHA6Ly94MS5jLmxlbmNyLm9yZy8wDQYJKoZIhvcN
|
||||
AQELBQADggIBAI910AnPanZIZTKS3rVEyIV29BWEjAK/duuz8eL5boSoVpHhkkv3
|
||||
4eoAeEiPdZLj5EZ7G2ArIK+gzhTlRQ1q4FKGpPPaFBSpqV/xbUb5UlAXQOnkHn3m
|
||||
FVj+qYv87/WeY+Bm4sN3Ox8BhyaU7UAQ3LeZ7N1X01xxQe4wIAAE3JVLUCiHmZL+
|
||||
qoCUtgYIFPgcg350QMUIWgxPXNGEncT921ne7nluI02V8pLUmClqXOsCwULw+PVO
|
||||
ZCB7qOMxxMBoCUeL2Ll4oMpOSr5pJCpLN3tRA2s6P1KLs9TSrVhOk+7LX28NMUlI
|
||||
usQ/nxLJID0RhAeFtPjyOCOscQBA53+NRjSCak7P4A5jX7ppmkcJECL+S0i3kXVU
|
||||
y5Me5BbrU8973jZNv/ax6+ZK6TM8jWmimL6of6OrX7ZU6E2WqazzsFrLG3o2kySb
|
||||
zlhSgJ81Cl4tv3SbYiYXnJExKQvzf83DYotox3f0fwv7xln1A2ZLplCb0O+l/AK0
|
||||
YE0DS2FPxSAHi0iwMfW2nNHJrXcY3LLHD77gRgje4Eveubi2xxa+Nmk/hmhLdIET
|
||||
iVDFanoCrMVIpQ59XWHkzdFmoHXHBV7oibVjGSO7ULSQ7MJ1Nz51phuDJSgAIU7A
|
||||
0zrLnOrAj/dfrlEWRhCvAgbuwLZX1A2sjNjXoPOHbsPiy+lO1KF8/XY7
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
52
certs/privateKey.examole.pem
Normal file
@@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDHR7I087VhXM5k
|
||||
aILX2XkgR50V4x7Vh/ZPWyslSv0xKkMoqITyfcBjQmr1aBSi70TxlWK7dxiMTxQS
|
||||
4/qzhb5TWJdhYzCpzqECRJ6OfyZvi9J0g7V1URb0E0yykOBdm9vBKzpRab6xSnHC
|
||||
i3iFX1GXMh1KhniestEme7s5Jb987vFTxQ08CA8dqsd7/EE89N5gqr4JwNvsclR0
|
||||
kxjEN3WhG1lCb9TPmsOup/RHgPECFQFxleCnzOfjIfWc+MhH7ZdQIHafhCU582qG
|
||||
I9r0nAPplWtQiZoUF0NNmN6cT8cdUn3iTUvMxMaAdG8Kn9q/Hl/DiViVjwnvZ4oo
|
||||
Urvt8dClRSTsXuSVn4IcOWsuL5EPKqIl7OFUmU+5deeK8Ra+utU3QRn7Wk75efXG
|
||||
oEOMnYo/5VsbNK94aDGw7lcfLYFoa6ZjHudDCGrKdeQEwoLk8K5+7zN++x3Y4NIZ
|
||||
S8iGKGCxdsVFpYRY5CIuoWrfP/gY5GefT0O8GqciwpAsYjjEOudv8liaQKznPEt7
|
||||
LWQNY62pGPNn9pXNJQzyHVQ6ah9co8aD+5wzVIwSp7movKJ+BizFn85SevHo5m3X
|
||||
Fbh5E4bGi/J6GxxoIggx4gCEpniiImFZ9niaBv216IVHKUlJP5heo338PJ1V7oDI
|
||||
hQicoBBkvk+xjfoH9AroVyRvYac4rwIDAQABAoICAFFRcWfoNxCm5VXVy+a2yJWi
|
||||
g3hl+LQbyifxxPZv1kfUvhj+Q1oMdJBMjwbbVOh0CMcoNWTYIX1H26Ilw6y0G8k4
|
||||
8nT8G+R++/bH94egXRfRj6yZ/lcEIwCwS3Dma5fnPNJjiGWmZ/lCro87iI+sKMgw
|
||||
3AEIRHpF79DrVqfoPm6FtpZ/Z3ois8BgawyuEBUGuyPpKKkkONoQgWQcjlOraeW3
|
||||
GkJhDg81UTqZMLZo6G/4EGHATi9LDykBN4+5eUjYrBE3XhCTxPkT2lkoknWUoIgV
|
||||
v/faXrRqFb25bsWMTG0rt1C8R/0kIvhSCunj90hb5aoOBsbo2p4FuzvfHu7m6UN/
|
||||
Tnj8WLvm9UNaSSJtIICkVZe602odJRpE658xFXZCuI9DZk1zRAJYB13lXOyO1U5D
|
||||
Z3IgkEy2K1QjObE1lpkP+W/TfumgLOBu3dnIWEBiWQeG9VfJmGfZ6HAtuRkLSK47
|
||||
JCmtKsApZOVlN1jUqTBFhN9vWndqZTK4h5enA0yK2TDFrEWjtSyBaHzPmXhbUIP1
|
||||
1EGkOZjQgIhZ7SBXJl6ck+L/QHwXA+LCqZuHtf+K49aoKG4A21UFuTOL63QuvtCJ
|
||||
LENuex7WMhl2LyfibGjqZJx7DnpwQgs89bA8p8Wi/Yx9eZd2ERXSe3T9e84LLUWJ
|
||||
AgjxFlf4bG3c0nUTWBppAoIBAQDj7S1oQtuvHkkPw+KUkU5LuOogdSE3iye3+jLp
|
||||
jceyaRxk4xUJlKC+TZDYmpSyEEe/2P423jgF6CiAXLY1MJTeeqMiQrV5ywYP0ZrO
|
||||
LjYgbrOHAIR/wexxjJUBQdJ3mX8bANkOhHp52NwU95Waj9EVC7kmWvhJEtb0JAZ+
|
||||
QsL/AbvUQBDAxSIjvelEB4AGdWXRd+IqZCtjf+VTolc2NKB8WNT5WdPzlRf+fh6L
|
||||
oXtVw7olX13PtrH7WZ8hgtGeSkJBKgQ3omCMV+kdD+XnkUyMDPVFG2KcGmidFu23
|
||||
aypNOpkzLoFfFS2ngCfcgD0DR1CLvMQhRVet7xk+NdF62Ab7AoIBAQDf00J0UCVx
|
||||
N2U8PSNSS7u7WLwOycEZT485XegvnmnzP9nX8I1VJWQiNgnVr4RQWpH9kvwSO02+
|
||||
J7NPPf98oUmTe+mtG0feJf92z8q9uAK/9lXsMmqFKNl35MjqYFjELzUnxlSz+zG5
|
||||
X+pyD6fXJXlXZ0Md/OODzjzp+p2A/YYIgSqNEhAjb5MGkO/7DIt6+3IJvyruDzfO
|
||||
OzmLgbc0WbjSwQlL1tceHCbVVu9LTeyFIQPvYYsA+Ei5CALL0C8BCN6yI+u/tUph
|
||||
GOz0rqA+OdjM+/GYV3zBH3EbD7jtnNBWvic0FDKMMqV40MntJqul1Bv6Nu9slCGx
|
||||
Nm15P58BHPbdAoIBAE6+uHtW7fMYcYGC2ZsegIBkyG6iSPGZoAVN6Z0LIL0g13B7
|
||||
i98dfFODFNHgxhKm0UMUwu9N4ukXhjai0UibGjOrBwVlKrGDVPrOHb+x831NAbVY
|
||||
lm5VH00zlp8ykHZFj8ZSiqsbVf0W0SJlT0hw+3lb7YG02CbW3XDHqX6hriDQBoaU
|
||||
A7W15c+XYynftXmFwcGWu4qNxPfBTgeRBLRzhiavwhTL1hBHqFyCUidHiQbeckdL
|
||||
JWwH4IHIOtQnECix2yYMUBywes7B6IXj4jgY2Oth5rMTfQQVk6MCMuq1mY3I+vjV
|
||||
zlh9RqKiAiOKIoopb0h31QLxpBMxkfUOPutEC1UCggEBANzOX/e4/UcUnBVyRv8v
|
||||
4VLwNg3ssUeT+jpgzubzQ5iKPBFQqUz/Zyps3wTkcwaGYxGiSHR/9rEKH1WkVwAP
|
||||
aTNLAfsZN6wLFluSoHLLLkNL8/Xgwr78zpT9qcu2IrvfynOjr/oibCpxWisOEMkp
|
||||
mexE3ayex6BG/EbjSzBuayTGsECdOjiLIKNQpr6m4I8Bsb21ztctQiN8v8dFv4Ow
|
||||
o6meb9pWZr+4jALZEZbbl+K58FTeiK/7QFrxcTi59zTxGCjrUO4+HdNuMI0uHL1m
|
||||
ed+3CN7+J/+pUf6dYxVeJxX731b8OeWfLSjj6ODAzoL4nmUYfthBxn85r4P25JjH
|
||||
hy0CggEAM7rgVFejJ44iNxDCAbLZMi0Xosbf7ZpXDe2cphPRMngd17XfrG/6BUxB
|
||||
tUFzfiDgG4FiM7l/X8zXMFkzM56n6RO05a/JSA4TxvsUo7ZVEJR/HD6hsOdiD16Q
|
||||
l5wcxngEx/rz84N5SjoCKitvldWOZaXGl35YMrri5kyVfgnXsQFO2S+Xewv5BLco
|
||||
UE8D7r9xWNT5VbbApPsAZ9sta/Xrl3Mserb1bMKIf0n36/0uC+GmMOAXCxw5DJps
|
||||
Cuc8bDmUVk2ktIJGsy0rDv5GwVfphBXgIVWI/JAC7eYZTJnLImDunnFastxrTBPY
|
||||
Fsu0vT7dhy5GJP6Qmrt7ogjJ9K4grQ==
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,3 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
...
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
web:
|
||||
build:
|
||||
context: ./backend/guilan-ace-backend
|
||||
dockerfile: ../Dockerfile
|
||||
dockerfile: ./backend/Dockerfile
|
||||
env_file:
|
||||
- ./backend/guilan-ace-backend/.env
|
||||
environment:
|
||||
@@ -102,7 +102,7 @@ services:
|
||||
worker:
|
||||
build:
|
||||
context: ./backend/guilan-ace-backend
|
||||
dockerfile: ../Dockerfile
|
||||
dockerfile: ./backend/Dockerfile
|
||||
env_file:
|
||||
- ./backend/guilan-ace-backend/.env
|
||||
depends_on:
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
beat:
|
||||
build:
|
||||
context: ./backend/guilan-ace-backend
|
||||
dockerfile: ../Dockerfile
|
||||
dockerfile: ./backend/Dockerfile
|
||||
env_file:
|
||||
- ./backend/guilan-ace-backend/.env
|
||||
depends_on:
|
||||
@@ -129,7 +129,7 @@ services:
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend/guilan-ace-frontend
|
||||
dockerfile: ../Dockerfile
|
||||
dockerfile: ./frontend/Dockerfile
|
||||
env_file:
|
||||
- ./frontend/guilan-ace-frontend/.env
|
||||
labels:
|
||||
|
||||
@@ -2,6 +2,8 @@ FROM node:20-alpine AS deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm config set registry https://package-mirror.liara.ir/repository/npm/ --global
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm ci
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
backup-upload-s3.sh
|
||||
|
||||
Environment:
|
||||
DEPLOY_ROOT Deployment directory. Defaults to ~/guilan-ace-deployment.
|
||||
|
||||
S3_BACKUP_ENDPOINT_URL S3 endpoint URL.
|
||||
S3_BACKUP_ACCESS_KEY_ID S3 access key.
|
||||
S3_BACKUP_SECRET_ACCESS_KEY S3 secret key.
|
||||
S3_BACKUP_BUCKET Rclone bucket/root name, e.g. c284984.
|
||||
S3_BACKUP_PREFIX Backup folder path. Defaults to backup/guilan-ace.
|
||||
S3_BACKUP_REGION S3 region. Defaults to us-east-1.
|
||||
|
||||
BACKUP_ENCRYPTION_PASSPHRASE Encryption passphrase.
|
||||
BACKUP_LOCAL_KEEP_LATEST Number of latest local plaintext backups to keep. Defaults to 3.
|
||||
BACKUP_REMOTE_KEEP_LATEST Number of latest remote encrypted backups to keep. Defaults to 7.
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[backup-rclone] %s\n' "$*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '[backup-rclone] %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_var() {
|
||||
local name="$1"
|
||||
[[ -n "${!name:-}" ]] || fail "$name is required"
|
||||
}
|
||||
|
||||
load_env() {
|
||||
local env_path="$1"
|
||||
|
||||
[[ -f "$env_path" ]] || fail "Deployment env file not found: $env_path"
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "$env_path"
|
||||
set +a
|
||||
}
|
||||
|
||||
normalize_path() {
|
||||
local value="${1:-backup/guilan-ace}"
|
||||
|
||||
value="${value#/}"
|
||||
value="${value%/}"
|
||||
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
positive_integer_or_default() {
|
||||
local value="${1:-}"
|
||||
local fallback="$2"
|
||||
|
||||
if [[ "$value" =~ ^[0-9]+$ ]]; then
|
||||
printf '%s' "$value"
|
||||
else
|
||||
printf '%s' "$fallback"
|
||||
fi
|
||||
}
|
||||
|
||||
rclone_remote_path() {
|
||||
printf 'parspack:%s/%s' "$S3_BACKUP_BUCKET" "$S3_BACKUP_PREFIX"
|
||||
}
|
||||
|
||||
cleanup_local_backups() {
|
||||
local keep_count="$1"
|
||||
local latest_dir="$2"
|
||||
|
||||
[[ "$keep_count" -gt 0 ]] || {
|
||||
rm -rf "$latest_dir"
|
||||
return
|
||||
}
|
||||
|
||||
mkdir -p "$latest_dir"
|
||||
find "$latest_dir" -maxdepth 1 -type f -name '*.tar.gz.enc' -delete
|
||||
cp "$ARCHIVE_PATH" "$latest_dir/$ARCHIVE_NAME"
|
||||
|
||||
find "$latest_dir" -maxdepth 1 -type f -name '*.tar.gz' -printf '%T@ %p\n' \
|
||||
| sort -nr \
|
||||
| awk -v keep="$keep_count" 'NR > keep {print $2}' \
|
||||
| xargs -r rm -f
|
||||
|
||||
log "Kept latest $keep_count plaintext backup(s) locally in: $latest_dir"
|
||||
}
|
||||
|
||||
cleanup_remote_backups() {
|
||||
local keep_count="$1"
|
||||
local remote_path="$2"
|
||||
local stale_files
|
||||
|
||||
[[ "$keep_count" -gt 0 ]] || {
|
||||
log "Skipping remote cleanup because BACKUP_REMOTE_KEEP_LATEST is $keep_count"
|
||||
return
|
||||
}
|
||||
|
||||
stale_files="$(
|
||||
rclone lsf "$remote_path" \
|
||||
--config "$RCLONE_CONFIG" \
|
||||
--s3-no-check-bucket \
|
||||
--files-only \
|
||||
| grep '\.tar\.gz\.enc$' \
|
||||
| sort -r \
|
||||
| awk -v keep="$keep_count" 'NR > keep' \
|
||||
|| true
|
||||
)"
|
||||
|
||||
if [[ -z "$stale_files" ]]; then
|
||||
log "No remote backups need cleanup"
|
||||
return
|
||||
fi
|
||||
|
||||
while IFS= read -r stale_file; do
|
||||
[[ -n "$stale_file" ]] || continue
|
||||
log "Deleting old remote backup: $remote_path/$stale_file"
|
||||
rclone deletefile "$remote_path/$stale_file" \
|
||||
--config "$RCLONE_CONFIG" \
|
||||
--s3-no-check-bucket
|
||||
done <<< "$stale_files"
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
command -v rclone >/dev/null 2>&1 || fail "rclone is required"
|
||||
command -v openssl >/dev/null 2>&1 || fail "openssl is required"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEPLOY_ROOT="${DEPLOY_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
||||
|
||||
DEPLOY_ENV="$DEPLOY_ROOT/.env"
|
||||
BACKUP_SCRIPT="$SCRIPT_DIR/backup.sh"
|
||||
|
||||
[[ -x "$BACKUP_SCRIPT" ]] || fail "Backup script is not executable: $BACKUP_SCRIPT"
|
||||
|
||||
load_env "$DEPLOY_ENV"
|
||||
|
||||
require_var S3_BACKUP_ENDPOINT_URL
|
||||
require_var S3_BACKUP_ACCESS_KEY_ID
|
||||
require_var S3_BACKUP_SECRET_ACCESS_KEY
|
||||
require_var S3_BACKUP_BUCKET
|
||||
require_var BACKUP_ENCRYPTION_PASSPHRASE
|
||||
|
||||
S3_BACKUP_PREFIX="$(normalize_path "${S3_BACKUP_PREFIX:-backup/guilan-ace}")"
|
||||
BACKUP_LOCAL_KEEP_LATEST="$(positive_integer_or_default "${BACKUP_LOCAL_KEEP_LATEST:-3}" 3)"
|
||||
BACKUP_REMOTE_KEEP_LATEST="$(positive_integer_or_default "${BACKUP_REMOTE_KEEP_LATEST:-7}" 7)"
|
||||
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
PLAIN_DIR="$WORK_DIR/plain"
|
||||
mkdir -p "$PLAIN_DIR"
|
||||
|
||||
log "Creating local backup archive"
|
||||
|
||||
DEPLOY_ROOT="$DEPLOY_ROOT" "$BACKUP_SCRIPT" "$PLAIN_DIR"
|
||||
|
||||
shopt -s nullglob
|
||||
archives=("$PLAIN_DIR"/*.tar.gz)
|
||||
shopt -u nullglob
|
||||
|
||||
[[ "${#archives[@]}" -eq 1 ]] || fail "Expected exactly one backup archive, found ${#archives[@]}"
|
||||
|
||||
ARCHIVE_PATH="${archives[0]}"
|
||||
ARCHIVE_NAME="$(basename "$ARCHIVE_PATH")"
|
||||
|
||||
ENCRYPTED_NAME="$ARCHIVE_NAME.enc"
|
||||
ENCRYPTED_PATH="$WORK_DIR/$ENCRYPTED_NAME"
|
||||
|
||||
log "Encrypting backup archive"
|
||||
|
||||
openssl enc -aes-256-cbc -salt -pbkdf2 -iter 200000 \
|
||||
-in "$ARCHIVE_PATH" \
|
||||
-out "$ENCRYPTED_PATH" \
|
||||
-pass env:BACKUP_ENCRYPTION_PASSPHRASE
|
||||
|
||||
RCLONE_CONFIG="$WORK_DIR/rclone.conf"
|
||||
|
||||
cat > "$RCLONE_CONFIG" <<EOF
|
||||
[parspack]
|
||||
type = s3
|
||||
provider = Other
|
||||
access_key_id = $S3_BACKUP_ACCESS_KEY_ID
|
||||
secret_access_key = $S3_BACKUP_SECRET_ACCESS_KEY
|
||||
endpoint = $S3_BACKUP_ENDPOINT_URL
|
||||
region = ${S3_BACKUP_REGION:-us-east-1}
|
||||
acl = private
|
||||
force_path_style = true
|
||||
EOF
|
||||
|
||||
REMOTE_PATH="$(rclone_remote_path)"
|
||||
|
||||
log "Uploading encrypted backup to $REMOTE_PATH/$ENCRYPTED_NAME"
|
||||
|
||||
rclone copy \
|
||||
"$ENCRYPTED_PATH" \
|
||||
"$REMOTE_PATH" \
|
||||
--config "$RCLONE_CONFIG" \
|
||||
--s3-no-check-bucket \
|
||||
--progress
|
||||
|
||||
cleanup_local_backups "$BACKUP_LOCAL_KEEP_LATEST" "$DEPLOY_ROOT/backups/latest"
|
||||
cleanup_remote_backups "$BACKUP_REMOTE_KEEP_LATEST" "$REMOTE_PATH"
|
||||
|
||||
log "Backup upload completed successfully"
|
||||
@@ -1,183 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
backup.sh [output-directory]
|
||||
|
||||
Environment:
|
||||
DEPLOY_ROOT Deployment directory. Defaults to ~/guilan-ace-deployment.
|
||||
|
||||
Creates a timestamped .tar.gz archive containing:
|
||||
- PostgreSQL dump from the db service
|
||||
- media files from the Django media volume
|
||||
- deployment, backend, and frontend .env files when present
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[backup] %s\n' "$*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '[backup] %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_file() {
|
||||
local path="$1"
|
||||
[[ -f "$path" ]] || fail "Required file not found: $path"
|
||||
}
|
||||
|
||||
compose() {
|
||||
docker compose --env-file "$DEPLOY_ENV" -f "$DEPLOY_ROOT/docker-compose.yml" "$@"
|
||||
}
|
||||
|
||||
wait_for_db() {
|
||||
compose exec -T db sh -c '
|
||||
set -eu
|
||||
: "${DB_USER:?DB_USER is missing}"
|
||||
: "${DB_NAME:?DB_NAME is missing}"
|
||||
until pg_isready --username="$DB_USER" --dbname="$DB_NAME" --host=127.0.0.1; do
|
||||
sleep 1
|
||||
done
|
||||
' >/dev/null
|
||||
}
|
||||
|
||||
copy_env_if_exists() {
|
||||
local source_path="$1"
|
||||
local target_path="$2"
|
||||
|
||||
if [[ -f "$source_path" ]]; then
|
||||
cp "$source_path" "$target_path"
|
||||
fi
|
||||
}
|
||||
|
||||
env_value() {
|
||||
local key="$1"
|
||||
local file="$2"
|
||||
|
||||
awk -v key="$key" '
|
||||
$0 ~ "^[[:space:]]*" key "=" {
|
||||
sub(/^[^=]*=/, "")
|
||||
print
|
||||
exit
|
||||
}
|
||||
' "$file"
|
||||
}
|
||||
|
||||
set_env_value() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local file="$3"
|
||||
local tmp_path
|
||||
|
||||
tmp_path="$(mktemp)"
|
||||
awk -v key="$key" -v value="$value" '
|
||||
$0 ~ "^[[:space:]]*" key "=" {
|
||||
print key "=" value
|
||||
found = 1
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (!found) {
|
||||
print key "=" value
|
||||
}
|
||||
}
|
||||
' "$file" > "$tmp_path"
|
||||
mv "$tmp_path" "$file"
|
||||
chmod 600 "$file" || true
|
||||
}
|
||||
|
||||
sync_postgres_env_aliases() {
|
||||
local db_name db_user db_password
|
||||
|
||||
db_name="$(env_value DB_NAME "$BACKEND_ENV")"
|
||||
db_user="$(env_value DB_USER "$BACKEND_ENV")"
|
||||
db_password="$(env_value DB_PASSWORD "$BACKEND_ENV")"
|
||||
|
||||
[[ -n "$db_name" ]] || fail "DB_NAME is missing or empty in $BACKEND_ENV"
|
||||
[[ -n "$db_user" ]] || fail "DB_USER is missing or empty in $BACKEND_ENV"
|
||||
[[ -n "$db_password" ]] || fail "DB_PASSWORD is missing or empty in $BACKEND_ENV"
|
||||
|
||||
set_env_value POSTGRES_DB "$db_name" "$BACKEND_ENV"
|
||||
set_env_value POSTGRES_USER "$db_user" "$BACKEND_ENV"
|
||||
set_env_value POSTGRES_PASSWORD "$db_password" "$BACKEND_ENV"
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/guilan-ace-deployment}"
|
||||
OUTPUT_DIR="${1:-$DEPLOY_ROOT/backups}"
|
||||
BACKEND_ENV="$DEPLOY_ROOT/backend/guilan-ace-backend/.env"
|
||||
FRONTEND_ENV="$DEPLOY_ROOT/frontend/guilan-ace-frontend/.env"
|
||||
DEPLOY_ENV="$DEPLOY_ROOT/.env"
|
||||
|
||||
require_file "$DEPLOY_ROOT/docker-compose.yml"
|
||||
require_file "$DEPLOY_ENV"
|
||||
require_file "$BACKEND_ENV"
|
||||
|
||||
log "Ensuring PostgreSQL environment aliases exist"
|
||||
sync_postgres_env_aliases
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
TIMESTAMP="$(date -u +'%Y%m%d-%H%M%S')"
|
||||
ARCHIVE_NAME="guilan-ace-backup-$TIMESTAMP.tar.gz"
|
||||
ARCHIVE_PATH="$OUTPUT_DIR/$ARCHIVE_NAME"
|
||||
|
||||
mkdir -p "$WORK_DIR/env"
|
||||
|
||||
log "Checking Docker Compose configuration"
|
||||
compose config -q
|
||||
|
||||
log "Starting database service if needed"
|
||||
compose up -d db
|
||||
wait_for_db
|
||||
|
||||
log "Dumping database from db service"
|
||||
compose exec -T db sh -c '
|
||||
set -eu
|
||||
: "${DB_USER:?DB_USER is missing}"
|
||||
: "${DB_NAME:?DB_NAME is missing}"
|
||||
pg_dump \
|
||||
--username="$DB_USER" \
|
||||
--dbname="$DB_NAME" \
|
||||
--format=plain \
|
||||
--clean \
|
||||
--if-exists \
|
||||
--no-owner \
|
||||
--no-privileges
|
||||
' > "$WORK_DIR/database.sql"
|
||||
|
||||
log "Archiving media files from Django media volume"
|
||||
if compose ps --status running --services | grep -qx 'web'; then
|
||||
compose exec -T web sh -c 'mkdir -p /app/media && tar -C /app/media -czf - .' > "$WORK_DIR/media.tar.gz"
|
||||
else
|
||||
log "Web service is not running; using a temporary container for media volume"
|
||||
compose run --rm --no-deps --entrypoint sh web -c 'mkdir -p /app/media && tar -C /app/media -czf - .' > "$WORK_DIR/media.tar.gz"
|
||||
fi
|
||||
|
||||
log "Copying environment files"
|
||||
copy_env_if_exists "$DEPLOY_ENV" "$WORK_DIR/env/deployment.env"
|
||||
copy_env_if_exists "$BACKEND_ENV" "$WORK_DIR/env/backend.env"
|
||||
copy_env_if_exists "$FRONTEND_ENV" "$WORK_DIR/env/frontend.env"
|
||||
|
||||
cat > "$WORK_DIR/manifest.txt" <<EOF
|
||||
name=$ARCHIVE_NAME
|
||||
created_at_utc=$TIMESTAMP
|
||||
deploy_root=$DEPLOY_ROOT
|
||||
includes=database.sql,media.tar.gz,env/deployment.env,env/backend.env,env/frontend.env
|
||||
EOF
|
||||
|
||||
log "Creating archive $ARCHIVE_PATH"
|
||||
tar -C "$WORK_DIR" -czf "$ARCHIVE_PATH" .
|
||||
|
||||
log "Backup completed: $ARCHIVE_PATH"
|
||||
@@ -1,153 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
restore-from-s3.sh latest
|
||||
restore-from-s3.sh <s3-object-key>
|
||||
|
||||
Examples:
|
||||
restore-from-s3.sh latest
|
||||
restore-from-s3.sh guilan-ace-backup-YYYYMMDD-HHMMSS.tar.gz.enc
|
||||
|
||||
Environment:
|
||||
DEPLOY_ROOT Deployment directory. Defaults to ~/guilan-ace-deployment.
|
||||
S3_BACKUP_BUCKET Rclone bucket/root name, e.g. c284984.
|
||||
S3_BACKUP_PREFIX Backup folder path. Defaults to backup/guilan-ace.
|
||||
S3_BACKUP_ENDPOINT_URL S3-compatible endpoint URL.
|
||||
S3_BACKUP_ACCESS_KEY_ID S3 access key.
|
||||
S3_BACKUP_SECRET_ACCESS_KEY S3 secret key.
|
||||
S3_BACKUP_REGION S3 region. Defaults to us-east-1.
|
||||
BACKUP_ENCRYPTION_PASSPHRASE Passphrase used to decrypt backup archives.
|
||||
|
||||
Existing RESTORE_SKIP_DB, RESTORE_SKIP_MEDIA, and RESTORE_SKIP_ENV flags are
|
||||
passed through to restore.sh.
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[restore-rclone] %s\n' "$*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '[restore-rclone] %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_var() {
|
||||
local name="$1"
|
||||
[[ -n "${!name:-}" ]] || fail "$name is required"
|
||||
}
|
||||
|
||||
load_env() {
|
||||
local env_path="$1"
|
||||
[[ -f "$env_path" ]] || fail "Deployment env file not found: $env_path"
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "$env_path"
|
||||
set +a
|
||||
}
|
||||
|
||||
normalize_path() {
|
||||
local value="${1:-backup/guilan-ace}"
|
||||
value="${value#/}"
|
||||
value="${value%/}"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
rclone_remote_path() {
|
||||
printf 'parspack:%s/%s' "$S3_BACKUP_BUCKET" "$S3_BACKUP_PREFIX"
|
||||
}
|
||||
|
||||
latest_object_name() {
|
||||
rclone lsf "$REMOTE_PATH" \
|
||||
--config "$RCLONE_CONFIG" \
|
||||
--s3-no-check-bucket \
|
||||
--files-only \
|
||||
| grep '\.tar\.gz\.enc$' \
|
||||
| sort \
|
||||
| tail -n 1 \
|
||||
|| true
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
REQUESTED_KEY="${1:-}"
|
||||
[[ -n "$REQUESTED_KEY" ]] || {
|
||||
usage
|
||||
exit 1
|
||||
}
|
||||
|
||||
command -v rclone >/dev/null 2>&1 || fail "rclone is required"
|
||||
command -v openssl >/dev/null 2>&1 || fail "openssl is required"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEPLOY_ROOT="${DEPLOY_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
||||
DEPLOY_ENV="$DEPLOY_ROOT/.env"
|
||||
RESTORE_SCRIPT="$SCRIPT_DIR/restore.sh"
|
||||
|
||||
[[ -x "$RESTORE_SCRIPT" ]] || fail "Restore script is not executable: $RESTORE_SCRIPT"
|
||||
load_env "$DEPLOY_ENV"
|
||||
|
||||
S3_BACKUP_PREFIX="$(normalize_path "${S3_BACKUP_PREFIX:-backup/guilan-ace}")"
|
||||
|
||||
require_var S3_BACKUP_BUCKET
|
||||
require_var S3_BACKUP_ENDPOINT_URL
|
||||
require_var S3_BACKUP_ACCESS_KEY_ID
|
||||
require_var S3_BACKUP_SECRET_ACCESS_KEY
|
||||
require_var BACKUP_ENCRYPTION_PASSPHRASE
|
||||
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
RCLONE_CONFIG="$WORK_DIR/rclone.conf"
|
||||
|
||||
cat > "$RCLONE_CONFIG" <<EOF
|
||||
[parspack]
|
||||
type = s3
|
||||
provider = Other
|
||||
access_key_id = $S3_BACKUP_ACCESS_KEY_ID
|
||||
secret_access_key = $S3_BACKUP_SECRET_ACCESS_KEY
|
||||
endpoint = $S3_BACKUP_ENDPOINT_URL
|
||||
region = ${S3_BACKUP_REGION:-us-east-1}
|
||||
acl = private
|
||||
force_path_style = true
|
||||
EOF
|
||||
|
||||
REMOTE_PATH="$(rclone_remote_path)"
|
||||
|
||||
if [[ "$REQUESTED_KEY" == "latest" ]]; then
|
||||
log "Resolving latest encrypted backup object"
|
||||
OBJECT_NAME="$(latest_object_name)"
|
||||
[[ -n "$OBJECT_NAME" ]] || fail "No encrypted backups found in $REMOTE_PATH"
|
||||
else
|
||||
OBJECT_KEY="${REQUESTED_KEY#parspack:$S3_BACKUP_BUCKET/}"
|
||||
OBJECT_KEY="${OBJECT_KEY#s3://$S3_BACKUP_BUCKET/}"
|
||||
OBJECT_KEY="${OBJECT_KEY#$S3_BACKUP_PREFIX/}"
|
||||
OBJECT_NAME="${OBJECT_KEY##*/}"
|
||||
fi
|
||||
|
||||
ENCRYPTED_PATH="$WORK_DIR/$OBJECT_NAME"
|
||||
DECRYPTED_PATH="$WORK_DIR/${ENCRYPTED_PATH##*/}"
|
||||
DECRYPTED_PATH="${DECRYPTED_PATH%.enc}"
|
||||
|
||||
log "Downloading encrypted backup from $REMOTE_PATH/$OBJECT_NAME"
|
||||
rclone copyto "$REMOTE_PATH/$OBJECT_NAME" "$ENCRYPTED_PATH" \
|
||||
--config "$RCLONE_CONFIG" \
|
||||
--s3-no-check-bucket \
|
||||
--progress
|
||||
|
||||
log "Decrypting backup archive"
|
||||
openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \
|
||||
-in "$ENCRYPTED_PATH" \
|
||||
-out "$DECRYPTED_PATH" \
|
||||
-pass env:BACKUP_ENCRYPTION_PASSPHRASE
|
||||
|
||||
log "Restoring decrypted backup archive"
|
||||
DEPLOY_ROOT="$DEPLOY_ROOT" "$RESTORE_SCRIPT" "$DECRYPTED_PATH"
|
||||
|
||||
log "S3 restore completed from: $REMOTE_PATH/$OBJECT_NAME"
|
||||
@@ -1,191 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
restore.sh <backup-archive.tar.gz>
|
||||
|
||||
Environment:
|
||||
DEPLOY_ROOT Deployment directory. Defaults to ~/guilan-ace-deployment.
|
||||
RESTORE_SKIP_ENV Set to 1 to keep current .env files.
|
||||
RESTORE_SKIP_MEDIA Set to 1 to keep current media files.
|
||||
RESTORE_SKIP_DB Set to 1 to keep current database.
|
||||
|
||||
This script restores:
|
||||
- deployment, backend, and frontend .env files
|
||||
- media files into the Django media volume
|
||||
- PostgreSQL database from database.sql
|
||||
|
||||
Database restore is destructive unless RESTORE_SKIP_DB=1 is set.
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[restore] %s\n' "$*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '[restore] %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_file() {
|
||||
local path="$1"
|
||||
[[ -f "$path" ]] || fail "Required file not found: $path"
|
||||
}
|
||||
|
||||
compose() {
|
||||
docker compose --env-file "$DEPLOY_ENV" -f "$DEPLOY_ROOT/docker-compose.yml" "$@"
|
||||
}
|
||||
|
||||
wait_for_db() {
|
||||
compose exec -T db sh -c '
|
||||
set -eu
|
||||
: "${DB_USER:?DB_USER is missing}"
|
||||
: "${DB_NAME:?DB_NAME is missing}"
|
||||
until pg_isready --username="$DB_USER" --dbname="$DB_NAME" --host=127.0.0.1; do
|
||||
sleep 1
|
||||
done
|
||||
' >/dev/null
|
||||
}
|
||||
|
||||
restore_env_if_present() {
|
||||
local source_path="$1"
|
||||
local target_path="$2"
|
||||
|
||||
if [[ -f "$source_path" ]]; then
|
||||
mkdir -p "$(dirname "$target_path")"
|
||||
cp "$source_path" "$target_path"
|
||||
chmod 600 "$target_path" || true
|
||||
log "Restored $target_path"
|
||||
fi
|
||||
}
|
||||
|
||||
env_value() {
|
||||
local key="$1"
|
||||
local file="$2"
|
||||
|
||||
awk -v key="$key" '
|
||||
$0 ~ "^[[:space:]]*" key "=" {
|
||||
sub(/^[^=]*=/, "")
|
||||
print
|
||||
exit
|
||||
}
|
||||
' "$file"
|
||||
}
|
||||
|
||||
set_env_value() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local file="$3"
|
||||
local tmp_path
|
||||
|
||||
tmp_path="$(mktemp)"
|
||||
awk -v key="$key" -v value="$value" '
|
||||
$0 ~ "^[[:space:]]*" key "=" {
|
||||
print key "=" value
|
||||
found = 1
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (!found) {
|
||||
print key "=" value
|
||||
}
|
||||
}
|
||||
' "$file" > "$tmp_path"
|
||||
mv "$tmp_path" "$file"
|
||||
chmod 600 "$file" || true
|
||||
}
|
||||
|
||||
sync_postgres_env_aliases() {
|
||||
local db_name db_user db_password
|
||||
|
||||
db_name="$(env_value DB_NAME "$BACKEND_ENV")"
|
||||
db_user="$(env_value DB_USER "$BACKEND_ENV")"
|
||||
db_password="$(env_value DB_PASSWORD "$BACKEND_ENV")"
|
||||
|
||||
[[ -n "$db_name" ]] || fail "DB_NAME is missing or empty in $BACKEND_ENV"
|
||||
[[ -n "$db_user" ]] || fail "DB_USER is missing or empty in $BACKEND_ENV"
|
||||
[[ -n "$db_password" ]] || fail "DB_PASSWORD is missing or empty in $BACKEND_ENV"
|
||||
|
||||
set_env_value POSTGRES_DB "$db_name" "$BACKEND_ENV"
|
||||
set_env_value POSTGRES_USER "$db_user" "$BACKEND_ENV"
|
||||
set_env_value POSTGRES_PASSWORD "$db_password" "$BACKEND_ENV"
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ARCHIVE_PATH="${1:-}"
|
||||
[[ -n "$ARCHIVE_PATH" ]] || {
|
||||
usage
|
||||
exit 1
|
||||
}
|
||||
|
||||
DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/guilan-ace-deployment}"
|
||||
BACKEND_ENV="$DEPLOY_ROOT/backend/guilan-ace-backend/.env"
|
||||
FRONTEND_ENV="$DEPLOY_ROOT/frontend/guilan-ace-frontend/.env"
|
||||
DEPLOY_ENV="$DEPLOY_ROOT/.env"
|
||||
|
||||
require_file "$ARCHIVE_PATH"
|
||||
require_file "$DEPLOY_ROOT/docker-compose.yml"
|
||||
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
log "Extracting backup archive"
|
||||
tar -C "$WORK_DIR" -xzf "$ARCHIVE_PATH"
|
||||
|
||||
if [[ "${RESTORE_SKIP_ENV:-0}" != "1" ]]; then
|
||||
log "Restoring environment files"
|
||||
restore_env_if_present "$WORK_DIR/env/deployment.env" "$DEPLOY_ENV"
|
||||
restore_env_if_present "$WORK_DIR/env/backend.env" "$BACKEND_ENV"
|
||||
restore_env_if_present "$WORK_DIR/env/frontend.env" "$FRONTEND_ENV"
|
||||
else
|
||||
log "Skipping environment restore"
|
||||
fi
|
||||
|
||||
require_file "$BACKEND_ENV"
|
||||
require_file "$DEPLOY_ENV"
|
||||
|
||||
log "Ensuring PostgreSQL environment aliases exist"
|
||||
sync_postgres_env_aliases
|
||||
|
||||
log "Checking Docker Compose configuration"
|
||||
compose config -q
|
||||
|
||||
if [[ "${RESTORE_SKIP_MEDIA:-0}" != "1" ]]; then
|
||||
require_file "$WORK_DIR/media.tar.gz"
|
||||
log "Restoring media files into Django media volume"
|
||||
compose run --rm --no-deps --entrypoint sh web -c 'rm -rf /app/media/* /app/media/.[!.]* /app/media/..?* 2>/dev/null || true'
|
||||
compose run --rm --no-deps -T --entrypoint sh web -c 'mkdir -p /app/media && tar -C /app/media -xzf -' < "$WORK_DIR/media.tar.gz"
|
||||
else
|
||||
log "Skipping media restore"
|
||||
fi
|
||||
|
||||
if [[ "${RESTORE_SKIP_DB:-0}" != "1" ]]; then
|
||||
require_file "$WORK_DIR/database.sql"
|
||||
log "Starting database service"
|
||||
compose up -d db
|
||||
wait_for_db
|
||||
|
||||
log "Recreating and restoring database"
|
||||
compose exec -T db sh -c '
|
||||
set -eu
|
||||
: "${DB_USER:?DB_USER is missing}"
|
||||
: "${DB_NAME:?DB_NAME is missing}"
|
||||
psql --username="$DB_USER" --dbname=postgres --command="SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '\''$DB_NAME'\'' AND pid <> pg_backend_pid();" >/dev/null
|
||||
dropdb --username="$DB_USER" --if-exists "$DB_NAME"
|
||||
createdb --username="$DB_USER" "$DB_NAME"
|
||||
psql --username="$DB_USER" --dbname="$DB_NAME"
|
||||
' < "$WORK_DIR/database.sql"
|
||||
else
|
||||
log "Skipping database restore"
|
||||
fi
|
||||
|
||||
log "Restore completed"
|
||||
log "Run: docker compose up -d --build"
|
||||
@@ -1,44 +0,0 @@
|
||||
# Bundle 0 flow, accessibility and performance baseline
|
||||
|
||||
Captured 2026-09-24 with the current Next.js 15 frontend, Playwright Chromium, and local production build. Viewports: 320×800, 390×844, 768×900 and 1440×900 CSS pixels. The backend at `127.0.0.1:8000` was not running for this initial capture; a backend-backed replay followed on 2026-09-25 below. Screenshots in [`evidence/2026-09-24/`](evidence/2026-09-24/) are full-page browser captures, not design comps.
|
||||
|
||||
## Route and state map
|
||||
|
||||
| Journey | Current route/transition | States to preserve and replay |
|
||||
| --- | --- | --- |
|
||||
| Visitor/association | `/` → `/about`, `/events`, `/blog` | Home/About render and responsive shell; external contact links |
|
||||
| Auth | `/auth` mobile lookup → existing user password or OTP login; new user OTP → registration details → profile/return; `/auth/google/callback`; `/verify-email/[token]`; `/reset-password` → `/reset-password/[token]`; `/logout` | Invalid mobile, expired/incorrect OTP, resend cooldown, duplicate submit, Google failure, expired reset, protected redirect |
|
||||
| Event | `/events` search/filter → `/events/[slug]` → free registration `/events/[slug]/success` or paid gateway → `/payments/result` | Open, full, closed, already registered, ineligible, invalid coupon, payment pending/success/failure/canceled, callback replay |
|
||||
| Blog | `/blog` banners/search/category/tag/author → `/blog/[slug]` | Empty/failure, long article, media fallback, comments/interactions and share |
|
||||
| Staff | `/admin` → dashboard, events, users, taxonomy, coupons, authorizations, blog editor/review/assets | Anonymous redirect, role denial, save conflict, validation, successful edit/publish, mobile table/form navigation |
|
||||
|
||||
## Browser evidence
|
||||
|
||||
| Page | Captures | Observed behavior |
|
||||
| --- | --- | --- |
|
||||
| Home | `home-320.png`, `home-390.png`, `home-768.png`, `home-1440.png` | Loads with current identity/CTA. At 390px the fixed mobile navigation overlays the first offering card while scrolling; record for later shell work. |
|
||||
| About | `about-320.png`, `about-390.png`, `about-768.png`, `about-1440.png` | Loads with static statistics, affiliations and contacts. The long page contains no `<main>` landmark. |
|
||||
| Auth | `auth-390.png` | Mobile entry screen loads. OTP/login success requires a working auth backend and test account. |
|
||||
| Events | `events-390.png` | Listing displays `رویدادی یافت نشد` when its API request fails, which looks like a real empty catalog. Browser console has `ERR_CONNECTION_REFUSED` to `127.0.0.1:8000/api/events/`. Registration states cannot be replayed from this environment. |
|
||||
| Blog | `blog-390.png` | Listing shell loads; API requests fail and a mock banner is visible. Post navigation and interactions need backend fixtures. |
|
||||
| Staff | `admin-390.png` | Anonymous `/admin` redirects to `/`. Authorized staff walkthrough requires a test account. |
|
||||
|
||||
Screenshots were inspected for page identity and nonblank content. The route screenshots listed above are the accepted current-site baseline; screenshots of rejected concept drafts are not part of that baseline.
|
||||
|
||||
## Measured baseline
|
||||
|
||||
`npm run build` passed on 2026-09-24. Its First Load JS report (production build, compressed route estimate): shared **102 kB**, Home **107 kB**, About **103 kB**, Auth **163 kB**, Events listing **124 kB**, Event detail **469 kB**, Blog listing **141 kB**, Blog detail **469 kB**, Admin dashboard **302 kB**, Admin event editor **727 kB**, Payment result **613 kB**. The build emitted existing `no-img-element` warnings and an ESLint plugin detection warning; it also logged API connection refusal during static page generation.
|
||||
|
||||
`npm run lint` exited successfully with 14 existing `no-img-element` warnings and no errors. A focused backend baseline command, `.venv/Scripts/python.exe manage.py test apps.users.tests apps.events.tests apps.payments.tests --settings=config.settings.test`, was started and stopped after repeated failures reaching the notification store at `127.0.0.1:6379`. Docker Desktop's engine was unavailable, so Redis could not be started locally. This test run has **no passing-suite result**; rerun it with an isolated Redis service for the flow gate.
|
||||
|
||||
At 390px in one local Chromium production run, Home navigation duration was **554 ms** and first contentful paint **424 ms**; About was **173 ms** and **204 ms**. These are local machine observations, not network performance targets or p75 Web Vitals. Both pages had one `<h1>`, no horizontal overflow, and zero `<img>` elements missing an `alt` attribute. Home had one `<main>` landmark; About had none. The desktop and 320px screenshots establish layout comparison points. No automated WCAG score is claimed; a full axe/keyboard/screen-reader audit remains part of the release gate.
|
||||
|
||||
## Manual replay status and next gate
|
||||
|
||||
At the initial capture, completed: Home/About rendering at four widths; auth entry; unauthenticated admin redirect; event API failure display; blog shell with backend unavailable. The backend-backed journeys were pending at that point. Do not infer a business-rule change from the initial frontend-only failure state.
|
||||
|
||||
## Follow-up with Redis and isolated backend, 2026-09-25
|
||||
|
||||
The owner started a Redis container on port 6379. The local backend and frontend then ran against an isolated SQLite test database and disposable fixtures. See the [local journey replay](evidence/2026-09-24/local-replay.md) for auth, free/full/paid event, blog, staff, and failure-state evidence. The event integration suite passed 25 tests. The broader users/events/payments test run executed 148 tests with 89 errors from stale test fixtures/patch targets; its [raw log](evidence/2026-09-24/backend-focused-tests.txt) is preserved. `manage.py check` passed.
|
||||
|
||||
The core browser flows are now captured. External SMS delivery, Google OAuth, and a live payment gateway were deliberately replaced with safe local mocks or left for staging. The baseline also found a countdown hydration mismatch and a notification stream-token 405. The owner accepted this baseline with its then-current limitations in `bundle0-review.md` on 2026-09-26, closing T02 and Checkpoint A at discovery scope. On 2026-09-26, the repository's current pytest suites passed: 79 unit and 158 integration tests. See [the rerun record](evidence/2026-09-24/test-rerun-2026-09-26.md). The original 148-test/89-error run remains historical evidence; the external SMS, OAuth, and live gateway paths remain untested.
|
||||
@@ -1,52 +0,0 @@
|
||||
# Rebuild the line-level source ledger for the static public-content files.
|
||||
# Run from any directory: pwsh -File tasks/build-content-line-ledger.ps1
|
||||
$frontend = (Resolve-Path (Join-Path $PSScriptRoot '../../guilan-ace-frontend')).Path
|
||||
$sources = @(
|
||||
'src/views/Home.tsx',
|
||||
'src/views/AboutUs.tsx',
|
||||
'src/components/Navbar.tsx',
|
||||
'src/components/Footer.tsx',
|
||||
'src/components/MobileBottomNav.tsx',
|
||||
'src/app/page.tsx',
|
||||
'src/app/about/page.tsx',
|
||||
'src/app/layout.tsx',
|
||||
'src/app/events/page.tsx',
|
||||
'src/app/blog/page.tsx'
|
||||
)
|
||||
|
||||
$rows = foreach ($source in $sources) {
|
||||
$lineNumber = 0
|
||||
foreach ($line in [System.IO.File]::ReadAllLines((Join-Path $frontend $source))) {
|
||||
$lineNumber++
|
||||
if ($line -notmatch '[\u0600-\u06FF]|https?://|favicon\.ico|enamad\.png') { continue }
|
||||
|
||||
$category = 'CMS editorial'
|
||||
$note = ''
|
||||
if ($line -match 'schema\.org|contactType|availableLanguage') {
|
||||
$category = 'code-owned UI'
|
||||
$note = 'Schema structure or vocabulary; referenced identity values remain editorial.'
|
||||
} elseif ($source -match 'Navbar|MobileBottomNav') {
|
||||
if ($line -match 'auth|profile|reset-password|logout|admin|ورود|ثبتنام|حساب|داشبورد|پروفایل|رمز|خروج|منوی') {
|
||||
$category = 'code-owned UI'
|
||||
}
|
||||
} elseif ($source -eq 'src/views/Home.tsx') {
|
||||
if ($line -match 'aria-label|<Button|<Link' -or $lineNumber -in @(55, 60, 121, 122)) { $category = 'code-owned UI' }
|
||||
} elseif ($source -match 'src/app/(events|blog)/page.tsx') {
|
||||
if ($line -match 'search|جست|robots') {
|
||||
$category = 'code-owned UI'
|
||||
$note = 'Query-specific title/description or search behavior; default listing introduction remains editorial.'
|
||||
}
|
||||
}
|
||||
[pscustomobject]@{
|
||||
source = $source
|
||||
line = $lineNumber
|
||||
category = $category
|
||||
source_text = $line.Trim()
|
||||
note = $note
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$output = Join-Path $PSScriptRoot 'content-source-lines.csv'
|
||||
$rows | Export-Csv -LiteralPath $output -NoTypeInformation -Encoding utf8
|
||||
Write-Output "$($rows.Count) source lines written to $output"
|
||||
@@ -1,44 +0,0 @@
|
||||
# Bundle 0 review for the association owner
|
||||
|
||||
Prepared 2026-09-25; revised after the owner rejected three earlier design concepts and gave three visual references plus the Home structure. This page collects the decisions needed to close Checkpoints A and B in [plan.md](plan.md). It does not authorize a production seed or change any event/payment rule.
|
||||
|
||||
**Owner approval recorded 2026-09-26:** the owner read and approved this review, including the content ownership and exclusions, the local baseline with its stated test/external-integration limits, and the CMS/API decisions below. T01, T02, T05, Checkpoints A/B, and Bundle 0 are closed at discovery and contract scope. Bundle 1 has not started. Later seed, staff-account, media, staging, and release gates remain in [todo.md](todo.md).
|
||||
|
||||
## Already chosen
|
||||
|
||||
- **Home/About design direction approved 2026-09-25:** use the current association logo's teal, pale gold, and charcoal in a modern content-rich interface. [View Home](concepts/d-logo-led-home.html), [About](concepts/d-logo-led-about.html), the [Shadcn/ui and Lucide component map](concepts/d-component-map.md), and the [design record](../../guilan-ace-frontend/DESIGN.md). Home follows intro → featured events → featured blog posts → CTA → footer. This approval settles T04's visual direction; production components and content still require implementation review.
|
||||
- **Identity:** keep the existing repository logo; use the official full name in About and metadata, with the current short lockup and faculty subline in the header.
|
||||
- **Public contacts:** use the About-page email `eastguilanceassociation@gmail.com`, Telegram `t.me/guilancea`, and Instagram `instagram.com/guilance.ir`. The current footer variants are recorded for cutover.
|
||||
|
||||
## Content ownership for review
|
||||
|
||||
The [route matrix](content-inventory.md) and [208-line source ledger](content-source-lines.csv) classify current public copy, images, and metadata. The proposed boundary is:
|
||||
|
||||
| Owner | Examples |
|
||||
| --- | --- |
|
||||
| New CMS | Association identity, approved contacts, Home/About editorial sections, navigation labels/order, footer description, page introductions and default SEO. |
|
||||
| Existing domain models | Events, capacity/prices, registrations/payments, blog articles/authors/media, user profiles. |
|
||||
| Application code | Route destinations, auth/payment state labels, forms and errors, admin controls, responsive behavior, canonical/JSON-LD structure. |
|
||||
|
||||
The initial seed will exclude disputed statistics and founding-year mappings, affiliation relationships, personal LinkedIn/generic X links, and the trust-seal claim until their source and entity are confirmed. The exact legacy values remain in the inventory; nothing is silently rewritten.
|
||||
|
||||
## Approved CMS/API decisions
|
||||
|
||||
The [architecture contract](cms-api-contract-draft.md) and [backend response schema](../../guilan-ace-backend/docs/content-api-schema-draft.md) specify:
|
||||
|
||||
1. Fixed Home editorial sections: association-first `hero`, `featured_events`, `featured_posts`, then `participation`. Event and blog cards read existing domain APIs; the CMS owns only headings/intro/actions around them. About uses `identity` first and `contact` last, with optional mission, activities, values, committees, affiliations, and FAQ. Route IDs and destinations stay in code. The proposed featured-item selection policy is in the contract.
|
||||
2. `content_editor` can draft and preview authorized content; `content_publisher` can publish and restore; superusers retain full control. `is_staff` alone cannot publish. Public readers receive published revisions only.
|
||||
3. Staff preview requires authentication and returns exact draft revisions with `no-store` and `noindex`. A publish swaps an immutable revision pointer and records an audit entry; restore creates another revision rather than rewriting history.
|
||||
4. Public API responses use `ETag` and a proposed 60-second shared-cache bound. The frontend avoids an additional persistent CMS cache. Missing/unpublished content returns 404 and the frontend stays on legacy rendering until cutover.
|
||||
|
||||
Actual staff account assignments, media rights/import, uncertain content claims, and deployment rehearsal happen in later tasks before production release.
|
||||
|
||||
## Accepted baseline of current state
|
||||
|
||||
The [dated baseline](baseline-2026-09-24.md) and [local replay](evidence/2026-09-24/local-replay.md) cover Home/About captures at 320/390/768/1440px and local auth, event, mocked payment, blog and staff journeys with failure cases. With Redis running, `manage.py check` and **25 event integration tests passed**. The historical combined users/events/payments Django test command ran 148 tests with 89 errors, mostly stale test fixtures and patch targets; its raw output remains in [the original test log](evidence/2026-09-24/backend-focused-tests.txt). On 2026-09-26, the current documented pytest suites passed **79 unit tests and 158 integration tests**; see [the rerun record](evidence/2026-09-24/test-rerun-2026-09-26.md). External SMS, Google OAuth, and a live payment gateway remain untested.
|
||||
|
||||
The replay also found a countdown hydration mismatch and notification stream-token HTTP 405. These are current-state issues for later implementation planning. SMS delivery, Google OAuth, and a live gateway were not exercised; the OTP and payment callbacks were safely mocked locally.
|
||||
|
||||
## Approval record
|
||||
|
||||
The owner approved this document on 2026-09-26 after separately approving the Home/About design direction on 2026-09-25. Checkpoint A accepts the inventory classification and recorded local baseline, including the historical 89-error run and the unexercised external paths. The current pytest suite rerun subsequently passed, as recorded above. T05 and Checkpoint B accept the section registry, featured-item policy, field bounds, roles, published/preview API behavior, and cache rule for implementation. This approval does not authorize production content publication.
|
||||
@@ -1,26 +0,0 @@
|
||||
# Bundle 1 — Checkpoint C review
|
||||
|
||||
Prepared 2026-09-27. **Approved by the owner on 2026-09-27.** This covers T06–T07 only; draft/publish permissions, admin editing, public and preview APIs, seed, and frontend adapter had not started at this checkpoint.
|
||||
|
||||
## Implemented
|
||||
|
||||
- Additive `content.0001_initial` migration creates a single site identity, site/page revisions, fixed page slugs, ordered typed page sections, managed media, and a publication audit record. Published revision pointers are nullable so migration adds no public content.
|
||||
- Model validation restricts Home/About section keys, fixed Home order, page kinds, text lengths, plain text, action route IDs, navigation/contact shape and URLs, image alt text, image signatures, dimensions, and upload size. The revision and section uniqueness rules have database constraints.
|
||||
- Draft and publication columns are present for T08; no permission or publishing path is active. The required section set and About contact-last rule will be checked by the T08 publish service across the whole revision.
|
||||
|
||||
## Review choices
|
||||
|
||||
- CMS uploads accept PNG, JPEG, and WebP, up to **5 MiB** and **4096 × 4096 px**. The source/rights and alt fields are required. An `approved` flag exists for the later asset review gate.
|
||||
- Text rejects HTML tags and character entities. Contact web links require HTTPS; email and phone contacts require `mailto:` and `tel:` respectively.
|
||||
- A missing optional image is represented by a nullable media relation; text remains present. Publication must check asset approval before exposing media.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Focused CMS integration tests: **6 passed**.
|
||||
- Backend unit suite: **79 passed**.
|
||||
- Backend integration suite: **164 passed** (includes the 6 new tests).
|
||||
- `python manage.py migrate --noinput --settings=config.settings.test`: `content.0001_initial` applied successfully on the test database.
|
||||
- `python manage.py check --settings=config.settings.test`: no issues.
|
||||
- `python manage.py makemigrations --check --dry-run --settings=config.settings.test`: no changes detected.
|
||||
|
||||
No production database, media, or public route was changed. The owner approved Checkpoint C and the schema and validation choices above on 2026-09-27.
|
||||
@@ -1,28 +0,0 @@
|
||||
# Bundle 1 — Checkpoint D review
|
||||
|
||||
**Superseded on 2026-09-27; not approved.** The owner rejected this draft/publish workflow and custom admin screens. The replacement [direct-save CMS plan](bundle1-simplification-plan.md) defines the next Checkpoint D review.
|
||||
|
||||
Prepared 2026-09-27. **Awaiting owner review.** This covers T08–T09: editorial draft/publish/restore rules and Django admin editing. Public and private preview APIs (T10–T11), seed (T12–T13), and frontend adapter (T14) have not started.
|
||||
|
||||
## What is ready
|
||||
|
||||
- The `content_editor` group can create and preview drafts. The `content_publisher` group can also publish and restore. Active superusers override these rules. Staff status alone grants no content action. Migration `content.0003_content_groups` creates both groups and their explicit permissions without assigning accounts.
|
||||
- Each save creates a new content revision. Publishing validates the full page and any referenced media, then swaps the published pointer in one transaction. A stale pointer rejects the publish. Restore creates a new revision from a prior published snapshot and keeps history. Audit entries record actor, operation, and revision numbers.
|
||||
- Home publishing requires all four visible sections in the approved order. About requires identity first and contact last. Optional About sections can be reordered or hidden. Listing pages contain an intro and SEO but no sections. Unapproved media cannot be published.
|
||||
- Django admin exposes named fields for site identity, navigation, contacts, page copy, sections, actions, and typed About entries. Editors do not edit raw JSON, revision status, or the published pointer. The dashboard shows drafts, preview links, the published revision, and publisher actions. Approved media becomes read only.
|
||||
- Admin draft preview requires the preview permission, stays on an admin route, and sends `Cache-Control: private, no-store` plus `X-Robots-Tag: noindex, nofollow`. No public content API is active yet.
|
||||
|
||||
## Verification
|
||||
|
||||
- Full backend suite on the isolated SQLite test settings: **79 unit passed; 184 integration passed; 1 PostgreSQL-only concurrency test skipped**.
|
||||
- The two-publisher race test ran separately against a local PostgreSQL test database: **1 passed**. One publish won; the second received a conflict. The SQLite suite also tests a second publish attempt using the same stale pointer.
|
||||
- Django system check: no issues. Migration consistency check: no changes detected. The additive content permission/group migrations applied on the local test database.
|
||||
- Browser walkthrough on a local test database: editor saved a Home draft at 390px, publisher opened its no-store preview and published it, and the dashboard then identified revision 1 as published. The draft form had no horizontal overflow at 390px or 1440px. Captures: [editor form at 390px](evidence/2026-09-27/cms-admin-home-390.png), [editor form at 1440px](evidence/2026-09-27/cms-admin-home-1440.png), [publisher dashboard at 390px](evidence/2026-09-27/cms-admin-publisher-390.png), [published dashboard at 1440px](evidence/2026-09-27/cms-admin-published-1440.png).
|
||||
|
||||
## Review points
|
||||
|
||||
1. Confirm the editor/publisher separation and the admin draft → preview → publish → restore workflow.
|
||||
2. Confirm that the Home section order and About optional reorder/hide controls match the approved contract.
|
||||
3. Confirm the admin layout at phone and desktop widths from the captures. Actual staff account assignment remains a later gate.
|
||||
|
||||
No production content, media, account assignment, or public route was changed. Checkpoint D closes only after owner review; T10–T11 begin afterward.
|
||||
@@ -1,35 +0,0 @@
|
||||
# Bundle 1 — development database audit
|
||||
|
||||
Audited 2026-09-27 against the configured local PostgreSQL development database, `guilan_ace_local` (`127.0.0.1:5432`). This is the development database the owner authorized in place of staging. No staging or production database was accessed.
|
||||
|
||||
## Migration and data state
|
||||
|
||||
All content migrations are applied: `0001_initial`, `0002_alter_siteidentity_options`, `0003_content_groups`, `0004_direct_edit_content`, and `0005_seed_current_website_copy`.
|
||||
|
||||
Before running the mock-content command, the current editable tables contained one site record, one Home record, one About record, two listing records, and nine About items. There were no image references or missing media files. Django admin change history contained two changes for Home and none for Site, About, Events, or Blog.
|
||||
|
||||
The legacy revision tables were empty: zero SiteIdentity, SiteRevision, EditorialPage, PageRevision, PageSection, MediaAsset, or PublicationAudit rows. Their model definitions, database tables, and historical permission records remain from the applied migration history; no migration or table cleanup was performed.
|
||||
|
||||
## Permission state
|
||||
|
||||
The database has one staff account, which is also the only superuser. The `content_editor` and `content_publisher` groups each have zero members. No user has a direct content-model permission assignment. The two legacy groups contain standard content-model change permissions plus old workflow permission records; they grant access to no account in this database.
|
||||
|
||||
This is an environment-specific snapshot. Staff account assignments in staging or production remain part of the release audit.
|
||||
|
||||
## Mock-content command
|
||||
|
||||
The owner approved mock copy and empty optional images for initial content. The command was first reviewed with:
|
||||
|
||||
```sh
|
||||
python manage.py seed_mock_content --dry-run --database default
|
||||
```
|
||||
|
||||
The dry run proposed replacing Site, About, Events, and Blog, and skipped Home because it has Django admin change history. The command was then run against the same development database:
|
||||
|
||||
```sh
|
||||
python manage.py seed_mock_content --database default
|
||||
```
|
||||
|
||||
It updated those four records, preserved Home, and replaced the untouched About items with six mock items. A read-only follow-up query confirmed the expected mock records, both listing titles and SEO titles, six About items, no image references, and the preserved Home row with its two admin changes. The command did not modify any migration.
|
||||
|
||||
The owner has reviewed and approved the mock content and the development database result. Production initialization remains subject to the release gate.
|
||||
@@ -1,16 +0,0 @@
|
||||
# Bundle 1 — mock content initialization
|
||||
|
||||
The owner approved the original source-content review, then directed that the frontend show mock content until staff enter content through Django admin. The existing migrations, including [0005_seed_current_website_copy.py](../../guilan-ace-backend/apps/content/migrations/0005_seed_current_website_copy.py), remain unchanged.
|
||||
|
||||
Use the backend management command [seed_mock_content.py](../../guilan-ace-backend/apps/content/management/commands/seed_mock_content.py) after migrations and before enabling CMS page rendering:
|
||||
|
||||
```sh
|
||||
python manage.py seed_mock_content --dry-run
|
||||
python manage.py seed_mock_content
|
||||
```
|
||||
|
||||
It replaces content for records without a Django admin change-history entry with clearly labeled Persian sample copy, including About items. A record with an admin change entry is preserved as a whole. Re-running the command is safe; `--dry-run` reports intended work without writing. The command is explicit and is not run automatically by deployment or migration.
|
||||
|
||||
Images remain empty because no approved mock image files exist; the frontend treats them as optional. Contact mock URLs use `example.invalid` and the mock email uses that reserved domain.
|
||||
|
||||
The frontend flag remains off by default. The page routes still use legacy rendering until later Bundle 1 tasks wire them to the adapter. Do not enable CMS rendering in a target environment until T08's database audit is complete and the command's dry-run output is reviewed.
|
||||
@@ -1,52 +0,0 @@
|
||||
# Bundle 1 course correction: simple site content CMS
|
||||
|
||||
Decision recorded 2026-09-27. The owner rejected the draft/publish/revision workflow and custom content admin screens. This plan supersedes the CMS workflow and API portions of the approved Bundle 0 contract and the pending Checkpoint D review. Checkpoint C remains an accurate record of what was reviewed then, but its revision-based schema is no longer the target design. The approved Home/About visual direction and the content ownership inventory remain useful.
|
||||
|
||||
## Agreed behavior
|
||||
|
||||
- The CMS edits the shared site identity and the four fixed public pages: Home, About, Events listing, and Blog listing. Event and blog detail records remain in their existing systems.
|
||||
- A superuser, or an active staff user with the standard Django `change` permission for the relevant content model, edits the record in Django admin. There are no content-specific editor/publisher roles, drafts, publication flags, preview endpoints, revision snapshots, or custom restore action.
|
||||
- Saving a valid record makes its text, images, and SEO available to the public site on the next request. The frontend must not retain a stale page cache after an admin save; verify the actual deployed cache behavior before cutover.
|
||||
- Each fixed page has one editable record with named fields that match the approved layout. Editors do not create pages, edit slugs, manipulate section types/order/visibility, or edit raw JSON. A record's SEO title, description, and social image are editable; canonical routes and structured-data shape stay in code. Shared site identity supplies defaults.
|
||||
- Admin forms use the project's installed Unfold `ModelAdmin`, native fieldsets and supported widgets, matching the existing Blog Category form. No handwritten content admin CSS or custom dashboard/form templates.
|
||||
- Keep useful plain-text, URL, image-signature, size, dimension, and alt-text validation. Use direct image upload fields where practical. Remove media approval and immutability rules that existed only for publication.
|
||||
|
||||
## Work sequence and review gates
|
||||
|
||||
### 1. Migration and permissions audit
|
||||
|
||||
Inspect each target environment for applied `content` migrations, existing content/media rows, and assignments to the content-specific permissions or groups. Record the result before changing schema. Preserve real content and images; map any existing public revision to the new current record. If no published revision exists, do not expose an unreviewed draft automatically. Add forward migrations for shared or applied migration history; do not silently rewrite committed migrations. Translate any actual staff access to standard model permissions before retiring custom permissions. Keep other apps and their permissions untouched.
|
||||
|
||||
**Check:** migration works from a fresh database and a copy with the current content schema; existing real records and access are accounted for.
|
||||
|
||||
### 2. Replace the content model and admin
|
||||
|
||||
Create one fixed record per page and one shared site record, using explicit text, image/alt, and SEO fields for the approved design. Use ordinary Unfold change forms grouped into Content, Images, and SEO. Ensure the four records exist without presenting an arbitrary “Add editorial page” flow; disable deletion. Remove the draft/revision services, publication audit, custom content groups, and custom admin templates after data and permission migration. Django's normal admin history is sufficient for basic change history.
|
||||
|
||||
**Check:** a superuser and a staff user with the relevant `change` permission can save; a staff user without it cannot. Review the actual Home, About, Events, Blog, and site forms at phone and desktop widths against the supplied Blog Category example. Saving invalid text or media shows a field error in the Unfold form.
|
||||
|
||||
### 3. Serve current content
|
||||
|
||||
Expose only the small public read API needed by the frontend for shared site content and the four fixed pages. The implementation and response shapes are documented in the backend [content API v1 note](../../guilan-ace-backend/docs/content-api-v1.md). Responses contain current text, media URLs/alt text, and SEO fields. Remove revision IDs, status, draft preview, publish, and restore from the schema. Define empty-record behavior and cache handling so a successful admin save appears on the next public request.
|
||||
|
||||
**Check:** anonymous reads return current saved values; unknown pages and missing records have explicit responses; an admin save is visible through the API immediately. No draft or preview route exists.
|
||||
|
||||
### 4. Populate reviewed starting content
|
||||
|
||||
Leave all existing migrations unchanged. Use an explicit `seed_mock_content` management command after migrations to ensure the five fixed records contain obvious mock copy until staff save content through Django admin. It replaces untouched migration-provided copy, preserves records with a Django admin change-history entry, supports `--dry-run`, and is never run automatically. Complete the T08 database audit before running it against a target environment; inspect the dry-run output first.
|
||||
|
||||
**Check:** confirm the command fills mock copy, skips admin-edited records, and is idempotent. Optional media remains empty. The owner reviews the mock output before frontend cutover.
|
||||
|
||||
### 5. Connect the frontend and verify SEO
|
||||
|
||||
Keep the frontend feature flag defaulting to legacy content until the new records are complete and checked. Read the fixed page records server-side, populate the approved Home/About layout and Events/Blog listing copy, and use CMS SEO values in rendered HTML and social metadata. Existing event/blog detail APIs and dynamic content stay in place. Remove frontend preview/publish assumptions from later redesign tasks.
|
||||
|
||||
**Check:** editing a heading, image, and SEO title in Unfold changes the public page and server-rendered metadata after Save, with no separate publication action. Verify at 390px and desktop, including Persian text and missing optional images.
|
||||
|
||||
### 6. Release gate
|
||||
|
||||
Rehearse migrations and media on a database copy, check backups, then enable the frontend flag after content and admin walkthrough approval. Keep flag rollback available. Verify public pages, metadata, permissions, and unaffected blog/event flows. Production changes require the existing release gate.
|
||||
|
||||
## Explicit removals from the old plan
|
||||
|
||||
Retire old T08–T11's draft/publish/restore/preview requirements and Checkpoint D's role review. Replace T12–T13's draft seed workflow with reviewed initial records. Update T14, T16–T18, T24, T30, and T38–T39 so their checks require direct-save content, not publication. The old review and contract files remain as historical evidence and are marked superseded, not presented as active instructions.
|
||||
@@ -1,75 +0,0 @@
|
||||
# Public content CMS and API contract — approved for implementation
|
||||
|
||||
**Superseded for CMS implementation on 2026-09-27.** The owner chose [direct-save content editing](bundle1-simplification-plan.md) and rejected the draft/publish/revision and custom admin workflow below. This file records the earlier approval only; use the course correction plan and current [checklist](todo.md) for new work.
|
||||
|
||||
Date: 2026-09-24; revised 2026-09-25; approved by the owner through `bundle0-review.md` on 2026-09-26. Status: **Bundle 0 contract approved for implementation; no public API or schema exists yet**. Inputs: `content-inventory.md`, `product-review.md`, frontend `PRODUCT.md` and `DESIGN.md`, existing Django Ninja routing in `guilan-ace-backend/config/api.py`.
|
||||
|
||||
## Ownership boundary
|
||||
|
||||
The CMS owns association identity, approved public navigation/footer/contact entries, and page introductions/SEO. It owns ordered, visible sections of `home` and `about` from the finite registry below. It does **not** own event/blog records, registration success markdown already stored on each event, auth/form/admin text, payment outcomes, or free-form layouts. Stable slugs initially: `home`, `about`, `events`, `blog`.
|
||||
|
||||
## Proposed persistent records
|
||||
|
||||
| Record | Fields and constraints |
|
||||
| --- | --- |
|
||||
| `SiteIdentity` singleton | Stable site key and nullable `published_revision_id`. Singleton enforced in service/admin and database. |
|
||||
| `SiteRevision` | Immutable snapshot of `official_name` (required, max 180), `short_name` (required, max 80), `faculty_subline` (max 140), `description` (max 500), logo media FK and required alt when logo exists; approved contacts/social entries and navigation entries with fixed IDs, label, sort order and visibility; footer copy; default SEO. Tracks editor, publisher and timestamps like page revisions. |
|
||||
| `EditorialPage` | Unique immutable `slug` from an allowed registry; `page_kind` (`sections` or `listing_intro`); current published revision FK nullable. Slug never derived from editable title. |
|
||||
| `PageRevision` | Page FK, monotonic revision number, `draft` or `published` snapshot, title, intro, SEO title/description/OG media, creator, publisher, timestamps, source seed key/version and immutable content checksum. Store each publish as a new immutable snapshot; do not mutate the public revision in place. |
|
||||
| `PageSection` within revision | Stable section key, approved type, position integer, visible boolean, bounded type-specific text fields, optional media FK and alt text. Unique `(revision, key)` and `(revision, position)`; type/slug registry restricts allowed combinations. |
|
||||
| `MediaAsset` | Uploaded file, MIME signature, bytes, dimensions, alt, license/source and approval status. Logo and OG media use the same managed-media path; no frontend build URL becomes permanent content. |
|
||||
| `PublicationAudit` | Actor, site or page target, from/to revision, operation (`create_draft`, `update_draft`, `publish`, `restore`), timestamp. Append only. |
|
||||
|
||||
Proposed section registry aligned to the owner's fixed Home order. Section records supply editorial text; they do not duplicate event/post records or select a card layout, colors, or button styling:
|
||||
|
||||
| Page | Section key/type | Required | Editable content | Order/visibility |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `home` | `hero` | Yes | Heading (max 120), lead (max 500), primary/secondary CTA labels (max 40 each), each CTA target selected from fixed `about`, `events`, `blog` route IDs; optional approved image and alt | First and visible. Site name/subline comes from `SiteRevision`, not a second copy. |
|
||||
| `home` | `featured_events` | Yes | Section heading (max 90), introduction (max 300), all-events link label (max 40) | Second and visible. Event cards, posters, dates, prices and status read the existing events API, not this section. |
|
||||
| `home` | `featured_posts` | Yes | Section heading (max 90), introduction (max 300), all-posts link label (max 40) | Third and visible. Post cards, titles, categories, images and links read the existing blog API, not this section. |
|
||||
| `home` | `participation` | Yes | Heading (max 90), body (max 600), up to 2 CTA labels (max 40 each) with fixed route IDs; optional approved image and alt | Fourth and visible, before the shared footer. |
|
||||
| `about` | `identity` | Yes | Heading (max 120), introduction (max 600); official name/mark from `SiteRevision` | First and visible. |
|
||||
| `about` | `mission`, `activities`, `values`, `committees`, `affiliations`, `faq` | No | Heading (max 90), body (max 1200) or typed entries (max 20, each title/question max 120 and text/answer max 600) | Reorderable after identity or hidden. Unmapped statistics and affiliation claims are omitted from the initial seed. |
|
||||
| `about` | `contact` | Yes | Heading (max 90); approved visible contact entries from `SiteRevision` | Last and visible; contacts have one source of truth. |
|
||||
|
||||
`events` and `blog` use bounded title/intro/SEO fields and no sections. Every allowed section key is unique within its page revision; publishing rejects missing required sections or an invalid order. The Home order is fixed by the owner's design requirement; About optional sections remain reorderable/hideable. Section text is plain text. Navigation destination IDs and route paths are code-owned; editors can edit display labels/order/visibility on the approved destination list, not supply arbitrary in-app hrefs. The initial public contact set is the owner's About-page selection. This registry and field limits were accepted for T06 implementation on 2026-09-26.
|
||||
|
||||
The Home featured-event source needs no new CMS event fields. The first release selects the next three published future events by start time; if none exist, it shows recent completed events with an archive label. Blog uses published `is_featured` posts, then recent published posts as a fallback. Empty results show a short honest message and a link to the full listing. This selection policy was accepted on 2026-09-26; it never changes registration, payment, or publication rules.
|
||||
|
||||
## Edit, preview, publish and restore
|
||||
|
||||
| Actor | Draft | Preview | Publish | Restore | Public read |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Anonymous | No | No | No | No | Published only |
|
||||
| Authenticated nonstaff | No | No | No | No | Published only |
|
||||
| `content_editor` staff group | Yes, on allowed pages | Own/authorized drafts | No | No | Published only |
|
||||
| `content_publisher` staff group | Yes | Yes | Yes | Yes | Published only |
|
||||
| Superuser | Yes | Yes | Yes | Yes | Published only |
|
||||
|
||||
Use explicit Django permissions (`change_content_draft`, `preview_content_draft`, `publish_content`, `restore_content`) and group assignment; `is_staff` alone does not grant publishing. A transaction checks permission and locks the page row, writes an immutable revision and swaps `published_revision_id` atomically. Restore creates a new revision from the chosen prior snapshot, preserving history. Publisher and editor identities remain visible in admin audit. Preview requires authentication, authorization and a draft revision; no public route accepts a draft ID.
|
||||
|
||||
## Proposed API v1
|
||||
|
||||
Mounted under the existing `/api/` Ninja root in `config/urls.py`:
|
||||
|
||||
- `GET /api/v1/content/site`: only published identity. `200` with `revision`, `published_at`, names, logo URL/alt, approved contacts, navigation route IDs/labels/order/visibility, footer and SEO defaults. `404` when there is no published identity.
|
||||
- `GET /api/v1/content/pages/{slug}`: only published page. `200` with `slug`, `revision`, `published_at`, `title`, `intro`, `seo`, and ordered visible `sections`; `404` for unknown or unpublished-only slugs. Hidden sections never leave the response.
|
||||
- `GET /api/v1/content/preview/pages/{slug}?revision=<id>`: authenticated authorized staff only; exact draft revision; `403` for unauthorized staff and `404` for missing draft. The server must not expose draft existence to anonymous callers. Set `Cache-Control: private, no-store`, `Vary: Cookie, Authorization`, and `X-Robots-Tag: noindex, nofollow`.
|
||||
- `GET /api/v1/content/preview/site?revision=<id>`: the same staff permission and no-store rules for a site-identity draft, including logo, navigation, contacts, footer and SEO defaults. Public site data remains at its published revision during preview.
|
||||
|
||||
The [backend API schema draft](../../guilan-ace-backend/docs/content-api-schema-draft.md) contains complete site, Home and About payload examples. Use canonical path from the fixed slug map, not an arbitrary editor URL. Proposed public cache: `Cache-Control: public, s-maxage=60, must-revalidate` plus `ETag` from the published revision and response checksum. The frontend CMS adapter performs uncached server reads so a second page cache does not extend that 60-second bound. Publish/restore invalidates application cache and requests frontend revalidation for `/`, `/about`, `/events`, `/blog`, and shared shell; revalidation failure is logged while the shared API cache still expires within 60 seconds. The deployment proxy already routes `PathPrefix(/api)` to the Django service; the release rehearsal must verify the deployed configuration. `404` is explicit for empty site/page; the frontend flag remains on legacy rendering until approved content exists. Draft preview uses no shared cache at any layer. Unknown slugs and unpublished-only pages have the same public `404` shape.
|
||||
|
||||
## Media, validation and seed rules
|
||||
|
||||
- Reject unsafe markup; text fields are plain text or tightly validated Markdown only where specified. Reject non-HTTP(S) links except explicitly supported `mailto:` and `tel:` contact types. Verify media by content signature, dimensions and byte limit; require meaningful alt for informative images.
|
||||
- Public images use backed-up Django media URLs. Optional image absent means the section remains legible with text alone. The source logo checksum and license/source are recorded before import.
|
||||
- The T12 manifest records stable keys, source path, exact legacy value, checksum where relevant, destination, approval actor/status, and exclusion reason. T13 dry run emits create/skip/conflict and field diffs, never overwrites an editor-modified row by default, and creates drafts only.
|
||||
|
||||
## Paper walkthrough and later release gates
|
||||
|
||||
1. Editor changes `home.hero` in a draft. Public `GET` stays at published revision 3. Authorized preview of draft 4 returns new copy with `no-store`; anonymous preview returns no draft content.
|
||||
2. Publisher reviews preview at 320px and desktop, publishes. Transaction swaps revision pointer to 4 and logs publisher. Public cache invalidates or expires within agreed TTL. Event and payment records are untouched.
|
||||
3. Publisher restores revision 3. Service creates revision 5 with revision 3 content, logs restore, swaps published pointer. Readers see revision 5; revision 4 stays in history.
|
||||
4. At 320px, long Persian identity wraps, navigation remains reachable without covering content, optional image leaves no blank frame, and hidden optional About sections do not leave layout gaps. At desktop, Home section order matches the owner's fixed sequence and the approved visual direction.
|
||||
|
||||
The earlier concepts were rejected. Frontend `DESIGN.md` records the approved logo-led Home/About direction. **T05 and Checkpoint B were accepted on 2026-09-26:** fixed Home order, featured-item selection policy, section registry and field limits above, `content_editor`/`content_publisher` permission policy, exact preview behavior, fixed-route payloads, and the 60-second API cache rule. **Later release gates:** assigning actual staff accounts, importing licensed media, deployment-proxy rehearsal, and mapping disputed content claims before seed publication.
|
||||
@@ -1,38 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="./assets/favicon.ico">
|
||||
<title>مسیرهای انجمن — طرح A</title>
|
||||
<style>
|
||||
@font-face{font-family:Vazir;src:url('./assets/Vazirmatn-variable.woff2') format('woff2');font-display:swap}
|
||||
:root{--paper:#f2f6f2;--ink:#103d3b;--green:#226d64;--mint:#c9e2d9;--white:#fff;--signal:#ad3c20;--line:#9cb9af}
|
||||
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--paper);color:var(--ink);font:400 16px/1.8 Vazir,Tahoma,sans-serif}
|
||||
a{color:inherit}a:focus-visible{outline:3px solid var(--signal);outline-offset:4px}::selection{background:var(--mint);color:var(--ink)}
|
||||
.wrap{width:min(100% - 48px,1200px);margin:auto}.mast{border-bottom:1px solid var(--line);background:var(--white)}
|
||||
.mast .wrap{min-height:94px;display:flex;justify-content:space-between;align-items:center;gap:24px}
|
||||
.brand{display:flex;align-items:center;gap:14px;text-decoration:none;min-width:0}.brand img{width:56px;height:56px;object-fit:contain}.brand strong{display:block;font-size:18px;line-height:1.45}.brand small{display:block;color:var(--green);font-size:12px}
|
||||
nav{display:flex;align-items:center;gap:8px;flex-wrap:wrap}nav a{text-decoration:none;font-weight:650;padding:8px 13px;border-radius:4px}nav a:hover{background:var(--mint)}nav .active,nav .active:hover{color:var(--white);background:var(--ink)}
|
||||
.hero{position:relative;overflow:hidden;border-bottom:1px solid var(--line)}.hero:before{content:"";position:absolute;right:max(24px,calc((100% - 1200px)/2));top:0;bottom:0;width:2px;background:var(--signal)}
|
||||
.hero .wrap{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(300px,.75fr);gap:7%;align-items:center;min-height:610px;padding-block:80px}
|
||||
.hero-copy{padding-right:36px;position:relative}.hero-copy:before{content:"";position:absolute;right:-9px;top:31px;width:20px;height:20px;border-radius:50%;background:var(--signal);border:4px solid var(--paper)}
|
||||
h1{font-size:clamp(36px,5.2vw,76px);font-weight:850;line-height:1.25;letter-spacing:-.02em;margin:0 0 24px;max-width:12ch}h1 em{font-style:normal;color:var(--green)}
|
||||
.lead{font-size:clamp(17px,1.6vw,21px);max-width:45ch;margin:0 0 30px}.actions{display:flex;gap:10px;flex-wrap:wrap}.button{display:inline-flex;align-items:center;justify-content:center;min-height:50px;padding:9px 24px;border:2px solid var(--ink);text-decoration:none;font-weight:700;border-radius:4px}.button.primary{background:var(--ink);color:var(--white)}.button:hover{background:var(--signal);border-color:var(--signal);color:var(--white)}
|
||||
.identity-panel{background:var(--ink);color:var(--white);padding:34px 32px;min-height:400px;display:flex;flex-direction:column;justify-content:space-between;position:relative}.identity-panel:after{content:"";position:absolute;left:22px;top:22px;width:20px;height:20px;border:2px solid var(--mint);border-radius:50%}.identity-panel span{color:var(--mint);font-size:13px}.identity-panel p{font-weight:700;font-size:clamp(21px,2.1vw,28px);line-height:1.65;margin:16px 0}.identity-panel .faculty{border-top:1px solid #4c7770;padding-top:18px;font-size:15px}
|
||||
.paths{padding-block:72px}.paths h2,.about h2{font-size:clamp(28px,3vw,42px);line-height:1.4;margin:0 0 26px}.paths .route{display:grid;grid-template-columns:80px 1fr auto;align-items:center;gap:22px;padding:22px 12px;border-top:1px solid var(--line);text-decoration:none}.paths .route:last-child{border-bottom:1px solid var(--line)}.route .mark{display:grid;place-items:center;width:48px;height:48px;background:var(--mint)}.route .mark:before{content:"";width:12px;height:12px;border:3px solid var(--green);border-radius:50%}.route strong{font-size:22px}.route small{display:block;color:#345c55;font-size:14px}.route .arrow{width:14px;height:14px;border-left:2px solid var(--signal);border-bottom:2px solid var(--signal);transform:rotate(45deg)}.route:hover{background:#e3eee8}
|
||||
.about{background:var(--white);border-top:1px solid var(--line)}.about .wrap{display:grid;grid-template-columns:1fr 1fr;gap:8%;padding-block:92px}.about .intro{position:sticky;top:40px;align-self:start}.about .intro p{font-size:20px;max-width:34ch}.about .intro img{width:74px;height:74px;object-fit:contain;margin-top:24px}.about .details{border-right:2px solid var(--signal);padding-right:34px}.detail{padding:4px 0 24px;border-bottom:1px solid var(--line);margin-bottom:25px}.detail h3{font-size:20px;margin:0 0 7px}.detail p{margin:0;color:#345c55}.contact a{display:block;overflow-wrap:anywhere;text-decoration-thickness:1px;text-underline-offset:4px}
|
||||
footer{background:var(--ink);color:var(--white);padding:36px 0}footer .wrap{display:flex;justify-content:space-between;gap:20px;align-items:center}footer p{margin:0}footer a{color:var(--mint)}
|
||||
@media(max-width:760px){.wrap{width:min(100% - 32px,1200px)}.mast .wrap{min-height:76px}.brand img{width:42px;height:42px}.brand strong{font-size:13px}.brand small{font-size:11px}nav{display:none}.hero:before{right:16px}.hero .wrap{grid-template-columns:1fr;gap:34px;min-height:0;padding-block:54px}.hero-copy{padding-right:25px}h1{max-width:14ch}.identity-panel{min-height:260px}.paths{padding-block:50px}.paths .route{grid-template-columns:40px 1fr 18px;gap:12px;padding:17px 2px}.route .mark{width:38px;height:38px;font-size:19px}.route strong{font-size:17px}.route small{font-size:12px}.about .wrap{grid-template-columns:1fr;padding-block:55px;gap:26px}.about .intro{position:static}.about .details{padding-right:20px}footer .wrap{display:block}footer p+ p{margin-top:10px}}
|
||||
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="mast"><div class="wrap"><a class="brand" href="#top"><img src="./assets/favicon.ico" alt="نشان فعلی انجمن"><span><strong>انجمن علمی مهندسی کامپیوتر</strong><small>دانشکدهی فنی و مهندسی شرق گیلان</small></span></a><nav aria-label="ناوبری طرح"><a class="active" href="#top">خانه</a><a href="#about">درباره انجمن</a><a href="#paths">مسیرها</a></nav></div></header>
|
||||
<main id="top">
|
||||
<section class="hero" aria-labelledby="hero-title"><div class="wrap"><div class="hero-copy"><h1 id="hero-title">جایی برای <em>یادگیری و همکاری</em> دانشجویان کامپیوتر</h1><p class="lead">انجمن علمی مهندسی کامپیوتر دانشگاه گیلان در دانشکدهی فنی و مهندسی شرق گیلان فعالیت میکند. اینجا میتوانید با فعالیتهای انجمن آشنا شوید و راه مشارکت خود را پیدا کنید.</p><div class="actions"><a class="button primary" href="#about">آشنایی با انجمن</a><a class="button" href="#paths">دیدن مسیرها</a></div></div><div class="identity-panel"><span>هویت انجمن</span><p>انجمن علمی مهندسی کامپیوتر دانشگاه گیلان</p><div class="faculty">دانشکدهی فنی و مهندسی شرق گیلان</div></div></div></section>
|
||||
<section id="paths" class="paths wrap" aria-labelledby="paths-title"><h2 id="paths-title">از کجا شروع کنم؟</h2><a class="route" href="#about"><span class="mark" aria-hidden="true"></span><span><strong>آشنایی با انجمن</strong><small>مأموریت و راههای ارتباطی</small></span><span class="arrow" aria-hidden="true"></span></a><a class="route" href="https://east-guilan-ce.ir/events"><span class="mark" aria-hidden="true"></span><span><strong>رویدادها</strong><small>برنامهها و فرصتهای یادگیری</small></span><span class="arrow" aria-hidden="true"></span></a><a class="route" href="https://east-guilan-ce.ir/blog"><span class="mark" aria-hidden="true"></span><span><strong>بلاگ</strong><small>نوشتهها و محتوای آموزشی</small></span><span class="arrow" aria-hidden="true"></span></a></section>
|
||||
<section id="about" class="about" aria-labelledby="about-title"><div class="wrap"><div class="intro"><h2 id="about-title">درباره انجمن</h2><p>یادگیری پیوسته، اشتراک دانش و فرصت برابر برای مشارکت، محور فعالیت انجمن است.</p><img src="./assets/favicon.ico" alt="نشان فعلی انجمن"></div><div class="details"><div class="detail"><h3>مأموریت</h3><p>توانمندسازی دانشجویان با مهارتهای موردنیاز صنعت، ایجاد شبکههای حرفهای و فراهمکردن مسیرهای یادگیری پروژهمحور.</p></div><div class="detail"><h3>فعالیتها</h3><p>برگزاری رویدادها، کارگاهها و نشستهای تخصصی و انتشار محتوای آموزشی.</p></div><div class="detail contact"><h3>ارتباط با انجمن</h3><a href="mailto:eastguilanceassociation@gmail.com" dir="ltr">eastguilanceassociation@gmail.com</a><a href="https://t.me/guilancea" dir="ltr">t.me/guilancea</a><a href="https://instagram.com/guilance.ir" dir="ltr">instagram.com/guilance.ir</a></div></div></div></section>
|
||||
</main><footer><div class="wrap"><p>انجمن علمی مهندسی کامپیوتر دانشگاه گیلان</p><p><a href="#top">بازگشت به آغاز</a></p></div></footer>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 141 KiB |
@@ -1,32 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="./assets/favicon.ico">
|
||||
<title>دفتر انجمن — طرح B</title>
|
||||
<style>
|
||||
@font-face{font-family:Vazir;src:url('./assets/Vazirmatn-variable.woff2') format('woff2');font-display:swap}
|
||||
:root{--navy:#14243f;--blue:#42649a;--paper:#fff9ea;--cream:#f1e8d0;--gold:#f0be54;--white:#fff;--line:#b8b2a4}
|
||||
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--paper);color:var(--navy);font:400 16px/1.85 Vazir,Tahoma,sans-serif}a{color:inherit}a:focus-visible{outline:3px solid var(--gold);outline-offset:4px}::selection{background:var(--gold);color:var(--navy)}
|
||||
.wrap{width:min(100% - 56px,1240px);margin:auto}.header{background:var(--navy);color:var(--white);border-bottom:1px solid #66758e}.header .wrap{display:flex;justify-content:space-between;align-items:center;gap:30px;min-height:92px}.brand{display:flex;align-items:center;gap:13px;text-decoration:none}.brand img{height:52px;width:52px;object-fit:contain}.brand span{font-size:14px;font-weight:750;max-width:250px;line-height:1.5}.header nav{display:flex;gap:26px}.header nav a{color:#ecf0f7;text-decoration:none;text-underline-offset:9px;font-size:14px}.header nav a:hover,.header nav a:focus-visible{text-decoration:underline}
|
||||
.cover{background:var(--navy);color:var(--paper);overflow:hidden}.cover .wrap{padding-block:70px 90px;display:grid;grid-template-columns:1.3fr .7fr;gap:8%;min-height:620px;align-items:end}.headline{position:relative}h1{font-weight:900;font-size:clamp(48px,7.3vw,112px);letter-spacing:-.035em;line-height:1.16;margin:0;max-width:12ch}h1 span{display:block;color:var(--gold)}.headline:after{content:"";display:block;width:70%;height:1px;background:#8190a3;margin-top:36px}.cover-note{align-self:center;border-top:2px solid var(--gold);background:var(--paper);color:var(--navy);padding:28px 30px 32px}.cover-note p{font-size:19px;line-height:1.8;margin:0 0 24px}.cover-note a{display:inline-block;font-weight:750;text-underline-offset:5px}.cover-note strong{display:block;font-size:13px;color:#49628b;margin-bottom:18px}
|
||||
.ribbon{background:var(--gold);padding:16px 0}.ribbon .wrap{display:flex;align-items:center;gap:32px;flex-wrap:wrap;font-weight:750}.ribbon span{margin-left:auto}.ribbon a{text-decoration:none;border-bottom:1px solid currentColor}.ribbon a:hover{text-decoration:underline}
|
||||
.chapter{padding-block:88px 100px}.chapter-head{display:grid;grid-template-columns:1fr 1fr;gap:7%;padding-bottom:40px;border-bottom:2px solid var(--navy)}.chapter h2{font-size:clamp(36px,4.2vw,62px);line-height:1.25;margin:0}.chapter-head p{font-size:21px;line-height:1.8;margin:0}.ledger{display:grid;grid-template-columns:repeat(3,1fr);border-bottom:1px solid var(--line)}.ledger div{padding:30px 22px 32px;border-left:1px solid var(--line)}.ledger div:first-child{padding-right:0}.ledger div:last-child{border:0}.ledger h3{font-size:23px;margin:0 0 10px}.ledger p{margin:0;color:#384c69}
|
||||
.about{background:var(--cream);padding-block:92px}.about .wrap{display:grid;grid-template-columns:.8fr 1.2fr;gap:9%}.about aside{border-top:2px solid var(--navy);padding-top:22px}.about aside img{width:86px;height:86px;object-fit:contain}.about h2{font-size:clamp(38px,5vw,70px);line-height:1.2;margin:15px 0 26px}.about .name{font-weight:760;font-size:20px;line-height:1.7}.about article{padding-top:34px}.about article p{font-size:20px;line-height:2;max-width:44ch;margin:0 0 32px}.about dl{border-top:1px solid var(--navy);margin:0}.about dl div{display:grid;grid-template-columns:130px 1fr;border-bottom:1px solid var(--line);gap:20px;padding:18px 0}.about dt{font-weight:800}.about dd{margin:0;overflow-wrap:anywhere}.about dd a{text-underline-offset:4px}
|
||||
.end{background:var(--navy);color:var(--paper);padding:58px 0}.end .wrap{display:flex;justify-content:space-between;align-items:end;gap:30px}.end p{margin:0;font-weight:700;font-size:22px;max-width:30ch}.end a{color:var(--gold);white-space:nowrap}
|
||||
@media(max-width:820px){.wrap{width:min(100% - 32px,1240px)}.header .wrap{min-height:75px}.brand img{width:42px;height:42px}.brand span{font-size:12px}.header nav{display:none}.cover .wrap{grid-template-columns:1fr;gap:52px;min-height:0;padding-block:52px 68px}.cover-note{max-width:550px}.ribbon .wrap{gap:12px}.ribbon span{width:100%}.chapter{padding-block:58px}.chapter-head{grid-template-columns:1fr;gap:17px}.chapter-head p{font-size:18px}.ledger{grid-template-columns:1fr}.ledger div,.ledger div:first-child{padding:23px 0;border-left:0;border-bottom:1px solid var(--line)}.ledger div:last-child{border:0}.about{padding-block:60px}.about .wrap{grid-template-columns:1fr;gap:20px}.about article{padding-top:0}.about article p{font-size:18px}.end .wrap{display:block}.end a{display:inline-block;margin-top:22px}}
|
||||
@media(max-width:400px){h1{font-size:45px}.about dl div{grid-template-columns:1fr;gap:3px}.cover-note{padding:22px}}
|
||||
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header"><div class="wrap"><a class="brand" href="#top"><img src="./assets/favicon.ico" alt="نشان فعلی انجمن"><span>انجمن علمی مهندسی کامپیوتر<br>دانشکدهی فنی و مهندسی شرق گیلان</span></a><nav aria-label="ناوبری طرح"><a href="#top">آغاز</a><a href="#chapter">فعالیتها</a><a href="#about">درباره انجمن</a></nav></div></header>
|
||||
<main id="top">
|
||||
<section class="cover" aria-labelledby="cover-title"><div class="wrap"><div class="headline"><h1 id="cover-title">انجمن برای <span>یاد گرفتن با هم</span></h1></div><div class="cover-note"><strong>انجمن علمی مهندسی کامپیوتر دانشگاه گیلان</strong><p>در دانشکدهی فنی و مهندسی شرق گیلان، فعالیت انجمن فرصتی برای یادگیری، اشتراک دانش و مشارکت دانشجویان است.</p><a href="#about">انجمن را بشناسید</a></div></div></section>
|
||||
<div class="ribbon"><div class="wrap"><span>برای آشنایی با فعالیتها</span><a href="#chapter">برنامههای انجمن</a><a href="#about">مأموریت و ارتباط</a></div></div>
|
||||
<section id="chapter" class="chapter wrap" aria-labelledby="chapter-title"><div class="chapter-head"><h2 id="chapter-title">فعالیتهای انجمن</h2><p>از رویدادهای تخصصی تا نوشتههای آموزشی، برنامهها برای یادگیری و ارتباط میان دانشجویان شکل میگیرند.</p></div><div class="ledger"><div><h3>رویدادها</h3><p>کارگاهها، سمینارها و نشستهای علمی.</p></div><div><h3>محتوا</h3><p>مقالات آموزشی و یادداشتهای تخصصی.</p></div><div><h3>همکاری</h3><p>مشارکت دانشجویان و اشتراک تجربه.</p></div></div></section>
|
||||
<section id="about" class="about" aria-labelledby="about-title"><div class="wrap"><aside><img src="./assets/favicon.ico" alt="نشان فعلی انجمن"><h2 id="about-title">درباره انجمن</h2><p class="name">انجمن علمی مهندسی کامپیوتر دانشگاه گیلان (دانشکدهی فنی و مهندسی شرق گیلان)</p></aside><article><p>مأموریت انجمن، توانمندسازی دانشجویان با مهارتهای موردنیاز صنعت، ایجاد شبکههای حرفهای و فراهمکردن مسیرهای یادگیری پروژهمحور است.</p><dl><div><dt>ایمیل</dt><dd><a dir="ltr" href="mailto:eastguilanceassociation@gmail.com">eastguilanceassociation@gmail.com</a></dd></div><div><dt>تلگرام</dt><dd><a dir="ltr" href="https://t.me/guilancea">t.me/guilancea</a></dd></div><div><dt>اینستاگرام</dt><dd><a dir="ltr" href="https://instagram.com/guilance.ir">instagram.com/guilance.ir</a></dd></div></dl></article></div></section>
|
||||
</main><footer class="end"><div class="wrap"><p>یادگیری پیوسته و اشتراک دانش، از ارزشهای انجمن است.</p><a href="#top">بازگشت به آغاز</a></div></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,52 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="./assets/favicon.ico">
|
||||
<title>پالایش طرح فعلی — خانه و درباره انجمن</title>
|
||||
<style>
|
||||
@font-face{font-family:Vazirmatn;src:url('./assets/Vazirmatn-variable.woff2') format('woff2');font-display:swap}
|
||||
:root{--background:#fff;--ink:#0f172a;--muted:#475569;--line:#e2e8f0;--wash:#f4f8fc;--accent:#1e4f85;--focus:#2563eb}
|
||||
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--background);color:var(--ink);font:400 16px/1.8 Vazirmatn,Tahoma,sans-serif}a{color:inherit}a:focus-visible,summary:focus-visible{outline:3px solid var(--focus);outline-offset:4px}::selection{background:#dbeafe;color:var(--ink)}
|
||||
.wrap{width:min(100% - 48px,1120px);margin-inline:auto}.site-head{position:sticky;top:0;z-index:10;background:#ffffffed;border-bottom:1px solid var(--line);backdrop-filter:blur(16px)}
|
||||
.site-head .wrap{min-height:74px;display:flex;align-items:center;justify-content:space-between;gap:24px}.brand{display:flex;align-items:center;gap:10px;min-width:0;text-decoration:none}.brand img{width:39px;height:39px;object-fit:contain}.brand strong{display:block;font-size:15px;line-height:1.5}.brand small{display:block;color:var(--muted);font-size:11px;line-height:1.5}
|
||||
.top-nav{display:flex;align-items:center;gap:5px}.top-nav a{padding:9px 15px;border-radius:999px;text-decoration:none;font-size:14px;font-weight:600;color:var(--muted)}.top-nav a:hover,.top-nav a.active{background:var(--wash);color:var(--ink)}
|
||||
.hero{background:linear-gradient(180deg,#f3f8ff 0%,#fff 100%);text-align:center;padding:clamp(78px,9vw,126px) 0 100px}.hero h1{max-width:19ch;margin:0 auto 23px;font-size:clamp(36px,4.4vw,61px);line-height:1.33;font-weight:850;letter-spacing:-.025em}.hero .lead{max-width:61ch;margin:0 auto;color:var(--muted);font-size:clamp(17px,1.6vw,20px);line-height:1.95}.hero .actions{margin-top:34px}
|
||||
.actions{display:flex;flex-wrap:wrap;justify-content:center;gap:12px}.button{display:inline-flex;align-items:center;justify-content:center;min-height:48px;padding:10px 24px;border-radius:10px;border:1px solid var(--ink);text-decoration:none;font-weight:700;line-height:1.5;transition:background .18s,color .18s,border-color .18s}.button.primary{background:var(--ink);color:white}.button.secondary{background:white;color:var(--ink);border-color:#cbd5e1}.button:hover{background:var(--accent);border-color:var(--accent);color:white}
|
||||
.section{padding:86px 0}.section-head{display:flex;justify-content:space-between;align-items:end;gap:24px;margin-bottom:26px}.section h2{font-size:clamp(26px,3vw,35px);line-height:1.45;margin:0}.section-head p{max-width:47ch;margin:0;color:var(--muted)}
|
||||
.offer-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.offer{padding:27px;border:1px solid var(--line);border-radius:16px;background:white}.offer h3{font-size:20px;margin:0 0 10px}.offer p{color:var(--muted);font-size:15px;line-height:1.9;margin:0 0 20px}.text-link{color:var(--accent);text-decoration:underline;text-underline-offset:5px;font-weight:700}.text-link:hover{color:var(--ink)}
|
||||
.home-join{background:var(--wash);border-block:1px solid var(--line);text-align:center}.home-join p{color:var(--muted);max-width:50ch;margin:12px auto 24px}.about-hero{padding:86px 0 70px;background:linear-gradient(180deg,#f4f8fc,white)}.about-hero h1{font-size:clamp(35px,4.3vw,55px);line-height:1.35;margin:0 0 16px}.about-hero p{max-width:68ch;margin:0;color:var(--muted);font-size:18px;line-height:1.95}.about-full-name{margin-top:30px;padding-top:18px;border-top:1px solid var(--line);font-size:14px;color:var(--muted)}
|
||||
.about-layout{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,.8fr);gap:clamp(32px,6vw,90px)}.about-layout h2{margin:0 0 16px}.about-layout p{max-width:66ch;color:var(--muted);margin:0 0 22px}.activity-list{list-style:none;padding:0;margin:0}.activity-list li{padding:15px 0;border-bottom:1px solid var(--line)}.activity-list strong{display:block;font-size:17px}.activity-list span{display:block;color:var(--muted);font-size:14px}.contact-block{border:1px solid var(--line);border-radius:16px;padding:25px;background:var(--wash);align-self:start}.contact-block h3{font-size:20px;margin:0 0 10px}.contact-block p{font-size:14px;margin:0 0 18px}.contact-block a{display:block;overflow-wrap:anywhere;margin:8px 0;color:var(--accent);text-underline-offset:4px}.contact-block a[dir=ltr]{text-align:right}
|
||||
.faq{padding-top:0}.faq details{border-bottom:1px solid var(--line);padding:17px 0}.faq summary{cursor:pointer;font-weight:650}.faq details p{color:var(--muted);margin:12px 0 0;max-width:65ch}.site-foot{border-top:1px solid var(--line);padding:35px 0;color:var(--muted);font-size:14px}.site-foot .wrap{display:flex;justify-content:space-between;gap:16px}.mobile-nav{display:none}
|
||||
@media(max-width:700px){.wrap{width:min(100% - 32px,1120px)}.site-head .wrap{min-height:68px}.brand img{width:33px;height:33px}.brand small{display:none}.top-nav{display:none}.hero{padding:82px 0 72px}.hero h1{font-size:clamp(35px,9vw,48px)}.hero .lead{font-size:17px}.actions{flex-direction:column}.button{width:100%}.section{padding:62px 0}.section-head{display:block}.section-head p{margin-top:8px}.offer-grid,.about-layout{grid-template-columns:1fr}.offer{padding:23px}.about-hero{padding:70px 0 52px}.about-hero p{font-size:17px}.contact-block{margin-top:10px}.faq{padding-top:0}.site-foot{padding-bottom:130px}.site-foot .wrap{display:block}.site-foot p{margin:0 0 8px}.mobile-nav{position:fixed;z-index:20;bottom:calc(12px + env(safe-area-inset-bottom));left:16px;right:16px;display:flex;justify-content:space-around;gap:4px;padding:8px;border-radius:20px;background:#fffffff0;border:1px solid var(--line);box-shadow:0 12px 32px #0f172a20;backdrop-filter:blur(14px)}.mobile-nav a{flex:1;text-align:center;text-decoration:none;padding:7px 2px;border-radius:14px;font-size:12px;font-weight:700}.mobile-nav a.active{background:var(--ink);color:white}}
|
||||
#about,#home,#contact{scroll-margin-top:80px}
|
||||
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}.button{transition:none}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-head"><div class="wrap"><a class="brand" href="#home"><img src="./assets/favicon.ico" alt=""><span><strong>انجمن علمی مهندسی کامپیوتر</strong><small>دانشکدهی فنی و مهندسی شرق گیلان</small></span></a><nav class="top-nav" aria-label="بخشهای نمونه"><a class="active" href="#home">خانه</a><a href="#about">درباره انجمن</a><a href="https://east-guilan-ce.ir/events">رویدادها</a><a href="https://east-guilan-ce.ir/blog">بلاگ</a></nav></div></header>
|
||||
<main>
|
||||
<section class="hero" id="home" aria-labelledby="home-title"><div class="wrap"><h1 id="home-title">انجمن علمی مهندسی کامپیوتر</h1><p class="lead">انجمن دانشجویان کامپیوتر در دانشکدهی فنی و مهندسی شرق گیلان است؛ جایی برای یادگیری، برگزاری رویدادهای تخصصی و همکاری با یکدیگر.</p><div class="actions"><a class="button primary" href="#about">آشنایی با انجمن</a><a class="button secondary" href="https://east-guilan-ce.ir/events">مشاهده رویدادها</a></div></div></section>
|
||||
<section class="section" aria-labelledby="offer-title"><div class="wrap"><div class="section-head"><h2 id="offer-title">در انجمن چه میگذرد؟</h2><p>سه راه ساده برای شناختن فعالیتهای انجمن و پیدا کردن نقطهی شروع.</p></div><div class="offer-grid"><article class="offer"><h3>رویدادهای تخصصی</h3><p>کارگاهها، سمینارها و نشستهایی برای یادگیری مهارتهای تازه.</p><a class="text-link" href="https://east-guilan-ce.ir/events">دیدن رویدادها</a></article><article class="offer"><h3>محتوای آموزشی</h3><p>مطالبی در حوزههای گوناگون علوم و مهندسی کامپیوتر.</p><a class="text-link" href="https://east-guilan-ce.ir/blog">خواندن بلاگ</a></article><article class="offer"><h3>همکاری دانشجویی</h3><p>فرصتی برای مشارکت در فعالیتها و ارتباط با دانشجویان علاقهمند.</p><a class="text-link" href="#contact">راههای ارتباطی</a></article></div></div></section>
|
||||
<section class="section home-join" aria-labelledby="join-title"><div class="wrap"><h2 id="join-title">دوست دارید همراه انجمن باشید؟</h2><p>برای آشنایی با فعالیتها از همینجا شروع کنید یا برنامههای پیش رو را ببینید.</p><div class="actions"><a class="button primary" href="#about">درباره انجمن</a><a class="button secondary" href="https://east-guilan-ce.ir/events">رویدادها</a></div></div></section>
|
||||
<section class="about-hero" id="about" aria-labelledby="about-title"><div class="wrap"><h1 id="about-title">درباره انجمن</h1><p>انجمن علمی مهندسی کامپیوتر در دانشکدهی فنی و مهندسی شرق گیلان با برگزاری رویدادها و تولید محتوای آموزشی، فضایی برای یادگیری و همکاری دانشجویان فراهم میکند.</p><div class="about-full-name">انجمن علمی مهندسی کامپیوتر دانشگاه گیلان (دانشکدهی فنی و مهندسی شرق گیلان)</div></div></section>
|
||||
<section class="section" aria-labelledby="activity-title"><div class="wrap about-layout"><div><h2 id="activity-title">فعالیتهای انجمن</h2><p>محور فعالیتها، یادگیری، اشتراک دانش و ارتباط میان دانشجویان و متخصصان است.</p><ul class="activity-list"><li><strong>برگزاری رویدادها</strong><span>کارگاهها، سمینارها و نشستهای تخصصی</span></li><li><strong>انتشار محتوا</strong><span>مقالهها و مطالب آموزشی در حوزهی کامپیوتر</span></li><li><strong>مشارکت دانشجویی</strong><span>همکاری در برنامهریزی و اجرای فعالیتهای انجمن</span></li></ul></div><aside class="contact-block" id="contact"><h3>ارتباط با انجمن</h3><p>برای پیشنهاد برنامه، همکاری یا پرسش، از یکی از راههای زیر پیام بدهید.</p><a dir="ltr" href="mailto:eastguilanceassociation@gmail.com">eastguilanceassociation@gmail.com</a><a dir="ltr" href="https://t.me/guilancea">t.me/guilancea</a><a dir="ltr" href="https://instagram.com/guilance.ir">instagram.com/guilance.ir</a></aside></div></section>
|
||||
<section class="section faq" aria-labelledby="faq-title"><div class="wrap"><h2 id="faq-title">پرسشهای رایج</h2><details><summary>چطور در رویدادها شرکت کنم؟</summary><p>رویداد موردنظرتان را انتخاب کنید و شرایط و مراحل ثبتنام همان رویداد را بخوانید.</p></details><details><summary>چطور با انجمن همکاری کنم؟</summary><p>برای پیشنهاد همکاری از راههای ارتباطی بالا پیام بدهید.</p></details></div></section>
|
||||
</main>
|
||||
<footer class="site-foot"><div class="wrap"><p>انجمن علمی مهندسی کامپیوتر دانشگاه گیلان</p><a href="#home">بازگشت به آغاز</a></div></footer>
|
||||
<nav class="mobile-nav" aria-label="ناوبری نمونه در موبایل"><a class="active" href="#home">خانه</a><a href="#about">درباره</a><a href="https://east-guilan-ce.ir/events">رویدادها</a><a href="https://east-guilan-ce.ir/blog">بلاگ</a></nav>
|
||||
<script>
|
||||
function syncPreviewNav(){
|
||||
const current = location.hash === '#about' ? '#about' : '#home';
|
||||
for(const link of document.querySelectorAll('.top-nav a[href^="#"],.mobile-nav a[href^="#"]')){
|
||||
const active = link.getAttribute('href') === current;
|
||||
link.classList.toggle('active',active);
|
||||
if(active) link.setAttribute('aria-current','page'); else link.removeAttribute('aria-current');
|
||||
}
|
||||
}
|
||||
addEventListener('hashchange',syncPreviewNav);
|
||||
syncPreviewNav();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,18 +0,0 @@
|
||||
# Logo-led Home/About comp: implementation map
|
||||
|
||||
Status: Home/About visual direction approved by the owner on 2026-09-25. [Home](d-logo-led-home.html) and [About](d-logo-led-about.html) are static review comps. They use the association's current logo, its sampled teal `#28626E`, pale gold `#F9E697`, and charcoal `#313332`. The event posters are copies of existing backend media. Blog titles and artwork in the Home comp are explicitly illustrative.
|
||||
|
||||
The frontend already has Shadcn/ui source components under `src/components/ui/` and `lucide-react`. Production implementation should compose those local components with theme tokens, not copy this comp's static HTML or inline SVG sprite:
|
||||
|
||||
| Comp element | Frontend composition |
|
||||
| --- | --- |
|
||||
| Primary/secondary action | `Button` with `asChild`, `variant="default"` or `variant="outline"`, and the app's `Link`. Use semantic theme tokens for the logo palette. |
|
||||
| Event/post preview | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`; image from event/blog domain record. |
|
||||
| Archive/category label | `Badge` with `secondary` or `outline` variant. |
|
||||
| Footer and article dividers | `Separator` where a semantic divider helps. |
|
||||
| Mobile navigation | Existing `MobileBottomNav` pattern, adjusted for brand tokens and content clearance. |
|
||||
| Icons | Existing `lucide-react`: `ArrowUpLeft`, `CalendarDays`, `BookOpen`, `Newspaper`, `GraduationCap`, `UsersRound`, `Mail`, `Send`, `Instagram`. The comp's SVG sprite uses their icon paths. |
|
||||
|
||||
Home order is fixed: association intro → featured events → featured blog posts → participation CTA → footer. The CMS supplies intro/CTA and shared identity/contact copy. The event and blog sections read their existing APIs. The events model currently has no editorial `is_featured` flag; proposed first pass selects the next three published future events, then clearly labeled recent archive items if none are upcoming. Blog has `is_featured`; proposed first pass selects published featured posts, then recent published posts as fallback. Dates, prices, capacity, and registration status must come from each event record at render time, never from the static comp. A later owner decision can add manual curation if needed.
|
||||
|
||||
The comp is the approved visual and structural direction for T04. T05 CMS/API contract approval remains open; no production Home/About route or business rule changed.
|
||||
@@ -1,27 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#28626e">
|
||||
<link rel="icon" href="./assets/favicon.ico">
|
||||
<link rel="stylesheet" href="./d-logo-led.css">
|
||||
<title>درباره انجمن — طرح مبتنی بر نشان انجمن</title>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-head"><div class="wrap head-inner">
|
||||
<a class="brand" href="./d-logo-led-home.html"><img src="./assets/favicon.ico" alt=""><span><strong>انجمن علمی مهندسی کامپیوتر</strong><small>دانشکدهی فنی و مهندسی شرق گیلان</small></span></a>
|
||||
<nav class="top-nav" aria-label="ناوبری اصلی"><a href="./d-logo-led-home.html">خانه</a><a href="./d-logo-led-home.html#events">رویدادها</a><a href="./d-logo-led-home.html#blog">بلاگ</a><a href="#about" aria-current="page">درباره انجمن</a></nav>
|
||||
<div class="head-action"><a class="button outline" href="https://east-guilan-ce.ir/auth">ورود / ثبتنام</a></div>
|
||||
</div></header>
|
||||
<main id="about">
|
||||
<section class="about-hero" aria-labelledby="about-title"><div class="wrap"><h1 id="about-title">درباره انجمن</h1><p>انجمن علمی مهندسی کامپیوتر در دانشکدهی فنی و مهندسی شرق گیلان، فضایی برای یادگیری، اجرای برنامههای علمی و همکاری میان دانشجویان فراهم میکند.</p><div class="full-name">انجمن علمی مهندسی کامپیوتر دانشگاه گیلان (دانشکدهی فنی و مهندسی شرق گیلان)</div></div></section>
|
||||
<section class="section" aria-labelledby="mission-title"><div class="wrap about-columns"><div><h2 id="mission-title">کاری که انجام میدهیم</h2><p>با برگزاری رویدادها و انتشار محتوای آموزشی، فرصتهایی برای تمرین مهارت، آشنایی با موضوعهای تازه و ارتباط با دیگر علاقهمندان به فناوری ایجاد میکنیم.</p><p>دانشجویان میتوانند از راه شرکت در برنامهها، خواندن و نوشتن مطالب یا همکاری در اجرای فعالیتها همراه انجمن باشند.</p><a class="button primary" href="./d-logo-led-home.html#events">دیدن رویدادها <svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#arrow-up-left"/></svg></a></div><div class="about-points"><div class="about-point"><span class="icon-box"><svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#graduation-cap"/></svg></span><div><strong>یادگیری کاربردی</strong><span>کارگاهها و برنامههای آموزشی</span></div></div><div class="about-point"><span class="icon-box"><svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#calendar-days"/></svg></span><div><strong>رویدادهای علمی</strong><span>نشستها و فرصتهای آشنایی با تجربههای تازه</span></div></div><div class="about-point"><span class="icon-box"><svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#users"/></svg></span><div><strong>همکاری دانشجویی</strong><span>مشارکت در برنامهریزی و اجرای فعالیتها</span></div></div></div></div></section>
|
||||
<section class="section alt" id="contact" aria-labelledby="contact-title"><div class="wrap about-columns"><div><h2 id="contact-title">با انجمن در ارتباط باشید</h2><p>برای پرسش، پیشنهاد برنامه یا همکاری، از کانالهای رسمی انجمن پیام بدهید.</p><p>برای شناخت برنامههای برگزارشده و نوشتههای انجمن هم میتوانید بخش رویدادها و بلاگ را ببینید.</p></div><div class="about-contact"><h2>راههای ارتباطی</h2><a dir="ltr" href="mailto:eastguilanceassociation@gmail.com">eastguilanceassociation@gmail.com</a><a dir="ltr" href="https://t.me/guilancea">t.me/guilancea</a><a dir="ltr" href="https://instagram.com/guilance.ir">instagram.com/guilance.ir</a></div></div></section>
|
||||
<section class="section faq" aria-labelledby="faq-title"><div class="wrap"><div class="section-header"><div><h2 id="faq-title">پرسشهای رایج</h2><p>چند پاسخ کوتاه برای شروع.</p></div></div><details><summary>چطور در یک رویداد شرکت کنم؟</summary><p>رویداد موردنظرتان را انتخاب کنید و شرایط، زمان و مراحل ثبتنام همان رویداد را بخوانید.</p></details><details><summary>چطور با انجمن همکاری کنم؟</summary><p>برای پیشنهاد همکاری، از راههای ارتباطی بالا پیام بدهید و موضوع پیشنهادیتان را توضیح دهید.</p></details></div></section>
|
||||
<section class="cta" aria-labelledby="cta-title"><div class="wrap cta-inner"><div><h2 id="cta-title">از برنامههای انجمن شروع کنید</h2><p>رویدادهای برگزارشده و نوشتههای انجمن، بهترین راه برای آشنایی با فعالیتهای ما هستند.</p></div><div class="actions"><a class="button gold" href="./d-logo-led-home.html#events">رویدادها <svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#arrow-up-left"/></svg></a><a class="button outline" href="./d-logo-led-home.html#blog">بلاگ</a></div></div></section>
|
||||
</main>
|
||||
<footer class="site-foot"><div class="wrap"><div class="foot-grid"><div><div class="foot-brand"><img src="./assets/favicon.ico" alt=""><span>انجمن علمی مهندسی کامپیوتر</span></div><p>دانشکدهی فنی و مهندسی شرق گیلان، دانشگاه گیلان. فضایی برای یادگیری، رویداد و همکاری دانشجویی.</p></div><div><h3>دسترسی سریع</h3><a href="./d-logo-led-home.html">خانه</a><a href="#about">درباره انجمن</a><a href="./d-logo-led-home.html#events">رویدادها</a><a href="./d-logo-led-home.html#blog">بلاگ</a></div><div><h3>همراه انجمن</h3><a href="https://east-guilan-ce.ir/auth">ورود / ثبتنام</a><a href="./d-logo-led-home.html#events">برنامهها</a><a href="#contact">پیشنهاد همکاری</a></div><div><h3>ارتباط</h3><a class="contact-link" href="mailto:eastguilanceassociation@gmail.com"><svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#mail"/></svg><span dir="ltr">eastguilanceassociation@gmail.com</span></a><a class="contact-link" href="https://t.me/guilancea"><svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#send"/></svg><span>تلگرام انجمن</span></a><a class="contact-link" href="https://instagram.com/guilance.ir"><svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#instagram"/></svg><span>اینستاگرام انجمن</span></a></div></div><div class="foot-base"><span>انجمن علمی مهندسی کامپیوتر دانشگاه گیلان</span><a href="#about">بازگشت به بالا</a></div></div></footer>
|
||||
<nav class="mobile-nav" aria-label="ناوبری نمونه در موبایل"><a href="./d-logo-led-home.html">خانه</a><a href="./d-logo-led-home.html#events">رویدادها</a><a href="./d-logo-led-home.html#blog">بلاگ</a><a href="#about" aria-current="page">درباره</a></nav>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,52 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#28626e">
|
||||
<link rel="icon" href="./assets/favicon.ico">
|
||||
<link rel="stylesheet" href="./d-logo-led.css">
|
||||
<title>خانه — طرح مبتنی بر نشان انجمن</title>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-head"><div class="wrap head-inner">
|
||||
<a class="brand" href="#home"><img src="./assets/favicon.ico" alt=""><span><strong>انجمن علمی مهندسی کامپیوتر</strong><small>دانشکدهی فنی و مهندسی شرق گیلان</small></span></a>
|
||||
<nav class="top-nav" aria-label="ناوبری اصلی"><a href="#home" aria-current="page">خانه</a><a href="#events">رویدادها</a><a href="#blog">بلاگ</a><a href="./d-logo-led-about.html">درباره انجمن</a></nav>
|
||||
<div class="head-action"><a class="button outline" href="https://east-guilan-ce.ir/auth">ورود / ثبتنام</a></div>
|
||||
</div></header>
|
||||
<main id="home">
|
||||
<section class="hero" aria-labelledby="hero-title"><div class="wrap hero-inner">
|
||||
<div><h1 id="hero-title">انجمن علمی کامپیوتر دانشگاه گیلان</h1><p>در دانشکدهی فنی و مهندسی شرق گیلان، برای یادگیری، برگزاری رویدادهای تخصصی و بهاشتراکگذاشتن تجربههای دانشجویی کنار هم هستیم. اینجا میتوانید با انجمن و برنامههای آن آشنا شوید.</p><div class="hero-actions"><a class="button primary" href="./d-logo-led-about.html">آشنایی با انجمن <svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#arrow-up-left"/></svg></a><a class="button outline" href="#events">دیدن رویدادها</a></div></div>
|
||||
<div class="hero-visual" aria-hidden="true"><div class="gold-rule"></div><img src="./assets/favicon.ico" alt=""><span>یادگیری · رویداد · همکاری</span></div>
|
||||
</div></section>
|
||||
<section class="section" id="events" aria-labelledby="events-title"><div class="wrap">
|
||||
<div class="section-header"><div><h2 id="events-title">رویدادهای برگزیده</h2><p>نگاهی به برنامههای انجمن؛ وضعیت و زمان هر رویداد در صفحهی خودش مشخص میشود.</p></div><a class="section-link" href="https://east-guilan-ce.ir/events">همه رویدادها <svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#arrow-up-left"/></svg></a></div>
|
||||
<div class="cards">
|
||||
<article class="card"><div class="card-image"><img src="./assets/event-docker.webp" alt="پوستر کارگاه آشنایی با داکر"><span class="badge image-badge">نمونه از آرشیو</span></div><div class="card-content"><h3>کارگاه آشنایی با داکر</h3><p>آشنایی عملی با مفاهیم کانتینر و ابزارهای توسعه.</p><div class="card-meta"><svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#calendar-days"/></svg><span>جزئیات و زمان در صفحه رویداد</span></div></div><div class="card-foot"><a href="https://east-guilan-ce.ir/events">مشاهده رویداد <svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#arrow-up-left"/></svg></a></div></article>
|
||||
<article class="card"><div class="card-image"><img src="./assets/event-git.webp" alt="پوستر کارگاه گیت و گیتهاب"><span class="badge image-badge">نمونه از آرشیو</span></div><div class="card-content"><h3>کارگاه گیت و گیتهاب</h3><p>مروری بر همکاری تیمی، مدیریت نسخه و گردش کار توسعه.</p><div class="card-meta"><svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#calendar-days"/></svg><span>جزئیات و زمان در صفحه رویداد</span></div></div><div class="card-foot"><a href="https://east-guilan-ce.ir/events">مشاهده رویداد <svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#arrow-up-left"/></svg></a></div></article>
|
||||
<article class="card"><div class="card-image"><img src="./assets/event-algorithms.webp" alt="پوستر دوره ساختمان داده و طراحی الگوریتم"><span class="badge image-badge">نمونه از آرشیو</span></div><div class="card-content"><h3>ساختمان داده و طراحی الگوریتم</h3><p>دورهای برای تمرین حل مسئله و مفاهیم پایهی الگوریتم.</p><div class="card-meta"><svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#calendar-days"/></svg><span>جزئیات و زمان در صفحه رویداد</span></div></div><div class="card-foot"><a href="https://east-guilan-ce.ir/events">مشاهده رویداد <svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#arrow-up-left"/></svg></a></div></article>
|
||||
</div>
|
||||
</div></section>
|
||||
<section class="section alt" id="blog" aria-labelledby="blog-title"><div class="wrap">
|
||||
<div class="section-header"><div><h2 id="blog-title">از بلاگ انجمن</h2><p>نوشتهها و تجربههایی برای یادگیری فراتر از کلاس.</p></div><a class="section-link" href="https://east-guilan-ce.ir/blog">همه نوشتهها <svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#arrow-up-left"/></svg></a></div>
|
||||
<div class="blog-grid"><article class="card blog-lead"><div class="editorial-art" aria-hidden="true"><small>انجمن علمی مهندسی کامپیوتر</small><strong class="art-word">{ code }</strong><small>خواندن · تجربه کردن · ساختن</small></div><div><div class="card-content"><span class="badge gold">یادگیری</span><h3>از کجا یادگیری برنامهنویسی را شروع کنیم؟</h3><p>یک مسیر ساده برای انتخاب موضوع، تمرین مستمر و ساختن پروژههای کوچک.</p></div><div class="card-foot"><a href="https://east-guilan-ce.ir/blog">خواندن مطلب <svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#arrow-up-left"/></svg></a></div></div></article>
|
||||
<div class="blog-stack"><article class="card blog-row"><span class="blog-mark"><svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#book-open"/></svg></span><div><span class="badge">راهنما</span><h3>چطور برای یک پروژهی دانشجویی برنامهریزی کنیم؟</h3><p>از ایده تا نسخهی قابل ارائه</p><a class="section-link" href="https://east-guilan-ce.ir/blog">ادامه مطلب <svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#arrow-up-left"/></svg></a></div></article><article class="card blog-row"><span class="blog-mark gold"><svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#newspaper"/></svg></span><div><span class="badge gold">تجربه</span><h3>یادگیری گروهی چه کمکی به رشد مهارتها میکند؟</h3><p>تمرین، بازخورد و همکاری</p><a class="section-link" href="https://east-guilan-ce.ir/blog">ادامه مطلب <svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#arrow-up-left"/></svg></a></div></article></div></div>
|
||||
<p class="review-note">پوسترهای رویداد از رسانههای موجود پروژهاند. عنوانها و پوششهای بلاگ برای بررسی چیدمان نمایشی هستند؛ نسخهی نهایی، نوشتههای منتشرشده را از سامانه میخواند.</p>
|
||||
</div></section>
|
||||
<section class="cta" aria-labelledby="cta-title"><div class="wrap cta-inner"><div><h2 id="cta-title">در جریان برنامههای انجمن باشید</h2><p>رویدادها و نوشتههای تازه را دنبال کنید و اگر پیشنهادی برای همکاری دارید، با انجمن در ارتباط باشید.</p></div><div class="actions"><a class="button gold" href="https://east-guilan-ce.ir/events">دیدن رویدادها <svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#arrow-up-left"/></svg></a><a class="button outline" href="./d-logo-led-about.html#contact">ارتباط با انجمن</a></div></div></section>
|
||||
</main>
|
||||
<footer class="site-foot"><div class="wrap"><div class="foot-grid"><div><div class="foot-brand"><img src="./assets/favicon.ico" alt=""><span>انجمن علمی مهندسی کامپیوتر</span></div><p>دانشکدهی فنی و مهندسی شرق گیلان، دانشگاه گیلان. فضایی برای یادگیری، رویداد و همکاری دانشجویی.</p></div><div><h3>دسترسی سریع</h3><a href="#home">خانه</a><a href="./d-logo-led-about.html">درباره انجمن</a><a href="https://east-guilan-ce.ir/events">رویدادها</a><a href="https://east-guilan-ce.ir/blog">بلاگ</a></div><div><h3>همراه انجمن</h3><a href="https://east-guilan-ce.ir/auth">ورود / ثبتنام</a><a href="https://east-guilan-ce.ir/events">برنامهها</a><a href="./d-logo-led-about.html#contact">پیشنهاد همکاری</a></div><div><h3>ارتباط</h3><a class="contact-link" href="mailto:eastguilanceassociation@gmail.com"><svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#mail"/></svg><span dir="ltr">eastguilanceassociation@gmail.com</span></a><a class="contact-link" href="https://t.me/guilancea"><svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#send"/></svg><span>تلگرام انجمن</span></a><a class="contact-link" href="https://instagram.com/guilance.ir"><svg class="icon" aria-hidden="true"><use href="./d-lucide-icons.svg#instagram"/></svg><span>اینستاگرام انجمن</span></a></div></div><div class="foot-base"><span>انجمن علمی مهندسی کامپیوتر دانشگاه گیلان</span><a href="#home">بازگشت به بالا</a></div></div></footer>
|
||||
<nav class="mobile-nav" aria-label="ناوبری نمونه در موبایل"><a href="#home" aria-current="page">خانه</a><a href="#events">رویدادها</a><a href="#blog">بلاگ</a><a href="./d-logo-led-about.html">درباره</a></nav>
|
||||
<script>
|
||||
function syncNav(){
|
||||
const current = location.hash === '#events' || location.hash === '#blog' ? location.hash : '#home';
|
||||
for (const link of document.querySelectorAll('.mobile-nav a[href^="#"],.top-nav a[href^="#"]')) {
|
||||
if (link.getAttribute('href') === current) link.setAttribute('aria-current','page');
|
||||
else link.removeAttribute('aria-current');
|
||||
}
|
||||
}
|
||||
addEventListener('hashchange',syncNav);
|
||||
syncNav();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,18 +0,0 @@
|
||||
@font-face{font-family:Vazirmatn;src:url('./assets/Vazirmatn-variable.woff2') format('woff2');font-display:swap}
|
||||
:root{--brand:#28626e;--brand-deep:#1c4c57;--gold:#f9e697;--charcoal:#313332;--ink:#22363a;--muted:#53686b;--line:#d9e5e4;--wash:#f3f8f7;--warm:#faf8f0;--white:#fff;--radius:16px}
|
||||
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;color:var(--ink);background:var(--white);font:400 16px/1.8 Vazirmatn,Tahoma,sans-serif}a{color:inherit}button{font:inherit}a:focus-visible,button:focus-visible,summary:focus-visible{outline:3px solid var(--brand);outline-offset:3px}::selection{background:var(--gold);color:var(--charcoal)}.wrap{width:min(100% - 48px,1180px);margin-inline:auto}
|
||||
.site-head{position:sticky;top:0;z-index:20;background:#fffffff2;border-bottom:1px solid var(--line);backdrop-filter:blur(12px)}.head-inner{min-height:76px;display:flex;align-items:center;justify-content:space-between;gap:28px}.brand{display:flex;align-items:center;gap:11px;min-width:0;text-decoration:none}.brand img{width:44px;height:44px;object-fit:contain}.brand strong{display:block;font-weight:800;font-size:15px;line-height:1.45}.brand small{display:block;font-size:11px;color:var(--muted);line-height:1.5}.top-nav{display:flex;align-items:center;gap:5px}.top-nav a{font-size:14px;font-weight:650;color:var(--muted);padding:9px 14px;border-radius:10px;text-decoration:none}.top-nav a:hover,.top-nav a[aria-current=page]{color:var(--brand-deep);background:var(--wash)}.head-action{display:flex;align-items:center;gap:10px}
|
||||
.button{display:inline-flex;align-items:center;justify-content:center;gap:9px;min-height:44px;padding:9px 18px;border-radius:10px;border:1px solid transparent;text-decoration:none;font-size:14px;font-weight:750;line-height:1.45;white-space:nowrap;transition:background .18s,color .18s,border-color .18s,transform .18s}.button svg{width:17px;height:17px;flex:none}.button.primary{background:var(--brand);color:white}.button.primary:hover{background:var(--brand-deep)}.button.outline{border-color:#b8ceca;background:white;color:var(--brand-deep)}.button.outline:hover{background:var(--wash)}.button.ghost{color:var(--brand-deep)}.button.ghost:hover{background:var(--wash)}.button.gold{background:var(--gold);color:var(--charcoal)}.button.gold:hover{background:#ffeeb0}.button:active{transform:translateY(1px)}
|
||||
.icon{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;flex:none}.badge{display:inline-flex;align-items:center;gap:5px;border:1px solid var(--line);border-radius:999px;padding:3px 11px;color:var(--brand-deep);background:var(--wash);font-size:12px;font-weight:700;line-height:1.65}.badge.gold{background:#fff8db;border-color:#efe2a7;color:#655019}.badge.neutral{color:var(--muted);background:white}
|
||||
.hero{background:linear-gradient(180deg,#f8fbfa 0%,white 100%);border-bottom:1px solid var(--line)}.hero-inner{display:grid;grid-template-columns:minmax(0,1.14fr) minmax(330px,.86fr);gap:clamp(35px,5vw,80px);align-items:center;min-height:496px;padding-block:62px}.hero h1{font-size:clamp(39px,4.2vw,62px);line-height:1.3;letter-spacing:-.025em;font-weight:850;margin:0 0 19px;max-width:15ch}.hero p{max-width:50ch;color:var(--muted);font-size:18px;line-height:1.95;margin:0 0 25px}.hero-actions{display:flex;gap:11px;flex-wrap:wrap}.hero-visual{position:relative;min-height:330px;border-radius:22px;background:var(--brand);display:grid;place-items:center;overflow:hidden}.hero-visual:before{content:"";position:absolute;inset:18px;border:1px solid #ffffff38;border-radius:14px}.hero-visual:after{content:"";position:absolute;left:-55px;bottom:-105px;width:270px;height:270px;border:1px solid #f9e69766;border-radius:50%}.hero-visual img{position:relative;z-index:1;width:190px;height:190px;object-fit:contain;filter:drop-shadow(0 17px 30px #102c3080)}.hero-visual span{position:absolute;right:30px;bottom:24px;color:#e8f4f2;font-size:13px;font-weight:650}.hero-visual .gold-rule{position:absolute;top:0;right:0;left:0;height:5px;background:var(--gold)}
|
||||
.section{padding-block:77px}.section.alt{background:var(--wash)}.section-header{display:flex;align-items:end;justify-content:space-between;gap:24px;margin-bottom:27px}.section-header h2{font-size:clamp(27px,2.7vw,38px);font-weight:800;line-height:1.4;margin:0}.section-header p{color:var(--muted);margin:6px 0 0;font-size:15px}.section-link{display:inline-flex;align-items:center;gap:6px;color:var(--brand-deep);font-size:14px;font-weight:750;text-decoration:none;white-space:nowrap}.section-link:hover{text-decoration:underline;text-underline-offset:5px}.section-link svg{width:17px;height:17px}
|
||||
.cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}.card{min-width:0;overflow:hidden;background:white;border:1px solid var(--line);border-radius:var(--radius);display:flex;flex-direction:column;transition:transform .2s,border-color .2s}.card:hover{transform:translateY(-3px);border-color:#b6ceca}.card-image{position:relative;aspect-ratio:16/9;overflow:hidden;background:#e5eeeb}.card-image img{width:100%;height:100%;object-fit:cover;display:block}.card-image .image-badge{position:absolute;right:13px;top:13px;background:#fffef5ed;border-color:#fff;color:var(--ink)}.card-content{padding:19px 20px 0;flex:1}.card-content h3{font-size:18px;line-height:1.6;font-weight:800;margin:0 0 9px}.card-content p{font-size:14px;line-height:1.8;color:var(--muted);margin:0}.card-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;color:var(--muted);font-size:12px;margin-top:15px}.card-meta svg{width:15px;height:15px}.card-foot{padding:17px 20px 19px}.card-foot a{display:inline-flex;align-items:center;gap:5px;color:var(--brand-deep);text-decoration:none;font-size:14px;font-weight:750}.card-foot a:hover{text-decoration:underline;text-underline-offset:4px}.card-foot svg{width:16px;height:16px}
|
||||
.blog-grid{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(0,.9fr);gap:18px}.blog-lead{display:grid;grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);min-height:320px}.editorial-art{min-height:220px;background:var(--brand-deep);color:var(--gold);padding:24px;display:flex;flex-direction:column;justify-content:space-between;position:relative;overflow:hidden}.editorial-art:after{content:"";position:absolute;width:230px;height:230px;border:1px solid #f9e69785;border-radius:50%;left:-85px;top:46px}.editorial-art .art-word{font-size:clamp(46px,5vw,74px);line-height:1;letter-spacing:-.04em;font-weight:900;direction:ltr;align-self:flex-end;opacity:.92}.editorial-art small{font-size:13px;color:#fff8d2;position:relative;z-index:1}.blog-lead .card-content{padding:28px 26px 0}.blog-lead .card-content h3{font-size:23px}.blog-stack{display:grid;gap:14px}.blog-row{display:flex;align-items:center;gap:17px;padding:21px;min-height:150px}.blog-row .blog-mark{width:76px;height:76px;border-radius:14px;display:grid;place-items:center;flex:none;background:#e8f2f0;color:var(--brand-deep)}.blog-row .blog-mark.gold{background:#fff6d1;color:#6b5716}.blog-row .blog-mark svg{width:30px;height:30px}.blog-row h3{font-size:17px;margin:7px 0 5px}.blog-row p{color:var(--muted);font-size:13px;margin:0}.blog-row .badge{font-size:11px;padding:1px 8px}
|
||||
.cta{background:var(--brand-deep);color:white;position:relative;overflow:hidden}.cta:before{content:"";position:absolute;width:410px;height:410px;border:1px solid #f9e6975e;border-radius:50%;left:-110px;top:-135px}.cta-inner{position:relative;display:flex;justify-content:space-between;align-items:center;gap:30px;padding-block:67px}.cta h2{font-size:clamp(27px,3vw,40px);line-height:1.4;margin:0 0 9px}.cta p{margin:0;color:#d0e2e0;max-width:60ch}.cta .actions{display:flex;gap:10px;flex-wrap:wrap}.cta .button.outline{background:transparent;border-color:#a5c6c6;color:white}.cta .button.outline:hover{background:#ffffff1a}
|
||||
.site-foot{background:var(--charcoal);color:#e9eeed;padding:55px 0 27px}.foot-grid{display:grid;grid-template-columns:1.55fr .8fr .8fr 1.15fr;gap:35px}.foot-brand{display:flex;align-items:center;gap:12px;font-size:17px;font-weight:750}.foot-brand img{width:48px;height:48px}.site-foot p{color:#c0cdcb;font-size:13px;line-height:1.9;max-width:37ch}.site-foot h3{margin:4px 0 14px;font-size:14px;color:white}.site-foot a{display:block;color:#d4dedc;text-decoration:none;font-size:13px;margin-bottom:9px;overflow-wrap:anywhere}.site-foot a:hover{text-decoration:underline;text-underline-offset:4px}.site-foot .contact-link{display:flex;align-items:start;gap:8px}.site-foot .contact-link svg{width:16px;height:16px;margin-top:4px}.foot-base{display:flex;align-items:center;justify-content:space-between;gap:15px;margin-top:39px;padding-top:19px;border-top:1px solid #ffffff30;color:#b7c5c2;font-size:12px}.foot-base a{margin:0}
|
||||
.about-hero{background:var(--wash);padding:85px 0 72px;border-bottom:1px solid var(--line)}.about-hero h1{font-size:clamp(38px,4.2vw,58px);line-height:1.3;margin:0 0 17px}.about-hero p{max-width:70ch;color:var(--muted);font-size:18px;margin:0 0 25px}.about-hero .full-name{border-right:3px solid var(--gold);padding-right:14px;font-size:14px;font-weight:650;color:var(--brand-deep)}.about-columns{display:grid;grid-template-columns:1.1fr .9fr;gap:65px;align-items:start}.about-columns h2{margin:0 0 16px;font-size:31px}.about-columns p{color:var(--muted);margin:0 0 15px}.about-points{display:grid;gap:12px}.about-point{display:flex;align-items:start;gap:13px;padding:15px 0;border-bottom:1px solid var(--line)}.about-point .icon-box{width:40px;height:40px;border-radius:10px;background:var(--wash);color:var(--brand-deep);display:grid;place-items:center;flex:none}.about-point strong{display:block;font-size:16px}.about-point span{font-size:13px;color:var(--muted)}.about-contact{background:var(--warm);border:1px solid #eae4cb;border-radius:var(--radius);padding:30px}.about-contact h2{margin:0 0 11px}.about-contact a{display:block;color:var(--brand-deep);overflow-wrap:anywhere;text-underline-offset:4px;margin:9px 0}.about-contact a[dir=ltr]{text-align:right}.faq details{border-bottom:1px solid var(--line);padding:18px 0}.faq summary{cursor:pointer;font-weight:700}.faq details p{color:var(--muted);margin:10px 0 0;max-width:70ch}
|
||||
.mobile-nav{display:none}.review-note{font-size:12px;color:var(--muted);margin:22px 0 0}#home,#events,#blog,#contact,#about{scroll-margin-top:88px}
|
||||
@media(max-width:1000px){.hero-inner{grid-template-columns:1fr .8fr}.cards{gap:12px}.blog-grid{grid-template-columns:1fr}.blog-stack{grid-template-columns:1fr 1fr}.foot-grid{grid-template-columns:1.4fr 1fr 1fr}.foot-grid>div:last-child{grid-column:1/-1}}
|
||||
@media(max-width:760px){.wrap{width:min(100% - 32px,1180px)}.head-inner{min-height:67px}.brand img{width:39px;height:39px}.brand small{display:none}.brand strong{font-size:14px}.top-nav,.head-action{display:none}.hero-inner{grid-template-columns:1fr;gap:28px;min-height:0;padding-block:54px 40px}.hero h1{font-size:clamp(36px,8vw,48px);max-width:18ch}.hero p{font-size:16px}.hero-visual{min-height:175px}.hero-visual img{width:108px;height:108px}.hero-visual span{font-size:11px;right:18px;bottom:12px}.hero-visual:after{width:160px;height:160px;bottom:-70px;left:-30px}.section{padding-block:56px}.section-header{align-items:start}.section-header h2{font-size:29px}.cards{grid-template-columns:1fr;gap:15px}.card-image{aspect-ratio:16/8.4}.blog-grid,.blog-stack{grid-template-columns:1fr}.blog-lead{grid-template-columns:1fr}.editorial-art{min-height:180px}.blog-row{min-height:130px}.cta-inner{display:block;padding-block:55px}.cta .actions{margin-top:22px}.foot-grid{grid-template-columns:1fr 1fr;gap:27px}.foot-grid>div:first-child,.foot-grid>div:last-child{grid-column:1/-1}.site-foot{padding-bottom:120px}.about-hero{padding:65px 0 55px}.about-columns{grid-template-columns:1fr;gap:28px}.mobile-nav{position:fixed;z-index:30;bottom:calc(12px + env(safe-area-inset-bottom));right:16px;left:16px;display:flex;justify-content:space-around;gap:3px;padding:7px;border:1px solid #cadbd8;border-radius:20px;background:#fffffff2;box-shadow:0 12px 34px #143e4640;backdrop-filter:blur(14px)}.mobile-nav a{flex:1;text-align:center;text-decoration:none;color:var(--muted);font-size:11px;font-weight:700;padding:8px 2px;border-radius:14px}.mobile-nav a[aria-current=page]{background:var(--brand);color:white}}
|
||||
@media(max-width:480px){.hero-actions,.cta .actions{flex-direction:column}.hero-actions .button,.cta .button{width:100%}.section-header{display:block}.section-link{margin-top:8px}.foot-grid{grid-template-columns:1fr 1fr}.foot-base{display:block}.foot-base a{margin-top:7px}}
|
||||
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}.button,.card{transition:none}}
|
||||
@@ -1,12 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
|
||||
<symbol id="arrow-up-left" viewBox="0 0 24 24"><path d="M7 17V7h10"/><path d="M17 17 7 7"/></symbol>
|
||||
<symbol id="calendar-days" viewBox="0 0 24 24"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M16 14h.01"/><path d="M8 18h.01"/><path d="M12 18h.01"/><path d="M16 18h.01"/></symbol>
|
||||
<symbol id="book-open" viewBox="0 0 24 24"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></symbol>
|
||||
<symbol id="mail" viewBox="0 0 24 24"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></symbol>
|
||||
<symbol id="send" viewBox="0 0 24 24"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/></symbol>
|
||||
<symbol id="instagram" viewBox="0 0 24 24"><rect width="20" height="20" x="2" y="2" rx="5" ry="5"/><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/><line x1="17.5" x2="17.51" y1="6.5" y2="6.5"/></symbol>
|
||||
<symbol id="clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16.5 12"/></symbol>
|
||||
<symbol id="users" viewBox="0 0 24 24"><path d="M18 21a8 8 0 0 0-16 0"/><circle cx="10" cy="8" r="5"/><path d="M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3"/></symbol>
|
||||
<symbol id="graduation-cap" viewBox="0 0 24 24"><path d="M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0z"/><path d="M22 10v6"/><path d="M6 12.5V16a6 3 0 0 0 12 0v-3.5"/></symbol>
|
||||
<symbol id="newspaper" viewBox="0 0 24 24"><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/><path d="M18 14h-8"/><path d="M15 18h-5"/><path d="M10 6h8v4h-8V6Z"/></symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,73 +0,0 @@
|
||||
# Bundle 0 public content inventory
|
||||
|
||||
Captured 2026-09-24 from the local frontend source and browser. This is a classification record, not an approved seed. `E` = editorial/site identity (future CMS), `D` = existing domain data, `U` = functional UI kept in code. Paths below are relative to `guilan-ace-frontend/`. Every public route is listed; repeated controls inherit the shared-shell row.
|
||||
|
||||
The [line-level source ledger](content-source-lines.csv) records all Persian-bearing, URL and image-reference lines in the static public-content files with an E/U classification and exact source text. Regenerate it with [the extraction script](build-content-line-ledger.ps1). Dynamic event/blog values are not hard-coded literals, so their domain ownership is mapped in the route table instead. Mixed lines are annotated in the ledger and must be split field by field in the T12 seed manifest.
|
||||
|
||||
## Route map
|
||||
|
||||
| Route | Source | E: future CMS | D: existing model/API | U: code-owned |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `/` | `src/app/page.tsx`, `src/views/Home.tsx` | Identity, hero, three offering descriptions, closing invitation, page SEO and Organization JSON-LD | None today | Link destinations and action labels, layout |
|
||||
| `/about` | `src/app/about/page.tsx`, `src/views/AboutUs.tsx` | Identity, mission, values, affiliations, committees, FAQ answers, contact/address, page SEO and AboutPage JSON-LD | None today | FAQ interaction, external-link behavior |
|
||||
| `/events` | `src/app/events/page.tsx`, `src/views/Events.tsx` | Listing introduction and default SEO/site name | Event list, dates, prices, capacity, status, images and filters from events API | Search/filter labels, loading/empty/error text and controls; query-specific SEO wording |
|
||||
| `/events/[slug]` | `src/app/events/[slug]/page.tsx`, `src/views/EventDetail.tsx` | Shared identity only | Event title/body/poster, organizer, eligibility, registration dates/capacity, price, SEO and registration-success markdown from event API | Registration/payment actions, validation, state and error messages; Event JSON-LD structure |
|
||||
| `/events/[slug]/success` | `src/app/events/[slug]/success/page.tsx`, `src/views/EventFreeSuccessPage.tsx` | Shared identity only | Verified registration/ticket/event details from API | Confirmation, pending/failure states and metadata labels |
|
||||
| `/payments/result` | `src/app/payments/result/page.tsx`, `src/views/PaymentResult.tsx` | Shared identity only | Verified payment and event details from API | Callback outcome text, retry and status controls |
|
||||
| `/blog` | `src/app/blog/page.tsx`, `src/views/Blog.tsx` | Listing introduction and default SEO/site name | Banners, posts, categories, tags, authors and images from blog API | Search/filter/pagination, loading/empty/error text; query-specific SEO wording |
|
||||
| `/blog/[slug]` | `src/app/blog/[slug]/page.tsx` | Shared identity only | Post text, authors, media, metadata, comments/interactions from blog API | Reading controls, sharing, error/empty text and BlogPosting JSON-LD structure |
|
||||
| `/auth`, `/auth/google/callback` | `src/app/auth/**`, `src/views/Auth.tsx`, `src/views/GoogleAuthCallback.tsx` | Shared identity only | Account existence, OTP and authentication responses | Form labels, constraints, steps, errors, resend/callback states |
|
||||
| `/verify-email/[token]`, `/reset-password`, `/reset-password/[token]`, `/logout` | matching `src/app/**`, `src/views/**` | Shared identity only | Verification/reset result from auth API | Instructions, form labels, success/failure states |
|
||||
| `/profile` | `src/app/profile/page.tsx`, `src/views/Profile.tsx` | Shared identity only | User profile and notifications | Profile controls and validation |
|
||||
| `/admin/**` | `src/app/admin/**`, `src/views/Admin*.tsx` | Shared identity only | Events, users, blog and taxonomy records | Staff navigation, forms, authorization, errors and confirmations |
|
||||
| not found | `src/app/not-found.tsx` | Shared identity only | None | Error explanation and navigation |
|
||||
|
||||
## Shared shell and static source fields
|
||||
|
||||
| Stable group | Current source | Destination and notes |
|
||||
| --- | --- | --- |
|
||||
| `site.official_name`, `site.short_name`, `site.faculty_subline` | `src/components/Navbar.tsx:135-139`, `Footer.tsx:20`, `src/app/layout.tsx:20-27`; Home/About have more variants | E. Official name from `tasks/plan.md`; preserve the current header's short lockup and faculty subline as the refinement basis. Existing variants must be replaced together after cutover. |
|
||||
| `site.logo`, `site.logo_alt` | `public/favicon.ico`; Navbar, Footer, Home/About OG and JSON-LD | E. Preserve source logo; CMS media copy later. `favicon.ico` is the only association mark found in `public/`. |
|
||||
| `site.navigation` | `Navbar.tsx:143-145`, `MobileBottomNav.tsx:38-59`, `Footer.tsx:27-49` | E for public link label/order/visibility; U for route IDs, selected state, auth/profile actions, responsive behavior. No arbitrary external route can bypass URL validation. |
|
||||
| `site.footer_description`, `site.copyright` | `Footer.tsx:22-25,125` | E for descriptive copy and name; U for current year. |
|
||||
| `site.contacts`, `site.social_links`, `site.address` | `Footer.tsx:51-98`, `AboutUs.tsx:117-136,315-344`, `src/app/page.tsx:39-43` | E. User selected About-page contact values on 2026-09-24. Footer and JSON-LD conflicts are listed below. Keep route-level links functional. |
|
||||
| `site.seal` | `public/enamad.png`, `Footer.tsx:103-118` | E for approved seal image/destination; verify legal ownership and destination before moving into CMS. |
|
||||
| `site.seo_defaults` | `src/app/layout.tsx:20-27`; site name repeated in events/blog/event/post metadata | E for title/description/OG identity; U for canonical URL construction, search `noindex`, schema structure. |
|
||||
| `home.hero` | `Home.tsx:9-11,47-51` | E: title and paragraph. |
|
||||
| `home.offering_events`, `home.offering_articles`, `home.offering_community` | `Home.tsx:68-101` | E: headings, subtitles, paragraphs; U: links to existing routes. These are editorial claims, not new domain entities. |
|
||||
| `home.join` | `Home.tsx:106-122` | E: heading and explanatory paragraph; U: signup action/route. |
|
||||
| `home.seo` | `src/app/page.tsx:5-48`, `Home.tsx:13-43` | E: title, description, OG image, organization name/description/contact; U: canonical and JSON-LD field structure. Two SEO implementations currently duplicate values. |
|
||||
| `about.intro`, `about.mission`, `about.values` | `AboutUs.tsx:46-55,163-200` | E. Intro count claims need evidence before publication. |
|
||||
| `about.affiliations` | `AboutUs.tsx:58-99,234-283` | E if association relationship is confirmed. Names, establishment years, focus areas and links require owner review; do not seed as verified facts merely because code contains them. |
|
||||
| `about.committees` | `AboutUs.tsx:100-113,289-302` | E. Committee existence and descriptions require owner review. |
|
||||
| `about.faq` | `AboutUs.tsx:304-310` | E for questions/answers; U for `<details>` disclosure and registration route semantics. The student-ID assertion needs backend confirmation. |
|
||||
| `about.location`, `about.join` | `AboutUs.tsx:315-348` | E for address, postcode, telephone, map URL and invitation; U for mail/Telegram link actions. |
|
||||
| `about.seo` | `src/app/about/page.tsx:5-57`, `AboutUs.tsx:7-40,139-160` | E for title, description, keywords, OG image, identity and sameAs; U for canonical/schema structure. Duplicated client/server SEO values conflict. |
|
||||
| `events.intro`, `blog.intro` | `src/app/events/page.tsx:19-37`, `src/app/blog/page.tsx:24-42` | E for default listing title/description/OG image; U for search-specific title/description and robots behavior. |
|
||||
|
||||
Every image reference in the targeted static public shell is covered by `site.logo`, `site.seal`, or the external About map. Event posters, blog images, banners, avatars and user uploads stay with their existing domain records. There is no Home/About photography in the frontend repository. The owner's revised Home proposal adds featured event and post previews after the intro. Those previews are **D** content from existing APIs; only their section headings/intro/link labels and CTA are **E**. The existing three-offering copy remains in the legacy inventory for audit but is no longer a required Home section.
|
||||
|
||||
## Conflicts and decisions
|
||||
|
||||
| Field | Conflicting source values | Status |
|
||||
| --- | --- | --- |
|
||||
| Association name | Home says `انجمن علمی کامپیوتر دانشگاه گیلان`; Navbar says `انجمن علمی مهندسی کامپیوتر`; Footer says `انجمن علمی کامپیوتر گیلان`; About leads with plural `انجمنهای علمی کامپیوتر گیلان` | Plan supplies official full name. The current header's short name remains the refinement basis; legacy variants stay listed for migration review. |
|
||||
| Email | Home JSON-LD `admin@east-guilan-ce.ir`; Footer `info@east-guilan-ce.ir`; About `eastguilanceassociation@gmail.com` | User selected About-page contact on 2026-09-24. |
|
||||
| Telegram | Footer `t.me/guilance`; About primary contact `t.me/guilancea`; About structured data lists both | User selected `t.me/guilancea` on 2026-09-24. |
|
||||
| Instagram | Footer/About current association link is `instagram.com/guilance.ir`; About affiliate links include others | Current association link retained provisionally; affiliate links await relationship review. |
|
||||
| Other social | Footer links to a personal LinkedIn profile and generic `x.com` | Do not seed into association identity without owner confirmation. |
|
||||
| Statistics and dates | About says `۱۴۰۴`, `۱۰۰+`, `۱۰+`, `۲۰+`, while affiliation rows say `۱۳۹۲` and `۱۳۵۳` | Owner says repository facts are verified; map each value to its entity and as-of date before automatic publishing. Not needed in identity-first concepts. |
|
||||
| Address/hours | Footer has a broad faculty address; About adds Vajjargah, postcode, phone and response hours | User says in-repo contacts are verified; selected About page is source for contact details. Preserve exact legacy values in seed review. |
|
||||
| About association links | `AboutUs.tsx:67` has a leading quote in the Instagram URL | Treat as a source defect; no automatic link publication. |
|
||||
|
||||
## Asset ownership
|
||||
|
||||
| Asset | Current use | Owner/approval action |
|
||||
| --- | --- | --- |
|
||||
| `public/favicon.ico` | Navbar/Footer mark, browser favicon, OG and structured data | Current recognizable logo approved by user as repository asset. Need original vector/high-resolution master, license/source note, and CMS media copy before release. |
|
||||
| `public/enamad.png` | Footer trust seal | Existing site asset; verify seal account and permitted use before migrating. |
|
||||
| `src/app/fonts/Vazirmatn-variable.woff2` | All UI text | Code-owned font file; record license before redistributing in a new design bundle. |
|
||||
| `public/placeholder.svg` | Generic placeholder | Code-owned fallback, not editorial association imagery. |
|
||||
| Backend event/blog/gallery media | Dynamic listings/details | Existing domain data; ownership stays with those records. |
|
||||
|
||||
Source code was classified against local rendering at 390px and 1440px. The owner accepted the content boundary and exclusions in `bundle0-review.md` on 2026-09-26, closing Checkpoint A at inventory scope. Uncertain claims and URLs remain flagged for later seed review; no claim or URL is silently corrected here.
|
||||
@@ -1,209 +0,0 @@
|
||||
"source","line","category","source_text","note"
|
||||
"src/views/Home.tsx","10","CMS editorial","const heroTitle = ""انجمن علمی کامپیوتر دانشگاه گیلان"";",""
|
||||
"src/views/Home.tsx","12","CMS editorial","""با ما همراه شوید و در دنیای مهندسی/علوم کامپیوتر و فناوری پیشرفت کنید. رویدادها، محتوای آموزشی و جامعهای پویا برای رشد شما فراهم است."";",""
|
||||
"src/views/Home.tsx","15","code-owned UI","""@context"": ""https://schema.org"",","Schema structure or vocabulary; referenced identity values remain editorial."
|
||||
"src/views/Home.tsx","18","CMS editorial","url: ""https://east-guilan-ce.ir"",",""
|
||||
"src/views/Home.tsx","19","CMS editorial","sameAs: [""https://east-guilan-ce.ir/blog"", ""https://east-guilan-ce.ir/events""],",""
|
||||
"src/views/Home.tsx","21","CMS editorial","logo: ""https://east-guilan-ce.ir/favicon.ico"",",""
|
||||
"src/views/Home.tsx","39","CMS editorial","<meta property=""og:url"" content=""https://east-guilan-ce.ir"" />",""
|
||||
"src/views/Home.tsx","40","CMS editorial","<meta property=""og:image"" content=""https://east-guilan-ce.ir/favicon.ico"" />",""
|
||||
"src/views/Home.tsx","54","code-owned UI","<Button size=""lg"" aria-label=""مشاهده رویدادهای انجمن"">",""
|
||||
"src/views/Home.tsx","55","code-owned UI","مشاهده رویدادها",""
|
||||
"src/views/Home.tsx","59","code-owned UI","<Button size=""lg"" variant=""outline"" aria-label=""مطالعه مقالات آموزشی"">",""
|
||||
"src/views/Home.tsx","60","code-owned UI","خواندن مقالات",""
|
||||
"src/views/Home.tsx","70","CMS editorial","درباره انجمن",""
|
||||
"src/views/Home.tsx","75","CMS editorial","<CardTitle>رویدادهای تخصصی</CardTitle>",""
|
||||
"src/views/Home.tsx","76","CMS editorial","<CardDescription>برگزاری کارگاهها، سمینارها و نشستهای علمی</CardDescription>",""
|
||||
"src/views/Home.tsx","80","CMS editorial","با شرکت در رویدادهای ما، دانش و مهارتهای خود را در کنار اساتید و متخصصان ارتقا دهید.",""
|
||||
"src/views/Home.tsx","87","CMS editorial","<CardTitle>مقالات آموزشی</CardTitle>",""
|
||||
"src/views/Home.tsx","88","CMS editorial","<CardDescription>دسترسی به مطالب آموزشی و پژوهشی باکیفیت</CardDescription>",""
|
||||
"src/views/Home.tsx","92","CMS editorial","تازهترین مقالات تخصصی در حوزههای مختلف علوم کامپیوتر برای یادگیری و توسعه فردی.",""
|
||||
"src/views/Home.tsx","99","CMS editorial","<CardTitle>جامعه فعال</CardTitle>",""
|
||||
"src/views/Home.tsx","100","CMS editorial","<CardDescription>ارتباط با دانشجویان و اساتید علاقهمند به فناوری</CardDescription>",""
|
||||
"src/views/Home.tsx","104","CMS editorial","به جامعهای پویا بپیوندید و از فرصتهای همکاری، شبکهسازی و رشد شخصی بهرهمند شوید.",""
|
||||
"src/views/Home.tsx","115","CMS editorial","آماده عضویت هستید؟",""
|
||||
"src/views/Home.tsx","118","CMS editorial","همین حالا به جمع ما بپیوندید و از برنامهها و فرصتهای یادگیری انجمن علمی کامپیوتر دانشگاه گیلان استفاده کنید.",""
|
||||
"src/views/Home.tsx","121","code-owned UI","<Button size=""lg"" aria-label=""ثبت نام در انجمن علمی کامپیوتر"">",""
|
||||
"src/views/Home.tsx","122","code-owned UI","ثبتنام کنید",""
|
||||
"src/views/AboutUs.tsx","6","CMS editorial","// ------ دادههای قابل تنظیم ------",""
|
||||
"src/views/AboutUs.tsx","7","CMS editorial","const SITE_URL = 'https://east-guilan-ce.ir';",""
|
||||
"src/views/AboutUs.tsx","8","CMS editorial","const SITE_NAME_FA = 'انجمن علمی کامپیوتر شرق گیلان';",""
|
||||
"src/views/AboutUs.tsx","10","CMS editorial","const ABOUT_TITLE = `درباره ما | ${SITE_NAME_FA}`;",""
|
||||
"src/views/AboutUs.tsx","12","CMS editorial","'آشنایی با تاریخچه، مأموریتها و دستاوردهای انجمن علمی کامپیوتر شرق گیلان و راههای مشارکت دانشجویان در برنامههای انجمن.';",""
|
||||
"src/views/AboutUs.tsx","14","CMS editorial","'انجمن علمی کامپیوتر شرق گیلان, انجمن علمی مهندسی کامپیوتر دانشگاه گیلان، انجمن علمی علوم کامپیوتر، انجمن علمی شرق گیلان، انجمن علمی مهندسی کامپیوتر شرق گیلان، دانشکده فنی و مهندسی شرق گیلان، انجمن علمی کامپیوتر, فعالیتهای دانشجویی, انجمنهای علمی ایران, رویدادهای فناوری، برنامه نویسی، انجمن علمی دانشجویی، دانشگاه گیلان، فنی شرق';",""
|
||||
"src/views/AboutUs.tsx","17","code-owned UI","'@context': 'https://schema.org',","Schema structure or vocabulary; referenced identity values remain editorial."
|
||||
"src/views/AboutUs.tsx","26","CMS editorial","logo: `${SITE_URL}/favicon.ico`,",""
|
||||
"src/views/AboutUs.tsx","28","CMS editorial","'https://instagram.com/guilance.ir',",""
|
||||
"src/views/AboutUs.tsx","29","CMS editorial","'https://t.me/guilance',",""
|
||||
"src/views/AboutUs.tsx","30","CMS editorial","'https://t.me/guilancea'",""
|
||||
"src/views/AboutUs.tsx","46","CMS editorial","title: 'انجمنهای علمی کامپیوتر گیلان', // تیتر کلی",""
|
||||
"src/views/AboutUs.tsx","48","CMS editorial","'پیوند دانشگاه و صنعت با برگزاری رویدادها، کارگاهها و نشستهای تخصصی — با محوریت رشد مهارتهای فنی و مسیرهای شغلی.',",""
|
||||
"src/views/AboutUs.tsx","49","CMS editorial","foundedYear: '۱۴۰۴',",""
|
||||
"src/views/AboutUs.tsx","50","CMS editorial","membersApprox: '۱۰۰+',",""
|
||||
"src/views/AboutUs.tsx","51","CMS editorial","eventsCount: '۱۰+',",""
|
||||
"src/views/AboutUs.tsx","52","CMS editorial","volunteersCount: '۲۰+',",""
|
||||
"src/views/AboutUs.tsx","55","CMS editorial","// اگر چند انجمن زیرمجموعه دارید، اینجا معرفی کنید",""
|
||||
"src/views/AboutUs.tsx","58","CMS editorial","name: 'انجمن علمی مهندسی کامپیوتر دانشکدهی فنی شرق',",""
|
||||
"src/views/AboutUs.tsx","59","CMS editorial","university: 'دانشگاه گیلان',",""
|
||||
"src/views/AboutUs.tsx","60","CMS editorial","city: 'رودسر',",""
|
||||
"src/views/AboutUs.tsx","61","CMS editorial","foundedYear: '۱۳۹۲',",""
|
||||
"src/views/AboutUs.tsx","63","CMS editorial","'برگزاری رویدادهای آموزشی و صنعتی، منتورینگ دانشجویی، و اتصال دانشجویان به فرصتهای شغلی و پژوهشی.',",""
|
||||
"src/views/AboutUs.tsx","64","CMS editorial","focusAreas: ['مهندسی نرمافزار', 'هوش مصنوعی و داده', 'طراحی محصول', 'DevOps/امنیت'],",""
|
||||
"src/views/AboutUs.tsx","66","CMS editorial","website: 'https://east-guilan-ce.ir',",""
|
||||
"src/views/AboutUs.tsx","67","CMS editorial","instagram: '""https://instagram.com/guilance.ir',",""
|
||||
"src/views/AboutUs.tsx","68","CMS editorial","telegram: 'https://t.me/guilance',",""
|
||||
"src/views/AboutUs.tsx","73","CMS editorial","name: 'انجمن علمی مهندسی کامپیوتر دانشکدهی فنی',",""
|
||||
"src/views/AboutUs.tsx","74","CMS editorial","university: 'دانشگاه گیلان',",""
|
||||
"src/views/AboutUs.tsx","75","CMS editorial","city: 'رشت',",""
|
||||
"src/views/AboutUs.tsx","76","CMS editorial","foundedYear: '۱۳۵۳',",""
|
||||
"src/views/AboutUs.tsx","77","CMS editorial","about: 'برگزاری رویدادهای آموزشی و صنعتی، منتورینگ دانشجویی، و اتصال دانشجویان به فرصتهای شغلی و پژوهشی.',",""
|
||||
"src/views/AboutUs.tsx","78","CMS editorial","focusAreas: ['مهندسی نرمافزار', 'هوش مصنوعی و داده', 'DevOps'],",""
|
||||
"src/views/AboutUs.tsx","80","CMS editorial","instagram: 'https://instagram.com/ce.guilan',",""
|
||||
"src/views/AboutUs.tsx","81","CMS editorial","telegram: 'https://t.me/CSAOEF',",""
|
||||
"src/views/AboutUs.tsx","86","CMS editorial","name: 'انجمن علمی علوم کامپیوتر دانشکدهی علومپایه',",""
|
||||
"src/views/AboutUs.tsx","87","CMS editorial","university: 'دانشگاه گیلان',",""
|
||||
"src/views/AboutUs.tsx","88","CMS editorial","city: 'رشت',",""
|
||||
"src/views/AboutUs.tsx","89","CMS editorial","foundedYear: '۱۳۵۳',",""
|
||||
"src/views/AboutUs.tsx","90","CMS editorial","about: 'برگزاری رویدادهای آموزشی و صنعتی، منتورینگ دانشجویی، و اتصال دانشجویان به فرصتهای شغلی و پژوهشی.',",""
|
||||
"src/views/AboutUs.tsx","91","CMS editorial","focusAreas: ['امنیت', 'سیستمعامل', 'سختافزار'],",""
|
||||
"src/views/AboutUs.tsx","93","CMS editorial","instagram: 'https://instagram.com/csguilan',",""
|
||||
"src/views/AboutUs.tsx","94","CMS editorial","telegram: 'https://t.me/guilanCS',",""
|
||||
"src/views/AboutUs.tsx","99","CMS editorial","// کمیتهها/گروههای کاری",""
|
||||
"src/views/AboutUs.tsx","102","CMS editorial","title: 'کمیته آموزش',",""
|
||||
"src/views/AboutUs.tsx","103","CMS editorial","desc: 'برنامهریزی و اجرای دورهها و کارگاههای مهارتی، هماهنگی با مدرسین و طراحی مسیرهای یادگیری.'",""
|
||||
"src/views/AboutUs.tsx","106","CMS editorial","title: 'کمیته صنعت و اشتغال',",""
|
||||
"src/views/AboutUs.tsx","107","CMS editorial","desc: 'تعامل با شرکتها، دعوت از متخصصان صنعت، و شبکهسازی برای فرصتهای کارآموزی و استخدام.'",""
|
||||
"src/views/AboutUs.tsx","110","CMS editorial","title: 'کمیته محتوای دیجیتال',",""
|
||||
"src/views/AboutUs.tsx","111","CMS editorial","desc: 'تولید محتوای آموزشی، خبرنامه، مدیریت شبکههای اجتماعی و پوشش رسانهای رویدادها.'",""
|
||||
"src/views/AboutUs.tsx","115","CMS editorial","// راههای ارتباطی اصلی صفحه",""
|
||||
"src/views/AboutUs.tsx","118","CMS editorial","label: 'ایمیل',",""
|
||||
"src/views/AboutUs.tsx","123","CMS editorial","label: 'تلگرام',",""
|
||||
"src/views/AboutUs.tsx","125","CMS editorial","href: 'https://t.me/guilancea',",""
|
||||
"src/views/AboutUs.tsx","128","CMS editorial","label: 'اینستاگرام',",""
|
||||
"src/views/AboutUs.tsx","130","CMS editorial","href: 'https://instagram.com/guilance.ir',",""
|
||||
"src/views/AboutUs.tsx","133","CMS editorial","label: 'وبسایت',",""
|
||||
"src/views/AboutUs.tsx","135","CMS editorial","href: 'https://east-guilan-ce.ir',",""
|
||||
"src/views/AboutUs.tsx","153","CMS editorial","<meta property=""og:image"" content={`${SITE_URL}/favicon.ico`} />",""
|
||||
"src/views/AboutUs.tsx","158","CMS editorial","<meta name=""twitter:image"" content={`${SITE_URL}/favicon.ico`} />",""
|
||||
"src/views/AboutUs.tsx","168","CMS editorial","{/* آمار کوتاه */}",""
|
||||
"src/views/AboutUs.tsx","170","CMS editorial","<Stat label=""سال تأسیس"" value={ORG.foundedYear} />",""
|
||||
"src/views/AboutUs.tsx","171","CMS editorial","<Stat label=""اعضا"" value={ORG.membersApprox} />",""
|
||||
"src/views/AboutUs.tsx","172","CMS editorial","<Stat label=""رویدادها"" value={ORG.eventsCount} />",""
|
||||
"src/views/AboutUs.tsx","173","CMS editorial","<Stat label=""داوطلبان"" value={ORG.volunteersCount} />",""
|
||||
"src/views/AboutUs.tsx","178","CMS editorial","{/* درباره ما */}",""
|
||||
"src/views/AboutUs.tsx","183","CMS editorial","<CardHeader title=""ماموریت ما"" subtitle=""چرا اینجا هستیم؟"" />",""
|
||||
"src/views/AboutUs.tsx","186","CMS editorial","توانمندسازی دانشجویان با مهارتهای موردنیاز صنعت، ایجاد شبکههای حرفهای و فراهمکردن مسیرهای یادگیری",""
|
||||
"src/views/AboutUs.tsx","187","CMS editorial","پروژهمحور؛ از معرفی نقشها و مسیرهای شغلی تا تمرین مهارتهای نرم و فنی.",""
|
||||
"src/views/AboutUs.tsx","193","CMS editorial","<CardHeader title=""ارزشهای ما"" subtitle=""چه چیزهایی برایمان مهم است؟"" />",""
|
||||
"src/views/AboutUs.tsx","195","CMS editorial","<li className=""rounded-xl border p-3"">یادگیری پیوسته و اشتراک دانش</li>",""
|
||||
"src/views/AboutUs.tsx","196","CMS editorial","<li className=""rounded-xl border p-3"">کیفیت، شفافیت و مسئولیتپذیری</li>",""
|
||||
"src/views/AboutUs.tsx","197","CMS editorial","<li className=""rounded-xl border p-3"">فرصت برابر برای مشارکت</li>",""
|
||||
"src/views/AboutUs.tsx","198","CMS editorial","<li className=""rounded-xl border p-3"">ارتباط مؤثر با صنعت و جامعه</li>",""
|
||||
"src/views/AboutUs.tsx","203","CMS editorial","{/* تماس سریع */}",""
|
||||
"src/views/AboutUs.tsx","206","CMS editorial","<CardHeader title=""راههای ارتباطی"" subtitle=""با ما در ارتباط باشید"" />",""
|
||||
"src/views/AboutUs.tsx","224","CMS editorial","<CardHeader title=""ساعات پاسخگویی"" subtitle=""پشتیبانی داوطلبانه"" />",""
|
||||
"src/views/AboutUs.tsx","226","CMS editorial","<p>روزهای شنبه تا چهارشنبه، ساعت ۱۰ تا ۱۸</p>",""
|
||||
"src/views/AboutUs.tsx","227","CMS editorial","<p className=""mt-2"">پاسخها توسط تیم داوطلبان انجام میشود.</p>",""
|
||||
"src/views/AboutUs.tsx","234","CMS editorial","{/* انجمنها */}",""
|
||||
"src/views/AboutUs.tsx","237","CMS editorial","<h2 className=""text-2xl font-bold text-foreground"">انجمنها</h2>",""
|
||||
"src/views/AboutUs.tsx","238","CMS editorial","<p className=""mt-2 text-sm text-muted-foreground"">اطلاعات انجمنهای زیرمجموعه/همکار</p>",""
|
||||
"src/views/AboutUs.tsx","249","CMS editorial","{a.foundedYear && `• تأسیس: ${a.foundedYear}`}",""
|
||||
"src/views/AboutUs.tsx","267","CMS editorial","<a className=""rounded-xl border p-3 hover:bg-muted/50"" href={a.links.website} target=""_blank"" rel=""noreferrer"">وبسایت</a>",""
|
||||
"src/views/AboutUs.tsx","270","CMS editorial","<a className=""rounded-xl border p-3 hover:bg-muted/50"" href={a.links.instagram} target=""_blank"" rel=""noreferrer"">اینستاگرام</a>",""
|
||||
"src/views/AboutUs.tsx","273","CMS editorial","<a className=""rounded-xl border p-3 hover:bg-muted/50"" href={a.links.telegram} target=""_blank"" rel=""noreferrer"">تلگرام</a>",""
|
||||
"src/views/AboutUs.tsx","276","CMS editorial","<a className=""rounded-xl border p-3 hover:bg-muted/50"" href={`mailto:${a.links.email}`}>ایمیل</a>",""
|
||||
"src/views/AboutUs.tsx","286","CMS editorial","{/* کمیتهها */}",""
|
||||
"src/views/AboutUs.tsx","288","CMS editorial","<h2 className=""text-2xl font-bold text-foreground"">کمیتهها و گروههای کاری</h2>",""
|
||||
"src/views/AboutUs.tsx","301","CMS editorial","{/* پرسشهای متداول */}",""
|
||||
"src/views/AboutUs.tsx","304","CMS editorial","<h2 className=""text-2xl font-bold text-foreground"">پرسشهای متداول</h2>",""
|
||||
"src/views/AboutUs.tsx","306","CMS editorial","<FAQ q=""چطور در رویدادها شرکت کنم؟"" a=""برای ثبتنام در رویدادها ابتدا باید حسابکاربری با کددانشجویی خود ایجاد کنید و سپس از طریق صفحهی ثبتنام در رویداد موردنظرتان شرکت کنید :)"" />",""
|
||||
"src/views/AboutUs.tsx","307","CMS editorial","<FAQ q=""آیا امکان همکاری انجمنها/شرکتها هم وجود دارد؟"" a=""بله، ما همیشه آمادهی همکاری با سازمانها و انجمنهای مختلف دانشگاهها جهت توانمندسازی دانشجویان با مهارتهای موردنیاز صنعت و ایجاد شبکههای حرفهای هستیم."" />",""
|
||||
"src/views/AboutUs.tsx","308","CMS editorial","<FAQ q=""برای سخنرانی/منتورینگ به چه چیزهایی نیاز است؟"" a=""رزومه کوتاه، موضوع پیشنهادی و زمانهای دسترسپذیرتان را ارسال کنید تا هماهنگ کنیم."" />",""
|
||||
"src/views/AboutUs.tsx","313","CMS editorial","{/* نقشه و آدرس اختیاری */}",""
|
||||
"src/views/AboutUs.tsx","315","CMS editorial","<h2 className=""text-2xl font-bold text-foreground"">نشانی دانشکده</h2>",""
|
||||
"src/views/AboutUs.tsx","319","CMS editorial","<p>آدرس: گیلان، رودسر، واجارگاه، دانشکدهی فنی و مهندسی شرق گیلان</p>",""
|
||||
"src/views/AboutUs.tsx","320","CMS editorial","<p>کدپستی: ۴۴۹۱۸۹۸۵۶۶</p>",""
|
||||
"src/views/AboutUs.tsx","321","CMS editorial","<p>تلفن: ۰۱۳-۴۲۶۸۸۴۴۷</p>",""
|
||||
"src/views/AboutUs.tsx","322","CMS editorial","<p className=""mt-3"">برای هماهنگی حضوری از قبل پیام بدهید.</p>",""
|
||||
"src/views/AboutUs.tsx","327","CMS editorial","src=""https://maps.google.com/maps?q=37.06285,50.42324&hl=fa&z=16&output=embed""",""
|
||||
"src/views/AboutUs.tsx","339","CMS editorial","<h3 className=""text-xl md:text-2xl font-bold text-foreground"">مایل به همکاری هستید؟</h3>",""
|
||||
"src/views/AboutUs.tsx","340","CMS editorial","<p className=""mt-2 text-sm text-muted-foreground"">برای مشارکت داوطلبانه، سخنرانی، اسپانسری یا همکاری صنعتی به ما پیام دهید.</p>",""
|
||||
"src/views/AboutUs.tsx","342","CMS editorial","<a href=""mailto:eastguilanceassociation@gmail.com"" className=""rounded-xl border px-5 py-2 text-sm hover:bg-muted/50"">ایجاد ارتباط</a>",""
|
||||
"src/views/AboutUs.tsx","343","CMS editorial","<a href=""https://t.me/guilancea"" target=""_blank"" rel=""noreferrer"" className=""rounded-xl bg-primary px-5 py-2 text-sm text-primary-foreground"">",""
|
||||
"src/views/AboutUs.tsx","344","CMS editorial","پیام در تلگرام",""
|
||||
"src/views/AboutUs.tsx","354","CMS editorial","// --------- اجزای کوچک ---------",""
|
||||
"src/components/Navbar.tsx","54","code-owned UI","<Button className=""rounded-full px-5"">ورود / ثبتنام</Button>",""
|
||||
"src/components/Navbar.tsx","66","code-owned UI","aria-label=""منوی حساب کاربری""",""
|
||||
"src/components/Navbar.tsx","83","code-owned UI","{[user?.first_name, user?.last_name].filter(Boolean).join("" "") || user?.username || ""حساب کاربری""}",""
|
||||
"src/components/Navbar.tsx","88","code-owned UI","مشاهده پروفایل",""
|
||||
"src/components/Navbar.tsx","94","code-owned UI","تغییر یا بازیابی رمز",""
|
||||
"src/components/Navbar.tsx","101","code-owned UI","داشبورد مدیریت",""
|
||||
"src/components/Navbar.tsx","109","code-owned UI","خروج از حساب",""
|
||||
"src/components/Navbar.tsx","130","CMS editorial","<img src=""/favicon.ico"" alt=""لوگوی انجمن"" className=""h-10 w-10 object-contain"" />",""
|
||||
"src/components/Navbar.tsx","134","CMS editorial","انجمن علمی مهندسی کامپیوتر",""
|
||||
"src/components/Navbar.tsx","137","CMS editorial","دانشکدهی فنی و مهندسی شرق گیلان",""
|
||||
"src/components/Navbar.tsx","143","CMS editorial","<NavItem to=""/"">خانه</NavItem>",""
|
||||
"src/components/Navbar.tsx","144","CMS editorial","<NavItem to=""/blog"">بلاگ</NavItem>",""
|
||||
"src/components/Navbar.tsx","145","CMS editorial","<NavItem to=""/events"">رویدادها</NavItem>",""
|
||||
"src/components/Footer.tsx","19","CMS editorial","<img src=""/favicon.ico"" alt=""لوگوی انجمن"" className=""h-9 w-9 rounded"" />",""
|
||||
"src/components/Footer.tsx","20","CMS editorial","<span className=""text-xl font-bold"">انجمن علمی کامپیوتر گیلان</span>",""
|
||||
"src/components/Footer.tsx","23","CMS editorial","ترویج علم کامپیوتر، برگزاری رویدادهای تخصصی، تقویت شبکهی دانشجویی و پیوند با صنعت.",""
|
||||
"src/components/Footer.tsx","28","CMS editorial","<h4 className=""mb-3 text-base font-semibold"">لینکهای مفید</h4>",""
|
||||
"src/components/Footer.tsx","32","CMS editorial","خانه",""
|
||||
"src/components/Footer.tsx","37","CMS editorial","رویدادها",""
|
||||
"src/components/Footer.tsx","42","CMS editorial","بلاگ",""
|
||||
"src/components/Footer.tsx","47","CMS editorial","دربارهی انجمن",""
|
||||
"src/components/Footer.tsx","54","CMS editorial","<h4 className=""mb-3 text-base font-semibold"">ارتباط با ما</h4>",""
|
||||
"src/components/Footer.tsx","56","CMS editorial","<li>ایمیل: info@east-guilan-ce.ir</li>",""
|
||||
"src/components/Footer.tsx","57","CMS editorial","<li>آدرس: دانشگاه گیلان، دانشکدهی فنی و مهندسی شرق گیلان</li>",""
|
||||
"src/components/Footer.tsx","62","CMS editorial","href=""https://Instagram.com/guilance.ir""",""
|
||||
"src/components/Footer.tsx","67","CMS editorial","<Button variant=""outline"" size=""icon"" className=""h-9 w-9"" aria-label=""اینستاگرام"">",""
|
||||
"src/components/Footer.tsx","72","CMS editorial","href=""https://t.me/guilance""",""
|
||||
"src/components/Footer.tsx","77","CMS editorial","<Button variant=""outline"" size=""icon"" className=""h-9 w-9"" aria-label=""تلگرام"">",""
|
||||
"src/components/Footer.tsx","82","CMS editorial","href=""https://www.linkedin.com/in/amiirkhl/""",""
|
||||
"src/components/Footer.tsx","87","CMS editorial","<Button variant=""outline"" size=""icon"" className=""h-9 w-9"" aria-label=""لینکدین"">",""
|
||||
"src/components/Footer.tsx","91","CMS editorial","<a href=""https://x.com"" target=""_blank"" rel=""noreferrer"" className=""inline-flex"">",""
|
||||
"src/components/Footer.tsx","96","CMS editorial","aria-label=""ایکس (توییتر)""",""
|
||||
"src/components/Footer.tsx","106","CMS editorial","href=""https://trustseal.enamad.ir/?id=649977&Code=m0wWM1DFYqd4fLEnjyMU3o2pupfuqDVW""",""
|
||||
"src/components/Footer.tsx","112","CMS editorial","src=""/enamad.png""",""
|
||||
"src/components/Footer.tsx","114","CMS editorial","alt=""نماد اعتماد الکترونیکی""",""
|
||||
"src/components/Footer.tsx","126","CMS editorial","<div>© {year} انجمن علمی کامپیوتر گیلان - تمامی حقوق محفوظ است.</div>",""
|
||||
"src/components/MobileBottomNav.tsx","37","CMS editorial","label: ""خانه"",",""
|
||||
"src/components/MobileBottomNav.tsx","44","CMS editorial","label: ""رویدادها"",",""
|
||||
"src/components/MobileBottomNav.tsx","51","CMS editorial","label: ""بلاگ"",",""
|
||||
"src/components/MobileBottomNav.tsx","58","code-owned UI","label: isAuthenticated ? ""پروفایل"" : ""حساب"",",""
|
||||
"src/app/page.tsx","5","CMS editorial","const title = ""انجمن علمی کامپیوتر دانشگاه گیلان"";",""
|
||||
"src/app/page.tsx","7","CMS editorial","""با ما همراه شوید و در دنیای مهندسی و علوم کامپیوتر پیشرفت کنید. رویدادها، محتوای آموزشی و جامعهای پویا برای رشد شما فراهم است."";",""
|
||||
"src/app/page.tsx","17","CMS editorial","siteName: ""انجمن علمی کامپیوتر شرق گیلان"",",""
|
||||
"src/app/page.tsx","19","CMS editorial","images: [`${siteUrl}/favicon.ico`],",""
|
||||
"src/app/page.tsx","26","CMS editorial","images: [`${siteUrl}/favicon.ico`],",""
|
||||
"src/app/page.tsx","31","code-owned UI","""@context"": ""https://schema.org"",","Schema structure or vocabulary; referenced identity values remain editorial."
|
||||
"src/app/page.tsx","37","CMS editorial","logo: `${siteUrl}/favicon.ico`,",""
|
||||
"src/app/about/page.tsx","5","CMS editorial","const title = ""درباره ما | انجمن علمی کامپیوتر شرق گیلان"";",""
|
||||
"src/app/about/page.tsx","7","CMS editorial","""آشنایی با تاریخچه، مأموریتها و دستاوردهای انجمن علمی کامپیوتر شرق گیلان و راههای مشارکت دانشجویان در برنامههای انجمن."";",""
|
||||
"src/app/about/page.tsx","13","CMS editorial","""انجمن علمی کامپیوتر شرق گیلان"",",""
|
||||
"src/app/about/page.tsx","14","CMS editorial","""دانشگاه گیلان"",",""
|
||||
"src/app/about/page.tsx","15","CMS editorial","""انجمن علمی دانشجویی"",",""
|
||||
"src/app/about/page.tsx","16","CMS editorial","""رویدادهای فناوری"",",""
|
||||
"src/app/about/page.tsx","23","CMS editorial","siteName: ""انجمن علمی کامپیوتر شرق گیلان"",",""
|
||||
"src/app/about/page.tsx","25","CMS editorial","images: [`${siteUrl}/favicon.ico`],",""
|
||||
"src/app/about/page.tsx","32","CMS editorial","images: [`${siteUrl}/favicon.ico`],",""
|
||||
"src/app/about/page.tsx","37","code-owned UI","""@context"": ""https://schema.org"",","Schema structure or vocabulary; referenced identity values remain editorial."
|
||||
"src/app/about/page.tsx","44","CMS editorial","name: ""انجمن علمی کامپیوتر شرق گیلان"",",""
|
||||
"src/app/about/page.tsx","46","CMS editorial","logo: `${siteUrl}/favicon.ico`,",""
|
||||
"src/app/about/page.tsx","48","CMS editorial","""https://instagram.com/guilance.ir"",",""
|
||||
"src/app/about/page.tsx","49","CMS editorial","""https://t.me/guilance"",",""
|
||||
"src/app/about/page.tsx","50","CMS editorial","""https://t.me/guilancea"",",""
|
||||
"src/app/layout.tsx","23","CMS editorial","default: ""انجمن علمی کامپیوتر شرق گیلان"",",""
|
||||
"src/app/layout.tsx","24","CMS editorial","template: ""%s | انجمن علمی کامپیوتر شرق گیلان"",",""
|
||||
"src/app/layout.tsx","27","CMS editorial","""انجمن علمی کامپیوتر شرق گیلان، برگزارکننده رویدادها، کارگاهها و محتوای آموزشی برای دانشجویان و علاقهمندان فناوری."",",""
|
||||
"src/app/events/page.tsx","19","code-owned UI","const title = search ? `جستوجوی رویدادها: ${search}` : ""رویدادها"";","Query-specific title/description or search behavior; default listing introduction remains editorial."
|
||||
"src/app/events/page.tsx","21","code-owned UI","? `نتایج جستوجوی رویدادها برای ${search} در انجمن علمی کامپیوتر شرق گیلان.`","Query-specific title/description or search behavior; default listing introduction remains editorial."
|
||||
"src/app/events/page.tsx","22","CMS editorial",": ""رویدادهای جاری و گذشته انجمن علمی کامپیوتر شرق گیلان، شامل کارگاهها، مسابقات و برنامههای آموزشی."";",""
|
||||
"src/app/events/page.tsx","33","CMS editorial","siteName: ""انجمن علمی کامپیوتر شرق گیلان"",",""
|
||||
"src/app/events/page.tsx","35","CMS editorial","images: [`${siteUrl}/favicon.ico`],",""
|
||||
"src/app/blog/page.tsx","24","code-owned UI","const title = search ? `جستوجوی وبلاگ: ${search}` : ""وبلاگ"";","Query-specific title/description or search behavior; default listing introduction remains editorial."
|
||||
"src/app/blog/page.tsx","26","code-owned UI","? `نتایج جستوجوی مقالات برای ${search} در وبلاگ انجمن علمی کامپیوتر شرق گیلان.`","Query-specific title/description or search behavior; default listing introduction remains editorial."
|
||||
"src/app/blog/page.tsx","27","CMS editorial",": ""مقالات، نوشتهها و محتوای آموزشی انجمن علمی کامپیوتر شرق گیلان."";",""
|
||||
"src/app/blog/page.tsx","38","CMS editorial","siteName: ""انجمن علمی کامپیوتر شرق گیلان"",",""
|
||||
"src/app/blog/page.tsx","40","CMS editorial","images: [`${siteUrl}/favicon.ico`],",""
|
||||
|
1
tasks/evidence/2026-09-24/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
__pycache__/
|
||||
|
Before Width: | Height: | Size: 315 KiB |
|
Before Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 305 KiB |
|
Before Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 150 KiB |
@@ -1,74 +0,0 @@
|
||||
"""Create disposable Bundle 0 browser fixtures in the isolated Django test database.
|
||||
|
||||
Run only with --settings=config.settings.test and BUNDLE0_TEST_PASSWORD set.
|
||||
Never point TEST_DB_NAME at a shared or production database.
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.blog.models import Post
|
||||
from apps.events.models import Event
|
||||
from apps.users.models import User
|
||||
|
||||
|
||||
assert os.environ.get("DJANGO_SETTINGS_MODULE") == "config.settings.test"
|
||||
assert settings.DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3"
|
||||
password = os.environ["BUNDLE0_TEST_PASSWORD"]
|
||||
now = timezone.now()
|
||||
|
||||
|
||||
def fixture_user(username, mobile, *, staff=False):
|
||||
user, _ = User.objects.get_or_create(
|
||||
username=username,
|
||||
defaults={"mobile": mobile, "email": f"{username}@example.invalid"},
|
||||
)
|
||||
user.mobile = mobile
|
||||
user.email = f"{username}@example.invalid"
|
||||
user.is_email_verified = True
|
||||
user.is_mobile_verified = True
|
||||
user.is_staff = staff
|
||||
user.is_superuser = staff
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
student = fixture_user("bundle0_student", "09198000111")
|
||||
staff = fixture_user("bundle0_staff", "09198000112", staff=True)
|
||||
|
||||
for slug, title, price, capacity in (
|
||||
("bundle0-free", "Bundle 0 Free Event", 0, 20),
|
||||
("bundle0-full", "Bundle 0 Full Event", 0, 0),
|
||||
("bundle0-paid", "Bundle 0 Paid Event", 50000, 20),
|
||||
):
|
||||
Event.objects.update_or_create(
|
||||
slug=slug,
|
||||
defaults={
|
||||
"title": title,
|
||||
"description": "Local baseline event for browser review.",
|
||||
"start_time": now + timedelta(days=7),
|
||||
"end_time": now + timedelta(days=7, hours=2),
|
||||
"registration_start_date": now - timedelta(days=1),
|
||||
"registration_end_date": now + timedelta(days=6),
|
||||
"status": Event.StatusChoices.PUBLISHED,
|
||||
"price": price,
|
||||
"capacity": capacity,
|
||||
},
|
||||
)
|
||||
|
||||
Post.objects.update_or_create(
|
||||
slug="bundle0-article",
|
||||
defaults={
|
||||
"title": "Bundle 0 Article",
|
||||
"content": "# Local baseline article\n\nA sample article for the browser review.",
|
||||
"author": staff,
|
||||
"status": Post.StatusChoices.PUBLISHED,
|
||||
"published_at": now,
|
||||
},
|
||||
)
|
||||
|
||||
print("Created isolated student, staff, free/full/paid events, and article fixtures.")
|
||||
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 168 KiB |
@@ -1,29 +0,0 @@
|
||||
# Bundle 0 isolated local replay
|
||||
|
||||
Replayed 2026-09-24 to 2026-09-25 with a local Redis container on `127.0.0.1:6379`, Django `config.settings.test` using the ignored SQLite `db.test.sqlite3`, Next.js dev server, and Playwright Chromium. No production database, payment gateway, SMS provider, or email service was used. The disposable data came from `create-local-baseline-fixtures.py`; the payment and OTP scripts in this directory mock external side effects.
|
||||
|
||||
## Replayed paths
|
||||
|
||||
| Journey | Happy path observed | Failure path observed |
|
||||
| --- | --- | --- |
|
||||
| Authentication | Browser: existing mobile `09198000111` → password login → profile. Isolated API: new mobile OTP verification → signup → OTP login → password reset → login with new password. | Browser stayed in auth for invalid mobile/wrong password. Isolated API rejected invalid OTP request, incorrect code, duplicate registration, and the old password after reset. |
|
||||
| Events | Browser: published events list and detail; signed-in free registration reached the success page with confirmed status and event details. | Full event had a disabled registration button and zero remaining capacity. Anonymous free registration redirected to auth. |
|
||||
| Payment | Mocked Django client: gateway request produced a pending payment; successful callback marked payment paid and registration confirmed. Browser payment result rendered the paid event, amount, and reference ID. | Mocked gateway outage returned 502 without a persisted payment; canceled callback redirected to failed state; duplicate paid attempt returned 400. Browser rendered the failed payment result. |
|
||||
| Blog | Browser opened the published fixture article with reading content, author, and comments area. Staff blog list showed the published article. | Missing article slug rendered the site's not-found view. |
|
||||
| Staff admin | Browser login as isolated staff; event list displayed fixture events and registration count. Editing the free-event title saved and the API returned the new value. At 390px the staff event list switched to cards and the menu exposed the blog route. | Empty required event title disabled Save. The earlier anonymous `/admin` replay redirected away. |
|
||||
|
||||
The staff-edited fixture title was restored after verification. These fixtures live only in the ignored SQLite test file. The mocked payment success is not proof of a live gateway checkout, and the mocked OTP replay is not proof of SMS delivery or the Google OAuth path.
|
||||
|
||||
## Backend test baseline (historical run)
|
||||
|
||||
- `python manage.py check --settings=config.settings.test`: passed, zero issues.
|
||||
- `python manage.py test apps.events.tests.integration.test_events --settings=config.settings.test --noinput`: **25 passed**.
|
||||
- `python manage.py test apps.users.tests apps.events.tests apps.payments.tests --settings=config.settings.test --noinput`: **148 tests ran, 89 errors**. Raw output: [backend-focused-tests.txt](backend-focused-tests.txt). These errors were recorded as stale tests at the time. The current pytest rerun and results are in [test-rerun-2026-09-26.md](test-rerun-2026-09-26.md) and supersede this as the current suite status. No application business logic was changed to make the historical expectations pass.
|
||||
|
||||
## Runtime observations to carry forward
|
||||
|
||||
- Event detail's second-by-second countdown produced a server/client text mismatch and React hydration warning in the dev browser.
|
||||
- The signed-in browser requested `/api/notifications/stream-token` and received HTTP 405.
|
||||
- The old footer still shows the unselected contact variants. The selected About-page contacts are recorded in the content inventory and design contract for cutover.
|
||||
|
||||
The browser replay covers the principal local auth, event, payment-result, blog-reading, and staff-edit journeys with a failure case in each. Full SMS transport, Google OAuth, and a live payment gateway need separate integration work. This file records the starting behavior; it is not approval of those external integrations or of the CMS/API contract.
|
||||
@@ -1,95 +0,0 @@
|
||||
"""Replay signup, OTP login, and password reset in the isolated test database.
|
||||
|
||||
SMS and the short-lived OTP store are mocked. Run after the fixture seed with
|
||||
--settings=config.settings.test and BUNDLE0_TEST_PASSWORD set.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import Client, override_settings
|
||||
|
||||
from apps.users.models import User
|
||||
|
||||
|
||||
assert os.environ.get("DJANGO_SETTINGS_MODULE") == "config.settings.test"
|
||||
assert settings.DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3"
|
||||
password = os.environ["BUNDLE0_TEST_PASSWORD"]
|
||||
mobile = "09198000113"
|
||||
client = Client()
|
||||
|
||||
|
||||
class TemporaryOtpStore:
|
||||
def __init__(self):
|
||||
self.values = {}
|
||||
|
||||
def setex(self, key, _seconds, value):
|
||||
self.values[key] = str(value).encode("utf-8")
|
||||
|
||||
def get(self, key):
|
||||
return self.values.get(key)
|
||||
|
||||
def delete(self, key):
|
||||
self.values.pop(key, None)
|
||||
|
||||
|
||||
store = TemporaryOtpStore()
|
||||
|
||||
|
||||
def post(path, payload):
|
||||
return client.post(path, data=json.dumps(payload), content_type="application/json")
|
||||
|
||||
|
||||
with (
|
||||
override_settings(ALLOWED_HOSTS=["testserver", "localhost"]),
|
||||
patch("apps.users.services.auth._redis", return_value=store),
|
||||
patch("apps.users.services.auth.send_critical_sms.delay"),
|
||||
):
|
||||
invalid = post("/api/auth/otp/send", {"mobile": "123", "mode": "register"})
|
||||
assert invalid.status_code == 400
|
||||
|
||||
sent = post("/api/auth/otp/send", {"mobile": mobile, "mode": "register"})
|
||||
assert sent.status_code == 200, sent.content
|
||||
code = store.get(f"auth_otp:register:{mobile}").decode("utf-8")
|
||||
wrong = "00000" if code != "00000" else "99999"
|
||||
rejected = post("/api/auth/otp/verify-register", {"mobile": mobile, "code": wrong})
|
||||
assert rejected.status_code == 400
|
||||
verified = post("/api/auth/otp/verify-register", {"mobile": mobile, "code": code})
|
||||
assert verified.status_code == 200, verified.content
|
||||
registered = post(
|
||||
"/api/auth/register",
|
||||
{"username": "bundle0_signup", "mobile": mobile, "code": code, "password": password},
|
||||
)
|
||||
assert registered.status_code == 201, registered.content
|
||||
assert User.objects.get(username="bundle0_signup").is_mobile_verified
|
||||
|
||||
duplicate = post("/api/auth/otp/send", {"mobile": mobile, "mode": "register"})
|
||||
assert duplicate.status_code == 400
|
||||
|
||||
sent = post("/api/auth/otp/send", {"mobile": mobile, "mode": "login"})
|
||||
assert sent.status_code == 200, sent.content
|
||||
code = store.get(f"auth_otp:login:{mobile}").decode("utf-8")
|
||||
login = post("/api/auth/login/otp", {"mobile": mobile, "code": code})
|
||||
assert login.status_code == 200 and login.json()["access_token"]
|
||||
|
||||
sent = post("/api/auth/otp/send", {"mobile": mobile, "mode": "reset_password"})
|
||||
assert sent.status_code == 200, sent.content
|
||||
code = store.get(f"auth_otp:reset_password:{mobile}").decode("utf-8")
|
||||
new_password = password + "New"
|
||||
rejected = post(
|
||||
"/api/auth/reset-password",
|
||||
{"mobile": mobile, "code": wrong if wrong != code else "88888", "new_password": new_password},
|
||||
)
|
||||
assert rejected.status_code == 400
|
||||
changed = post(
|
||||
"/api/auth/reset-password",
|
||||
{"mobile": mobile, "code": code, "new_password": new_password},
|
||||
)
|
||||
assert changed.status_code == 200, changed.content
|
||||
old_login = post("/api/auth/login", {"identifier": mobile, "password": password})
|
||||
new_login = post("/api/auth/login", {"identifier": mobile, "password": new_password})
|
||||
assert old_login.status_code == 401 and new_login.status_code == 200
|
||||
|
||||
print("Auth replay: invalid OTP request and code rejected; signup, OTP login, and password reset succeeded; old password rejected.")
|
||||
@@ -1,85 +0,0 @@
|
||||
"""Replay paid-event outcomes in the isolated Bundle 0 Django test database.
|
||||
|
||||
External gateway calls, notifications, and SMS are mocked. This script is baseline
|
||||
evidence, not a production payment test or a browser gateway substitute.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import Client, override_settings
|
||||
|
||||
from apps.events.models import Event, Registration
|
||||
from apps.payments.models import Payment
|
||||
from apps.users.models import User
|
||||
from core.authentication import create_jwt_token
|
||||
|
||||
|
||||
assert os.environ.get("DJANGO_SETTINGS_MODULE") == "config.settings.test"
|
||||
assert settings.DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3"
|
||||
|
||||
student = User.objects.get(username="bundle0_student")
|
||||
event = Event.objects.get(slug="bundle0-paid")
|
||||
client = Client()
|
||||
headers = {"HTTP_AUTHORIZATION": f"Bearer {create_jwt_token(student)}"}
|
||||
body = json.dumps({"event_id": event.pk, "description": "Local mocked payment replay"})
|
||||
|
||||
|
||||
def create_payment():
|
||||
return client.post(
|
||||
"/api/payments/create",
|
||||
data=body,
|
||||
content_type="application/json",
|
||||
**headers,
|
||||
)
|
||||
|
||||
|
||||
with (
|
||||
override_settings(ALLOWED_HOSTS=["testserver", "localhost"]),
|
||||
patch("apps.payments.api.views.notify_user"),
|
||||
patch("apps.payments.api.views.send_critical_sms.delay"),
|
||||
patch("apps.payments.api.views.requests.post") as gateway,
|
||||
):
|
||||
gateway.side_effect = RuntimeError("simulated gateway outage")
|
||||
failed_request = create_payment()
|
||||
assert failed_request.status_code == 502, failed_request.content
|
||||
assert not Payment.objects.filter(user=student, event=event).exists()
|
||||
|
||||
gateway.side_effect = None
|
||||
gateway.return_value = Mock()
|
||||
gateway.return_value.json.return_value = {
|
||||
"data": {"code": 100, "authority": "BUNDLE0-CANCEL"}
|
||||
}
|
||||
pending = create_payment()
|
||||
assert pending.status_code == 200, pending.content
|
||||
assert pending.json()["start_pay_url"]
|
||||
canceled = client.get(
|
||||
"/api/payments/callback",
|
||||
{"Authority": "BUNDLE0-CANCEL", "Status": "NOK"},
|
||||
)
|
||||
assert canceled.status_code == 302
|
||||
assert "status=failed" in canceled.url
|
||||
assert Payment.objects.get(authority="BUNDLE0-CANCEL").status == Payment.OrderStatusChoices.CANCELED
|
||||
|
||||
gateway.return_value.json.return_value = {
|
||||
"data": {"code": 100, "authority": "BUNDLE0-SUCCESS"}
|
||||
}
|
||||
pending = create_payment()
|
||||
assert pending.status_code == 200, pending.content
|
||||
gateway.return_value.json.return_value = {
|
||||
"data": {"code": 100, "ref_id": "BUNDLE0-REF"}
|
||||
}
|
||||
paid = client.get(
|
||||
"/api/payments/callback",
|
||||
{"Authority": "BUNDLE0-SUCCESS", "Status": "OK"},
|
||||
)
|
||||
assert paid.status_code == 302
|
||||
assert "status=success" in paid.url
|
||||
assert Payment.objects.get(authority="BUNDLE0-SUCCESS").status == Payment.OrderStatusChoices.PAID
|
||||
assert Registration.objects.get(user=student, event=event).status == Registration.StatusChoices.CONFIRMED
|
||||
duplicate = create_payment()
|
||||
assert duplicate.status_code == 400
|
||||
|
||||
print("Paid replay: gateway outage 502; cancellation redirect; mocked success and confirmed registration; duplicate blocked.")
|
||||
@@ -1,12 +0,0 @@
|
||||
# Backend test rerun — 2026-09-26
|
||||
|
||||
Reran the repository's documented pytest suites from `guilan-ace-backend` with `.venv/Scripts/python.exe` and `config.settings.test` from `pytest.ini`.
|
||||
|
||||
| Command | Result |
|
||||
| --- | --- |
|
||||
| `.venv/Scripts/python.exe -m pytest -m unit` | 79 passed, 158 deselected, 3.80s |
|
||||
| `.venv/Scripts/python.exe -m pytest -m integration` | 158 passed, 79 deselected, 17.58s |
|
||||
|
||||
Together, all 237 collected pytest tests passed. The integration run included users, events, payments, notifications, blog, and other app integration tests. No external SMS delivery, Google OAuth provider, or live payment gateway was exercised.
|
||||
|
||||
The previous `manage.py test apps.users.tests apps.events.tests apps.payments.tests --settings=config.settings.test` command discovered zero tests because the current suites are organized and marked for pytest. Its `NO TESTS RAN` result is not a passing test result; use the pytest commands above.
|
||||
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 36 KiB |
@@ -1,72 +0,0 @@
|
||||
# Guilan ACE redesign and public-content CMS: implementation plan
|
||||
|
||||
**CMS course correction (2026-09-27):** The owner replaced this document's draft/publish and flexible-section proposals with the [simple direct-save CMS plan](bundle1-simplification-plan.md). Follow that plan and the updated [checklist](todo.md) for all CMS work. The original decisions below remain historical context for the broader redesign, not active CMS requirements.
|
||||
|
||||
Status: Bundle 0 discovery and design contract approved by the owner on 2026-09-26; product implementation has not started. This plan is shared across the frontend, backend, and deployment repositories. The executable checklist is [todo.md](todo.md). Later implementation checkpoints and production rollout approvals remain required.
|
||||
|
||||
## Outcome and boundaries
|
||||
|
||||
Redesign the public experience in this order: Home/About, authentication, events and registration, blog, then the staff-facing frontend admin. Build a Django-admin-managed content system for public editorial copy, shared identity/logo, approved images, navigation, footer/contact information, and page SEO. Editors may change bounded text in predefined Home/About sections and reorder or hide optional About sections; the owner-specified Home sequence remains fixed, and new layouts are developer work. Existing event, registration, payment, cancellation, and eligibility rules remain unchanged in the first release. Functional form labels, validation messages, permission-sensitive controls, and the staff-admin interface text remain versioned application code. Transactional email/SMS/in-app templates are a separate later phase.
|
||||
|
||||
The official association name is `انجمن علمی مهندسی کامپیوتر دانشگاه گیلان (دانشکدهی فنی و مهندسی شرق گیلان)`. Use it in official metadata and About identity; the owner approved the shorter header lockup, `انجمن علمی مهندسی کامپیوتر` with a faculty subline, in the logo-led visual direction. Preserve the recognizable existing logo until an approved replacement is supplied.
|
||||
|
||||
## Existing-system facts that shape the work
|
||||
|
||||
- Frontend: Next.js 15, React 18, Tailwind 3, Radix UI, Persian RTL. Main surfaces include `src/views/Home.tsx`, `AboutUs.tsx`, `Auth.tsx`, `Events.tsx`, `EventDetail.tsx`, `Blog.tsx`, and `Admin*` views. Home and About currently contain public copy, contact data, and SEO strings in code; About contains contradictory names, email/social links, and unverified numeric claims. Shared navigation/footer also contain static public content.
|
||||
- Backend: Django 5, Ninja API, PostgreSQL, Django Unfold admin, Redis/Celery. Events, blog, gallery, and announcements already have domain models; there is no public-page CMS. Event-specific success copy is already data-backed, and should not be duplicated in the new CMS.
|
||||
- Deployment: Docker Compose runs the backend without an automatic `migrate` step; PostgreSQL and Django media are backed up. Schema migration, media staging, and content seeding therefore need explicit, separately verified release operations.
|
||||
- The blog was recently redesigned. Audit it for coherence and accessibility before replacing components wholesale.
|
||||
|
||||
## Decisions and proposed contracts
|
||||
|
||||
1. **Public-only content boundary.** Inventory every public route and classify strings as editorial/brand/SEO, domain data, or functional UI. Only the first category moves into the CMS. Homepage/About sections use a finite, documented type registry; no arbitrary HTML or general-purpose page builder.
|
||||
2. **Publishing workflow.** Designated staff can save drafts and preview; a narrower publisher group can publish/restore a previous published revision; superusers retain full access. Public API returns only published revisions. A draft may never become visible through a cache, preview URL, or guessed identifier. Audit who changed and published each revision.
|
||||
3. **Typed content model.** A site identity/settings record holds official/short names, logo, approved contact/social links, footer, and SEO defaults. Public editorial pages have stable slugs (at least `home`, `about`, `events`, and `blog`, plus any other editorial surface found in inventory). Home/About have ordered/visible sections with approved section types; simpler route introductions and SEO use bounded fields. Prefer typed fields/validated schemas over opaque, unbounded JSON. Individual events/blog posts stay in their domain tables.
|
||||
4. **Public and preview APIs.** Add a versioned, read-only published-content endpoint with an explicit response schema and safe cache policy. Draft preview requires staff authentication and is `no-store`; publish should invalidate or quickly expire public caches. Frontend renders public content server-side where SEO needs it, with safe fallback during rollout; no permanent duplicate copy after cutover.
|
||||
5. **Images and copy.** Logos/photos are managed files with alt text, dimensions/size limits, and a documented license/approval source. Prefer actual association/campus/event material, strong Persian typography, and clear information architecture over generic gradients, fabricated stats, decorative 3D, or motion that impedes tasks. Respect `prefers-reduced-motion` and keep admin interaction mostly motion-free.
|
||||
6. **Incremental release.** Ship backend CMS and verified seed first behind the existing frontend. Switch Home/About via a reversible feature flag. Then release auth, events, blog, and frontend admin as independently testable slices. New CMS data must not alter registration or payment state.
|
||||
|
||||
## Design process and approval gates
|
||||
|
||||
Use the repository-local Impeccable workflow, **visual review before UI implementation**: review `PRODUCT.md`, compare the current Home/About with real mobile/desktop captures, and use the owner-provided references to refine the current site. Three earlier concepts were rejected by the owner on 2026-09-25; the owner approved the logo-led Home and About visual direction on 2026-09-25. Home follows the owner's order: introduction, featured events, featured blog posts, CTA, footer. Shadcn/ui and Lucide are the implementation component system; the logo supplies teal, pale gold, and charcoal. The approved direction is recorded in `DESIGN.md`; the owner approved the T05 CMS/API contract and Checkpoints A/B on 2026-09-26. Start with existing approved logo, event posters, and other available real assets; new association photography is not a dependency. Run the installed Impeccable hooks during design work; after cloning, developers must trust/enable project hooks in Codex (`/hooks`) for their environment. Hooks are review aids, not substitutes for human design and accessibility review.
|
||||
|
||||
Design acceptance is based on actual tasks and content: a student finds the next event, understands eligibility/cost, completes registration, and sees a trustworthy success/payment state; a visitor learns who the association is; a staff editor publishes safely. Test Persian line breaks, mixed-script dates/numbers, 320–1440px widths, keyboard navigation, contrast, reduced motion, loading/empty/error states, and realistic long content.
|
||||
|
||||
## Phase order and effort
|
||||
|
||||
Effort is person-days for one experienced full-time developer/designer with normal stakeholder review. It includes design, implementation, tests, and rework; it is not elapsed time from merely installing skills. Overlap is possible after the CMS contract is approved, but the requested release priority is preserved.
|
||||
|
||||
| Phase | Scope | Effort | Exit gate |
|
||||
| --- | --- | ---: | --- |
|
||||
| 0. Discovery and design contract | Content inventory, facts/assets, flow map, Impeccable concept approval | 4–5 d | Approved content matrix, Home/About direction, baseline captures |
|
||||
| 1. CMS foundation and migration | Model, roles, admin editing, published/preview APIs, seed and dry-run | 9–12 d | Published content matches approved inventory; no draft leak |
|
||||
| 2. Home/About and shared shell | Design system, pages, header/footer, responsive/SEO integration | 9–12 d | Edited content appears correctly at all target widths |
|
||||
| 3. Authentication | Login/signup/verification/reset/Google callback states, recovery UX | 5–7 d | Existing auth behavior preserved, end-to-end tests pass |
|
||||
| 4. Events and registration | Listing, details, free/paid journeys, payment/success states | 9–12 d | Existing event rules preserved and state matrix passes |
|
||||
| 5. Blog | Cohesion audit, selective refinement, reading/discovery | 4–6 d | Current blog capabilities and URLs preserved |
|
||||
| 6. Frontend staff admin | Navigation, tables/forms, role clarity, mobile usability | 7–10 d | Essential staff tasks work at phone and desktop widths |
|
||||
| 7. Release hardening | Cross-browser/a11y/performance regression, deploy rehearsal, launch | 4–7 d | Backup, smoke, rollback, and owner sign-off |
|
||||
| **Total** | | **51–71 person-days** | **About 11–15 calendar weeks for one full-time person**, allowing reviews |
|
||||
|
||||
Two developers with clear frontend/backend ownership could shorten calendar time, but not person-days proportionally; shared design/API decisions and release testing remain on the critical path. Unapproved assets, conflicting content facts, or additional business-rule changes increase the estimate.
|
||||
|
||||
## Production content migration and cutover
|
||||
|
||||
1. **Freeze and inventory.** Capture the current production pages and code strings into a versioned content manifest with stable keys, source path, intended destination field, exact text, image path/checksum, and an approval status. Separate existing domain data from static editorial text. Mark disputed contacts, affiliated-association claims, counts, and old links for owner approval; do not silently “correct” them.
|
||||
2. **Build schema and tests.** Add additive CMS tables and role permissions. Do not drop old columns or repurpose event/blog data. Test migration on an anonymized production-sized database copy, including repeated deploy/seed runs and Unicode/RTL content.
|
||||
3. **Prepare assets.** Stage approved logo and other source images into backed-up Django media, record checksums, dimensions, alt text, and target CMS records. Never reference a frontend build path as a permanent media source. Unapproved imagery may remain absent; layout must handle it.
|
||||
4. **Dry-run seed.** Provide a versioned, idempotent import command that prints create/update/skip/conflict counts and a field-level diff. It must refuse to overwrite any editor-modified content by default. Use stable keys and a seed version, log outcomes, and support a no-write validation mode. Seed approved snapshots as drafts first; publisher reviews and publishes. A Django data migration may create immutable baseline records, but media transfer and editorial approval remain explicit operations, not filesystem side effects inside a schema migration.
|
||||
5. **Production gate.** Take and verify fresh database/media backups. Deploy backend compatible with the old frontend, run `migrate` explicitly, stage media, run seed dry-run, review conflicts, then execute seed and validate API/draft permissions. This can occur before the UI cutover with no public visual change.
|
||||
6. **Switch and observe.** Deploy frontend with CMS reads behind a flag; compare rendered copy and SEO against approved manifest, test mobile/desktop and all key journeys, then switch Home/About. Monitor CMS API errors, missing media, cache staleness, and registration/payment signals before advancing to later slices.
|
||||
7. **Rollback.** For a frontend/content regression, disable CMS rendering or restore the previous frontend image and republish the prior content revision. Keep additive CMS tables and original content until the new path is stable. Database restore is a last-resort incident procedure because it can erase registrations/payments created after the backup; decide on it with an incident owner, never as the routine rollback.
|
||||
|
||||
## Verification and definition of done
|
||||
|
||||
Every task in `todo.md` has explicit acceptance and verification. At each checkpoint run backend focused tests and `python manage.py test --settings=config.settings.test`, frontend `npm run lint` and `npm run build`, then browser checks on desktop/mobile and reduced-motion/keyboard modes. Add automated tests for CMS permission, draft isolation, seeding idempotency, page content API, event/auth state-preservation, and critical browser journeys. Measure accessibility and route bundle/performance against a captured baseline rather than an invented target; budget regressions deliberately. Check Persian content with a human reviewer. A release is done only after stakeholder approval, backup restore rehearsal, documented rollback, and verified production smoke tests.
|
||||
|
||||
## Risks and decisions still requiring approval
|
||||
|
||||
- **Contacts and factual claims:** The owner selected the About-page email, Instagram, and Telegram entries on 2026-09-24. The other legacy variants remain in the inventory. Address, phone, statistics, and affiliated-association claims still need entity/currentness review before production seed publication.
|
||||
- **Approved imagery:** Existing recognizable logo and approved current assets are the baseline. Catalog their usage rights; do not generate substitute “association photos.”
|
||||
- **Editorial ownership:** Nominate actual staff and publisher accounts/group membership before rollout; document the review/restore procedure.
|
||||
- **Scope guard:** Transactional messaging templates and changes to event/payment business logic require a separately approved specification and estimate.
|
||||
@@ -1,32 +0,0 @@
|
||||
# Bundle 0 product truth and asset review
|
||||
|
||||
Reviewed 2026-09-24 against `guilan-ace-frontend/PRODUCT.md`, frontend source, `tasks/plan.md`, and owner replies in this task. This record separates product commitments from legacy page claims. It is the input to the visual concepts and the later seed manifest.
|
||||
|
||||
## Confirmed for concepts
|
||||
|
||||
- The site serves the computer engineering student association at the University of Guilan's East Guilan Faculty of Engineering. The plan's official name is `انجمن علمی مهندسی کامپیوتر دانشگاه گیلان (دانشکدهی فنی و مهندسی شرق گیلان)`.
|
||||
- The first Home/About viewport should explain the association before inviting event discovery. The user chose this priority on 2026-09-24.
|
||||
- Preserve the existing `public/favicon.ico` association mark. The user identified existing repository facts and assets as verified. Keep the current header's short name and faculty subline as the refinement basis; the full official name stays in About identity and metadata.
|
||||
- Use the About page's association contact set for new concepts: `eastguilanceassociation@gmail.com`, `https://t.me/guilancea`, and `https://instagram.com/guilance.ir`. The user chose About-page contacts over the Footer values on 2026-09-24.
|
||||
- Events, registration, payment, cancellation, eligibility, blog articles and post metadata remain existing backend data and rules. New CMS content controls public editorial context only.
|
||||
|
||||
## Files and provenance
|
||||
|
||||
| Asset | SHA-256 | Current use | Next approval/source action |
|
||||
| --- | --- | --- | --- |
|
||||
| `guilan-ace-frontend/public/favicon.ico` | `A5EA52EBC4DA83932457F495575BC6E397ACA324976D786C92EFC61D5A1172D8` | Association mark in navigation, footer and SEO | Find original master and record license/creator for CMS media transfer. |
|
||||
| `guilan-ace-frontend/public/enamad.png` | `1EE71330241EFA235F7CBACB62674BDC3465C4085DDC2322C5D6C34BADEC6864` | Existing footer trust seal | Verify trust-seal account and destination before treating it as editable content. |
|
||||
| `guilan-ace-frontend/src/app/fonts/Vazirmatn-variable.woff2` | See source file | Self-hosted Persian UI font | Retain as a code asset while checking its redistribution license. |
|
||||
|
||||
The concept files use a copy of the current favicon solely for portable review. No replacement logo or documentary campus photograph was generated. `public/placeholder.svg` is a generic fallback, not an association image. Dynamic event/blog images are owned by their existing records.
|
||||
|
||||
## Still pending before seed publication
|
||||
|
||||
| Claim | Evidence gap |
|
||||
| --- | --- |
|
||||
| About's `۱۴۰۴`, `۱۰۰+`, `۱۰+`, `۲۰+` statistics, association founding years `۱۳۹۲` and `۱۳۵۳` | The owner says repository facts are verified. The association each value describes and its as-of date still need mapping before an auditable seed. Keep the exact values in the inventory; the concepts do not need a statistics block. |
|
||||
| Three listed associations and their relationship to this organization | `AboutUs.tsx` mixes one East Guilan association with two Rasht associations and calls them subsidiaries/collaborators. A relationship label needs an owner decision. |
|
||||
| Committee roster, response hours, postal address, phone and map position | Present in About source and selected as contact source; confirm current operational ownership before production seeding. |
|
||||
| Personal LinkedIn URL, generic X URL, and trust seal | Existing Footer URLs are not proof that they are official association channels. Exclude from new identity until explicitly confirmed. |
|
||||
|
||||
The owner withdrew the Plan A selection and rejected the first current-derived comp on 2026-09-25. The owner then specified a content-rich Home order (intro, featured events, featured blog posts, CTA, footer), Shadcn/ui and Lucide components, and a palette based on the association logo. The new logo-led Home/About comps are pending review. The remaining claim-to-entity mappings and the CMS/API contract still need review before production seed publication and implementation.
|
||||
@@ -1,83 +0,0 @@
|
||||
# Redesign and CMS implementation checklist
|
||||
|
||||
This is an **implementation backlog**, with work-in-progress noted below and checkboxes reserved for accepted work. See [plan.md](plan.md) for scope, estimates, migration sequence, and release gates. `F`, `B`, and `D` mean frontend, backend, and deployment repository. Each item is intended to fit one focused session (S = 1–2 files; M = 3–5 files); split any item that grows larger. In addition to each item’s check, apply the definition of done: focused and regression tests, lint/build, runtime/browser check, security/rollback where relevant, and human review.
|
||||
|
||||
Bundle 0 started 2026-09-24 and was approved by the owner on 2026-09-26. Deliverables: [content inventory](content-inventory.md), [baseline](baseline-2026-09-24.md), [local journey replay](evidence/2026-09-24/local-replay.md), [product/asset review](product-review.md), [CMS/API contract](cms-api-contract-draft.md), backend [API schema](../../guilan-ace-backend/docs/content-api-schema-draft.md), and the approved Home/About visual direction linked from frontend `DESIGN.md`. The owner approved the [Home](concepts/d-logo-led-home.html) and [About](concepts/d-logo-led-about.html) direction on 2026-09-25, then approved the [Bundle 0 review](bundle0-review.md) on 2026-09-26, accepting the content boundary, recorded baseline, and CMS/API contract. T01–T05 and Checkpoints A/B are closed at discovery and historical contract scope. The current pytest suites pass all 237 tests; the earlier 89-error run is preserved as historical evidence. External SMS/OAuth/live-gateway checks, disputed seed claims, and production media/account assignments remain follow-up work in later gates. Bundle 1 started 2026-09-27; Checkpoint C was approved by the owner on 2026-09-27. The owner changed the CMS direction on 2026-09-27; the active scope is [simple direct-save content editing](bundle1-simplification-plan.md), which supersedes the earlier draft/publish contract.
|
||||
|
||||
## 0. Discovery and design contract (4–5 person-days)
|
||||
|
||||
- [x] **T01 — Classify static public content (M; deps: none).** Files: F `src/views/*`, `src/components/Navbar.tsx`, `Footer.tsx`, `src/app/*`. Acceptance: every public route/string/image/SEO field is mapped to CMS editorial, existing domain data, or code-owned functional UI; conflicts and asset ownership are flagged. Verify: owner reviews the manifest against live desktop/mobile pages. Owner accepted the content boundary and exclusions in `bundle0-review.md` on 2026-09-26. The unchanged `0005` migration retains legacy About affiliations and years; the mock-content command replaces that copy on untouched records before CMS rendering.
|
||||
- [x] **T02 — Capture flow and performance baseline (M; deps: none).** Files: D `tasks/` evidence, F test artifacts. Acceptance: route/state map covers auth, event registration/payment, blog, and staff admin; screenshots and bundle/accessibility baselines have dated device widths. Verify: manually replay each existing happy path and one failure path on staging/local. Owner accepted the recorded local baseline on 2026-09-26 with the historical test run and unexercised external paths documented; the current pytest suites passed on 2026-09-26 (see rerun record).
|
||||
- [x] **Checkpoint A (deps: T01–T02).** Content categories, current behavior, and baseline evidence reviewed; no business-rule changes inferred. Accepted 2026-09-26 with the limits documented in `bundle0-review.md`.
|
||||
- [x] **T03 — Review product truth and assets (S; deps: T01).** Files: F `PRODUCT.md`, asset inventory. Acceptance: initial product record is checked against source assets and owner input; logo sources are cataloged; conflicting contacts/statistics remain pending rather than guessed. Verify: owner signs off facts and supplies approved contact channels before production seed publication. Concept-scope sign-off: owner confirmed repository facts/assets and chose About-page contacts; claim-to-entity mapping remains a T12 seed gate.
|
||||
- [x] **T04 — Settle Home/About visual direction (M; deps: T03).** Files: F `DESIGN.md`, D static design artifacts. Acceptance: owner reviews the logo-led Home/About comps at phone and desktop widths using real approved assets; Home follows intro → featured events → featured blog posts → CTA → footer, and the component system maps to Shadcn/ui and Lucide. Verify: compare with current captures, owner references, product truth, accessibility needs, and real content lengths. Owner approved the Home and About design direction on 2026-09-25; final content, responsive behavior, and accessibility remain implementation checks.
|
||||
- [x] **T05 — Approve design and CMS/API contract (M; deps: T04, T01).** Files: F `DESIGN.md`, D architecture note, B API schema draft. Acceptance: owner accepts the logo-led direction and fixed Home content order; section types, field constraints, payloads, cache/preview behavior, and edit/publish roles are signed off. Verify: walk a draft→preview→publish→restore sequence and 320px/desktop layouts on paper. Design direction accepted 2026-09-25; owner approved the CMS/API decisions and documented paper walkthrough in `bundle0-review.md` on 2026-09-26.
|
||||
- [x] **Checkpoint B (deps: T03–T05).** Approved content contract and design direction recorded on 2026-09-26. This closes the discovery gate; later implementation and release gates still apply.
|
||||
|
||||
## 1. CMS foundation and migration (9–12 person-days)
|
||||
|
||||
Checkpoint C was approved by the owner on 2026-09-27 after reviewing [the schema and validation record](bundle1-checkpoint-c-review.md). The old T08–T09 draft/publish workflow was rejected. The direct-save Unfold admin was reviewed and approved by the owner on 2026-09-27; the old [Checkpoint D review](bundle1-checkpoint-d-review.md) is superseded. The current-content API and Checkpoint E were approved on 2026-09-27. Follow the [course correction plan](bundle1-simplification-plan.md); the seeded About content review is documented in [Checkpoint F review](bundle1-seed-review.md).
|
||||
|
||||
- [x] **T06 — Add site identity and page schema (M; deps: T05).** Files: B new content app models/migration/tests. Acceptance: one site identity, stable page slugs, ordered typed sections, image/alt and SEO fields exist with uniqueness/validation; migrations are additive. Verify: migration test and `python manage.py check` on a test database.
|
||||
- [x] **T07 — Validate fields and media handling (M; deps: T06).** Files: B content validators/tests. Acceptance: URLs, section payloads, image type/size, alt text, and unsafe markup are constrained; absent optional media renders safely. Verify: invalid/oversize/malicious inputs rejected in focused tests.
|
||||
- [x] **Checkpoint C (deps: T06–T07).** Schema and validation reviewed before permissions/API work. Approved by the owner on 2026-09-27.
|
||||
- [x] **T08 — Audit current CMS data and permissions (S; deps: T06–T07).** Completed against the owner-authorized local development PostgreSQL database on 2026-09-27; see [database audit](bundle1-development-db-audit.md). Content migrations `0001`–`0005` are applied; the five current page/site records exist, with no media references. Home has two admin changes; the other content records do not. One staff account exists and is the only superuser; the legacy editor/publisher groups have no members and no users have direct content permissions. The old revision tables contain no rows. Staging was not needed or accessed; production permissions remain a release-gate audit.
|
||||
- [x] **T09 — Simplify content schema and use native Unfold forms (M; deps: T08).** Files: B content models/migrations/admin/forms/tests. Acceptance: one editable record per fixed page and shared site identity; explicit text/image/alt/SEO fields; direct Save; standard Django model change permissions; no arbitrary page creation, custom admin templates, draft/revision/approval workflow, or delete action. Verified: backend suite passed 248 tests; Django and migration checks passed; owner reviewed and approved the Unfold admin on 2026-09-27. The development database audit is complete; production access remains a release gate.
|
||||
- [x] **Checkpoint D (deps: T09).** Owner approved the direct-save Unfold forms and permissions on 2026-09-27. The old draft/publish review is superseded.
|
||||
- [x] **T10 — Expose current-content read API (M; deps: T09).** Files: B content Ninja router/schemas/tests, `config/api.py`; contract: backend [content API v1 note](../../guilan-ace-backend/docs/content-api-v1.md). Acceptance: shared site record and four fixed pages expose current content and SEO, with explicit missing/unknown behavior and no revision/status/preview fields. Verified: anonymous read, save-then-read, missing content, and cache behavior; backend suite passed 255 tests and Django checks passed. Owner approved the API checkpoint on 2026-09-27.
|
||||
- [x] **Checkpoint E (deps: T10).** The current-content API shape was checked and approved by the owner on 2026-09-27. No private preview API is planned.
|
||||
- [x] **T12 — Review starting content and media (M; deps: T01, T09).** Files: D source review, B initial content data. Owner reviewed the source content and approved blank CMS image fields, then directed that mock copy be used until staff save content. No current-site claims or media are in the mock content.
|
||||
- [x] **T13 — Initialize fixed content records safely (S; deps: T12).** Files: B management command/tests; do not change existing migrations. Implemented as `python manage.py seed_mock_content`, with `--dry-run` and `--database` options. Three focused command tests passed. On the owner-authorized development database, the dry run proposed four untouched records and skipped Home because of two admin changes; execution updated Site, About, Events, and Blog, preserved Home, and created six mock About items. See [database audit](bundle1-development-db-audit.md). No migration was changed.
|
||||
- [x] **Checkpoint F (deps: T12–T13).** Owner reviewed and approved the mock starting content and blank optional image fields; the development database seed result is recorded in [database audit](bundle1-development-db-audit.md).
|
||||
- [x] **T14 — Add frontend CMS adapter and cutover flag (M; deps: T10).** Files: F `src/lib/content-cms.ts`, `.env.sample`. Acceptance: current content can be fetched server-side with typed mock fallback; blank CMS fields and request failures use mock copy; the flag defaults off; `no-store` fetches expose the latest admin save. The adapter returns mock values rather than caller-provided legacy copy. Frontend lint and production build passed; pages remain on legacy rendering until later tasks connect them.
|
||||
|
||||
## 2. Home/About and shared public shell (9–12 person-days)
|
||||
|
||||
- [x] **T15 — Establish approved visual tokens and shell (M; deps: T05, T14).** Files: F global styles, fonts, navigation/footer components. Acceptance: approved typography/colors/layout, responsive header/footer, and CMS identity/navigation/contact fields match comps; RTL/keyboard/reduced-motion work. Implementation reviewed and approved by the owner at Checkpoint G on 2026-09-28.
|
||||
- [x] **T16 — Build CMS-driven Home editorial sections and domain previews (M; deps: T15).** Files: F Home view, section components, tests. Acceptance: hero/featured-section/CTA text comes from the current CMS record in the fixed owner-approved order; event and blog cards read existing domain APIs with truthful status, useful links, and sound empty/long layouts. Home was reviewed and approved by the owner at Checkpoint G on 2026-09-28; frontend lint and production build passed.
|
||||
- [x] **Checkpoint G (deps: T15–T16).** Owner approved the Home design and content at target widths on 2026-09-28. T17 can proceed.
|
||||
- [x] **T17 — Build CMS-driven About sections (M; deps: T15).** Files: F About view, section components, tests. Acceptance: owner-approved About identity, purpose, member-year archive, selected event highlights, separate memories gallery, and general history timeline come from current CMS records; unsupported claims are absent. The development frontend now reads the CMS API, with mock values filling empty fields; the admin Save-to-public change remains for owner review at Checkpoint H.
|
||||
- [x] **T18 — Make page SEO/structured data content-driven (M; deps: T16–T17).** Files: F route metadata/SEO utilities/tests. Acceptance: canonical, title, description, OG image, and structured-data identity are consistent with current CMS records and server-rendered HTML; no stale hardcoded names. SSR checks passed for Home, About, Events, Blog, event details, and post details; filtered listings are noindex with their base page as canonical.
|
||||
- [x] **T19 — Finish Home/About interaction and accessibility pass (M; deps: T16–T18).** Files: F affected page/styles/tests. Acceptance: motion supports comprehension but does not block content, reduced-motion works, contrast/focus/read order and Persian line wraps pass. Added the shared skip link, semantic main landmarks for listing routes, labeled search controls, and valid ordered-list markup; global focus and reduced-motion rules remain active. Owner visual/keyboard review is part of Checkpoint H because the in-app browser connection could not be established.
|
||||
- [x] **Checkpoint H (deps: T17–T19).** Owner approved Home/About on the CMS-enabled development frontend, including keyboard/focus and mobile review, on 2026-09-28.
|
||||
|
||||
## 3. Authentication (5–7 person-days)
|
||||
|
||||
- [x] **T20 — Simplify auth entry and navigation (M; deps: T15, T02).** Files: F Auth view/auth shell/tests. Acceptance: mobile entry automatically routes password accounts to password entry, passwordless existing accounts to login OTP, and new numbers to registration OTP then details, with a clear way to edit the phone and no backend auth-rule changes. Implemented this single phone-first path and removed the separate login/signup selector.
|
||||
- [x] **T21 — Clarify form, OTP, loading, and error states (M; deps: T20).** Files: F auth components/tests. Acceptance: each step has one clear action, input constraints and errors are accessible, resend/countdown are understandable, and retry does not lose valid input. Added inline announced errors, field autocomplete/constraints, an accessible OTP label, cooldown display, duplicate-submit protection, and automatic sign-in after successful registration. Removed forgotten-password UI because OTP login covers account access, improved dark-mode error contrast, and standardized outline/toggle/nav and account-menu hovers on the theme's secondary color. Frontend lint/build pass.
|
||||
- [x] **Checkpoint I (deps: T20–T21).** Owner reviewed and approved the phone → password/OTP → registration details flow, dark-mode errors, and button hover colors in RTL/mobile on 2026-09-28; backend auth rules remain unchanged.
|
||||
- [x] **T22 — Align verification and OAuth callback screens (M; deps: T20).** Files: F verification/callback views/tests. Acceptance: expired token, failed Google callback, success, and return-to-intent states follow the same system; forgotten-password recovery stays removed in favor of OTP login. Added persistent accessible Persian errors for incomplete/expired Google callbacks and preserved safe internal return destinations through Google login and recovery. Browser smoke confirmed the callback error state at 390px in dark mode. The error icon and text follow RTL flex-start alignment with the accessible red/dark error treatment.
|
||||
- [x] **T23 — Auth regression gate (M; deps: T21–T22).** Files: F end-to-end tests, B focused auth tests if needed. Acceptance: password login, OTP login, signup, verification, logout, protected redirects, and the retired recovery URL behave with existing API semantics. Added 4 safe-return-path unit tests. Password login submits the selected mobile as name=username with autocomplete=username in the same form as current-password; browser DOM confirmed the field metadata; frontend auth tests, lint, and production build pass; 43 focused backend auth/OAuth tests pass with isolated SQLite. Browser smoke confirmed protected /profile returns to /auth?next=%2Fprofile, /reset-password returns to /auth, and expired Google callback displays its error state. No live SMS/OAuth sign-in was exercised. Next.js upgraded to 16.3.5 for the runtime verification tooling. Existing lockfile audit reports 11 production dependency advisories (1 critical, 5 high); dompurify remediation requires a breaking jspdf upgrade and is deferred to T37.
|
||||
|
||||
## 4. Events and registration (9–12 person-days)
|
||||
|
||||
- [ ] **T24 — Redesign event discovery (M; deps: T15, T02).** Files: F Events view/cards/tests. Acceptance: upcoming/past/full states, date/location/price, filters, and empty/error states are clear without changing query semantics; listing intro/SEO reads the current CMS record. Verify: fixture-based state matrix at phone/desktop widths and an admin Save check.
|
||||
- [ ] **T25 — Redesign event detail and eligibility explanation (M; deps: T24).** Files: F EventDetail view/test. Acceptance: schedule, capacity/availability, prerequisites, organizer, and call to action match existing backend data and rules; no invented promise. Verify: compare UI against API fixtures for open/closed/full/free/paid cases.
|
||||
- [ ] **Checkpoint J (deps: T24–T25).** Student can decide whether an event is relevant before entering registration.
|
||||
- [ ] **T26 — Clarify registration forms and transitions (M; deps: T25).** Files: F event form/registration tests. Acceptance: field requirements, validation, duplicate/closed/full responses, and retry are legible; requests and eligibility rules unchanged. Verify: browser/API contract tests for every backend error/status.
|
||||
- [ ] **T27 — Clarify free, paid, and callback outcomes (M; deps: T26).** Files: F success/payment views/tests. Acceptance: pending, success, failure, canceled, and already-registered states show accurate next steps and do not imply payment when unconfirmed. Verify: callback replay and browser state tests against existing backend responses.
|
||||
- [ ] **T28 — Event regression gate (M; deps: T26–T27).** Files: F end-to-end tests, B existing event/payment tests. Acceptance: registration, capacity, payment, cancellation, eligibility, and notification-trigger semantics remain identical to baseline. Verify: automated state matrix and staging test event with no real charge/message side effect.
|
||||
- [ ] **Checkpoint K (deps: T26–T28).** Owner signs off registration/payment flow before broader release.
|
||||
|
||||
## 5. Blog (4–6 person-days)
|
||||
|
||||
- [ ] **T29 — Audit existing blog before editing (S; deps: T15, T02).** Files: F blog audit notes/tests. Acceptance: existing strengths and gaps in discovery, article typography, media, SEO, and interactions are recorded; no unnecessary rewrite. Verify: editorial and reader walkthrough on mobile/desktop.
|
||||
- [ ] **T30 — Apply selected blog refinements (M; deps: T29).** Files: F Blog/list/detail components/tests. Acceptance: visual system is coherent, article reading and navigation improve, existing URLs/actions/content remain intact; listing intro/SEO reads the current CMS record. Verify: screenshot/keyboard review with long Persian article and code block plus an admin Save check.
|
||||
- [ ] **T31 — Blog regression gate (M; deps: T30).** Files: F blog tests/SEO. Acceptance: listing, category/tag navigation, article render, share/interactions, and metadata still work. Verify: browser and API contract checks.
|
||||
|
||||
## 6. Frontend staff admin (7–10 person-days)
|
||||
|
||||
- [ ] **T32 — Map staff roles and mobile task priorities (M; deps: T02, T15).** Files: F admin IA/spec. Acceptance: top tasks for event manager, blog editor, content editor, and general staff are documented; access is not confused with visual hiding. Verify: permission-based task walkthrough with actual members.
|
||||
- [ ] **T33 — Restructure admin navigation and overview (M; deps: T32).** Files: F AdminLayout/Dashboard/tests. Acceptance: navigation and summaries work at phone/tablet/desktop, with no off-screen controls or meaningless dashboard clutter. Verify: responsive screenshots and keyboard navigation.
|
||||
- [ ] **Checkpoint L (deps: T32–T33).** At least one staff member completes top tasks at 390px and desktop widths.
|
||||
- [ ] **T34 — Make tables, forms, and confirmations responsive (M; deps: T33).** Files: F admin event/user/blog shared patterns/tests. Acceptance: filters, bulk actions, destructive confirmations, and long forms remain visible and operable at 320px; dense data has a deliberate mobile pattern. Verify: real-device/viewport walkthrough of create/edit/search/confirm tasks.
|
||||
- [ ] **T35 — Expose roles and failure states clearly (M; deps: T34).** Files: F admin auth/empty/error components/tests. Acceptance: permission denial, save conflict, loading, empty, and server error states explain next action without exposing unauthorized actions. Verify: role matrix and forced-failure browser tests.
|
||||
- [ ] **T36 — Admin regression gate (M; deps: T34–T35).** Files: F admin browser tests, B permission tests if affected. Acceptance: event, user, blog, and metadata management still work for authorized users across target widths. Verify: staff UAT plus automated smoke.
|
||||
|
||||
## 7. Release hardening and production cutover (4–7 person-days)
|
||||
|
||||
- [ ] **T37 — Cross-site quality gate (M; deps: T19, T23, T28, T31, T36).** Files: F/B tests and D QA record. Acceptance: lint/build/tests pass, critical routes meet accessibility/SEO/performance budgets against baseline, no critical mobile/RTL defect remains. Verify: CI plus Chrome/Firefox/Safari-equivalent checks where available and reduced-motion/keyboard review.
|
||||
- [ ] **T38 — Rehearse migration, backup, and rollback (M; deps: T13, T37).** Files: D runbook/scripts, B content initialization. Acceptance: staging clone has tested `migrate`, media stage, safe content initialization, frontend flag rollback, and verified backup recovery path. Verify: record commands, checksums, timings, and pass/fail evidence.
|
||||
- [ ] **Checkpoint M (deps: T37–T38).** Owner approves release checklist; production remains untouched until gate passes.
|
||||
- [ ] **T39 — Production CMS initialization and phased cutover (M; deps: T38).** Files: D runbook/observability, production state. Acceptance: verified fresh backup, safe backend migration, approved current content/assets, flagged frontend release, and smoke checks occur in order. Verify: compare public text/SEO to reviewed starting values; check health, API errors, media, auth, registration/payment signals.
|
||||
- [ ] **T40 — Observe and close release (S; deps: T39).** Files: D release notes/incident log. Acceptance: defined observation window ends without unresolved critical defect; known issues and next-phase messaging/business-rule requests are recorded. Verify: owner signs off live mobile/desktop journeys and rollback readiness.
|
||||