diff --git a/.kilo/skills/blog/SKILL.md b/.kilo/skills/blog/SKILL.md
new file mode 100644
index 0000000..f9ac104
--- /dev/null
+++ b/.kilo/skills/blog/SKILL.md
@@ -0,0 +1,400 @@
+---
+name: blog
+description: Blog/CMS domain knowledge - posts, categories, tags, comments, authors, SEO, RSS, scheduling
+---
+
+# Blog Skill
+
+## Purpose
+
+Universal domain knowledge for building blog and content management systems: posts, categories, tags, comments, authors, SEO optimization, RSS/sitemaps.
+
+## Capabilities
+
+### Content Management
+- Post CRUD operations
+- Draft / Published / Archived states
+- Content scheduling
+- Rich text / Markdown editing
+- Media embedding
+- Featured images
+
+### Organization
+- Hierarchical categories
+- Flat tags
+- Author assignment
+- Content series / collections
+
+### Comments
+- Comment moderation
+- Threaded (nested) comments
+- Spam filtering
+- Guest and authenticated comments
+
+### SEO
+- Meta tags (title, description)
+- Open Graph / Twitter Cards
+- Structured data (Schema.org Article)
+- XML sitemap
+- RSS / Atom feeds
+- Canonical URLs
+
+### Analytics
+- View counts
+- Reading time estimation
+- Popular posts
+- Related posts algorithm
+
+## Database Schema (Universal)
+
+Adapt table/column names to your framework conventions. Use `id` types appropriate to your DB (INTEGER / UUID / BIGSERIAL).
+
+### Posts Table
+
+```sql
+CREATE TABLE posts (
+ id INTEGER PRIMARY KEY AUTOINCREMENT, -- or UUID / BIGSERIAL
+ title TEXT NOT NULL,
+ slug TEXT UNIQUE NOT NULL,
+ excerpt TEXT,
+ content TEXT NOT NULL,
+ featured_image TEXT,
+ author_id INTEGER NOT NULL,
+ category_id INTEGER,
+ status TEXT DEFAULT 'draft', -- 'draft' | 'published' | 'archived'
+ published_at DATETIME,
+ meta_title TEXT,
+ meta_description TEXT,
+ canonical_url TEXT,
+ reading_time INTEGER, -- minutes
+ view_count INTEGER DEFAULT 0,
+ allow_comments BOOLEAN DEFAULT 1,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (author_id) REFERENCES users(id),
+ FOREIGN KEY (category_id) REFERENCES categories(id)
+);
+
+CREATE INDEX idx_posts_slug ON posts(slug);
+CREATE INDEX idx_posts_status ON posts(status);
+CREATE INDEX idx_posts_published ON posts(published_at);
+CREATE INDEX idx_posts_author ON posts(author_id);
+CREATE INDEX idx_posts_category ON posts(category_id);
+```
+
+### Categories Table
+
+```sql
+CREATE TABLE categories (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ slug TEXT UNIQUE NOT NULL,
+ description TEXT,
+ parent_id INTEGER,
+ image_url TEXT,
+ meta_title TEXT,
+ meta_description TEXT,
+ sort_order INTEGER DEFAULT 0,
+ FOREIGN KEY (parent_id) REFERENCES categories(id)
+);
+
+CREATE INDEX idx_categories_parent ON categories(parent_id);
+```
+
+### Tags Tables
+
+```sql
+CREATE TABLE tags (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ slug TEXT UNIQUE NOT NULL,
+ description TEXT
+);
+
+CREATE TABLE post_tags (
+ post_id INTEGER NOT NULL,
+ tag_id INTEGER NOT NULL,
+ PRIMARY KEY (post_id, tag_id),
+ FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
+ FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
+);
+
+CREATE INDEX idx_post_tags_post ON post_tags(post_id);
+CREATE INDEX idx_post_tags_tag ON post_tags(tag_id);
+```
+
+### Comments Table
+
+```sql
+CREATE TABLE comments (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ post_id INTEGER NOT NULL,
+ parent_id INTEGER, -- for threaded comments; NULL = top-level
+ author_name TEXT NOT NULL,
+ author_email TEXT NOT NULL,
+ author_url TEXT,
+ content TEXT NOT NULL,
+ status TEXT DEFAULT 'pending', -- 'pending' | 'approved' | 'spam' | 'trash'
+ ip_address TEXT,
+ user_agent TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
+ FOREIGN KEY (parent_id) REFERENCES comments(id)
+);
+
+CREATE INDEX idx_comments_post ON comments(post_id);
+CREATE INDEX idx_comments_status ON comments(status);
+```
+
+### Authors Table
+
+```sql
+CREATE TABLE authors (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER UNIQUE,
+ name TEXT NOT NULL,
+ bio TEXT,
+ avatar TEXT,
+ social_links TEXT, -- JSON {"twitter":"...","linkedin":"..."}
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+);
+```
+
+## API Endpoints
+
+### Public API
+
+```yaml
+GET /api/posts # List published posts (paginated)
+GET /api/posts/:slug # Get single post by slug
+GET /api/posts/author/:author # Posts by author slug
+GET /api/posts/category/:slug # Posts by category slug
+GET /api/posts/tag/:slug # Posts by tag slug
+GET /api/posts/search # Search posts (?q=query)
+
+GET /api/categories # List categories (tree or flat)
+GET /api/categories/:slug # Category detail + posts
+
+GET /api/tags # List tags
+GET /api/tags/:slug # Tag detail + posts
+
+GET /api/posts/:slug/comments # Approved comments (paginated)
+POST /api/posts/:slug/comments # Submit new comment
+
+GET /api/feed/rss # RSS feed
+GET /api/feed/atom # Atom feed
+GET /api/sitemap.xml # XML sitemap
+```
+
+### Admin / CMS API
+
+```yaml
+GET /api/admin/posts # List all posts (all statuses)
+POST /api/admin/posts # Create post
+PUT /api/admin/posts/:id # Update post
+DELETE /api/admin/posts/:id # Delete post
+POST /api/admin/posts/:id/publish
+POST /api/admin/posts/:id/archive
+
+GET /api/admin/categories # List categories
+POST /api/admin/categories # Create category
+PUT /api/admin/categories/:id # Update category
+DELETE /api/admin/categories/:id # Delete category
+
+GET /api/admin/tags # List tags
+POST /api/admin/tags # Create tag
+PUT /api/admin/tags/:id # Update tag
+DELETE /api/admin/tags/:id # Delete tag
+
+GET /api/admin/comments # List comments (all statuses)
+PUT /api/admin/comments/:id/approve
+PUT /api/admin/comments/:id/spam
+DELETE /api/admin/comments/:id
+
+POST /api/admin/media/upload # Upload media
+GET /api/admin/media # List media
+DELETE /api/admin/media/:id # Delete media
+```
+
+## Markdown & Rich Content
+
+Use any framework-appropriate parser (e.g. `marked`, `commonmark`, `python-markdown`).
+
+```javascript
+// Example with marked + highlight.js
+const marked = require('marked');
+const hljs = require('highlight.js');
+
+marked.use({
+ renderer: {
+ code(code, language) {
+ const highlighted = language
+ ? hljs.highlight(code, { language }).value
+ : code;
+ return `
${highlighted}
`;
+ },
+ image(href, title, alt) {
+ return `
${alt}`;
+ }
+ }
+});
+
+function calculateReadingTime(content) {
+ const words = content.split(/\s+/).length;
+ return Math.ceil(words / 200); // 200 wpm average
+}
+```
+
+## SEO Implementation
+
+### Meta Tags Generator
+
+```javascript
+function generateMeta(post, siteConfig) {
+ return {
+ title: post.meta_title || `${post.title} | ${siteConfig.name}`,
+ description: post.meta_description || post.excerpt,
+ canonical: post.canonical_url || `${siteConfig.url}/posts/${post.slug}`,
+ ogType: 'article',
+ ogImage: post.featured_image,
+ articlePublishedTime: post.published_at,
+ articleAuthor: post.author?.name
+ };
+}
+```
+
+### Schema.org Article JSON-LD
+
+```javascript
+function generateArticleSchema(post) {
+ return {
+ "@context": "https://schema.org",
+ "@type": "Article",
+ headline: post.title,
+ image: post.featured_image,
+ author: { "@type": "Person", name: post.author.name },
+ datePublished: post.published_at,
+ dateModified: post.updated_at,
+ description: post.excerpt
+ };
+}
+```
+
+### Sitemap Template
+
+```xml
+
+
+ {{siteUrl}}/daily1.0
+ {{#posts}}
+
+ {{siteUrl}}/posts/{{slug}}
+ {{updated_at}}
+ weekly
+ 0.8
+
+ {{/posts}}
+
+```
+
+## Comment Moderation
+
+### Spam Detection Signals
+
+```javascript
+function detectSpam(comment) {
+ const signals = [];
+
+ if (countLinks(comment.content) > 3) signals.push('excessive_links');
+ if (isDuplicate(comment)) signals.push('duplicate');
+ if (isBlacklisted(comment.ip_address)) signals.push('blacklisted_ip');
+ if (containsSpamWords(comment.content)) signals.push('spam_words');
+
+ return { isSpam: signals.length >= 2, score: signals.length, signals };
+}
+```
+
+## Content Scheduling
+
+```javascript
+async function publishScheduledPosts(db) {
+ const now = new Date();
+ const posts = await db.posts.findAll({
+ where: { status: 'draft', published_at: { [Op.lte]: now } }
+ });
+ for (const post of posts) {
+ await post.update({ status: 'published' });
+ // notify subscribers, regenerate sitemap, etc.
+ }
+}
+// Run via cron / scheduler every minute or use framework queue system.
+```
+
+## Related Posts Algorithm
+
+```javascript
+async function getRelatedPosts(db, post, limit = 5) {
+ return db.posts.findAll({
+ where: {
+ id: { [Op.ne]: post.id },
+ status: 'published',
+ [Op.or]: [
+ { category_id: post.category_id },
+ { '$tags.id$': post.tags.map(t => t.id) }
+ ]
+ },
+ include: [db.tags],
+ limit,
+ order: [['published_at', 'DESC']]
+ });
+}
+```
+
+## Caching Strategy
+
+```javascript
+async function getPopularPosts(cache, db, ttlSeconds = 3600) {
+ const key = 'popular-posts';
+ const cached = await cache.get(key);
+ if (cached) return cached;
+
+ const posts = await db.posts.findAll({
+ where: { status: 'published' },
+ order: [['view_count', 'DESC']],
+ limit: 10
+ });
+
+ await cache.set(key, posts, ttlSeconds);
+ return posts;
+}
+```
+
+## Security Checklist
+
+- [ ] Sanitize all user HTML input (use allow-list approach, e.g. DOMPurify)
+- [ ] Validate author permissions before edit / delete
+- [ ] Rate-limit comment submissions (e.g. 5 per minute per IP)
+- [ ] CSRF protection on all state-changing forms
+- [ ] CAPTCHA for guest comments
+- [ ] No raw SQL concatenation — use parameterized queries
+
+## Integration Points
+
+| Concern | Options |
+|---------|---------|
+| Email subscribers | SendGrid, Mailchimp, Resend, framework mailer |
+| CDN / image storage | Cloudflare R2, AWS S3, MinIO |
+| Analytics | Plausible, Google Analytics, Umami |
+| Search | Algolia, Meilisearch, Elasticsearch, Postgres full-text |
+| Comments external | Disqus, Facebook Comments, Giscus |
+
+## Handoff Protocol
+
+After implementation:
+1. End-to-end test: create → publish → view → comment
+2. Verify SEO meta tags and Open Graph
+3. Validate RSS feed and sitemap XML
+4. Test comment spam filtering
+5. Confirm scheduled publishing works
+6. Security audit via `@security-auditor`
diff --git a/.kilo/skills/booking/SKILL.md b/.kilo/skills/booking/SKILL.md
new file mode 100644
index 0000000..9a031f3
--- /dev/null
+++ b/.kilo/skills/booking/SKILL.md
@@ -0,0 +1,547 @@
+---
+name: booking
+description: Booking domain knowledge - appointments, services, staff, schedules, availability, reservations, notifications, calendar sync
+---
+
+# Booking Skill
+
+## Purpose
+
+Universal domain knowledge for building booking and appointment systems: services, staff, schedules, availability, reservations, notifications, calendar sync.
+
+## Capabilities
+
+### Service Management
+- Service categories
+- Service duration and pricing
+- Service variants (different durations / prices)
+- Required resources / rooms
+- Active / inactive toggle
+
+### Staff Management
+- Staff profiles and roles (admin / manager / staff)
+- Specializations (linked to services)
+- Availability schedules
+- Time-off / vacation tracking
+
+### Booking Flow
+- Service selection
+- Staff selection (optional or auto-assign)
+- Date / time slot selection
+- Customer details (guest or registered)
+- Confirmation
+- Payment (optional)
+
+### Scheduling
+- Working hours per day of week
+- Break times
+- Days off
+- Multi-location support
+- Timezone handling
+- Advance booking limits (min / max)
+
+### Notifications
+- Email confirmations
+- SMS reminders
+- Calendar attachments (iCal / Google Calendar)
+- Push notifications (if applicable)
+
+### Admin Dashboard
+- Booking list with filters (status, date, staff)
+- Availability editor
+- Customer CRM view
+- Revenue reports
+- Staff utilization
+
+## Database Schema (Universal)
+
+Use `id` types appropriate to your DB (INTEGER / UUID / BIGSERIAL). Adapt table/column names to your framework conventions.
+
+### Services
+
+```sql
+CREATE TABLE service_categories (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ slug TEXT UNIQUE NOT NULL,
+ description TEXT,
+ image_url TEXT,
+ sort_order INTEGER DEFAULT 0
+);
+
+CREATE TABLE services (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ category_id INTEGER,
+ name TEXT NOT NULL,
+ description TEXT,
+ duration INTEGER NOT NULL, -- minutes
+ price DECIMAL(10,2) NOT NULL,
+ buffer_time INTEGER DEFAULT 0, -- minutes between appointments
+ max_per_slot INTEGER DEFAULT 1,
+ is_active BOOLEAN DEFAULT 1,
+ image_url TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (category_id) REFERENCES service_categories(id)
+);
+
+-- Optional: per-staff overrides
+CREATE TABLE service_staff (
+ service_id INTEGER NOT NULL,
+ staff_id INTEGER NOT NULL,
+ custom_price DECIMAL(10,2),
+ custom_duration INTEGER,
+ PRIMARY KEY (service_id, staff_id),
+ FOREIGN KEY (service_id) REFERENCES services(id),
+ FOREIGN KEY (staff_id) REFERENCES staff(id)
+);
+```
+
+### Staff
+
+```sql
+CREATE TABLE staff (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER UNIQUE,
+ name TEXT NOT NULL,
+ email TEXT UNIQUE NOT NULL,
+ phone TEXT,
+ avatar_url TEXT,
+ bio TEXT,
+ role TEXT DEFAULT 'staff', -- 'admin' | 'manager' | 'staff'
+ is_active BOOLEAN DEFAULT 1,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+);
+
+CREATE TABLE staff_schedules (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ staff_id INTEGER NOT NULL,
+ day_of_week INTEGER NOT NULL, -- 0=Sun ... 6=Sat
+ start_time TEXT NOT NULL, -- '09:00'
+ end_time TEXT NOT NULL, -- '17:00'
+ break_start TEXT, -- nullable
+ break_end TEXT, -- nullable
+ is_working BOOLEAN DEFAULT 1,
+ FOREIGN KEY (staff_id) REFERENCES staff(id)
+);
+
+CREATE TABLE staff_time_off (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ staff_id INTEGER NOT NULL,
+ start_date DATE NOT NULL,
+ end_date DATE NOT NULL,
+ reason TEXT,
+ FOREIGN KEY (staff_id) REFERENCES staff(id)
+);
+```
+
+### Bookings
+
+```sql
+CREATE TABLE bookings (
+ id TEXT PRIMARY KEY, -- UUID recommended
+ booking_number TEXT UNIQUE NOT NULL,
+ service_id INTEGER NOT NULL,
+ staff_id INTEGER,
+ customer_id INTEGER,
+
+ -- Guest booking support
+ customer_name TEXT NOT NULL,
+ customer_email TEXT NOT NULL,
+ customer_phone TEXT,
+ customer_notes TEXT,
+
+ booking_date DATE NOT NULL,
+ start_time TEXT NOT NULL,
+ end_time TEXT NOT NULL,
+
+ status TEXT DEFAULT 'pending', -- 'pending' | 'confirmed' | 'completed' | 'cancelled' | 'no_show'
+
+ service_price DECIMAL(10,2) NOT NULL,
+ addons_total DECIMAL(10,2) DEFAULT 0,
+ discount DECIMAL(10,2) DEFAULT 0,
+ total DECIMAL(10,2) NOT NULL,
+
+ payment_method TEXT, -- 'cash' | 'card' | 'online'
+ payment_status TEXT DEFAULT 'pending', -- 'pending' | 'paid' | 'refunded'
+
+ source TEXT DEFAULT 'website', -- 'website' | 'phone' | 'walk_in'
+ notes TEXT,
+ internal_notes TEXT,
+
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+
+ FOREIGN KEY (service_id) REFERENCES services(id),
+ FOREIGN KEY (staff_id) REFERENCES staff(id),
+ FOREIGN KEY (customer_id) REFERENCES customers(id)
+);
+
+CREATE INDEX idx_bookings_date ON bookings(booking_date);
+CREATE INDEX idx_bookings_staff ON bookings(staff_id);
+CREATE INDEX idx_bookings_status ON bookings(status);
+```
+
+### Customers
+
+```sql
+CREATE TABLE customers (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ email TEXT UNIQUE NOT NULL,
+ phone TEXT,
+ date_of_birth DATE,
+ notes TEXT,
+ total_visits INTEGER DEFAULT 0,
+ total_spent DECIMAL(10,2) DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE INDEX idx_customers_email ON customers(email);
+CREATE INDEX idx_customers_phone ON customers(phone);
+```
+
+### Settings (Key-Value)
+
+```sql
+CREATE TABLE booking_settings (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL,
+ type TEXT DEFAULT 'string' -- 'string' | 'number' | 'boolean' | 'json'
+);
+```
+
+Default settings to insert:
+
+| key | value | type |
+|---|---|---|
+| timezone | Europe/Moscow | string |
+| currency | RUB | string |
+| booking_interval | 30 | number |
+| min_booking_notice | 60 | number |
+| max_booking_advance | 30 | number |
+| require_phone | 1 | boolean |
+| require_payment | 0 | boolean |
+| deposit_percentage | 20 | number |
+| cancellation_hours | 24 | number |
+| reminder_hours | 2 | number |
+| confirmation_email | 1 | boolean |
+| confirmation_sms | 0 | boolean |
+
+## API Endpoints
+
+### Public API
+
+```yaml
+GET /api/services # Active services
+GET /api/services/:id # Service detail
+GET /api/categories # Service categories
+
+GET /api/staff # Active staff
+GET /api/staff/:id # Staff detail
+GET /api/staff/:id/availability # Availability slots
+
+GET /api/availability # Available slots (?service_id=&date=)
+POST /api/availability/check # Check specific slot
+
+POST /api/bookings # Create booking (guest)
+GET /api/bookings/:id # Booking detail
+POST /api/bookings/:id/cancel # Cancel booking
+POST /api/bookings/:id/reschedule # Reschedule
+
+GET /api/customer/bookings # My bookings (auth)
+POST /api/customer/register # Register customer
+```
+
+### Admin API
+
+```yaml
+GET /api/admin/services # All services
+POST /api/admin/services # Create
+PUT /api/admin/services/:id # Update
+DELETE /api/admin/services/:id # Delete
+
+GET /api/admin/categories # Categories
+POST /api/admin/categories # Create
+PUT /api/admin/categories/:id # Update
+DELETE /api/admin/categories/:id # Delete
+
+GET /api/admin/staff # All staff
+POST /api/admin/staff # Add
+PUT /api/admin/staff/:id # Update
+DELETE /api/admin/staff/:id # Remove
+PUT /api/admin/staff/:id/schedule # Update schedule
+POST /api/admin/staff/:id/time-off # Add time off
+
+GET /api/admin/bookings # List (filtered)
+GET /api/admin/bookings/:id # Detail
+PUT /api/admin/bookings/:id/confirm
+PUT /api/admin/bookings/:id/complete
+PUT /api/admin/bookings/:id/cancel
+PUT /api/admin/bookings/:id/no-show
+
+GET /api/admin/calendar # Calendar view
+GET /api/admin/calendar/staff/:id # Staff calendar
+
+GET /api/admin/customers # Customers
+GET /api/admin/customers/:id # Detail + history
+
+GET /api/admin/reports/revenue # Revenue report
+GET /api/admin/reports/services # Service popularity
+GET /api/admin/reports/staff # Utilization
+GET /api/admin/reports/trends # Booking trends
+
+GET /api/admin/settings # All settings
+PUT /api/admin/settings # Update
+```
+
+## Availability Algorithm
+
+### Generate Available Slots
+
+```javascript
+function getAvailableSlots(service, staffSchedule, existingBookings, date) {
+ const dayOfWeek = new Date(date).getDay();
+ const schedule = staffSchedule.find(s => s.day_of_week === dayOfWeek && s.is_working);
+ if (!schedule) return [];
+
+ const slots = [];
+ let current = parseTime(schedule.start_time);
+ const end = parseTime(schedule.end_time);
+ const duration = service.duration + (service.buffer_time || 0);
+
+ while (addMinutes(current, duration) <= end) {
+ const slotStart = formatTime(current);
+ const slotEnd = formatTime(addMinutes(current, service.duration));
+
+ const isBooked = existingBookings.some(b =>
+ b.start_time < slotEnd && b.end_time > slotStart
+ );
+
+ const isBreak = schedule.break_start && schedule.break_end &&
+ slotStart < schedule.break_end && slotEnd > schedule.break_start;
+
+ const slotDateTime = new Date(`${date}T${slotStart}`);
+ const isTooSoon = slotDateTime < addMinutes(new Date(), settings.min_booking_notice);
+ const isPast = slotDateTime < new Date();
+
+ if (!isBooked && !isBreak && !isTooSoon && !isPast) {
+ slots.push({ start_time: slotStart, end_time: slotEnd, available: true });
+ }
+
+ current = addMinutes(current, duration);
+ }
+ return slots;
+}
+```
+
+### Create Booking
+
+```javascript
+async function createBooking(db, data) {
+ const service = await db.services.findById(data.service_id);
+ if (!service || !service.is_active) throw new Error('Service unavailable');
+
+ const staff = data.staff_id
+ ? await db.staff.findById(data.staff_id)
+ : await autoAssignStaff(db, service.id, data.date, data.time);
+ if (!staff) throw new Error('No staff available');
+
+ const available = await checkAvailability(db, service.id, staff.id, data.date, data.time);
+ if (!available) throw new Error('Slot already booked');
+
+ const endTime = addMinutes(parseTime(data.time), service.duration);
+
+ const booking = await db.bookings.create({
+ id: generateUUID(),
+ booking_number: generateBookingNumber(),
+ service_id: service.id,
+ staff_id: staff.id,
+ customer_name: data.customer_name,
+ customer_email: data.customer_email,
+ customer_phone: data.customer_phone,
+ booking_date: data.date,
+ start_time: data.time,
+ end_time: formatTime(endTime),
+ status: 'pending',
+ service_price: service.price,
+ total: service.price
+ });
+
+ // Optional: lock slot in availability cache / table
+ await sendConfirmation(booking, service, staff);
+ return booking;
+}
+```
+
+## Booking Status Flow
+
+```
+pending → confirmed → completed
+ ↓ ↓ ↓
+cancelled cancelled cancelled → refunded
+ ↓
+no_show
+```
+
+Valid transitions:
+
+| From | To |
+|---|---|
+| pending | confirmed, cancelled |
+| confirmed | completed, cancelled, no_show |
+| completed | refunded |
+| cancelled | refunded |
+| no_show | — |
+| refunded | — |
+
+```javascript
+const STATUS_FLOW = {
+ pending: ['confirmed', 'cancelled'],
+ confirmed: ['completed', 'cancelled', 'no_show'],
+ completed: ['refunded'],
+ cancelled: ['refunded'],
+ no_show: [],
+ refunded: []
+};
+```
+
+## Notifications
+
+### Email Templates
+
+```javascript
+function bookingConfirmation(booking, service, staff) {
+ return {
+ subject: `Booking Confirmed - ${service.name}`,
+ html: `
+ Your booking is confirmed!
+ Booking #${booking.booking_number}
+
+ - Service: ${service.name}
+ - Staff: ${staff.name}
+ - Date: ${formatDate(booking.booking_date)}
+ - Time: ${booking.start_time} - ${booking.end_time}
+ - Price: ${formatCurrency(booking.total)}
+
+ Manage booking
+ `
+ };
+}
+```
+
+### SMS Templates
+
+```javascript
+const smsTemplates = {
+ confirmation: (b, s) =>
+ `Your ${s.name} booking confirmed for ${formatDate(b.booking_date)} at ${b.start_time}. #${b.booking_number}`,
+ reminder: (b, s) =>
+ `Reminder: ${s.name} appointment in ${settings.reminder_hours}h at ${b.start_time}.`,
+ cancellation: b =>
+ `Your booking #${b.booking_number} has been cancelled.`
+};
+```
+
+## Calendar Integration
+
+### iCal Export
+
+```javascript
+function generateICal(booking, service, staff) {
+ return `BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+DTSTART:${toICalDate(booking.booking_date, booking.start_time)}
+DTEND:${toICalDate(booking.booking_date, booking.end_time)}
+SUMMARY:${service.name} with ${staff.name}
+DESCRIPTION:Booking #${booking.booking_number}
+STATUS:CONFIRMED
+END:VEVENT
+END:VCALENDAR`;
+}
+```
+
+## Reports
+
+### Revenue Report
+
+```sql
+SELECT
+ DATE(booking_date) AS date,
+ COUNT(*) AS bookings,
+ SUM(total) AS revenue,
+ AVG(total) AS avg_booking
+FROM bookings
+WHERE status IN ('completed', 'confirmed')
+ AND booking_date BETWEEN ? AND ?
+GROUP BY DATE(booking_date)
+ORDER BY date;
+```
+
+### Staff Utilization
+
+```sql
+SELECT
+ s.id,
+ s.name,
+ COUNT(b.id) AS bookings,
+ SUM(
+ CAST(strftime('%s', b.end_time) AS INTEGER) -
+ CAST(strftime('%s', b.start_time) AS INTEGER)
+ ) / 3600.0 AS hours_booked
+FROM staff s
+LEFT JOIN bookings b ON b.staff_id = s.id
+ AND b.status IN ('completed', 'confirmed')
+ AND b.booking_date BETWEEN ? AND ?
+GROUP BY s.id
+ORDER BY bookings DESC;
+```
+
+### Service Popularity
+
+```sql
+SELECT
+ s.id,
+ s.name,
+ COUNT(b.id) AS bookings,
+ SUM(b.total) AS revenue
+FROM services s
+LEFT JOIN bookings b ON b.service_id = s.id
+ AND b.status IN ('completed', 'confirmed')
+ AND b.booking_date BETWEEN ? AND ?
+GROUP BY s.id
+ORDER BY bookings DESC;
+```
+
+## Security Checklist
+
+- [ ] Validate service exists and is active before booking
+- [ ] Prevent double-booking via DB unique constraint or atomic check
+- [ ] Rate-limit booking creation (e.g. 10/hour per IP)
+- [ ] Sanitize customer notes / internal notes
+- [ ] No raw SQL — parameterized queries only
+- [ ] Admin endpoints protected by role middleware
+
+## Integration Points
+
+| Concern | Options |
+|---------|---------|
+| Payment | Stripe, YooKassa, PayPal |
+| SMS | Twilio, SMS.ru, Vonage |
+| Email | SendGrid, Mailgun, Resend, framework mailer |
+| Calendar | Google Calendar API, iCal attachments |
+| Analytics | Google Analytics, Yandex Metrika |
+
+## Handoff Protocol
+
+After implementation:
+1. End-to-end test: select service → staff → slot → book → confirm
+2. Verify no double-booking with concurrent requests
+3. Test status transitions and notifications
+4. Validate calendar attachment downloads correctly
+5. Confirm reports return accurate numbers
+6. Security audit via `@security-auditor`
diff --git a/.kilo/skills/ecommerce/SKILL.md b/.kilo/skills/ecommerce/SKILL.md
new file mode 100644
index 0000000..e743948
--- /dev/null
+++ b/.kilo/skills/ecommerce/SKILL.md
@@ -0,0 +1,489 @@
+---
+name: ecommerce
+description: E-commerce domain knowledge - products, variants, categories, cart, orders, payments, inventory, discounts
+---
+
+# E-commerce Skill
+
+## Purpose
+
+Universal domain knowledge for building e-commerce systems: product catalogs, shopping carts, order processing, payment integration, inventory management, discounts.
+
+## Capabilities
+
+### Product Catalog
+- Product CRUD with variants (size, color, material)
+- Categories (hierarchical)
+- Tags / attributes
+- Pricing: base, compare-at, cost, per-variant adjustments
+- Image galleries
+- Search and faceted filtering
+
+### Shopping Cart
+- Add / remove / update quantities
+- Guest (session) and persistent (registered) carts
+- Price recalculation
+- Discount codes
+- Shipping estimation
+
+### Order Processing
+- Order creation from cart
+- Status workflow with history
+- Invoice generation
+- Email notifications
+- Guest checkout support
+
+### Payments
+- Stripe, PayPal, YooKassa integration patterns
+- Payment status tracking
+- Refund processing
+- Webhook handling (idempotent)
+
+### Inventory
+- Stock tracking per SKU / variant
+- Low-stock alerts
+- Inventory adjustments with reason
+- Concurrent purchase safety (locks or optimistic)
+
+### Discounts
+- Coupon codes (percentage / fixed / free shipping)
+- Usage limits (global, per-user, per-code)
+- Date validity
+- Minimum order requirements
+
+## Database Schema (Universal)
+
+Use `id` types appropriate to your DB (INTEGER / UUID / BIGSERIAL). Adapt naming to your framework conventions.
+
+### Products
+
+```sql
+CREATE TABLE categories (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ slug TEXT UNIQUE NOT NULL,
+ parent_id INTEGER,
+ description TEXT,
+ image_url TEXT,
+ sort_order INTEGER DEFAULT 0,
+ FOREIGN KEY (parent_id) REFERENCES categories(id)
+);
+
+CREATE TABLE products (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ sku TEXT UNIQUE NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ price DECIMAL(10,2) NOT NULL,
+ compare_at_price DECIMAL(10,2),
+ cost_price DECIMAL(10,2),
+ quantity INTEGER DEFAULT 0, -- base stock if no variants
+ category_id INTEGER,
+ is_active BOOLEAN DEFAULT 1,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (category_id) REFERENCES categories(id)
+);
+
+CREATE TABLE product_variants (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ product_id INTEGER NOT NULL,
+ sku TEXT UNIQUE NOT NULL,
+ name TEXT NOT NULL, -- e.g. "Red / Large"
+ price_adjustment DECIMAL(10,2) DEFAULT 0,
+ quantity INTEGER DEFAULT 0,
+ attributes TEXT, -- JSON {"color":"red","size":"L"}
+ is_active BOOLEAN DEFAULT 1,
+ FOREIGN KEY (product_id) REFERENCES products(id)
+);
+
+CREATE TABLE product_images (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ product_id INTEGER,
+ variant_id INTEGER,
+ url TEXT NOT NULL,
+ alt_text TEXT,
+ sort_order INTEGER DEFAULT 0,
+ FOREIGN KEY (product_id) REFERENCES products(id),
+ FOREIGN KEY (variant_id) REFERENCES product_variants(id)
+);
+```
+
+### Cart
+
+```sql
+CREATE TABLE carts (
+ id TEXT PRIMARY KEY, -- UUID
+ user_id INTEGER,
+ session_id TEXT,
+ currency TEXT DEFAULT 'USD',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+);
+
+CREATE TABLE cart_items (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ cart_id TEXT NOT NULL,
+ product_id INTEGER NOT NULL,
+ variant_id INTEGER,
+ quantity INTEGER NOT NULL DEFAULT 1,
+ price DECIMAL(10,2) NOT NULL, -- snapshot at add time
+ FOREIGN KEY (cart_id) REFERENCES carts(id) ON DELETE CASCADE,
+ FOREIGN KEY (product_id) REFERENCES products(id),
+ FOREIGN KEY (variant_id) REFERENCES product_variants(id)
+);
+```
+
+### Orders
+
+```sql
+CREATE TABLE orders (
+ id TEXT PRIMARY KEY, -- UUID
+ order_number TEXT UNIQUE NOT NULL,
+ user_id INTEGER,
+ email TEXT NOT NULL,
+ status TEXT DEFAULT 'pending', -- see status table below
+ subtotal DECIMAL(10,2) NOT NULL,
+ tax DECIMAL(10,2) DEFAULT 0,
+ shipping DECIMAL(10,2) DEFAULT 0,
+ discount DECIMAL(10,2) DEFAULT 0,
+ total DECIMAL(10,2) NOT NULL,
+ currency TEXT DEFAULT 'USD',
+ shipping_address TEXT, -- JSON or normalized columns
+ billing_address TEXT, -- JSON or normalized columns
+ notes TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+);
+
+CREATE TABLE order_items (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ order_id TEXT NOT NULL,
+ product_id INTEGER NOT NULL,
+ variant_id INTEGER,
+ name TEXT NOT NULL, -- snapshot
+ sku TEXT NOT NULL, -- snapshot
+ quantity INTEGER NOT NULL,
+ price DECIMAL(10,2) NOT NULL, -- unit price snapshot
+ total DECIMAL(10,2) NOT NULL, -- price * quantity
+ FOREIGN KEY (order_id) REFERENCES orders(id),
+ FOREIGN KEY (product_id) REFERENCES products(id)
+);
+
+CREATE TABLE order_status_history (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ order_id TEXT NOT NULL,
+ status TEXT NOT NULL,
+ notes TEXT,
+ created_by INTEGER,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (order_id) REFERENCES orders(id)
+);
+```
+
+### Payments
+
+```sql
+CREATE TABLE payments (
+ id TEXT PRIMARY KEY, -- UUID
+ order_id TEXT NOT NULL,
+ provider TEXT NOT NULL, -- 'stripe' | 'paypal' | 'yookassa'
+ transaction_id TEXT,
+ amount DECIMAL(10,2) NOT NULL,
+ currency TEXT DEFAULT 'USD',
+ status TEXT DEFAULT 'pending', -- 'pending' | 'completed' | 'failed' | 'refunded'
+ metadata TEXT, -- JSON
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (order_id) REFERENCES orders(id)
+);
+```
+
+### Inventory
+
+```sql
+CREATE TABLE inventory_transactions (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ product_id INTEGER,
+ variant_id INTEGER,
+ quantity INTEGER NOT NULL,
+ type TEXT NOT NULL, -- 'purchase' | 'sale' | 'adjustment' | 'return'
+ reference TEXT, -- order_id or PO_id
+ notes TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (product_id) REFERENCES products(id),
+ FOREIGN KEY (variant_id) REFERENCES product_variants(id)
+);
+```
+
+### Discounts
+
+```sql
+CREATE TABLE discount_codes (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ code TEXT UNIQUE NOT NULL,
+ type TEXT NOT NULL, -- 'percentage' | 'fixed' | 'free_shipping'
+ value DECIMAL(10,2) NOT NULL, -- percent or amount
+ min_order_amount DECIMAL(10,2) DEFAULT 0,
+ max_discount DECIMAL(10,2),
+ usage_limit INTEGER, -- total global uses
+ usage_limit_per_customer INTEGER,
+ used_count INTEGER DEFAULT 0,
+ starts_at DATETIME,
+ expires_at DATETIME,
+ is_active BOOLEAN DEFAULT 1,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+## Order Statuses
+
+| Status | Description | Next |
+|---|---|---|
+| `pending` | Created, awaiting payment | `processing`, `cancelled` |
+| `processing` | Payment confirmed, preparing | `shipped`, `on_hold` |
+| `shipped` | Dispatched to customer | `delivered` |
+| `delivered` | Customer received | `completed` |
+| `completed` | Order fulfilled | — |
+| `on_hold` | Manual review / fraud check | `processing`, `cancelled` |
+| `cancelled` | Order cancelled | `refunded` |
+| `refunded` | Money returned | — |
+
+## API Endpoints
+
+### Public / Storefront
+
+```yaml
+GET /api/products # List products (paginated, filtered)
+GET /api/products/:slug # Product detail with variants
+GET /api/categories # Categories tree
+GET /api/categories/:slug # Category + products
+GET /api/tags # Tags
+
+GET /api/cart # Current cart
+POST /api/cart/items # Add to cart
+PUT /api/cart/items/:id # Update quantity
+DELETE /api/cart/items/:id # Remove item
+POST /api/cart/coupon # Apply / remove coupon
+
+POST /api/checkout # Create order from cart
+POST /api/checkout/guest # Guest checkout
+GET /api/orders/:id # Order detail (guest by token)
+```
+
+### Admin / Management
+
+```yaml
+GET /api/admin/products # List products
+POST /api/admin/products # Create product
+PUT /api/admin/products/:id # Update
+DELETE /api/admin/products/:id # Delete
+
+GET /api/admin/categories # Categories
+POST /api/admin/categories # Create
+PUT /api/admin/categories/:id # Update
+DELETE /api/admin/categories/:id # Delete
+
+GET /api/admin/orders # Orders (filtered)
+GET /api/admin/orders/:id # Order detail
+PUT /api/admin/orders/:id/status
+POST /api/admin/orders/:id/refund
+
+GET /api/admin/inventory # Inventory snapshot
+PUT /api/admin/inventory/:id # Update stock
+POST /api/admin/inventory/adjust # Adjustment with reason
+
+GET /api/admin/discounts # Discount codes
+POST /api/admin/discounts # Create
+PUT /api/admin/discounts/:id # Update
+```
+
+## Payment Integration
+
+### Stripe Pattern
+
+```javascript
+const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
+
+async function createPaymentIntent(order) {
+ const intent = await stripe.paymentIntents.create({
+ amount: Math.round(order.total * 100), // cents
+ currency: order.currency.toLowerCase(),
+ metadata: { orderId: order.id },
+ receipt_email: order.email
+ });
+ return { clientSecret: intent.client_secret, paymentIntentId: intent.id };
+}
+
+async function handleWebhook(signature, payload) {
+ const event = stripe.webhooks.constructEvent(
+ payload, signature, process.env.STRIPE_WEBHOOK_SECRET
+ );
+ switch (event.type) {
+ case 'payment_intent.succeeded':
+ await markOrderPaid(event.data.object.metadata.orderId);
+ break;
+ case 'payment_intent.payment_failed':
+ await markOrderFailed(event.data.object.metadata.orderId);
+ break;
+ }
+}
+```
+
+### PayPal Pattern
+
+```javascript
+const paypal = require('@paypal/checkout-server-sdk');
+
+async function createPayPalOrder(order) {
+ const request = new paypal.orders.OrdersCreateRequest();
+ request.requestBody({
+ intent: 'CAPTURE',
+ purchase_units: [{
+ amount: {
+ currency_code: order.currency,
+ value: order.total.toFixed(2)
+ },
+ reference_id: order.id
+ }]
+ });
+ const response = await client.execute(request);
+ return response.result;
+}
+
+async function capturePayPalOrder(orderId) {
+ const request = new paypal.orders.OrdersCaptureRequest(orderId);
+ const response = await client.execute(request);
+ return response.result;
+}
+```
+
+## Inventory Safety
+
+### Stock Deduction at Checkout
+
+```javascript
+async function deductStock(db, orderId) {
+ await db.transaction(async (trx) => {
+ for (const item of orderItems) {
+ const table = item.variant_id ? 'product_variants' : 'products';
+ const idField = item.variant_id ? 'id' : 'id';
+ const idValue = item.variant_id || item.product_id;
+
+ const result = await trx(table)
+ .where({ id: idValue, quantity: db.raw('quantity >= ?', [item.quantity]) })
+ .decrement('quantity', item.quantity);
+
+ if (result === 0) throw new Error(`Insufficient stock for SKU ${item.sku}`);
+
+ await trx('inventory_transactions').insert({
+ product_id: item.product_id,
+ variant_id: item.variant_id,
+ quantity: -item.quantity,
+ type: 'sale',
+ reference: orderId
+ });
+ }
+ });
+}
+```
+
+### Low-Stock Alert Query
+
+```sql
+SELECT id, sku, name, quantity
+FROM products
+WHERE quantity <= ? AND is_active = 1;
+
+-- For variants
+SELECT v.id, v.sku, p.name || ' - ' || v.name AS full_name, v.quantity
+FROM product_variants v
+JOIN products p ON p.id = v.product_id
+WHERE v.quantity <= ? AND v.is_active = 1;
+```
+
+## Discount Logic
+
+```javascript
+function applyDiscount(cart, code) {
+ if (!code.is_active) throw new Error('Code inactive');
+ if (code.starts_at && now < code.starts_at) throw new Error('Not started');
+ if (code.expires_at && now > code.expires_at) throw new Error('Expired');
+ if (code.usage_limit && code.used_count >= code.usage_limit) throw new Error('Limit reached');
+ if (cart.subtotal < code.min_order_amount) throw new Error('Minimum order not met');
+
+ let discount = 0;
+ if (code.type === 'percentage') {
+ discount = cart.subtotal * (code.value / 100);
+ } else if (code.type === 'fixed') {
+ discount = Math.min(code.value, cart.subtotal);
+ } else if (code.type === 'free_shipping') {
+ discount = cart.shipping;
+ }
+
+ if (code.max_discount) discount = Math.min(discount, code.max_discount);
+ return discount;
+}
+```
+
+## Email Templates
+
+### Order Confirmation
+
+```html
+Thank you for your order!
+Order #{{order_number}}
+
+ {{#items}}
+ - {{name}} ({{sku}}) × {{quantity}} — {{currency}}{{total}}
+ {{/items}}
+
+Subtotal: {{currency}}{{subtotal}}
+Shipping: {{currency}}{{shipping}}
+Discount: -{{currency}}{{discount}}
+Total: {{currency}}{{total}}
+We'll email tracking info once shipped.
+```
+
+### Shipping Notification
+
+```html
+Your order has shipped!
+Order #{{order_number}}
+Carrier: {{carrier}}
+Tracking: {{tracking_number}}
+```
+
+## Security Checklist
+
+- [ ] Never store credit card numbers
+- [ ] Use PCI-compliant payment providers only
+- [ ] CSRF protection on all state-changing cart / checkout endpoints
+- [ ] Rate-limit checkout attempts (e.g. 5/minute per IP)
+- [ ] Validate all discount constraints server-side
+- [ ] Inventory deduction inside atomic transaction
+- [ ] Webhook signatures verified before acting
+- [ ] Order totals recalculated server-side; never trust client total
+
+## Integration Points
+
+| Concern | Options |
+|---------|---------|
+| Payment | Stripe, PayPal, YooKassa, Square |
+| Shipping rates | ShipStation, EasyPost, carrier APIs |
+| Tax calculation | TaxJar, Avalara, manual rules |
+| Email | SendGrid, Mailgun, Resend |
+| Search | Algolia, Meilisearch, Postgres full-text |
+| Analytics | Google Analytics, Mixpanel, Plausible |
+| CDN / images | Cloudflare R2, AWS S3, MinIO |
+
+## Handoff Protocol
+
+After implementation:
+1. End-to-end test: add to cart → apply coupon → checkout → pay → order confirmation
+2. Verify concurrent stock safety (simulate two checkouts for last item)
+3. Test payment webhooks and status transitions
+4. Validate email templates render correctly
+5. Check discount edge cases (expired, over-limit, minimum not met)
+6. Security audit via `@security-auditor`