From e7d350ec3dc66829eafd3f882c24da42c92ce4e3 Mon Sep 17 00:00:00 2001 From: xiong Date: Tue, 28 Jul 2026 17:01:44 +0800 Subject: [PATCH] feat: bootstrap local platform development --- README.md | 24 +- .../ApplicationBuilderExtensions.cs | 2 + Tiku.Api/appsettings.Development.json | 2 +- Tiku.Api/wwwroot/platform-admin/app.js | 1351 +++++++++++++++++ .../wwwroot/platform-admin/assets/logo.png | Bin 0 -> 289534 bytes Tiku.Api/wwwroot/platform-admin/index.html | 35 + .../wwwroot/platform-admin/platform-api.js | 886 +++++++++++ .../wwwroot/platform-admin/platform-auth.js | 172 +++ .../wwwroot/platform-admin/runtime-config.js | 24 + .../wwwroot/platform-admin/spec-contract.js | 246 +++ Tiku.Api/wwwroot/platform-admin/styles.css | 50 + Tiku.DbMigrator/Program.cs | 19 +- .../DevelopmentPlatformAdminSeeder.cs | 192 +++ Tiku.Infrastructure/DependencyInjection.cs | 4 +- .../Api/PlatformAdminStaticEndpointTests.cs | 53 + .../PlatformAdminBootstrapperTests.cs | 37 + Tiku.Worker/appsettings.Development.json | 1 + Tiku.Worker/appsettings.json | 1 + docs/quickstart.md | 138 ++ 19 files changed, 3230 insertions(+), 7 deletions(-) create mode 100644 Tiku.Api/wwwroot/platform-admin/app.js create mode 100644 Tiku.Api/wwwroot/platform-admin/assets/logo.png create mode 100644 Tiku.Api/wwwroot/platform-admin/index.html create mode 100644 Tiku.Api/wwwroot/platform-admin/platform-api.js create mode 100644 Tiku.Api/wwwroot/platform-admin/platform-auth.js create mode 100644 Tiku.Api/wwwroot/platform-admin/runtime-config.js create mode 100644 Tiku.Api/wwwroot/platform-admin/spec-contract.js create mode 100644 Tiku.Api/wwwroot/platform-admin/styles.css create mode 100644 Tiku.Infrastructure/Bootstrap/DevelopmentPlatformAdminSeeder.cs create mode 100644 Tiku.IntegrationTests/Api/PlatformAdminStaticEndpointTests.cs create mode 100644 docs/quickstart.md diff --git a/README.md b/README.md index 141c54d..61780fa 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,26 @@ docs # ADR、架构说明和迁移路线 GET /platform-admin/ ``` -它是功能原型壳,不是正式视觉规范。默认 mock 模式;联调时通过 `runtime-config.js` 注入同源 `/api`、平台 access token provider 和 API 模式。 +它是功能原型壳,不是正式视觉规范。当前默认连接同源真实 API,并提供平台登录、首次改密和 TOTP MFA 流程;token 只保存在当前浏览器标签的 `sessionStorage`。真实模式不会回退显示 Mock 数据,目前开放概览、租户、员工和审计这组已经落地后端契约的页面,账务、公共题库等页面随对应 API 实现逐步开放。 + +本地首次启动先创建 `tiku` 数据库并执行迁移: + +```bash +createdb -h 127.0.0.1 -U "$(whoami)" tiku +ASPNETCORE_ENVIRONMENT=Development dotnet run --project Tiku.DbMigrator +``` + +Development 首次迁移会通过 EF Core 官方推荐的 `UseSeeding` / `UseAsyncSeeding` 初始化平台超级管理员;Migration Lock 保证并发安全,后续重复执行迁移会幂等跳过,不会重置密码或重复创建: + +```text +登录地址:http://localhost:5090/platform-admin/ +初始账号:admin@tiku.local +初始密码:由 Tiku.DbMigrator 安全随机生成,仅在首次初始化的终端输出一次 +``` + +首次登录必须立即修改初始密码并绑定 TOTP MFA。如果丢失首次输出的临时密码,应删除尚无业务数据的本地开发库后重新初始化,不要把密码补写到源码、`appsettings*.json` 或 README。Production 不会自动创建默认管理员,必须使用下文的显式安全引导命令。 + +开发环境只隐藏 EF Core 成功 SQL 日志,ORM 警告与错误仍会输出。 当前不把后端改成 MVC/Razor,也不为这个静态 demo 单独维护 Node 服务。后续平台端产品化时,建议迁为独立 React/Vite/Next 工程,.NET 继续提供 API。 @@ -85,6 +104,7 @@ PostgreSQL guard 负责 EF 无法表达的跨表租户不变量: ## 文档入口 +- [本地开发快速开始](docs/quickstart.md) - [当前认证、授权与 Host 安全策略](docs/architecture/authentication-authorization-security.md) - [认证与授权待补强清单](docs/architecture/authentication-authorization-hardening-plan.md) - [迁移路线与剩余范围](docs/migration-roadmap.md) @@ -117,7 +137,7 @@ dotnet ef migrations has-pending-model-changes \ --startup-project Tiku.DbMigrator ``` -首次部署可在迁移完成后创建平台超级管理员: +Production 首次部署可在迁移完成后显式创建平台超级管理员: ```bash export TIKU_BOOTSTRAP_PLATFORM_ADMIN_EMAIL='admin@example.com' diff --git a/Tiku.Api/Configuration/ApplicationBuilderExtensions.cs b/Tiku.Api/Configuration/ApplicationBuilderExtensions.cs index a188984..63a43ce 100644 --- a/Tiku.Api/Configuration/ApplicationBuilderExtensions.cs +++ b/Tiku.Api/Configuration/ApplicationBuilderExtensions.cs @@ -24,6 +24,8 @@ public static class ApplicationBuilderExtensions app.UseMiddleware(); app.UseForwardedHeaders(); app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); app.UseRouting(); app.UseCors(CorsOptions.PolicyName); app.UseMiddleware(); diff --git a/Tiku.Api/appsettings.Development.json b/Tiku.Api/appsettings.Development.json index 9057ce4..4b35d38 100644 --- a/Tiku.Api/appsettings.Development.json +++ b/Tiku.Api/appsettings.Development.json @@ -5,7 +5,7 @@ "Override": { "Microsoft": "Warning", "Microsoft.AspNetCore": "Information", - "Microsoft.EntityFrameworkCore.Database.Command": "Information", + "Microsoft.EntityFrameworkCore": "Warning", "System.Net.Http.HttpClient": "Warning" } } diff --git a/Tiku.Api/wwwroot/platform-admin/app.js b/Tiku.Api/wwwroot/platform-admin/app.js new file mode 100644 index 0000000..6d378d2 --- /dev/null +++ b/Tiku.Api/wwwroot/platform-admin/app.js @@ -0,0 +1,1351 @@ +const mainView = document.querySelector('#mainView'); +const sideNav = document.querySelector('#sideNav'); +const pageTitle = document.querySelector('#pageTitle'); +const toast = document.querySelector('.toast'); +const app = document.querySelector('.platform-app'); +const overlayRoot = document.querySelector('#overlayRoot'); +const roleSelect = document.querySelector('#roleSelect'); +const scenarioSelect = document.querySelector('#scenarioSelect'); +const responseSelect = document.querySelector('#responseSelect'); +const menuButton = document.querySelector('#menuButton'); +const taskCount = document.querySelector('#taskCount'); +const alertCount = document.querySelector('#alertCount'); +const platformApi = window.GongxuePlatformApi; +const STORAGE_KEY = 'gongxue-platform-spec-v1.1'; +const TENANT_PORTAL_ORIGIN = 'http://127.0.0.1:4174/'; +const HISTORY_APP_KEY = 'gongxuePlatform'; +const API_CONNECTED_PAGES = new Set(['overview', 'tenants', 'tenantDetail', 'billingProfile', 'staff', 'audit', 'alerts']); + +const pageMeta = { + overview: { id: 'P-01', title: '平台经营工作台', permission: 'platform:overview:read' }, + tenants: { id: 'P-02', title: '租户列表与创建', permission: 'platform:tenant:read' }, + tenantDetail: { id: 'P-03', title: '租户详情与生命周期', permission: 'platform:tenant:read', level: 2 }, + billingProfile: { id: 'P-04', title: '租户账务资料', permission: 'platform:tenant:billing_profile', level: 3 }, + subscriptions: { id: 'P-05', title: 'SaaS 套餐与订阅', permission: 'platform:plan:read' }, + billing: { id: 'P-06', title: '服务费账单', permission: 'platform:billing:read' }, + usage: { id: 'P-07', title: '用量与超额计费', permission: 'platform:usage:read' }, + dunning: { id: 'P-08', title: '收款、逾期与催缴', permission: 'platform:billing:read' }, + bank: { id: 'P-09', title: '公共题库目录与授权', permission: 'platform:question_bank:read' }, + staff: { id: 'P-10', title: '平台员工与权限', permission: 'platform:staff:read' }, + audit: { id: 'P-11', title: '平台审计日志', permission: 'platform:audit:read' }, + alerts: { id: 'P-12', title: '审计告警与通知', permission: 'platform:audit:alert' }, +}; + +const navGroups = [ + ['平台概览', [['overview', 'gauge', '经营工作台']]], + ['租户管理', [['tenants', 'building-2', '租户列表']]], + ['SaaS 账务', [['subscriptions', 'badge-check', '套餐与订阅'], ['billing', 'receipt-text', '服务费账单'], ['usage', 'chart-column-big', '用量与超额'], ['dunning', 'bell-ring', '收款与催缴']]], + ['平台资产', [['bank', 'database-zap', '公共题库']]], + ['安全治理', [['staff', 'user-cog', '员工与权限'], ['audit', 'scroll-text', '审计日志'], ['alerts', 'shield-alert', '告警与通知']]], +]; + +const roleDefinitions = { + super: { label: '超级管理员', permissions: ['*'] }, + tenant_ops: { label: '租户运营', permissions: ['platform:overview:read', 'platform:tenant:read', 'platform:tenant:write', 'platform:tenant:status', 'platform:tenant:billing_profile', 'platform:plan:read', 'platform:billing:read', 'platform:question_bank:read'] }, + finance: { label: '平台财务', permissions: ['platform:overview:read', 'platform:tenant:read', 'platform:tenant:billing_profile', 'platform:plan:read', 'platform:billing:read', 'platform:billing:write', 'platform:billing:payment', 'platform:billing:dunning', 'platform:billing:notification', 'platform:usage:read', 'platform:usage:write', 'platform:audit:read'] }, + audit: { label: '安全审计', permissions: ['platform:overview:read', 'platform:audit:read', 'platform:audit:export', 'platform:audit:alert', 'platform:audit:notification'] }, + bank_ops: { label: '题库运营', permissions: ['platform:overview:read', 'platform:question_bank:read', 'platform:question_bank:grant', 'platform:question_bank:ops'] }, + readonly: { label: '只读观察员', permissions: ['platform:overview:read', 'platform:tenant:read', 'platform:plan:read', 'platform:billing:read', 'platform:question_bank:read', 'platform:audit:read'] }, +}; + +const seed = { + version: 11, + tenants: [ + { id: 'tenant_gongxue_tj', slug: 'gongxue-tj', name: '恭学教育 · 天津', legal: '天津恭学教育科技有限公司', brand: '恭学教育', shortName: '恭学天津', host: 'study.gongxue.cn', plan: '旗舰版', planCode: 'enterprise', status: '正常', billing: '正常', mode: '正式', students: 2846, questions: 42680, storageGB: 186, trafficTB: 1.8, expires: '2027-07-22', balance: 0, owner: '张校长', ownerPhone: '138****6008', createdAt: '2025-07-22 10:18' }, + { id: 'tenant_qihang_gd', slug: 'qihang-gd', name: '启航专升本 · 广东', legal: '广州启航教育咨询有限公司', brand: '启航专升本', shortName: '启航广东', host: 'study.qihangzsb.cn', plan: '专业版', planCode: 'pro', status: '正常', billing: '待付款', mode: '正式', students: 5280, questions: 68420, storageGB: 248, trafficTB: 3.2, expires: '2026-08-05', balance: 12800, owner: '林老师', ownerPhone: '139****2288', createdAt: '2025-08-05 09:12' }, + { id: 'tenant_bosi_sd', slug: 'bosi-sd', name: '博思升本 · 山东', legal: '山东博思教育集团有限公司', brand: '博思升本', shortName: '博思山东', host: 'tiku.bosiedu.cn', plan: '基础版', planCode: 'basic', status: '试用', billing: '正常', mode: '试用', students: 642, questions: 12860, storageGB: 42, trafficTB: .6, expires: '2026-08-12', balance: 0, owner: '孙老师', ownerPhone: '136****1209', createdAt: '2026-07-12 14:20' }, + { id: 'tenant_mingtu_hn', slug: 'mingtu-hn', name: '明途教育 · 河南', legal: '郑州明途教育科技有限公司', brand: '明途教育', shortName: '明途河南', host: 'learn.mingtu.cn', plan: '基础版', planCode: 'basic', status: '暂停', billing: '逾期', mode: '正式', students: 1280, questions: 18620, storageGB: 86, trafficTB: .9, expires: '2026-07-18', balance: 8600, owner: '魏校长', ownerPhone: '135****8806', createdAt: '2025-01-18 11:03' }, + { id: 'tenant_xuezhi_heb', slug: 'xuezhi-heb', name: '学知专接本 · 河北', legal: '石家庄学知教育有限公司', brand: '学知专接本', shortName: '学知河北', host: 'app.xuezhizjb.cn', plan: '专业版', planCode: 'pro', status: '正常', billing: '正常', mode: '正式', students: 3160, questions: 39860, storageGB: 164, trafficTB: 2.1, expires: '2027-01-08', balance: 0, owner: '赵老师', ownerPhone: '137****6116', createdAt: '2025-01-08 08:40' }, + { id: 'tenant_youcai_js', slug: 'youcai-js', name: '优才专转本 · 江苏', legal: '南京优才教育科技有限公司', brand: '优才专转本', shortName: '优才江苏', host: 'study.youcai.cn', plan: '专业版', planCode: 'pro', status: '待配置', billing: '正常', mode: '试用', students: 0, questions: 0, storageGB: 0, trafficTB: 0, expires: '', balance: 0, owner: '顾老师', ownerPhone: '138****1120', createdAt: '2026-07-22 16:08' }, + ], + billingProfiles: { + tenant_gongxue_tj: { billingName: '天津恭学教育科技有限公司', taxId: '91120101MA07GX2026', contactName: '李会计', phone: '13800138000', email: 'finance@gongxue.cn', billingAddress: '天津市南开区科研西路 12 号', invoiceTitle: '天津恭学教育科技有限公司', type: '增值税专用发票', bankName: '招商银行天津南开支行', bankAccountMasked: '**** **** **** 8826' }, + tenant_qihang_gd: { billingName: '广州启航教育咨询有限公司', taxId: '91440101MA5QH2026X', contactName: '黄会计', phone: '13900139000', email: 'billing@qihangzsb.cn', billingAddress: '广州市天河区科韵路 18 号', invoiceTitle: '广州启航教育咨询有限公司', type: '增值税普通发票', bankName: '中国银行广州天河支行', bankAccountMasked: '**** **** **** 6672' }, + }, + plans: [ + { code: 'basic', name: '基础版', description: '适合单地区小型机构', cycle: '年度', amount: 4800, quotas: ['2,000 学生席位', '30,000 题目', '100 GB 存储', '基础内容运营'], tenantCount: 12 }, + { code: 'pro', name: '专业版', description: '适合成熟区域型机构', cycle: '年度', amount: 9800, quotas: ['8,000 学生席位', '100,000 题目', '500 GB 存储', 'CRM 与营销能力'], tenantCount: 24 }, + { code: 'enterprise', name: '旗舰版', description: '适合多校区与高内容规模', cycle: '年度', amount: 16800, quotas: ['20,000 学生席位', '300,000 题目', '2 TB 存储', '财务对账与高级审计'], tenantCount: 12 }, + ], + subscriptions: [ + { id: 'SUB-202607-001', tenantId: 'tenant_gongxue_tj', planCode: 'enterprise', status: '活跃', cycle: '年度', amount: 16800, start: '2026-07-22', expires: '2027-07-22', autoRenew: true }, + { id: 'SUB-202608-002', tenantId: 'tenant_qihang_gd', planCode: 'pro', status: '活跃', cycle: '年度', amount: 9800, start: '2025-08-05', expires: '2026-08-05', autoRenew: true }, + { id: 'SUB-TRIAL-003', tenantId: 'tenant_bosi_sd', planCode: 'basic', status: '试用中', cycle: '试用', amount: 0, start: '2026-07-12', expires: '2026-08-12', autoRenew: false }, + { id: 'SUB-202501-004', tenantId: 'tenant_mingtu_hn', planCode: 'basic', status: '暂停', cycle: '年度', amount: 4800, start: '2025-01-18', expires: '2026-07-18', autoRenew: false }, + { id: 'SUB-202601-005', tenantId: 'tenant_xuezhi_heb', planCode: 'pro', status: '活跃', cycle: '年度', amount: 9800, start: '2026-01-08', expires: '2027-01-08', autoRenew: true }, + ], + invoices: [ + { id: 'INV-202607-0018', tenantId: 'tenant_qihang_gd', type: '订阅服务费', period: '2026年7月', total: 12800, paid: 0, balance: 12800, due: '2026-08-05', status: '待付款', note: '2026年7月服务费' }, + { id: 'INV-202607-0017', tenantId: 'tenant_mingtu_hn', type: '订阅服务费', period: '2026年7月', total: 8600, paid: 0, balance: 8600, due: '2026-07-18', status: '已逾期', note: '2026年7月服务费' }, + { id: 'INV-202607-0016', tenantId: 'tenant_gongxue_tj', type: '订阅服务费', period: '2026年7月', total: 16800, paid: 16800, balance: 0, due: '2026-07-15', status: '已支付', note: '2026年7月服务费' }, + { id: 'INV-202607-0015', tenantId: 'tenant_xuezhi_heb', type: '订阅服务费', period: '2026年7月', total: 9800, paid: 9800, balance: 0, due: '2026-07-15', status: '已支付', note: '2026年7月服务费' }, + ], + usage: [ + { tenantId: 'tenant_gongxue_tj', period: '2026-07', source: 'Worker 自动采集', students: 2846, questions: 42680, storageGB: 186, trafficTB: 1.8, overage: [], estimated: 0 }, + { tenantId: 'tenant_qihang_gd', period: '2026-07', source: 'Worker 自动采集', students: 5280, questions: 68420, storageGB: 248, trafficTB: 3.2, overage: ['流量'], estimated: 7820 }, + { tenantId: 'tenant_bosi_sd', period: '2026-07', source: 'Worker 自动采集', students: 642, questions: 12860, storageGB: 42, trafficTB: .6, overage: [], estimated: 0 }, + { tenantId: 'tenant_mingtu_hn', period: '2026-07', source: '人工补录', students: 1280, questions: 18620, storageGB: 86, trafficTB: .9, overage: ['题目', '存储'], estimated: 4260 }, + { tenantId: 'tenant_xuezhi_heb', period: '2026-07', source: 'Worker 自动采集', students: 3160, questions: 39860, storageGB: 164, trafficTB: 2.1, overage: [], estimated: 0 }, + ], + reminders: [ + { id: 'REM-260723-01', invoiceId: 'INV-202607-0017', tenantId: 'tenant_mingtu_hn', level: 3, channel: '邮件 + 短信', status: '已送达', date: '2026-07-23 09:20', message: '服务费账单已逾期 5 天,请尽快处理。' }, + { id: 'REM-260722-02', invoiceId: 'INV-202607-0018', tenantId: 'tenant_qihang_gd', level: 1, channel: '邮件', status: '已送达', date: '2026-07-22 15:40', message: '账单将在 13 天后到期。' }, + ], + dunningChannels: [ + { id: 'DUN-EMAIL', code: 'finance_email', name: '财务邮件', provider: 'SMTP', endpoint: 'finance@gongxue.cn', enabled: true, retry: 3 }, + { id: 'DUN-WX', code: 'finance_wecom', name: '财务企微群', provider: 'Webhook', endpoint: 'https://qyapi.weixin.qq.com/***', enabled: true, retry: 5 }, + ], + banks: [ + { id: 'QB-TJ-MATH', name: '高等数学公共题库', catalog: '天津专升本 / 高等数学', source: '平台自建', questions: 12860, version: 'v2026.7', status: '稳定版' }, + { id: 'QB-EN', name: '公共英语能力库', catalog: '全国 / 公共英语', source: '平台自建', questions: 86640, version: 'v2026.6', status: '稳定版' }, + { id: 'QB-SD', name: '山东专升本全科', catalog: '山东专升本', source: '合作内容方', questions: 52360, version: 'v2026.7', status: '稳定版' }, + ], + grants: [ + { id: 'GRANT-001', bankId: 'QB-TJ-MATH', scope: '指定租户', allowedPlanCodes: [], allowedTenantIds: ['tenant_gongxue_tj', 'tenant_qihang_gd'], allowedRegionIds: ['天津'], allowedSubjectIds: ['高等数学'], status: '启用', startsAt: '2026-07-18', expiresAt: '2027-07-18', localEdit: false }, + { id: 'GRANT-002', bankId: 'QB-EN', scope: '指定套餐', allowedPlanCodes: ['pro', 'enterprise'], allowedTenantIds: [], allowedRegionIds: [], allowedSubjectIds: ['公共英语'], status: '启用', startsAt: '2026-06-01', expiresAt: '', localEdit: true }, + ], + bankSync: [ + { tenantId: 'tenant_gongxue_tj', bankId: 'QB-TJ-MATH', current: 'v2026.6', target: 'v2026.7', progress: 100, conflicts: 0, status: '已完成' }, + { tenantId: 'tenant_qihang_gd', bankId: 'QB-EN', current: 'v2026.5', target: 'v2026.6', progress: 72, conflicts: 3, status: '同步中' }, + { tenantId: 'tenant_mingtu_hn', bankId: 'QB-EN', current: 'v2026.3', target: 'v2026.6', progress: 38, conflicts: 12, status: '同步失败' }, + ], + staff: [ + { id: 'STAFF-01', authUserId: 'auth-chenhao', username: 'chenhao', name: '陈浩', email: 'chenhao@gongxue.cn', phone: '138****6600', role: '超级管理员', permissions: ['*'], status: '正常', lastActive: '刚刚' }, + { id: 'STAFF-02', authUserId: 'auth-zhoumin', username: 'zhoumin', name: '周敏', email: 'zhoumin@gongxue.cn', phone: '139****2188', role: '题库运营', permissions: ['platform:question_bank:read'], status: '正常', lastActive: '8分钟前' }, + { id: 'STAFF-03', authUserId: 'auth-liujia', username: 'liujia', name: '刘佳', email: 'liujia@gongxue.cn', phone: '136****9028', role: '平台财务', permissions: ['platform:billing:read', 'platform:billing:write', 'platform:billing:payment', 'platform:usage:read', 'platform:usage:write'], status: '正常', lastActive: '21分钟前' }, + { id: 'STAFF-04', authUserId: 'auth-wanglei', username: 'wanglei', name: '王磊', email: 'wanglei@gongxue.cn', phone: '135****2086', role: '租户运营', permissions: ['platform:tenant:read', 'platform:tenant:write', 'platform:tenant:status'], status: '正常', lastActive: '1小时前' }, + { id: 'STAFF-05', authUserId: 'auth-sunchen', username: 'sunchen', name: '孙晨', email: 'sunchen@gongxue.cn', phone: '137****1118', role: '安全审计', permissions: ['platform:audit:read', 'platform:audit:alert'], status: '已停用', lastActive: '3天前' }, + ], + auditLogs: [ + { id: 'AUD-20260723-094218', time: '2026-07-23 09:42:18 CST', actor: '陈浩', role: '平台管理员', action: 'tenant.status.update', targetType: 'tenant', targetId: 'tenant_mingtu_hn', target: '明途教育 · 河南', severity: '高', result: '成功', ip: '172.16.4.28', details: '将业务状态由正常更新为暂停,原因:账单逾期。' }, + { id: 'AUD-20260723-093602', time: '2026-07-23 09:36:02 CST', actor: 'system-billing', role: '系统任务', action: 'invoice.batch.create', targetType: 'invoice_batch', targetId: 'TASK-260723-001', target: '2026年7月账单', severity: '中', result: '成功', ip: 'internal', details: '生成 48 张账单,跳过 2 条重复候选。' }, + { id: 'AUD-20260723-085844', time: '2026-07-23 08:58:44 CST', actor: '周敏', role: '题库运营', action: 'bank.grant.update', targetType: 'grant', targetId: 'GRANT-001', target: '高数公共题库 v2026.7', severity: '中', result: '成功', ip: '172.16.3.16', details: '授权范围更新为 2 家指定租户。' }, + { id: 'AUD-20260723-084127', time: '2026-07-23 08:41:27 CST', actor: '刘佳', role: '平台财务', action: 'invoice.payment.confirm', targetType: 'invoice', targetId: 'INV-202607-0016', target: 'INV-202607-0016', severity: '高', result: '成功', ip: '172.16.8.12', details: '人工确认收款 ¥16,800,流水号已脱敏。' }, + { id: 'AUD-20260723-081209', time: '2026-07-23 08:12:09 CST', actor: 'unknown', role: '-', action: 'auth.login.failed', targetType: 'application', targetId: 'platform-console', target: 'platform-console', severity: '严重', result: '失败', ip: '45.62.18.9', details: '5 分钟内连续登录失败 12 次。' }, + { id: 'AUD-20260720-163510', time: '2026-07-20 16:35:10 CST', actor: '周敏', role: '题库运营', action: 'bank.catalog.read', targetType: 'question_bank', targetId: 'QB-SD', target: '山东专升本全科', severity: '中', result: '成功', ip: '172.16.3.16', details: '查看公共题库版本与租户采用状态。' }, + ], + alerts: [ + { id: 'ALT-20260723-008', title: '异常登录尝试', summary: '5 分钟内 12 次平台登录失败', severity: '严重', status: 'open', owner: '安全审计', targetType: 'application', targetId: 'platform-console', evidence: ['来源 IP 45.62.18.9', '账号枚举行为 12 次', '命中规则 LOGIN_FAILURE_BURST'], updatedAt: '2分钟前', note: '' }, + { id: 'ALT-20260723-006', title: '租户账单逾期', summary: '明途教育服务费账单逾期 5 天', severity: '高', status: 'acknowledged', owner: '平台财务', targetType: 'invoice', targetId: 'INV-202607-0017', evidence: ['余额 ¥8,600', '到期日 2026-07-18', '已发送 3 次催缴'], updatedAt: '18分钟前', note: '财务已联系租户。' }, + { id: 'ALT-20260723-004', title: '公共题库同步失败', summary: '河南租户同步发生 12 个内容冲突', severity: '中', status: 'open', owner: '题库运营', targetType: 'question_bank', targetId: 'QB-EN', evidence: ['当前版本 v2026.3', '目标版本 v2026.6', '冲突 12 条'], updatedAt: '42分钟前', note: '' }, + { id: 'ALT-20260722-019', title: '存储用量告警', summary: '启航专升本存储用量达到套餐 92%', severity: '中', status: 'open', owner: '租户运营', targetType: 'tenant', targetId: 'tenant_qihang_gd', evidence: ['当前 460 GB', '套餐上限 500 GB'], updatedAt: '昨天', note: '' }, + { id: 'ALT-20260722-011', title: 'Provider 密钥即将过期', summary: '优才专转本支付 Provider 密钥 7 天后到期', severity: '高', status: 'resolved', owner: '平台运维', targetType: 'tenant', targetId: 'tenant_youcai_js', evidence: ['密钥只写不可回显', '新密钥已于昨天更新'], updatedAt: '昨天', note: '完成轮换。' }, + ], + alertRules: [ + { id: 'RULE-LOGIN-BURST', code: 'LOGIN_FAILURE_BURST', name: '平台登录失败突增', severity: '严重', enabled: true, window: '5 分钟', threshold: '≥ 10 次', target: '平台账号与来源 IP' }, + { id: 'RULE-INVOICE-OVERDUE', code: 'INVOICE_OVERDUE', name: '服务费账单逾期', severity: '高', enabled: true, window: '每日 09:00', threshold: '逾期 ≥ 1 天', target: '未结清平台账单' }, + { id: 'RULE-BANK-SYNC', code: 'QUESTION_BANK_SYNC_FAILED', name: '公共题库同步失败', severity: '中', enabled: true, window: '任务结束时', threshold: '失败或冲突 ≥ 10', target: '租户题库同步任务' }, + ], + auditChannels: [ + { id: 'CH-SEC-MAIL', code: 'security_email', name: '平台安全邮箱', provider: '邮件', endpoint: 'security@gongxue.cn', minSeverity: '中', statusFilter: 'open,acknowledged', timeout: 8, maxAttempts: 3, enabled: true, lastStatus: '正常' }, + { id: 'CH-SEC-WX', code: 'security_wecom', name: '财务告警群', provider: 'Webhook', endpoint: 'Webhook · 企业微信', minSeverity: '高', statusFilter: 'open', timeout: 5, maxAttempts: 5, enabled: true, lastStatus: '正常' }, + { id: 'CH-DUTY-SMS', code: 'duty_sms', name: '值班手机', provider: '短信', endpoint: '138****6008', minSeverity: '严重', statusFilter: 'open', timeout: 10, maxAttempts: 3, enabled: true, lastStatus: '失败1次' }, + ], + notificationEvents: [ + { id: 'EVT-260723-001', channelId: 'CH-DUTY-SMS', subject: '异常登录尝试', status: '失败', attempts: 2, updatedAt: '2026-07-23 09:44 CST', error: '运营商返回频率限制' }, + { id: 'EVT-260723-002', channelId: 'CH-SEC-MAIL', subject: '异常登录尝试', status: '成功', attempts: 1, updatedAt: '2026-07-23 09:43 CST', error: '' }, + ], + dunningEvents: [], + tasks: [], +}; + +function clone(value) { return JSON.parse(JSON.stringify(value)); } +function normalizeData(saved) { + const normalized = clone(seed); + if (!saved || typeof saved !== 'object') return normalized; + Object.keys(normalized).forEach(key => { + if (key === 'version') return; + if (Array.isArray(normalized[key])) { + if (Array.isArray(saved[key])) normalized[key] = saved[key]; + return; + } + if (normalized[key] && typeof normalized[key] === 'object') { + if (saved[key] && typeof saved[key] === 'object' && !Array.isArray(saved[key])) normalized[key] = { ...normalized[key], ...saved[key] }; + return; + } + if (saved[key] !== undefined) normalized[key] = saved[key]; + }); + normalized.usage = normalized.usage.map(item => ({ overage: [], estimated: 0, source: 'Worker 自动采集', ...item })); + normalized.grants = normalized.grants.map(item => ({ allowedPlanCodes: [], allowedTenantIds: [], allowedRegionIds: [], allowedSubjectIds: [], localEdit: false, ...item })); + normalized.staff = normalized.staff.map(item => ({ permissions: [], status: '正常', lastActive: '尚未登录', ...item })); + normalized.alerts = normalized.alerts.map(item => ({ evidence: [], note: '', status: 'open', ...item })); + normalized.tasks = normalized.tasks.map(item => ({ status: 'failed', error: '', resultTarget: '任务中心', updatedAt: item.createdAt || todayTime(), ...item })); + normalized.version = seed.version; + return normalized; +} +function loadData() { + try { + const saved = JSON.parse(localStorage.getItem(STORAGE_KEY)); + if (saved?.version === seed.version) { + const normalized = normalizeData(saved); + normalized.tasks.forEach(task => { + if (['pending', 'processing'].includes(task.status)) { + task.status = 'failed'; + task.error = '页面重载中断了本次 Mock 执行,可从任务中心重试。'; + task.updatedAt = todayTime(); + } + }); + return normalized; + } + } catch (error) { + console.warn('Mock state restore failed', error); + } + return clone(seed); +} +let data = loadData(); +if (platformApi?.isEnabled()) { + [ + 'tenants', 'plans', 'subscriptions', 'invoices', 'usage', 'reminders', + 'dunningChannels', 'dunningEvents', 'banks', 'grants', 'bankSync', 'staff', + 'auditLogs', 'alerts', 'alertRules', 'auditChannels', 'notificationEvents', 'tasks', + ].forEach(key => { data[key] = []; }); + data.billingProfiles = {}; +} +const state = { + view: 'overview', previousView: '', selectedTenant: data.tenants[0]?.id || '', tenantFilter: 'all', tenantPlan: 'all', tenantSearch: '', globalSearch: '', + billingTab: 'invoices', bankTab: 'catalog', bankIncludeTenant: false, selectedBank: data.banks[0]?.id || '', + invoiceSearch: '', invoiceStatus: 'all', bankSearch: '', staffSearch: '', usagePeriod: '2026-07', auditRange: '24h', auditSeverity: 'all', auditSearch: '', alertStatus: 'all', selectedAlert: '', selectedInvoice: '', + role: 'super', scenario: 'normal', response: 'success', modal: '', modalData: {}, submitting: '', reading: '', contractResults: {}, history: [], + apiConnection: platformApi?.isEnabled() ? 'connecting' : 'mock', apiLoadedAt: '', apiLoadedOperations: 0, apiFailedOperations: 0, +}; +let pendingLeave = null; +let historySequence = Number(history.state?.sequence) || 0; +let restoringHistory = false; +let bypassHistoryGuard = false; +const dirtyScopes = new Set(); +const loadedTenantDetails = new Set(); + +let storageWarningShown = false; +function saveData() { + if (platformApi?.isEnabled()) return true; + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); + return true; + } catch (error) { + console.warn('Mock state persistence failed', error); + if (!storageWarningShown) { + storageWarningShown = true; + showToast('浏览器存储不可用,本次修改仅在当前页面保留', 'error'); + } + return false; + } +} +function backendStatusLabel() { + if (!platformApi?.isEnabled()) return '静态演示数据'; + if (state.apiConnection === 'connecting') return '正在连接后端'; + if (state.apiConnection === 'connected') { + const partial = state.apiFailedOperations ? ` · ${state.apiFailedOperations} 项无权限/失败` : ''; + return `后端已连接${partial}`; + } + return '后端不可用 · 已保留静态演示'; +} +function updateBackendStatus() { + const status = document.querySelector('.top-status > span'); + if (!status) return; + status.innerHTML = `${escapeHtml(backendStatusLabel())}`; + status.title = platformApi?.isEnabled() + ? `${platformApi.publicConfig().apiBaseUrl || '未配置 API 地址'}${state.apiLoadedAt ? ` · ${state.apiLoadedAt}` : ''}` + : 'runtime-config.js 当前为 mock 模式'; + status.classList.toggle('is-degraded', state.apiConnection === 'degraded'); +} +function applyBackendPatch(patch) { + Object.entries(patch).forEach(([key, value]) => { data[key] = value; }); + if (patch.tenants) { + const tenantIds = new Set(data.tenants.map(tenant => tenant.id)); + if (!tenantIds.has(state.selectedTenant)) state.selectedTenant = data.tenants[0]?.id || ''; + } + if (patch.banks && !data.banks.some(bank => bank.id === state.selectedBank)) state.selectedBank = data.banks[0]?.id || ''; + if (patch.usage) { + const latestUsage = new Map(); + data.usage.forEach(item => { + const current = latestUsage.get(item.tenantId); + if (!current || item.period > current.period) latestUsage.set(item.tenantId, item); + }); + data.tenants.forEach(tenant => { + const usage = latestUsage.get(tenant.id); + if (!usage) return; + tenant.students = usage.students; + tenant.questions = usage.questions; + tenant.storageGB = usage.storageGB; + tenant.trafficTB = usage.trafficTB; + }); + } + if (patch.plans || patch.tenants) { + data.plans.forEach(plan => { + plan.tenantCount = data.tenants.filter(tenant => tenant.planCode === plan.code).length; + }); + } +} +async function hydrateFromBackend({ silent = false } = {}) { + if (!platformApi?.isEnabled()) { + state.apiConnection = 'mock'; + updateBackendStatus(); + return false; + } + state.apiConnection = 'connecting'; + updateBackendStatus(); + try { + const result = await platformApi.bootstrap({ + includeTenantBanks: state.bankIncludeTenant, + period: state.usagePeriod, + }); + const successful = Object.keys(result.payloads).length; + if (!successful) throw Object.values(result.errors)[0] || new Error('没有平台接口成功返回'); + applyBackendPatch(platformApi.adaptBootstrap(result.payloads, data)); + state.apiConnection = 'connected'; + state.apiLoadedAt = todayTime(); + state.apiLoadedOperations = successful; + state.apiFailedOperations = Object.keys(result.errors).length; + render(); + if (!silent) showToast(`后端数据已刷新 · ${successful}/${result.requested} 项成功`); + return true; + } catch (error) { + state.apiConnection = 'degraded'; + state.apiFailedOperations = 1; + render(); + if (!silent) showToast(`${error.message || '后端连接失败'} · 已保留静态演示`, 'error'); + return false; + } +} +async function hydrateTenantDetail(tenantId, { force = false } = {}) { + if (!platformApi?.isEnabled() || !tenantId || (!force && loadedTenantDetails.has(tenantId))) return; + try { + const detail = await platformApi.tenantDetail(tenantId); + const tenant = tenantById(tenantId); + if (tenant && detail.tenant) { + tenant.legal = detail.tenant.legalName || tenant.legal; + tenant.brand = detail.tenant.brandName || tenant.brand; + tenant.shortName = detail.tenant.shortName || tenant.shortName; + tenant.createdAt = detail.tenant.createdAt ? String(detail.tenant.createdAt).slice(0, 10) : tenant.createdAt; + tenant.host = detail.domains.find(domain => domain.isPrimary)?.host || detail.domains[0]?.host || tenant.host; + } + data.subscriptions = [ + ...data.subscriptions.filter(item => item.tenantId !== tenantId), + ...detail.subscriptions, + ]; + if (detail.billingProfile) data.billingProfiles[tenantId] = detail.billingProfile; + loadedTenantDetails.add(tenantId); + render(); + } catch (error) { + showToast(`${error.message || '租户详情加载失败'}${error.requestId ? ` · ${error.requestId}` : ''}`, 'error'); + } +} +function icon(name) { return ``; } +function refreshIcons() { if (window.lucide) window.lucide.createIcons({ attrs: { 'stroke-width': 1.8 } }); } +function formatNumber(value) { return Number(value || 0).toLocaleString('zh-CN'); } +function money(value) { return `¥${Number(value || 0).toLocaleString('zh-CN')}`; } +function todayTime() { return new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', dateStyle: 'short', timeStyle: 'medium' }).format(new Date()).replaceAll('/', '-'); } +function requestId() { return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`; } +function entityId(prefix) { return `${prefix}-${Date.now().toString(36).toUpperCase()}`; } +function tenantById(id) { return data.tenants.find(item => item.id === id); } +function planByCode(code) { return data.plans.find(item => item.code === code); } +function invoiceById(id) { return data.invoices.find(item => item.id === id); } +function bankById(id) { return data.banks.find(item => item.id === id); } +function notificationChannelName(id) { + return [...data.dunningChannels, ...data.auditChannels].find(channel => channel.id === id)?.name || id; +} +function setAccessibleControlName(element, label) { + if (!element || !label) return; + element.setAttribute('aria-label', label); + element.setAttribute('title', label); +} +function recordLabel(name, id) { return [name, id].filter(Boolean).join(' · '); } +function rowActionAccessibleName(control) { + const tenantId = control.dataset.tenantDetail || control.dataset.tenantId; + const tenant = tenantById(tenantId); + if (control.dataset.tenantDetail) return `查看租户 ${recordLabel(tenant?.name, tenantId)} 详情`; + if (control.dataset.invoiceDetail) { + const invoice = invoiceById(control.dataset.invoiceDetail); + return `查看账单 ${recordLabel(invoice?.id, tenantById(invoice?.tenantId)?.name)} 详情`; + } + if (control.dataset.notificationDetail) { + const event = [...(data.notificationEvents || []), ...(data.dunningEvents || [])].find(item => item.id === control.dataset.notificationDetail); + return `查看通知事件 ${recordLabel(event?.subject, event?.id || control.dataset.notificationDetail)} 详情`; + } + if (control.dataset.auditDetail) { + const log = data.auditLogs.find(item => item.id === control.dataset.auditDetail); + return `查看审计日志 ${recordLabel(log?.id || control.dataset.auditDetail, log?.target)} 详情`; + } + const action = control.dataset.action; + if (action === 'prepare-tenant-status') return `${tenant?.status === '暂停' ? '恢复' : '暂停'}租户 ${recordLabel(tenant?.name, tenantId)}`; + if (action === 'tenant-to-billing') return `查看租户 ${recordLabel(tenant?.name, tenantId)} 账务`; + if (action === 'confirm-payment') { + const invoice = invoiceById(control.dataset.invoiceId); + return `确认账单 ${recordLabel(invoice?.id || control.dataset.invoiceId, tenantById(invoice?.tenantId)?.name)} 收款`; + } + if (action === 'edit-dunning-channel') { + const channel = data.dunningChannels.find(item => item.id === control.dataset.channelId); + return `编辑催缴渠道 ${recordLabel(channel?.name, channel?.id || control.dataset.channelId)}`; + } + if (action === 'edit-grant') { + const grant = data.grants.find(item => item.id === control.dataset.grantId); + return `编辑公共题库授权 ${recordLabel(bankById(grant?.bankId)?.name, grant?.id || control.dataset.grantId)}`; + } + if (action === 'sync-detail') { + const [syncTenantId, bankId] = String(control.dataset.syncKey || '').split(':'); + return `查看 ${recordLabel(tenantById(syncTenantId)?.name, syncTenantId)} 与 ${recordLabel(bankById(bankId)?.name, bankId)} 的同步详情`; + } + if (action === 'edit-staff') { + const person = data.staff.find(item => item.id === control.dataset.staffId); + return `编辑员工 ${recordLabel(person?.name, person?.id || control.dataset.staffId)}`; + } + if (action === 'edit-audit-channel') { + const channel = data.auditChannels.find(item => item.id === control.dataset.channelId); + return `编辑审计通知渠道 ${recordLabel(channel?.name, channel?.id || control.dataset.channelId)}`; + } + return control.title || control.textContent.trim(); +} +function switchAccessibleName(control) { + const enabled = control.classList.contains('is-on'); + const action = control.dataset.action; + if (action === 'toggle-auto-renew') { + const subscription = data.subscriptions.find(item => item.id === control.dataset.subscriptionId); + const tenant = tenantById(subscription?.tenantId); + return `${enabled ? '关闭' : '开启'}${tenant?.name || '该租户'}订阅 ${subscription?.id || control.dataset.subscriptionId} 自动续费`; + } + if (action === 'toggle-dunning-channel') { + const channel = data.dunningChannels.find(item => item.id === control.dataset.channelId); + return `${enabled ? '停用' : '启用'}催缴渠道 ${recordLabel(channel?.name, channel?.id || control.dataset.channelId)}`; + } + if (action === 'toggle-grant') { + const grant = data.grants.find(item => item.id === control.dataset.grantId); + return `${enabled ? '禁用' : '启用'}公共题库授权 ${recordLabel(bankById(grant?.bankId)?.name, grant?.id || control.dataset.grantId)}`; + } + if (action === 'toggle-staff') { + const person = data.staff.find(item => item.id === control.dataset.staffId); + return `${enabled ? '停用员工' : '启用员工'} ${recordLabel(person?.name, person?.id || control.dataset.staffId)}${enabled ? '并撤销会话' : ''}`; + } + if (action === 'toggle-audit-channel') { + const channel = data.auditChannels.find(item => item.id === control.dataset.channelId); + return `${enabled ? '停用' : '启用'}审计通知渠道 ${recordLabel(channel?.name, channel?.id || control.dataset.channelId)}`; + } + return enabled ? '关闭此设置' : '开启此设置'; +} +function updateMenuButtonAccessibility() { + const expanded = app.classList.contains('menu-open'); + setAccessibleControlName(menuButton, expanded ? '关闭平台导航菜单' : '打开平台导航菜单'); + menuButton.setAttribute('aria-expanded', String(expanded)); + menuButton.setAttribute('aria-controls', 'sideNav'); +} +function decorateAccessibleControls(root) { + const selectLabels = { + tenantPlanFilter: '租户套餐筛选', + invoiceStatusFilter: '账单状态筛选', + roleSelect: '演示角色', + scenarioSelect: '页面状态演练', + responseSelect: '下一次 Mock 写请求结果', + }; + Object.entries(selectLabels).forEach(([id, label]) => setAccessibleControlName(document.querySelector(`#${id}`), label)); + root.querySelectorAll('.row-action').forEach(control => setAccessibleControlName(control, rowActionAccessibleName(control))); + root.querySelectorAll('.switch').forEach(control => { + setAccessibleControlName(control, switchAccessibleName(control)); + control.setAttribute('role', 'switch'); + control.setAttribute('aria-checked', String(control.classList.contains('is-on'))); + }); + document.querySelectorAll('.icon-button').forEach(control => { + const fallback = control.title || control.getAttribute('aria-label'); + if (fallback) setAccessibleControlName(control, fallback); + }); + updateMenuButtonAccessibility(); +} +function hasPermission(permission) { + if (platformApi?.isEnabled() && data.apiPermissions?.effective && Object.prototype.hasOwnProperty.call(data.apiPermissions.effective, permission)) { + return data.apiPermissions.effective[permission] === true; + } + const grants = roleDefinitions[state.role]?.permissions || []; + if (grants.includes('*') || grants.includes(permission)) return true; + return grants.some(grant => grant.endsWith('*') && permission.startsWith(grant.slice(0, -1))); +} +function tone(text) { + const value = String(text || ''); + if (/正常|活跃|已支付|resolved|已解决|已授权|成功|已完成|启用|已送达/.test(value)) return 'success'; + if (/逾期|暂停|严重|失败|阻断|已停用|同步失败/.test(value)) return 'danger'; + if (/待|试用|警告|即将|acknowledged|已确认|同步中|执行中|开放|open/.test(value)) return 'warning'; + return 'neutral'; +} +function alertStatusLabel(status) { return ({ open: '开放', acknowledged: '已确认', resolved: '已解决', ignored: '已忽略' })[status] || status; } +function taskStatusLabel(status) { return ({ pending: '待处理', processing: '执行中', completed: '已完成', failed: '失败' })[status] || status; } +function escapeHtml(value) { return String(value ?? '').replace(/[&<>'"]/g, char => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' })[char]); } +function fieldValue(form, name) { return String(new FormData(form).get(name) || '').trim(); } +function parseMoney(value) { return Number(String(value || '').replaceAll(',', '')); } +function addAudit(action, targetType, targetId, target, severity, details, result = '成功') { + data.auditLogs.unshift({ id: entityId('AUD'), time: `${todayTime()} CST`, actor: roleDefinitions[state.role].label, role: roleDefinitions[state.role].label, action, targetType, targetId, target, severity, result, ip: '127.0.0.1', details }); + saveData(); +} +function addTask(label, resultTarget, execute) { + const task = { id: entityId('TASK'), label, resultTarget, status: 'pending', createdAt: todayTime(), updatedAt: todayTime(), error: '' }; + data.tasks.unshift(task); saveData(); updateCounters(); + setTimeout(() => { task.status = 'processing'; task.updatedAt = todayTime(); saveData(); updateCounters(); renderOverlay(); }, 350); + setTimeout(() => { + try { execute?.(task); task.status = 'completed'; } + catch (error) { task.status = 'failed'; task.error = error.message || '任务执行失败'; } + task.updatedAt = todayTime(); saveData(); updateCounters(); render(); renderOverlay(); + }, 1200); + return task; +} +function contractResult(key) { + const result = state.contractResults[key]; + if (!result) return ''; + const toneClass = result.status === 'error' ? 'is-error' : result.status === 'loading' ? 'is-loading' : 'is-success'; + const glyph = result.status === 'error' ? 'triangle-alert' : result.status === 'loading' ? 'loader-circle' : 'circle-check'; + return `
${icon(glyph)}${escapeHtml(result.title)}${escapeHtml(result.detail)} · Request ID:${escapeHtml(result.requestId)}
`; +} +function runContractRead({ key, actionId, operationId, title, apply, suppliedRequestId = '', liveRequest }) { + if (state.reading) return; + const id = suppliedRequestId || requestId(); + state.reading = key; + state.contractResults[key] = { status: 'loading', title: `${title}处理中`, detail: operationId, requestId: id }; + render(); + if (platformApi?.isEnabled()) { + Promise.resolve(typeof liveRequest === 'function' ? liveRequest() : platformApi.request(operationId)) + .then(payload => { + state.reading = ''; + const requestIdValue = payload?.meta?.requestId || id; + const detail = apply?.(payload) || '查询结果已刷新'; + state.contractResults[key] = { status: 'success', title: `${title}完成`, detail, requestId: requestIdValue }; + if (actionId) window.recordPlatformSpecResult?.(actionId, { result: 'success', operationId, requestId: requestIdValue }); + render(); + }) + .catch(error => { + state.reading = ''; + const requestIdValue = error.requestId || id; + state.contractResults[key] = { status: 'error', title: `${title}失败`, detail: error.message || '平台接口请求失败', requestId: requestIdValue }; + if (actionId) window.recordPlatformSpecResult?.(actionId, { result: 'failed', operationId, requestId: requestIdValue }); + render(); + }); + return; + } + const response = nextResponse(); + setTimeout(() => { + state.reading = ''; + if (response !== 'success') { + const detail = response === 'validation_error' ? '服务端拒绝查询参数,已保留当前筛选' : 'Mock 查询失败,已保留当前筛选'; + state.contractResults[key] = { status: 'error', title: `${title}失败`, detail, requestId: id }; + addAudit(`${key}.failed`, 'mock_request', id, operationId, response === 'validation_error' ? '中' : '高', `${detail}。Request ID:${id}。`, '失败'); + if (actionId) window.recordPlatformSpecResult?.(actionId, { result: 'failed', operationId, requestId: id }); + render(); + return; + } + const detail = apply?.() || '查询结果已刷新'; + state.contractResults[key] = { status: 'success', title: `${title}完成`, detail, requestId: id }; + if (actionId) window.recordPlatformSpecResult?.(actionId, { result: 'success', operationId, requestId: id }); + render(); + }, 520); +} +function addContractTask({ actionId, operationId, label, resultTarget, execute, suppliedRequestId = '' }) { + const response = nextResponse(); + const id = suppliedRequestId || requestId(); + const task = addTask(label, resultTarget, taskItem => { + if (response !== 'success') { + const message = `${response === 'validation_error' ? '服务端字段校验失败' : 'Mock 异步请求失败'} · Request ID:${id}`; + addAudit(`${actionId}.failed`, 'mock_request', id, operationId, '高', message, '失败'); + window.recordPlatformSpecResult?.(actionId, { result: 'failed', operationId, requestId: id, taskId: taskItem.id }); + throw new Error(message); + } + execute?.(taskItem, id); + window.recordPlatformSpecResult?.(actionId, { result: 'success', operationId, requestId: id, taskId: taskItem.id }); + }); + task.requestId = id; + task.operationId = operationId; + saveData(); + return task; +} +function canWrite(permission) { return hasPermission(permission) && state.role !== 'readonly'; } +function isCompactAdmin() { return window.matchMedia('(max-width: 767px)').matches; } +function requireDesktop(actionLabel) { + if (!isCompactAdmin()) return false; + openModal('mobileBoundary', { actionLabel }); + return true; +} +function closeMobileMenu() { + app.classList.remove('menu-open'); + document.body.classList.remove('menu-locked'); + updateMenuButtonAccessibility(); +} +function toggleMobileMenu() { + const opening = !app.classList.contains('menu-open'); + app.classList.toggle('menu-open', opening); + document.body.classList.toggle('menu-locked', opening); + updateMenuButtonAccessibility(); +} + +function setupPrototypeControls() { + roleSelect.innerHTML = Object.entries(roleDefinitions).map(([value, role]) => ``).join(''); + scenarioSelect.innerHTML = [['normal', '正常'], ['loading', '加载中'], ['empty', '空状态'], ['partial_error', '局部失败'], ['error', '加载失败'], ['forbidden', '403 无权限'], ['session_invalid', '401 会话失效'], ['submitting', '提交中'], ['success', '成功结果']].map(([value, label]) => ``).join(''); + responseSelect.innerHTML = [['success', '下一次成功'], ['server_error', '下一次服务端失败'], ['validation_error', '下一次字段失败']].map(([value, label]) => ``).join(''); + roleSelect.value = state.role; scenarioSelect.value = state.scenario; responseSelect.value = state.response; +} +function renderNav() { + const activeNav = ['tenantDetail', 'billingProfile'].includes(state.view) ? 'tenants' : state.view; + sideNav.innerHTML = navGroups.map(([group, items]) => { + const visible = items.filter(([id]) => + hasPermission(pageMeta[id].permission) && (!platformApi?.isEnabled() || API_CONNECTED_PAGES.has(id))); + if (!visible.length) return ''; + return `

${group}

${visible.map(([id, glyph, label]) => ``).join('')}
`; + }).join(''); + const staffShortcut = document.querySelector('.platform-foot>button'); + if (staffShortcut) staffShortcut.hidden = !hasPermission('platform:staff:read'); + const accountRole = document.querySelector('.platform-foot small'); + if (accountRole) accountRole.textContent = roleDefinitions[state.role]?.label || '平台管理员'; + const authenticatedUser = window.GongxuePlatformAuth?.getUser?.(); + const accountName = document.querySelector('.platform-foot strong'); + const accountAvatar = document.querySelector('.platform-foot>div>span'); + const displayName = authenticatedUser?.name || authenticatedUser?.email; + if (accountName && displayName) accountName.textContent = displayName; + if (accountAvatar && displayName) accountAvatar.textContent = displayName.slice(0, 1).toUpperCase(); +} +function updateCounters() { + const activeTasks = data.tasks.filter(task => ['pending', 'processing'].includes(task.status)).length; + const openAlerts = data.alerts.filter(alert => alert.status === 'open').length; + taskCount.textContent = activeTasks; + taskCount.hidden = activeTasks === 0; + alertCount.textContent = openAlerts; + alertCount.hidden = openAlerts === 0; +} +function pageHead(title, description, actions = '', options = {}) { + const level = options.level || pageMeta[state.view]?.level || 1; + const nav = level > 1 ? `
` : ''; + return `
${nav}${pageMeta[state.view]?.id || ''}${level > 1 ? ` · L${level}` : ''}

${title}

${description}

${actions}
`; +} +function metric(label, value, note, glyph, style = 'blue') { return `
${icon(glyph)}
${label}${value}

${note}

`; } +function statusBadge(value) { return `${escapeHtml(value)}`; } +function emptyBlock(title, description, action = '') { return `
${icon('inbox')}

${title}

${description}

${action}
`; } +function requestErrorBlock(partial = false) { + const id = requestId(); + return `
${icon(partial ? 'cloud-off' : 'triangle-alert')}

${partial ? '部分数据暂时不可用' : '页面加载失败'}

${partial ? '主列表保留成功数据,右侧聚合区加载失败。' : 'Mock 服务返回错误,未泄露内部堆栈。'} Request ID:${id}

`; +} +function renderScenario(content) { + if (state.scenario === 'session_invalid') return `
${pageHead('会话已失效', '平台会话已过期或被管理员撤销。')}
${icon('log-in')}

401 · 请重新登录

当前 Mock 已清理会话上下文,不展示任何平台业务数据。重新登录后返回安全首页。

`; + if (!hasPermission(pageMeta[state.view]?.permission) || state.scenario === 'forbidden') return `
${pageHead('没有访问权限', '当前演示角色无权访问此平台功能。')}
${icon('shield-x')}

403 · 权限不足

需要权限 ${pageMeta[state.view]?.permission}。菜单会按权限隐藏,深链进入则显示此页面。

`; + if (state.scenario === 'loading') return `
${pageHead(pageMeta[state.view].title, '正在从 apps/api Mock 加载数据。')}
${Array.from({ length: 6 }, (_, index) => ``).join('')}

超过 8 秒时可使用重试入口。

`; + if (state.scenario === 'submitting') return `
${pageHead(pageMeta[state.view].title, '正在提交 Mock 写请求。')}
${icon('loader-circle')}

正在提交

按钮已防重复提交,输入内容会保留;完成后将显示结果摘要与唯一结果编号。

`; + if (state.scenario === 'success') return `
${pageHead(pageMeta[state.view].title, '本次 Mock 操作已完成。')}
${icon('circle-check')}

操作成功

结果编号:RESULT-DEMO-20260723 · 已写入审计,可返回当前工作页继续处理。

`; + if (state.scenario === 'error') return `
${pageHead(pageMeta[state.view].title, '数据加载未完成。')}${requestErrorBlock()}
`; + if (state.scenario === 'empty') return `
${pageHead(pageMeta[state.view].title, '当前演示数据集为空。')}${emptyBlock('当前页面没有数据', '这是只存在于 Mock 原型的空状态演练。恢复“正常”即可继续测试真实交互。', '')}
`; + if (state.scenario === 'partial_error') { + const closingIndex = content.lastIndexOf(''); + return closingIndex >= 0 ? `${content.slice(0, closingIndex)}${requestErrorBlock(true)}${content.slice(closingIndex)}` : `${content}${requestErrorBlock(true)}`; + } + return content; +} + +function renderOverview() { + if (platformApi?.isEnabled()) return renderApiOverview(); + const active = data.tenants.filter(tenant => tenant.status === '正常').length; + const overdue = data.invoices.filter(invoice => invoice.status === '已逾期').reduce((sum, invoice) => sum + invoice.balance, 0); + const openAlerts = data.alerts.filter(alert => alert.status === 'open').slice(0, 4); + const totalStudents = data.tenants.reduce((sum, tenant) => sum + tenant.students, 0); + const totalQuestions = data.banks.reduce((sum, bank) => sum + bank.questions, 0); + return `
${pageHead('平台经营工作台', '查看租户规模、订阅账务、平台用量与安全态势。数据更新时间:2026-07-23 10:10 CST。', ``)} +
${metric('有效租户', active, `共 ${data.tenants.length} 家`, 'building-2')}${metric('活跃订阅', data.subscriptions.filter(item => item.status === '活跃').length, '未来30天到期 2 条', 'badge-dollar-sign', 'green')}${metric('逾期应收', money(overdue), `涉及 ${data.invoices.filter(item => item.status === '已逾期').length} 家租户`, 'receipt-text', 'orange')}${metric('平台学生规模', formatNumber(totalStudents), 'Mock 最近30日活跃 82%', 'users', 'purple')}${metric('公共题库题目', formatNumber(totalQuestions), `${data.banks.length} 个公共来源库`, 'database-zap', 'cyan')}
+

平台收入与租户增长

最近 12 个月 · 不含租户学生端交易流水
MRR有效租户
¥500k¥350k¥200k0
${[42, 48, 52, 58, 64, 71, 78, 82, 88, 94, 101, 108].map((value, index) => `${index + 1}月`).join('')}
+

需要关注

按业务影响排序
${openAlerts.map(alert => ``).join('') || emptyBlock('没有开放告警', '平台当前没有需要处理的开放告警。')}
+

租户状态

可进入租户工作列表处理
${data.tenants.length}全部租户
${[['正常', data.tenants.filter(t => t.status === '正常').length, 68], ['试用', data.tenants.filter(t => t.status === '试用').length, 16], ['待配置', data.tenants.filter(t => t.status === '待配置').length, 16], ['暂停/逾期', data.tenants.filter(t => t.status === '暂停' || t.billing === '逾期').length, 16]].map(([label, count, percent]) => `

${label}${count}

`).join('')}
+

账务结论

先行动,再看明细
待付款${money(data.invoices.filter(i => i.status === '待付款').reduce((s, i) => s + i.balance, 0))}可发送到期提醒
已逾期${money(overdue)}进入催缴处理
预估超额${money(data.usage.reduce((s, u) => s + u.estimated, 0))}需确认生成
+

最近平台事件

跨租户关键变更
${data.auditLogs.slice(0, 3).map(log => `
${escapeHtml(log.actor.slice(0, 1))}

${escapeHtml(log.actor)} ${escapeHtml(log.details)}${escapeHtml(log.time)} · ${escapeHtml(log.ip)}

`).join('')}
`; +} + +function renderApiOverview() { + const overview = data.apiOverview || {}; + const openAlerts = data.alerts.filter(alert => alert.status === 'open').slice(0, 4); + return `
${pageHead('平台经营工作台', `真实 PostgreSQL 数据 · ${state.apiLoadedAt || '正在加载'}`, ``)} +
${metric('全部租户', overview.tenantCount ?? data.tenants.length, `活跃 ${overview.activeTenantCount ?? 0} 家`, 'building-2')}${metric('暂停租户', overview.suspendedTenantCount ?? 0, '来自真实租户状态', 'pause-circle', 'orange')}${metric('已支付订单', overview.paidOrderCount ?? 0, `全部订单 ${overview.orderCount ?? 0}`, 'receipt-text', 'green')}${metric('平台收入', money(Number(overview.revenueCents || 0) / 100), '按已支付订单统计', 'circle-dollar-sign', 'cyan')}${metric('题库 / 题目', `${overview.questionBankCount ?? 0} / ${formatNumber(overview.questionCount ?? 0)}`, `学习活跃用户 ${overview.learningActiveUserCount ?? 0}`, 'database-zap', 'purple')}
+

租户状态

来自 /api/platform-admin/tenants
${data.tenants.length}全部租户
${[['正常', data.tenants.filter(item => item.status === '正常').length, 100], ['待配置', data.tenants.filter(item => item.status === '待配置').length, 100], ['暂停', data.tenants.filter(item => item.status === '暂停').length, 100]].map(([label, count, percent]) => `

${label}${count}

`).join('')}
+

开放告警

真实审计告警
${openAlerts.map(alert => ``).join('') || emptyBlock('没有开放告警', '当前数据库没有需要处理的开放告警。')}
+

最近平台事件

来自真实审计日志
${data.auditLogs.slice(0, 6).map(log => `
${escapeHtml(log.actor.slice(0, 1))}

${escapeHtml(log.actor)} ${escapeHtml(log.details)}${escapeHtml(log.time)} · ${escapeHtml(log.ip)}

`).join('') || '

暂无审计事件

'}
`; +} + +function renderTenants() { + let rows = data.tenants.filter(tenant => state.tenantFilter === 'all' || tenant.status === state.tenantFilter || tenant.billing === state.tenantFilter); + if (state.tenantPlan !== 'all') rows = rows.filter(tenant => tenant.planCode === state.tenantPlan); + const query = state.tenantSearch.trim().toLowerCase(); + if (query) rows = rows.filter(tenant => [tenant.name, tenant.slug, tenant.legal, tenant.host].some(value => value.toLowerCase().includes(query))); + if (state.scenario === 'empty') rows = []; + return `
${pageHead('租户列表与创建', '搜索租户并管理主体、域名、套餐、业务状态与账务状态。', `${platformApi?.isEnabled() ? '' : ``}${canWrite('platform:tenant:write') ? `` : ''}`)} +
${['all', '正常', '试用', '暂停', '待配置', '逾期'].map(value => ``).join('')}
+ ${rows.length ? `
${rows.map(tenant => ``).join('')}
租户套餐模式业务状态账务状态学生 / 题目订阅到期待付余额
${escapeHtml(tenant.plan)}${escapeHtml(tenant.mode)}${statusBadge(tenant.status)}${statusBadge(tenant.billing)}${formatNumber(tenant.students)}${formatNumber(tenant.questions)} 题${tenant.expires || '-'}${money(tenant.balance)}
${canWrite('platform:tenant:status') ? `` : ''}
共 ${rows.length} 家匹配租户筛选、搜索和返回上下文会保留
` : emptyBlock(query || state.tenantFilter !== 'all' || state.tenantPlan !== 'all' ? '没有匹配的租户' : '还没有租户', query || state.tenantFilter !== 'all' || state.tenantPlan !== 'all' ? '调整搜索词或筛选条件后重试。' : '创建第一个租户及首期订阅基础。', canWrite('platform:tenant:write') ? `` : '')}
`; +} + +function renderTenantDetail() { + const tenant = tenantById(state.selectedTenant) || data.tenants[0]; + if (!tenant) return `
${pageHead('租户详情与生命周期', '租户不存在或已被移除。', '', { level: 2 })}${emptyBlock('租户不存在', '返回租户列表选择其他租户。', '')}
`; + const subscription = data.subscriptions.find(item => item.tenantId === tenant.id); + const invoices = data.invoices.filter(item => item.tenantId === tenant.id); + const usage = data.usage.find(item => item.tenantId === tenant.id); + return `
${pageHead(tenant.name, `${tenant.slug} · ${tenant.legal}`, `${canWrite('platform:tenant:status') ? `` : ''}`, { level: 2 })} +
业务状态${statusBadge(tenant.status)}
账务状态${statusBadge(tenant.billing)}
当前套餐${escapeHtml(tenant.plan)}
订阅到期${tenant.expires || '-'}
待付余额${money(tenant.balance)}
+

主体、负责人和域名

平台只展示链接,不冒充租户登录
主体名称
${escapeHtml(tenant.legal)}
租户 ID
${tenant.id}
负责人
${escapeHtml(tenant.owner)} · ${escapeHtml(tenant.ownerPhone)}
开通时间
${escapeHtml(tenant.createdAt)} CST
学生端域名
${escapeHtml(tenant.host)} ${statusBadge('已验证')}
租户后台域名
admin.${escapeHtml(tenant.host.replace(/^study\.|^learn\.|^app\.|^tiku\./, ''))} ${statusBadge('已验证')}
+

关联工作区

详情承接上下文,编辑成功回详情
+

生命周期轨迹

状态变更写入平台审计

租户创建${tenant.createdAt} CST · 主体与初始模式已保存

域名验证${tenant.host} · HTTPS 正常

订阅生效${subscription ? `${subscription.start} 至 ${subscription.expires}` : '待创建订阅'}

账务状态${tenant.billing}${tenant.balance ? ` · 待付 ${money(tenant.balance)}` : ''}

`; +} + +function renderBillingProfile() { + const tenant = tenantById(state.selectedTenant) || data.tenants[0]; + const profile = data.billingProfiles[tenant.id] || {}; + const readOnly = !canWrite('platform:tenant:billing_profile'); + return `
${pageHead('租户账务资料', `${tenant.name} · 银行账号仅展示脱敏值,密钥和完整账号不得回显。`, ``, { level: 3 })} +

开票与联系人

${readOnly ? '当前角色仅可查看' : '必填字段离开时校验,提交时再次校验'}
+

发票与银行信息

平台只接受脱敏账户尾号
+
所有时间以 Asia/Shanghai 展示 · 保存将写入审计日志${!readOnly ? `` : ''}
`; +} + +function renderSubscriptions() { + const selectedTenant = state.selectedTenant ? tenantById(state.selectedTenant) : null; + const rows = state.scenario === 'empty' ? [] : data.subscriptions; + return `
${pageHead('SaaS 套餐与订阅', '维护套餐能力边界,并为租户创建或续订订阅。当前 API 只提供套餐查询,因此不提供套餐编辑。', `${canWrite('platform:billing:write') ? `` : ''}`)} +
${data.plans.map((plan, index) => `
${plan.name}

${plan.description}

${plan.tenantCount} 家租户
${money(plan.amount)}/年
    ${plan.quotas.map(item => `
  • ${icon('check')}${item}
  • `).join('')}
`).join('')}
+ ${rows.length ? `

活跃与试用订阅

${rows.length} 条${selectedTenant ? ` · 已定位 ${selectedTenant.name}` : ''}
${rows.map(subscription => { const tenant = tenantById(subscription.tenantId); return ``; }).join('')}
租户订阅号套餐周期开始时间到期时间自动续费状态
${escapeHtml(tenant?.name || '-')}${escapeHtml(tenant?.slug || '-')}${subscription.id}${planByCode(subscription.planCode)?.name || subscription.planCode}${subscription.cycle}${subscription.start}${subscription.expires}${statusBadge(subscription.status)}
` : emptyBlock('还没有订阅', '创建租户订阅后将在此显示。', canWrite('platform:billing:write') ? '' : '')}
`; +} + +function renderBilling() { + const query = state.invoiceSearch.trim().toLowerCase(); + const rows = state.scenario === 'empty' ? [] : data.invoices.filter(invoice => (!state.selectedTenant || invoice.tenantId === state.selectedTenant) && (state.invoiceStatus === 'all' || invoice.status === state.invoiceStatus) && (!query || [invoice.id, invoice.type, invoice.period, invoice.note, tenantById(invoice.tenantId)?.name].some(value => String(value || '').toLowerCase().includes(query)))); + const unpaid = data.invoices.filter(invoice => invoice.status === '待付款').reduce((sum, invoice) => sum + invoice.balance, 0); + const overdue = data.invoices.filter(invoice => invoice.status === '已逾期').reduce((sum, invoice) => sum + invoice.balance, 0); + return `
${pageHead('服务费账单', '查询、手工创建、预览订阅账单候选并通过 dry-run 确认批量生成。', `${canWrite('platform:billing:write') ? `` : ''}`)} +
${metric('本月应收', money(data.invoices.reduce((s, i) => s + i.total, 0)), `${data.invoices.length} 张账单`, 'receipt-text')}${metric('已收款', money(data.invoices.reduce((s, i) => s + i.paid, 0)), '人工收款需二次确认', 'circle-dollar-sign', 'green')}${metric('待付款', money(unpaid), '可发送到期提醒', 'clock-3', 'orange')}${metric('已逾期', money(overdue), `${data.invoices.filter(i => i.status === '已逾期').length} 家租户`, 'triangle-alert', 'red')}
+
${contractResult('invoice-query')} + + ${rows.length ? `
${rows.map(invoice => { const tenant = tenantById(invoice.tenantId); return ``; }).join('')}
账单号租户类型 / 账期总额已付 / 余额到期日状态
${invoice.type}${invoice.period}${money(invoice.total)}${money(invoice.paid)}余额 ${money(invoice.balance)}${invoice.due}${statusBadge(invoice.status)}
${invoice.balance && canWrite('platform:billing:payment') ? `` : ''}
` : emptyBlock('没有匹配账单', query || state.invoiceStatus !== 'all' ? '调整账单查询条件后重试。' : state.selectedTenant ? '当前租户没有匹配账单,可清除租户上下文查看全部。' : '账单候选生成后会在这里显示。', state.selectedTenant ? '' : '')}
`; +} + +function renderUsage() { + const periodRows = data.usage.filter(item => item.period === state.usagePeriod); + const rows = state.scenario === 'empty' ? [] : periodRows.filter(item => !state.selectedTenant || item.tenantId === state.selectedTenant); + const periodLabel = state.usagePeriod.replace('-', '年') + '月'; + return `
${pageHead('用量与超额计费', '按账期查看学生席位、题目、存储与流量用量;区分 Worker 自动采集和人工补录。', `${canWrite('platform:usage:write') ? `` : ''}`)} +
${periodLabel}学生席位${formatNumber(periodRows.reduce((s, i) => s + i.students, 0))}合同总量 176,000
题目数量${formatNumber(periodRows.reduce((s, i) => s + i.questions, 0))}合同总量 495,000
对象存储${formatNumber(periodRows.reduce((s, i) => s + i.storageGB, 0))} GB合同总量 13.5 TB
预估超额费${money(periodRows.reduce((s, i) => s + i.estimated, 0))}涉及 ${periodRows.filter(i => i.estimated).length} 家租户
+ ${rows.length ? `
${rows.map(item => { const tenant = tenantById(item.tenantId); const plan = planByCode(tenant?.planCode); const seatQuota = plan?.code === 'enterprise' ? 20000 : plan?.code === 'pro' ? 8000 : 2000; return ``; }).join('')}
租户来源 / 周期学生席位题目数量存储 / 流量超额项预估费用
${escapeHtml(tenant?.name || '-')}${escapeHtml(tenant?.plan || '-')}${item.source}${item.period}
${formatNumber(item.students)} / ${formatNumber(seatQuota)}
${formatNumber(item.questions)}
${item.storageGB} GB${item.trafficTB} TB 流量${statusBadge(item.overage.length ? item.overage.join(' + ') : '无')}${money(item.estimated)}
` : emptyBlock('本账期没有用量记录', '可以手工补录,或等待 Worker 自动采集。', canWrite('platform:usage:write') ? '' : '')}
`; +} + +function renderDunning() { + const overdueInvoices = data.invoices.filter(invoice => invoice.status === '已逾期' || invoice.status === '待付款'); + return `
${pageHead('收款、逾期与催缴', '人工确认收款、dry-run 预览逾期处理、生成催缴并跟踪通知事件。', `${canWrite('platform:billing:dunning') ? `` : ''}${canWrite('platform:billing:notification') ? `` : ''}`)} + + ${state.billingTab === 'collections' ? `

待收与逾期队列

${overdueInvoices.length} 张账单
${overdueInvoices.map(invoice => { const tenant = tenantById(invoice.tenantId); return `
${icon('building-2')}
${escapeHtml(tenant?.name || '-')}${invoice.id} · 到期 ${invoice.due} · ${invoice.status}
${money(invoice.balance)}
`; }).join('') || emptyBlock('没有待处理账单', '已无待收或逾期服务费账单。')}

全局催缴策略

可由平台任务生成提醒

到期前 7 天邮件 + 租户后台通知

到期当天短信 + 邮件 + 租户后台通知

逾期 3 天升级财务负责人并限制部分写操作

逾期 7 天暂停租户业务,保留只读访问

` : state.billingTab === 'reminders' ? `
${data.reminders.map(item => ``).join('')}
提醒号租户 / 账单级别渠道消息时间状态
${item.id}${escapeHtml(tenantById(item.tenantId)?.name || '-')}${item.invoiceId}L${item.level}${item.channel}${escapeHtml(item.message)}${item.date} CST${statusBadge(item.status)}
` : state.billingTab === 'channels' ? `
${data.dunningChannels.map(channel => ``).join('')}
渠道代码Provider端点重试状态
${escapeHtml(channel.name)}${channel.code}${channel.provider}${escapeHtml(channel.endpoint)}${channel.retry} 次${statusBadge(channel.enabled ? '启用' : '已停用')}
` : `
${(data.dunningEvents || []).map(event => ``).join('')}
事件 ID主题渠道尝试更新时间结果错误
${event.id}${escapeHtml(event.subject)}${escapeHtml(notificationChannelName(event.channelId))}${event.attempts}${event.updatedAt}${statusBadge(event.status)}${escapeHtml(event.error || '-')}
`}
`; +} + +function renderBank() { + const selected = bankById(state.selectedBank) || data.banks[0]; + const query = state.bankSearch.trim().toLowerCase(); + const bankRows = data.banks.filter(bank => !query || [bank.name, bank.catalog, bank.source, bank.version].some(value => String(value).toLowerCase().includes(query))); + return `
${pageHead('公共题库目录与授权', '维护公共题库目录、授权规则与租户采用同步状态。授权禁用不会静默删除租户快照。', `${canWrite('platform:question_bank:grant') ? `` : ''}`)} +
${contractResult('bank-query')}${contractResult('bank-sync')} + + ${state.bankTab === 'catalog' ? `
${escapeHtml(selected.catalog)}

${escapeHtml(selected.name)}

当前版本 ${selected.version} · 来源 ${selected.source}

题目${formatNumber(selected.questions)}
授权规则${data.grants.filter(grant => grant.bankId === selected.id).length}
采用租户${data.bankSync.filter(sync => sync.bankId === selected.id).length}
当前状态${selected.status}
${selected.status}

${selected.version}

公共题库内容由平台内容生产链路维护,本页只负责授权和采用状态。

更新时间 2026-07-23 09:00 CST
` : state.bankTab === 'grants' ? `
${data.grants.filter(grant => !query || [grant.id, bankById(grant.bankId)?.name, grant.scope, ...grant.allowedRegionIds, ...grant.allowedSubjectIds].some(value => String(value || '').toLowerCase().includes(query))).map(grant => ``).join('')}
规则题库授权范围套餐 / 租户地区 / 科目有效期本地改编状态
${grant.id}${escapeHtml(bankById(grant.bankId)?.name || '-')}${grant.scope}${grant.allowedPlanCodes.length ? grant.allowedPlanCodes.map(code => planByCode(code)?.name || code).join('、') : grant.allowedTenantIds.map(id => tenantById(id)?.name || id).join('、') || '-'}${[...grant.allowedRegionIds, ...grant.allowedSubjectIds].join(' / ') || '不限'}${grant.startsAt} 至 ${grant.expiresAt || '长期'}${grant.localEdit ? '允许' : '只读采用'}${statusBadge(grant.status)}
` : `

租户采用与同步状态

源版本、任务状态与冲突均来自专属查询
${data.bankSync.filter(sync => !query || [tenantById(sync.tenantId)?.name, bankById(sync.bankId)?.name, sync.current, sync.target, sync.status].some(value => String(value || '').toLowerCase().includes(query))).map(sync => ``).join('')}
租户题库当前版本目标版本同步进度冲突状态
${escapeHtml(tenantById(sync.tenantId)?.name || '-')}${escapeHtml(bankById(sync.bankId)?.name || '-')}${sync.current}${sync.target}
${sync.progress}%
${sync.conflicts}${statusBadge(sync.status)}
`}
`; +} + +function renderStaff() { + const query = state.staffSearch.trim().toLowerCase(); + const staffRows = data.staff.filter(person => !query || [person.name, person.username, person.email, person.phone, person.role, person.status].some(value => String(value || '').toLowerCase().includes(query))); + return `
${pageHead('平台员工与权限', '平台权限独立于租户权限;禁用员工可同步撤销会话,且不能禁用最后一名超级管理员。', `${canWrite('platform:staff:write') ? `` : ''}`)} +
+

平台员工

${staffRows.length} 名匹配 · ${data.staff.filter(item => item.status === '正常').length} 名活跃
${staffRows.map(person => `
${escapeHtml(person.name.slice(0, 1))}
${escapeHtml(person.name)}${escapeHtml(person.email)} · ${escapeHtml(person.phone)}
${escapeHtml(person.role)}${person.permissions.includes('*') ? '全部权限' : `${person.permissions.length} 项权限`}${person.status}
`).join('') || emptyBlock('没有匹配员工', '调整姓名、账号、邮箱、角色或状态关键词。')}

权限目录

遵循最小权限;当前演示角色可在顶部切换
${Object.entries(roleDefinitions).filter(([key]) => key !== 'readonly').map(([key, role]) => `
${icon('shield-check')}
${role.label}${role.permissions.includes('*') ? '全部平台权限' : role.permissions.join(' · ')}
${data.staff.filter(person => person.role === role.label).length}人
`).join('')}
`; +} + +function renderAudit() { + let rows = data.auditLogs.filter(log => state.auditSeverity === 'all' || log.severity === state.auditSeverity); + if (state.auditRange === '24h') rows = rows.filter(log => log.time.startsWith('2026-07-23')); + const query = state.auditSearch.toLowerCase(); + if (query) rows = rows.filter(log => [log.id, log.actor, log.action, log.target, log.ip].some(value => value.toLowerCase().includes(query))); + if (state.scenario === 'empty') rows = []; + const relatedAlertCount = data.alerts.filter(alert => rows.some(log => log.targetId === alert.targetId || log.targetId === alert.id)).length; + return `
${pageHead('平台审计日志', '检索跨租户敏感操作、账务变更与权限事件;导出任务提供文件、hash 和行数。', ``)} +
${['all', '严重', '高', '中'].map(value => ``).join('')}
+ ${rows.length ? `
${rows.map(log => ``).join('')}
时间操作者动作目标级别结果来源 IP
${escapeHtml(log.time)}${escapeHtml(log.actor)}${escapeHtml(log.role)}${escapeHtml(log.action)}${escapeHtml(log.target)}${log.severity}${statusBadge(log.result)}${escapeHtml(log.ip)}
` : emptyBlock('没有审计日志', '调整时间范围、严重级别或搜索条件。')}
`; +} + +function renderAlerts() { + let alerts = state.alertStatus === 'all' ? data.alerts : data.alerts.filter(alert => alert.status === state.alertStatus); + if (state.scenario === 'empty') alerts = []; + return `
${pageHead('审计告警与通知', '按 open → acknowledged → resolved/ignored 流转;状态接口禁止重新打开。规则当前只读。', `${canWrite('platform:audit:notification') ? `` : ''}`)} +

告警规则

后端只读 · ${data.alertRules.length} 条启用规则
GET /api/platform-admin/audit-alert-rules
${data.alertRules.map(rule => `
${rule.severity}
${escapeHtml(rule.name)}${escapeHtml(rule.code)} · ${escapeHtml(rule.window)} · ${escapeHtml(rule.threshold)}

${escapeHtml(rule.target)}

${statusBadge(rule.enabled ? '启用' : '已停用')}
`).join('')}
+

告警队列

开放 ${data.alerts.filter(a => a.status === 'open').length} · 已确认 ${data.alerts.filter(a => a.status === 'acknowledged').length} · 已结束 ${data.alerts.filter(a => ['resolved', 'ignored'].includes(a.status)).length}
${[['all', '全部'], ['open', '开放'], ['acknowledged', '已确认'], ['resolved', '已解决'], ['ignored', '已忽略']].map(([value, label]) => ``).join('')}
${alerts.length ? alerts.map(alert => `
${alert.severity}
${alert.id} · ${alert.owner}${escapeHtml(alert.title)}

${escapeHtml(alert.summary)}

${alert.updatedAt}
${alertStatusLabel(alert.status)}
${alert.status === 'open' && canWrite('platform:audit:alert') ? `` : ''}${['open', 'acknowledged'].includes(alert.status) && canWrite('platform:audit:alert') ? `` : ''}
`).join('') : emptyBlock('当前筛选没有告警', '告警规则仍保持只读,可切换筛选查看历史状态。')}
+
`; +} + +const templates = { overview: renderOverview, tenants: renderTenants, tenantDetail: renderTenantDetail, billingProfile: renderBillingProfile, subscriptions: renderSubscriptions, billing: renderBilling, usage: renderUsage, dunning: renderDunning, bank: renderBank, staff: renderStaff, audit: renderAudit, alerts: renderAlerts }; + +function render() { + renderNav(); + const meta = pageMeta[state.view] || pageMeta.overview; + pageTitle.textContent = `${meta.id} · ${meta.title}`; + mainView.innerHTML = renderScenario((templates[state.view] || renderOverview)()); + closeMobileMenu(); + roleSelect.value = state.role; scenarioSelect.value = state.scenario; responseSelect.value = state.response; + updateCounters(); refreshIcons(); + window.decoratePlatformSpec?.(mainView, meta.id); + decorateAccessibleControls(mainView); + updateBackendStatus(); +} +function renderOverlay() { + if (!state.modal) { overlayRoot.innerHTML = ''; return; } + const renderer = modalTemplates[state.modal]; + overlayRoot.innerHTML = renderer ? renderer() : ''; + refreshIcons(); + window.decoratePlatformSpec?.(overlayRoot, pageMeta[state.view]?.id || ''); + decorateAccessibleControls(overlayRoot); + requestAnimationFrame(() => { + const modal = overlayRoot.querySelector('.modal'); + modal?.querySelector('input:not([type="hidden"]),select,textarea,button')?.focus(); + }); +} +let toastTimer; +let lastOverlayTrigger = null; +function showToast(message, kind = 'success') { + toast.querySelector('span').textContent = message; + toast.classList.toggle('is-error', kind === 'error'); + toast.classList.add('is-visible'); + clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove('is-visible'), 2800); +} +function openModal(name, modalData = {}) { + const activeElement = document.activeElement; + if (!state.modal && activeElement instanceof HTMLElement && activeElement !== document.body && !overlayRoot.contains(activeElement)) lastOverlayTrigger = activeElement; + state.modal = name; + state.modalData = { ...modalData, dirty: false }; + renderOverlay(); +} +function markDirty(scope) { dirtyScopes.add(scope); if (scope === 'modal') state.modalData.dirty = true; } +function clearDirty(scope = '') { if (scope) dirtyScopes.delete(scope); else dirtyScopes.clear(); if (!scope || scope === 'modal') state.modalData.dirty = false; } +function hasUnsavedChanges(scope = '') { return scope ? dirtyScopes.has(scope) || (scope === 'modal' && state.modalData.dirty) : dirtyScopes.size > 0 || Boolean(state.modalData.dirty); } +function renderLeaveGuard() { + if (!pendingLeave) return; + let root = document.querySelector('#unsavedLeaveGuard'); + if (!root) { root = document.createElement('div'); root.id = 'unsavedLeaveGuard'; document.body.append(root); } + root.className = 'unsaved-leave-guard'; + root.innerHTML = ``; + refreshIcons(); requestAnimationFrame(() => root.querySelector('[data-leave-choice="stay"]')?.focus()); +} +function closeLeaveGuard() { document.querySelector('#unsavedLeaveGuard')?.remove(); } +function requestLeave(execute, { scope = '' } = {}) { + if (!hasUnsavedChanges(scope)) { execute(); return true; } + pendingLeave = { execute, scope }; renderLeaveGuard(); return false; +} +function closeModal(options = {}) { + const close = () => { + clearDirty('modal'); + const trigger = lastOverlayTrigger; + state.modal = ''; + state.modalData = {}; + lastOverlayTrigger = null; + renderOverlay(); + if (trigger?.isConnected) requestAnimationFrame(() => trigger.focus()); + }; + if (state.modal && !options.force && hasUnsavedChanges('modal')) return requestLeave(close, { scope: 'modal' }); + close(); + return true; +} +function navigationSnapshot() { + return { view: state.view, selectedTenant: state.selectedTenant, selectedInvoice: state.selectedInvoice, selectedAlert: state.selectedAlert, tenantFilter: state.tenantFilter, tenantPlan: state.tenantPlan, tenantSearch: state.tenantSearch, invoiceSearch: state.invoiceSearch, invoiceStatus: state.invoiceStatus, billingTab: state.billingTab, bankTab: state.bankTab, bankIncludeTenant: state.bankIncludeTenant, bankSearch: state.bankSearch, selectedBank: state.selectedBank, staffSearch: state.staffSearch, usagePeriod: state.usagePeriod, auditRange: state.auditRange, auditSeverity: state.auditSeverity, auditSearch: state.auditSearch, alertStatus: state.alertStatus, scrollY: window.scrollY }; +} +function syncNavigationState() { if (history.state?.[HISTORY_APP_KEY]) history.replaceState({ ...history.state, snapshot: navigationSnapshot() }, '', location.href); } +function navigate(view, options = {}) { + if (!pageMeta[view]) return; + if (platformApi?.isEnabled() && !API_CONNECTED_PAGES.has(view)) { + showToast('该模块尚未接入真实后端,本阶段不会回退到 Mock 数据', 'error'); + return false; + } + const perform = () => { + clearDirty(); + if (!hasPermission(pageMeta[view].permission)) { state.previousView = state.view; state.view = view; state.scenario = 'forbidden'; render(); return; } + if (options.push !== false) { + const nextEntry = navigationSnapshot(); + const last = state.history[state.history.length - 1]; + if (!last || last.view !== nextEntry.view || last.selectedTenant !== nextEntry.selectedTenant) state.history = [...state.history.slice(-2), nextEntry]; + if (options.browser !== false && history.state?.[HISTORY_APP_KEY]) history.replaceState({ ...history.state, snapshot: nextEntry }, '', location.href); + } + state.previousView = state.view; state.view = view; state.scenario = 'normal'; closeModal({ force: true }); render(); scrollTo(0, 0); + if (options.browser !== false) { historySequence += 1; history.pushState({ [HISTORY_APP_KEY]: true, sequence: historySequence, snapshot: navigationSnapshot() }, '', location.href); } + if (['tenantDetail', 'billingProfile'].includes(view)) hydrateTenantDetail(state.selectedTenant); + }; + if (!options.force && hasUnsavedChanges()) return requestLeave(perform); + perform(); return true; +} +function navigateBack() { + requestLeave(() => { + clearDirty(); + if (history.state?.[HISTORY_APP_KEY] && history.length > 1) { history.back(); return; } + const prior = state.history.pop(); + if (!prior) { state.view = 'overview'; state.previousView = ''; render(); scrollTo(0, 0); return; } + Object.assign(state, prior); render(); requestAnimationFrame(() => scrollTo(0, prior.scrollY || 0)); + }); +} +function closeSubpage() { + requestLeave(() => { clearDirty(); state.history = []; state.view = 'overview'; state.previousView = ''; state.scenario = 'normal'; render(); scrollTo(0, 0); history.replaceState({ [HISTORY_APP_KEY]: true, sequence: historySequence, snapshot: navigationSnapshot() }, '', location.href); }); +} +window.addEventListener('popstate', event => { + const entry = event.state; + if (!entry?.[HISTORY_APP_KEY] || !entry.snapshot) return; + const targetSequence = Number(entry.sequence) || 0; + if (restoringHistory) { restoringHistory = false; return; } + if (hasUnsavedChanges() && !bypassHistoryGuard) { + const delta = targetSequence - historySequence; + restoringHistory = true; history.go(-delta); + requestLeave(() => { bypassHistoryGuard = true; history.go(delta); }); + return; + } + bypassHistoryGuard = false; historySequence = targetSequence; clearDirty(); + const currentView = state.view; + Object.assign(state, entry.snapshot, { previousView: currentView, scenario: 'normal', modal: '', modalData: {} }); + lastOverlayTrigger = null; + if (state.history.length && state.history[state.history.length - 1].view === state.view) state.history.pop(); + render(); + requestAnimationFrame(() => scrollTo(0, entry.snapshot.scrollY || 0)); + if (entry.rootGuard) { historySequence += 1; history.pushState({ [HISTORY_APP_KEY]: true, sequence: historySequence, snapshot: navigationSnapshot() }, '', location.href); } +}); +function nextResponse() { + const response = state.response; + if (response !== 'success') { state.response = 'success'; responseSelect.value = 'success'; } + return response; +} +async function simulateWrite({ key, permission, validate, apply, success, close = true, rerender = true, operationId = '', body, request: executeRequest }) { + if (!canWrite(permission)) { showToast(`缺少写权限:${permission}`, 'error'); return; } + const validation = validate?.(); + if (validation) { showFormErrors(validation); return; } + const mockResponse = platformApi?.isEnabled() ? 'success' : nextResponse(); + state.submitting = key; + const activeForm = overlayRoot.querySelector('form') || mainView.querySelector('form'); + const submitButton = activeForm && (activeForm.querySelector('[type="submit"]') || document.querySelector(`[type="submit"][form="${activeForm.id}"]`)); + const submitMarkup = submitButton?.innerHTML; + if (submitButton) { submitButton.disabled = true; submitButton.innerHTML = `${icon('loader-circle')}提交中`; refreshIcons(); } + const finish = async () => { + if (mockResponse !== 'success') { + state.submitting = ''; + const id = requestId(); + addAudit(`${key}.failed`, 'mock_request', id, key, mockResponse === 'validation_error' ? '中' : '高', `${mockResponse === 'validation_error' ? '服务端字段校验失败' : 'Mock 写请求失败'},Request ID:${id}。`, '失败'); + showToast(`${mockResponse === 'validation_error' ? '服务端字段校验失败' : '提交失败'} · ${id}`, 'error'); + if (mockResponse === 'validation_error') showFormErrors({ server: `服务端拒绝本次输入(${id})` }); + if (submitButton) { submitButton.disabled = false; submitButton.innerHTML = submitMarkup; refreshIcons(); } + return; + } + try { + let payload = null; + if (platformApi?.isEnabled()) { + if (typeof executeRequest === 'function') payload = await executeRequest(); + else if (operationId) { + const input = typeof body === 'function' ? body() : (body || {}); + payload = await platformApi.request(operationId, { body: platformApi.buildBody(operationId, input) }); + } else { + throw new Error(`操作 ${key} 尚未配置后端 operationId`); + } + } else { + await new Promise(resolve => setTimeout(resolve, 650)); + } + state.submitting = ''; + clearDirty(activeForm?.closest('#overlayRoot') ? 'modal' : 'page'); + const result = apply(payload); saveData(); + if (close) closeModal({ force: true }); + if (rerender) render(); + showToast(typeof success === 'function' ? success(result) : success); + if (platformApi?.isEnabled()) hydrateFromBackend({ silent: true }); + } catch (error) { + const id = error.requestId || requestId(); + state.submitting = ''; + showToast(`${error.message || '提交失败'} · ${id}`, 'error'); + if (error.status === 400) showFormErrors({ server: `${error.message || '服务端字段校验失败'}(${id})` }); + if (submitButton) { submitButton.disabled = false; submitButton.innerHTML = submitMarkup; refreshIcons(); } + } + }; + finish(); +} +async function executeBackendMutation(operationId, input, apply, { refresh = true } = {}) { + if (!platformApi?.isEnabled()) { + apply?.(null); + return true; + } + try { + const payload = await platformApi.request(operationId, { + body: platformApi.buildBody(operationId, typeof input === 'function' ? input() : input), + }); + apply?.(payload); + if (refresh) hydrateFromBackend({ silent: true }); + return true; + } catch (error) { + showToast(`${error.message || '提交失败'}${error.requestId ? ` · ${error.requestId}` : ''}`, 'error'); + return false; + } +} +function showFormErrors(errors) { + const form = overlayRoot.querySelector('form') || mainView.querySelector('form'); + if (!form) { showToast(Object.values(errors)[0], 'error'); return; } + form.querySelectorAll('.field-error').forEach(node => node.remove()); + form.querySelectorAll('.is-invalid').forEach(node => node.classList.remove('is-invalid')); + const summary = form.querySelector('.form-error-summary'); + if (summary) { summary.hidden = false; summary.innerHTML = `请修正以下问题
    ${Object.values(errors).map(message => `
  • ${escapeHtml(message)}
  • `).join('')}
`; summary.focus(); } + Object.entries(errors).forEach(([name, message]) => { + const input = form.elements[name]; + if (!input) return; + input.classList.add('is-invalid'); + input.insertAdjacentHTML('afterend', `${escapeHtml(message)}`); + }); + const first = form.querySelector('.is-invalid'); first?.focus(); +} + +function modalFrame(title, description, content, actions = '', options = {}) { + return ``; +} +function formField(label, name, value = '', attrs = '') { return ``; } +function tenantOptions(selected = '') { return data.tenants.map(tenant => ``).join(''); } +function planOptions(selected = '') { return data.plans.map(plan => ``).join(''); } +function notificationProviderOptions(selected = '') { + const normalized = ({ Webhook: 'generic', SMTP: 'generic', 邮件: 'generic', 短信: 'generic', 钉钉: 'dingtalk', 飞书: 'feishu', 企微: 'wecom' })[selected] || selected || 'generic'; + return [['generic', '通用 Webhook'], ['dingtalk', '钉钉'], ['feishu', '飞书'], ['wecom', '企微']] + .map(([value, label]) => ``) + .join(''); +} +function channelEndpointFormValue(channel) { + return platformApi?.isEnabled() && channel.endpointRedacted ? '' : channel.endpoint || ''; +} + +const modalTemplates = { + mobileBoundary() { + return modalFrame('请在桌面端继续', `${state.modalData.actionLabel || '此操作'}需要完整工作区。`, `
${icon('monitor-up')}
移动端只支持查看与轻量处理

租户、账务、用量、授权、员工和通知渠道的创建或复杂编辑,请在宽度不少于 768px 的桌面端完成,避免误操作。

`, ``, { id: '全局 · 移动端边界' }); + }, + createTenant() { + return modalFrame('创建租户', '创建主体、独立域名和首期订阅基础。slug 与域名必须唯一。', ``, ``, { id: 'P-02 · 创建向导', wide: true }); + }, + tenantStatus() { + const tenant = tenantById(state.modalData.tenantId); + const restoring = tenant.status === '暂停'; + return modalFrame(restoring ? '恢复租户' : '暂停租户', `将 ${tenant.name} 的业务状态变更为“${restoring ? '正常' : '暂停'}”。${restoring ? '恢复业务写操作。' : '学生端保留只读访问,新增权益与部分写操作将受限。'}`, ``, ``, { id: 'P-03 · 生命周期' }); + }, + createSubscription() { + const selectedPlan = state.modalData.planCode || tenantById(state.selectedTenant)?.planCode || 'pro'; + return modalFrame('创建租户订阅', '日期与金额必须有效;同一租户的重复活跃订阅会被阻止。', ``, ``, { id: 'P-05 · 订阅表单', wide: true }); + }, + createInvoice() { + return modalFrame('手工创建服务费账单', '金额以元展示,Mock 写入时显式转换;到期日必须晚于今天。', ``, ``, { id: 'P-06 · 手工账单', wide: true }); + }, + invoiceCandidates() { + const candidates = data.subscriptions.filter(subscription => !data.invoices.some(invoice => invoice.note.includes(subscription.id))).map(subscription => ({ subscription, tenant: tenantById(subscription.tenantId) })); + return modalFrame('订阅账单候选 · Dry-run', '预览不会写入账单;已有账单候选将被幂等跳过。', `
${candidates.map(({ subscription, tenant }) => ``).join('') || '

没有可生成候选,全部订阅已存在对应账单。

'}
`, `${candidates.length ? `` : ''}`, { id: 'P-06 · Dry-run', wide: true }); + }, + payment() { + const invoice = invoiceById(state.modalData.invoiceId); + return modalFrame('人工确认收款', `${invoice.id} · 当前余额 ${money(invoice.balance)}。金额不可超过余额,原始支付载荷不会回显。`, ``, ``, { id: 'P-08 · 资金动作', wide: true }); + }, + overduePreview() { + const candidates = data.invoices.filter(invoice => invoice.status === '已逾期'); + return modalFrame('逾期处理预览 · Dry-run', '本次预览不改变账单或租户状态;确认后异步创建催缴记录。', `
${candidates.map(invoice => ``).join('') || '

当前没有逾期候选。

'}
`, `${candidates.length ? '' : ''}`, { id: 'P-08 · Dry-run' }); + }, + recordUsage() { + return modalFrame('记录平台用量', '人工记录会标记来源;同租户同账期重复记录会被阻止。', ``, ``, { id: 'P-07 · 人工用量', wide: true }); + }, + overagePreview() { + const candidates = data.usage.filter(item => item.period === state.usagePeriod && item.estimated > 0 && !data.invoices.some(invoice => invoice.note === `超额 ${item.period}` && invoice.tenantId === item.tenantId)); + return modalFrame('超额账单候选 · Dry-run', `${state.usagePeriod} 账期预览;确认后通过异步任务生成账单。`, `
${candidates.map(item => ``).join('') || '

当前账期无超额候选。

'}
`, `${candidates.length ? '' : ''}`, { id: 'P-07 · Dry-run' }); + }, + dunningChannel() { + const channel = data.dunningChannels.find(item => item.id === state.modalData.channelId) || {}; + const redactedHint = platformApi?.isEnabled() && channel.endpointRedacted ? ' 后端不会回显完整 URL;编辑时必须重新输入完整 HTTPS Webhook。' : ''; + return modalFrame(channel.id ? '编辑催缴渠道' : '新建催缴渠道', `Webhook secret 为只写字段,保存后不会回显。${redactedHint}`, ``, ``, { id: 'P-08 · 通知渠道', wide: true }); + }, + grant() { + const grant = data.grants.find(item => item.id === state.modalData.grantId) || {}; + return modalFrame(grant.id ? '编辑公共题库授权' : '新建公共题库授权', '授权范围与允许列表会按后端 DTO 保存;地区和科目字段使用 UUID。', ``, ``, { id: 'P-09 · 授权规则', wide: true }); + }, + staff() { + const person = data.staff.find(item => item.id === state.modalData.staffId) || {}; + const presetRole = roleDefinitions[state.modalData.roleKey]?.label || ''; + const selectedRole = person.role || presetRole || '租户运营'; + const editablePhone = /^1\d{10}$/.test(person.phone || '') ? person.phone : ''; + const permissionList = ['platform:overview:read', 'platform:tenant:read', 'platform:tenant:write', 'platform:tenant:status', 'platform:tenant:billing_profile', 'platform:plan:read', 'platform:billing:read', 'platform:billing:write', 'platform:billing:payment', 'platform:billing:dunning', 'platform:billing:notification', 'platform:usage:read', 'platform:usage:write', 'platform:question_bank:read', 'platform:question_bank:grant', 'platform:question_bank:ops', 'platform:staff:read', 'platform:staff:write', 'platform:staff:status', 'platform:audit:read', 'platform:audit:export', 'platform:audit:alert', 'platform:audit:notification']; + const presetPermissions = roleDefinitions[state.modalData.roleKey]?.permissions || []; + return modalFrame(person.id ? '编辑平台员工' : '新建平台员工', 'Auth 用户必须存在;按最小权限分组授权。', ``, ``, { id: 'P-10 · 员工权限', wide: true }); + }, + auditDetail() { + const log = data.auditLogs.find(item => item.id === state.modalData.auditId); + return modalFrame('审计事件详情', `${log.id} · 敏感字段已脱敏。`, ``, ``, { id: 'P-11 · 只读详情', wide: true }); + }, + alertDetail() { + const alert = data.alerts.find(item => item.id === (state.modalData.alertId || state.selectedAlert)); + return modalFrame('告警证据与处理', `${alert.id} · ${alert.title}`, `
${alert.severity}
${escapeHtml(alert.summary)}${alert.owner} · ${alert.updatedAt}
${statusBadge(alertStatusLabel(alert.status))}
    ${alert.evidence.map(item => `
  • ${icon('file-search')}${escapeHtml(item)}
  • `).join('')}
`, `${['open', 'acknowledged'].includes(alert.status) ? `` : ''}`, { id: 'P-12 · 状态机', wide: true }); + }, + notificationDetail() { + const event = [...(data.notificationEvents || []), ...(data.dunningEvents || [])].find(item => item.id === state.modalData.eventId); + const channel = event ? notificationChannelName(event.channelId) : '-'; + return modalFrame('通知事件详情', `${event?.id || '-'} · 请求与错误摘要已脱敏。`, ``, `${event?.status === '失败' ? `` : ''}`, { id: 'P-12 · 通知事件', wide: true }); + }, + syncDetail() { + const [tenantId, bankId] = String(state.modalData.syncKey || '').split(':'); + const sync = data.bankSync.find(item => item.tenantId === tenantId && item.bankId === bankId); + return modalFrame('租户采用与同步详情', `${tenantById(tenantId)?.name || '-'} · ${bankById(bankId)?.name || '-'}`, ``, ``, { id: 'P-09 · 采用同步', wide: true }); + }, + auditChannel() { + const channel = data.auditChannels.find(item => item.id === state.modalData.channelId) || {}; + const redactedHint = platformApi?.isEnabled() && channel.endpointRedacted ? ' 后端不会回显完整 URL;编辑时必须重新输入完整 HTTPS Webhook。' : ''; + return modalFrame(channel.id ? '编辑审计通知渠道' : '新建审计通知渠道', `secret 只写;状态和严重级别过滤由平台后端执行。${redactedHint}`, ``, ``, { id: 'P-12 · 通知渠道', wide: true }); + }, + confirmAction() { + const { title, description, impact, confirmLabel = '确认操作', confirmActionName = 'execute-confirmed', tone: confirmTone = 'danger' } = state.modalData; + return modalFrame(title, description, `
${icon(confirmTone === 'danger' ? 'triangle-alert' : 'circle-help')}

${escapeHtml(impact)}

`, ``, { id: '全局 · 二次确认' }); + }, + tasks() { + return modalFrame('异步任务中心', '批量账单、超额账单和导出均返回 task_id,可跟踪、重试并跳转结果。', `
${data.tasks.length ? data.tasks.map(task => `
${icon(task.status === 'completed' ? 'circle-check' : task.status === 'failed' ? 'circle-x' : 'loader-circle')}
${escapeHtml(task.label)}${task.id} · ${task.updatedAt}

${task.error ? escapeHtml(task.error) : `结果去向:${escapeHtml(task.resultTarget)}`}

${task.status === 'failed' ? `` : statusBadge(taskStatusLabel(task.status))}
`).join('') : emptyBlock('没有异步任务', '批量生成、导出或重试后会出现在这里。')}
`, ``, { id: '全局 · 任务中心', wide: true }); + }, +}; + +function confirmAction(config, execute) { + openModal('confirmAction', config); + state.modalData.execute = execute; +} + +function validateBillingProfile(form) { + const errors = {}; + ['billingName', 'contactName', 'phone', 'email', 'invoiceTitle'].forEach(name => { if (!fieldValue(form, name)) errors[name] = `${form.elements[name].previousElementSibling?.textContent.replace('*', '').trim() || name}不能为空`; }); + const phone = fieldValue(form, 'phone'); if (phone && !/^1\d{10}$/.test(phone)) errors.phone = '请输入 11 位中国大陆手机号'; + const email = fieldValue(form, 'email'); if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) errors.email = '请输入有效邮箱地址'; + const taxId = fieldValue(form, 'taxId'); if (taxId && !/^[0-9A-Z]{15,20}$/.test(taxId)) errors.taxId = '税号应为 15-20 位大写字母或数字'; + const account = fieldValue(form, 'bankAccountMasked'); if (account && !account.includes('*')) errors.bankAccountMasked = '不得提交完整银行账号,请使用脱敏值'; + return Object.keys(errors).length ? errors : null; +} +function validateNotificationChannel(values) { + const errors = {}; + if (!values.code) errors.code = '渠道代码不能为空'; + if (!values.name) errors.name = '渠道名称不能为空'; + if (!values.endpoint) errors.endpoint = 'Webhook URL 不能为空'; + if (platformApi?.isEnabled() && values.endpoint) { + try { + const url = new URL(values.endpoint); + const local = ['localhost', '127.0.0.1', '::1'].includes(url.hostname); + if (url.protocol !== 'https:' && !local) errors.endpoint = '联调/生产 Webhook 必须使用 HTTPS'; + } catch { + errors.endpoint = '请输入完整有效的 Webhook URL'; + } + } + return Object.keys(errors).length ? errors : null; +} + +document.addEventListener('submit', event => { + event.preventDefault(); + const form = event.target; + const desktopOnlyForms = new Set(['createTenantForm', 'tenantStatusForm', 'billingProfileForm', 'subscriptionForm', 'invoiceForm', 'paymentForm', 'usageForm', 'dunningChannelForm', 'grantForm', 'staffForm', 'auditChannelForm']); + if (desktopOnlyForms.has(form.id) && requireDesktop('提交复杂配置或资金操作')) return; + if (form.id === 'createTenantForm') { + const slug = fieldValue(form, 'slug'); const host = fieldValue(form, 'primaryHost'); const name = fieldValue(form, 'name'); const amount = parseMoney(fieldValue(form, 'amount')); + simulateWrite({ key: 'create-tenant', permission: 'platform:tenant:write', operationId: 'PlatformAdminTenantsController_create', body: () => ({ ...Object.fromEntries(new FormData(form)), amount }), validate: () => { + const errors = {}; if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) errors.slug = 'slug 仅支持小写字母、数字和连字符'; + if (!name) errors.name = '租户名称不能为空'; if (!/^([a-z0-9-]+\.)+[a-z]{2,}$/i.test(host)) errors.primaryHost = '请输入有效域名'; + if (data.tenants.some(item => item.slug === slug)) errors.slug = 'slug 已存在'; if (data.tenants.some(item => item.host === host)) errors.primaryHost = '域名已被其他租户使用'; return Object.keys(errors).length ? errors : null; + }, apply: payload => { const planCode = fieldValue(form, 'planCode'); const plan = planByCode(planCode); const fallback = { id: `tenant_${slug.replaceAll('-', '_')}`, slug, name, legal: fieldValue(form, 'legalName') || name, brand: fieldValue(form, 'brandName') || name, shortName: fieldValue(form, 'shortName') || name, host, plan: plan.name, planCode, status: fieldValue(form, 'status'), billing: fieldValue(form, 'billingStatus') || (amount > 0 ? '待付款' : '正常'), mode: fieldValue(form, 'mode'), students: 0, questions: 0, storageGB: 0, trafficTB: 0, expires: '', balance: amount, owner: '待配置', ownerPhone: '-', createdAt: todayTime() }; const tenant = payload?.item ? platformApi.adapters.tenant({ ...payload.item, planCode, brandName: fallback.brand }, fallback, Object.fromEntries(data.plans.map(item => [item.code, item]))) : fallback; tenant.host = host; tenant.shortName = fallback.shortName; data.tenants.unshift(tenant); if (!platformApi?.isEnabled() && amount > 0) data.invoices.unshift({ id: entityId('INV'), tenantId: tenant.id, type: '首期服务费', period: '首期', total: amount, paid: 0, balance: amount, due: '2026-08-15', status: '待付款', note: '创建租户首期金额' }); state.selectedTenant = tenant.id; addAudit('tenant.create', 'tenant', tenant.id, tenant.name, '高', `创建租户 ${tenant.name},初始套餐 ${tenant.plan},账务状态 ${tenant.billing},首期金额 ${money(amount)}。`); state.modal = ''; navigate('tenantDetail'); return tenant; }, success: '租户与首期账务已创建,已进入租户详情', close: false, rerender: false }); + } + if (form.id === 'tenantStatusForm') { + const tenant = tenantById(state.modalData.tenantId); const reason = fieldValue(form, 'reason'); + simulateWrite({ key: 'tenant-status', permission: 'platform:tenant:status', operationId: 'PlatformAdminTenantsController_status', body: () => ({ tenantId: tenant.id, status: tenant.status === '暂停' ? '正常' : '暂停', billingStatus: fieldValue(form, 'billingStatus'), reason }), validate: () => !reason ? { reason: '必须填写可审计的变更原因' } : !new FormData(form).get('confirm') ? { confirm: '请确认业务影响' } : null, apply: () => { const from = tenant.status; const billingFrom = tenant.billing; tenant.status = from === '暂停' ? '正常' : '暂停'; tenant.billing = fieldValue(form, 'billingStatus') || tenant.billing; addAudit('tenant.status.update', 'tenant', tenant.id, tenant.name, '高', `业务状态 ${from} → ${tenant.status};账务状态 ${billingFrom} → ${tenant.billing}。原因:${reason}`); return tenant.status; }, success: nextStatus => `租户已${nextStatus === '暂停' ? '暂停' : '恢复'},账务状态已同步` }); + } + if (form.id === 'billingProfileForm') { + simulateWrite({ key: 'billing-profile', permission: 'platform:tenant:billing_profile', operationId: 'PlatformAdminTenantsController_billingProfile', body: () => ({ tenantId: state.selectedTenant, ...Object.fromEntries(new FormData(form)) }), validate: () => validateBillingProfile(form), apply: () => { data.billingProfiles[state.selectedTenant] = Object.fromEntries(new FormData(form)); addAudit('tenant.billing_profile.update', 'tenant', state.selectedTenant, tenantById(state.selectedTenant).name, '高', '更新租户账务与开票资料,银行账号保持脱敏。'); navigateBack(); }, success: '账务资料已保存并回到租户详情', close: false, rerender: false }); + } + if (form.id === 'subscriptionForm') { + const values = Object.fromEntries(new FormData(form)); + simulateWrite({ key: 'subscription', permission: 'platform:billing:write', operationId: 'PlatformAdminBillingController_createSubscription', body: () => ({ ...values, autoRenew: Boolean(new FormData(form).get('autoRenew')) }), validate: () => { const errors = {}; if (data.subscriptions.some(item => item.tenantId === values.tenantId && item.status === '活跃')) errors.tenantId = '该租户已有活跃订阅,请先处理原订阅'; if (!values.amount || Number(values.amount) < 0) errors.amount = '金额必须大于等于 0'; if (!values.start) errors.start = '开始日期不能为空'; if (!values.expires || values.expires <= values.start) errors.expires = '到期日期必须晚于开始日期'; return Object.keys(errors).length ? errors : null; }, apply: () => { const subscription = { id: entityId('SUB'), tenantId: values.tenantId, planCode: values.planCode, status: values.status, cycle: values.cycle, amount: Number(values.amount), start: values.start, expires: values.expires, autoRenew: Boolean(new FormData(form).get('autoRenew')) }; data.subscriptions.unshift(subscription); const tenant = tenantById(values.tenantId); tenant.planCode = values.planCode; tenant.plan = planByCode(values.planCode).name; tenant.expires = values.expires; addAudit('subscription.create', 'subscription', subscription.id, tenant.name, '高', `创建 ${subscription.cycle} ${tenant.plan} 订阅,金额 ${money(subscription.amount)}。`); }, success: '订阅已创建并更新租户套餐' }); + } + if (form.id === 'invoiceForm') { + const values = Object.fromEntries(new FormData(form)); + simulateWrite({ key: 'invoice', permission: 'platform:billing:write', operationId: 'PlatformAdminBillingController_createInvoice', body: values, validate: () => { const errors = {}; if (!values.amount || Number(values.amount) <= 0) errors.amount = '金额必须大于 0'; if (!values.due) errors.due = '到期日不能为空'; if (!values.period) errors.period = '账期不能为空'; return Object.keys(errors).length ? errors : null; }, apply: () => { const invoice = { id: entityId('INV'), tenantId: values.tenantId, type: values.type, period: values.period, total: Number(values.amount), paid: 0, balance: Number(values.amount), due: values.due, status: '待付款', note: values.note || values.type }; data.invoices.unshift(invoice); const tenant = tenantById(values.tenantId); tenant.balance += invoice.balance; tenant.billing = '待付款'; addAudit('invoice.create', 'invoice', invoice.id, invoice.id, '高', `手工创建 ${money(invoice.total)} 服务费账单。`); }, success: '账单草稿已创建' }); + } + if (form.id === 'paymentForm') { + const invoice = invoiceById(state.modalData.invoiceId); const values = Object.fromEntries(new FormData(form)); const amount = parseMoney(values.amount); + simulateWrite({ key: 'payment', permission: 'platform:billing:payment', operationId: 'PlatformAdminBillingController_confirmPayment', body: () => ({ ...values, tenantId: invoice.tenantId, invoiceId: invoice.backendId || invoice.id, amount }), validate: () => { const errors = {}; if (!amount || amount <= 0) errors.amount = '收款金额必须大于 0'; if (amount > invoice.balance) errors.amount = `收款金额不可超过余额 ${money(invoice.balance)}`; if (!values.paymentNo) errors.paymentNo = '平台收款号不能为空'; if (!values.tradeNo) errors.tradeNo = '交易流水号不能为空'; if (!new FormData(form).get('confirm')) errors.confirm = '请先确认已核对银行流水'; return Object.keys(errors).length ? errors : null; }, apply: () => { invoice.paid += amount; invoice.balance -= amount; invoice.status = invoice.balance === 0 ? '已支付' : '部分支付'; const tenant = tenantById(invoice.tenantId); tenant.balance = Math.max(0, tenant.balance - amount); tenant.billing = tenant.balance === 0 ? '正常' : '待付款'; if (tenant.status === '暂停' && tenant.balance === 0) tenant.status = '正常'; addAudit('invoice.payment.confirm', 'invoice', invoice.id, invoice.id, '高', `人工确认收款 ${money(amount)},交易流水仅保存脱敏摘要。`); }, success: '收款已确认,账单与租户余额已更新' }); + } + if (form.id === 'usageForm') { + const values = Object.fromEntries(new FormData(form)); + simulateWrite({ key: 'usage', permission: 'platform:usage:write', operationId: 'PlatformAdminBillingController_recordUsage', request: () => Promise.all([['students', values.students], ['questions', values.questions], ['storage_gb', values.storageGB], ['traffic_tb', values.trafficTB]].map(([metricKey, metricValue]) => platformApi.request('PlatformAdminBillingController_recordUsage', { body: platformApi.buildBody('PlatformAdminBillingController_recordUsage', { tenantId: values.tenantId, period: values.period, metricKey, metricValue }) }))), validate: () => data.usage.some(item => item.tenantId === values.tenantId && item.period === values.period) ? { period: '该租户账期已有记录,避免周期重叠或重复' } : null, apply: () => { data.usage.unshift({ tenantId: values.tenantId, period: values.period, source: '人工补录', students: Number(values.students), questions: Number(values.questions), storageGB: Number(values.storageGB), trafficTB: Number(values.trafficTB), overage: [], estimated: 0 }); addAudit('usage.create', 'tenant', values.tenantId, tenantById(values.tenantId).name, '中', `人工补录 ${values.period} 平台用量。`); }, success: '用量记录已保存并标记人工来源' }); + } + if (form.id === 'dunningChannelForm') { + const values = Object.fromEntries(new FormData(form)); + const existing = data.dunningChannels.find(item => item.id === values.id); + simulateWrite({ key: 'dunning-channel', permission: 'platform:billing:notification', operationId: 'PlatformAdminDunningChannelsController_upsertChannel', body: () => ({ ...values, secretRef: existing?.secretRef, reminderTypes: existing?.reminderTypes, reminderChannels: existing?.reminderChannels, minReminderLevel: existing?.minReminderLevel, enabled: Boolean(new FormData(form).get('enabled')) }), validate: () => validateNotificationChannel(values), apply: () => { const next = { id: values.id || entityId('DUN'), code: values.code, name: values.name, provider: values.provider, endpoint: values.endpoint, endpointRedacted: false, secretRef: existing?.secretRef || '', retry: Number(values.retry), enabled: Boolean(new FormData(form).get('enabled')) }; if (existing) Object.assign(existing, next); else data.dunningChannels.unshift(next); addAudit('dunning.channel.upsert', 'notification_channel', next.id, next.name, '高', `保存催缴渠道 ${next.code},secret 保持只写。`); }, success: '催缴通知渠道已保存' }); + } + if (form.id === 'grantForm') { + const values = Object.fromEntries(new FormData(form)); + simulateWrite({ key: 'grant', permission: 'platform:question_bank:grant', operationId: 'PlatformAdminQuestionBanksController_upsertGrant', body: () => ({ ...values, localEdit: Boolean(new FormData(form).get('localEdit')) }), validate: () => { const errors = {}; const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const splitScope = value => String(value || '').split(/[、,,]/).map(item => item.trim()).filter(Boolean); if (values.scope === '指定套餐' && !values.planCode) errors.planCode = '指定套餐范围必须选择套餐'; if (values.scope === '指定租户' && !values.tenantId) errors.tenantId = '指定租户范围必须选择租户'; if (values.scope === '套餐 + 租户' && (!values.planCode || !values.tenantId)) errors.planCode = '混合范围必须同时选择套餐与租户'; if (platformApi?.isEnabled() && splitScope(values.regionIds).some(value => !uuid.test(value))) errors.regionIds = '地区范围必须为 UUID,多个值用逗号分隔'; if (platformApi?.isEnabled() && splitScope(values.subjectIds).some(value => !uuid.test(value))) errors.subjectIds = '科目范围必须为 UUID,多个值用逗号分隔'; if (values.expiresAt && values.expiresAt <= values.startsAt) errors.expiresAt = '到期日期必须晚于开始日期'; return Object.keys(errors).length ? errors : null; }, apply: () => { const existing = data.grants.find(item => item.id === values.id); const splitScope = value => String(value || '').split(/[、,,]/).map(item => item.trim()).filter(Boolean); const next = { id: values.id || entityId('GRANT'), bankId: values.bankId, scope: values.scope, allowedPlanCodes: values.planCode ? [values.planCode] : [], allowedTenantIds: values.tenantId ? [values.tenantId] : [], allowedRegionIds: splitScope(values.regionIds), allowedSubjectIds: splitScope(values.subjectIds), status: existing?.status || '启用', startsAt: values.startsAt, expiresAt: values.expiresAt, localEdit: Boolean(new FormData(form).get('localEdit')) }; if (existing) Object.assign(existing, next); else data.grants.unshift(next); addAudit('bank.grant.update', 'grant', next.id, bankById(next.bankId).name, '高', `保存授权范围 ${next.scope};地区 ${next.allowedRegionIds.join('、') || '不限'};科目 ${next.allowedSubjectIds.join('、') || '不限'}。`); }, success: '公共题库授权范围已保存' }); + } + if (form.id === 'staffForm') { + const values = Object.fromEntries(new FormData(form)); const permissions = new FormData(form).getAll('permissions'); + simulateWrite({ key: 'staff', permission: 'platform:staff:write', operationId: 'PlatformAdminOverviewController_upsertStaff', body: () => ({ ...values, permissions }), validate: () => { const errors = {}; ['authUserId', 'username', 'name', 'email'].forEach(name => { if (!values[name]) errors[name] = `${name} 不能为空`; }); if (platformApi?.isEnabled() && values.authUserId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(values.authUserId)) errors.authUserId = '真实模式必须填写已存在的 Supabase Auth 用户 UUID'; if (!platformApi?.isEnabled() && values.authUserId && !values.authUserId.startsWith('auth-')) errors.authUserId = 'Mock Auth 用户不存在,请使用 auth- 前缀'; if (!permissions.length) errors.permissions = '至少授予一项平台权限'; return Object.keys(errors).length ? errors : null; }, apply: () => { const existing = data.staff.find(item => item.id === values.id); const next = { id: values.id || entityId('STAFF'), authUserId: values.authUserId, username: values.username, name: values.name, email: values.email, phone: values.phone || '-', role: values.role, permissions, status: existing?.status || '正常', lastActive: existing?.lastActive || '尚未登录' }; if (existing) Object.assign(existing, next); else data.staff.unshift(next); addAudit('staff.upsert', 'staff', next.id, next.name, '高', `保存平台员工及 ${permissions.length} 项有效权限。`); }, success: '平台员工与有效权限已保存' }); + } + if (form.id === 'auditChannelForm') { + const values = Object.fromEntries(new FormData(form)); + const existing = data.auditChannels.find(item => item.id === values.id); + simulateWrite({ key: 'audit-channel', permission: 'platform:audit:notification', operationId: 'PlatformAdminAuditController_upsertChannel', body: () => ({ ...values, secretRef: existing?.secretRef, enabled: Boolean(new FormData(form).get('enabled')) }), validate: () => validateNotificationChannel(values), apply: () => { const next = { id: values.id || entityId('CH'), code: values.code, name: values.name, provider: values.provider, endpoint: values.endpoint, endpointRedacted: false, secretRef: existing?.secretRef || '', minSeverity: values.minSeverity, statusFilter: values.statusFilter, timeout: Number(values.timeout), maxAttempts: Number(values.maxAttempts), enabled: Boolean(new FormData(form).get('enabled')), lastStatus: existing?.lastStatus || '正常' }; if (existing) Object.assign(existing, next); else data.auditChannels.unshift(next); addAudit('audit.channel.upsert', 'notification_channel', next.id, next.name, '高', `保存审计告警渠道 ${next.code},secret 保持只写。`); }, success: '审计通知渠道已保存' }); + } +}); + +document.addEventListener('click', event => { + if (event.target.classList.contains('modal-backdrop')) { + closeModal(); + return; + } + const copy = event.target.closest('[data-copy]'); if (copy) { navigator.clipboard?.writeText(copy.dataset.copy); showToast('已复制到剪贴板'); return; } + const nav = event.target.closest('[data-nav]'); if (nav) { requestLeave(() => { state.selectedTenant = ''; navigate(nav.dataset.nav, { force: true }); }); return; } + const tenant = event.target.closest('[data-tenant-detail]'); if (tenant) { requestLeave(() => { state.selectedTenant = tenant.dataset.tenantDetail; navigate('tenantDetail', { force: true }); }); return; } + const invoice = event.target.closest('[data-invoice-detail]'); if (invoice) { if (requireDesktop('确认账单收款')) return; state.selectedInvoice = invoice.dataset.invoiceDetail; openModal('payment', { invoiceId: invoice.dataset.invoiceDetail }); return; } + const audit = event.target.closest('[data-audit-detail]'); if (audit) { openModal('auditDetail', { auditId: audit.dataset.auditDetail }); return; } + const alertDetail = event.target.closest('[data-alert-detail]'); if (alertDetail) { openModal('alertDetail', { alertId: alertDetail.dataset.alertDetail }); return; } + const notificationDetail = event.target.closest('[data-notification-detail]'); if (notificationDetail) { openModal('notificationDetail', { eventId: notificationDetail.dataset.notificationDetail }); return; } + const tf = event.target.closest('[data-tenant-filter]'); if (tf) { state.tenantFilter = tf.dataset.tenantFilter; syncNavigationState(); render(); return; } + const billingTab = event.target.closest('[data-billing-tab]'); if (billingTab) { state.billingTab = billingTab.dataset.billingTab; syncNavigationState(); render(); return; } + const bankTab = event.target.closest('[data-bank-tab]'); if (bankTab) { state.bankTab = bankTab.dataset.bankTab; syncNavigationState(); render(); return; } + const bankSelect = event.target.closest('[data-bank-select]'); if (bankSelect) { state.selectedBank = bankSelect.dataset.bankSelect; syncNavigationState(); render(); return; } + const auditSeverity = event.target.closest('[data-audit-severity]'); if (auditSeverity) { state.auditSeverity = auditSeverity.dataset.auditSeverity; syncNavigationState(); render(); return; } + const alertStatus = event.target.closest('[data-alert-status]'); if (alertStatus) { state.alertStatus = alertStatus.dataset.alertStatus; syncNavigationState(); render(); return; } + const actionElement = event.target.closest('[data-action]'); const action = actionElement?.dataset.action; if (!action) return; + if (action === 'close-modal') { closeModal(); return; } + if (action === 'execute-confirmed' || action === 'execute-staff-disable') { if (requireDesktop('执行高风险确认操作')) return; const execute = state.modalData.execute; closeModal({ force: true }); execute?.(); return; } + if (action === 'subpage-back') { navigateBack(); return; } + if (action === 'subpage-close') { closeSubpage(); return; } + if (action === 'retry-page') { state.scenario = 'normal'; scenarioSelect.value = 'normal'; render(); return; } + if (action === 'restore-session') { state.scenario = 'normal'; state.view = 'overview'; state.previousView = ''; state.history = []; scenarioSelect.value = 'normal'; render(); showToast('Mock 会话已恢复'); return; } + if (action === 'logout' && platformApi?.isEnabled()) { window.GongxuePlatformAuth?.logout(); return; } + if (action === 'logout') { closeModal(); state.scenario = 'session_invalid'; state.view = 'overview'; state.previousView = ''; state.history = []; scenarioSelect.value = 'session_invalid'; addAudit('auth.logout', 'application', 'platform-console', '平台管理控制台', '中', '超级管理员主动退出当前平台端会话。'); saveData(); render(); return; } + if (action === 'refresh') { + if (platformApi?.isEnabled()) { hydrateFromBackend(); return; } + state.scenario = 'loading'; scenarioSelect.value = 'loading'; render(); + setTimeout(() => { state.scenario = 'normal'; scenarioSelect.value = 'normal'; render(); showToast('平台静态演示数据已刷新'); }, 700); + return; + } + if (action === 'reset-mock') { if (requireDesktop('重置平台 Mock 数据')) return; confirmAction({ title: '重置平台 Mock 数据', description: '恢复到 v1.1 初始演示数据。', impact: '所有本地创建的租户、账单、任务、授权、员工修改和告警状态将被清除。', confirmLabel: '确认重置' }, () => { localStorage.removeItem(STORAGE_KEY); data = clone(seed); state.selectedTenant = data.tenants[0].id; saveData(); render(); showToast('平台 Mock 数据已重置'); }); return; } + if (action === 'open-task-center') { openModal('tasks'); return; } + if (action === 'create-tenant') { if (requireDesktop('创建租户')) return; openModal('createTenant'); return; } + if (action === 'prepare-tenant-status') { if (requireDesktop('变更租户生命周期')) return; openModal('tenantStatus', { tenantId: actionElement.dataset.tenantId || state.selectedTenant }); return; } + if (action === 'open-tenant-portal') { const tenant = tenantById(state.selectedTenant); let portalWindow = null; try { portalWindow = window.open(TENANT_PORTAL_ORIGIN, '_blank'); if (portalWindow) portalWindow.opener = null; } catch {} showToast(portalWindow ? `已在新标签打开独立租户端 · ${tenant?.name || '当前租户'}` : '已请求打开独立租户端;如未出现,请允许本站弹出窗口', portalWindow ? 'success' : 'error'); return; } + if (action === 'edit-billing-profile') { if (requireDesktop('编辑租户账务资料')) return; navigate('billingProfile'); return; } + if (action === 'billing-profile-to-invoices' || action === 'tenant-to-billing') { if (actionElement.dataset.tenantId) state.selectedTenant = actionElement.dataset.tenantId; navigate('billing'); return; } + if (action === 'tenant-to-usage') { state.selectedTenant = actionElement.dataset.tenantId; navigate('usage'); return; } + if (action === 'clear-tenant-context') { state.selectedTenant = ''; render(); return; } + if (action === 'create-subscription-for-tenant') { if (requireDesktop('创建租户订阅')) return; navigate('subscriptions'); openModal('createSubscription'); return; } + if (action === 'create-subscription') { if (requireDesktop('创建租户订阅')) return; openModal('createSubscription'); return; } + if (action === 'select-plan') { if (requireDesktop('选择套餐并创建订阅')) return; openModal('createSubscription', { planCode: actionElement.dataset.planCode }); return; } + if (action === 'subscription-to-billing') { navigate('billing'); return; } + if (action === 'subscriptions-to-usage') { navigate('usage'); return; } + if (action === 'toggle-auto-renew') { if (requireDesktop('变更订阅自动续费')) return; if (platformApi?.isEnabled()) { showToast('后端暂未提供订阅更新接口,真实模式禁止仅改本地状态', 'error'); return; } const item = data.subscriptions.find(subscription => subscription.id === actionElement.dataset.subscriptionId); if (!canWrite('platform:billing:write')) return; item.autoRenew = !item.autoRenew; addAudit('subscription.auto_renew.update', 'subscription', item.id, item.id, '中', `自动续费更新为 ${item.autoRenew ? '开启' : '关闭'}。`); saveData(); render(); showToast('自动续费设置已更新'); return; } + if (action === 'create-invoice') { if (requireDesktop('手工创建账单')) return; openModal('createInvoice'); return; } + if (action === 'apply-invoice-query') { runContractRead({ key: 'invoice-query', actionId: 'act.p-06.01', operationId: 'PlatformAdminBillingController_invoices', title: '账单查询', suppliedRequestId: actionElement.dataset.contractRequestId, liveRequest: () => platformApi.request('PlatformAdminBillingController_invoices', { query: { tenantId: state.selectedTenant || undefined, status: ({ 草稿: 'draft', 待付款: 'issued', 已支付: 'paid', 已作废: 'void', 已逾期: 'overdue' })[state.invoiceStatus], limit: 200 } }), apply: payload => { if (Array.isArray(payload?.items)) data.invoices = payload.items.map(platformApi.adapters.invoice); return `${data.invoices.filter(invoice => (!state.selectedTenant || invoice.tenantId === state.selectedTenant) && (state.invoiceStatus === 'all' || invoice.status === state.invoiceStatus) && (!state.invoiceSearch.trim() || [invoice.id, invoice.type, invoice.period, invoice.note, tenantById(invoice.tenantId)?.name].some(value => String(value || '').toLowerCase().includes(state.invoiceSearch.trim().toLowerCase())))).length} 张匹配账单`; } }); return; } + if (action === 'preview-invoice-candidates') { if (requireDesktop('批量生成账单')) return; openModal('invoiceCandidates'); return; } + if (action === 'run-invoice-dry-run' && platformApi?.isEnabled()) { const subscriptionIds = Array.from(overlayRoot.querySelectorAll('[name="candidate"]:checked')).map(input => input.value); executeBackendMutation('PlatformAdminBillingController_fromSubscriptionsBatch', { subscriptionIds, dryRun: true }, payload => { const result = payload?.item || {}; showToast(`Dry-run 完成 · 可生成 ${result.items?.length || 0} 条,跳过 ${result.skippedCount || 0} 条`); }, { refresh: false }); return; } + if (action === 'run-invoice-dry-run') { const count = overlayRoot.querySelectorAll('[name="candidate"]:checked').length; showToast(`Dry-run 已校验 ${count} 条订阅候选,未写入账单`); return; } + if (action === 'generate-subscription-invoice' && platformApi?.isEnabled()) { if (requireDesktop('为单个订阅生成账单')) return; const subscription = data.subscriptions.find(item => item.id === actionElement.dataset.subscriptionId); if (!subscription) { showToast('订阅不存在或不可用', 'error'); return; } executeBackendMutation('PlatformAdminBillingController_fromSubscription', { tenantId: subscription.tenantId, subscriptionId: subscription.id, status: 'issued' }, payload => { closeModal({ force: true }); window.recordPlatformSpecResult?.('act.p-06.04', { result: 'success', operationId: 'PlatformAdminBillingController_fromSubscription', requestId: payload?.meta?.requestId || '' }); showToast('订阅账单已由后端生成'); }); return; } + if (action === 'generate-subscription-invoice') { if (requireDesktop('为单个订阅生成账单')) return; const subscription = data.subscriptions.find(item => item.id === actionElement.dataset.subscriptionId); const suppliedRequestId = actionElement.dataset.contractRequestId || requestId(); if (!subscription || data.invoices.some(invoice => invoice.note.includes(subscription.id))) { state.contractResults['invoice-query'] = { status: 'error', title: '单订阅生成失败', detail: '该订阅已存在账单或订阅不可用', requestId: suppliedRequestId }; addAudit('invoice.from_subscription.failed', 'subscription', subscription?.id || '-', subscription?.id || '-', '中', `幂等冲突。Request ID:${suppliedRequestId}。`, '失败'); window.recordPlatformSpecResult?.('act.p-06.04', { result: 'failed', operationId: 'PlatformAdminBillingController_fromSubscription', requestId: suppliedRequestId }); render(); return; } const task = addContractTask({ actionId: 'act.p-06.04', operationId: 'PlatformAdminBillingController_fromSubscription', label: `为订阅 ${subscription.id} 生成账单`, resultTarget: '服务费账单', suppliedRequestId, execute: taskItem => { const invoice = { id: entityId('INV'), tenantId: subscription.tenantId, type: '订阅服务费', period: '下一订阅账期', total: subscription.amount, paid: 0, balance: subscription.amount, due: subscription.expires, status: '待付款', note: `订阅 ${subscription.id}` }; data.invoices.unshift(invoice); tenantById(subscription.tenantId).balance += invoice.balance; taskItem.invoiceId = invoice.id; addAudit('invoice.from_subscription.create', 'invoice', invoice.id, invoice.id, '高', `通过任务 ${taskItem.id} 为订阅 ${subscription.id} 生成单张账单。`); } }); showToast(`单订阅账单任务 ${task.id} 已创建`); return; } + if (action === 'confirm-generate-candidates' && platformApi?.isEnabled()) { if (requireDesktop('批量生成账单')) return; const subscriptionIds = Array.from(overlayRoot.querySelectorAll('[name="candidate"]:checked')).map(input => input.value); if (!subscriptionIds.length) { showToast('请至少选择一条订阅账单候选', 'error'); return; } executeBackendMutation('PlatformAdminBillingController_fromSubscriptionsBatch', { subscriptionIds, dryRun: false, status: 'issued' }, payload => { closeModal({ force: true }); const result = payload?.item || {}; showToast(`后端已生成 ${result.createdCount || 0} 张账单,跳过 ${result.skippedCount || 0} 条`); }); return; } + if (action === 'confirm-generate-candidates') { if (requireDesktop('批量生成账单')) return; const selectedIds = Array.from(overlayRoot.querySelectorAll('[name="candidate"]:checked')).map(input => input.value); if (!selectedIds.length) { showToast('请至少选择一条订阅账单候选', 'error'); return; } const candidates = data.subscriptions.filter(subscription => selectedIds.includes(subscription.id) && !data.invoices.some(invoice => invoice.note.includes(subscription.id))); closeModal(); const task = addTask(`批量生成 ${candidates.length} 张订阅账单`, '服务费账单', () => { candidates.forEach(subscription => { const invoice = { id: entityId('INV'), tenantId: subscription.tenantId, type: '订阅服务费', period: '下一订阅账期', total: subscription.amount, paid: 0, balance: subscription.amount, due: subscription.expires, status: '待付款', note: `订阅 ${subscription.id}` }; data.invoices.unshift(invoice); const tenant = tenantById(subscription.tenantId); tenant.balance += invoice.balance; tenant.billing = '待付款'; }); addAudit('invoice.batch.create', 'invoice_batch', task.id, task.id, '高', `批量生成 ${candidates.length} 张订阅账单。`); }); showToast(`任务 ${task.id} 已创建`); return; } + if (action === 'confirm-payment') { if (requireDesktop('确认账单收款')) return; openModal('payment', { invoiceId: actionElement.dataset.invoiceId }); return; } + if (action === 'preview-overdue' && platformApi?.isEnabled()) { if (requireDesktop('批量处理逾期账单')) return; executeBackendMutation('PlatformAdminBillingController_processOverdue', { dryRun: true, limit: 100, channel: 'internal' }, payload => { openModal('overduePreview'); showToast(`后端 Dry-run:${payload?.item?.processed || payload?.item?.items?.length || 0} 条候选`); }, { refresh: false }); return; } + if (action === 'preview-overdue') { if (requireDesktop('批量处理逾期账单')) return; openModal('overduePreview'); return; } + if (action === 'confirm-overdue-process' && platformApi?.isEnabled()) { showToast('后端逾期处理接口不支持指定 invoiceIds,真实模式禁止按当前勾选直接执行', 'error'); return; } + if (action === 'confirm-overdue-process') { if (requireDesktop('批量处理逾期账单')) return; const selectedIds = Array.from(overlayRoot.querySelectorAll('[name="candidate"]:checked')).map(input => input.value); if (!selectedIds.length) { showToast('请至少选择一条逾期账单候选', 'error'); return; } const overdue = data.invoices.filter(invoice => invoice.status === '已逾期' && selectedIds.includes(invoice.id)); closeModal(); const task = addTask(`处理 ${overdue.length} 张逾期账单`, '收款与催缴', () => { overdue.forEach(invoice => data.reminders.unshift({ id: entityId('REM'), invoiceId: invoice.id, tenantId: invoice.tenantId, level: 3, channel: '邮件 + 短信', status: '发送中', date: todayTime(), message: `服务费账单 ${invoice.id} 已逾期,请尽快处理。` })); addAudit('invoice.overdue.process', 'invoice_batch', task.id, task.id, '高', `生成 ${overdue.length} 条催缴记录。`); }); showToast(`催缴任务 ${task.id} 已创建`); return; } + if (action === 'generate-reminder' && platformApi?.isEnabled()) { showToast('后端仅提供批量逾期处理,没有单账单催缴创建接口', 'error'); return; } + if (action === 'generate-reminder') { if (requireDesktop('生成账单催缴')) return; const invoice = invoiceById(actionElement.dataset.invoiceId); const reminder = { id: entityId('REM'), invoiceId: invoice.id, tenantId: invoice.tenantId, level: invoice.status === '已逾期' ? 3 : 1, channel: '邮件 + 短信', status: '已加入队列', date: todayTime(), message: invoice.status === '已逾期' ? `账单已逾期,请尽快支付 ${money(invoice.balance)}。` : `账单即将到期,应付 ${money(invoice.balance)}。` }; data.reminders.unshift(reminder); addAudit('invoice.reminder.create', 'reminder', reminder.id, reminder.id, '中', `为 ${invoice.id} 创建 L${reminder.level} 催缴。`); saveData(); render(); showToast('催缴已加入发送队列'); return; } + if (action === 'record-usage') { if (requireDesktop('人工记录平台用量')) return; openModal('recordUsage'); return; } + if (action === 'preview-overage') { if (requireDesktop('批量生成超额账单')) return; openModal('overagePreview'); return; } + if (action === 'run-overage-dry-run' && platformApi?.isEnabled()) { const tenantIds = Array.from(overlayRoot.querySelectorAll('[name="candidate"]:checked')).map(input => input.value.split(':')[0]); executeBackendMutation('PlatformAdminBillingController_fromUsage', { tenantIds, period: state.usagePeriod, dryRun: true }, payload => { const result = payload?.item || {}; showToast(`超额 Dry-run 完成 · 可生成 ${result.items?.length || 0} 条`); }, { refresh: false }); return; } + if (action === 'run-overage-dry-run') { const count = overlayRoot.querySelectorAll('[name="candidate"]:checked').length; showToast(`Dry-run 已校验 ${count} 条超额候选,未写入账单`); return; } + if (action === 'confirm-overage-invoices' && platformApi?.isEnabled()) { if (requireDesktop('批量生成超额账单')) return; const tenantIds = Array.from(overlayRoot.querySelectorAll('[name="candidate"]:checked')).map(input => input.value.split(':')[0]); if (!tenantIds.length) { showToast('请至少选择一条超额账单候选', 'error'); return; } executeBackendMutation('PlatformAdminBillingController_fromUsage', { tenantIds, period: state.usagePeriod, dryRun: false, status: 'issued' }, payload => { closeModal({ force: true }); showToast(`后端已生成 ${payload?.item?.createdCount || 0} 张超额账单`); }); return; } + if (action === 'confirm-overage-invoices') { if (requireDesktop('批量生成超额账单')) return; const selectedKeys = Array.from(overlayRoot.querySelectorAll('[name="candidate"]:checked')).map(input => input.value); if (!selectedKeys.length) { showToast('请至少选择一条超额账单候选', 'error'); return; } const candidates = data.usage.filter(item => item.period === state.usagePeriod && selectedKeys.includes(`${item.tenantId}:${item.period}`) && item.estimated > 0 && !data.invoices.some(invoice => invoice.note === `超额 ${item.period}` && invoice.tenantId === item.tenantId)); closeModal(); const task = addTask(`生成 ${candidates.length} 张超额账单`, '用量与超额计费', () => { candidates.forEach(item => { const invoice = { id: entityId('INV'), tenantId: item.tenantId, type: '用量超额费', period: item.period, total: item.estimated, paid: 0, balance: item.estimated, due: '2026-08-15', status: '待付款', note: `超额 ${item.period}` }; data.invoices.unshift(invoice); const tenant = tenantById(item.tenantId); tenant.balance += invoice.balance; tenant.billing = '待付款'; }); addAudit('usage.invoice.batch.create', 'invoice_batch', task.id, task.id, '高', `生成 ${candidates.length} 张用量超额账单。`); }); showToast(`超额账单任务 ${task.id} 已创建`); return; } + if (action === 'new-dunning-channel') { if (requireDesktop('新建催缴渠道')) return; openModal('dunningChannel'); return; } + if (action === 'edit-dunning-channel') { if (requireDesktop('编辑催缴渠道')) return; openModal('dunningChannel', { channelId: actionElement.dataset.channelId }); return; } + if (action === 'toggle-dunning-channel') { if (requireDesktop('变更催缴渠道状态')) return; if (!canWrite('platform:billing:notification')) return; const channel = data.dunningChannels.find(item => item.id === actionElement.dataset.channelId); if (!channel) return; if (platformApi?.isEnabled() && channel.endpointRedacted) { openModal('dunningChannel', { channelId: channel.id }); showToast('后端不回显完整 Webhook;请重新输入完整 HTTPS URL 后保存状态', 'error'); return; } const nextEnabled = !channel.enabled; const apply = () => { channel.enabled = nextEnabled; addAudit('dunning.channel.status', 'notification_channel', channel.id, channel.name, '高', `催缴渠道${channel.enabled ? '启用' : '停用'}。`); saveData(); render(); showToast('渠道状态已更新'); }; if (platformApi?.isEnabled()) executeBackendMutation('PlatformAdminDunningChannelsController_upsertChannel', { ...channel, enabled: nextEnabled }, apply); else apply(); return; } + if (action === 'new-grant') { if (requireDesktop('新建公共题库授权')) return; openModal('grant'); return; } + if (action === 'apply-bank-search') { runContractRead({ key: 'bank-query', actionId: 'act.p-09.01', operationId: 'PlatformAdminQuestionBanksController_banks', title: '题库搜索', suppliedRequestId: actionElement.dataset.contractRequestId, liveRequest: () => platformApi.request('PlatformAdminQuestionBanksController_banks', { query: { q: state.bankSearch || undefined, status: 'active', includeTenantBanks: state.bankIncludeTenant, limit: 200 } }), apply: payload => { if (Array.isArray(payload?.items)) data.banks = payload.items.map(item => platformApi.adapters.bank(item, bankById(item.id))); return `${data.banks.filter(bank => !state.bankSearch.trim() || [bank.name, bank.catalog, bank.source, bank.version].some(value => String(value).toLowerCase().includes(state.bankSearch.trim().toLowerCase()))).length} 个匹配题库`; } }); return; } + if (action === 'edit-grant') { if (requireDesktop('编辑公共题库授权')) return; openModal('grant', { grantId: actionElement.dataset.grantId }); return; } + if (action === 'toggle-grant') { if (requireDesktop('变更公共题库授权状态')) return; if (!canWrite('platform:question_bank:grant')) return; const grant = data.grants.find(item => item.id === actionElement.dataset.grantId); if (!grant) return; const next = grant.status === '启用' ? '已禁用' : '启用'; const apply = () => { grant.status = next; addAudit('bank.grant.status', 'grant', grant.id, bankById(grant.bankId)?.name || grant.bankId, '高', `授权规则状态更新为 ${next},租户快照保留。`); saveData(); render(); showToast('授权状态已更新'); }; const execute = () => platformApi?.isEnabled() ? executeBackendMutation('PlatformAdminQuestionBanksController_upsertGrant', { ...grant, status: next }, apply) : apply(); if (next === '已禁用') confirmAction({ title: '禁用公共题库授权', description: `${bankById(grant.bankId)?.name || grant.bankId} · ${grant.id}`, impact: '后续同步将停止,但租户已经采用的内容快照不会被删除。', confirmLabel: '确认禁用' }, execute); else execute(); return; } + if (action === 'preview-bank-sync') { state.bankTab = 'sync'; if (platformApi?.isEnabled()) { runContractRead({ key: 'bank-sync', actionId: 'act.p-09.05', operationId: 'PlatformAdminQuestionBanksController_syncStatus', title: '采用同步状态', suppliedRequestId: actionElement.dataset.contractRequestId, liveRequest: () => platformApi.request('PlatformAdminQuestionBanksController_syncStatus', { query: { limit: 200 } }), apply: payload => { if (Array.isArray(payload?.items)) data.bankSync = payload.items.map(platformApi.adapters.sync); return `${data.bankSync.length} 条采用记录,${data.bankSync.filter(item => item.status === '同步失败').length} 条失败`; } }); return; } const task = addContractTask({ actionId: 'act.p-09.05', operationId: 'PlatformAdminQuestionBanksController_syncStatus', label: '检查租户题库采用与同步状态', resultTarget: '公共题库 · 采用与同步', suppliedRequestId: actionElement.dataset.contractRequestId, execute: taskItem => { taskItem.rows = data.bankSync.length; taskItem.failedRows = data.bankSync.filter(item => item.status === '同步失败').length; } }); render(); showToast(`同步状态检查任务 ${task.id} 已创建`); return; } + if (action === 'refresh-bank-sync') { if (state.modal === 'syncDetail') closeModal({ force: true }); runContractRead({ key: 'bank-sync', operationId: 'PlatformAdminQuestionBanksController_syncStatus', title: '采用同步状态', liveRequest: () => platformApi.request('PlatformAdminQuestionBanksController_syncStatus', { query: { limit: 200 } }), apply: payload => { if (Array.isArray(payload?.items)) data.bankSync = payload.items.map(platformApi.adapters.sync); return `${data.bankSync.length} 条采用记录,${data.bankSync.filter(item => item.status === '同步失败').length} 条失败`; } }); return; } + if (action === 'sync-detail') { openModal('syncDetail', { syncKey: actionElement.dataset.syncKey }); return; } + if (action === 'new-staff') { if (requireDesktop('新建平台员工与授权')) return; openModal('staff', { roleKey: actionElement.dataset.roleKey || '' }); return; } + if (action === 'edit-staff') { if (requireDesktop('编辑平台员工与授权')) return; openModal('staff', { staffId: actionElement.dataset.staffId }); return; } + if (action === 'toggle-staff') { if (requireDesktop('变更平台员工状态')) return; if (!canWrite('platform:staff:status')) return; const person = data.staff.find(item => item.id === actionElement.dataset.staffId); if (!person) return; const disabling = person.status === '正常'; if (disabling && person.role === '超级管理员' && data.staff.filter(item => item.role === '超级管理员' && item.status === '正常').length === 1) { showToast('不能禁用最后一名超级管理员', 'error'); return; } const apply = () => { person.status = disabling ? '已停用' : '正常'; addAudit('staff.status.update', 'staff', person.id, person.name, '高', `${disabling ? '停用并撤销会话' : '启用'}平台员工。`); saveData(); render(); showToast(`员工已${disabling ? '停用并撤销会话' : '启用'}`); }; const execute = () => platformApi?.isEnabled() ? executeBackendMutation('PlatformAdminOverviewController_updateStaffStatus', { staffId: person.id, status: disabling ? 'disabled' : 'active', reason: disabling ? '平台控制台停用员工' : '平台控制台启用员工', revokeSessions: disabling }, apply) : apply(); if (disabling) confirmAction({ title: '停用平台员工', description: `${person.name} · ${person.role}`, impact: '该员工的全部平台会话将立即撤销,后续无法登录平台端;租户端权限不在本域名内处理。', confirmLabel: '停用并撤销会话', confirmActionName: 'execute-staff-disable' }, execute); else execute(); return; } + if (action === 'select-permission-group') { const form = overlayRoot.querySelector('#staffForm'); const role = fieldValue(form, 'role'); const recommended = role === '平台财务' ? ['platform:billing:read', 'platform:billing:write', 'platform:billing:payment', 'platform:billing:dunning', 'platform:billing:notification', 'platform:usage:read', 'platform:usage:write'] : role === '安全审计' ? ['platform:audit:read', 'platform:audit:export', 'platform:audit:alert', 'platform:audit:notification'] : role === '题库运营' ? ['platform:question_bank:read', 'platform:question_bank:grant', 'platform:question_bank:ops'] : role === '租户运营' ? ['platform:tenant:read', 'platform:tenant:write', 'platform:tenant:status', 'platform:tenant:billing_profile'] : Array.from(form.querySelectorAll('[name="permissions"]')).map(input => input.value); form.querySelectorAll('[name="permissions"]').forEach(input => { input.checked = recommended.includes(input.value); }); return; } + if (action === 'clear-staff-form') { const form = overlayRoot.querySelector('#staffForm'); if (!form) return; form.querySelectorAll('input:not([type="hidden"]):not([type="checkbox"])').forEach(input => { input.value = ''; }); form.querySelectorAll('input[type="checkbox"]').forEach(input => { input.checked = false; }); form.elements.role.value = '租户运营'; state.modalData.dirty = true; form.querySelector('[name="authUserId"]')?.focus(); return; } + if (action === 'apply-staff-search') { render(); return; } + if (action === 'export-audit') { if (!hasPermission('platform:audit:export')) { showToast('缺少导出权限:platform:audit:export', 'error'); return; } const format = actionElement.dataset.format || 'csv'; if (platformApi?.isEnabled()) { platformApi.request('PlatformAdminAuditController_exportLogs', { query: { format, q: state.auditSearch || undefined, limit: 500 } }).then(payload => { const item = platformApi.downloadExport(payload); showToast(`审计文件已下载 · ${item.rowCount ?? item.rows ?? 0} 行 · ${item.sha256 || item.hash || 'hash 见响应'}`); }).catch(error => showToast(`${error.message || '审计导出失败'}${error.requestId ? ` · ${error.requestId}` : ''}`, 'error')); return; } const task = addTask(`导出平台审计日志 ${format.toUpperCase()}`, '任务中心下载文件', taskItem => { taskItem.file = `platform-audit-${Date.now()}.${format}`; taskItem.hash = `sha256:${Math.random().toString(36).slice(2).padEnd(32, '0')}`; taskItem.rows = data.auditLogs.length; }); showToast(`导出任务 ${task.id} 已创建`); return; } + if (action === 'jump-related-alert') { state.alertStatus = 'all'; state.selectedAlert = data.alerts.find(alert => data.auditLogs.some(log => log.targetId === alert.targetId || log.targetId === alert.id))?.id || ''; navigate('alerts'); if (state.selectedAlert) openModal('alertDetail', { alertId: state.selectedAlert }); return; } + if (action === 'jump-audit-target') { const log = data.auditLogs.find(item => item.id === actionElement.dataset.auditId); closeModal(); if (log.targetType === 'tenant') { state.selectedTenant = log.targetId; navigate('tenantDetail'); } else if (log.targetType === 'invoice') { state.selectedInvoice = log.targetId; navigate('billing'); } else if (log.targetType === 'question_bank' || log.targetType === 'grant') navigate('bank'); else navigate('alerts'); return; } + if (action === 'transition-alert') { if (!canWrite('platform:audit:alert')) return; const alert = data.alerts.find(item => item.id === actionElement.dataset.alertId); const next = actionElement.dataset.nextStatus; if (!alert) return; if (!['open', 'acknowledged'].includes(alert.status)) { showToast('已结束告警禁止重新打开或再次处理', 'error'); return; } if (alert.status === 'open' && !['acknowledged', 'resolved', 'ignored'].includes(next)) return; if (alert.status === 'acknowledged' && !['resolved', 'ignored'].includes(next)) return; const note = document.querySelector('#alertNote')?.value.trim() || ''; const apply = () => { alert.status = next; alert.note = note; alert.updatedAt = '刚刚'; addAudit('audit.alert.status.update', 'audit_alert', alert.id, alert.title, '高', `告警状态更新为 ${alertStatusLabel(next)}${note ? `,备注:${note}` : ''}。`); clearDirty('modal'); saveData(); closeModal({ force: true }); render(); showToast(`告警已${alertStatusLabel(next)}`); }; if (platformApi?.isEnabled()) executeBackendMutation('PlatformAdminAuditController_updateAlert', { alertId: alert.id, status: next, note }, apply); else apply(); return; } + if (action === 'new-audit-channel') { if (requireDesktop('新建审计通知渠道')) return; openModal('auditChannel'); return; } + if (action === 'edit-audit-channel') { if (requireDesktop('编辑审计通知渠道')) return; openModal('auditChannel', { channelId: actionElement.dataset.channelId }); return; } + if (action === 'toggle-audit-channel') { if (requireDesktop('变更审计通知渠道状态')) return; if (!canWrite('platform:audit:notification')) return; const channel = data.auditChannels.find(item => item.id === actionElement.dataset.channelId); if (!channel) return; if (platformApi?.isEnabled() && channel.endpointRedacted) { openModal('auditChannel', { channelId: channel.id }); showToast('后端不回显完整 Webhook;请重新输入完整 HTTPS URL 后保存状态', 'error'); return; } const nextEnabled = !channel.enabled; const apply = () => { channel.enabled = nextEnabled; addAudit('audit.channel.status', 'notification_channel', channel.id, channel.name, '高', `审计告警渠道${channel.enabled ? '启用' : '停用'}。`); saveData(); render(); showToast('审计通知渠道状态已更新'); }; if (platformApi?.isEnabled()) executeBackendMutation('PlatformAdminAuditController_upsertChannel', { ...channel, enabled: nextEnabled }, apply); else apply(); return; } + if (action === 'retry-notification') { if (platformApi?.isEnabled()) { showToast('后端未提供通知事件重试接口,真实模式禁止伪造成功状态', 'error'); return; } const notification = [...(data.notificationEvents || []), ...(data.dunningEvents || [])].find(item => item.id === actionElement.dataset.eventId); if (!notification) return; notification.status = '重试中'; notification.attempts += 1; notification.updatedAt = `${todayTime()} CST`; saveData(); render(); setTimeout(() => { notification.status = '成功'; notification.error = ''; saveData(); render(); showToast('通知重试成功'); }, 800); return; } + if (action === 'retry-task') { const task = data.tasks.find(item => item.id === actionElement.dataset.taskId); task.status = 'processing'; task.error = ''; task.updatedAt = todayTime(); saveData(); renderOverlay(); setTimeout(() => { task.status = 'completed'; task.updatedAt = todayTime(); saveData(); renderOverlay(); updateCounters(); showToast('任务重试完成'); }, 900); return; } +}); + +document.addEventListener('input', event => { + if (state.modal && event.target.closest('#overlayRoot') && event.target.matches('input,select,textarea')) markDirty('modal'); + if (event.target.closest('#mainView form')) markDirty('page'); + if (event.target.id === 'tenantSearch') { state.tenantSearch = event.target.value; clearTimeout(state.searchTimer); state.searchTimer = setTimeout(render, 180); } + if (event.target.id === 'invoiceSearch') state.invoiceSearch = event.target.value; + if (event.target.id === 'bankSearch') state.bankSearch = event.target.value; + if (event.target.id === 'staffSearch') { state.staffSearch = event.target.value; clearTimeout(state.staffTimer); state.staffTimer = setTimeout(render, 180); } + if (event.target.id === 'auditSearch') { state.auditSearch = event.target.value; clearTimeout(state.auditTimer); state.auditTimer = setTimeout(render, 180); } + if (event.target.id === 'globalSearch') state.globalSearch = event.target.value; + if (['tenantSearch', 'invoiceSearch', 'bankSearch', 'staffSearch', 'auditSearch'].includes(event.target.id)) syncNavigationState(); +}); +document.addEventListener('change', event => { + if (state.modal && event.target.closest('#overlayRoot') && event.target.matches('input,select,textarea')) markDirty('modal'); + if (event.target.closest('#mainView form')) markDirty('page'); + if (event.target === roleSelect) { state.role = event.target.value; state.scenario = 'normal'; if (!hasPermission(pageMeta[state.view].permission)) state.view = 'overview'; render(); showToast(`已切换为${roleDefinitions[state.role].label}`); } + if (event.target === scenarioSelect) { state.scenario = event.target.value; render(); } + if (event.target === responseSelect) { state.response = event.target.value; showToast('下一次 Mock 写请求结果已设置'); } + if (event.target.id === 'tenantPlanFilter') { state.tenantPlan = event.target.value; syncNavigationState(); render(); } + if (event.target.id === 'invoiceStatusFilter') { state.invoiceStatus = event.target.value; syncNavigationState(); } + if (event.target.id === 'usagePeriodFilter') { state.usagePeriod = event.target.value; syncNavigationState(); render(); } + if (event.target.id === 'auditRangeFilter') { state.auditRange = event.target.value; syncNavigationState(); render(); } + if (event.target.id === 'includeTenantBanks') { + state.bankIncludeTenant = event.target.checked; + syncNavigationState(); + render(); + if (platformApi?.isEnabled()) hydrateFromBackend({ silent: true }); + } +}); +menuButton.addEventListener('click', toggleMobileMenu); +document.querySelector('#sidebarBackdrop').addEventListener('click', closeMobileMenu); +document.addEventListener('keydown', event => { + if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') { event.preventDefault(); document.querySelector('#globalSearch').focus(); } + if (event.key === 'Enter' && event.target.id === 'globalSearch') { + event.preventDefault(); + const query = event.target.value.trim().toLowerCase(); + if (!query) { showToast('请输入租户、账单、告警或审计关键词', 'error'); return; } + const tenant = data.tenants.find(item => [item.name, item.slug, item.legal, item.host].some(value => value.toLowerCase().includes(query))); + if (tenant) { state.selectedTenant = tenant.id; navigate('tenantDetail'); showToast(`已定位租户:${tenant.name}`); return; } + const invoice = data.invoices.find(item => [item.id, item.type, item.period, item.note].some(value => String(value).toLowerCase().includes(query))); + if (invoice) { state.selectedTenant = invoice.tenantId; state.selectedInvoice = invoice.id; navigate('billing'); showToast(`已定位账单:${invoice.id}`); return; } + const alert = data.alerts.find(item => [item.id, item.title, item.summary].some(value => value.toLowerCase().includes(query))); + if (alert) { state.selectedAlert = alert.id; navigate('alerts'); openModal('alertDetail', { alertId: alert.id }); return; } + const audit = data.auditLogs.find(item => [item.id, item.actor, item.action, item.target, item.details].some(value => value.toLowerCase().includes(query))); + if (audit) { navigate('audit'); openModal('auditDetail', { auditId: audit.id }); return; } + showToast('没有找到匹配的租户、账单、告警或审计事件', 'error'); + } + if (event.key === 'Tab' && state.modal) { + const modal = overlayRoot.querySelector('.modal'); + const focusable = modal ? Array.from(modal.querySelectorAll('button:not([disabled]),input:not([disabled]):not([type="hidden"]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])')).filter(element => element.offsetParent !== null) : []; + if (focusable.length) { + const first = focusable[0]; const last = focusable[focusable.length - 1]; + if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } + else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } + } + } + if (event.key === 'Escape') { if (state.modal) closeModal(); else if (app.classList.contains('menu-open')) closeMobileMenu(); else if (pageMeta[state.view]?.level > 1) navigateBack(); } +}); +document.addEventListener('click', event => { + const choice = event.target.closest('[data-leave-choice]')?.dataset.leaveChoice; + if (!choice) return; + event.preventDefault(); event.stopImmediatePropagation(); + if (choice === 'stay') { pendingLeave = null; closeLeaveGuard(); return; } + const leave = pendingLeave; pendingLeave = null; clearDirty(leave?.scope || ''); closeLeaveGuard(); leave?.execute?.(); +}, true); +window.addEventListener('beforeunload', event => { if (!hasUnsavedChanges()) return; event.preventDefault(); event.returnValue = ''; }); +window.addEventListener('DOMContentLoaded', async () => { + if (platformApi?.isEnabled()) { + const authenticated = await window.GongxuePlatformAuth?.requireSession(); + if (!authenticated) return; + document.documentElement.dataset.platformMode = 'api'; + } + setupPrototypeControls(); + if (platformApi?.isEnabled()) document.querySelector('.prototype-controls')?.setAttribute('hidden', ''); + render(); + const rootSnapshot = navigationSnapshot(); + history.replaceState({ [HISTORY_APP_KEY]: true, rootGuard: true, sequence: historySequence, snapshot: rootSnapshot }, '', location.href); + historySequence += 1; + history.pushState({ [HISTORY_APP_KEY]: true, sequence: historySequence, snapshot: rootSnapshot }, '', location.href); + if (platformApi?.isEnabled()) hydrateFromBackend({ silent: true }); +}); diff --git a/Tiku.Api/wwwroot/platform-admin/assets/logo.png b/Tiku.Api/wwwroot/platform-admin/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3288dae674ad032c58a058828279263aadf10944 GIT binary patch literal 289534 zcmbq*c|4Ts-~T;!vLq?HO2}SWV#ZP_$}&h1*-Ew$!q}MzM_F3zj3qH-8%ui_l)XW= zAu-v;G9o(-ewTB;&-449b2_irbLx-#rRk6BzV7S#Ebq_z^S-VqL!>qv3m*#vL2T#G zow)!(bly7;69f1kg@@}h;19F=IWtcP+Qqf=z@XH09*FJ+?);fk7k$$yn*sN|KKt}V z2Ca^#f6ef-I=J_+Y~uZs`#TKwFyVBvF7M$yH^^+-Xs9g4^YnVC@kN*Yx8rRQ%+Ecp zaarFf#2HsV*+Zio3!_Eh-yO+4Y{(LE{C+&f?Ho()=G-8?8Q|7^13+VGy5OILLiVfie>&t6Yc zLI!3sEN+Q@xObHs8idwvK-RqK!7S)Zh}0UVUd@UZE2^QtC>-jNjdMm3_J7Wp^7!UB zb#E(nV6r;O*(V&e=sd-|efFw>oO!U;jbhCQElw{Qw`L9{s;?##ST0y^Uo|T)werL>}22d2>nrX>~-oVZUqk;=?d*lke$% zZQ7T@JG>R$=pSITA@Z7D{rZ0uQ0Fc@%v*dbN)wsl88?V9S~wv)1u##Rlq zr2%c}LLvP4;fT7B2gTM-eOs7|j!HF$tw#b}vqBQOZtm}vwVoEsiBtrkzRIB)!Q09u1X8ZvDFetrGL+` zQOcb8=Mx^YB^@_|Wz@dZ9HC5|ZEZhNB)zfkMoB@}KP-XJc42+M+EZ8i4>PSP%Hbu? zhMT87Urjro7RsL3y#C$u;n8P(hWl6?svE}%y6h&GXFVsreKd*d5`ULQWisCu_%`X$ zWs?2gx&GB|(>1e;4c4mzehG`)_k%XM&%(>8ZnSGxw1u}zDKp=(>6v{k*8J0i718QG zh7D7dS=6bX1wG7I`Ty8}WUyfy(d?-oE$Ip@kRR>FFfRjD(~+EIYd)UXuriG-$gXT( zdg)uar1N@zy;64&SKn8b*@?c7TwC|*GOQ}8(cR{ehroiyZT6j&4*Du({AR3XtKf0% zJ-9r|e%B1)$;9%wUixWMdx~`2W!E;MMB?=Z!n3{RZLp;G3%giMD`OP-~weHQuN z4x?&7R5mC?K5#4UV9M#4XQ`Lhoi!`^`uCS5wzog)C3$FJp5N7-~FP?*Tt^LL!*$!ziv#yqwFq4xQ`8qnp z@@u(WsOo)&HB5N9JNWcc*SzyM;}Tt*@j+c&#ekwU_+>+fkW%eqkYO^wc`Df~9}mTx z%`X3Rnk>pF1>TebKJK0YZn0^-2Abbr-SJ9!Ski1NdCsc-1qEj) zc-gAom1=pOy&Q@e*BxG)bM!tEidnwOjA}{RP1#LJ=R8D|xaWC{rj|he=dY<~^5c-` z-0*=>otWldJr}D*=3Z&d;_Wt@3p7f$!CJi#B(B>-HA48(Fa_o6te!I!lO=ht=@8>P z&j20btR(g`6Y6PhGd-f6oR;X6*oQ;U=bv)-3!TZ30R?)l?^@=MWcb(bz!KL>p|zad_{A3` zkHKz^_a2&L&aW8sD?3_?tonKjw`hD|H~GY=hS*ZDAHVNO*YB?98j9KRHabL)7Eb=k zIh?21KkdEorrM?*)8Kl}{f5?Ag`Kas5teZq-kNUu`&VQzBKV;iKD-erU#E|Tzpk$HDgK(1>qhmdgm>#&zLBa-v(`0OlVb_+?_7{y za2C9K5&1@K#d?3)(yLOv*@xR3*t_OI3PFQO8iOT)v&FtOMm}PSrE(4|Op!TERN?xk`0#3Ab?FgA9fh`rp)Ey7Bj6eE z-GomASsPK`W#N4wx>|H^%0eMDcwj~dh>~H2#8sI8XZbABY_R9XEBR}Y&p0V<`}R=M znJ-s?ux^x3dEVjIT+0Hy)*i&y5irVfz`#j{Z&Yn)ixoX*nY-gy&&BeCN2>1;6zgW{ zg0k@hIr{l}D46be=k%0GA05K`-^T~?^zb8^`BsP;32n(i!Q7;z559+h*{wxmfl0hr$Y?IMLxOiy(o2tRkdt>>Oy$ONmMO|#>F48cJY7Lt&lr0ehAM|HkmF|6hwf{ z_?Zn6wTavY=EM0AH}6T1yW-^-b2GuGs)`+#Bc~-wbjo7Je+Ni55~37JxZ{ydKfeC)ZLASvIGUop||%A zZ6hI+EBeq*MEy@>z4d>O+>6(2v}xB|p)D?GTN8TI)ha7wUidw9rQC*`k0nU;=NGfA zKCLCl(axWMsE?p(3yVxA+HFUbTEhQ?@qhO+Mo1l6%cNOLgMw$*?F@t@JA)1esNg&w z8!i0tvS~b_k9N&?$E)0YDLOdg;-7>gk8Kzs00U~+rd}+{6XXy9`4?B|DRy{5%9Ls8 z^&N94gb->wkxhePUMdp>cB=O$A&Cc$_FN67hJhr^h*PTp@uCIiZD z4MgRJYJ{=V3My)WDLYQ{YUE*>%$knNT00eIE??TJP1Udvg0HSrCt{tKC zAi2{1Q*te(p+ErGu6f>S|PL*%w*~EggF$m?`Ic> z1uTx+?TfX6!nFkW3vNU5>F!kyls~ksfZpDXH>imfV?vob{^Z8Oaf^!3mMU6YAwGyeug74;2Vlx&B8cEHV` zF*Ny}2dBT&B0@kQ;YZha2JwVq>~x2e6%PE41OH9PDrE;t)J`*>Gru-ls5awO>M~o4 zJlOqe_i_*#!w+r4(IH&(nemYaiDu8oVl-+0z<~cIcu3X~*w7*LLLqzcNyQ6AT~+1J z=n)&1F5M>KS1RWnXx4P0t?G(kw`-mNPDle2TKEIn?X4wj;DA#A!(&1bm^dl%?_crU zwjqb^RQ>-|SR{aev31jzDhdt19;iUB-tGOmZe~jVcI>-SEy0aWNW%uXQ5E$8z^QUD zTx(MGx8crdLpsDvJ=k}-gT#YG6_GPI<9+07bfK7Cf5!Rbo={9CFP<>`=8usl*3B}j zuwv77Z%Q6t35M}O+pK7fh@9>c@y(%k6ZdT#%>H%7vUmfmkLs2_w0#JDGrCFO09MvkNBN~vO`NCJZ#GzI!h0q<#QyX@p;1C117dhr%7sl& zQ~%y6s}C)inaghm_2f8K0Rmzo)l^4Kq!9(ctapLZG;oT4TdwJ&~E2*z!37Af5!qm)S^_)%4iq}iylDF${@|!i~G+m ztUtbuDWFcP-OMu(J`QcspYs&$()yKg9h*9GG+6N^^E-1c?#f*4_KQoKTF(|lYp|1q zT}r?2JMoh{$%cYRKF}H$q+ay<;4*z7gGbiE%->hj2!#llRapjne(_S}FwtyZqK(7) z#}iu!bHTed4ne>$7ewh0AeY=>LbZ%oM4!ZY*8UkAzxBs0w!*0NkcLM%FvK30l&z>K zQ*z+&lGdy<*h%!<>n-}VpPo7gs+zWZd)rA$6@(f4q-6t5OhvWZt(+Bkh?)se+m^pD zp%P`piDt$B9z9yA5!d~hi0i%FzneF9x&8OSZ{)0aLe!(An&+wl!2icVZr#DX{}THa zG9om1@z!+ni>AIi_Dz`4VDlzyR7b@O$U*ZVbn}-Sf_4aupF`GQbZZ!?opx>LkGXek zBxMuKhE1!CzTbql)zQK}ouJII+Nl!%eYpd+)N>c99a`J^WAMg}jd3N&^izJ2Fgk<- zFMjXWCVm3iI*hh{T?fMM4W+(5EW7Md(6miwbJ4DVTeYjOHRJo%M7VV*#{VZ#?sP3- zA3hVBZ#DUS;l+Z2rb(xVbs0@F+ct0&^usY{Pm-fZJEir{RRuj^7$N*N`&|BB-Awq) z4xi@QOX3wPB`&pKU$JF8ld7EQzq0fdwDWV&TKFFZzjZNZ>;#~=1m{EcIRs4Zo!kS6 zBJfQ80YCoF1mR9R=Rn5hgEjE18&C)gZ)&CgOLn+8da$da<|>%+P1^vPX{eNk5-5t^hGm~I*$!+dXkTO z^W|AU#<3}!!gZYnOx6zlb{eG0XU^4tbigyAOngh&%y(zlkX`55sVZ*Z7JHyAnLAOJcfy8gatTM^Gd8?1G*5dLEW@u( zdyZkg<_G`|mX2oYJ%u!g8cw{%#avxn*GVoqgbbj7cXFfT^?%bF`2(aeLeyRw5HslW zGQCBDQ2i8;bmmKy0FeT?m0zgf_$buf=uAtJ88)DHY4oxj3+m>9iYEB!S50YSrVI~rXrRA@$!(w~g=#{&`*o6CS^B0fw_MEt(rn>b4QkR&w z1bO7wRzsBd3Q)&i%C5|U`nU?+DeSVpd z@VIN1cJ)i2p{;CeQdQUv*#>H*L@r8E@4qMVO7~Exve5AMLmRRaSQ3*iwk~mKiw&BX zvism|dIyvm6Td)l{a(|T!q&dY(W7K{RwUKcV(S1p6Xj>-`8nZNgEgQfK!K3$*;>Me zN<0Nde|kbz10ubU>2uC|vr$s`E4YIZ70iXU_Pp7zSWCFIvoPPBf@s!4=pW`)o-Q?C z-AM%uzs7bDIKaA)6D@a9CifH1ZNngqWdK-zC;ooLRA>+y^mvWc?s8|epyl4zfg+or zjSyBCG(2kW?+vIYyH*hL02=#Ot=b(VMvn@XLeGWVFmuM`WWM(FD>_>Zio9rTJJX$7 ziw!XCKf|_z$PBo_l>$4N+SuS}f_Ith#+C?FT{-q7ajJO965yp@^GY`2Is>#lnQK>U z(CTitg!DewxE@M<>y*LfT3L+hUWR@PZQq23U&`WjetGQo zwFCqD`2bl!l21OCLBr{@?bc0UYeDVhMgf3s==qg;S^(@LRK2lKUJ;p58E%L@P}!&f zBemWQ)b;vRpV?23H~|H3kApS>+*vA%@U+94X8zM3umM2?oeOPTh09NNDjwUAUx3t- zl%(c2`K0&`{GR5vF+>>!h5sdho_@AWe2PX8kyEix?>Q!?@i&cG$TT_y?uk+oRqfUOwFkZa+zS+(TnRXp+jk(eIO< z1W(6&zWayUSiS9c?Wf;TyVsKeR7R*@aJ7Csvcb5;iXR}w{yK5m5N3R- zc@e6a2{izClAq;E9M&^}j~%vkz}P>hsCK8wsPeD*jE)2@o4$RZ{^pmBEpcwD#`Fnq zq8gjG{Wl@{EoLaVRq|ObXi@ACc0WnnS!@Pu1VGfWL;5vz(`_RkXtocaKOfKK7WcmO zYwrANdbOK&4U?PNnnjS#U_^hdoE$ko|BLQ?sRk>5UOq{2R+2Za<5c6eT5II>BBn9bcSt_f{ru^9!m+hnldMAKV;v zWaI9=EFbi>Sj$bZ=n~Gno{!$Se$yOV8uMV==gW3g57BGHxlOgCx9b(rSN4l@p<*<{ z#h2E0!%2ZPkEYyfdb5Efr{?uTy21kfJN6?qs)gzk`nQG;I=Kn1CEEP06 zlD;mKgxKls+jTE-d59! zqfXzML5mF9ZZQAgZRl`!6_G)YOSeGcj6vo9XR2_M9Zx7oXpzdl?^#GgiV~)=p0E); zy+6mBMuv0Ji#NSY3p*-SG}T6Rc2@K+#7*ffaCOMTgwvcRvaq|pVHb^l{7$67mgX2ozX>(adtNuYgtDyE0H4tJC|2nL$NvO9w`1ZttiMpF3YIQaZ-J zsFNXUu#hGma*Gb}N(!_Oe*?NVW9jptWz}^^y_V2ZjTL@(K7X+?&+7Xgc9Qd#Z?wjw z3s@cLeT!t{ptc7UKhI`=Z0Lgq}_OSoQkN$I?0;djSBdREI9 z{wi#xibUpW=+I{l?PEG>6t|+yoSeUYRf_7Sp4^bIFm0UBE^@4Cf4Gg* zkXtv#lShEd#9Dzb4xyR4}2)i6{qeGSF3Vd=M7K5T8&l-B$j8dM=qilga!)ZRj!>z zHBYL>FLbbXw9wkJPC||K2{2}q!((ag6xZm3bVXk2C{D+e%)EV{gF~V%LZGk@KojH z*k%9M?d!gQaeu5~P&iO~KjP1mnp7t*zE6Akrx-TwFxCg(TOjRyWq;WUd8qekrli$7 zYE!v0>&`TuM-kX(b!PA)T+ZjQ$MXS1JKKk01*QtovNxpKvT*HX@w z&xh$q_Az;&NaR}4g#~_aO7eOAk4bIi4J{&<{XKH7mt~ls{=(4`sA|1ESb!8dQM(Ap zlCPZ)1#j9-sf@O^ciTq^Za!(34PfMdcIj`W#byLPR$=bL z);i^~uZ8ZE;TErPzrM0k!o-*`KIfL8UgLwa*xX+?rZ%k z#YT-BU14o?UoXP?1|^^=L`b}5xQ$VHBU#MhL$F)Ln)*6&c;?&O)RLcy-thigpO-cd z#b49!Dn({~f7m!X-Z=m9&0@H>>9zl?8(@7zOn?-y>~Jj*k^lc4R{v}v9-LC#?N`tj z-MF$^+NmnP*)n>@m1sZ%bVSjOLWo%}xd_*yH_8?TUmpxVJ<2`oe5rU@>c8?6rOY_z zyV(D_MMXWfT5&y5_`BpCqjBxcYlkBSvGLK;Ngfl~J~0Ur#^xgWnDX_*Mamh2O?wLo zNMjtFU);iO^h5oVr-U0`TH#Zsd_k+eHxd;+G1P63Cw}Q@(b1k`{0>_{!&Nj@PQ5dd z?78ljudUISTXQP2*h{u>k4}Y5mToW;40!=U)#0s39t5awqO}r zx>l!-O?GrS(lLx1YhFK4#8DM<2yf(WsH0if5!NV?+$}EQsDf9fr;n_4n(VB1QkmEt zz*qoItE`)AejpJFVyxe~ZS=Rc&qWC<2T=B2`LJky3djm>3>iGv5N9_2KJXx%CV9@=nldGTU3=UaL!39rp*o^Re6Tx-$ zGkMb_m+@gz@G)#Tovu7iL%fRoe$`WX9+SP(3N3@F>&nY1E3DIZ=Dsiy$HZ(0>f5zJ zZ#ibM1 zCAu$hS37$P%lskIu$msQ%>GO@_pj5a`;AVt++!%Y`~sRudp9XM)QClgoT4IWH_B7o z>BE_Pha*76Hg8voQWdvNmzj*s1k=YeJhqjg&$zlDxCl4TcXCSF!C#?NP5JRgci>E; zoyJ?15ez>jIksi?+K$G*#}pACwUfWo@>JY8+EWyANM{x>wxt{nRB1gt)+J z|MfCo>AEyJM|;D@t4sGwD0 zN*YO3x1HEim%3DOI}=uZwkCY%{7=cd)V?Hn5S~7{COc>Ju>j42;bL%k%BWE?ivNAZ z@aU}_?SIkI=E$qucK2ZY!zB=H?EiwG0h{YVJI0tvOW8XK@?r#Wm~LK1cb_`OkM_X* zyv*CVC&gd2RUg5brXTb`^&{4HQJ=blnEO1P7l;HvS}bXe#@w3S-$Xp0W}~=0lYb#O zEIp?!DV{lJf)`%qC7EFKo|4W=7%RI~qi#2?XV{d?I(gkpNsE!^I{l&LgTecc2R!N4 zjbuHTaNGl#aQ*#pNkLs)UAv8y6}Ltqowt4CXky`r>9`tHO#1RAy`>zra7hv$BS1Y@ zknl}C!QeeB&l-~foj=n&r|lkKvRIFu6S%vDg zXUA-@mml&XjP>j(@r@1*$mP`~prK+Xm0Fp8{!H+%%%PO?~Sw|_Bcie%q;s(2xUB7~PR?Ol-DeasW z?DhF&Q+ee@$LtwCWW}PB{di5AdU?H|b6kVGrxPCttah}VlCa@{MWZ?9W&+VFl2i*c zmIP!%Wq(JGI}4K5$v#awu@?iRRaZc*{3mj|fkv`ugh!SO6*%B*a z$2UHHvCJBI^BcKy&xooWvp@l}$vcJ?=;N5=+Pa$wjOSog+_eh3;!D$Iqi;hw(ArYM z!D8w`v+3~$X4vv_iMWGk!M=l#P|Q`P`(kqI3m2}9-^v;lXfVPP>T5sf;OG|eFQz86 z)THaiq$e!gw{K(Nf#yb5#rtwX4McB{W59+c*f{vT13G<)#KS-n=fs%_B-raf#He<# z$?OV^cQx)jBd3bAMeG zf!J8thnhHSPdZAnS@@=ZI|##FLm|!=Qgy13Cl?Z^!wVJM_`- ztWE@B%gPuqJ;W6*V*h1%ewpeiyU%%<$5GKKI#2Zyqqz9@H5R44ZP#bFJ2ciT?|#U) z<%{7Re!o%y#e2tYr#ITRj5E5kdVMq~`JCaBFn<1slP%YmSjOGx_*E!9^7zB`5jC3w zSz7Cj{fOf?2J0sw>$dJ>qIUkMSE$0gvSeYZPTs$D5}(E%feykKQc{F!`tsTUVG8}xGC89ODPrcrJ*Q(^IV*b}(qKSbv@ zBuw@&AzV@5hBGuWTdD_s3CdD2te^$)3?=-v zqaA;+SjspbkZJDDJRKzxEZ$RRSkI+ zL@(n8Du^!@nb#&Y zPeMf2X{ez27LBUMpz`tjQL;$4ZP@rGX}&|ngFb3L*JTANS!sa(+xPNhS5nt~&DoQ2@j$>oPYREj+dnp{1EVJ2SJ ztrG;9mvr92-lIFK0W^AlYC!=Aca+#U?UC22AZC#x&k}?cmB*eSx*qCI!4s6OlU)$a zZTuQJQId9iKTXM~o#qi)v(4iX9^E;Sk#f2=HU z`J9vKPDfWT=v4icuY=5^Gj^0LVXV`T-P#p_m));@2G(JfdyYVVZiM@7qU#0eG8!@W zQQ9ZX?NVDIUddU6PB>X){z0{Bq>)aM5IcKXRf8tkJp?#I++=iJu=`}uV5uSvDa|G$ zJ&y4$dyO?CH#+VDns5A5^rPqdww|2A$@_p-FQ~$R_Fa@2$dJw70eRvqwQ&A$x8KJ3 zSyPKIz!%Ix-|#QKaDoZ7kfEa0uAQ)Iq8oukD{xK@-V{YRu)7{2rIdR<-;6Kg;upUn zFKi@hcgZuKi~k@k_MraF#>V#LcPc{q8KoL_Nx?C3(7n?2s?v^HTOyY$ilJajXWtBE z$oJ_`fMd>um0bs{uc#HUbbOoY9GBlzaB2}z)QYt;QeX5|*K1*kN#F?6bPy==n56Ku zeB0|U6@)6{#EWqCH9#v3F5Lli{xrdK8@^6UMQ`<*Srjh=g-ywSRZjmwtA*(y^yrX{ zLCnJfPw>68=((yoX2DmxDGGlTc8tGvgD6kL>a;Yao~*b$y2pAGlQ2T|DD=gURs!Bg63N zkzv;vd0^aM3qd^Pzna`>%d?k#;M7dPaJ!ud zIBffaJ*H51L4ow>?dfg|(1whPKbxb)y{cozxP#0H4mkbA^fQ?$`CjtP6V+H!1VKzc zH{!h*GX3LJHBlJ*!q_1F6GneG8O1N}L$`F!TJSbpqEOs!LC+9d0oTuc=z?*#m&`of zv+uFMwt$swX8MIUOP)GMM6Tosso4u@TRIEH1rVP^XLKC()6h$SAJf$q@}cp&7D3Q- zOm&G#(?80^#B;#l$cO2ox~m;H#>lV-Wz#gVs_bEP2bbn~wK5wjG%%Zo``TYMf;uFI zlVZWHi8Ge`Uv}(}11%{DS`)#3O<(O!(NGQ{>4a_vt1=chuz< zKNNhFbiGr|ZYHngKjCG9EiWV-x$JCj=rj`1)R#1;i>`lAWnI7i45R~F3PLK^4C8$(Gd4x} z+cEi|&nZW@0=<~O>h+(bc_768aGk3*R&TN;qvCeF(P8Vxc1dZKQ&mhT$!Ht$EpQUz z!ucId`A*8(rxK#E@08$y5clA1ZiC6_UA0KAzoWQlUc_|%`J>(4rD949y*fHYuaWgP zoPu2{S#nzFpD&q2>IsIsx@j)j#(ZwD?^iLF!=%NWCpp~66Mz{=f7p}UUJ`kL&CKN1iKx1BbXf*QGO6)HYkb zmxf;$zUU99LpUe?Na|viJ6msi^3*9mdROS3eLZaK2`gjM%lRP|o2eo#H*O^ivvuRQ zaT+f;znEFLlgXzc63Vvh^Q6l{j{;2UbegMM-P$7L;~!;P9PiIL(E-mPVM13Qn(qsM z8w4dh35jLSHDS4sy1%vTn(es2bW>#G(H3%*_aq{6^OUYcl8^__W_&SMLrH*K3 z*n>U~BQ2+fZE(nC0!9f>IvmP;I#U2vZLFAn?1>KU@ zCwMfoG}F?KvGhtQmx>@@yH8}mzB(qAw-tHg0-TbiMep!o1m*L&0?2yS8Ug}BE3zkb z5_*Mb-tk(xWKrysCUWLmPnoVf=WO!g&Zz-NI%$GN2Fci?1 z*);T;&eY5~`$)opr575~?M1ez8lOALcwalHMLkTX3I)%>%~{-t5YQOe4v+#RsY~k( zShUjhrD)_;FCiBE!Dw|Kp^h)+f*;>Whhl7hkk=~+HM{XOUV-yY`Ly78gzhS!N=zgF z+;$~gn)-(`cVB`Ovx$( zdf3C)pztZKW9Y$qCkHKBq?9r(&;A9kw=`MV@h7>>$TURWtCdRwuQZMoMd(2k*Cn2STh}>@dw#S98)7M&}J=da2iue zJ8w+>$JHl4Mx`GRwGel?rk{L^JM|Db$ZO=?QzBluHg)KZw`8<@W=sAdYLrMbC@5@@ z!l|-p9TqB)=getS&ES>tdEQOYoh$=>UM89`?C6N=6y+n6_ir=s%y!wfOmbp+u6|6Y zzv@o(`KUqOty2n@$~Enc9;pzfC2^j6!j3k1*z+jQ(vM?&@3m6NPz?45$XZAxL=^4T zV8q-jk-w;h9R)%9><{@3ly*OA^C%Q?}py;AnSh&d_-jNKj{2*j68UKDw@!aCIrtDz1Y)kzH5y14T@&gx}jFb$5El(;d4vf zs#2TWwN$LUMO|wH0?i*-l{|TlW?qiGnh^@u*q-vGLzs33{Xk8#*c872rWc)}63@B& z(Q%JG9c3^3`3BGAS3KBut9M+ycr78QYF0n>H*Rs~_YrLadS)sgi~KfH4@aU1*Iix) z%4~W=+@!2Up&S-Flc9RPl+%jvIKmNuBJC4LqH`Ka3K2)2QtvCC4^~!oB4SRQXJ<>a zT~B1^gX0A``j=vm^(v8_N0jA0#51F0-+;4K;N}!M#QTRksxujI&iycXPPv-QGaU%c z7bpFrMIn#pLWQ}55-ZQA5E%ba7ymFLb9I3!bZS3547&`xWU`2HUatYh+$zW#kgj4QV6bMn!r7HDKfcEu+>Gx&=u_dazP zkWmgEZ_0>E#)$^e5~Iz_ZpP-(p{Lu z@!-51I0d~!vvE@P>i$m#ALY)f=E7&jXj6>dWxkLVswG_dft;$^7?-m&xVpyX;4;sr zn{`c#P;!EJQ{krnZTU}zx1Q;*@(#L)C40V!;e}s_;I1_ga2gW*jY}(=k=WJWHT>z} ziH18Wm5!X$_5Rw%?G!NseIK~EnLOj{QRUmxWePJ#{aH=2`4!-!S<5`-&G*%HE13>n zY61NFO7FMZgO20J96Q~SsVchrI4|&;Q8%7|`zXLk=kcR-h}{1*ueCYoBD=ReXGrhH zKX}M@c04jTMY5my)Rdo6^hZg{F8E1|EIyGi-94tPq(0=u)zptN&LeziAk~-Xrod06 zF;mX0b$P>M4i9_5mky^K_hPbll$rfFI@N8j%MVlN<$=i&7+;Q-39CjJKTmlJ6z5iMqy{-OIu#qMxF@UxRM#%sf{u(xqIWJxHBhu&ZlJWldxi zh&(d@5DXI7P3Z=0q6$YkM8e9Bm}QJ$ww}P46^t@E7mAsg{JRt~Xw*jBD$jTVvQfOy zKr3ver=+=vgeo=KkCT_7Uea(Bw7#NuQn+L)03$fg~71Gbm5|IvclVfULj+K4a z%w_p_AS-)B_vez-Z$_U28JQ#Jf zQRSSmgLq^7;%~g?hG>~KefJE0Q*Kao02lbISumldN5Q2YL*TBJv*2Q(N(Uec&p?pX zyuqCk>vl>NUCaqTs@3l8BPTfx8m8hF+jNU7X`&zO$xqf7v2DRdG^%v>kBeSDm?lnx zNlX6R_33_RPNxq;P_nySG181pVZix0(^=6lZA~eToS@fv3q#6{nYL32QFi8%dS5QU zwhrjE9cStFr7(KOmx<-Kmi0*_VU zIC)9E!K;UlEjg^ts#QV8>76PgqJEMyO&rm*Onm~bi8+Nc_LT*yL64I-V>fVp$YbCM z13k5b7qn};@PQjWFM7_TTQL6+@vX*$2|~XGc~*uhNc7U6u4&w_-c@z8sNs3QAha%~ zSN4$kvia7qpxsw5^H^^&#gnm{eoS7DZl5bJ9KDLV;lQ2V#Z$cWx*v{@3|20vG&-2T zlNWWfzpJd*xPNquL<8Nr8vi=}I*jSVBTZ{er|kfHCQSA3@Y?z$Zc$W~&6@>j1N7>i z+&@s9y6vTm_0#G|T+X>l;}})tS_GDKJjmff#VGT1GV64*!bD)#dxn2{8%2M>QLTS(iVYUr4kQbXluIQ1K{aphVn9{zt!c_8L^T85^bAn#U;LBY ztNIj<>qMg1Oy(n-dzX&iP_-vMu~K;$#g$En#Kd27b)i$?t4i>u@us75MQ}Ad(zQpX z@1}dtDmd`Hoasi95fA3{oTz7`UDnxy@ra0z+BRFqQ~YP|D6eF>W3?P}%S-{RTV=|< zO!LUXw|Kcl^OROrreY`tTpROBAi@TF@ed}9Dz_UPFkL&#?j4MJ#FY$AYW)bNedVV| zuypa*6=GddiJIaojZtd1=q8i9jmh@TomZxwRWRq82OX&>&vrnnf0jgUMU9JZy&5v; za5PnVl{j`xeZr>khVKEpj(0Qq^uqK{oNMc`T5X&!8T!RlO2 zhooC-#E2Pm73~4dK#b*)XVbIs{axC>JyJAFESt=3(>1U=qSu{Wb>dz>Tox@zzHN1% zBRZB>S=MgUQH|g~bIz$ZS*PN$1hmR{M_SOPQYfd(olx&|*!^%noXO7m2kM-y{SbxP zw_D8SZ2sbMQVuw)0VLdi7A1XWz$2yKl$+<4=QDYtsvbNnu);nVe{rR``&GYrAklmW za8)=3wNc8%luS&HnnyM?(Rd!}h)U~u?q=>(nWUHt(HS~bhRU;G`sRmDNwCoi3vGkg zTXOsIMt((Ey0BPKSEy1Dn1xGYDe`$G`6UwIj4M+dvU?O}r*%Mf|8I zIN9<8i2K=;1r@(w$P{x|(_TL_LGOH*?W2ZxZ09)h*E8 zIi8i{y(Lo_yr`3-miekkzKn(Ke$@ibyp}&JGQvYhE5M5|n#EnTIOTs7j;!BvO@8?` zpRv7x1y_YHbQ?}{YfR##1Y@y6gc17@MrHB7{UClCf~$GJMNzF+nNT|a;VeVQHe~+$ zr!a1B2bkObDPYmp5(-+Smdlu{Jg*g86hO$1Upk&at?#|&8K2_G>aFfFJJzL7%4_92 zh}A01ey8hTXKG&1Z+xnLbyH!W!i-%fo<;!*>84(m!u!Hhau#t~T92*>>h7tpEOZZg zS8ZGn^-9S2d=fKT;-d4}v#!PNOkCw{N6ZNU%2-lWzFBAXsD88g4;Vv3ZcP8049GTO+pjog7JsJUQVgHe_Y64uHTW?-~cZRljw#|Oa956v7^oT2MK`1dWq z+K{odT-|<;&)>@V=hz{?8S8cAdxv?s$KKuL*RIKr6-bmT9xqp2X@2$joD$)td~I6V zhi-&JD4ZtbW0}5yX^Bmpmz1;DKypJ{*kz^T?Xm~jva4JX%M0AXeS9n}O|9v@4<$yc zbVQAF7J0PjodfyYO7=n;?H?bo3N^2kajx(r#7Z1cxWqs?H8LAh2cTh2{UrLTmpk%Q zA>?^FF+Dx%a?8*Uz6ablfu%vSaXh5SH+H9y2kb)O4-iw;$GLOiWG{L_CBJD-y<@y> z$INr1E~d%GUvz!)#e+^o;4|XD1t$JGcldz@$H%*?Ue;{y^aST>Ci;~U{^=Wn zB8H9VRFtmIc!{k%V;+64@8~G7F*WGIdeg^vLZ42B>s@n8In6vL4wU1^kPc#5T&LK- zk9IhE-s>u?WDIO->tr$1nHE)pI3BeWToW!D&4BkR^Qh%f5U~6UuY0b~?x3E)RX>ZdiI zcxxrVf5}x$KasfQaET8w`w?Ut?YYhsJftDM^ynWFRd?E z4RMU&jI%b3;Z>-PfzhMqBoY;`vc5J&x9=*$UjTl6_P<+7R393YlHr=P?Q#UWAxBZR zw8&%eK~CWwOIQP0K5%u>pNa7;1Pr0kOtbcM{S+{4(ChrO{N8m9k-GI%SWDd5sZA1) zFjb16Jlk*yTz}s8lDW!Dog%GHd2MJfI&Ko=fSXlNx*8;$bzkv9ot;QWr?DFcU(EBN z$_w)?4!M_Da?a|tR|*JWV?VpL$*8WfkMYuqdDRFZ=xzPvnbD(7tnl338>{Eq zGamu4{1;Yu8whqy%^;0$9;`o{DHZWBqQGP6Sb0R#LrjHQCqF>;2B?R*Txr7Cwv3yS zedSu=MVU@?o|Mf~Ii1teD;>PFl^#Rck;v6?{r1(cqy?#&yu3+&brwNM{1D`tuE|p< zn*wj}67(ud71umUiOm1cg^6(KZ9ahGhZ{OC&h#X*{pRjRYR?{(Eth`XKGAlCI1p_O`hkIUaAdLN$GT7FtC77B+)>~db+}`3f z!SiNXF6Po^ipT23?5E$cVm@K;X`e9fXT|iY2J}bU0;EJu8;>^L;hZY__S6I_|5z9? zSMO9QSM;8ftVP=*^{V7PtzcZ|d5NA$Re!{C#f9HSJIN=tbV^w*(4ZRf;K3}7^QxtD zhLGzkq8AOn&E#A*Hr!3w3$zmd%?N;6X>n9eQxI)K9stb!?4OPX?F!cRXe(n2QikW8 zHXiNJ#b%-j`i`N2Z)Q%T2mlzPoXci&3vLZh#V>QU7U@lY;^Y&VdHJAi=p-kl!?952 zG*>R<`$R}3M{}bH6Y3#EXkB`G?D`OESmKoF)iJ-N53PqVi#!)=xL?e7R?4sWRzK)fdF8~{-VZnW2FSHAOD^Ez5-5a?3|7I-!oEQ?+!muIK!T6 zVwsrosNB;9t@3dPN^o_oeKSyjgMl@l1oc0*pIdP01en5=FJxyQ+cW6VTOZ*fNZ*{*r882X=is&Q%yYzt2hd>#Ri0z1 z-W}|QB1c=M@QjvZ_t)*90sGgO#7~2e%KEXnekq}5%hpGZ>!(uBT1A$POX@#;s3#zi z`}#97_mVKmvZ<+TZLEPIzYAD|rUAwY+yVu|3|?b6>GDu7r*j~s^FD>AsTC6$dK=Hp zH1_~V$L7>thamyPvu%0YTGCErRS^>YOy_k?ETfYpvbNSyqVtwjZw%6(f^YWh5P1$- zihqquT6jXz6-c=EJ-F2ADOlM*A2rHMy3q9LIQMUNO_)fcdQZyXc!5VcPu2Uq0f!oR zt4lP0u~|P-?#+}vp8Uwr|JkQ=au{NocIK;wTR5+2j#WA}g@NOU>WGmEsdlvPszzsm zg)?KW%jJC)sQda@jjtG^}_+7r= z@BR5i_wV0&^yq!w*ZaC&uj{#9Q2=O6I#H5jA)ZbmL7Runpp?D?p#{g`DgPIuQYmC| z=-}(kYNPXA*ni@A_?zcm2zAzQT|37{8YMR;FLpS^nN=HwFTDq6;I7&9mdFg>cV370 zNMm3wFQuR*eV6a3Yria?qa<;igT(2wS+tF(Bsmc5IPzGV!0oR*cVkxkiVfvx{QhWf zP^-e1HV)5Ta&s`H&+iQWCeWvx*kvH}*ZO=jzqL5vnNC$rTbHuB&%A+`hHN~S zmE2+UP@Y@ScOI6{G6zeu(o1;eM;g^mi*4SQRn52~DrQFy?_BAUf^Ke_+DdsUsco@_ z!C|DKpiSgizDY~4WRe=j2#4rcCrzo+1R25EcA0N>O$`MN=|5g>wR)+s3?u`m<*gvn z#ZhEl_TfE)NuAp3Ae8l_cChK(1Oeg94K3)oA`u1ty}?MDXzSLb%!-Ge=WC~(#vlit z)0Va6ph*-&Jhb+g52lU&>Ujy-vQg5yCKb_w-BPTs)VG%Fm!P7?N0o)R-A zs9sPD4Un~-nsHpwjaRM*jL76@QM4wD(zLn7^>NK#Y=rFKouwNQ`jf4euUD(2&$Y=D z)iS2g7oZ4yl4B@!$1%l|=tZ&vd{gKTR*h(-E-`3= zjy2K0H9@w9Wq@~M4o4OdyVY^svRt-J4E+#9W@;5BWCW_*Lo-ioq1WKMPW zcqu3mjqp?8GnIQfBnypwJ+d7KmxIrh^8LknSH{Ef_2tUJk;+&~2gJrgQ|sDWuPCXt z;a#>r&lq7Mg&sn&e|?}EVtJ78!i#@zNy!)m;VuWIO^bc`D8qW|CwHP{`{$L0xiD&W z!G@1fA(5zF!^&^%;L8fXWb=U;JIF$^RjpG@iRxaizcS`K|J-!vFSc=rB#K z)2TYVm}IVb@UBL-Zj#wCMDM*OCx=6xY^=>BfCFEO*laMDSVV{H4%>G6VLnwNd_8<( z53YHHCx!#S--|WPVVmbF^^}lFAt3rdG}EbwJayyd*4U6ASGWN+azp|!na~tYTM*i>?x=| z;1?^`B=nL6v)99dkupi<>*YYVX#cu|&DLPd1hS&cKc9!jVwHNdkm2G){mWD5Y(jq0 z_p3_CyzX?KEt&FXxWpv4%4&-3Yn<%q(ZbI-U^0n!1cOpxQd54?j5%eAW?y7(@AfQa z6F68;C8@AlxCc=QD(E|fL)@roLp5yKfv*l5^6s30L$i0d<;4k~p3{=iUwJJ!`s)Sy zAUgfksV=}KKhbJ!LTvp-rBF+TT-tXVhtJ&2tSoot^2}kJgcFgdfyId(;>W`iADnGA z=cq>Ps+eSe$8V>Qy8^;NgAEYqZY?g2*@b@@ix*#Yb!1cO7_wkiicp2qjxCB&QoG_b zJLvq4IuSK zirV}(Yk-TZX$qEpx{TVBS{F(i-mP`&8_OA*tGW98Z-(rg`J`rc&ch||EnC1VhAoQU z((v6NA9bNvqRy#}*|q5zrf}I?6Bpo*>(mSC>f9{rwEvg3ouaJr%)dO=Cj}A(E!$sT z+}_K*nhA3)dhjs6d>OE5(NQ8yL8$G8{DfFcJ35^<3*37g8;E_JWX=chTN`)SvAD7Q zbQL{JxXjfgi*tHyCpDEW9`1};)zdXo)ymBel{sS`EXRTK69||3(i2Y2u}U;Z!N6~` z6IOPej0`IsG1TTm!bI>i!S>qf-(MpWfcyFMMZ>AAlJH-jm`Fj30MSKrVIZC+(Xm;_ z&Wugc{ob#P>CMCd9teML&t&-7m7G{yzf7X_1Ky&7EuD{9yK9z@J|w2*fj#-a?<3Uc zdgaEP-Q?fCTMJFWyA?L>*H{#GxSjdcQd5SAxTIZ_!z3;-N%$gG7d*OVhMMIb8xdp zE5_!yF=JZhiJPxvFDm8c3JtPti3`84GFz}blj6qAQ02;;frctEDAIB(Xb=T;6btSa zUwrasxt4DRA+&-_49B?AiR*U&nFSfS3cuCz+xQPM$b1QK_g;fV127~q@8;PLK>D+| z6nMzSkNSd9waxcVyMjhwFNt9Z92j6&ag*4&`_!0DG0J&)Y7Vrx)oz%+NU)+0OES=c z4r)PSpll3~Eu|=9igWE#23{|Nnf|Pg)h#gr_5ALCJQZLD-+#B`-EN0oijG$ZhNYXM zrdb?iM~I6Qg;!VXDh$Kq1G=`$jXIJYeq75$V26-tA&Q(HM!j9GKmvpyP*@x*&|$at z#xG>3zLn4-pqpy3BMCoC`I(}tMZL|B-&FapfhgM`B*=(A4Usy8uv2+A3j^mK7%@_H z+`Il0xb?&XWnkqXReYA2zTjp%K0UoeGdk`e{mlABH(ONMX6_XS)S`P4d1P*J(PsK( zu!M8Ena`H$3o9wH362;Hm2To4{BxrHiAVegLUz)v_nCff(^50rhEH-dPN-*%x0LhZ# zAJWY?q!Zn~AHm-|0Rb1kJ2rN6orp!tfo3d?^`RdDtU;_v3w+o6OZn-#4UesjWz!3@ zVycI!+UyvAQX;`QzoDz@yWuT*#%6j$YJpN^jly$@J}5??(k!;}sOcLsB2qfar-oLn z99$&+P{Mp411an`BwBElcBfW^-y=4iN4q(8-uebKf&Jg zj=-Y+g0;3Fvgz+3yWUG#R@POm%oQH4PnL~bc9Q%z#Tl)2MKN>gh-|;u7N02EvJMor zg~q3>iXd`Njz1cxT4DwM{JUrWd>fLId5Z26V(ps7#~uY=BUcZ42@m~s!?mJHs^|nS zmmNca=`;!VW_x3BftgUeaOP`Hyn>jtVy5|dL$fXV#?(*7g^t28>YNDsIz7f)bG29uNL};O|o(#6|iP?C^wzV8igl*YnfSEkL@ra15%l!>uOeeIk$< z-tSDKaS% z1LdM+q9dk7H_rr1b<3QWMPAoQb3F`kTsi;*Tbh(zj(y6P6l@LoC1nqhw9d1qss6QI7e3SD3_B zaW6YNDU^1k+^SHE2ZyB?rN}dmJIWxs-8GX+#063TvgdQYGxYlX+f$j9?GpdnQ@v@n z=nR`HgcihQ%$KHbY$?GO%jr@h=ton(Gu%uI@^B<{QTxRv>X1Y@+M+6qNxDw}-F1I^pMGE?$P>{rqt?3Pog-Ouk z5Lpg0KNHC$ZzM}FJRm?ZU~511TA^T#5hWTZw=pW1>k&z|yHT{2XV)CewwJ(U49Wo@ zSccOAIRt?9O{+v9$BCUGL@l^4CvJNE|6@_P4sDT5hIgBMPPsb5a%rT*22_*9u;CYU zvUh~)U(M=f?PkKn)eGoMVSi&BdVEq19tjl;*;Vo5XS8pHNMBzAi4z=@^ta=OU)VQ| zT}ut|q01D^WQ#-eQPPp2EpN(E)8YnttE_yJiLxs)6*T^YZ0{0!o>#-n1KcqH41B2h zkI;4ZPB1&~qlK{`BQqJP>w{2xA;WQdysu#Ot4XsNr;Dt%J%eYv+|q+Hro{*xSo!We zPy=z*`FHa4izHe3q0tD32U-MU0uU$%LG#|(W@c0ROm$SguGAxj&Xlv?{DMc00B#d* z>CW6iK^aZUTus;^slrc(EOVPZL5ys&hhiA7^Mhed^{-Eyp$8qn?Y3CRl@+ZoMhcW~ zZ&X-$&g5@NIaMIK1Ql$@^Nz7cm5f=Nxidb~td5n`7@Xb2bzc%Gzbs8~pgz$}s}$b5 zvxXk&TEzGDe*5uyf?x?DV)AolUEXA&<=;EY4DXuV(YtI|wqBHCo^ ztBg^}NB3eePX_w`Jmj=nKr2PD0Iu;T%ie33GAyB0B>l_cp)w=Q_vv7l2F0|0dXE3d z_iJK;`opzuBPI#As2gQw+J(>f26Mpu;^Dztr6R@*LuL#T(n)6HHbd2k3Q?-u5mBDq z%AneYS$$M|tWx$ToGnVA5dN>rhG>bQ)I0iIItB~E%-wgtBaoFwfN>1-S<7*&ET8yt zeDr$q^vB6!DEi|-|BdaH&(A<$oA?Y;w8|0uVhS2^`cJKMk|=d!-Lg^6Z~ca=N?N?5 zkoPhJasOtXcth;z_@x8n^dCw<{Yuvl$7a@`>i2KmJne58Q>jji^O~jno~K zg|*-L{v2lJx0F}5gmT8j;(DL&4WY<6Z4#NC6n*%(CQ|?C4zdUpzP^3B1KeDvcRFcv zj5s9*2)+5UNAytLu^K6>G*2Ja%IG$JRHwsC*wxdGyo`z;NFxTtuj$?s2pJ@Uw2CN7 zMr=R-buUPVcRUw5aQbSDPux@~_=&>l^cp&W2ZSzB3 zP;^T9qEw@oTvTV1$FOOzh3%))WK32lMslPSTM#LEfC7)-Ee!o9xCuLUOO{|iXndR=U zUP6MWo>F(n%8sEdjYM^IxyX-x7DsjF?i3BNL}B$d8jT@R5krzF+nm$Vlt5wyy(lR# zWe+~9cCw~W#$NTyqpS8Wm|Ps=gPv5W03j| z4RL^Hgu)gUWu!Fc!tt38Z1pFThDPk%!}N>1qg0MRP=vzxmUa(fdBgtk`^2FH-;Mp0 zV4i^?_|GTb$a=8VAx2m$(fhpDZ8K~S_Rr9rVP3-c68q~cWDxj~}hugj#`9W=ywGZs+!2xV1R0zO~%DF?4%Z;ZL%@5!y4TQM^z~ zk(0Zp>cZjIr(CMa#?GRf1wDqZRr8!X||bkI0#$7^0FT3Ux(UWEx8L+7Hf1xSx9~@AOxr zv}IMjPQyoqgo=QB220NgbRHepY#pDCnSD@_RmN90EZv8hRtN2=S?N`xf;HBP7J1@Q zGwbq9zmq#K9JW|03s9e(*@vLd9Da%7FeAY3IGeYqnzOqbz2qIKQv{(czw_`0^pChl zO_}y`MgQpJI%2PqXgjCC@)#WL%pI%Lm^xy#lVehQUaFZIX1FFndC-Wi>A8rX z@@BqkCaLc4OfTtz0v8z<39jFBMeP)iLcgvRx8~E=Ny0*hx}k!FYS6oxdWR!g_waQ3 zvYj-cvy>=ca%mO*N5|< zZ%jc&sv38A8ox$A@u|3VUg}V+=C#Gj0KELAT?>Y;AQIxGe_tmLug^P3Z;)Lg_O4DUQ%(2 z2|w*Kl>Q8cR*k4K9RM)*?g`3Egj9i2NJdpgVp50@z#exW9y<3y31o7r?$@TKUFbYl zJZsal)E$fiFB7JuJ4k75{5cBxx)b!uRkD__C^Vz=?D~S13&`3@UiP>DS zW%!CtIpFC}*u`^fv0HsiPLB!Sw9NSHOV{Zq%liyI$b;i_w$C)G(X&Y zg}3u+30*H_uLH+~=%j2p#f~_D(u}9)^5v!*V`CzDhOQdO2tPhPNCUWaRWs3xSBm96 z-4^78YeyE-(%^Lu@fIa&wO(b2g%&w?ghh-rrSI0T+Y35QEzOk}vf(bb(xvWkVGG6k z85tSiLHte#s+%s9q3#k(E&X}yGbx_u-MlMd?Y>V{PSXS+QJs;J8rCNd1R5UmbU%y@ z!b!cl4SLCA9YwO#)v+9O%=x%nUiB_N<)bHn3OinAHCCcpM zISS$K7mkZsD^`0m?p?;iK}$KCZH-A@HQXxD7q)N2&(q9{&x-PzQpN!HyRG$q12O6u z?n-!&d(Oo<<(#+{`K(;Vd?}3=G&b-=W@+W+-&po}m~jW9*bv>aS<|+8IS3QoogJe~ z6K}AU0f~kc(W_{!2Ac$)K`P++ySzc0>nBSRHeBS~OsHjV$B0O<&3o}#-Ll6MAC-^^ zi~64h_xSIE%QOD^pIM!{l2Rv_!_|Q+tTNmSqHQj17go*oE)CnoiiAK{tyL-kNF5{3RF1SemmZ9a=&}QIryN_{q_kEZWkSec1I*<9lwMTfg@%UZOZf zkR_eBlRE3WIQznD2&S%s$a#2#KSW=d?750}lK$>4{Mhn#vJ6uNVRm`?DNgTG(?XU? zVAp!ilE!c=SkdL}etx=Ez9(xsATq$R8E+C+Y0|%xF?fV|Ju>k{|Mk0Dl1X=1l4N?} z{@OFdBWAZ|oMUipWPeG@*-I(F=wy33q$%|wFT#MtW#ueHD`^AlC%4re*^&ph(+&epnO}jQz9PfT*(_YsL@0Q0hIj|49wvTg#0V~{9?QgZ z*U#Vo4IO7pqofDwVCaR;iXze`LISoEDJ0^=G}b--B*wAauQ)C zsOxJdH3}@NGZFc2Biep8Q~}l`AE{}9+jc?h-XrJZSRS-^aHh+iCF;pdPwRVGg22;_ z8bfRJ7%E16-KO34sWpXevgiu07on;tw??HKpA9W>1Pw`KDz^5JyIt7co4Xjqb6w+Q z_|42U=|&>N#*jSCYHn?e(XRL^u>vquvHX)`@_NF;mSvOx6tlz=#Y`Sm__-*C1r{e> zo3r$yl3>l9&^`aB9HBEutaxqVDzXo^UZTlsBreG>V-3J6rP}t+8Rnj1T5M4idNQ?zI?S#7C9mSea5!T(IU=|cV)eRjJ?{eUVq-~`dM`h?0e`G3TQ zr@kV|{A$Wn*x6THj%IncU(~{MiYP5?Sv8W#>Jp;G-#e)l8V0EbtfkZUz8vmy=ZdDW3yW?N@l@*ke9ug{N>k50Nn=|VedB%SAS`z!axZxjx> z;)?)59M=9*51ZUsMl(=P8RR*vBX5&UP?5@tX*nY?dQbiHK6ihc`ug8MlxX#0ldS%7 z<1A8F3WEPd;b?XJ@ zuuc_QCf_w3p21z9>#aZYOGpPiQ+K|{{kTTXjOL6Vf5mC-P0dsL~7 z+dCE~GRLI**c*kV*uXU-9rATM{_-|irTF0BcvCJOArSD2q_>*&@hcpqmxrI-bZ&&FE z-QaInKYzAto(O8RQ3qpR>%+u#_#xfRr7P?5(({9S^X5LzkFG{k) zb^VeKiu1u5rbRJkrD47W$?h1*Z!>eoP~pW3OOY82;{fX!YLjRD&~Ou%x3DmBvsjWJ zsQrEuF#4A_BmdkW`>mmv#svq*$0B(~d|+Fr$~K^~D+8$Aon-iQJ1aaN+v<*K@C!E$ z&&&>IPG-b%X+XJ@*>YoX>H_tL-vgUhy}*)mtqy!5`S|u+aUD|95aDZ_&?gl)h9%rJ%6msTP8}pfHZp)w$}p!B-UCI zbRWofW;=4hg{ir|`}ONtk2(q!4L);C)KXocG$OB$ge8?Vjfc~AW8XU_T=^VylOn;9 zfaMlkw;oEeg4gi(k1NiIr6YpB;oP2_4I21FyN`z_S}29Up7z&(>@A%x*#fPe%HsG) zb(^np-^x5c3X^P$>osJycLv}e&}Z328$B(c=cvUA%dO@lB4a!E8ewPkE}kPl9FccSkw{d~_`w`)1R;AuPL#obmQS=q;i{UQ~7 zwNT1h_(!|k5uDu1t~yy3g^?+->@n=a$zO?+QSb$Dun7H^ zn_K-6%07LZ$aeB~8(Z+Bd0pHDAC?))U~KO{X0wcfus@l8KrAyWqOiImaBJ0up5H=~ zXZeA$^pzU^!&L#UXt4p=3MsRa_NXv9&zbu5n29wHW|CGs-7v#JpmM%M(ZCje+3rD8 z;+=L!Oe|f6C$nPXQ<~?}P&7O!GnzPA-zbt$fQOIW-)w~_lyF9>#7N?@E) zuDLC2rIU{OjWy{G2Tl#X5LsQ0oeZAqSP|1HTBdaazsZ-q?P@CV{iPxC63 z>k@P5D}i@}^GL3ReZ5@rgC(;IbM6wckP1c>Ey6AzyFW1|bTs(cDo-P_nYVt$vCfD= ztyJ(5>+MqY;y7!KGIa+_Z4PzuP<=t4`JVlqL1TWMS34>}VuZfus3^RAyQI$*2)!Uu zn=H}p@2=jxj=E5K9jVK8-m)#F6Cg96PQZ6Y6rM51nuf$<$Eo2zda+N89I-CgA^^j{ zC$$`|MZ^HvC8MvflC%EgyR*%azur(pKlNfwhw?x4<3ZsRQoY|zPcWMn2&za;RqF`m zzS-8urteJDD3grZEnhwgZTrD3b@est_WCS$Z%_QwqKR9pi$8*5zwy97k+IQ!;4GDL z;C(5^QD1ZKd>RBZs33Hk$Yp6)3>t;WFy3n1n~5A*XoRpW#zXLhu1wI)G(PKkm4}Ro zf;Q>A1>Y<#dwls~yb@eV|2(ql=2t)Gg{io6rEOfgi6mw)whZmm+_Rthd^dsO`M0Ej zEihKyE>Wk<;MlwTUmuCFzhVcCbsuiXtox==kyT$^(oflRWKeW-1}&$jIFD^lPh_)g zZ7Ng{L>zUhphp7i1|F10Zmjt;7ve{9h3#z=2{1>4+Wg2@J*+&B0=>?}xhdynDtx0u zr6n_ZrFmw^l)|jW>^vY`akd}t@}9RGna_>K^JQd7Lz~b#ToRN)R<-NS#b5lcLmxy^ zObo3>p)0w0`W@EUmluhJ`#2WK$M0IfT-l>?RN>iV!LXwiRniQiW6^y04dG_Z-KENt zlyIYRnVZcaxUi)HRNoMUYyR(~3-iyPnI%2$)gYNF++f0DHUQY#IlEj=;o3K$Nt1X~ z=!JBAIpgJ)0)mbFkRw`_)RQpW5wHfbEv~tu+vc6$JX`7d=HhoaIBy{4b1 zwks80QG6K8Q9U*yzxaHj6OTjX=Xi<7`b$X21^f;HQLN|TxxgP)sr1-G3Y&52$Yi>}_7)-kv-tUu3 zjj`O51F2J_&w5HeR25C=@n`vdIcSR;!_!^1US&fBiyzoXBUf%{@sX^DQpG+Z3BMr$ z-N`SVD-ikHl#c5mgsX-1xDjHmro`snT_e-PTIW|w&2AC)MY1Tk5GG6M%dj+3E0qww zh_aBh8IdnAC|pX(f9pd%Iqe9t@-@IY`rpX(BZvZ1HZl~jfC`tLh01S`X0hwP|v z>ajw1h>+b5&j?#^g1;wTt!{>5NUj&OP@KZXKCd*#=sz8H(#i1T1!4wj7*CUP4Td72 z_(Eg3y`oRwRB8~?F2YgDkvY;yM`ZIJxC1YXs3ndX*v0`jxA+d+W}STS;5$6gEF7QF z5SQb|Ikts{xT|%NCX{uG>7G*TS z$p9e13@FC&Z(vq0Bi=~b8y_-n@oq*(%(dFGxt8(7jiCC{lC)RSvaB0uFj6#I86DY5 zv;;vhIm8Zsr=1GM!cCJZQ04sF>IJixoXMuj*EG)Y7gEjGmj5~!$?0-vXzu&&mf&PX z&NC#am)uF25Z(7gLv~F4J}lPosF8^sben*+vCFXGTGp=5{^714%blO=o$5Wq2l&9| z*|s1wB=nvrMzOb<$6@8g#a$a`jzCDU7r`1=x+`y36Zh{AcR&dC)njZi{mRtcb|tqR==~-#p!vT)22VZ zM5pT`Zv#KFp(mYZ1Q6&r3X@0`jS9D1yil@v0BCaNa6?*UyPb@o;}~ zyC0)mrWNiT_6aP$D_xm8SH^;#@%02HKBJ}-%TewUdu}S4$)Kj`8AHozRr0~(BJX9) z(8Mh?*@guz?U;bT)gUduz}|!h$3Xzr#0XekcA|DdQBaq^quo=*13#{o)X8#)U&N3l zv$ebLs9}$~b*j_TVD9OHI=rZZbmCxYly&UH{kz&vDJrrP;>s_or_;z7o46oOo+;TRuiKOC}dzsP16v z<1<`%q-a$sn{>-sjDN5HfLXhwHL+-MXqj_sd2v^aW+PBp+Ss%38#+s`yr+Vcfg#a& z+j$$Ax69tD=}n-p@PivrUN=*1Sl@hU z6g1w~9JLhpnNRFF!fO+F)u)JSlT< zsdhy(r+>F+8P@huN!IHa1J9}(Yb6va{CmBVo*?ub{%C@tD^g`uomipaa*{=RphEf} zo9Tw!=KU%67iHqnO&&XLIYakFS`!TD@`g0&Fhf3yY`Kq+8BSlsrBsuD$}JW*ejI5a z$eLx}{v1dhw;H6k#jC!0IZ7NL%ZqDjrd|H(X<5_m=!^yCm~PgwZcFCd5cQq2Ee?*^ z#feX8Dtmg%SDZD31y$Ee&@|)w^~IR|h}3bel5h(Dc$dY?4N#B$sW1_#)Uvgao3gRq zKPE;UHoZSgm$!>#GCx!%9~3u3MU8B#wYhF8c9`0axrm*#mp&xJPuR{nE4u=DgTn6> z>BYDOhm1}8+y3l2-A7+!eTC|$zVZd9(cOA7dm|Na8@1E4wFDo!%qgE+iq$t(ktj+XJr3G0DXmh0|_x zlO5PoH4!cM?@6AQ)^xC>pYZX&tC(9I%yQco@8jK^KDlxU4O<6jn|W#5E3y)cs5b6I zFi=g{Yy=TJmzG1>!4#_v7$@2Y0y%IWyN=Rc!Ktu@sJF1kttT z3V1R)Y_R1X3-j>$fvS)6#yg_r;{fLKSO1w)GfwA;-{TCy*aG?Pe~eFtuhvBmghDc| zlUm=4h>4(}iEkKW@EJ$jbP;^eQF42=J!t{L-<+tO?iCtB?_L(ar--U}Dl8_9{_4MS z7Qt!Dsh;5~T{n8>pmIWC6~2+=CfeloNq>M%aNsdbJSVL|=(1(pYjzJ?-^12zbD3Pa z&`@~+MH4hGMvP7ff>G^x(HfrgOg3%XROZdFU#=GvcR~8O^c2yVE`ztZc2!yilww*f zEYWipbn4aCv`Ks%sQV6ACFr&@rMR*1Z3=9H?Khtaoiy~!JgqZ zNEKv$(Y4^HfU^7_?-1_7>_isiFK3PLVm1=2wEMpBn?%i2sT%(V@xsra(X(uyW*qbR zx?Nh|$?Ui1cexZ-R6}7wJgsa~ZYGX@3nk?Fr#VZemA){Zty0Q=_goisZ)%N z-pZAP;sm^}RobvN0|TeNjnp7ZxaDZs6t)&Rr)M$tVW{%a)(BStkneyU@-VQ` zeXqGb>cpeu29KnPQ!jki-;tjw?*dIuG}D|kvc0$g3dh;CLGvEPI4CtpMV}{bRSeTf zKWa+6t*O4GF3x@m$@F!5bLff>f|561Mi1KE_L19~cV8>7-21_N!fUb%Yr!q91B?S~@v;BN)B8(R zeGG&r<l{;xbY568~4QsGm7U1f-a*#$&9+ec!#!W4JYV1Wrrb%fRgkyvX zn#$zqsGWKSeJ1pNIPbt=S!mMk;WMVb>x%e0A#g7O;!QFFSlc|{8h_sGf0tz5xx+UA z?yJ!7qc-OG{*yj620fa1CB!aMg&X{Bs^9Ez5IW+s_AS>h*O22@tqV`-c*>r zTJ^>%wigwFd$5*O*(>2zlk*5^EA0rq1#ih%Z?fB90+de$@a*T|?=xG&-u(m?jO0N1 zax{+H8t)&-&ue@Z5}BpVdmAONt_4}997E4aV)77Wt-!Br5Eo~Is zSk>x$s`Jd(qP>{Iyt3+9#V8TK)R$UBn-BVO2u5SWkPue>_PQXywTa5u#Tk@4bWUO7 zhUMVM&8~&B8N8?DwdQma@^l{hilHkIMl3NQ=ZVh_+6o(YnXJyD6!A|uXsW(PCeg~BygRO%%?Cxt7ftS z6CG3~{jov*(EUpxfFjtn5@+AaG|j*#f2-?Z71Dl#u{Rr&^Q9hZixRRX@mY<1>2S)2 zvFZ_oDab)S);L*Wce|<(UAaSol?bhTDNoj2<~2vPe57McWA(%riRJqVU*^}IDvo0ZyxSVTls zW>cA+s4d||rK)6OU3x8|Wut=5Qk)i*0Rks?hwNypL@*4T9zeAfU zv$LjgYjbA4l+>@tAvxDx@p6Le;KmywKv_tp?mor1k>?rFftzuh1rndPyWR=<(B{|x z#XcwxU8putGj~uEeDu~ye|vRo+oKN!eUERu!nNuzkVP2~R$)^y1d{O~Id~z+1jMV# zsH`K0Q;m;K)MSbZgbpe|q_}wB!h<-v%Z9X69mq=Z>LnoET`qxX{v;%Mq!8}5OTA+* zaOM2@oZy!303gCXVNmulEvuydmp@e-KqBcw0U-nbh+RTu}jpA!n?7wN*+~4y|Rr`W<0wb`cBevGn^SxFj{Ok zD%BAF`gxo2qzph)yC}8x0+B!{P9}o(zci$ABzIZgz8%KGA~Ml z;3-LNuw@3LzM^?=IR zltIn{?NoVAr$Y^cmp0dJrx)6g{#J?gQ6jZ`>8_0M4+pu4ULJ?v?W?NK%i0GuqxU@m zAAMJhkq87~Z3o{2{S)I89{t?=6vTOyB7F^fHXqh@xTDl#)9RDaeutvpp1m$iV!GO= z;v6@dr**x9|F0L)Vscq&uYLQn6t2i5-|kn{{VD%#@{0tAS$g43VP$?U$4tH3-=wCX zy`0L!^M3cQaRgIxK{|YS>F2BTO0%^{_UXSbI-vY@w7(N*O(y7mk2Wd`Z40~(H2Kt- z_LUo*BX^#!JCra)~kD?V^hyC!lgVteT zKb!_IEim>#9>QmIg7~dJg?UJfDeAOSn!-V1#`@bxSb3k2vNE(JPjfQVUkUc?=RG>E z#OqvNx+mypzY(nE;?s+ZKkkB{EL*RNPoEazB>dp;u%8NK-J_A|uL^LV?ll;-T7-hnVrP? zBc4fD#L?v9WPPC(rCdOypfv91RsbZSu&)=zX5DwNrWm0rv4ddbHs}g*Gj&c123qLf zthB2>Er9aX1QGI?chdifFL*BiQjy7EeA8BWzdBu(5xP9Rz_Xg_mY#ICowVz7oeEp3 zdo|6-ODq3+DU%wr0Snrr2ZNct`uSQy=yP54El+ki0JG9b){Q&({!T1MG^6LZTw}#~UZE)tp$SS4!*m^I262+m zw+P$8@Kt&rCj$5TSEy8qOc7_IJs)VN+LJOf4ytn5h&+R4&_PBEo?hM3A%w4|#9{|m z%lqe@2RUGg;`ez2suKx;orfjo2a-019R28vW~E!<>4cn5l!{Q^l07ac)cXEWrMPsN$=fRz8KMGbVb&V_2M9WdQ~#K z&`UQ%1zamasrsL2OunVTb{@ZhUy4bl)=F-K^Yx(6KZ=`E_~ysdfdQhCx>0TZzeKCX zzrbW*bkwwF1V^E*O)l4gVlyF#mg8BrRr;{^`?W^JEIGP|N(iPN(Ez(TYKTqG735FO^MfMKdCJ) zj!G8v_7o9+?6MLeP)HYBeo<+#5(_jjO=X(z*^Zm(wmKJd+nPA_Wvkh!O8p|et(`(6 zP#kMHT{tV;zKSc@I?I9i$MF~clBM#zK+KAW{V@-)?w{EfdnuU;JEWD}i5ZWLgmlOV zWlXVab3_#_Bvq*u?)z0Leu9!_mZSO?CRoBRH)H0IG$7pW&-={4yU;)yt;q!Ny7C9Ati-`Ujbq>fdUtaL{ z7txEg4~8SS_{FT-IW9n4oz1ycfqL-O5G58Zb@3b1OW?8neUHBa$t^Gwj#!J;Nni~9 z-^iVBfY@L}ByUi!r3@-)phhTiCmoA}(jSm|RNPo@(Cr#tr;#C8dn*T6Duaj^sjEhl ziNes$t0LB!I?MV;q5iMm$J!^>C*IQKHO`u)A@~YXZ~-uW)F!o{1-+oHIi8q1@^k9W zp;C=MyW82YXpYeHXZQv=!>mqc~NpoUU zu1b|P$Qv>r&ee_5UcPb^s%{t z+m)sOcLa~J;=lkOr&y@=;@~2f*99WL-e#$OXWNRkw*M^OV#k|MGm{#4L263Utj|gs zn&ZVVAFcL&eQ&={RG6es;+;)7SC9tU=!re~Pt~|<)*)3aAx8yJ->1I0zEGv*Y6~@_Cw-dl-3ZEB~Q)FX{u)JS| z&u74(JQ}Ub7TZ=+O0eoRG1vB4Mp-hnEgYtZ^|m|PbOzMR`>qN{@yR@1b|cvg*X>4) z0VmYwxt!Xkp7fFfAI8mu1wg#x4T(!sf(9D4r2#Ijncspc&zwkuAl}o3VK#v1^)4FhVg>^%e}W*F|?Bp z38kWqN8T!a28zm=-_wNbsg4a{N#}kC1`?q34$GGPiH7t?bGbHMIU2ZrPg-}Y4~DYo zfb)$na4jl)h_sWjR~z6^O1gXJ{&OwtonGR#Ymz8?Cy`|Xm&b;VA>YNVHvI!-bAR=| z6<)7(R8BT z1oRD`eJ(;@<(Tn8x{Cqc_W#)W%BU*4uIodmG!oL?jdV##cZW36edsPhy6aHV-H6hi zhYkrri9@5Lq~GQJym9-E@0Sk$IIey6UTdy3=US5yWfj9HA`!~kyOWuiQJ)^{si1JX z%Xcl>BLWY}YJ3^jn5PkqJduKns7xZ(hp%RO^H)1hRpif5y|&{2W}Z_bg5yE$pMt*Q zS%OtM(PhK)B~>R38K&8YuFh@b;xbA23~vHgyec!HfGRd+DrbKPkCX6jzXJb{Af^Sz z1KUnxCA!;+Yj&^ttnKdA3)GthrE{#zJ)it>DlS}I1tWg$4xp0BWex|9i1WD3BUACq zL)O;dMBF;!oWMW6Mlj5X^F?Lg&V0}+YGF?Z%hK&LU7YXsy1_7kX?Gwxd&nz)LWy=|I0#Bx~ zb7GUrZ`w6l{|;2DFaBAC^zQ=@K@njAmU_dRy&M}Lj{J%q@#WeO7zAK<{tlaAg@M;1 zHCe|N6bdYF7j@;bBXd4;G?)- zkwlv>RNf?Dh!~aXjGKb?rj@kbq||6DRWA%g$oV_Zlm7Td`rd5#t(j}c5-AkU(EKGx zvNZ9)4br6D8itkn9b|Q6*;D27OfYM9yO69URa4a`-7I;8`863}-teyt_?0MsINNv` zU`>=S80c>v{j>0<3w~vcGVCX(&pEQD+KZAT=F|{XHt&7Dwjyt+6S6za;4NK1!ic8U zj(Wn?SbRX&24aO}i7`zAN04Wo`Tt;)_R5 z=1recgy&D6V$0L(IU}>@>4g-O3CLD>-2QxO>rJ?z2trJIQIsAPofbQIAg-R`Hg9ym zO;^hFy;^!)GrzAy2P^c#9lr#V(P58E@F{4zOKQ&pvv$EIk-XllMJ<@>r)s`a-21@h zl4&T{Cy-;AgfSCU=QX@;qB)fmo7Av|O-|fr9>2i>sXJwIWE7FA%LhDOf8&QA_TbtQ zEvyCYgX7<}z$S3H?u`rU``(ea=^dvc|8bRT>0Z8*Q!-8Zr)cJ(%M34wf9iK z)WSk8A>b5$X3QLV0v`Vktn3ufI2D0N&ZWXeC&>(p$ffgs1vCGYTHRASVJ!RYe^wd{ z{i+h^-jp~kE|=ti=yhtnx@c>@vGMrsbNJnuro^HjUDmU(}F4eFOd*s+N>7UEDoS zIqB#oCWi&b@x?NMkI$wOwREA-GRu^7OZ=HHk<)AOLttPlP^nN8KU-p<$+V_RY=c4a zIc3319k&Q@1hZC5m|DCXc2T1n5JtZ|;`r^6{V*7vyJdU&eF_N0EOR`)Zn(ZbnJ2M9 z9^XZqE9X}G(rO7!*h~}EV;<*uOtsEW2gcDGTCg~3`&ZK+Q7=@K%%#_SQ57a93|giT z#5~V?-lxK5r;*i!(~39ZQ_x97t6a9YR%4~FGOeIWZ8ab~I6Ow8r+Vb+#wm+MA(5| z6;RXttJ--9FEamP_~UGHrRLB2P)PFd(2_EKT^X|;+(LxguFy(y;*6#3GJOc(2v=T>RHI}QSxpT2i&Bd0f- z`Ch-N;?pe07N54P`WLsj5l`l?hu}qEEsz^P{6nyFyWc0gE;gvbW~R&}!HuAO{22Km z@GY{Jh4k*npAbbwxZ1!`&m^XiO{`|mx9sfV`Ei=+UlC^ZzZIsbaxvO0^wM6X-LktX>(|a?0U9|l{Hb{J$;mOM; z@Vt?uFl)BBftHnX#@lIgML1M3x&UNXEP!q?9?wcn{x0rd#wN*_Kt)tY`{*wIHSyQ+ zY0`Lu_*b=31E7uC`)Dc&rM{dYdG)aJ>!uU;7=&*n5_c{lh^dKe`zkv|7u6v0wY82p zX{Xf#kdLG(%Z{>-Z`m(VJmmU(V&Fhai2Ja`51PcJwXJrgpp$Q1VLwA!(Yy3qRY@cL zL8FqYr$qP1`eN2+`n~g}E)UP7FWD)hg zf1AJn(o=0oNGYD$|G7MfBD26aw6?P~zc8!8O9)|M^NF?9tqQSTzFfMkq%E+Y+VT8g zVE;n!xrs-+j6d~75KCZe7emx6+71aLD*o>tiPsX4=>dZ36&q^t z8kg21Nc7^RUcd{!RrL3%^)2TIt?g%?<5bG?8)S&NH4z1}U;BCewO!f*J zeGEODL1%+6fOGXpTpnV7#7s>aA7ZBtS0a|OUiENt~3Ng!YzldajasG+q} z+4qwpP18wW4kQ%MhAqhmpKtO};-$=~Fnd956t|nFkR8^^?NnrZ_l;J4I(6Em=<&$q zA3`pO^U;;!n)Xz>d3%NUcK&mC9AhxCD-na?2Bp=BH0TxXew_Jg+{5mD%oJW9G6I?JFuw!FJR{1!0^OYOR}y~5*zi7OG5T7=wP=Cm-$4;8N^JeAW7^uT za%0P=%jqfgNB$l7|62X9b`LKC4#MtJl?_1$#*u7i|M0T+a=i&lDfv#5+v38xSw;AF zJA;Y2wSeO>itx`BM9({rhwzK>y1359xOowvFlx$z;7@o&abH3eNQ7yxQKF{@7OL*M ztiH)q&>9G#1l(x=B&V3*`Uwg^&Hfg4hoia@yf^zpwmXIm;v-S&;Oz0>&cFlcXa^=rmq5l$6~W+D>3njfa+wecE}lD zE^AX+5;ma1S*q8ig5M+z$m$nrgxuFl_yeZOW}&NqA8L-H7wS~v%^wHnG<}8CZ8*v^ zYr5Nwa6aj$UtH;ifqsmZ6B6Re*`m06m>;ZSfawdw3SKyfr<@NV5EfC7Q9o)B%b`%k zRfgqWtsbDnOV>9_L<}!~^WK7A$hK7z5@d8a(+>DtPxk?kV2A6fk0ZGL$jXeLd#l$9 zvZzTO`S5C#bbfB|GcUye)2=2*h;sFAm)pa(LO^GkBjm>j0q6(TIe1k~Wk|l9dhs0= z@%I?iU%;;Zzv@+?XW+d*t6~0krfx$I5&T`6xqvLycK>pzuo*e`YZOq3b*x6R<>k(x z-b&|yt;^-ce81>0wKwr*(fZXE^w_mnzPA>VT#~R&>v5%w0!#*!D_4~`A@|YTQhC$T zn|X;vKi$~6?v9^u7)%t9r_Hl+*yKa-<%>pmqPZ{U-HM{$hry>w#?CWhhe6D^=RxnR zJ@8jzB#x-l5(Y7Aqy^HKhm<28e$QzQj((6fU)ojze1<(o0hfe-!BgDeR9UHX>S{SdHPhP{(FYn7ZXg82yCUHUh&oXFX7f3)MGLU$Fc*G`TcXWJ>@S#YhTSi zqeKDGgHPcP=Q1shJmW#El>r4nnQw7fK>no|#qU-lfaA2Y z^B`^V%)3Yt`=c`oUqT<)H7PEttn(tnz#M z#b9Dlx$0qc>e5)nh5k4{%YkL-7F;=w5;&l7x<^BRg|E&91 zoY3^lm^DjFu2uQQ$e?BM&<84*hZ5p}Q|J-=`j;0DKa^9Yt!atXe*-gRq>WYUz!;fD zhkTPk`jNxWALS|bV(1MjczDcGgAl=8irMd!>9KQIVas!z(A>bEWLGMgOV$1acFuVi zj$7C*U%aOvn}$;+Ojk9v9T5{!g)YNc&uTA9Rzrx>01WiK(tP0x#|2U{Q(U+ILkyHz$BqgOW6zH&4>$)pjm8;RxrYK#T0qnPVceFK|Ux8-jO zhrG~r9r%R>7Podp5jhh%WmH}meqP;+h?Nlqr&2Jtua!~4$}95~+8CP4y)mA@*_y$6 z`_%JDx_|rFy-@9Rhj1!%?Ns(8{6qu!8@|iMlP4FiciC;YF`T#l_Jt{|b*_ zoYbA@)GAMRJ8`i&@t@~y;$oD*n*JPFbbALlYOdi;!R8W+#wr9Ap+y3;)^^j@K-!6m zDu=2&aaR8`G7_zt*QNgHSwtjSNR29myLtXX2iG2uWuwE6N7*#dY9bH_?z}@hgImtQ zx6N~6LT6dHjS?Ls%#EWolu|)5p82ZKSQ2OZT+{Qm9O`$u?(59o6b^A{AL@gi9EFBM z){X#q1FQ&Y4;+C&%~sS;^5cN<6)f;q@AYYr$P57{4I&Mq4wNtPg~R8ZL4=Xj+Ee7h z3rhj-hUS8X&8*;r64O>XHm9!D$+`aBsAAIsowATdK?hz&ibY;#qO0z)4FZF;>eECt z3CiaW!j~Dg>cv47 z!i=#Tc(rHbj`moi0yS;EQDpOz%m-Y7xsmMA2Mb|y#HeuRYUW9%f=XRyNG+Mm7eg{D z$`2)^N%S1frAv*la}S}EY@V<(8}`buN~#88KF*F;nBzm%*;Eq*Iuo^9(D#z%J+pn# z^tJD1uq7!%umVZtzDPKUhOJ zF2~J9m^aThxl*!c}M+3~vR8;s*l zOfOwak(CZ}B!RdyGnm#exZ$FEJLreZ)Ptkp=MV>+5EGxP6DU&@zvOrD^dE6DH~LW2{?zI z`dF&e6+>>>B?7h!PF$yi8tJxhpDtC-`ste9UIJ8RE{mF#NqF+=Cq4U_OMwdz25YmG z5ZPyqb2RG^*jyB0%RH;YbfuKK*|XI*!G^949#?IGD;$r~<&E1F5y`k>EP zKbi-ih!pSu;o^q;W}r`1ts{1_Y6h3886s=loLd^Hs4cjpNx0*$1)H=86P5Rsxv%=! zV>5ibdu32+ZtpIz%D|%2shI({pO+CwkCrEt@2<6@{~e&;Vt6I;>8l=qD9TB`mzOzb z_&GoB+N;6i@gwCAAqG=LBnAAY3_4fXAYxg~y{c{jw}Gx?yk8R6s%$+ouHzi5O{+zg zvNkSyktH2RxX?C_J+Ov+Kz?+uM}mJd3toxnBtly&q^&zH(TsBC0j80lmRUQgrTp{ZJi} zt_vXT5~RhLSwvPlW~kC2=o9#IIRA zFw?dA98GyKFD!&DgB0eVCJ;p4yQu&;*X6|fuM{E`Avsj##(l=Png(56?eQy{MC?=J zOHA>s=III6Wz98XulVs4aU&BmmkX+}80c{w&?Re;O2-rIU=5mKIiVlAOsnK_FIG}k zR0hlYN2Rp3(D`4}UG=*G?=cFWv6h(8Q z4`b9-DcNhhyXkVICnOyB;Zb~zYTPUHt|Lqx2mM1+0YTT3a{S(AC~H#{m_=Wfzx6ih z%e_PTOEw2yx-fV(jr`qoJrY_gZVXPH1+nWO6p=(CuZD}e|4Q2DfZ)gHN|EeXaOGBW z+f?X3j}aoCHYHyUmo=J2%a0AI&7m~k6}L-9S$KzwzMi+`n0us z^+ks@$-{~LrEI+0lsv2jQxcZo$gr$QA=7E?c3lTzX%X3RXD2CeJFsp|dR05^=&`_L#{rdY z^<+S>xli!lj&3S7AqmMY4`D`}^ymfKIPQf&;B)0AjN)3Ws{8Qz9n^#-&|6J$ zQ;{xc6yz5wlH(X+EQtgq`1y)ei5r9r>cC<>4i9ukQt6GJ9q7ty9Fb{5E{;C(^?C@h z=LDql^>>aZ70Oaq~)S6pA>;uh$22>kEFTqQnm(_s>o5%Z;||uaA~BZU1$yHS9Jf z$lfZOxi*Lhs%kSKS?~>5{?Z5CLT`65^`l=P!!C%u0rUCrwlXwmiY zS7u!7JM`?yj2I*T4CWW%@D$l*(slBy!lsYY7w3N3G*E-^S6cJ%pImr)$#66~1UJRS z+v7(Rl%S+R9Ai06YSE=sjXt?o*KAbj=u5gI6v1DW2$Fnu?fQTK@Gnda>w~(`OaTDX z1Pk0f^HDG1#0<7EOkVnktP_Iei@878n^jDwLo(Cx`EBi8BT-26W?dijt6}p?@_k3` zQv3yJTQ6_5_I^eh(=!7?9+FYnILBu@S+7P!$)6($GhRV&z4QszQMZ6UejG`eewN62 zWouCBjb-cm*z!)weSB_-|4u1x?aG)4sCe3I4m~7Bl${pyN8_r}haf0rHt#0$^ag6u z(FrTUzcs~ci2UNMNL7;6*78LViA364IhV2~JoEo{d}VUv9Ua{YatBo$`O1Xq4;tAj zH(Le%_Q6Nlm*><%F1lBQXh~^rl81|=g^C)Z7~UTI;~aH&BicLV~f5JEZFD_Gzz_Rubj|Rq>tIti{^ExqZs|)RW+M zy0Y!5Yq=x4`cVdHfst%lI4(GXj>F{L@2wt!ULLz1cpg^aa=Xn?Edcvb0*j7D z6l?YIKrGMpTSnc-zlVOv#2|cIMWKY!BvjQ}nP%9?3|FeHj1DeStVq`g525+t|7O(R zx2roVl$l2n>1CBNTWRs+`NhO_^{nxika8xU-Cta-Sop4G=wmIwj5=%FDxba3oY}7 z?rRy$ofx^L^AXCJSRfT#(FR+|K#yU%YPJCY*$bFLgrP#rtTRHkWY83MdK}xn`tVAx z1p!z^;~uhSM%Z>rV@a6D_!NQA(Fb3}fF)Q_IOJ&Lk*Xopqpv@r!oI;3niyxT#2aJ6 z=`!sn@?0Igx`WLO-jtE!6p3zo#RUm&=#YR)2Q>BM*LCEKGfLVEAGZB?-}McKNU z?N*rCkJQPX?m}&a4GX-DgjQy8qra+%Ck!)K`v3j0ohgV^0o9@AWlyys1X4=xLt8YDHS8z+mDnIiadM&th&1K3ebFJ8nSnoDF7Ee`Sz!65CxEtgVqy4BBl4=*g9xRibF(>=$5ooDphQ1 z#fDZJ*>GJiH{g!D;?(VVpRSi!>m>}z8qk#ARsCXbEq?y0vdJTd2PH|tgakA3{t0{Q z-E~mm+q@3CNqP|u-O+u4kvgDN1U#IHYPSZDlcYC5-2ivE-tZT6TijkPk>cq6SgJWR zI<=s0SieB$`@x<}sd?_*IrkU)SU8X4!#uOb-MYD-cj4BxdM`Uv4T^`K9p^n)s1=5k zrXRX0o|qQ`PgpQ7_3m`Esl4BhiN~eC(#A+Oo!u%d9ZjdNf2uUg2=qXoqj?)Od=f~9o~!vv>*7+^ATSH<{D){} z?BK#05q+`J@}U{bT?0j={>}Cj0Mrhtdw5oSX%{-omHv&f+vD}&EFd^IInk<^5q9V+ zszp)|NSSdhUOl+6+{g*(*pc8Abw#7F`P0NP{*dF9M_ju~R)Hk=AN2M@lPrRpBV+&pxkdS}vM@{Vc5X zqmwiFiVsh{Q(W~f7SDR5TEN8H$9F^jQ$3H~Ak|^c)L>$#CrC(hlC)e3VgWl5?kulT zjK&S~Q7koXd~2j@yN?Ct_^W=X4ub%@Zl4)_Hyi1LX8spasifOb3{l0TW7b2M`7(uT zq_eyJENawSB|Mo2<&AE`kHzxag{pQ(w`R}xxXzp zzM!R4eLVVEbE5kxUv~M6s3*FvcU}P-5a#3WhkCI>5wE2f>V@>^s;H_!Ji~AAGxiZ8 z1w|Pwyg=FlMXmM<_qtM;3x&AptA=fzo#M?bRbMA7Rm@+8=Q2Wln&pGo&PYNNBM6rk zg`eGxigQSgXgjuQ6Oc}S{=eS`?CoSN`oG^-m4#X(7sZWJR#7&bN(|T-G`1RS*sU&Z zIQMVp5(ryW`1p!+z%BKxh> zls7A-T>DB!snz!?Qh8KZ*Ax;A+!hYt!d^Lab^ztB*p428h`|31t+!6NF;+e0T3j!A z9Wc`Gb{vr-q2PW~DJZHZ+toPDgn-A)6_RpjUmwRx=M~VVNMrI;3>Q+mm?4>~Q$>rm zya7Ojv1{=II5K{MK`#)%@5`CfR%_mR|v0{{xxx9PpA50+*_|4JLaP49qmFhVjb z>~1gX%TGDOI=0!vk;0iTE+R7>T7@3%x!R=zD!cf6=o_Mb@E#6bG%iR#?gmNE_FrbD z66{qZCeRy-G4LqpmCt+{(vkY#m8Dfk9P9Flo`qQ<7dp8bi(Nj0HX{)PV^

VShH!t)0D-9=l{AC=Lp_?B(ILb{X;u`YYt1uf&`+GyGLK!OaAp zC@E=AS|MDq5shFy6Cf1lg~AdAm-k?eed~UC*Zo) zAL2cRrLjOOS=|#>#l5)nJrtjvUS3xt-Job4mn`L5myIh}P8j7RC+Ll1AdmHIyaifM z*6TfF$1BA;v?J_e(dx2^C=b1LQ`rb7PehufUSu%{y%VGA|483j4o*Ael@Q z0~_~kUw!2dYO9wjZX;m{VQ!`2s*mgY%1dqp9A%=ICxzG5quoh)h;}y#{kcb^YI1oG<%ikOTq*?gKRuo zXD3~UA=TjS4Vj-?*=(Qk{1ZDjnD-iyj>g}FXD-Xe0Vt|`SZ&@c_l4Xzd^b8O=+v9-)_-g(=q5H+qc>n?h@9#dml#bXP3L_iu?N|@il*M z0yE~jnKWbNxZ+w3CU|qr43Aww4fapRyLZaZmIg5qUh~OXk((x#z8GJHg$L+*&TK0J zr4!RyCQAsTtl$zuB#YEDV?REj1)PA_-pyX(S6$=?r4~Q-XP5Nr2Hb#s=Esk_2irg% zWd|P9?jrCDfJb|Pr|oknxZ>7uF$NUQw<~TE#K*=@=tD+Q9QK7~(O3OU72c|58i$js zEUh~DJF|?o?HW2zjyBzDo0OVq@7zk?Q~jew0{y#f!kxH~F`(h@pKh|B7&WNVH+~4; zt&Jp{;krmWMyU+(Q2t1XnR13b!2);WYRI=|oF;A?DI=6A?;Wys-ADJkJ%Hi&hM6sY z-=~lGE)u~zMXfpSYR&Tk)Sd;@wC&4|UH~V=Zb3mE$%9%`ofKTs=ZX+@ZcmyfEQaJz z!+jc{eYMhUU=NIFHXt7(zA`I3j1cD9@k2a&dG_OO_-amEhRRh^>nJO1%1o)&YjIo{ z9$HLgVptCk`niY3Hli^O4_;Cn2i!VA z4p3?b9Dk%l1smR>35%ln%}x(QisaU-sHdx%{b?MXb_;o1<}BLu2NC_W6xeoVl(cP+ zx#Q(kKmECz%ltO%IqPKTcx9mb*42~j0%wcE!C_XAKwBGKmGzHlUqQQm9W5AJbxXC< z7Hw62+E_B)9(GY07l2J;E^_Hl5_rF?Wo^OD{x#b!Z8*M z$j%{*u8C{~G?wi#E0b{kT8GE<->PnR9Ji5XvR8T5ge-U+*3#=Z5tlG{uhBx6qcfH7 zt`cfnD=XYbICj|E)ppNNqJ_6MFMfS=?)Mjyhi-E!k-Z^|YFFxjFonZLl0d zM$smxVHQ|W_SbW$C0{{}L(u1(WdRklt}W%S?s0;^93yo)+gVz(dc=9?3Cd>BcYTwI z9xs~Ot1n??hn0jC^n5?E%VRruXz&%OWZsj9(Y;-~>CU)0_p=`3N2kq<71CQ^+2qWA zF=IF0+!WRAewt(=P(gZ<7Tf!%*;gqI(Pm-#;#*d%4KFRJinPRayEqd5_qDoyq!sT; z`csV-hytC}MDiPiKGjOXzSFFuK^7ibIj43_!YMxPy|-nfho$qY^?N!p9in$PYZ*!O zC%PAdS!GzB#jnUwgiQ+!z_`xdmd%Q%nc59IG<&14xb4dD+*Tp?;FFJGLL^G#g5~Sx z(5(*$Tk+99dY*wn9ZD2gxnU#SE)MgXMwNs}JY!*P#vwDQ&R^e62HhwA3Nu5&(ShHc zTR@qNV8%%(FJ{&Y{HSEDf*`t(m%@Qp5#4A1(M_s{Ey28)xE_n*)$)t z%@PRL6ylT3PxKsqXd)wFhvGcfXdQ}du(IoWwoW_4PF@|B-3tee0L z#AFo|SAF4;glyj@&}Iy!VCp}v52R=ItS?I2O}plMZZN_jW^PkX?m4-$ zKeeR4C;|U@vvN3SVVpG>FQhNE%uNiR_2f_E6qZDcDuiG5It!z{q)(HAQ){6xztBtw z$Aalyo|`IRGSVqP!zsgahlXjQ%Brbl9!|*D7W`esshHWGR07p{j4Tjk?*5aa5)nlr zz*1JjqaLVLHE6Y7v8dND2uoo(5Js9#c=4Xrxbe1v9x z-W@j`ooddm7C+~g6EBb3HcNUqGLsv%TMY6NU6Wn_I(eE+X{`LKY=IU5YmqlS2;evu zTmpt@aq8v_HH~EblD1w_UglDGf>FER%EV_Wn^zCNWcw0)V@lX4sJZnbv{!MaZ?Rgh zSxxBeCNdtMO=ga3ODaeFn*8(!Q5zbTv4Az}M)4(hN`hO+tR%2k%ZRgapog;QN*GB= z%~-TVg-gZgg`dvt{hR*oTNw31Ek9WOeG$5OS4aqjmEjG`h`C1U7Pt$WAw!(D0y5c~ zPTMo?rE;SlgEdZuOU{J+V%FA!apF?04cov^k}q6ZTA&}`JDkEl{LoK1qFGb2$YSCH zW15^r^BlKX5-P)~ZSU<%NP}R6g^>g7=#wG1(fS0dCYW;v-s(0BW2)rlJo7OSxFXo; zigRF#e1OC$@a29RwRCP15|3H;mZ&)ae14_<;v>NfoeC9l zzcE@Xj-s`gSVeRW!<%@;7Y<^M_aa2D@F8J_Bo&gCNX!T^GYQGB%E%t3p8h76HbLP0 z*FApD)F>hVR`CUd0+b@BB4OkV&P0bi*(oD@kxM+S5ben3_j5L-cDmj~V|r!dUna>B zr&9gSvtBXkqrYeM?0iyAHg-~cp~g`wR+>$lNCiuC6Qp0KB;r?6opUc`p->v(Nl(mj z0ArKKECe1tKfOT~A!n$kd=7C^k6u#<(;c%aal#puw3e%#!%L>5ZaKpME5gBJGt_s4 z6nvz^aX4h#tgbg~$n%ig9$(&Dl!2qx8&uC-hOXg_basAiCoSA5dRC32Xr8v>dsZQc z;D_;iCVp+e=fW;TGy^p=timfKOcSn-Fp?gv&c5X?6^IA_;@1k0MbxArwnXlKT3D(7 zi3#1bD~%$})bDuYJ6&WNEh&UPpP+h{5ia2V&Qmah(oMKr;znmw|D+k4I8sYzBKA96 zOu@*IHL7npIW*fFJuI?x9l=b+9*Nji&=LN$Ud#f?i&k)coI-jMKoME$rez`LSI4e8 zbV#l0wqlNu(Gl}5+5LtY6U~Vp+mS>kY4UlB^O3fjc*Rb^9G#me<(Ib|ZL+2LgME;& z`LB(&O;ix7M(;G2TS9gc$(+rvAP;oT`f4SrxZZD?N)FqO@AT`X*}kri_tRwQbjUR& z8MmV{wN57(qP61DAb>1q7ldA+yjBD1fq(Tqf&o1|wpYH%=%30&jRN4SMiTsiLm+nh z%qy-YWmw0cP`zs8klto+(vE*E;?!Z!Fy_Il@D*zQb_4yS?w%lw$@7)VMapOU5+jcL z3N_?T@lW=$N*@(h?OWMCaJ!Q(2-$JTss8d(_LF;K-`dew$&YdtD)URA!!58J&$d0$ASd2=Mh zgNL@#xFZlQ0Crq;NN$GTt#K;4UZheNFyTNyW<^g5Aw@N)hpW6UkN{NA|Dw>Jm>Vwq zD=sEq{e$fPD{Qj%CYbahKL15?NSdvj?VV8RQnP5^1L5m7?oD9wvUzAP$WgQv83C6E zZa5E}^h+SZqxhYd9C-V7iGOlu)?YvvF|5TpOr9?uPkf(6O^Fvo{ygwC#?nCJNKOG1 z-ZO)3Z#2Wl8hLD&hH&@x&eSZr0$nCyT|4xJ*l$MymUhohWUo&LE~pV%#G&t%yERFc z*nii_0>}5XT1~ad81UOV8X@`Tj;gir=@D)puDQd-znw+d2Oea%DBxrKvbF$~J zFL@|d_keiPV~9ab99cSLOjX{hONe+XkWn+F?(Ax!G9osFanrj!<_z|;qZcxa3fyoc0-up6sqsq z_Qdbcxk%TJADFzr07E!xRWp!B{OOI_=PkWaUgzA*W%&#fb(rP?P6yv+UU+|A^ar19 zQL@Z@u$M8xG2J>P;-zRs*hohEiaSu~AMQ=G#u}Wnw&QIg9cPU!p?b9K4e*a0%^JQu z(JNLjhig$s`H}j0mUAqEotwIJ&X9i9rqjyiE+hC1kSq-k4T`o=Nh@0@ope)$qA}~0 z=yNVu-}!R?k}@eHcaz2Ag29OQaeehj%Ni z-OmV1*Wu5S-3-2b+oi z2b;kkk3yJRzPg2#Wa&?&%Lc^)GmQs2ONL(Uxvl~}uw$K4OL5`PLN6sCkQ}<{WL#vg z1m%XARVGq(zrC|>Nh3!&cP9Q?ku10iW;c0?4iHiuq3G1X6s3v9uxPF4g&sf?L?7TJ zyV^|2Gdne5X_~pOMzn=HP_-NEQ$uarZ#I!d5UE%uzc|6a^gt0_6Ef?W9qe3L6b9E- z%xpTJ&9^0<2Hf$}CttbXxe8EP^eJ+0v94R!Su|%)C=afJS0YT{8K)l#2eAWp?d3 zugep6$rEwJ^2_mOJq@-6-F9Ev7hZ^u=sIavNW!u3urxX&BoPdjC}+Vt7%41H$BIM8 z*DAW0f*qFH*mz0_@a=K0rqlc}HcIDE{2JK~QBXup&$ODdmCMbfYlp2|b=obi$Uu<{-Y6I~k{JsM6s%_@>rK<8G=p3QQh}ZYU zZ*)$2u##Fm141DrbS(RO!y^&0i#MR-q@c7c7MxyE^!*MhWW4FTx8r2|76k}x=w8>k z@1|DG(^`P5z89i`eTuZ1`FQm0@bu% z?ul$ScQrgZ%IrMf>IWP?pdIgj@Gu0d|5TDxLNAzh$3viB44VgoVJ)N5+7d{WX+^E& zZKFn&-okr5s4LuyNC$DY%JI};KKql!&@WR`^H-O~#YwiC4;{qGrRk}j^X_%8OXa7h z@huZdZ!ct2*51Qt%oweIyb{#1t2J7BE_G)Jr)_=tUHP!%uTxfgZhdUbimszht$-CS zMee{qc9@u|i>43Cx%23p;s0nmvPnkLaa*mSrJ>F}L%Y?SCN6b-em82NgQJK1I}+aXd1&hxHQo6!zfGWTt@8U{cietix! z^y^W8WM6-^^|OPr(@ku-cH4_JaKHlixBt6lx{VIJK+cdi4{*uuV&C0A($i#)^W9J=*^{N4u- z(@%=>y(%~3+y!|9%mR_npJ^{9g&?UdMg|k0P%Yi;W}40 zfH_I-@p}`}TczSobUmylJ+vbM6Q~VcM!hw(-zUHUPXopdSK|zCY$7_dKiruh#&hWL z&d+e}bpdr?6^mtTb{ET}LCW1eora_S!!O`Of#~dcVfmg};G>5iJsII1@r(f=HQyNywew$0GYd{aiUAXO&9IVGQ-c!PNinpxtjNOtT?p`VK8pEFoh zimmVtksr_LbJ0zFPj!-r&$?wUUM|@R;Dj2%lj-Onq#wEU_y93 zlmcasB>lx@(KpX$Af4p`o}z{z(;~%VA-LVjCAGgKDH7<%qULGG0ObV$_GuD8o&(%a z&)ZI7GxQlWJWA1AYP$tu51oTQms6>Fv28;tQKhJPHX=tsMVZmb>+M&uAlcI^l&Vsr zN<~`*2W-eiikm4qmTe5RL>})c&`1R+;e??a$_3005*kjiw)It?bC753#@=HC#$XTS zF$lc8`rt8>7Is0mW&Ig#rLCl^P|_D9vi9zCycD?|EH1pSuCg&hR&vAFT=_jSQU`O` zsHpLZVNjFf=BNpaI>l5O(4cQj)09Hz!4+el3&L9vZELN>q#9XmerFwT;|O^laGHir zz&neQq|t}D;N2y7eeP$(?WLAS%7&YmyE(s~J3v?ItWx;_C1hjz`}^(wac$vHv}}5` z>LnKRj?I(bf8qBa#1&r#K)VL;tw1Y`7hvpz;a8GHlxkSW9jsBG?dyxAE0+yE{2EbR z5cZE0+Jjh*=bexdVn)7<`PBLG=lR{Fed-9hQ%sd0qsD^%=fxEC1Q?dROm_3ua(Ihb z_U+FBg~G(pyltU&uPkPFU9BBsmZ~x)vDHEsbnxnqt@^^T$%$=cm1nD22|!fF?`-YDGYF_usA^C zM~Mj5JwXvJid`KxW7d1F(Y^NCe-YB3_xtoueX}_KDoyC~)E4B!8+NR&7f9V}wqcpe z)k>Htvh2+z#V5MwS9~B61mP63bfY*x!RS}5<19B`1{Go_nV@N4+U1e4%tTJ3a3Ss{ zUpW|$&v&P%FfDP?M)zDmy;UU?@OEweE~_KCE6WpZ zyNG9!KOYgL)*ySaDBA3(C-azfiwQi|M%ko>cybaOFGp!rN?Nka}dF%(LZp1r)Qt*5{rW%&8^2) z6xJ;DVM1aD*iOWm@fH*WTKyA%GfRe{RZX2~ac9NEPbt2j+- zqKlWN_0t?-3?@Yd(@tTsgXmtU)Y^l_IhwY75_LMHXtlL0nKOl|9E4g`S+At9B8*WR zQ!2dBD>Xh+_)h~ux_`AE&RPq)L0(pXsv5|~rb|HQA@MQ6Ox}?4HKQ&%m_`X0myc=A zRjK9u=1jD#ZYyBka}YI}YrtSF^&yh|dDolX`9sx@Zxa0jjao)R=&fa6#%fX4%Ve{rahkC>{W*5K!cWPs(Yfey%%_EWv#*FxE6J& zMO3>_zUOtzr>3TyX_XiMkF2+Fi*oJWzv&VY>Fy3`kS>wVp-Z|$Y7nGBX^`&jp<@VX zq+>u@QbiaV>4x|AxA%UY{e0iQfaAEh=DOE9*Li-H(*ilZ7&B69#4|MvLQsW*!G%$< z2o$P0QlDV$On8!!AnVwz)Mhaf!>HF!x64mD7Dqo#AcqdjU;r;x_h1(AE1r|>8Vb0< zDyIjW@xYMeya5pO0`|R;M1w<{j8W}VmQ+HiVKz>ki9^pmyp%*Mr+*--C#P%iWY^`B z4i;Vgq%H3aBswsym11pvWj;wwCLC2byK-aY?Ab1V z2;{j@NmPhcB<1caWIp0Y1|(WI6|-^EQ#yE*7Sc)atyZ~e?72&KWm)PylGzY}YMX+Q z!YE-sA-tq1Z|s%VyY7dji20A#7kmb``jphASM@}~bZBR;)(0M@rql-PEEQcM#ZNYK8^iA{p|laL2sK=u-e)WiYYj-`cZ;rzlS8^F?R*mq-UC7)JDb5})p+*qWt{Iek{jKfP$N~&^W7nr zzWCLc0~>zU6h*Se^H0yT-m9jP&QQI$C+z5`sVFnda%cS+uC+TVDz62>!y!`*Fl>;? zu5#9mhIsLRzGIm@Qb+QFMYH%NzRo8fueUp!PNsCYxS8#E^d35p#0CQ<0WD;1o2ZVA zP__sCPItgL9dFVfD0&}2?bd=1*dY$9^{emMmac6()k$VWhYFodf!<^rPOYeyD5+nK zyux~a`yjM+;T_PjJ_j0m_+VC+(|umpG|YvbVKuE-fgt^2@@X9OObE4l!;PoZ4!Z7^ zQv4FR(SQGeRBP6|c(!-@2Gcqn@UBA9L=f1dRHRT_k)YD#yLGSET@`gVPMV)BKd?&4 z|A>8I2~P{i(e7yr>KWEPzkB@@!^FjQ{TU`R+S?8yg|S2e=i|;9s=4pg;$}~U=ja{Fcg0tBRTh+==j7}HbbdL z<&$$>gQ7)RJu!!D*4&Tu-^#tQtnTBI@C8~ZQPmsyP)W!d>3BkQEW0hD0^zo^p0m3z zX|Q-MTNm*nZB4qR20qx-9~xFU?*h$w+Y)=#-Hs{Q{QU9Ypo8J9tq{6>yB6POb+dn_ zR(R7rq!%6ay$2-Sa!~QdWcr}3L*Lp-QMCth5clJgEYD3X&9sIP9HGZQUm-3OQC;AU zB!{dlsgM+GA1AO|uneR1@jj^wZfecgRg9Z+AZQVG>8DIdZ9Yblkr)I5RZCn@W>6FX%yfv|U=F@9d#<{#CbuQ3e`~(~Ke3FER zS%~CS{1OF_OaE&q`u8B-@p48;hLZG8@sm@T1{nH}EhX#o_(1LKuS7Hxj=nPS27B>B zpQl?M=xil{{O$0vK zH>R2aQ7RV(Zg*R0`Uf@4~N?`FhES`tWC-<${;`Hl_3U=(PYwXor8B zUTVLo*9M93CjTih^|8e2Yec z4+R|L@cHhMpd_+*95n~x5{jnKaLV7q}gR*L5{CEW2%Hmtom5< zhYafmdG#%&Y{ha#F%s9Ug~XmIB;EPFfL=3f_C5U`_E~N1=a0)D)OtWPqd9LtYEh-? z)@^FCb_#~y92l?j(L)6!Qc3+SarSp!Ws@}_4Kb326wNPp%=GhgAU^l6&{y^C&i{R< zzXZU>C%TzX97eNyioGjIL9>wwH}L%y{KE@3>!209+6!QGk)b_eH99M75ho`1XQ~Y< z66u*07DQADEsHJDFj$^bq#u4CH8(vggpMkBaBJCj&(pH-jQMyup}1;g-tj=ER%^9G z{|)~B?2@$NX(YngU1&<#yjY}`tthIKtqardai)gQ1X(v+6raP`11AI77kwO_ z+gBiAVYT8p)Pq|?3Rxwj(1Y$9>7nac&OY<=RnIT9we!KU--r;atG(+Lvg)tccr0)y zlZ+ASS}c6-+cJ?X@g4b$xpY*LttF`@`ne3n9G%gA1P6Q2aTC@xTOcB6A!=-r6tV62 z4IrF;a0klOe{}*a`(;8>|NCg{(H&T1sK&i;#^k9h22}{}fU_0s$L8V*r=H8fz;TNc znp*WX0&?@BhdeKV*)X5nt}ZKa_iEKkAQ_wJ3E=_Xy&ZkJeWwMm=ypGj40#M5^Ow8B z+T?Qc2)438(n`ovK2197&-P=Uc=phb_T8iD)iOQyzY z&F$bDY_L;(R8%5+I*IWnS-v(>zRjq1lQr8V?MGyYbPGq9aA&pVOcUgR^Xy0n9_3>( zyJ*yn`y`^3RY#sNBW=N!_&I|cKd)eOV5M&v@)j12sT3vhFN4bsy4w@dAiE8MB?2q7 za_vu}Px3BqLqc=1{_88QT~*&GlYRQ1f%AilnS5H}qM3aQZ|F3udng~8Gsh1=bc^pq zplS!bICy^Ir7JtFIAb~Z3LS{Oa`4r0LLkEiG%#U%<7)&DH8d}R?K@u${9hm;C5c?# zxag}b3WDF+Fv@KXvdFa#@qNQ<6UmJ(s>O;={6t#x7<3@?C@pSYT9m=p1#ua4PqYue z{*r74zLYPT!Bwles>wn~VWoCavWATMoh4T81L;a~QG{8n{7H^HyPTf`t2oMe~pjDGjPd`}zl~GIJLDkBq@1kFLPwwN>`DQ>W9f=lo&}Nsd%W!}ZBX3*HV(jZyOFivyQeFD3;t_-z3++!{w34a+%D5N zh^FlKc2-df{pBO+?6*nw6qpPBj&7D>{!~@95y%s$bm5h%2v9)ij4RQmgrPY$FqpDV zU01w?zav}oAd2u1Qy#yzA8F`9+|n0?jcHvRp@Q>?X;30ji637CzW_0)1lP|cLnbIP zsTW*;$%;^a`g%pkloagqs?wxns%u0Q(f{{|Jr4P;QmO1Bj| zO_Z0b7#hS{<}WnEg3~*Q?m{)wE5;d&3Nz}LBmlR+d@}KMG9O8 zNWg5NQT#E*TD@&_ZUY%nU1frV*%e+yvC?mJ+1h@H52RvFB=yJCVNP|2`kY_crY_`h z(b^Kzdp_UTxCq~j>teAgD@M@F$JiR#sFtbM?kF5ABSG}-D&_C z*ZS_CX8!NIIJI!I2|<584O1eU=Qu^1EM=(h#p2GM;E$#9ybl=~ydKe8XD2sF);qE# z7npfh+rPHrJ*MnV&{f-Se1MASOGxMqoI*X(#>;dzoY}g#xWd5RIl?Jvq)Z579d933 z#0)cL-d#Y>L23vl*qyTC9@srcxbV&MGcSWk@cEZ^4GfYYbFaiyU;o-{y41j&w!bOfbEVQ(by$rb<} zPWu4t0?J!n@4a|>y{sDx5<1=8d)zTKJ<N4xaA8$u7%kZzs3K$FA}uTV-c*HOeHj1bF#MwOwn!$Y zh{Y$>*!Pu_W6N$XrSlZA5cWFGKJJci8RJ35NeOa}fsTj~vS_A_b*{;KaUo|Yqa(j| z^X5Uejd#7P_~T+8$SnEh(XJix&R9Q0XZul2roEN-F}3ukJ*EK?o4dI^@eQZn%||nI z)N<^gvnwPiLNC_gOj97qLHk^*N?*%4K%^3-L9lm@fVUwmf8|bID>eO8%Nl8aN6$2{ z0sA-Ja|;9|#_{hLd~}F>Dzg`Rg=Xj%QsKhjs%((G(rZUf&XwLzK$`G>?v%mp*Ef9C8FG}JDq-#EXvYw5kH+Q;x5M- z6|(NMB7`K0_G;dNcu*`M+grQBlJ$MNYEQtjj&Qqhvu@RWX%@?o;PYjheb~eoN0kNN zS_`@`gnk(oO+$aB3Kr0IwGQ#))z{($!}nxPET8-&l5tcJhk`+}{s;CD5M`~T zF-nsl2P>ke3ZY_cJ<6h)MkLv*dCT%BL*~sW#`N6S!V51t2gicu3h35pN&#PbGN`nqq+2+QG(#ZIZj#H z$5S7HV1Clg2Froo<&a!KTu%<*taw#I&uLT?1FQ$&jI^Z@f$sj& zo}61ZsHgYNXoLD>W*k!pbopT|!Z^5XBe>R`Z?6@C)b_hXj1-rav7LrfvXiBTCbN`?wC36-z_`jkkyjrUm&k`-vDyjyQt_m|w$bkyw# zTKx)rQeJ?^&`}@ZloRYdPS5Z;X5ya*KTz&y#+bn;LgT*hOqSRztZ2*_=uR7RC^ zp}uf^r|V9Cs9;sN9CqSlTLya;udSC5sN30FLD|IQtdmU@PK_$sZ%1Pr6~p=|D7*Jb zS)~Wc{BYarSrN%*(Cu}=)#ufIG|x;$cLERaa!Aei6y`nQBSR=J9v4U8I!Wv5920K@ z!yC?!#U)=1yW4&e1JkdniK+vj0!#se?{dBbZV%%}*AwU7Avs`$gal3T01ZuE+*X8q zue0`=c*4)BSOZ$h^`G@G>qshVLGRUy^Q_x4?(`6#xbHTf%g4`eZl14M3-yyop$cfk zn6Gn`(52v1A9RKIzdnZ}iBvyr+CHuNJrvcJ7e5|hJm~vlE9`%?VnUr15kT;)|F6g3 z#aYq9E__V6cOIXF#a}hiq{}qexjMR34oHW0uFC>VW4>@&tz!)=Tam~i}i&57%0p!Q)re-IB4#QnJIwg&<6UsyL zZO%+1?k@CGMbQUFwWz8k(lq77y!qZO$O~JOi4820Tf^0;PTH7ImY)}f2X$o-yzuM zvYzh@-0|lh<=%Hg{AxaYG&SvJ=aGIby3#M$@kK2N*K1uc>6b;W2=N|o#2pLem|rQz zcPr<7s9gS7);?eA3+$Be|CwPlw7dcEDg*zWfY&#GL%$9cteWdCT2SG7#4L_WBgDyM ziQeDj^-*MKo21hpF#%KXLHL0MUn~r?oVO43Gr$SyPR}>O>^^S7j%NW|d z#9Ig&{#RDUW*Axq)SY_&nujxeoonwaG8~*1-|zE?;(fm4{1FVf|DM&D_j|odHoJXw zMI~u~!eEJtV{nB)WTP3#j9X>k4Pw)daYrUat7T~CfqE5z{gv^U-Bc*sz!^vL!t1^- z$oAdS+V7<_*@#mRQ^ZZXdx=G(tB-zhQg8VwJJ}Klq@10kd04*oG(504RHih^^K2O; z3ug(RF|fySbc1wYix>7hTg~HaU4Gp1RKYM?0{md4;PR02LJiGwQ18lQI~a#)m({6y z_tWUCHuD}G0=$?raN9Ys1WUkia;#D1nDG1(i0Q}WD9ah}Vq~@slJ;zj)Zgd$_IFez z4%5w+m{@BfyEyW>RD0cA#-;>Gu|VG+eNw$}8o#+BIRV08nMegNn-Gp$57LXhoBnIG zx_bheQ49`;rc+#uPvw4w6D8KtB4}-}a7m^nEcwCBW|=H2^pg{%~|2 z*UW4?a0y$Bh^rvuw00s2+_GQuopF|wN~Q+8)}24Lx_3sqRO^!a4-atOeyCU|OM@DaUsi5);IsW% zw(hbSx}>?DFnV}u6cNYyuFR%`25df8SuPW)R0^Yj|I1NNSh?FYAqw5=Ak(PDV>1Vg z-6*u}y`z_0$0tZMw^ue6$G_NmbW_5J-sCpra1*PJLU3zyjF%rsT7wK!QZh=M-4s!u zSXt-?=W?L)ch}b+kOci#fMjqB` zH3J!(6`;4`=Y9xA@-)0*DbVIPuF0l zYbw6ArTV;GtIR)V30S=(h`+hEhHL2ibk9Typjer>YZ@|cIwXKAJrYw{mD@g3Csog? z!=*RIfKs-MapRrZ%Ut}_WH-~O*PW?eeRIBM=%TW*Y`up(GODZ0&Y%^;`sUK5jHai` zld8+Y3VzoV?F9{No~1oVY4k!KhD=W?9vT@(6%-_q_+&T%q=Ia(_CS+&F3IxBQF0ka zbx7&jPHD+J6XGnLv$HZ@9evp<65VSVxeDu&ldl56T;g}q^hVZXbdyDF(n3x1ZkBnq zM`EE~0+uBViO{1Wot|C8CYey%N_D0X+8rlLQLD~^9EmmqG8?ZS${KUS^;On6kgWW2 zg0zdsBn;11=uuNem5cqc;0Etz{g**Tz}1Ipx5J_HPb&%}>c-ui9q1$3c@d+QPH4Da zGizYEjdPVT8%+TDK2glY44G(&_@yqwMId6ht_jR85Mx+8Pm~G(=g^&>{+XN0TX=w7 zw@5Y$QjShVQj}Bw`4 zt2!{U7-iOkMSUJw$Z!ETvbd~Y6UvA;qWik@!Cz1DS{6nVpxK@w-eYMN@-M*!#sX<-ccSY059cdCkyP_ZK|3MNfaVfCQA8+SNXtM88R3p z;^j-9HRbRc;b^m_M}FIi_`4$i@H!s;aS8as3c@(73D(j3g>;we9N~=R*uJ zOCrqQy@1%vSi)O|I{#Z{olYACJo7Szn*QC$Z?*ET_+SScwa=B7^uO9~#y@QSnK835 zik{z?#b6EeRM`9dUjx6prpx*G-?C-)n}3W0u9+v1R~%IDUfT=ycsPk?^6=J03jQwj zg59w*F|{%#d(QH^0QnCyz`FCT1kJw);$Wlm(}_vAY#%+Zru`tFY$e82q`ty?UMgG< zPFpIuQF3Yw?1Yd(YG5OQ5+(QTqBG#LpElJgrb=hKG%TB^K-1-5FP} z!WM$vgWP=A{1R%g;0~F8-2QM2LuWR-ARm26l-#`%HY{b>Uw?a@b7?&Up(dZLmKgah z!vb^Q*P6ZaXF8y7+dlNfePYK&{8c>n5CYLN3>BYEcGRl0B+JMiZB2#C9juOb|3-dw zzgO;fpgWC4UKi|PUpxGx7BR=)hxgzhPaN6s3%XDVXGPqX-op7);;wh?Zc_$QTs^g{ zONRqzP)5g;r^B>M8VM-=sFAQvGKNP#AgP`P@r)}A3uQ8*oNT+^wsMa2A)Kne88jP` z3WFNOO4(+91XmhpzucOGHZBlsR}qLp_A|1SG-$kj9TLCpdGjnS64Q9^li2VCd70Jg zRZl*vP?6MfhDV8OmU_(0&Y@JrU|e2WM|Q&zb>|PEIW}mO4wQvhTS$fog)vwV>HEeydajI-+z?#dZ42f#aKTEacgW6wNaDx)Fp*Ao-UV z9}Bm0Z8elmPq@3x__=;Ze=EjH1r9jZc(xa|xA4vbC@L>Fk262iP+c`6njZfn+p438 z(k<)FXnb5*)dgu~X^>9wfPPWGkL6z2cIRV1+XfN{x#ZGuENtuSfpWE-~Sxg+DzrN3$S;$6)beFv!in{L;WQ~IYJYDXO!j$2eIMY!Q|4>)%;wM)5WHqZ zM40Vpa;PLc*Ud5SJmEc%i9JO+vT6WRRV+q~x z>zoUYrfTVi4i0%+8h~^Lq>z@BP)RB1I`16P6r|IKA zL?9II;c%d15#y(uPA@g2lBlkH_!Z9TNUlfPp=MPq9ac-AVN zSv{B-?HYIE>s)lR)jwZ2M|HE}=UD}P@|jiK{pRR5qkR{?bHgiMMXldDO^jeRLhIJg z4O8r(cVR@eoVB%=Jz)@yUW+MEjq^Ix`Q7I(u0?aS{J?mv3dZ~Nk*24NtXBH|Ft2wM zRu-Z`rXez~SB27ydn+OetF*P9+boc<*hq-(R33zP&&{osB$;FwXzzJFc8GvM$D0Zbp)(VyQbdMvv)Mv0t5 zLpb9Ywv=sI0HOD81s{{CVsaaU={G9}U5{VoSTybL&Yx%iY#At$G9GRt2bu?79Grtw zXXvnEW>)x_t?Fk(!!z=67_NUD)4jnODC!cUg;G4;hM{&Zh^p&Oyq7M$4? zZ~vYUIoKom%iwPbzGG;htFm<_fe3-i^Bo+pFLa}2G?`6E){4ik`oq#I0m%fg(9PbN z7r5L$VVg%&nWABYYi`NE(QWKJJZ)ib3~bIsqKSfv#5#*O<>!s!f}>%(IQi5+&~HKF>~_&1m2$3;C4iKmG^*m77MN|e%2e9 z3%Su403aC*^yUk&yGgm;241@olb!yF7smD-AAI&C$Hw-eM&+acvlZo}fyG$-myLXT zJ;0Xwec?Xj4VG6N+50XIxg*%vfc@7eMhL2O=9lxek1N-gJ#jh2Dgz6K;><3C!$zn= z-pdix%b4Dx0xi@NqlY#k4CxSXwnKa$@GYZFKj|w!d$KDWzy| z8k^$nHVBcT*CXV{xFQK}&dgOiJuu%}b*V%YM7X)XNax!N(@mvEih|I-+(scokd*>K z34y5=NT*zq7@q`-=zMVm)$E+%iAuyc&1j|UnAmhCFS(rqm3IE_v6;^jWzmB@2;AXkqu;=h&jQ6MzWE9xd*V`gvSal&9EZpwg zAGqYg_*PyeF_7i=H<%lO2+9_Y$1gSEr;)Tc9vy3CgJ;EQY{bWt!r3QmKX#>;Ayuop zWl$4wjw>IoH%EZRPEE9uhX>=_fz(LjpzM1RDya9h9eBXk5k!lDuZ)axan!s+iM`H= z4$W~6(>}m)TUO<~SJDnOfj07$z`i3l1J%=20L0LDj9QFXT=)Xlz*i3>7C=Rg+Bw9u784`2;H%O&GqjC z76X}(RpJMyu$3wmqMb)8fwrA&oU514^-sR?l`$HB^FS5x-mPt1Jp;(+Tf>y@4%N>z z@-07(|CyD{#f=;b&j&H4MOoK-R+f;`lJ{Rb^5Ym~8cN>CexvEZs%N&w} zss+l5g!&DNV^vwHek~u`qoG;ZEQEe^B0&bZob$xn0xj_dKC9I(g6Kc|Rnj&t%!2@c z0X=cw+8SaEto?kG(`|47-}I8p@J)D!g6@1IuhELvIy%u_9}B&3R1OvQbW4{MprRQX zOvIAGjpsW4&U4P5X>heIIzMmp!wg>fH@x-^ZMVan9x#3Vr-ga6shjzMIr8MdcI5?| z0ki%p_P3z844TKq$=2nG*5%8vN-FC$`f=}NMJ5oZguXb(K8}Z@*A*F0pXr<0eVN+K zY;xQT?<8+Mj<)Yr37beBM4CVCtgdUn*hO^kf1*cF@bipgwqH=Hpy}rJ@fUiN2p|~t zNxD@|6H3k=cQGxu29^z$#i0idsGiM84k+86P+0jiPlP46T~VZeV~MT5X8vhAmWq_s z+X{?Kl_Ueha+S8mUp3$B1G}Jx8HOkaUe_WwR*QS^PbCMzQ}LA|Z0h@M9G>4y&)oev z>t-H(;>8=-*p$$eSXm%zRYSp_2wwx2h|H&=` zn=E}0|9|IeT8C`HhmQ6vi33@LhdX5L>l)61u6MhEA=O2r45j}>(4;QHWPqrB<$Up0 zu_m+D^kL7|P7sdU0~9kAXnkINlGWt8a@b+jh8FtgCBEBUp(flC%g*AJU#falV7a4v ziO!=Uke!H6nJ*=u|Fzq<3sX$WU3WlD@EnJ>m`Ba%!M)_Hef{Dy z$@|O51b>Oe1~*#O;ly7h7G~uRVcw_?3GqF2OnkN4J2B_b9++R)$-?CZ z-A=}Y9+YgY-o#hI>4{Cecq46<^HDON`+nSA{0DV70Vp|uv8#3k1AwaOkN-6YzUzLn z54^_(j)FQk(l1_EV(<2vNr-3`j%J0cp$pSbeS7LXB;?_=Mwp~lSc;2FFC>8bon~my z1JV+n&io;S($SfPkeTn51Ouvw?Onq~CAuRNV8f!T_KW23sX0_-Ps{O6p_8RQJ{$x- zTda??$%nGmLNh#%fT#Q$+wAlFD%ST7E>0i!=GDB)lyXE>*Z3Z@Csr0LgY0r!$>|?A zK&DJc`gAo}2`?g-WIK$D8Pr70GM>OHu&c$0sm01lGx#`CN1u%?VtZ(B=|f4^V1yGY*tM>Ft=r= z<8kHs@QJSF-ls^y!~*X{KK#Ve<2*Bfej!dyS{*eFOUvj4DDISz&M$|;|7kL>bN?Sn zFa_uU|D_gr2U8+&c1me!j;naYJZmP3STvgeM&FcoMkkQf;%fJYuk#ffg6%e9gj4+YFOWQ|Hk`Kscwj-#t zk?O+@1RS7&#NGT+o?*e}NF*Z3z_tui(?Cnojx>IbJ_>_gE;6ZR$QMUK${XY6U?hZs z@7Y0p)4l^tBL|LOT;t#WBW0Z4_rS>WVeCSEy)bqnk2;SpSS}=wM@XVJ8XOI}vCrZt z`_kh9&i5ZycFsX@HO|kp#&xxuH7Wy_Jv@zKDsdbjP@pmLA!dzoeA>awloxh$-V~t( zf2$&1diNvd)^c}5cL+7S=xQrFHRO~oiANd3ca)$lC6p53hMh)0mKj^B>~=N1uXZ4H zl5%^+1W0QeU4ItJ7xMy02=rs}ZztA&|C%wX4?<8-()uL246-U7R~<>_rDxr)2SePZ zFeU0OIYjcRnXSqAKs^;f$f#&1M3x!%`ur@^|XGoyg zyv^TQ-T+YI*0(0Cyul*O_Cmrrd~r*!yY|ZGK;6XH53%2>1c^!rH9kI^by0#`FTY?noMnuXIG zJOh65TNP!XjCk0*iu}{9{jIGEU^$3ocUIjDxN?=Lxj9nNhQ{GaX{#ePhZR_Mz;!AiJL$)nRs znS8exS8aF2O`AMbs)Rl*rXGLCWLI3S=?R%hQ%EKk$tQQ`y?_-o0AKtMJ@Z_(2L=#N z|Ia&R+lv#-FQLCwG%n@I_kl|EB z{rKwme7Dm@m~p=TBtQT1O+Y}cKLG!2OFQ@LHLR)Ugytl79loPFBtZ$#fXuRS@2aob zX^|QvX2Wlo&}3jI?Mo^J1J*{~e#mYU_@T?!#sNd56b-&jBe&|a?bO93G7_$>xF<_; z6gc3ipCdyruXH!8Ljt&>7IJlWHqUjU4CefGE{tBp7^324;k{LnH7PkJ9ha6wc^Vb9 z6#G`q-E{O7&1bP(fY|sCys-;$vF-m~k5DX{%bdb83Q3a&iA>h0usj3ypKcGIuIet; zfR`frUVOZ4f_?2R?oU|Mww3?NtLTpqaBI-OtMEe+w^3a15zIOor(3Q?)v5s4P9$wbxucr$;4y&NDFfgT zfdCoX{9U=hNoa*9tb((f;q1{aqLNX`f zc=U;Qml|DkLWo&`EG65U!ND)8L+eKjcNrp;4)WoPpX2g%jgAh|nur;n3$C}fnMi}K zUU;=V1NKJCa)o9_qYY~BV%=30s9v%p=3QP;huTcPmUgzV>EhlZwzM}?4k(=1&I-w? zhjL<5X`Hl0Ug&qLRi_*`IG0eb>0I8sHhp=2KPMOT&Zok>9qNdN2Jl=Q+jUWX*%H~B zaz^Bno$bIsVF|ZngL)|x3BpSX?V|Ny@7iUi{xNLdWtlOreGUKd?n{SKo-{r8`1N>!=CP%-3+$bL1v0%8srNWDbT8ZIrU-oXVB!v~o?dY#cB!A^0s>B7KZKfvOFzAW zB}uIxj3>S=+09OMf*vSQ@F_GOO_2C>?^O6LXJpm8gLeC&-f4koOfo%gZqe zx``l7ecvYKt<|ur*5MbK!41ml#7I5K2>s2<3x-_UnFufohnrPG(O0;rMpi1BM8Y2q zQRh0!GKF&dyD~-ObuGSR$y8UbjO}r;9eNS>gS!x9dT=j;{%DJX~8Y|F-*Q+yH`b#!4GXz)#fb!&ggA_C`)%0ya252ffEehJK5CjmepU6{F7S`GDD-nKfJ&irKk zbsMS`Dtd9>c%AZ8>GAN*&VLY&zrBP0+N}}jL{7!|HwfV2)C^vs>M-@1>!RvM*GPlv z((2(|hEiE*R*GI+h1bVz_uFsry~?)QJFm`Rq(+n~6{joU_dX_|jSmZq`U&j;OnK7E z$-=oF%79Z;7>?!<0L3_Me4S}THXjoHC1i%q*{C8J&C_&I=`OPUArB3#`+YGPaqr37 z_TPfryz-iGjHyWR9VPKL?A<$!p1V9I(+msSGoayOXaHC846aNHxN>Q_EhUdhC}Szn zglRknxwac3(U$Ty8B3@2PT^Tq%TydtPjJ~RDyCy7(-muJ5&~67hg%?lYwnoB8GM%u zL3BIF_N<*W&^E~L#TeYJ$K}g}*`jko5;}5r9?dW>_h>}twJnd-K}a7pBB;5R55F3&3PMv zXRLa+6W=$Xk*&*EQ`g=1vv@U!%_b!?^E{|dOoRN2>^sF#Xp-wIy1|%GFt-MzNd|=} zj3Lkn8569xvQ#!Mi|%QD;|Re!2lH&=@X=;BsZ@9Weo0- zt})5D>3xy6XZL#VWbHYEH|Ww0yDc)~In1%#GAbX-9&w^AB>G}pmCY8HmZoC{XQin|lf-Ymc|M2j1I%J1!F(4s zY7PmT%bFQ{(BvwgxP!RD>$AliBjBUPL5Gl{aZY7OS*Vh=pc>2sQe2w*k^GUr5+F7p zxgQ#d>Z-ShWb?+sXA8&DtPw2HeCcGav*fmA zSQNJb-LY4$LQTdM@h~ugEd5k{1nJdQvKy6Ar&DSxA1YZ4kJf9?=DqV zp=PvLv{9R83W0AU5{+uguQ3Q2FD+XO9kPmU?6Dbs!2poq2U=aG``PU~ko; zpqSx5B25{g=t*$%T1^0Yd=@Wr9h<%9Nn=KFiFu{>z@=!gh-7AbT&XGR@Hf{o!Q&-t zXM(L--2EQ`!lr@wDu9iO!Dz%pXeUp7cg{`3mO>5Ka|mmtUaCEORzb=nnT@?gGQ6{= z&?e@9dJQux+r4g8J+N2VB#rrsTtWMuV6L~h(iyy@1zlFUom1=X21RttC?49CI^p`{ zlJG>8Md$2wRbC&<6I80B>SQcpOCnkXL+ucGzw*efY+4OrLn7tnBBgRJj!`pW2DIzn zO0yBt>M?kcmm$p>BYM6)ySTZt)VtGTk7$+79{!o}#c68@%Zq7n^~KL2wF@8R_k2(o z7^`}93ZeAiR-96~*-d*A3g4DIJ-K0H#1fLh*5Fd;Gu97^&c%qhd9L)un%85#7BY(& zcB=JOEsRl{qm2fR_>p?r?_TsRaZQ&Z~RunmCA)UX)~JB4T;Q7r%>A>&>1; z6T5ZcPQdNJ!tFf`a3PSDK#SY%v;JQDH9n4Q7Ca0o3mE1JqC2z+D~fQeQ3Nf`TI}GK z4-n^m!gJdmoB{_GzMcML9yy+*I0wIu3{uHvB#*=9hR1k^oP$#44g=rpDoaXxi8Vdo zkx5(7<8W9=Bjn+BuShSF%JG%77ud3z7LPYSm;8NI>&GMne}mD=_#O2BB6&U=Fhk+007 z*Iw~uE*}n&LBRHi-X&yOm~C9RLWwy;Mt;I*VIdv|_(v-GPO;b^i!|mkS8XbNk3-15 zZ!R;)AR7_`AA*U@U!d)K{7S8{h10y(e-f1!C{c>R5XGT$c=kj9O%svr$#^88JyvD; zkF7ny^t8AK5l6ZBeREsYwZbZge5`cc^5o#3T?_C>W{P$+V+)$qa7FQag-&-AU$Uv| zdcKl4pMAS_3yS#4uN*!HZ)RsT^{I&iV&3@=?0(!!>`!}Q9ygt+bLKhKQArkYh(%>U z1TmXTq)Nj|ef6@eaG};Wt^(D=DmzK67Fw8iwM)P>wY>?9ewLoTyZvhq?lz=8b8Hh~4(2UmY)J()j^npV6ExQkQ}{KRJSPTJ%6LZ<(Gr|X zx+nd^SAZ*oEEjwKELGiMYiA3+VSUi0sjD8-(MdvtewN4eInXCtqL0|%u{*QxVc6Ej zv*R^~LcAx~k&RKQR1y;02W%iLjQskt+ircOzxe0kSB`7gF&IX3#jY9%XW5co7LE~Q z1L*m#jCY6CfGUD9(>Pt-8I&<$PJikdztv>^Q zHmFp4g$&*3GmB$1#+=IdZMgvY0f36aT4RHAh}7x?-%#PVRAsirQFEBSY1&`09PtZg zFIB)D_>K@Ct}QNFd!}omvs3QoZuXtRA+%6jF^MK&D<*0h2GYq{GFra0{%4U;W-Tgs z4FN!|?vVrV4%IOLkiTITUXVoYESql8Lr|=0jv^w8D$wriTD|1DaJ-3UD`DQAaYK1B z-%U-%5>@|3A#u$M+D;Dh1_)S6gum(RiI!ZTR_BAyfry!=8LX4Z^uB{Q|1`f=oT5Zv z?~m=T#Ca?xp;4~r5zG$wF6wp3K(PLviQze>8Z_GrEVlq4z9RT@NOw{$l=K9wZzCG6 z6P@Dk3eWpGG{#(&2P(xdr!u8?$wTZxupgFSp4GFxYUj}u5^>(EKOYlu%5l%;b1zr; z(DUI_ZFE6QC2MnBnRmpavr&r9}(&uO$CU3@AC!yXmX|mkun|ij?uZFmaS-4eKLBM5>iDVSzW_ z%~Ap#`(I1iPQ7~6{BA!}?%&lrzxxY7_`CEtO9=7XuaD@ba_x38i!MwVH1=a(z>R|c zA6ai17v=Z$ZPVQ)NQiVxcO#wB-8rNT-6;*yIh5qkEz%N04&4YU-3pQdKBvEbUC-x! z&AVameeS*1cO8c`8Q7Ljq4396$Ou4u%%$5iny<4qygNvAG8Qbk#8zyO(i=`p&EA-N zk6&5e-6@qQ?SSizx0Y--GRpZ$T(3Z9R?Mg8YJ6uybNL2YOszXYPm5aIS2pmPM)|q5b<9D4gKH6Ezd)TzP8Qw>D!hO|RTZyX$N zB;%+rFZQh#mfH3s-da(Eqk^OsfdyL{DJ%+$q!`aKK<_`qtUc#2$n8V^X7|#~O0!V! zKf02XKOL7dVlQ+{_~b!H1^z_%RL)InZlxNf0P4`dwTm}1x!R|FtxHOsdT$i+R{bn? z+8+eM)NZGSSzc{r zG#0%adzE}A^Ry4v=YI4lo-fguxzup)Yql+#8ajOtP;}D{ zpqUmL^sfwjsaq^+8!{aL>a|8ldh_PWy?-W)WnnLvIEMf{OoFj59oxi@@UjlvEU7yB zJW0c@;Ob+!&!R?jt4aNN#ve1w7>tWmDzQl*0sl~P)b__GT^bT>a76xf<6!@uiekP@ z*=XL9tUld<5`49|=xqK+8k1WZ=m(P0MbQ?`m+S5@7vm9gE511!@*$R8ldaV9z$&i^ z@LKe#r$|svS|>fRg|-8Y37{SBpCq*W3_wqtDHsn3M8|DhY(iHyL9k@K~pk;?)BG`It!zgG}^9y-Z2{w981!b_tap( z;fCe~r>JbX-P?Gf!zU3U%(UUWzSs~0);R()tUr#6CB>K#r7FRorV5*Nz410pI&{NB z8nGENHMjJosRfftg<|^s=LwGOWYccR8zdrN0@Ba6g)f`yl}Rx^hQXZs2&DFd6eOMm zj<@KQRFM$-Nu*h*kD}tEr@H%a6h@E7R)rNJ6>J4JvfL^QCDxwa+n_E?b}w%s`_O@7 zG6I6g0izAlS^M!{$_J7!5dRkCBz68!r+?ZKqSTWY(b~$>pouuGUi+Z+^B&ZuG8Y+y zKv`RjqZ=z^0BAlJ(7bU@Vp9xKo$cYc5xfo(S)#i22huP%rj?aSRD$BcVzdT!7G%|4 zHCSsB&}aNBt(>U2>l)Aqx>I*I|L+;=@7rjj6&?GoYmuqz>?pU6N!JLa(|=Dq0Ov`> ztc1_If!Zvku1KBD{e=g=G_~;-H89DWsFJ55vX|-B|&UU zKMOp}Np^PP%bE_dMs?V<1Q3tMy54Fo0RjXD@2LF!tlQwK8<@h^NgotZG_Y=_hcBUB z@4SBc6cvqpF-%(hpU7sa9okt$%5TEj0# zRb8vE$O>r7|NWET!vK|AUGm;6-#u|B3+&i}BU0oP!<~D@JROSVT}G-37o#Td&D8u_ zOQ5*k_|AEC$xUx|iBpIjHdsST*pI4Y)LyDD%l6Om4F;AkfKvGG`l8B!)8mWizXKlb zb!}e8h2(}jf2a~TgsvvV<5H{I)W^!4ma<|fd;^_&jAbC|E*=iL7bhJ}!^N;-T18me z=~1QA-i=6{U+`8lHf?@mrnZT5lBwZU+=gHH;(Q&^89pXHGpZ1Dl*jMV!}PBJ@$w6or0kuO<{1NBIl-Ew_8>QV0Xv~gINv(J130QS<2Y!_=ZmbSYYdyozZ! z;|`d}jrZwGqWyWM*AqM~O|>~_9gSGUk7`VH0pUXC zHGc<`?|-#U|F*~s{EmR!?$iKhxL;Q!qyfce>EaNIRRlh&N>c~^ zl)8F=r1^sEtAPD15NRVs&9y~iH4w!M82vc&fzoc|++-K@?TxNX66(w-lD{ zVv6(j+k%C|k&r|VW#Uj{)V^ZesByb!!hEY{ zsjlOh6ms~WF|Okx?#gh~7XSfgy?W2~4vf3%@BzsoIHj9QWdICiDKGYLjW6{X1Tium z2~~zz{I8&Pj_&h4y+QKlFLmc0pjq_KJ0c&C7m>|T+5rp680Tyf%S%wFf8_f(=>o>g zY*x%;Z5>|E?szePqS!K-$fRI^$w1~i_V53ip3b4j)Rz-gAnlE!^&7yhK6hQ61uBm( z00=Y!1Mj}@Z9x_F0h9Bop#B2bp<@# zn}0_5_DJ{FD}{+c|{juVx+eAq_bDcCS|8+N;n(eM{gn> z;34zG^}u(dS6e3g@I4r)F9__{!Lw?ca|I!Nw0S1bK2; ztK}1%`+5S5mBts#(t%*ez1~!69jde1@EW|SL-cjHNcwm;fQ9|giYYgcyx}@MoEV=V z*)Y{FTwZD}OnsDSx&N)q0)2ibk`0WvA2lzc{L7!sP+MV%XT0XDAs{5rEJ3uHICLoC9>Sv_^86NOx!Z*)3#f=)M)V-7(} zZ>f>M2>OxsI)}O>_euY!6(nKFEO`e7X6%4;GvIjXBU?H2o!E_JW%JARq66xJ`<+{kP7$^X!e~H)pmLB3u@H%+1FWT|(I3KR)rULh(}sI$ z$nBj>O{_zuT~F7bUS9O`;Rmq(dezLH{>l5vg@HPTu*jskVGtkGFtpoTKHXUf5@SydalLVSABC{+CLu6eWrfz~BT&vH)tN zaMFE7A-gG{^(byfr9oy;Z!Q9Bvq)|;TwR_|d#i7k%n}yQhWrk@-W|s$pi5Qi#xOq) z(IVd~Pi5x&MBNprFAkt95r^nz@`V-ENtPDE^1gKiyt}o%+TyYYKt|3CZ!w}ouqJcC z^10;YQ5BylGDXUZ%jb5p)RwpAuOYfxk@8d)G(5{M@Lm=A?X5FQwMw2$tN%cCLIwL?jP&g#!pv`9s)a4*khgTo6mn*Z{%t>8a zL@OgMwewzH_>Y~|$m9pQ)}`^+JUw`vj2E=zu@+?|GFBrgt#_)li9J3uFx?_K;V(wk zJH%;ZL;+!;h};ZSztTO< zN@sZbI}N6|kBF3g3j>PSiX-a_72%wsL0DRDwdp_j1V>haE#W8YhaM7C8a@B!^aZxb(vIqWqoVBdP_yz;nSRz zp|%)!RZg3*ug>wU)%RWRU_#=Ly4=X5Q7jO>s!ZvjV*6$;tUmtMs683#m1)f$b$&n$ zgUZ9Lo#o9vaaSj+8{%^-S-l33PyfA<@lLuCyzkkH0`K>B8~*bR=|Eqtn!y|pXO>m% zB8t{1%N^H^;jlZ$pTIDZl;tU!O4{z zV|x1>AtG3BMQ4o4c8^%T!ibv5P{mZ!uGXSgt5DvL6-H9C=PSjhB>R2IN$Y27T(WLk zY=@sJ6m=+F&LpnX?T#m=x_$>82F#gzevukJJ|;utEEGdmm?Ra^bz$mVfVVx*6#1gt zqd5W!M=*({Yv32YSXeU;#UVuT$&+P9t2HL%jCgQ!6e7{FRSx4jFYw_|LtIzRS_Nsz zXm)C2prVTa!D6E+(*oEs%24cKQ=4sEQL>*es#6QwMTpL+;itl?1onzi!{$~iyLt=+ zm6s#{vCP!)L>%yw{=1ms&<|%X*09(GYi@Nv=mCM_ie2gxasT&(B{g6q0peMwqx90Y zYz-l~&GRK?zQC&mogxP ztc;qc?GeOywE12OR=MCOeDs<6+Gj$lr^`%QwWd$P2IT3i#|#bveVifEMM{W{o1&*V zT+JaSA%!~*sYW;%JVRLcCcpA*MiccQKJ9s*vkt809clv<=faII*M}e%W!Ko$h`s&%+Ves z<~*^8I@8o%(**!P-1Lb@!W`f3HmPyBe7_TXZfM>Pe27N4<=8w)&H$fdl^lq!ZQ?XqcZsu5C_!JeDkGAn`(kzpQ zu(#T=6LsozXtG>UX@z;(k5A;yHa3M^3m`Ii$&+ngIhVUH`o+M2L;G69$J|LZ5q^tm zk!uD{eLm_xN&u(5=(*H(E+eQQM@wnFEZ#H_87W|At&H6hW%J;Yx81)~rbs?KIowGk zNcBY*f;6g1pa1=@k?)Jkl)&wJ<6lCC24pe!u>b!L`KQ9tAy`c>ir~3RT)4@`t^H9Q zPMS-(?~rfH(1$aCKQVFHMP?%jBov#rhT8G+)#Q^n_P-?;w*~zCTC)wJ22w#Z6wo?i zP<)W~pMuEe)PF<^NY}~~H${NPUg?4eUY!BV`*WtB>o1=q-ilEVeE_bH@q9!$JN z{CVDwGYufxU_A!EUJ5XMr*vLhWYn$QV935}x5`kf@0#QorkJQ!w4x&JjYAk$4GqiL zlx*8t`APfxBZE5Hn|;Ucnc_@(2fqZ_FQ;?S-V?8UCcvOlAPs+kZEoJNaP(jUm6E86 zK+5eebInI7MM}^l&!$e1lQr+*vL7p{4qGm=Ld~#C%)rK~Mg*5&lS{bq1F;t~ARcQz zwI2=Rd z{n*DuF;sCfD;k@=-*Dt1r`h4D#J4?Rj*J>P_Rx3-{v17hf__SE)Mr&M&t~zKdmmINJ#6 zwVHv1uqT}qM%QPE>Yvs%Z~JS*tR%r~4&Cks0+UuZ}aK124TF%SOrG<<0+YLqNdi ze@APe%b=b+zz1pwhy2WOI;0pp?Qn~&l_YyZo7gHgw5eh@qmU_ldffYG(zuZ$&YrJY z#?x|Wl>Pmnt0!ZnR^ZvLw`?LhGZA^#1Z7 z0G<6zg(`XzgNo1W)YBYQ%lItAbWND|`Z-nWsXy7lFW*qR#>>3v<9<)K965ARG)uKi z%MsY`I(2;@sA+IH-?^?lu~JyccI&vT9pkKot5t9PZI6lpdA1gV7VV}hLmTrTl+XsnOnw$rh>e&=@+z9G3$Kc z_G9^!qw11=Xz!L|P-noI%ianuIkJ>C=G}1Ga^T|bupiH#=X{Lqx0(>ka=?gMs9`sz zx0hn`!~WN#zc6EPQLBlB;aVot3%=i-*Ntlu<10}JIK~m*VbWdMY>g+r-;Zmeo_v4m z>@2kEBamE;R(A^ONSbno>Ef!upQjS${@_sS70;<*F|_<-0P09Rz!zX)uFPeMQ|>)&^+EgQ=EfP_d$ zDC$WoaaZbZncVra*CTF;;_@Tn*tM&!HbRn{cnYH?ms5wB24Yv!P0BfURyi`w)xc|4 zvh?KphlRD?20d}`$f#AITk7_{e2@p;QSRmOx5Og+FDORE$wS#D8FzB3oktbpm0$(7 zT_I>JAdLJJRL~k+PD!GuPMt?`GK!*0tM1&jHkA1s5S;n0uNXHgM8ox`CG0tz*#^#y zw{DA`Dy-A=%hUk4yeV6(ip~!N!PPk0qcFcQR90A~Zx#z81uKuX)z7X*!5FM44p!kX zwY?K}+uy7_+^AQdDZ-|Dsj+-5RkrSqzN`m(6c_#330dt8?J z{@aDw?uRG+KTFo2KUty$d&FZ_?uBABoFjy+OMOAGhC=GsF?10WCekI-$u~`SNy4Kk z@srKZIdhpg4q- zi&?zz)p!iBtX!&Ojz`Ok!$`$Fz`!80vvk}~pCUnv)^Nds{PCG5FmWSLTH?9)LzVjD z*Hcn^8ut=HfD%F?goDub4)I9Wb(bVZhig%^?i34wH_i7ux0CskS z9pRt+mJo>oZOyg-Lw$v@@457x1~9NT0WjG~WLD_)rB3o!!TnV~yxclQx5UPr=7V-+ zo4M1cls4VCN&^?}{k|^?mhg}oENav-aBGGHq5{wD9qicEj)pwTjq6$T29rKGb19~b z@bf;SO|DJg=Au{+V+{_QtbNs=ukj2SoRioeaT zR!l6hVNF=E#sMrGak`G2`Md+MtIqG}WUZ;6^C<``u`0d@n8)}*$!TtGQJf>+Gzf)G zqvn*dShD+FnbW;F+#y3lB(Ni|>=fTommo)e`3zC1d;u*g#N@x5GI&7aWJ`3#zwZ=C z_Zx&!H!sE1m*uLJ&wmeHCU9s#8x2%%6Q2!lt;zPQ|M8ma|s#k@xl14=}6u|~>J=SE^f=G?t) z0aTa0`i0ec91HZV_ooZyLii9P=0>os-X8a7OSE&~A`q8X9>JCuP@yY}nMfhcmMDT5 zOJYfda%(^0gQcXLn4yDL7yTJ0UT0764fl*ScXKq^YefEaH1F@0IU6aFUn=q=@>3SX zKdT=#j-~)M*#-asE3YS}w;S05na_xp*1sxd%`lm(sE2L7T2@HZ#`d%-Gab+!P#e5T zWcT5E1~>)o1vX93F?`Du=sTI>;q?jFmgBN>b*_5oX`4)0|G^HzrSx^O%gE|fmC2@Z^UZgPEs_R(bqdUJk zm1HzGUnV2$r`1ivwy>A2;vHbq%dpqUxUO)*QqN7-9_FnhZi`+su=i`#&f9ob_2e-tlhsoYdeh@so+#Z-XJ|-y%EhN-KVOo$TH<3}4 zg4iA!RE?I`ygdmR<_% ziOb$~U?{X@Ai3J!g}3cuHn()VtLS=W)j6ZH%C`*FIo`gC1N0(aclC<`fm3sgg;GGU zxzT#&?SU8Ze5UL^gR9=nl&WG_a+x{g=P|9aA~s< z?oCvCR8R*+ouc)3PG592ZL>IC5H=F`-FZxJH+@U67?ot_YTgeZgL`_7R?W_4ts|6z z4%w<0b*G4J(vip{_Fd8-kr5pD5DbEs(GldBzLMJn;M&`V9*ZMB@y z+q`B*qei0uL=#dX76}LhFqM!0&7@vVdiHj@4475;*9tNMQVTWwY(Qph;lNKUMka~4 zX$xM{S_61tx^jnLvhz(9tOt;>;=Qu90D%n(e)JGvIaQgKae|alS>ry9tW``>?t7@H zN83D!kl8$WsQC#O{PZBAw=>cY^20f>MOENktvQWQ2HYW;>LKdUGjN&Ird$T=)yY-H zPUd#qBE1^D$VrJqhYyy_QH7<8%uN+@O;Z6439lN?bw5}{OE@6)<3LHn%4UGb_3II% zM|V&CmK~xzCsx`Rg|CSj6s)8fpmYKCE-c>aG@Gae&C2{3>>Hv^cuQIW!Y<4%R?cpb)N>RS_KWeGH-WJd`sbX zR?>>0ZcJk)dPj-r{Cws#=NTPSzr=RUUzqwRN*LMtbK&k4ki~Hbfp=ok+YP9*(ZF29 z4as?l-jWVR7Hm~yDfJ^0HYuWEK5up{6`7zsv4qWck-shpgxudlBJxgWtlKm`#ZMJ~ z0oe04u6wj0FQxh9-QFIC^d45^RI%%*BTQ`#hp7AX+T-f}{CEsoT#HkcM|{bvMQ=5> zA*~8e6pxmCllN}L6PKltUsAd9&F$Fr9j0;w=Y3e4d?*u@?YW ze)0ulB{fd&>fBG|JKn{>aHI)xL6@uMClD5-PWA<#i-ARhDrrP6Z^wCGt}M)FnAf!N zgZ1toug>W;h^4Ak^S%VnVW6fhEHp4Whmo(ikhq_OO^~=Fs>bu@Z@0K~wVdxWpN(uG zAEwPLN6G+CU}~X2>BiTjT)ydNM^u;dO(ZHhjjMtyeY;(YjNMIJzi%Z*wvjc9P)+h^ z>Ovc~9&dl#e*_%{a))yGn=SB@hVK>(cGkPu5?uXOzwVL9z6_nY`n9Zb$hqr)grsVs zPuFu+?FdulN@*B2{%=aKBNFng81Jj)eox2X09;^VeYjK{G4^8*9>8!dXN;`RE%DtTZ?F&=^OTS4;|8j~o33j_-Z8h^+-3*Sqef5@li7)d zkXHBfD@!xp4K}NPTR21UO?ejg4qcY}{2i?E#Nv_K;;9RC`gNiZInf`gl*QLZJ76(Zyym7Mu#2AIJ zpq(l^oTgy6*~nGf=RSa{S#{&G?7&oJ3J53OK~MJ_i&)hR%bg|uHV*)Nd$z%4dqS6i zG&Mjm40H!VM#~(1qO0uEh`+20b~C=D^P}<2RgC9)r}5i3&^icpG)P^y^uN$MdsrE} zJhlJqLtim&V+zVgG4(t2^Y1ZOjrQlVJD@n{wbq&AI4S~*IwY|Pk-{mY@EyddeznI$q5mO5MNET-iLQ6YA?K+-E2cvym4*F*$LPC|NlGZ!AQS(97`|kWDszrLhR-z?t?7h(nu7*vd zD7p6K&i*8lU4sz?G2u+PWhoQUJ*4Mb_NBduS>e*O)j{SD^oK&1;2d&wo3UwPrqeg_)+%1|D5cs zj>nIx+f%QTOcI4##u_@m%oes?LH!R*%S?fHT~bH`jhKmmf?#n=t}LHKoAQQVZoANf@k!(cW{2u^hMz#r1C|wM&m0nPBoA9W5N_T`PBfgg6B2ClXH;EO5shYp`-cvX~*8?#yJ@VG2kvc z0^I)_Q2y5J0VeFtCLYiSF$*DmRtvb8P?Dp_BgQ{QZhoP`O|_%5Lx6?B&4a-{ls~6d zCwr!>v|lBX*X=?(%>Dg?RaU7DP({bWG_+qq09a|(QcaA`bIW8z$jfg>9|4i0{V0(y zm#ay_b4~}>9=00xOkXsP2J_P`M4uhf({p!oc5QnN7h===#JsT@{{UAe^-pWp*KCNQ z_6szC)-9MKT1If7?J{Ng6AE3GtYZ2aoYfiIDl<5cRjIs#_&JL}<{}n>A@=+BYqeCK z|OMAC3-QU#%FgB8r625o=V3X)Uakgds2-edQ;X zUaZH$&}@J8-^Nq+0GzVqzaNV8tcGiPox>=%W^%yM9VMMp!E#8B*DbTmLt)7j^slhIG#n5`mjW1RM6y7PocgeqDzL^r zjjtQAmGd^}Q2#u@{BF(C;o+n9Q6O-C8jTK|p1zzU5uN$tb zg{%cFm}uU3Gee0z?eZ5y|BvPA?h*3_s|p!-*`5D=+2!r-aurEuk$1&tw35a8gE$R* zOCqxs0x_-n_R2O=p#r#Y6qzD{w2u zrXuBY|NXmjycQ5wgr(I8N#gRuy)CqsTY`4S6N+}V?vbM=53^`YV8^d zzU2JtQcnA2q?08>VwZ{T&dR@4)x_}y#IGxaT2e5G<%kI4L?ACGBVlsdcXjsv76FiN z{-q@o$M>m^`tRv7%_+6{(t(C+v~p&yeovWMIlnrW{?CfL=z`JR;cJ(DW#f9m(jU1M zA(bB8VP5pM4N`Bu=s0u>p97`4bOcI1MZqd?L$$&8CO!lR`12tLTTxlBxDl!7D_^r< zi$>&wI|skS^x2AqTqK|Mu%5iy^L(SO+x-VEz(wgp^rs-H3ad`tx$vD2vDi%zAL*#` z1PLZEzoHW(5(7?afJFuBRmU~g@fW4KJ+VnEN{aDj0VUZD<7ndx(>li~{nrTb55a7~ zLaA`5()@cm4EaS-pO}2*Ia|--3R)+??Q^R00n;5P$ZCATHF%rn;Ki7x0je>jsrKa$ zU_Q^3_iqc$s8_1`g^4zcXvMvf3Dwvgi?UbG!-jZzaS0(P?XfwgD|sofeY!RivXv>gXi8-NSgsZ_T_x47sbzc!awoF(4v_Pk?=dSYkrs%8D?AjiZ#SOOdGboEy+F5v=R8Vw3X)Z%@4+#4PD z0i+qLeFhZ>;l1iQolP9?>}j3%7NtkP<0{BJ-_$_J)L69y(={njSLZlU($bk^c1vzl z{`$8mFE4QwOLdknu#5x#!vXmch&z4uU0U=19nTuly@VY1uI3Ltf4)eCkzoEA~PWmie%2w9cx4 zUvvJetEWAl4LglZOsCq=)5-FW2U*UghlZc9y})<_bR}bq6LD($fP|t;{bnvT4xHs& zEod=hjScpdn9hODNIS4fCzF=P z`f75a=3-E6YXA|*|FC?#-y#x8;l49%p#g65-H!m?(yZP!HSfD!)9etNvsI&`Wdchd zWrk?QOw-K-wU6F!wG#O(A!LPYzre>5Xj|!yH-AemWy;%~QniB+QO?f~EYgZhEdf7! zt2&gps>(|jnm;8xJ&b%^kD%@}Bss^x`{Gc;>cw9NvQahw1Az?}%R4t`h12IwZcL|O z7Xk#nWLbE%9B+WiVOd*src2ZCAKQMqGMfeUIFVdkzp2)ecrENjxO8X*?pG;qK z_5_dO1iQ}B5`0dym1liDL|b3HX5-VRn)+m&Q*Oo?AyYNV&{zp`k&>L|x1c%-gM9^D zbkILfc4k}Hg+03VNG8qIqFqfP+R^Ygj}^)LwXAU$s~(1)mrF*?EcmgX-d-t8*@tg# z66Jb`s+tBTCi-gyEgdbZnTOWj`ofT`R|;3mFT)$Cl3)Ha9niEs!VDOA3^pIxt@%1kS-o0MkKEYbjRIi8 z^JLYD$k$mgJ+B}9(PWj+y|jln&S#LARIjng8m6nY1>OnfM@e=hT z$8m8l&lJw((F@zz-SX0m2pCy}=7JEa7>aA8wJZ+#8h=;kS*@}`m(dF-yi?}|qze+# zFnv?^$k(b*d`ZaDKy!KN+1OHVJlkDtY%<16{;o!Q-+b!xpBq}(@VAAONbOxZKB&W} zc-&1Y0-eNjwCLDi8n3DkP-=2Nb}Z`NqLN;<(lF=`R2PLYX;z6dUw-BGU_b&ulu|$b z;_(sbn2htc*$t)yBnLx);!OA7d${4r{ZH7+g%bjxpAa}?0L5}_ z<;paRpZT|k$*!sJpMsPAPP=R;yBtm_l#yQ?CmAz2okEMP_jTTFVb6|Sc@H@MvRB(3qbYYS|dk>&1{t`j1Up)-ewCb4|PgGWrBMP=ySW@yz@ESSVjYz$F8x0e)re z4~wb1tej3nQJ`STCiaJSBC4HNn!=Lz z@>(=#X`FY-I2x^BcD>=(o7VMq653~$h0y0`3HRs(Qczv0lLxndAN6|2U#o%9<4@lI z@YUQ+HktH!Je}QFpmX-i86r*9Z*dMDJ}P}`j$3pS6Nm=PoSJXwCSK_XiT@n`i{f!q zmSom@K{cY!p5HC#4e!jQ&zFTgc&U}s^FC}$@_|{^ON+pZVh0^TT2CgHFu?m6#Et5< zU*g{kPGxbOq<4rCkwPm5I}q*-{yu9bZg1(|eG?sK<#|j)cbul|BBP>bor|tx>(nvM zi3m@QDZ&V=%%CMCG)9L--Xliu8!dS>4~-ZbA=L#H0n z9O70Luf8Z+v64Fd{O1-$3EZFSxi^;$pLhB#dCX4Vx4bc*zyxT=y+$Z~!*tEh1=)jr zdkKMX+D*}yxhgN(6Q7-?Bboy?=&vFh&K%GbaC zrH9)hKdDVh1GYuSKii=8Rp_0}mhCF{G?JTdG|tY6@o!ml(Td4cH@c^FcaM&2pBgds zywsb?71!_3#qkW0y~X+S@d<`{_v|O)5U}gf?p4>(*vsiZ-MUxOh5@>FgKyGnH2{&L zRJ$=Q@rnih<5eK4*nWmdO{Vg(b6c0%9G}}b$``C4hen&nTOK@xlZ~ka;EzbTUp6v@ zq)7H!VIlh7Z~`mI`$HF5RqW;_*zUUVRciJyF|#VUuL7EJ%4 z83J|I7yhXq+1?SOdZxddeZ~t zy0-Ng;qSFiL(C0Buf=NZxEV#aV+B@ey_sGRj1S?*Xq zG?hRrqtgv&7n+++&e?L|%3wN563NJ@QU!q(uBP1BkEkLAe?y+rpI4&2#koGYO78SO zTrs*I|Ce*FAshLkC71tF?=D^3dP`& zhTnW=noC)XG<4Jlqo16O+!NGfU*W3#QE#*%j;jo1YFIBDo~x22C;s`myaj?Rm|o@8 zo?kz5*Vjey$5+Vroa%CxBJCD1DMFPjzh$2CpOO%*AGoFyE7Jj=Wp> zWWiSDeNclM_R5W`jAUoJvzRuM{b!P)R z@Qy8-qw7lzv6?ukhxnetQ{&zK zj_E2mm>&vMs?;l-R17efyh8Nqb#6mmEoe%U;?eHD!u{5N)vSv- z=8hzS?oV!VYMTC)Wm?pNXfNl;-+i46Z4cop$jJzEbFtl+SQ|?V<0uN071wxQ*ny5* zpzKojw%cRi^&o4tA1~ zUp2q_(aVx^fZ{h+V)O?;UCz?t^FSv9rlF*bGG64^%Np@HF(*aB=IIRVpunK|TEQoW zYb$gg^I#4soRv=8qZL81#>?-nUY7f_o~Rz?`mXcxYDncA&nXNsRVyQek02io_32um z3Zq?2s0n`x>k%MtF%&2&M4ZkIe+5jZU5@D`2^8~u_grSGgIH>4$P=lOUCDC)u&;Na zR{zy>>zPMqGStan!>Ov?snXeuZLYtCyy7Gww6;L4y5h9rFZcCIA*f?y0GL`$?Bv7S zEFE=vZyH{pY-jM9-1gI=8=oa#fEw0b)C>F4-v#4|DXw(mtfU3m4AqheN|kVMzFQlM zoP*m=OjCQD$Tt@Rhu&2*Pr}B7ZiY*2r>Q2)TS_wBBR*UZn5myWGV)4fmn~NA4|$~| z_F0c;TTgF5nRqRE`Pb zkRV8#0<hU?=a`@49$NDA#EB--H>d59V{BC-F*^uhEYLEa4RscaBmnw z(0D0aWwU_yV}y~I*fGOuef8ZjH(GsrLSJT z$YyWLdFZ}fH4p75s3GP-@iOLS7I4xni09ao}rc_L){ws)AYS>^-8a$T44W5owzJ;Na#Qa78aCk8^$Kh+H06F72v#S;JadALEd~vJhA1y%P6#3gg@x6| z*z=7jKv+K*(zQ`@bCBPwa!VI|O}Rl;MTvNo11Yl>Vcr`nw^eZg*IB?pxjMI{B0)hMbQ%B zo&zd4>dS|)WVctlE?-k=5JN;8k(FSqe&`1X8`gf2F}^d-?}TXm#n5`t+6edPI@dG= zPN7NMTWoR;eD|3PVBIW@zP$xdIOG`dq_%jcn?;&$g7(U*I*lmRj8#W*OKci5JE1)( z`cUE|dF$z?YB-Li+z0_t*1$J$=6nnfw*|PD=SGJaUTNArITUupkyObjI=on4Ek7J6=wE2X8wB0@s@nWf1PLuD|dYVXY1te zekCY)*14#)i=Pfpb7%2(EFG8k)Wc1`>B8)qVtKot&(@JQVySFUkR${OM)wMM+~o~@ zU$96YHHlEZ;2d>#n3k9;rwIwnG=^{`+(mLzI~S&pwI4{NVII`x*`oU(_$O2pp?Jx@ zXL#1^BxWAdSQj0wOJ4 zL$}f$0@9`DdEfJW=lp`1Ywu_8z1Cf8N8N7URTTXyyK6w~ytSI*+rOoRg*UdZ>oM3a zH$L;>vskn{FG@EhGuBHcMN2Czt((c@-Lux`?i;eH5NCw@lUWLtnC%~H<_cGj+L;SR z(4%j0PN!}!3DY6V35+f3b9;ydBQo6-7NA7*Iqtf2<5%Bn=9sf|;>%q%RBB8*bN!PS zy!>A-ck;pCFpgF_pw4t`P~-47nRy~#vJ|$Z&={U8M=Obu(B*1a>AjhJ)$) zaBhpuamvm^&*j;#%-kol4C+bhf#J9bkRwNbyST+28bt9^{4rm$g2 z#q$}4+JW+CisSJy1cZzxF386)Yw+p4Ch}(t*F})Yn4zh$W`7zlrBQn!x@6^5NZ>^G3ct43A8tsheVArNN>f1$=83yG7L6~!>B~3Q{ z)YHIU6o>&0s!&xmvOlV?vwc**FL%5#{>=DmWA~sARO+N1J~bYjZ=3pe5a3$idk$m6^hK%Kb!h)+EbfQ%#O@CCfEAOyIcbx41-LA;3_5f^ z(qs>OylU8b_nJC#B)}HrV^%ib$$c&}9d+I<{&7uPMS=1?rWXyBZ)w%Yg_?90?0b+) zF#*VqmjXNTBc`y5ar!e05lr2D*dXvae-09@Jr~p(Xj%-(BX{47L>IRmoj_XB%=CmN zcjn005gamCJ{pkN10IrW*UO2gS$8M%No^ zpDcZ>T#QT6%n$+D642VPI4a)m(381boMu){e5uC0La=9;fH~_Mq#zdL|%`@-p#RKMA zj0$jBhRe|{cN!sRT@wV0I@)H(K~(so;zIpRmlKe{KDoJTrCoD`0GDo03_}-mTEc=b zNi(CLOA&}CV8Nqy7_v_Y{4PhMA48F5$CSbq|H{y^kEA(Cd-(W@*sB|u0W>TL)xImEZP=O0EL;e79#4x5_4{CCNDYtOy@|B|DLqq;Mpf|)v5q1j1q zO2IjvemHwaz2duXUYHcO5Pa!Bu<&$?qRFlE=1!To?AAwi8qU{O`fEE45lSl}auzZf z`2ZwB7x_ic#XQBIaY8ooFEdD3^cHXJ_H$Q430bnt=lOhP@S4-btg51JWu~iLN@azh zw~4H6=m^@5Fe14g-PC^I-bb%PvASrKD>nW)Y#tyso}Rr-WidEE?SW?dACYA3rN)=o z(4MlPae=BTh)x;qYOL zvs?|8R;E{sD@d8=g3Pr+|DEe_Inu(Mn-Q0KtH~1kG7(B`*=yTk#;L3H=GmbFqpP4{ zNpeM3OWe5`SY@})nVc0G9%h(DR$?sck(p8DN}Ko*FO=^^`~Yyh4hl;5i{DfQy}+cv z+y44|Ml}?m&s!1MzSa@5I}Sv+1&bYH6=Z?;uOM_2j-<2^n9+=WZISB@>lVUJvvI`k=@K;U>~-hxGIu)1R|^ zN-{!PCzWrkx)R>L>C2`JQ!470QCobG3hEXgmV99y80esI6ri3R3K~`G)X`F8L}9Cn zeF>vZdo2S?@Qo_50H$KY)s4{MYD9shqkH-#hPHb@^Boho$FK4HP<8dQ)|-4Rb2ta* zXT2?eBZ6_jU`;z|%&_;}o+OKMZ_8VcHv;*rk$Ar|+xV(N+MRRFNWbDC>dFcq2Z!aE z4TQg0=*Z}?j#Z6PpMCs7Pnfz2**(aCx9v&x1-=!8h~mX1%59!jcA>hlE+y?Zd%WPf} z6Lu~C_jxr(W8noIE<6RYot#(WI%LwMgC5FilZ%=*rl~_MFNgN>|8q|6j5?mwo(KHD zgOURhpC|u*s1f?MBbu8ib4E=s(3}08Q4ZaSx>p}~&NeQuJ;2PB6&&6#fb;A9Db{mB zDh8S#*}VVOmM>)tkZ-wHa!q%-tB%6XX=YFT1c1?<3E8eCc|PiGu-uu4OYQXt23SHgVfXZAW=N`J`yescQ5& zdJ;7jcDL<5=~XNN=&*LmFYOZpd3KlR|a;ghu|)(}a_Idr96 zJTKol`^*@YO4|w9^tV*Vl->e*R-}(@S>rKdIiKdp(b>v1ymQ{7k~4?oTmJkX2Asjn zv8X1ur%fo1l4O^a+__iu;bj2n2Up89GPgM?C&lOph*4J*R+E5 zzfE@_x@B2B+%4AL+MG~!K&He12qjq7^Pff1wtFdvydops)u4pEZN=@pqW*c@tEgy8 zx?#rvN_b(T@_jM3Ea8Ycsatex9Hv_~Z_2rAO~SVFmZzQh8MiWQCIjm9(~GMNNJr=~ zE*vH*lh4A99YXf%DyCQeg$_L^vh!J~eQ)cB-5ylj?Q|ua6NCH-NDN`oA+Xg-tV71f zpYGTX&A>SOpx)qn;OC3L5^u$=uzn-eQrOh@mgBk!UinL|`;=DaGdEOJ)eX}WNhINV zklW?b@@Z2#>)ZPTKQ%!8OSl-%I_s33mipKT^an=6C39XR6(Xb2Ib|9lnMQxyx75CG z*P4Ng+-iT?%%b@lKfdqfZlK>rIBn6lTAn_aeS#%R_cQ1pqt;Y5xlp}CDG+a4ruySi z8ANOKMgHN0{W$D|c1Aan!^rGJf-N__?N0>Sb2PEz`^KmBlOgs7)3%|xZ6|8L%6O4& zA7eI->E129SqwqErG3z_xuiP;9^{XR1nCe#ANtXHSFfs229WHaQDH!de1HnHbpV@) zK+5K;+&9}1;)$)lz4?k)OgbZlj|xEJoPm}>YxX2!JnY~UovK5RHy;adcQqkoBB8@Ds?Hb8geKIw4i)b?B!`pBcEciv8MeIfR?NfENn^4(=l2zznpH8>f(=A3O z3&D<>!@0)2vV3@cMa@6fW)C3cQZ1lJBBm>$*r-H5CN1}bv6BRms_d~3*4-7{T;1~=Z-V+}1F6^WE#n%9N+N|q+O8eBA89(BIgthRLL?PIS0tI|JZ zATIs;xM1Gpi=;&x9KnYOH~>jEVZ3N5#b0jTgI`8%G|8D~@uzI7oYVW5#!0sR=TME2eN#LzCgM1tpo^dw%qMozWLDwc|>dTRT`5; z*!OV;6VuV0jkYW&rwOgk2bm}%ed8~jJD;sZ3hcC2D={cwdOODbM1$tC9KX0XcdvLh z0{R9n0C70v_}f+a>6Jp>dkv1Zu-WV~3s!E*7aLv=P?kYA{B$gL^}6Y@nfL^Ok)eYX5bbR<2-Z+)#?g9g2F`<)Dq@u* z+jm#Y-6sbf3;IP@mjYZ+N7OqqqL%ndP|KV{_r?@8vMN?ZqaPl7FTReOH$U*ZqJgBf zd#uo44w#}tGwWdrgJ`ECvI|a=1SyGBro06jzvldtuj~d+qw?4CJJ1E!zVh*nBUB+A zV$7ItO)NhMa@&+YOKWuCM}jKRYygol177Jkatl$1vm zAiKEe%2b~)?M5J#^RsD~;IrV*eyvpp0;%;ju{asq!ESO4J_J%#T89|D4{U7PP4wkI z4d@Bc5y8BBTLrB4{JUGAw&egcQ@PwQb%H@p`Kh{c^R%Tj1e|SgACHRBoMmsZI;u*k z(L3N*mnEg8arSn__6lgXX$<2*_j+oiO!zJJVpZ)|bpNYzdt_Myfu3gnR5adcd7N#{ z3Tzj;5tuyR5Rkylk@dAQ&8R&<`hIUCA278=$GK$sjUgEQBAqVmt<|z-#r#rVA2*X2 zcKpJK=2^u{f|sReqQP42sV^oM<2uk`iF%WwuRt~|q-ye`NjCjz4IXK1D~FrS^-pe2 zdi)JULkY||nqhorxY7d&SSPqgqXPTk@+QCBO{9~3j=N=p>FP$l2UUz?_ZUobO;kni)_$+2qvD)Ew$HA*zEoWPSRZ77 zfK2F6Tj(t+)Rb63$!|82@?;^G57&@>9s22h5h++@lh=E8JnvGOMO<^+q;4eg3ofyl`mqMT2apaSprGBD!0EX?*^02JpAi-xTkxPw|3&rB_-eUPIucr+7P~+JU|W z4=^jAFVGe=H3?pq)&F#@@7*mOz6BMbi_>}1tdcK)Ep;l_km z&CiARu8Wy2&DKTpl!eqQRj2f2@YSKgI79>!(&zs=0>3*z;P_0dMjYNLZlT4sS}mL8up*x%9e1Gc^r??mGn%@KQh_OJM9grO{1^8u}CD zy_W|U947`V@Ar^y5|R$(Tn2S%aA>i9B2D~|G%CBkgwxu;G{5=*{aun>;Nhr)V=hw|I`c6$6$$sYj@L4^|Q*#N8 znnh?k;l-UP%P_iSc*dcxea@AB(B2Ym*%wP3srl=`c%|zVhSB;_ng$>mfyN8mMs?8~ z_PFduJ$@t#zzG1pi5ZedtuRCj3=i473F?A~7hN^bk^UC0 z$<^Vi_iWM?(OFTOC1|0__mwW2A4t#DeZ$hpxQx)L6sz5IVmr(ZrIXoa?zNq-pnLfTtjojN0`=o8R>+*h}) zxMx}FZw|d(Ubix9QApR6B@L^o2zbmsuf56HVtfv^ZFsU*JQ^+q`^c7UIOwF^ncHW} za`BNwx~!>X7o1adjqbY_OdQzXHds}8{!K*+j$%@)I+{R)U-j znZkwhiKHiJ&`)4qx z&BbH@5S-Bi`6WNRflfkOD;EhBBM@LRO9?&Z&!?&9Kz`))H7W@YNTda*oaFYww$Qii zU<$M~!L_hM-~qGXqg;)$u%Mz5?V}b8e(z=;0%NG&ik@X<6d4%~c*Z}h^u4Os^YQb^ zJdoHibB*EqT)7jdJqBn7uED;{cm`A%*AS^eZm7E43!APt;?_bj>wCyYtouBQl$B%F zSoT$SMG^jLg~1#bUz9Xcdw}OrSfx4L>yo`Vmzp76)u&kMGcpHu%B-!h%J2j4Cnq## z=oRMlWg}Dk@WP=gieL?0Th`dEhbft(gT1p~2wG`}s&d`87~V7yGEAcb!!rz-4ZkpA znOEH__s`x4QB43=GPPkHdc_*ajbF3aPc?y|z+PVvkP?PnW1|t#2kKP)ypsbY2 z*t-vA>EW3vV0j{v>c4?{enggKPcw75cSHS$0{B!8K9dsS*rNfS8q$Jf8%#02s!x67 z@srPD{!!3)aWKBnE|u$CeIHLkW^k#_Sets0+v4~~AAgVL>6UUU z+!GP9z1hQCJGGpCkUtJY*VE(FceNZK$nSLK|hbP>&G~$FgQuT4Z3ov~zg{ z{UmV$@M#fuL0u@)6{o9yQ@zal9>}%) zUz##tN7!KL7ARBV&Cp$!n+Y1e2c0NDmM!DkAp{;=^?*VSydX2IDj~zg;VXQNjUn zn@iN(7y-F$e|B%c`pe2Pql+m)djfr6?y+Pd2Ver=6rs8BZf3);uK!(mc5^aJz ze9=&nFcmy3bMLwjeyW;k@`f$`f-a?9Ei6)(j?_x5jeot)5NJ$KmYBIB8N}6jN7=EY zV15~H&`{WvgR<|j`!X7mQh=O$h=DagRlmXWKXeJa-5J~W%{*xV3cvYcixsF1HrxWA zSVn4Z9n@~^Y4{UlQeDs@7|OR8=*{Z90Y1-^rJ+XT z);T&pIHC0Bt0hXAMgN*mb+>?oTR;XC|1xS&2V+#3Eb@~GEn72F3*-p#o=WxSG%AfB z<2^doJFsKvgi?)(0ffcS#_rS+xIbrH7PP>5>7u_5_lJ#*U~1p3#i7PaX?|*t2DQ3f zx#j(X+RK`POp7eDTijMv?dgqJZO;}yx0Q+no0~OhuymIOAir!n3`7EnMk;m1=lSdJ zzv%D|B77&hUpcS^#&Z_b1^q|wtv#M&t*q~WmW$yn-2W4lr>g$FKGQ2ywr=lzVW#Hv z2;|Q(@LYvz%w2)FE9iUU7DeJPnnI{8tBP?wH$rhe<+~>fNBqRtI&J(Q)D?zEBHlZB z!VoVxqDX53n$mw7VFPwUI8uw>FOsd#d^m&we&>1iCOzkS+qoT~Ci!PeA$$P6iAjW)~PmepfEmII62wWv@` zhagkdTfG*2uEy`n@myMw^gz2W?64XqQ(}h3*#Q4wk61Yb^2BsGlv=1FEI!6-NtPD& zWzcB=nk&r|$Xf~dRL3z@^-JXgN*y~xx zlTnAVl5_0XewH)~(nzlme7`c33-X$Gm`&x)f*BN9bFvGqr0f{1f$TCrcgZ*?fon-C zQizLeJDaEQuYTlNs-H8*^lmE}fEZVGqsPAW7}oP3Bp#M)`|-}ShG3yEM^%?!`iP+t zTSbSp>wrx1$ zU1Q^DuOIG#@ntJo0Nw77RenCoBk4l=Uus5*qjvLYGTD9@LAWSPykl0l^05gGMqyn?vHqoQ+${|xFtGd$<{o$x&ol0Bu@$-_qEgIpJhb4OB z=MDb59$ncwK%D8tYkH=2gWO>vY~Q%gvWv580udtO4)d9G|CXMNI!VB5%%_R7bgBjX z+JDas@KCBFeeBY5qDJMqGS8<2jUtBPQ6$D}|5g;$4s@|)W!%aVV^EwSZO zY6}FK0T%aFj|ryFgRB-dy{RIqtO}RSw+7SUX|S=n5nk~+x-_6NDXW@$IZ$(~ZNK7x z>21<7yI!x+2rGx)tUpbwOO308e=kns({!~p#3Ve&=fm(@0Uc>UbZE4M>)7nd+4Ss+ z^@pq-b;4popb|krWDUWfy`;yr!2Tq;h*`;J?;c|rH&c?gy@=RPYv^}E*D#anL}6rV zcn4qYQt2t@q|)_jnG;#axIbIcEEwqh&NMKU#`c}7^aZk1gkc7H(NE01 znc0g5mp`Cs3;>X z&sRdx1z%Wkr!&zxDG;W3iC>|;M>E<~CE`N#4`+cR;C!fs9*tdoss0+JHbzM73OlDE z$YjQ>RZb>8xK*H^oP#6a9nW8Zz7yP@Kh0uS)~!(kR2Bs4ZQ0aUV=co-_69**gLBIE zl0h1la#>}5P+oWNF=Ee+mIE72NS?F~B)x!tEI@AeiA*KS9idt8>R0=iU5m%vZ<*8$ zN16v$I))QO?tJIH0;WPsiHrP+RG%p^8=p2%UoDj@rMdXKUniI7KeSU#8a96g!DvT) zRh$XQyZl1CkQ@LmKab>Tcl@8T@T7K=hB#Mp8PHpbWh`@~higea-A&4d=P7FG3lOs`jJIMu-Gv!VKS!a^d~{wt$bESk29jP0 zEL?lOvZ`{1^S!2Zzj@-&JtMCOujwlyfnTU0pn7&dyB!X>%l-tj-KHog+8H`eIeZ_| z6;*G#^ll)hqZePJ)IfpJje)7+nvhGi>7$gC(|O9VOkF+6?#_|cjDVwr=2tSRlXk+C$OWSwyB`Wrq^1F9|MP@Q;9g8EYM$(gfBq90(y$#CB69C$ z2N24(h|AR=8*`YTIImhB@myT)W!;c#smtGAmGh=CFJ7Lpl+)ua3X;8kp2}jHC-Py- zJ3;J=S$G_})sY}6)UV~`nGb)NPzAMZ#E8y(K7UP3}gi>&Ym zi^p;U`OI%y>q4R8R7`;X%FPId@j=wa=+@U$dNLFS9Cf3qXn=-uaAU*TGcsimV%zx3 zjXc2fD&yOO%@BCEO))@7|n!`pPc#SAIRx<|C)8@ zkv=Z%gpaVSlbnC&d72s6^S62zxBCbAybzx=7Qg5&i{M&dUJ|=JYCR=S@LAi?RPS3A zoeeSL0Z}U{-(%37^Qw$2)Q_e`I7Qz3w0(XcEezdE#>4B8++_plq0)JZO&4GOqm%(~ zG-*rhwW{&WL+lgb?HEf`LBv2&$?%L!TIuV3Su7*ctDGm)RH=~9XQQ5D0t=1xF6Hmq zU6P-r!kZw1;&~k8mlH$ZxP6jT47&FV5ecE91JiL~cM&uiLJ4J#pI|h#P08C(2g!&l z31oADL!anBV#@*lszA7$GfM#h)8N~?1!@n8_4x$uyeId0f<7Nh8gi(l2cB<~!7GCf zd?JrG?(So;6)r=vZ481rFr88H7E$s-T$%8yXhWWfo&|efZ=`s+J^%gz;*UGJc%Qj5 zd28|Vp-3bOd~;y)sE#>P(Z3pmxySFR*``$bxggG*l1fmEw_ISa{qGx3OZzGsb@?{# ziwSMZh|!#eP^fL$>x&@In!nK9mVm=tD%ow8Z8pvE%iGIz@ML~+lIe8`fmKuCQ)V_$ z0vsYB9==5!*iAxHs*sYBBif~Sxg0{6c^hwZQ8>(LOc{tNi`!usaZR=D7nF;4CPL_G%--gMYglv3s3JFc*quh~fQx@hzY; zqdqtUiz6|7-nj11{?7{qX4A6kI*q_TPc0XUlg619SEP8s1Z-5Na8s`5Hpy$Lx!`ku zaqDKQk40tRPNLFmODw1AULuB&NmN}r?77arv%abCN@)GQ(;Rly6ndSh$0}0cfU?J1;F?Y>fY=%Q zJ7#-W{Z%~A*upw#7`ZlHx`?uWy1%zzOk{uoH))9s?1C`I4Uy?;SLpw6&!(p%f6oH~ z=m()7Ir{8xMQelh>Qd+xTW3ZeM;j#jo%N zv^V|r{(UMz9oF+^Gg(#rZBd3bhkm-elR?uv>Go@#(7pHVFN)RXh(a{Mq0g)T!<=`J z)t+N%?K-QvFUkE^q(%e_;XdwDR?9~RwF^v9w4@me?8gYLzr%F9sU4q{@CPP`3@|?I zYZbKqo;`g1g-V1B-Mf&=jJ0H(k3*H_yb#wg19PDN=H~j$+DWpL=xNJ{kw3Y1NH%?x;#GHTx!cbz zLdaxODIy$wUfyhVX$Zh__6wACp)XN+bkgVD1v4H!YO?vzO8ZVUg4!+WhO zjQ5`q?;5mkd_VgTlIQyxRj!jj^0h%K9-Q-8xtTFZ77OJ|Y=Ej6mdZjj8ct-*KSzU2 z)7Otf_9V=CY3cv!)NfL;u?Pv_`#Desti&%!Yuh*^JFqzy2S~cO{hZ$h{cpawyP`;?SEhE zTXcmwjb;`OC`{KBqJ5nD9ac9ns#!J&J@OV`iA{OsiY9C#iH_?^N4wBsepv<<@3BCq z|I`?*>BZgLbRHi<>r3xyxjkpHT`)uwhRTK`?A8*{Bz|-y2Wyg&)-kWgOm3%m&7RZ% zmp^4mM{?3vS6VcHGO#vI$zseO{U8Gz!QN=Uv%}l716jH>F;m-1jtILe$(89YRyZTGd1nQe zQhX$wFu0EUBxKdRSh({JWb~PG_^5o|40&-Nv0M)CHo6l2zM1+w{*C)6({K*dWo*=+ zgN#pU`N?AmD=+SY3Yb05o~c49`MrOwYFXlVcRUjCyoL{Z zly#7M3_b>yx?N+|3Kg1nTL6qQ5JCGvdz4ZmB#)Sum(VX*$H3H%W`-xJWxBqMBKVU| zw|&a4)oIiUM=DQja!cx=v4^zhkMLu+qWMmCs`siBVa%_2v?a>ZhinqSv4VbbzSLj5 z$s1pbWq|#-1&YcSWF)@OOoZeq8a^s_p-#=kf$m9>b^u6v$dsF)LgCgHeVila{t+ld zNO7?U`ZQ+Qu!gqb7e}S9Vd9D-Qqu-#eP3hxuxi%9i8qv`_o+WsnazO3C`@_BtJPk$3~`gLkujd^a9r*XEP>kuZ5!++GXs|u6%<{p3Ye0=c_(xritHh_|4;o5j)7KL?+ zq5If22P!aOx;20Cno!2-@I0k}ciU^x5BSqRqgCR1ENhN8|5v+ZRy7pWEv{CisD`;L zV3~!NZd5d?9*~2tQe*JUE&?MEgA$d&_*eU8T4eM4f6KX_T_}&ovt@u|ql41~V*@2Me-K)qy}pw=%n1e`F#nyhJFwH$-R?!#A+A5t#4MD9AL%$udc)i~25FH(Ca_ zqotX_Xt^AH!V&FSL<`FJno#(@FJz+?U0j-U|EQO=o9jlLK;-++(f9$pBmV5b5wY`l zkhrNrdg8P2rbEqg+Ap7|yj%A?weh&keUukb@a&%Fe3OX0)%c??0lRv^6(ck(EK0_> zLplt0v0k#eK{vG{7ijq$5^N64iV|XgBEh>UujTRgG1TSXS z;9YwYg@qZOL23I%duEzb|K1r{d9p26yMr0?RRkB;R1iSW9FuYg7lF_L zf^-fut%qx`U!CvY09KiR*?iCuL9dR;B~6<-Jlswech(*+ z-FKFXd6h%fq4mi)&D}foHV7e_66xT&NV!GCn~cm8gO?eOf1zl5xek~xL(j2u&R;6F zhCmQ1AN+>|+o*tXp}N85%`DuNSJLO!BR8A%2rO8*_; zr|{u)bae`c7Z?Jt9Ck~8r5Zo0K!+w!c7dH_PmS7rOf*|{9%sK~d0o1_dpP>tg}VF# z@RBW7A0L#vJ^}^Q8)|p4kWtmhBPv?rA&@GFHd!XFa=>1ej`n@r>`TQhQ|Gt`)}2>K zb9b(Tdk!wEfN_$_3l$16TdJfyq4{gQbRT2eB$b|d7W?ANF})@QnNEwyo9)WLHqVa)G04?c=7#-(=o;YcC~v1<|WB7fdT8)1jzd z2W5H&=X#1&Fw|%Fk=&RS`l)AKoIO*#dKN-kk0$S%GS=`xE$_g_a1jPFm?u~TC9k~0 z;Il;W<9xFLf?m78?9LL)gpq*tE!9vJURD6-P+|R z#+2Y+8C>?6KP%@uIU@l%2Ucf-FrQ2a4W_Uz{=v+)PU+WqhSHtqq04d2kRu7ROL27Z z#A1v0B>W}l;>hKGE)DwKt*6a7pF^S3coWH#UdfVGHe;sgkW{;fZfdP9_laRZ!Dz}7 zR#_D`>#9=$WwrSQU=%_0KTz8Cp`|yu`o2FdxydBw=84+8forQ^P|O35QJ0D(+EByJ z5N}EC<2$U&DN2uKea*brB8us9QpwJQ-920dLKCXkxwt3ug#(<2p!9|w%yPz5IMfekdpt{pr=RTFmh?p2zl+K zTzG+F&cZD8!h^zh#O+9GU4F-%8%RxSU;zG1mq$MT=Q~i z>$y(%x`L3MraTL;{(WT-bB%f74z=aF2O2YJjTqu<7gv=^)q#vHhV!NbuO10Bi3^l*!j##bgn-tUfaJ89T9A zE-{Gc_!47Nq_vkhQ$dm7@QFIe=f`I+dG>r9z%kUk|bp#UJYm4>IixnmzfFd z_~X3T8XDH3H;o);5c9+2YLDzIQO(Zf_w`%34hV|gO1j3kn_|D|bc};;4kl(aMxypp zJc3Oc;0fPZY ztiweh%lM%H-VJdy;+ycSDEObQRu4%?Z7Bqa5D0c$c+Y5Z@H_^0 zvSswT%a0T}FFS-r>XK&zthR`Q*qmu(=qU9NqaE((*S3rSPI`eJbjsc!l(?c0N%V-ri5T4{30*=rWXhyb!r`nMzYiJ^YJe zBBb9-;A-ft{ZX;m+Iy_}7m0ZvOKdcq3^*LOA`jtu51zRp!hv5oIMV`%ekpc~`E6|w z{5;xy%#Yn=0}qy*rDkWuZOW@(@O(Ez1=o8*4)U1HVfc_;+ladXRk=9Ak#G`5s)r7| zt%vNcgu!AOYhIir_?&p;7Of7N^{Mr9mm$j^Dr*Rz%T}50&sb04K|C%O3HJsk6^zPq z-k6T@ga6Sur{MB{&lDx;md6WrMG?+9Dzc1?M!Sm^*Lw6GKVI>xrCzX`8 zLgI_=h3!&b7G&%R+?*fa6iCVQ52? zPwC%-wIKFd+0`UzvI43vn$Eo%5hw%W5I{rmcRBv1F~4>d0(fo0t7XG%!5_nvk)lOr zt)Ru1NT^#t60LqG1Y91rIAUfq%)tXSw@!6sL7+~;>gO*WSIeUAH{jk$O=pTY=O5YE8(@bC8oMq?Ib}`#~Q1j0=VM4cfuiHFipvTcLrG ze>y=o({ACB>08~US=HgL-2U9R0{%6%aOO*%3g}JfJ)nb>3x#6%nJ{_A@+U4LqF&(^ z{?6}0jr=N=;;V&+d6&N9@qjXQFW>S!%7*6jZGH3l2T8|M$Wp;SNHXqwA7BweWreJ! z^pN#n@9VZJ8>yKG04vGZU2b}ijVELiv~{!wj|gkFYX~^>Na;i5F?x+b4Tf0Xzl~ss z?KF9sv>Wp9Z~e()!l_*m0#aTX0TlH zP@(VoIk6bV@gR2wf7r(2Aq@EP{0+!V-%#KJG0xh{>;+-VhW{9Vg=JdREEX;Su?z*^ zxub!+JVg0f?jye82h)X0r@0km5WOz@wCXb<4nccf@|m7Iu@2OSPg)ozP88dJi?t(6 zKpV^3*~{4st8f5PC7O>}vL0fRcgte$klS1cy7}XJvyIWseXIYz?p9oJI6( zmtK0c)A*~SPw?kT;_~;pwJn_HB z?=>QCoh7(U6I}X#x6$@V%RV%R5*Nc83+!lHAQE%o{9^sh%k>O20CgXVQ+XRNF#=*> zwZtT%dq8*gN17u!a?ZF!yhIP9N=q8q)X9t~1_D3Z%4Ld`!Fx|OtHP!zBa1>Mft!BR z0qmWSQI0<6ZD-vq_59uAO#3A@NIa2N$`~(@P)QGt(`Ao`cAz#J{=O3V)|Ay)zvUTS z=T#gZT|C1#m*=(zaST;ht-fj}Ue*5sFBj-15$Gy`1?Zo#F4PEPSrwH3K&j59)qW6_ zIY+1!=sv*7On3ZhqCIU7h$k4rj=vi;M7ulV=aVi7tbL)hY;r-9R-P3C)8E^-t3!;I zHG(+;k!a}{d!igRLRbKIsAepo91$whe%#-UE}KUwinUC-y3MuH@DC&A^dwnGF1wOG z0U=KHTH_y0sws;L_4O`hXCi(O5Ca|o8B66VfPZ4Lx7!b#IXZH{^j+#ZScwrqo4?q8 z{UzuXT8%9M)vcPYZKQ~jy1j|hm6fzZh1wWe%Z^&uKyjA_m(>NT`+SL#p2_2s!s2q| z;=pcOQmeio77Sq__lqLnuj-O>*!3DF-k~3xO&y?-+@UxUx@{hGi_GkE;gKBAsC?KD z1*o zvLo>e8Zx)G0+Sc)o#^<4W&cKWp*CY@u2NNGj0I`5NSB6X)6q9`agCPK@y2|ubkZ_y zPru7Wijl=>7bmQUx7ufMCli=_oGl)C3zjjoG^r?{z}A2|pnLB`h`( zH@nJ8^+76I;Llk-PA2>#_zOSL4iI-+fi)Dz*PH;QgbCOaE0qtGoX);nRcJa+#k?iK?y^XNez;i`R`SB+Mx*`ZSN|-haH}WqUkbVHB(kx<; z4V0V;Q1+d`9TW-Twq+%2g^;;glR5ug07)p@PGDR+)*it{NU;wPlr@3-S2HS^HQh9S z6H2;@2`@sjFK-3ea>>u8os40fnvBr4d0(ygWa@~7yPDS@Z_hzL0dA$?YOIqe>1h;3 zsHX4ldOxl=lvIaPVw1&$Lk5}Nfxr!sPs&kqrMIhLSy7T}9BfmoS z;X!{TSlXq6NrmJ2tM*b?RsoTfKgKqL&C~)fS1K6(1iwr6``etamm*@~4ekEyS+#bq z+14+y(=J15EjX91_a43{AWyHCi97V*{hK;^_KfE2-e}thYmdF?xJuazUQ8Fw?|rdn zeq90-PD~_XkrXtqyfT>#q+kd^)u(M7j))kvd(&|N%gLs?Xi854BP``~Rhv5FCFf7v z%l3a_1_HDmW@DeM3xYwd7DGvjDu)Hj$Qf!7#=*9c?)bQ^bQ~Q@Im*r-( zkWBaaVsw7}hp#gj}{iBNCiL3g8oVBUJxk|Di2P0>+^24 zqG;g|{tO$2;l0CRV;t`9@p-6(ih*>ZV(0!$i|x}#CrnXqM!z1`;}N`gN#@l!IgR=)EM znt9%quDm5|5X!Lb8nIL6lHTo|&zMoFObLZ$2b7GWQUzyF@E?UFH59SnGR->!ph{T0 zud$zlpzJVCJkDw$S~(rA_^0CR_x-a&SSqz~#h@;6qI~?&;Df&_mGc81!$3zR()X%e zGoTln^g)i(^lHGfL26fD1rirl)mWU+=xR+nDS9i7wcK^tvhv?8VXTFVKLSW6uq8;) zku+f6snwg}ClHx*p7N$dlgRPxDyN4O#bBqIyhewv)~7BU=BJHKWf^-4h^o&d+!-6c zU`x}dMg16r0)nP?D%N~dtnY8knRWUF>k;@ZdO^=!AnnF(^`9oNoD)_kPo_8o+*3eH zY)7DG!!KQ?-nMiSo(vzj`EzV}lzT8RO;P&}srTRx!+CFCUCfG5_Qv^YByrBD)-aOm z&9B;`ESSEu{?*79`skzKK-u;wzK^VKuSuscUq8s7?X2Q1xhuikT^Q3b!MH@LRM4sBO*4?IPJG5YJdyRIdznL%!Z9}+n zskqQxEj6bHe%HB1`-+ntyn66(aw!tiQ3_$ijf*C=*6618r@b(dbdK2*o4}s6nz6I? zisu77iyVd?KhC6HzoF#(FBpo%I+A$q1nkZAFBBV=?@|_la(4=tbaQZMW-)Ho#-Yeb z$UcVV%M7RERKy`co;jT!Pf0e1xVAAmhQo6ZOjxe;NQG%k)4_^m6yIiCD4m5fbJ~wz z;@{qB7OZ#NV*4093WhT)hfQSns4wl%NvZDDxQvl%_UpKmOIoocZ}_)>XhA(ZbXR02 zE%HIVQhH0G-(1%oRW&Z`*-lZwgcw_=QI8pn+Ubd|Fj&lVKd%ZiLd)Md{If6#EY5f= z4!;T!^NC;}nthk_2;RbMMi4F(|vLQ1+Dl8IM!< zyi(AGik{i3W$NLpFJH%gP}GF%fjFV>GCguNo3#NpD^=R!9s0^$cycoUTVhVaqLSb5;}#x z=Y!ycI5J8ch~&=Nzl6{(7!LnsUqZwa)o%3+FHb+_>4Ne@Zs=jrHv3qzynbdQmwvqSR40N&dq~9NtI`g4})=0acc3n%e`cA2N zWsbKyy@V%oDnnpv*;@hyh&R9cDuGS&!*VNoLil~3BAks1Q7<{)gTn3@AIUF|it%qf zG&fTf5lz8%R6v0OeTzf_`5kC+PWy9g6q53JhB(VIh2D?CptKrd)Y9f5rs+371QW`* z)eoUP-==!<+U#R&foGj3&m{oC4@M2%Rf8Df;#KY9Cs6400*q;++ApUokoW9O3jYn% z|1J7!!p=7YOP68@wQ!!~Qy;B+oT?7pM9WIBX!(r2h`Z_`K+M)_@k{@}Ww-1edUK{~ zs3Eu8kYs;u2rGs2dD&@_VlC#6i6D&zYHkL!HRYCg*42N^G-D({cy^s|&-5SgY(tjr zw{QRayO4f(7nk*G7c{*p5~%EIS4eJ%0VF7OZpxMq%F3+m z*)->I6-BNG!oH`iH2mm?nEh4MCfyr8u#F@7r{=0c>@}7?eazyn%%iGral7D^ty>Y< zTxeRF`*h~jK)XT>G$R~rca=l$I-M!Pf20^JGzLJt-KcRa58fUsGiso@UgvTMh48Mn z*?j0~iC-t<$Z)T^8e7IQ4`|5g;=-uZ+wfva z%V1u;UhtZHN*-$fY3oaF5(Imd;;)W9&5WNGu;o9I9X8)$g)7K|U>F7yEyT(dwvT!4 z6-GjnV2+gd9ZkA)gyiy9W<$gxGiscBp{E${&cEF~D*mwlBxNa-0{Bl273<_Wys^ox zKK~I)jP6zotC^K$=jJUfgVM~c%;R~QSa%=mg1jQEYXH-Sf7cMaS7=?+#>P=$Gz41M zSS4SqbEgIwR!VZT>VXFe3JIZS!w3M}sEV`Zm#~f>k`T{FJ4mkCt~I zLLFmlCtlq>@|p?BgRyG3MPP%|LekVj1*uuF)TCrkEt$*+3s^9uvm$D46oQ6I2hJfH zcJE5uLCA34-(NeK+;twBY$_F#y~%T@krv^w*MWX%kL2tyfT;S4O`*A0-=k(5@R*L` z>9+-X{G+z=n>?|rL1v@pa6xWKy^?Aic)uD6=l(EGInQZ>x4xGe_vxPHTD zd?twkID!I|w`$i;6h=Iiri~!5bO9ZmJJxjZX)Hc12ZrOKG7Bz zuICZC>$kt(49T;0f~A0jdrS{FOJs`i`<3Jw0u!ikvV@AVXC#q)%8m73O%e882NJCpV28N9 z0cqlOELvWTZlOorvbWRAYIY?OP@Ub;AL6ngilMZWhNexX!zKz&%ziyEo67|BPW8;C zNouIS8|&CyCI*RT@;i*VLC7c^z)`hn_KL7|&;aIn7zDeY+`M-G_=~wl;cGs?HA=)9 zk6Y=~)Ad3gIg1NC`2L8j+w3Z)Vqj4%F=VKlB}GrSvy^X1VqXuS9QS>y1D7|aRvEyt z%VbU|4jAQV(p1f~xX1%>dq2|NtjdQfDvy3Je=sDBcN}%DH6GIClbWPApwz8G;N0~HA`F!GZJL$^`NSneME_qRa0405QyF#Am} z7KcXl1JOlWiiUmBm)>(*e=L`&mB&LG!IpmV{4swL4DgD2j;)csB8J6$#{cKJcs4f~ zuCh?D2}t|IgEyCTYz`orrPGJpUIktN3(lpi0t(d5Mszp!eD9sQ+y{yEpWRiI10Ez_ z+$X3WtM4OlRp%`M)vcaQ!E|gR@NB+1t+j2kNUlXbCY4hMk}YJhEiKt zUtS{D{zS~|qnxcHbQx)1j;mVjM1G@i`Dk!zO^Nb%?EJ+!rkcbw)$%htpw3Mx{fI^& zYd$urqe=3W%Y#$`<`FG8qhu^e0YZ0L;QwB5dvph!XIMIx_~(?(4|TFormZ7#H!phS zVp_cN`#3w4!c{;6$BJt6E~oCXT1h`(eG#B!(d|>H)piw@_M7szYY$o7m=TE~I}F9H z?==ig7x%Z+Xb}r@EsLv@CmM29>=XdK`(jm)jtMM6-3=Hvoa#9_!tz2WHe&PhnV1hg zHK`|%%Qul?c76H#N%ZF0mUPZ~*qnS(og^=7_M|ALsX#r>_$bHSiH;6=Ma`*sZt>Bi z17}u+=yocaN&GM|V;t3S% z-f5nyA~4a)fvKiffQr*MRq#QU4ZiNX33gpGKIM@bqwJm$9(U|Dii zy!)F`Q-7+y3YqP*zS}>vnEB)6%B5!9??I;sW1Gc|;(I)iJNXy^fKIy~A3gO_D22jO z?Ujoy_e_1=kAKD~GRjrAwOIvw)gUah_1Ai{!qzfOScp*z8tFbp z^S0_;ktbckXV=fWar<_tGG0z0U@T=6@Qa?6-dQHv;d`BLxml_Z)?tn{v z#!s4wC`jz+Af_Nqx6HNm8bhN=(d_&9I;_D*#4r^jmyc(Q7n?6bvnNs~kJ*wm=OHhZ znOs$9S5`NA8l0NvbhUrGJKl6gt~|>TC$*{bWT1oKlwL+F2GyzrTy1|yz%McHDbuQ-yB0Aw3D&c*Hfc{ zwq(=W;8=)<9C zhg; zPLiSPcLy`OO}%>g+w4t+FQXf&jzYh|qYDP^{jntAs?%xVHrZci3A!j=wYo13|0nUQ zH*asw^M?0A-9vrH;+;1yJ)kW$JNdpeelU$pv&`2)hTCJoNLcFkhtyM}f+iiFA5~dX zLMaoH@=DUgh!~>ph|H-tCy!lT83qXvJmcwnm+uCA#H0f}Z0;U+{8s_p>IZsE55WCU zbYJCW<0@SVP)Nrnm6<@G&@T7A%T0hb7Ldtw<)zo)J~B=6(H4eNd2&6CAD^l-*)OzD z$Fzu;Z#jB=Pkq`942gz@5do|P64|gn%zvb?STa1PdGxQ8kB zQ)r`wISsLm=JzlgM-V!yhS7>c^PB}+`$hyydYBOgd16@WfZa0A9fOTo;bzmB*aE%J zX_w4Tp2!O4)Q^*&v)>xqM6ps$zHsf_%TM$Nzp>vT6ZN#oWyJ*OY|z)HUdJIof5g#h zh~T5910z%0;?|02{w16FI;Tp51>4Rt2VJYoSSv&#?%Em1|C1 z9)0a(H)ckH-dMTlHGOG_zzrq_Bk$Oh=X&}&GA&43NAa^MVqZW%$W@ZBmd$(e=~0l3(#l%^~^1slr~t*8qUF49?<$C*5h1MiNF%A zd0`|$!IMTP5lIE*MMm%~Wik=?d6@a{;U7w`8iqP)RamP;W`8e@H5 z7gf*Za?0ec1(e#8oR6q79P?H?dVUej!G+#qOQok$LI2Fy%!=F?Jm9vxV}Ye)x&;~v zl2B=s(-BR4{uVjjnEnwj5u9b~^L_g*1i-gTdy{^+JLUi26gg?B%lv2K_Y$=EAZBG^ z4HqshRkX~?q#JL7eeSLsX#J-Q79fd)t(pFfx+TwmJAg?4>*By6M5XSY^*p>Jg`4vr zl()Rvi&i+YuL$yhcmVfMtx${+lN~dBip!15M{;5aO4AKkV_yo%)1@4z`TPPm=R1TA zT(Ui@z9=RrdI&<$`8i_xd8Hz5SJv9Dw2#rW_tPcOs*-u zv*SWHW_t`oAC*{}T1P28u=3ZbLjrUlK&hZ2J@ zAnY_w%$Ub~rJy;MQ}cs77Jg&+6IF!D$CE7UYU11*K%|M_V&_i<=slQHG_&LzThRDq z#HL0819)_FHg$=wOCuh-09d$qevk0Wyq%C055j`VMZ+pz!Ya?er)i!$A&+^+;!j*v zcIer92m!OyYkFuj308v&?@4fOj_LMq4G3G}hXp~Jm4%%p27HT72zQFNax}OUQvI2t z8VW)We`lg|P5>!6@bbX@ClrjO0KUw?)@$s(_%(;YwYa7eYTUU?_&v2>g*edklrK^^ zc|;SJqDX~{D5=xW`}B0ljJM%(lUv#-H4K(-o{dPT>@C`5_zE~0elqk3nBL)D*rjnxqXEXYjIY0FtMY^7gej_~} z{Vi19^9&ggX~FS(*PVi)6A(}xXpfURD9}u3o(dsLE=D9{6 z;wHg9ZEw+VBJMESs`cL+ve*aZ0hEFNdpik$gxLxSQuBZ*PX5mUDbDlU7q3B;H@x!c zhi;e6#1Vx8b<8Kn^FT!1o*m+N<|W+%_MN5EMA8&qB>m4AO6uX=&{=6(`V6pHK0$h_ zmQ$LeNx&-0-e*kaO~+oG`^gZJI_ep?^pg=zM4+~p+RT;VZ`KSm4;6koThS~_Gd$xD zlW%4`B1duNDek5uQpU^jvJ6|yLasl4^_4q$v#kAcmb!_pQvSE!w88>ux1JrqQS|-% z_oGQM@SI^O9fc zX3efPVMHDlrK4=EZ%>#K3wi-R1g3i^=j7oCldqjkKE&x=6@qc!{% zaLA@`5kZc=YQPL0Z~ixb_8_gPK6AY$90ihq>bxq+RksWGbBiei*~`P;6dL%p_(Vk4WHxb@D9VW(Nu-5x%%{Huel=q8 z)=J4uJh4Yd#zi}5^%TpQYyhay{pN#_PP=gb-z~pwmB*s}<}??3%tN9S@=wk4YBFh+ z_*!0^Og2wi_d%!E`1bK|$H5KC8}h0*aeS@OBfI~^c4%8<*C+o|UXd7e24Eg?NR@dc zIrX{ye;3?u)6w5s%_TGcLzsa$WA}=)ZHrMI9iq=KFU>gCO*w}Lt7;<=prYrIrC6%? z@nC*b{nHpQ<2qn8qNe9EV6HdEL2_4dPs=w*otKJGA@h9k83!Xh zUA28FPvXdn#=ryf*oGzlmXRp4tQ!^G%__tOT0m8|H@gQu`7YmWjE>oBym_n+8ymh#_?l9B)U!oOg0mam!t#b%h-o= z+z=k^(L#`r#S-oH92ZC81NvN63dyH}*mj+iCbLS`NO9v?0CDd6@t2~WCYhG1GxQX) zhh7+)2ai_&DwvgY*srs$X%Yc`rv9%42OGQu+i!(l6kS@C6>#6{d(*b~tPuWt^w7MW zK+Szk?d0+^bI$@fWJWw>8NCUs}yxxOTuc?;pL8X;A`>Jih z{}_Gl3rw!wnzxX?nvJzWj#-_3q{4uJ(eKtTgB!ArvFnCO34H}3f%>Y2reV|S%obeU%dAm0v^}pYe+9 ziwM~%Ft!Fx{bFohXeu04j_x}kjIC3HP8%wkD5?<(BPU$)Jn)AE%A=S%!xQP0d3UxMsJN*jq|K6LiQ`2%g5Sn#Uhd1pQc1T)+d ztrXQ#Z-ris1fLZ_%<(HP$#nanC^ybI*yjWwp=AnDqMrH7XC^a!&iF9>hhUZqMztGehRXxCH6Z*Fvc$W{ojM|Sk5xqA4-(@aBw&W`0V%N<%I1q{-y+D4!xSX@7Zc?gEN7=PA28U9UEocx+h3?atiCc{} zdV#Jj%A}rQZ*Ah(6kJti0Ke6?;&-p*96-A7Fg1qW)qNp*h}D>7J{;bx$Jr0xyESn~0Y(kVx@te3f>5 z`!ZhJoM?qRhY*KTch$>;pom+aY|=W?BnoXB_PP;_qukx%jIduQ3Cp^#z6VIIB8_>v zwU4ckJb_T5;0=K1y77Ud_sfhX17gV<(pc>tbsyBG54!F&levwT6W8cVUpg~)!y+6G zojbZ!NpnjJn^62N-Hi>+_}z#5u(1S&KYkX2w&{ie0G<-NqJ~h=EtbCTvCXB?UDv;h zU*L4n-x@O;h>wcw*VQSY+ed>mZ88g63+OHL+v_jXO3g>E;K{P z|70<;z}2Uxa_ZM1a4Q=$T`P1DT?X?g=`dE47Uv z$J(3F!%wNT>+}I_U2JB2gsAXJlr?&c#eC63*Q|5mivr%;KP@az?D~6AwIHr&nAU{^ z6b<}YHTn}{>aqOjj_BAb0zWkuVkFoq50MGA=LT`u===i|juM;XhaSI(_|Yc;izD3{ zi9GPGAe~ZEV~uQrxlQbqq2mY2UseQxwc^ib|5fcg4T1>qiiYt{8I^L8z`*d@Eov;x!l~ zU{YsNU+A6qoqwos@y_#dH`BIi0UNHYO zfLw}b55V0cd7Qiig*+%#ywM({ z8k&MA`Op!FS#u-^alN5fI=XeToRCkO5rW|snl!jZ)!xddk?15{w!bpi%boc-&nQl z(8$v0VdlW}xQxuZ45A$QbgR7teNCeE$F%oMJnWuWHp$wFmm)fiZ z?j`*1FZyVfP3=UO6GP#p3JtV1r(Yt!27b`VlN=fkE3MwmA%Y{1#)EC8CF5Aqlm{eM z^JRx^vMl)q6Ef|b7je5m(IT%ypRUbH>xVyg^)QZ?#j)4%xZt4FVNd<=0iJxx>0@$b0O%06`+?fs> z2{ap`F7^9!rC&;*?i$F}sl)pD!Ee$kiVD^X_HXqAy@=g|I%o~d-YGnbS$Zl3ix~pH zroYPmHytcYn4umc5bb>B`Hq@4vR|=x(69!>r7@(iw96C|rLOLQ1bGi0Gi+e%jf|>s z0Ze>5Xz|38u3EqKvDMwH!g#%iVlSJeaSk$`+>pmzN`0)SGI5D^8jc)=NolVtwRi)k z(Z4!e{~!5^CM0jW0cXP=h2!D(D~zC1{vPJ~UAwP8ih0g+KW*CWq)Fqnf>a3!vj zF-K$p96LPIW$Y~TQA=d_Gc@*O9#q*6gPqm%tx4C9<8-{X^9F1*SV+r1pSo399r^P~ z^Uv>RzS``L0lz@# zNQ?A+0WL;5ZCDLX6{K68OaBrxV~XG@$fZs<$b=F*YKm#u?0|86QB|#n-!~bu;oU26 z=hx-*a&{KbK{H8@5l1fK3ug9l{He17|hk}*LO~1LS z4XN~w-q=;0Wg08yt;|(aJA_Mji`vAn=AHBZtPB;p&vxAje8y5Z!04Xj zgpA_Y1SM$qeM&c z6km6JHsEBHQ&*zWPB@`>`++8tM;1x=)e_jA^IR;C>*7q{Z|p zNK?EHLAap(EdAYG-07;GhGTp~CyM6Et-N zl5U2+!KW6?o0Yo5({}d7szpyz?F~Ux72p*=&4aDVKsoFzI@C_FL%eeBmKJ95@*% zPG$=o9@y%9{rZ^Ik%`-cJ}0&jAKL`Y%MIk$p#>VIgxS=gJ?2^~f7gICDUW~PU4~5o zs2|SqIRk>1oBC9#<$7M1(c}Zmz*1F|JGKp2f?p<#baZG7G$(m>9Hl^E^RW|j+QHUm zg+$Bo`C9^i&@=C6+uz%)ja8crNpuQ1a#)2aF8TJIX3r1!*XT-q{{L-D@7czN_ZSkp zr2!Gxn?q6NqC&oH#zIaP%Zef!4gbYEYUFATzeq*IhspsWOox?c0c5X2MPDS#Pu;Ms3Hxy} zvPdRfwgId0-&b)ULGMm^f*P&H_9TxgEFfDfguHtJ7xNe0k0o0W>CZJe#ncd7O(6AL z&xQ@6saaFGeImJvBw47x?5y4@k34-1{4;MFH&xM3>1Vx#*;o{Mn+wGE=eMfQq1>?X&;i$e=QMqac)m$MTs(K`31n@A zf^+)~dC!jbA=oFtExB;yU^PE1i8U2AfEyaZl`vX5;azn(yM6NF)fe&N7-ZS{t7{%nZ1NIy8FD`qy$+}R|Gx@Ma>KSs zu*}f>Mo-m6s88fcA!)M2pk4_sXO9c#t?RagpPG=WrK%611+B4psbL7W``*$-WLX%v zJ72#lsG!cI7!reZvFi!`dy#z}%?%Iyz|p@h)gMk+_5Hs<#7pIXWMF7W;TTynlxHM( zM=bJ=ilt1fv?0k5sTJO%l6p;;+^x>}V~u^Az9*{oQuRS-I_l?|!)| zYM?tR;^fM5**5p=HzVxBCG}uc6O^tb|C~JPLg6^dS;9GtGHy5iR8z;w=Sq0+?y)Dv zp%Tr}LFFnoPPc%ceAc2?1W82B=J3Q`rAVU$61kGqE)R z&CSdX{^3|$kpuAVDG&L~*hSl!JK@}Ix|~(l>rsplH6+iWehkGL)FsUrS+r-NY|;!@ z_r7)Ks_s8p*ta4TyKRwmDChNux1BZp!WQ>scWc0*Z@nJV{^x)QKWBS)a{zQfkrcos zbf`QeWFUlLGIdZ!eVQvd>%5W`;CJ|YNVY5xunepDs_v$sKlz!5J%dsG8~Q}9HvcO)e-Olgi4LR9aB6r?QYh5oP~O8%520+KeM{%DEfz*{I~v0AdNAX3s86k55j@_ZZ=w8cJ>YzrYCZyV-P0g zbt2xomV1Q|F7og|HGLsOcVc7cH=z1I%#cZx>2k$au{PbFCd{RRN%l`K&ot0J2FUN* z!Ea?beg&-4SI;wSI&tnD<-@U-1Z7cz)VErdSaQIh9 z)okwJ6sk0p2>`QmfQnMvV(!K?GTF~K6Cc*eJV2_I&c|FY#Sv>i>GERE-ZsQRKffE|oTY$L4#81UGB8{9QF|cwus$?|D zGA?Vr|F9LC=S}i#w-aTe#(qL8X4Lv)v5XN{|C?x3LVSbUL3&ovDS39()J}zz3G5H; zedfs^t5d;5agLFY>K`xQTDbjp?--uvNGK~IZD5Sp1Rj%EU98jlw{U;=9VMk>+YmzZ zl*zjRvZ%PnjPic96LJ(a(6Z(g5ASw^grL!X2^+(%^J->Kye|EmyaVGAMrT}iS_!nU z1rgy+>ew`gx02T4PJo~~qn3Y|OuF+GrdrwZY!2|Eox8Fo{AozrQM&ka5W(M9^gPx% zHnKh7pCi3hSglZ7sSCIlE&k|dR@VPil0v3057eq=ta$^jL%s)FX+c6f*svc$SFud5 zj{~?>5kqB}MCmyINqP1cz>_scK5k~!qUua-GNCn$gx;AC&R%v}c3)puc^rUZ@3Fp= z+TETEo>fM>I2yR~)34apUXG`I>LV0PGs`P#@MCwkBSu9(R9m?bEX6bv zKQX$&Z6nVWI0vxgf(UH_cGl@;B*LoT8%lS5I?7V z-xsU&8VnY{<{wZr;VIrg!%V@ijTPBO%{K3SdDI;DblCv3^3nFkGu{?s6=kQJTB7#Ztnk(MO6Fv&VC}5altlUz-XLg2Md5rp-U&8!F&R zZ+x}Yty!~EIW9}rm4H2bpW=nHk006s8-E?O8p$0wDALS&RrllzK=H>fLD4NA@3Y+{ z?<_6z4H9Zwu*_wM`-+Zq$0Syj;FvH*nnsbhswKUTigf9bKt8&O+45Ql-sEZPt{47(Uwx7+SUqd4e)Ax3p7Sy^gTcS5pqe0UKKA2{MbfgC@(I`o z;MaNq&pn``|AXNJEv38jf@fm?SnFw*5C0FcuT(H*hy^OxVtlnhO zXixj9^6zE>iL%FI%miAq=uN0#_^U+oo10&qw}loaiOO!$oG=kx`tQqq5=slq*tzMbL@)>9RH&kO(~sYjk2;d8ad644tPs9O zCKheclo&%Y$0d{l-IG#GB7$KeT`Vrx@TWFKzMZixi>l=skmM7&L^{}qTXC|`f${`G zgMqnipPxA>E4#u2akCBo*B{*E0As=beVgqQ_FbKG#s)9ZmjusTe9vQr)=5jb7jn;^ zN~LsK{Q!i*_(6T%D`2$8<^|2HdQ^v3K+O0(|4gGBcJ_w5AbOO$R+B}j(C}<1hA7@w z8L{Emxq4TJyEKgyagF0(?E2kYn=cHO`zu9dMN-36(lcjte&yg z44 z?QWf)k~QW=@B2fnetCCz1qfiwZ#}+8@@kWVaziFS1`%)+qp#W!_fEGH-^-N5l5+*d zX|JqjIwyOAMjjZ90-1a59!HecVk%6y%y%s+bS?JC=NbPN*8!@^X@(tMYg^gu7{1}` z&=G$rI5pBl*t`S9(;0A(fzM{9r4#+}=ZTk2gvydYM#x+HP_Dsb)ON!#DBC{o3y z_UDYa2S0Kyazg5OSkNj!TbwC)3lBh{8UH3*J$pI8O!5D=EPlTOu5l$N%%i2UeH&LK zLdHp298<9$J*~z)ki(%!&i|$Q?KmXZkB7cgRgyWAECMPYk{cGvH%v4EX3KWbmdu^a zvZ`;AS7Zd@p*0SD4_*a2j_T?HlfF0iFF>4FVLo_lBn!Uui%jzQ+|6$%VUu10Tv#uV ziRDG`%PZTinW7=*y;1WjW~R(?dWj_;{rK*oZY+5Y%oSQoARf{%pvk%FU3*?;P@oojie6#q^Z}!jA3re2~ zI}-McBa;cbPD9;$n8Nd0??EuQoO*R41W)Uv0HFUf$?-|^{X&@tl-4rBlwTS|)5z~V9Z=i*p8^uDGzN>}s)|Hk7wVe4v&z5##Bl(erEALc)n>DHBv*?P}Q zg7PFM+ih*cvEWZf_6i`ei&s^Xhst74!$g1vJHrv!EV28XF8u%7EX9Oz@D-P?ubh3JlaT5HSW50o$3wSQge zp5w&1>SxE;)+g^=s~j)lRv~(w)*GLV7O{o8z^~oDJbW_VNr4kyOndm0w8b6F0ViOdyy+z;G+MIw0#9Y>{E}e@ zWh)qM_J_=d3zp(eHdyxh`V?pj5$H5lX*_nfF7ECV`glXuQMw~5`E$8Fa_L`>d5Y}w zX_kSOFqO=z&Vkaz9wh9H2vjA3H^5ND{D)Q*iekrs)kbEpq#4W1V0@w;c4W~wlZo?c&my`_mn-~s77sMCp|Vpo;`v_2I7>_=^gQ@W z)GTQl5esi>yRDa-<2G2v#4IYYvZBOZ6Y7S<%+yO5sp!0P^kGwJR2Eh@Nz-W-(HmkZ z!yA2M%9w|BdEw4Q%hNI|L1+ryvyir^d1@>VCV+W_U40j_kn!r}AId)La}ukbDx3I4 zu`l&i9)+;yNwn{|RUS<$2U_T!I zHz-M*ff&@tSb536Tz;56Huy;*1c>-k1gM2Y1ddlj=f+x6 z`?D>ZifG-EB#sfX68xc!!n~)!gVUf<2syVG=H7BOTwa{tCGIpyn82tuiU(!gm`XX5 z?!1V|{7q}x9^k!qGl@rI0SB0F2)V?OCOpAfE}I|}D9%kuVGI|H_u`?6Y?&tZL~x%* zglMY<`;g1FnyZ@5CdZuH?g9)=WFd92nO!8yQ(Oa6qd_nt^1{(-Bu>UQP~VcHRcf^o z|7RZE;M{8if(GboB(HDEE`J@z=EFFyq(Ugt5kN4VSlrP3d9ovFshsMROdf?=Tgx5y z;Xh?QQvtvm*ASTU!<^g0@)4Xt2|8bVx!p?fyHX=c;libgbL)N^McO6e=EbY6U+1j@V!DO=QsCn1ip`hcp$6dTIC;Xkgx%SXHunJ<&?*-OXO~} z+2b!fQuDk}iv1pz{AwiLvy$ZiFVw`1${uUcGXlB&Be;9N9lB|Ep+K`|A3ZPq#$8D- znf4)S@0i5t=`7@h%4B$jH)fRi2ZG|;iL!oOPFb4K`<$Q2b-=u>P_{2mIF1X(J9KPd1tczCCj<5{VvCPMX6z0x>ao!_+qjs$2193b6X}bLN&X7E6!Ccrkg&tG4iLcn^1IXc-ST&|PnpV(Ia-fbd#@it`mjsH;QtI{+Vd*V zp$derymnx*9+UmSV>3va?Khcob3(E(B?7HCz3%qee- z@OVn~urj0wd-JKITU9QQsMWAH&j-5m*{yM``=EVy=k7>8p^z9RLY@W`5TOaV$b!2Z zfFQRQsiK}@o}&!$`ad3?J-e6;KdneQ{w+*3Myv5+>up@Dg%@RZJ$I`#N+7bf(Oi&B zm@Ji1?EHE^{6+o(%$u}7>^`VZf;t4%$0sWNU94~(r2fKbf-;TWp$V8}XrmU+X3_}Y z9mF!V1`8B$<>w}|aC?Me$A{ftY*WvE(Pe`%G9+DRhE1n?x?lk@zL~#eQZ8&x$q|HK z5t^;~7|ejz1DZ z*xiw+zgW3g1gRWIn{Bz4<;O@i>O%MmFXw)G5omHzr6w~ErCc{jGZ2pa`m(tSc!-c0 z@1ej8vnmAsPPFeb=JF1=1bmBhWIAXgad~nPtSs`jeiHH^%nH>bjGVU|I{(<&`XJG4 zLAC08NgNrwf4oL7mkqy)OzQg@QV8^zyGFr`*E6=oBiwL@ItkQ*m;9@fD z9D0&^o~{A)^^I9sXBY~CaH_G$bNMZ9d7jY+>!faTG2wu^O*+5z^3cdUpD&|8Ywd!M zjIEp${DvL*S7M2~j(E~%F^(}n!%t&P|IkJFb6fHcrpYjETE2ie6=Rwg57t4EREh<+ ziw&e}RiR_vNpl(ekw?1LYDCi7o$2H)?%K&E^N`5MkVzreRfBw|P%hE4(@brqkEPpt z%brQzAZ@DVy~S^FZvb*=WMSUCZ7O2h;396VDH$ni@Cnj*cIu1YN%(1{`1k)URlwAy=kjQdq zF!9n4_J$LMCE}%LXY;Y5jWaq3G)`q^rv1+SxTO()-)vZHBwmL~?}tOif!*UUP~91xW2vP@sB z()}62-2Lu=E;|xCC17O8^mzV7Jzq7>8|oWBb`edl_(e;4XU3SSv%|M=lWge{y-P=x z^(awQa;7%iRq9QiIwe`sN4&0zf(j8ueW8@0w1h7*X(;Eqi*ZH?86I7x8p-7lsHKV^ z(}k}VKlCN0rpRiCS5{}>LXj{6ULUP}Pr&2gpI>pF+7E%E;-)kH8KUq!B2HyDr`D^2 z;^bsv9)O$G78MXxpzE6`#mH^0@76zAHK&T;l?qPOkh~$`@|$3&wS zoHUk2Zkfg}OV1#s7&!dZFOliAM|x5d09;82nC;QZM4_ zUW%3l$+)pDKJ4zz^Bnj7i_0y>Bt0qQo%4b7AXXG(L6Ge7KwN5%Iy#a5npsu+-pb5; zY-DnRu+Mi(U=A1#e&Apw&Bf`jLW!>@u<=@+ zdtn!^8kE!O_FyE-qC1QExi{=iIDK8uFXJ@3#VA^FR>UV0q`3OKr!QWvj&O?{0Z55d zSU;P?4G=czL`z>8Vw+L4Ho}kS-wn7#6_`qZ7cgVT&Fu!?=0?>Urv967B2yfO@ z-YTv+GxIb(mX3B<#9;n3HQ?@|@mD4h`9eke&?(02bPfcy-F~hddI(C3Ax;sf&#Z*bCSRdc`$+X`d8O+wH?WePVOJ&FO#_On(4pdQifB`!2)WX&C81w=X_ul$A zfsG3ib!<@WH-Xe_AL+66F~wm~i(ev69cw2Hi_D|4%LvzRV?$FEDp3!kBB7xJ05998 zf19zf>T@GbIvrX$0TScbLQ-$KVRwULpp;3O&bmza540Q#@6ISoIF z@A0@;{_z(AxAPj=tLrfo6+;q$oZ+4(A)V_cfa^O}iaa}RqR_6H%otRtnXgB`1gE`_ zFiE{|M4~;8qc-vhfEDgu3A(Tu)$URY{(n53g_Vs7z?uH?xOFD-X1Ze~$1nH3O zt^rg)LRxA7MH*oMDd~`AknWO{?vD5I-rsxwgZa#P&N=(+wf1+3+q}{FNgAoMw$YWj z#sV`kg0G$RM;kqemQb(vq;NMWQ_IM~vjgb+PCa3eB6Y|B0eg!XR+jDoxv@g+qjfdmZdo&*t3))JsZHncTPaCw_qh9dh|JyIsTrcj)%YpvJHI^wtMG_U|9gJm^)*Q-AkbR`s`)gHeI?XCn{46*()nCqPy(aU?IOzI?f4@=wpcZcY zXQK?s(K_N`*`n?8R~15a|E_XZt4-lntZ$>A@&}6E#;RXWrZSzxw-W^WDkXjcREv6W z<@SB*qVL+=(Pr*Bt-cOFyOPDWwhwDN`|zg>mtPlxWfC45tzSQ3C8uNUrRSnp=jks6 zxP&kRdAlmXw8{`fyy?iXIc&2Mo_(wCxyG1+ZL>zVpS`tIno?n!dv*Oskcj+cpU2_7 zJw3Ds;Z7|B6=!$OO9^Lso2wYAP@P}Fnk{O`yUW6(W;8VW&FI2(fJ@$JkeK@7NQ$EC z64NIevnR~FNZ07AptlmA6H>!jXpN)Q$+Sb#B9RO)M$k)@n-eD;Bks#3^{B+REZMse z3?G-^CruoiZ9V--V`pJ#+eQ&k47`@rzJU8Y2h9%A>x%O!nxHDb(++mLRn-u5b36h8 zsUqqB>8;BwlPm-r7YWM4GR0AY^I!y-QCu6)b-h^(Z zuiq0HR3XuVud2!|^2k#Dbg%VDpnEq{sg)`AUfb5qRP1`| zKF6$LH879dgTLJh9qrq88p236`|JAXp)vsUH=FYfKD=-+o=t__e*r(D)>#$j#Z$_K z!8UD*-M27W3&L43QoH=SO|;YQqS{7B(0xjCRnQ>}JX63N&8z*kN#$K2T3M6*w0kI| zZ(1D!4oZ_1^mf7r{`j8w2V%r2(S(sRU>{;=jWbv{if%yJr7ku3STKdBAV($DeAzi> zf#!jGGNV!SX(A+`YK=m-M@o69W}uu#E|OD5IgY2TEr2dw=zx$?KubS20T_$c*>|=p zhTqO-9_Qlk;mIcjzLv97vyBJnCzE8?E@G?GrVLPk^nl%WT13e z1Kjl&TXGMJ9dw*^$ALg3T!ZOosHcue+%sx2qsn-yglm4xQE@f= z1|CYDM2(XiTLN+IhjjpaSLv>v_INp@GxT<}Ise1TYBLD@r-+mvza)`;+B18a`@#6& zGqxLnjAUZ3zrlukfuIIX(R3RVf_V=m5=A%!7rWK(9$&GQTG#%Dl5qUgFQhU!1`vk( zz9Z7U3Ny0fXsb@;r>!;cq6hT&-UH6w&^aJNiHOmz4I+HZZ(fgoN9s=>0dT|Qewn85d$NxEH#C*>PVet7pq{IK+-$x&0 z2?YC4ryvV^dAih+p%GyaS9B^%(T^1X}HMnMN2^(}qCqFyyda z0o@az>FT;|xyaI3*_92USJ>snt5)?ewFmm1UOuW!zpo1>8nV1yajbr8dmiPGTpr>m zb$!?c&~s^qzJvjp+m`J}72Wl2x@(J4t$EQfXuv{9SsvbfFyXm9S-xL*BfA z!>mkC`p}eh6nb-2YhdNp!cYzF(Np9SV-tedZGFCVbEA6p&XZs*Z;%&T&CM`LS!ZqO zTaK~-xlq041O;#GvZq@pJ{5=f`@Sc`=DYGU?^PFOma~+=K0aB%wU_r{72Hk?*5usdQpaeKZ`i${+#uy0YYC>eJ_~&iU z8=XGR`_CBS^{saRf~UC}Jr5$Gv zO*wd4LCrkgz?ns?zadz%`q3cd7vF1)G~x29Z+PI_eBnVo1?$w;y#48pZG=J(z7y8m z=%R9RjF8trl2J;k5C9gf zgY-G767*_Ids_~qQ&5AF(_B`;#rt9QOhrZq@XsB_sz&M+$C%tlPnjj#3oLm^T(-k5 zk=&t0TYL}h|6~SM6q!EP6i;HdXHRd$cm@Jq6Ml5QH#w9>CtoPpcgJ_Cme%q4CI2JB zQSQC(RRv&3u#Y6>#f@CSXMLF+TT}dpZNp*gUD>_qj=JD*Jz@oxcbJ3Oja4zJ|l zvF@f?SLAv}-smvy@pTg2d*dCC?@m92S9-Za5Xz6&U%kzK%zL+Wn88OTJ3%0>-)x}~ z=54(9u|v3rINM*0cxTC48wu*d*ewHYe^!p|Zt20Jz7`Si`$ev!E0R^xeX{2;{17|5 z@=|bR&a+G$-Fpe(9UxAD21k)ReDVSiQw*boW*LTD()FKwf#DK#1-Ir2_#`O8w)zoJgfeq&V@QI{!M*pb3b7pjL596f12 zNn{S~`g!Hm9zR?ySx?{nldaSQ46&HN!_QM#CQ3E@ha2Hc9|uV`?VJDANAqGs_oA{0 zaW=mpjaj6)Dc08SG@MPTmZ9usK@BK0fsDKa!(&h+fvAj2chN_>%nuAA_{gs#lX50I z)S)rHhoE2r^WLy(G#h$xv9xJmIKF?WO{7fkn&x5QvasDMYL@J@WuQ{Cv`(+;#Z zSf8hI8#X&HGaqJv6^j@#4Emr}a2(f96+YxMaIV!>W~|lPyy+V-`{yv!RBy^qUIon^ zMEGDUIM-sDtX@Ak?E(Xbqah_UFuBU^`;~TD=k0(`sC8Es#mCRlK^GI8^LTPA;FF!~ zu5bjia%vpvaV>-m6AeM_D$BFYnuyzc9JTF=Z2QMi^Q~1HUI#jrv%mNFx`23y%SP2< z&Zo%Jl~J1`FzuacJcd4^niqw0^B+n)IAhh}FImt@*Eu}XFt-y@-Jt?AgoqL#VL2TY zLYf`cIlRI_5pB;dyZUT}P1 zXefra{yYFRiF93~Mm6^ll+c~3ki?^TN%szi<{7%1zWZiEm~F3DYp4W7*UdI1hghWA z#Bz|*h9R45q6DUSj`C%8_xcHlAkHEqNu>7&hL<5^W|Vr%67^LQQMRunEJgE1+|Q}U z!9hwRMX^kg)4q+V z;L{qA&>sQvyG*X|&M~JyGv3^=fena3P)P!Ab2Q?N&gZ2aICIaA<7Gb7NE3~y- zNCLJ_A8bk@a{fd0$Rpge^{V%c>qc3hp#8=dUG~g{9dR1%;t6mF_h0vb(1vTFd)|a& z1`u@rdvs+6-R4>L*0rlVm8e|u2ut09RTDAD3^vkXDCvu{M>O$;VUphu;Dp*Re_(qT z;bbbHT%j}-4brL@TK@K7Dk(@m*f%>KVnR$Bz%Pn)tRmCTY75s3OosQs_PkFRg47rQ z*&evxDPi!fFv+t2>yku*`R%A}3flxbA%tP7KfRps}ImY+T8Hq#Habyf?UG1MReN^?$dnX%Id?%GK z&pT*TmR({Kx%2WProMwDc>^Pd=zXZDuZMO~Fwt9P4S=Zl7eAbsi!3aSt(_XHy`Fk@ z1Nja=d3yS;gA*pij+04j3FgQADG;7Uf@v%sWzXyH5gB*p=9oJwqX@M!rv?=1aYJgX z$NdSaAgHJufppO`W}!2T9gy6K%`YmB=Qh%DLEThXhJz`;PM=_x6=4a-@gf7+VxqcNNb4u7 zNGu%(-Nm%W3w2r9ObTQCuvLVLqYvKUG$e)+UOf1m{BfLqQS(8b8j4CyhP*+mCM>pl z)#iTr^NpxVpKf>Jtij$wl_!@(>H~J4C;mgPVODEwn zhcwJKUwm;}T3hHB05iqkNmY!ri5HG~Y2uDvjaM4jd7e_w>5%xv<3D1?GZHuYn?0Y# zfS>|4BEiAsiFZ8aMQ;PwVJa9Jf_)jn@W_a1mGPB#-uxBT25lF$_`v*4`zyN+|G6UV zOci|p%ED#|D@G8`j`kD#nlq<*dv8vaMn)D3PBM@-2JXk3=;pt3rxIb1r#g{)4E%nf zqpN^2vh~yNy9*fU?zWS$Jw%nkqplK$dqbt}QM-(>Cw3B0mh0MM-qVt=c{M60a+MkI zO1lGsBA!x0F%gy9%NCIq+*%eN)zT=7=bA zCP(*%U+(b%^2#9y%7^;Dvo(=cCZ9V@l9nUpb1J9DAPC|2OD5;Gu>iua(x}=qCh>+M zhPTaNB>QnOMnDh@N0s*S`W?{9Bl$ayYmMZusz1r-s{2YWFxWyael>~zdEfsxazXf& zc_fTL+UuZBR*yO*#!i_3vH7dj!h4!q5-|51gDh0?CbARp`Y99MZ47X;nBo&-ro2!8APK^#ga zP|T)QWV4YTeNGhnk~Nf1RS})&Yn7l8nL^i8iD?j@49%mohyOx{HJI$o2nC3NA;7%@Lg8ZAq^5-sf->*q3NVrRn^W2rF77*?P#sR{Vo2l6%gs-p5%_;#=6FQ5ik@43MY|4w&+;KPc#>o{|J z`yv_8lYpj@zlQU}dDE~SS@s6xnE%(eXqnYRf+H_u6aI#J7Wz&|WHx`)DO=tV^OnT@ z-F1$ICh(BWX)vYvFrytpfv`elBQPbOn00x3CS7dJv@&gqm-INkB|RTeHDR46{I(Mh z{{^qUG4vxe7P9_YJ7 zgAh>|jDVUU$$A@zvKgp2pous#o9_`0D#aLxsHYo1JB-d z{Lq4(&fKMp`VJ={*nhj~GHrQh>L&yK-qFoEwQ)rrl4VI^d>dbUUigF?1>iAYXDNR+ z5ZZl%`olRh(D%HJxd|mReClwu#l>Jyj(j$i-%D}RY(+|+H0aM-)TG2D#=(Rb6fyqv zDg(gk{O=ZX7Xi6Qdsh%owQKIJ5Kr(oiYVKn*<6lY`~M1aRHq(H%IetpdOTF~Jk8jv zQP3;CXx0$RT3{%KKwnn_Kj7s(kPd>EB+_qcstA6rF=bk>C&EIO) zc=FU3nR8Y`K`q1ezrc;uVL($2>umA;ua@Wh!qpKAL z=ts$C4na^xcX%wRP*%{_2<0ZC zG`{Q37GV7(9MHY@%Z&Xk2?Z=^YCZw@G*35!LrBf-MEZ1KFYi1jf(*cL95lQ+rJY@_ z+5jvdp(~;1`?HI*AKdzZ=${z%^v?{hwysl(s4UZcCxS{y)w1$IJE-q0%O;q4z z-}=ZFhEFUndFh?=;J2z#T2j^6V&l5_h3R@-)G1+-pZmgkggGxei7^Cy6|BiM3=)r)ul9@RYflwSrAqcc z)3$mnJ>7`}L7St;6e3^W4QuOG@chh{krd4XY|mLeR7Q3-V!9g5rB!8d6m9UofxFZ9 z7{5e2eB={OzMVW%{T_eQ;A%MtWP^le$#?5x0!j<=N? zp`e|NTIgOZL&nDb(tk1(2y5}t#NFPj?;3dPh1JT|`rbi_0MrM=MOuDF2Mzx;4f!p~ za?p5fi_R?(-KUMQ&NAI85YR23fdh^bj?&n@I_n%g5kv9OH{0k?Jlv9@d~7%X^qRXC zaqKO8IXBx(Ek4e0A>n6BCE2Amn+yA1g67P5n&<>*69TYtwi`r}v4nx{AJBwX`x6B! zLGL~HRM;55Edq17E1_Qxe>huCYEd=K7px)M`k$tZ6T`g#Km4|~#;?NCoCJ`EHT zn-w6}{lPbVH?U|1;Z=I3n(k<{Ay(|Hs4QUZ(_GQYfan72ii8xTd{Yt}$7|MTx;b=7 z>MjFl90ZorDh6LrB}y#j@Lc<_C!NOR#aPAwrL(e z&R`I6;%D&|17BA|vp8Q`rIaKiCGf}>_4VU(`X+zX)ao6B6Phf~RQH0tgb4mWtvCqM zAk=k^zPh3bRWMzX=*yG3#K60{#5YDCa{+`yzKv?__VhT-?9Gzh;d$quS)LVw+fa0w zavtm2Xnj$YP;*p-N^6D;gtx^(vVu4F_bmz;Bj$B}ti}yt zJJ=DDK6Xc5fbx!$a$S!!{A-2<)4a$W$_y+K+S<3|hQrUG(U)HqQrT_lFxJop6 zEL#u$UozCm%Wn&tAgDgcVpI6?beRllJU%v&>7U}k4;?J8QE2><+7xqEpp4fSi z%V#%zSL!pPW~h8J5UepwgIP2pB0o4Iu^EhFGmU#R6$Zril{zkq0Z#$2S@w<1@m+XT zJH195PUz1%U-P0jM>Qa*!YvSmb@zSEe_phQ$^7fx&POipn#V4h8G*w1^ukV;u|@vt zvRCuR_v5)wIe}oOe3fF3#-Fm$-Kw#scBd0n)9e;U*-v*`^2d_Ob;@=L{Hin|vVI%> z=f7a^NUxKhp#k0rM#}s?LKy1f=0op>S@0m#2u&bt&aJ)@58e_GwcyN7do_S}P)z?s zSCFrFu*sTmLo-1*B(6(->>QilK-9FB}z%#t_y7LPOyxBWxdzJ%|MI7nsfxzNB|Ad-F+MrhqhzCx$!wO^3Of(c_BFd&J73h#0Akwbw$aD^NyHm@VQgL^t#20EY!oeL;bkiX$rYJk z(G7PgeHqdPAk$9E29NJ8P8#XR37YfypcSY^Jx&$cXx^K%lr$TIM^);~Ksdg~b^$5B?I{YE69AZ`E_S_WbS{ zf#)d?NqJYvKn0M|V_tB8>gOk22`Lc!kqAsDmvjC-=(u&ME$2tyc7I+%zLqAI&+qg$ zw%I`YpJD!SU#wvsXr&bP`Of$M8RTp%9pj#L5z%UsIXGOLnU26#I?!jxN_$D1kFWjx_1adzmaokJ)%*Ld?zZClplZoFfH_soCRy_aMxi6Hr; z^ARlBRDvSz+tW%Nn|QkHLQ*nWmuRwIT2IiSQSH?iRhGl_R)^v7Xfku8Wy#q8P95`W z@SDK^M@$nCCgLZA&@Y#mLIoYCpSd75-P*XnBi0Ig4`Frn^QIn18l=2lYDeybfFp^t z!<3i#h0<>dR|Wz5#~xrx9=;dl=mX%|)$Uk-7WR9%T6AWoY`@k2*)gY=FgKKp_ch9J zF`Ab{7lZ6b7o5+7OHKMJ`rlTLpoOn53v4KLdf$Gon;vPbtC$MH+9M)c{q2aJRvtp{ zdQt+R)@H+eBjkDDb8^X~wl&?VITb6PE|XmC)?A4($vpl5bg)hR@L@h>t=He1r=e-S zasr?_Ml$xH#t$71qZ|AFuX32$8~|Oc4E<5$|7PKHAu}009qE#zlNf+L4<6??r;FIB z*#NrGE_D~FWd)L+6hm)EicgaKRm77sJX3_r0?|D}7@l4&-vcS<3Zzyc;{C95!7_s= z0JuCk4*!0LwI>yeS(!uY&B(n6(x}Z0mm%yKnvMO!X5cK5PM#J|)uE~JuCcGuhsI2^ zWWbiDJUEix(ZD9GV8Fa@sGr8T)o|cdrw#FJ>Xiy{U&&tW@{JQ7yhmZniy~v{x_TwC z(7owlQ|@N;mrMx%u;@!xw3N&~Mo*ytl{y_+96|SsdHFdQV5y1kv*Bo(bH>~&1dp!| zM_8wx_=ybOCwUN{NRl`9lCTuHzGxa{mxpW-1qs@AHCyupIB`;5;Wg+H$v0Aj_SSnP z0p)M=Fo31Tgn_aClwl+G*}L#hKr!AU=@4Tldc6>tqZTj@vH0zNl*23 zNaGMKNa;%(z)#)c$lN#fEgvf`AIHiGC$QPJ#8>q$D($Ck{RPfUb=-D)_4VOCP z;6%+d(-+v&rmlR4bUKMtp3bUA)L5o>YvGZ7;0T4E{S(BED>eUJYgWYRi*bYcb)MDxnb#+zx`$2o^;oN~5eF}EVaMYE5WC8Mq-0vZ z7jbGs+?ZMj;#E^ruRMGmtPEgreNlK{Apz2K92Wz6J9Y0AR5=XJIl)p!UMxgitjj#; zI5%ng#El!SoEX#m;22WR70Wp-{FO>5jbkXAs^E>u=0tZ|8S#m`z0NX6RZ?V?=Tk1L z32#TmLw3ju*`G7Ld-c}o#8tj9gEUYFg*~O9-y*P-v~pXYX~G`o^dGbDHPdEYl@jHP zM>NlIu(|AxY*B9Mojah>`dW>QS{-tbPLpRdcdvb{x(Jx^b^{f*sm`Ok3Ic) zi>dM2zbG4{+xvY&rJ=BXt)^wHyFu{-%q2(j)laQJ#|c^h>WhmO$5Hh!^2`VOaIluq ziz040UNJ^<>_`O>7K)8rq*YQ`|`8)b-bm zYz5?OJFOLKcX{Ff_34wmp<|vBeYQ$TfLLZra4)avkQ4N>#!yMBmt0{Bl`kdKBngZ{?1(y44|Wtjbz&=c$LTThwR|F^4o9t zem1x-(HH8$@u`Lo{k8}ccbrRA{2bGN{73-Q%MInQ>|$n6Jc|u6V^T4IH1}h? z-?n5BOZNdfYe~iCqZ?xs)>BO=;(}4m?b*)fRpz&|nii~$H&}9!zo^dzD8H=}C;!+I z9iujA3-7<*u3R5`6`2t+ADhi39(3yq%;9ETUaP<$jmi`4I4IEZVzy>0bg#MfdWs2w z#}a3asI2Y5!X=6P=T>cy?Mq|_I{Yu6(MB?&SCq1w4Dl@-exU$Lr5NMS8KNaRV)3kA z`BRqplR(DJx&PO)nhj}roTK} zAog^;Ha?kCWJ|;U`ev~nwnT_IsWnO1HhCE!FR`_vD)8aWk%y~87pkKEY>q+d>ZzqR zCcxO341YxVM*;9(O?zknYUt)^(LNME#QSiEgz^W8SRWCecvp?XCBMFwhX4u8gf#UG zY-%i^xZSW(NzZ?w);H9sS?iD?A!=T`uZP&apF6w+`Oxjil~1-;+AgNR8CxYH0%-9x z946l?_1=E(s}#p=(j|mE6|#>Ec1o&IblDhEud26t@TXJlAKp7wkc-^QKBaR9I^BGO zsy=>FeB!3x_M@sAzV!@S-)xJHYYb1a)Bkr*PN#bRRFV$0Y47IPd^0G=h-*iRULKZU_>UXIg;_X(Gq@rv6Rk(a)Q+Uw6zEpvZi&*zD8z zm6bE?=(zvaz^uWEh{*{plX@h^@4bnaHdhW~kO|MIvb8}CzA0ag)Z9C6+7vHyJN!Kp zf82z)m8=dk?#3qxU0e4`>5|a%@h|l+H14S#OFxB4(pzB(&d;;lot3`Iq+TuG6N#pw z>?hYM+#{w)}~VtPD&E>Gliu~o(wd!kG@#H zj4YADe(NR(MX;9+@J$;Bz)OaxYk?LT468O8w@|5j(p`Gwv!Z=ohJA$(JbA+y6aH7+kg!mMG4 z3YgqO^Fn%dZzQ=Hm3k>A1wOsVv+H{~oS^lY!(3bag#(LPfLB=rU}1&7+!Cri>iIaH zAu*sPntby?*tQqV*Jqx%sifMMWvp3A#8oVUx?L6T#ym;2+AH9D8Ay(}#;w{bjU#ca zDCS3WpoW*#yX#fqwa`l(uN>QCJNQ=AAdmTP5>nghX&utkhxNpuv9PoJn3l(o7SEp2 z#B|$4E+>)w{b`>Gu}We(6B^r=O`+TY}PY+$N!t(+QHuRw$K@eNub((NxcwWK}_1O3m0^E+Dq? zqNgpG1%)k zQyU~6UV`RzHfqYxF&l8JU55(Y4-dti@ng!&C98sTOj*jfH5A{8t zk^DPad2-X1s8C{>++l^2%3EvCV&I$R^{T)v<=3QjIqZA2*IRXI(M$U#Gf%n2!|9d> zotMZ!{Oo|kc7L>i%8HS}a*uM~3^Cx2b?9gVpv|C(>#j7@=6^Sp5AF9NB$o{cWyg)* zP`9%AxlUbN!w5-Gi$k2aZc@iqHH*np6=@Ge51Z;T(at-!j=wy4NR?nf4A+ZR6=Z8mdRr_Hs1}c5U>~9=MVp!WG;=EF_Hul{Gp@ zHon#9XL9U?vDreFCw&r(g*=x#=>oF#n2TZX`omK`K$es}v&qCr)pCCQ51RTgKi#{i z->%U^=t+A9D~a|%KKUtIZH-_b-2)}mAWSSt?tNTyBkBcCTBnMGa(v*rMp#3yqWUm_&W6RNsFZY++=xSl>mj1p{5G`HHBKy zcLuDzqn@p}or5Vn2>khCEcx$Q$Lh&N({%tP_j0^q-Luf*I0J_C5yIh?zqBaN>u~gV zbJ_XxX?V6;IlV5WM&px+eLsrX)qDGw81u1#HZ}J$rfxqKllyrzs!dd>1^Qw4k~x&Yj~aFDa<4AS#lxlc9afKm0J zd|^{TnWFzoOQL)JMpc4aP8>_Fxc!^u{KE2cS!~29 zj`mg}Z%e?xQ}C~-_%5o6fHw|HYxZA*R-4@kD%+>?ySu4Wq+IFwGLl3d48ItV@XU>Z%QV(e(Gg=lgc&itFhSkYogq&cVel)nL!uxoeAH;<=1rAC&{l%FM{T_+^|vcN}lVR+$xPeRjrGnxd!4TmS``J?Nbx>e2~c(4d&{p?{C z4a+=U!_s)RC!sYP1I`|q3NE&MGIL9$FS@(8nFbF3o4_5t5}pwzLGm;Kt7v*Pk_#nO z^v|r)m;V-SDma86u?E?waY5fq_bNJeHWHZeiSXChuSYEmJMm8^OE|fv2^oc9F`#)# zE+QNc-vL2$AG=7+w@x3Fkn*z{T-XrS z;8?avs7C#hs8dJw>0)Wp{<)W{6l6~X5hf{+i z^#Fdx@m>qG2LKEjs|v*ak*fG+F);l>+L;TDI;tAYID$1=$;zsTAfXyhji%l6#xgQS zr9|~NY1YxNieduR0qec+#*SOIduRg{^Qv3+u(I9Md4`vbxu*RSU+>&K#w8iXJ1p}^ z_?F}vR~~N1=Y2B7EFMlD9>(_XtE*`jLAS^pNn;TeipPYP(CCN(O$=(euRb5)m5-t~ zJTgA~w?-egmcJizGTI5cmc*zM^VMWBn>~r=R{8ThxuWz=^_wB?Q%@($QuLWGx$lPM z9BEgNr5NZQyHWotQwGk4z$3cB$u{{p;!PTF?+TPe-a(FYRS^euC?N+la&}|FnfAp_ zx+)i8i{qgCd$`vpnNr?#V5ryw0t0-1TGT3-ZgeK&91;cPTWTeTqXz7D>v-rBN+cU@-x%Zx1j9 ziBDzPkzB&P$FtzZyzPgcj(L$W8P{od!urvHs`b~n;@!5n>K}FErjrDUifFoXAC+SH zmSaUiOdi+#eG&O6l!wTZQ%Epz-vb4=dO>^4yAESmVXVY%Z3!7Ye#yX1tkdVPOK6 zU?nFfMfy%5?AS@xT4x6k)UM?rCJgQilT_wyX^IB#WzT;QbcgM~pAOvVrh=1kyA-hutm1{3OzJJlPnh1Jv5&iseIa_{`0MCKt?*vJXrtlAswfj(JZwh8_%hAL(uQcZ^vY}>L+e?M?XH?edW2kW^hN{j&k})%4UV>rTAd`*Xl|1 z`0~CFr5G#$1!<~Zw4~&EEnTslB(uY(2Lf5lxw07E1Z}rUXvskw7{iG%>qKDi8q>%3 z?MSFRJYQ*fu^Z%rv{zuy0K;E`kGicK6ZR4f8SLWO$wptA=@zj?m1~E= zN0A)pd`Zz6N{F|87h<@up6dE3^3|V_S-CpuH*;F_ogcoqEk(9t!rK5LVwN7L7UuG( z6J8u6pJ;;$Q3WkgxZ8&-fmZm^e){`7JUFwqvXOL+e>{uIXjD}7kneucmOy)--Z`_8 zT8W{@uO~ynh-9m0el)oiXL zkd-3M6KTys*u{e+#Z-n8q|>{~YeN@WFugY=GQYhvXqg1*SK8=)9Y#A9Qkx~l4}MaX zYsOkyC3;al*s)$(z~UKroV6d3do4sqMdD5xzz|JH^~d--G0-iFXYy>Z~X zBx$PCfJu0b>>=^wtrM=yhpdFswf@>N@G_cfFL7ciKnJY?x}nV$wozlH9gY0 z{H_$_jGsfM=EgTx#;X=qU=YcQTihi`8WN)$@lA}5Y8h)rLgqjkmE|2l z@!QNk7u9rDeIwnnlrQ*JDi>YK%bF$}Vk2c}Fvk&kK}zvjBl z$Af4ME2=+MT~-UKZLKE!QvN9I83G-l!|?A`ymPm(p9--Vfp>j0$+11k$z}=da}Vht zwXe^>bW7(Yy0Q8p?W)7Fuvkc7j#GR(I2J?~hd!T!}m#zAYaJ={8SD3em=wXRFUtd%H-Mm zc2ix_s9A|QK5>}Zpb$zOu1d}%Du@~%$M*bGhJ-er;Zf;9>d$g-8#~WSEig@TD>iO# zY^kgGz+G+@LFpoPpbxvpm_#D$ami{m4GW>1-2`h2gPHDzLQ|>t$`1%Tzj1+^I2Z({ zW9e7T6bcBFBL2;g;V8$B-{)B#eVlXo+Zp$=p*UK# zpqNg*HEV9$(h^^_w#9>q*v}i%p97j1J}YI{v)zYfY22&{y+i7p>bO2O^Sw_?WyJM9X;T#qGLY(Q^bY|YC4+?r)EgExFploHdEjQgHO~2!xi;rM6o=X1Z7H(>(?Jrx z)=dYz53H2sj{BSjYMT@Fk*E*8Z7ZQ|66gDuT@G9Q5uogyrhMDgeO{hOZjjGe;IR{R z{9YW7#K(@<>#baXC87&7;f8zXmTUY=0^a_NFbXk8PFwhz|z`qW-1^UQAZQ zx63p9MBsyEj_mmw;762CKR!2SZ#jXCICz1exlOV`Va&JnwN!L*sVGqrC$xOwimKUK5Q>uhaYJO;gaY;8*HFAtYQg8Xxm6ie7tem_y0 zpxIih8`3HgLG_v%T3OGeUYFvbdsD#!U;flN(P(zm76|sHe_nbu(^7n)W>-!QWe0Zu zf-RGef4}t!s!Rxb;`KpyP>K=OB6lwfo7fPtarS&d%w}6XnI`T6`$Pq`7ncv7&Zd&H zpn(?eYgs^j(HVDs;_47r^yC2GUD!wCS|! zZ@X&v(5ojGazD57Ti8$PBWwbn$oc{oxaO6Oy4Jaq)vW$qp`1L+x%U%`bv^#Rt_^C5 zp%+tEC*Mm1+gI4o(QqCt|P&Ln@ zNGv+<=IvRA6> zB#xC%(rFbcTU{2Rn};oz&bGF6xF>rnlW9otUwz5r^AdrfAoCAMUo@{D6TgkvZ-*+P zN$gEoUjb2$?vHDx4DETp!{k!rW7HrxcybQ3y=4_yH0{XSE? znWnkATTK;J|1pQ~}u*J&-g1hCj+2_BgDbINCtm{#J`P-&=`O1#Q8-3%G z)=1KLo9x9Aq#bEg`H0(&Oe{ja2Xmm#NEoTUzl>0jBhqKDug;>Q`zcub<7TzM6Fm8| z`5Tt5GBq}4s^<6EuY;3x{#M~UJn!x;eywT`L3;xeviX`M8KC!YHUfQhUzEplHb(1eIg3dWjX)$@r4dHm8nlxxVS(i>Ggm z2&~0>C_7HT?`zy;mP$n{XC5k6m)wk`1W93c-g-8cw~Xp9kAiaaQyG}5g2ZOyO>|`@ z-{W8Cf0tmJ6QXO+P*Gk^d)(OwXT`Yvu+|66p)Es2F~#KUdgVG@EI*8pK@`qDnNoZ< zCPR6>!Wk{-OM~*78PzUNLv(S1p+vw38wLs#lCo0m1fAw0ye6^C2N@UG|Kib!3(Ifr~Aw*M#mn0}J*q z6Uu7kYJz?Saz5EHe0sY%ERO%&z{oC&->aVCBcC4agBo8{T%c^zy=zIE^wV!gj@xFT z&WxUL%F^#OXC!N+x%k*KH(I7;-sr0@3MuOMHoYsW@j|(gxL#C~r-thZ=8@ zSdRO)mUYHrCJ)%XvX7szqHuN;ZTE!1A2PzI-!LkM-V5kP$}fkL;ZCXdAe_i4L|2!g zeFJd;uwfD(x2ZApZfT^E?(S}+ zk#2?%3CW>h2f6^E~f+zVABcC(NGx?6uat?l2WM4}2_))hk|{#ncsX z^QFo96bdupZG>2eVVs0G!rR3K+t9RcfOUTIQv-iu5jW>ay{@ z0ZUiU_TjpSR5zcc_qlJPMSD6G5Y`hFsy(@dHFutk#RQ`Ye+2~IZHR4{CjTXvxKpEx zet{(JScLRV43k2tC3a#@;vE6H1r_^>FrDS=>nsFbCtQktal6;Dk~ircTDsXF;YMZ4 z(&;$4Ak1^Mck~G668dpYheGu5vU9&Y%X$KQ6Cz|P^t2zUXir==XS;$)mJ)232-D}E zt^7d3Cn4h~Q8Lu{UI|2ecl%C|df$ptiZ8~=f7Jx9I7qi}uJi@iXZ0Cjt-g@e zU9@o((n~^z+pgxys~gASr@WiQqqK!-Sx{{E2{P1X>Pxlr$1Y!i8ziLK>aFV|%n;T% z+i1`KNWZz$iP^j_HJ6wv(?~Zpf)2c#m2!>m{pVY}`NyvIfMtdVK*ILH7N8NJQEnQD zI6qKAJrI&${-hPeTfJ}5lhqWnAc$(U`FJ3Z5lE&fTAVDVrj&gSd&u70VkCond((}fUc;)!*s3k$cx&al_kdOP> ziZ$A~WuU~78e5A4V&V5eEChYNkiFXmMHSO*o*13&_>uXh9u1IuXP7m}1NSYG%Gl(y zh@}c)xwbXN(AF1n;751<fQPukG9y>tF(1>Nvny%b z|NY!W_z9{gQMM03PGV0A6;O${xsKYe0;ihGVq{oaoP4Fv(BOWn`(6T0TY}wDo7&8U z=faRKU-E@zaB)H5g}2zeCswgR$GbRVs?9p#9@}f5v_j0O7V8&KhyUl?$H@u1K>OeJ z$;W|j;1P&jOo;tc$nh0;az^OPVVhsVhbxG``QhU31dnU7^2wRfww0c z{%krV2fhsb*-CT28cNtPuCGw2zULGH3%xX<&Y>RevfON{nA3kXu+eE1`(X@%o3)ON z{R?o77l}<6W1~t^z}xGTdZ(r|en$`r;y$U-r2E67lRMO@zqx%+kL(YHvJBLX9&TP( z{MLYu#c1z+v;GOZHhZF1#!*R}c+qlMpq#XlzVnI@z}00i{pCPksI@afuzWh^OQ_^N z>lGpgrjeAc#X4WEwdRMYY5pZXb1P}5!5*j`2Gfaqe~0&7)lI?v2`Xa}Eux0s$H)bt zi|MWep$W)uGK0)c1<`m(ap&M4Cp-?*MgY)uviiAIq?4rU)q!4)7@p@HZ z=mEW2i2aNA`!bCo2P2*b3FIc2azQFHi-$Lxgk=s2`muE8 ziVOGfn_KLd(C1zqa<!;~{JjR3JAt&$d6ls3}1o3*0X-;ECd)Ni8pv{*+5se3q8LBEoj3{<0-rONgXL&3p znmwjw#G$%`y z?6Yv&PwrNNS0w~>%6(746+_KDUKLweHv&55@<9nHBy%AG4fB5dem;2H!-&G?Z#2Xy z6P{}0Dc|tzPdd_$YeweuJoAokE_loSr%x99wq@V*FAoamk$~%UZLHcK05_U^{;X$d zn-F7`jIql(OLHl)dI95C{cP%$Ob+|scU3cP2VG` zbum8NUE;`nN9tIxs=1H11w(W%DHoqz9q+pNgX~aNr>d9N_XFi@TQs#Ylal9@d6KRD zri#ysE5f|=`$4hm9O9ugMutjFVqXZ&x+-RGK*M+wUs&q!L7W2V z+!St+z$B|F<)oYh0wvrK1?%=K$`@Lce?2ZG6js3-U~-)OOOq)&trO-elUnqw#aT%y z$478-EdSibf>`$D$)icS*f1Sczk?H;ndq=2(#A$zZcssF>3@uBP35 zb-ltkWX(6E?-_3AF)3t&Y?wJwAG#i0O9kLVi|hvxGbA*vkXoIA*ULp4u0Gl!feUkB zVM*i#k?9SxKYMEoc^H=d%X$k|B3G_lrWD+jILtP0#6$-w`+-2vR!t<#>g}Eh63jtF z#Nfw3^?!hU46iL}H5V0;s-`x? za1~{302^^046+zWyleNhuR^%jI!vxyB1aT_k#CscDzg{hQhugyH3ElhGhbUh_jL#E zEOLTy<8`?&i81eYwsGm)p~~tf%qXHb<(6Y?T*eH?bCtJMqKrK;^>2rm%6JZ(EnDId zL@{=K%}YNst`*JwSzCY|Z&%iP{%aCt~tFefvTB;sX3TGF~Bb_u9$pC@8K6h&?!UkTNx~_$|@r`Ab+D z!SJR|E4N=)CtNs+RzelkYyRyfWxBstrxJrsrX?@u;A$oMo?12Ty#7re56Jf)eN3Me z*lKmo@ysiyRbVYt$RqzD6d93w2J>6YNmR4WlW*@4)BX9x-A*6iV56w<5&E(48?-UWBlO2$kjOj(*Q=TnnR$4$!7E z{MN{`GpTIq;?(?%{f&47WRvRz$cTrja>RM#D^Y-oP~oAj?51OjX2+Ma$V%a z?n$`O22N_*ukV!6L*k^+SX_bKjTua-O zuF$|85CxE0=JC&akV1PzztdJ?rw#K`IfA?cNkt`E=bB(HhB=zRBs1MI<&j|eyMp>> zNzUQR!_BGdlOc2ikA3AO$CEtSI3W|@HU0Np;OM7O?)4iAAQK1VWqK7KQd-T5=&<@Id^9Dbk}i>rTrWF)v4+>BezwjdQgrWVi-QFa1&jgjI}{b;rx6_08Guzr`kn z=o)&__o}{(rDRH^wG^(JB#O}^nG48VkA5^CTZscKZl9hGYyn%F3_$^iT=ofr>CtP> z!EYX(I$|Ry?>K~Y3EoZmyYw1%SgSu|&;aq9wHkT6T`qn4bwms)4W!=;f;GHd(e%Oj z+S(t4v3!q3?yQ2$%u5^L=1-sLDf_7?>@6!gW80=uDwk-zaBzncr=8F{2=Uw8WIBJ3Al9dHDL) zdb%vO7U*71*Rrqw+a!zDUC65M=$zd8EOyYC%Q0qtFtS&1Bt6?z*(Ceb`xG+0HGNj$ zHy753=c=YaO{ZYh@LirJMH2IT#{c!t@B90qb#R!F ze-QY#XLjyYh6=Z?9M14~yLbyF9VP7?Dtw0-AhYpqIe>&pV{OE+JYt5*E#Y5b>S~~r zdPJDf!oRBgco_*jwssl?$`)JxG^}E30;<9=mQy-VSJ3mBum43YiZ`ib{@8KUN`Ot4 z|MJyJ4Wp5>wO%Xmm;=-Wj(@UR_;BXivK>}U2}c&6{nb>|B}Hd_W~#Jfg)JE75ue-H zE=w`&f!SNogyt>!mXZ3v`fj0Y+`ZKQ|2>xhE42SD`-)KV+`xu`V8nfp?H6OwI<1F@ z3z_SIvbW56v$*8>JZgBqXwDY2`a~E<9lwjn07+f8-EX6brI}JUJXRFvC=X&n;f8M< zKF}zYMe*eiDc)thO0L1SO_$ak!JDAH4^|-8a`3IjiV9i&q%ody!?muX*Wh6lqoHfU z=VFBQ7UJ#ky+M{uOPZsrv~hK_E%;qsAtAhvjy{RDZwQh#{qQE9CJ$hG)n@k%>!n@( zWY%Y6|F~F5?qgZ90xC#3!VlBaUUqrzYpduBWPA zwqtK^03Tr>X@f*a7~>_PvXzks1?%s zD!g{OrbS#``$}Gd98Oh~V759dch#q7dD{L$F1!*Z*@7P&ZJB)agIr$T>-8QyTvq?m z!0@GP>!h9KT%|Bt{9(O3rZ}b1i>)Au66wiOIj-kzd5>=GGStbmE9?9TN%{J!h|3QuDv79Z-hwf9KUN>CJA?a^sx1dBR+Ky=Zn9) zhynbRH9$JH1PBpO3=Fzh9ZY#|em19q$;D7a@3K7BH_^wk4vgr57ImJoasEO7|E<0S z|4+rg`WZ@obp!g5^jJ}#i|>kyv+ZP~-*Hq{`!>4?gEdrd(#AX!GO`S#d=2(=FE7KM zK1&vcCM0NT#}K-`7C0Dzl2;qPLCC0vutYdRi1QVYKhyuBYcp3s{xzELTdS|>^{C@O zgRHVr{i=(=nQNLu0G!Na8aDa8TalJvcRG453zF7u-u2JJ>i4GwM+93$kv6kg8o3`{ zU||LsG?wvb(LEc6Q7!xdi1_t^_drjY^-Lz3fUw2ZA@A^Kt6x4mzp6_J*x93^{#d6P z#cjNN!x)i|#k$ypuw|YPLUc2VR7e(ti2Qob}IzxBcI{+i`XjXf}!Bg`OPRv_v!Y zY@|HL)=^`D`qj>u}!n2Q2-xBJ|St=OXwY8z99w_1E2N~t$h2Xyt#=bf_-pj=W*M3Ul$ zaaA_LPY9AhLN4M4dZ0t!#Ks01@b>^M5~EUc6Lc*${n;-+6*Fd|o|3s#@Pq*o%<3PN zijow+b;5;%4~ic#iC5KwKBXQ$!12{`T|RZCDQ!&W8N1b8=CvdlA%i_V!)DZLWn+qA zB)OMzjO-S}1ljEkff2OX0S&1oX64fk0&)u+0501T&M@Rek4bDkX2b&RD(n+!QNi_7 zcoFlnG-S(N9u58j`+XUy{sn!6K5|&&m>Ftl^>dTe<}fjEhdO`INX>e(GTVS=^n~FN z7$;cXZ+g&WQa21(!|EiW#9TJ-reXu)2C5`)d>uvJTd&bW%e?r{lN1`7>32e}g9iR; zxbkaCd=3f8VbuYk(QIuV>rYAx4TBzHT5fI6J?{sHYW?Lf&36v1qtJR{H>zSjoBtooJ`ikQUzxz*BSC11LD>T4%pss?UEKq4)JCWVL zO=}REr)10z-Mo$CFSwe-!(B?)1y-VO$cgs2m&15D)bwI-J zK^e@*bU>6+6YKS`1$tTnw_aYknE>YQp=~OctbL_Y2a3L1@Fyye>Kd<_*!Eq_vOI#M zL*hqOi9$y8qNnrsO*{ML8$=UBm`RA*-h2+zYV0BGvk!CO{5#YCZ)BSqM4+(ywz2AR z0C>0`mi}nkH*QfTj|t5bVfp>>3%U5&OAgF`+EEELgaFZU3rror9K|?U6D}aSf6ZjC zK!QG*nff)sM{0FLf0^ij6iR+oR3Y(7MgBHoHlw>S@<;v;(!{S}{CEob@3g+8b*JEm z_Tj=L)n=l&=G)UW`^ZqNcA90lPx)b+1cLM*Dux7x$3h#x{K9WynbyuGP>)9fPu=?& zuMnYdT&g`?Q5m(3%pK-+U54tBZ|$8b1EY60a{!AK#q;ZM^?bl*(YQ+ybsTb7628-< z^=c=#;=tL9B_6W0TZfK39zlh&bZlKg8Dos#Ud{6g?dApS#E_nK6h!#KHLLA(;>!jf2r`B+*X}uH@K3Pd2LoS;% ztQXzEfjOud`U$Fn*W@?2iZ2fUrXF5Z+mE4$8#_qF*3CFm+}6MC$45U)G(lqd6`C>f z8;0OBTntTN6R)_4aR>VBoD%5AEmZfLXSV6Y=z@o_ zAFwHyULE*i)~{`7AmAh56cNZ;znSF?4AEagj<-43x%?E;T~wI8%L_zuk=)Rx^-$D~ zR|0xwDGj$@2%xVKG?6O8`09sUhc&i5_B1ZP_vSoj^Yl)Uh>%Zg-TpFBXfaLjy`H+^lr|DCwPS+mV4)dk#yd6``h~kZI@4hWEXaN zAP`3WYl((6O-G)(#$lqxV%p>8;{aXTD!Y@2wP){1HnYM6(koa+uB_joAoj|_7^OqN zwwK+^|K9kl(kaT;+bZ(=C1)V#LJsS_6q>%rUiR?Ese@O~G}Iq5O`JSyeMW<1)?- zYW(ZZUx-D8sfnY0?wHyscwXSW^wkLq`RO#xZ&B!s1q=F&$?cdSdr5m2chh*# zJY$hTA^jWj1{f=Ao@C>- zsuC)@!k^gR^zqCL+_`m##J)xHM2Qb97-_p)67#E{jehHgcu71l+kyFNMa$vKXSt^c zq3Dfy-*UkgRZ7$)??jm~ANZ^P9U$A)O2u8yYJero7GP;o?1qmI+Jxm}Gxlz3skORCbhOiqnep12a@ApFN=(KG zi^Pu}dp-pa9hjo>#{dOE|FEQ3(fMe z5JV2>5aH6P0aDgh45L3(GuZ#_d#8UX;E5gO>{yl38n!$HIMK%^Wu`1V($#3|{e)ZX z#G$%x6zMnhlOpO1SBnMY7{cVm?*z`oqpv-l{J(cwpn^X!A;Ij(ADRhGux?R$$GnLi z9Qm-t271loON@S#A5Irlc4jlN!81cuQ_WTw6e2g~RPoJBkM3g`cfOlTzqCL4yhex* zc7f}|?f-W-ln45U&xP zu4V<1C(fjsor%Q^n~>NFpk*{W*K+>FU~zHc`Lk7tYL#Fd6qMP~;Mb1j8A$Rf%(N6O zT;-b_q@^4Yi@Ld$@=U%oxJr3casuzKkGiaz{0PIx>*6+5leP+t?ovV7=8JVVI8*K1 zpRUkBU!jIljl`E54*m!^`MY^$C#c@>#Yl$!fzlY3_ai0!0z3i#m>@eY>28d$KOru{ z`4;^WqIFadf8jU^cH>ra=t!Q2Hpnk8Hu3cPC@S*>W?MQVzY^=*8O76%6(J zRbyi!?{bMM5BuaY4g?9A&4;~ciDn**SulLRXygeJIZ?j&ss>WjS>*|f9Pu!NLOXDh z!cuxR+w87Y(v6-x1pciqp}#EC*>0g_{5;zl22?m?aq!n>`=#QZB2O3l%iUIi35Bv< zH5F6A-UfuqrZyvSShuUKPrDzVLEe9Ra6bB(b3S>5;C{uI!z8-DA`S2NSgOcRrq&tp?iEk0G!$k2 z)|bC69jp83%=W%WKY}1T!}`Vwd^-5aD#Q2>reP?L3059HWi4NS_hb!)Nq(ZVAeSW-uo2Tu-AtKR z8no8I*k9hQZuADq2oJ$rwrqwRDe6m^Mj=Pz@(=}z;x5gpZB^ruj_@x^HPe?_QD9-V zP#fr6sFoW{JsAjWc0OEcEe$*N(==4BZ|S-BWW&HhgLv~FRy-`th0M(M2YwtY8Plk@JP%E-?@R1K~H1qK$MpT(Tecod~rK8SAe6K5xHJ=n| zxMV!p)<1&swU~c;vMwzqMJd4vdtSL_`j^at8%EKd!wpUHkO6+gtg)adQ@wy}>U87m z;y|9X)~~~-iI?t3O<5J%vw1vF6`#L*&Vjk~mWyd}*~~rJ*Rsy6~@R9|AYP~t|M!Ax5QZ6^I!V|w~lw(|RCi#^UZ4<5N#PP>Gph$TVG zT)_o9#R&VavoyTNL_Ip*wE}bjmXSuUK$D<>D<=DK1Svf~zM<%Vy z4)*paBwPxBY)HNA`#18P^Oi*gsqwGI0L@T}lLy9X0gm}jC*b1<6#`6E^A`SIAqYEi z=%-^wl%BJK^ye%g*Txu%3QYK}Pu_AD8hJX)<49JE(vyQ3@I=rkzEQY;aCx)>OvG84P88o(g-+h8K9hb3kP48$G%UQH!Xi*x5i z+PmMW**bEVrFHQ)f=dZ__X{cC{`rA+UKI{=rKT%I@80N+RxL&ESTPW%0rC>FL=|i7 zIzaNCHLj}?`9A8!+JgbsSE`Sw$|>aZrpac>@w7umZ_*@O4AYE zBLBYIpRhG|H@@#8%G@PwvuD+VcNOlK&II%D6vnvANXR$~SyXescB3u8AtnB{Pk)YM z$`G#M*JJYYBGIEI2Knw)`#eTw5Y_AG^(?;n$ z&ii2g1PcoXFON0oS_BcYrSVTsu)O84232!sSTV;QiSqYSq8<`2@yV0wW1{pbbD8H7 z@8&QjF&ra}-tld`1phuP@R%Dfv^&DvDbeJwvs@nK_>sw~|99}IRBC^@K z77c^-fe(#^>O4@&F7Sj;GmlIFC$C(=swC#(#i@}?bv1H6IbGadHSN!NV_T`*-cXMI zNuBQ~&prx(?suU`CvPf`C!#wU!&5?zv1ZvDWG&eV&7^kZ;13n63`VQFn0yehSkO+| z-u$tr&!uM(V*L;tTPNkNy0Y3ID!c%N4}YoIVWyCgW00<(aiButr3x!H*E?L5pw5r9 z)s(Rs%pRgGG1_?n~|JIjiNkk9|Sb;xb1 zus@@FR^BiCIaxj(M(*Tnbo59s07>loQtwwTIAnq zC&^1`$q>HIF_>I+NHY;OI{pCwatgbZNT9SKl?vx(sYyMj_dsP0p$vqh?+5l_2igYA zAs=dU4Pk{cereG`q|uqyj5EoH{Wk1eMTJ%x0)7$2ta*dIs+A)0)qSIJv%dI3=_55) zzZ-EbUyu4A;`07|*dRs@dG*Ze_Qhw9U*+u_fSVgSdV@zl>D4Q(5wJaXuQMdE5vjJZ z9Q$x>%WDj%{#1z{Dz;3AxRS$dFZB7aekIt9`!?o1^TA@T?m!Q$?`zsA$!u z2=<>>cu$+&$T)CYIPdNxgb^z_BJAQiurh5$tz4yt!a+NQw6Iw}%e!1|3>Q0o!Q=X^ zMySx#0H=Wg8k>?v1yU2A7mcyN-du90U>^N`tM zLz@^t5q!#3ImEUK_rJ{>uXd%{i0UEEw`>1*5h-`rYuDl<+N+vwM((#i^wd!CSYi1a znO;sQ{0}6s&Woeezo&su&^B7Wab}Rg&e1tZPo-Gu`s^}ogwB}>%@gz4UNX^{b}(XK zGdJi%pqBX#A5qopJZq^sqPrK7LcS~tZi!}pjtGYOR7kdQAC%X1LCB`2%&P$+5S0@a zrxdHah(^t;{-LY@98sxkqS2!I!QpxY(u%F=li$%2xirc4m7;f;(A#Rit(%j|yr99N zXGRwn{Le>)P`O?NcWqD&vp?i(t{;3|13yK$$+R(9{hg<;?+TN4p|0{ zLkdfPO?;<)+x=Eq-dIo1v6aUyGTnEJL!#aY>=}7w+BhJtMo(BxJonB_d$Av-C9*x6}&Q*oK>%-202%xbC zVgpY*Hiw_vM@%azixt9G!Z?a+#p+>_+(YY8r=?gWi=gOP?mNW5LLW#1-7 z&$@8*k79*JuxD1=d6tsv=iTiySQaFDS|U*p6lGq4t3hZ-O2DUaO`-Mkl-XTzCJw~s z)7dvLC_vNPp|D-LVITw$Y^F;`{PrfUE<69TDe9it!F9nb3~E)H)Tsom)_S#9^G5>6 zOgm0GAgKgT-$jw%j$Q|g)xkayf_iDn!5I-hzj-Qhslktnd7wE;%&Eo;Bg}Mabhp6_ zXjqlP(F>WT_Bx^q08pp5# zsA{=eud4aBY82Xgt!1eDrq1SjWpm+B*XGdY~)I9Sr9>Fd~v~Sm@K$#<)Grdp3jRI0N-#% zOS43Co&T78P-axvQ*9=X5tL~GRs?#7Mo^(I!IxDA0Ps}D(Ww! zGWh&WmsjXIU*z8BEGdk|S(^&=xG*lqFwUroc=F!GA#}Vye|IexM|u@EGB32dp-g_@ zN^c<7t9Y=H|4e1Ni-=n1!TjGHX~KNyxYNlFf-uHnL>(WvS^mPfzNQ&bI5|QmwO&g& z+zKR|u7k12#H6)iHCS2kt?Rz_G(uSvS!hMfNuOH6&J*m^Q9TG94v#Min8Q5OemQy8hm7oUhjNrv`h+JSWN6c?9>3-nu9Jl9uO!N60IzTAOLRwk z+uts8T_0Vf4Zn6%`v?fx!{ahf$B#xoj&a+sDEyeIO4?1re=6@lQRl@ClOF-yX!grN84y-VMZ5JORCGvv8r$#x7^l6)zk`h|c?n&MT^V7Jlw#Nf7Fqcy3krQ9(n^(S87IaK(FZ;nk^|euL-n z9Qt9?0ED0^j0J$~7etR=qR~8}%aXlylsrnda)Nb~8VOG|?~$kT5(dsc_Q+IY(c?KJ z$F_ikl6K|PL@pZ_+CkUjbs6rL3MC1_J09U|>+fQ|m8rQ6Gqek}Z(T&XpO=D#{Z&bDWffuRgIfoP^D%q3E#N_$dc#!wptQ|!JDl-(< zVR^X!^jg~XUa&VlY>x}&u%u0)=AK@e48;Wyan?o`{yIS#YPX`2+tTw9*yoawmp)`H z&P&RTBm!m|hw0+8=#30A zKaz(T>M)K$z8IJ9N~98eU9NMwZ#rIsVa9fjOQI{|C7Y3e2fx#1pUTOVall?t`GBXU z)~4~KtbcLqSY(B}*odOSR38bl735{W!=_jM8+(a#8M~$8yW#me`-=od$nW2X{$B(T zEocZ4KhDDb2lDQ z8TJLXwtb+DZ(`d7y6yi1@zjNe^w-su~%|1W6{ih04dJ@;Tvm$%?`^v%ErG!a)1gf|rgX+)zTMk0r%->O|?;HuA`Yb*e z^p0h{sx65$N68a8bvv&#_W#mQk5r*?a(jeow9}c2-2|})85;QGTS;(m3-YL?i7Bwz z99%Rc)wX>P5a)f|FUlAeG9R2eY5umyBM!wmDEs?#k0TuN@_7I4ta_1j+`!V{XC&90 zeM1*HU)vZBoQe0+zw00g72%?n8ki&MGxtWo-MfN=Q7F|#b@;Jtv zR%lzg-9-a=3UmFmv%O0#F)EfvS=$YuR8k4PuRn(=X0i?vZs}J{YD5%TX_$4|%f-KJ z-`82aWx)V{ZZ`i-v&w>5vaN|XQTH`0>*mDIFY$1RvCC!e0Sppa`#Q;VJz;@jWcPRU zwbpY;d}iTaL5#5H4uxAeVkd#lD8YWBgqx2X|3LscQ*M-X44mwOt$xkoc^7xR=0q9) z>Tr7LVk{gA`!nNp9 zMV1;Z&s=6M@sk;9p+#lfu> zdZZU~N0Wo3e$Z3=%-|8b>;huwh5pSyQlHP4o}(j|Ps&Bs03BW2-vna1v-B3^$7i8c zE+<7fE#<_L;4JzfrY&fIvOvcjh|hYb+=?o7v2^vL3K>H92EiMXmB-$gm&?ZtRvT_V z#z)70ix{j3O2{n;j)(}^p2yv{%Y+)RPdEl2Ni>k`{8T}}{Bpbzj?(4bcN7K!eRY6A zXXn+sXQ=E${R4z1eDS*|pl(PVEat^df}%ur3Kbpj1vSM?$CqVyYeB?!`+WVaOM>gV zW5s?n%cCJE0A&L>T;b{APv0vxf|JZM$&?0Inw?STh2jMrzlQ)-;N<5jg7>76>7_fs z>>!WJ)lr1M`Zpxq3@%PC)@FkKOs*VZY{~A$OdGr5;2*>Uy@N;S@uZrrz7N-Aoy}>X z;21M2@7$==a4-F^kEf|9LtU`2gCSjTDJ5cOTEU9!P3(&d7%yn$72#HQ(fe@tqeqC9 z?)VDdSAzzn(ZOJ(ud1C_%H-n%9g%=ndr~f0=Hk}f$+TPm|BZjI!s}=gpG#L5uV$}> z4VHgu1dqxq2c(D*$cOPjiQOCw#4}0Mgy|;VTr`2s56W*;T03T<+%wBq=6^0J@HWyJ zAJNe}p5y&iP3GVCXF;N(mvqKzue;GJ$MT3qSqIg5tj!0gF`}YOqck3HT z6hKcSy6zsS48Y{bs_m2!!-$Y*Yt3(Y#1sEER!WWhqEr19xhD3;uq^KSd`+I#fq5x7 zn1r)-L&h_|Wc4OOs%LFZB_*_R09a`V22ehu)NO3)5uxM;8V0OV&X*U{KO=-zQ>5OC)Mzcx5tl)r0eGJz(Hwa0gtz&U0O)|MZ-<^Nw_A0A7US_YB5dPG+od0J$)ZX6 zSNF-w5HYJkqQ-3NzadI6mRZzdSvyh!=To4r(6r2;6Nhy>nn zuGuWI$iZYeWY_thEc15)DiMP877chVK7lTe9>0<+LtOAzh1154I2Qe^>$hylo*JhR zfiBW3Xp2NxD1N%TlP|xiyVvZ^T6zzud2GiM(7y)~nePl~?fmG4sX3ki(GSJ^i8U2| zCGnZ&(bI~VM9m$Lwgt^)^Zmyh%{vsF!uD9z<^mPGX1?Xoa>PIeyuF#YyibO8qL&s8s(@=(YDlJy>u@nE7mRML?XBK-D~qmZCvI=bI1a&{EIYG2ZI zE&iV*Ztu52sO&`*Y{DhJN;6qF+OC_Y1pqM0{~e_0r~Mch`ol0>is>e2V#bUs zNRQ9&8i+7-aQ1I&1%uca&8@`Dn?_q#*ngjCEi)BR z_TLI-bcUSAv#4+~tHe-obr%v>!5wXEnX=16aW(!Ro#rsUh+0`ujjzd}Airw?QTHzw zPJAjy?3I8$CBJu^Z&FAF!}F=)Rc*Hf>Q~jZn&TO&eDF=i7={`-ar&g}=C<@&e@td> zHItgud_lcBAc2lCos<&LQqM%QQioii73}o)^Y6`n>RMZo>$>qPv1C?q z2Cle%I-DTe+z#V?S?f>0vQOm+AN}h{?XyOEsv<*{2A9dsRF4 zszul8k3Gg}BegnO+6Zh<@#sQ1PE+KUL-b~DXjVe{AUEW@3N zJr#_G3V?$TUFIJkazj_B&Mab)wSeJ%AC<1Wy&mcp>^psS{3GxdQ4Q%(da-7bh>;I- z!Xt`=_m50=0CU>iegp+Fg}QGYLH36d62uGbna{Lvp24EJh7_VAY{1npfBQscm^P!%Wz$3Nja?J^!r1 zo0=P~@_E(3Ob~Ti-l-`PrU%_QAPt2Nti5L%soJM9U{8A6096sH5LN?68PpG!@@E4N4VU+C*h03bSfV5D5DrWOB znE4`|@~-RP$BW-|rW!>iT9&m5@Y|5xzDwQYnqwFLh;|iynUdmxA=r|djmR!t@HCUe zV9^Q=i|h|fs)&0!-vTAC&QP%jIcKaZL4)x$6P__@gR50VSCaKZ$L0YD8>yJ^3fh?$$`#6Y&B=)};b+F{!@u>BmlfZ%#>VA93}{0;YWp1kd5f$i|0-RnQIjQ)Q~`c;<~>2;kZ7Ft*x zv9d!F?){1pbz$Z+3}VQ5KmwY_5-8YEw&3lG{z9O!VGk2PTf;9c-R;Y737a+jcxVO~ ze86b=wr!>~Y$x0dm3dmC(y!HbDVW5z^&kzJEE#FbqFOZLYf@-v#WZMbQvdwv^$p)| zlE*<&P<$lUR*R;ixXKoCb$T6T0>S0ddcH_a!YnGlSno{kxQ9e^X-rPivJGU-YJWI=luH1<2*G{h|@*& z(5hqkJfS2rmLqkM@=!f>TI3=^8HWGHP{Cp;GKRcER28(SZnlK(kPPhj+zub*eRN^e z*p3H8;yeJEIqo0CBsu3^Ndt;#@tN!+Fj|OLej_bxbVu4ebHD~~dcA|J5mLTY!3OY- za=`RM0^YcKJPHVDgh`wIO@n9l{-^|bk`C(4&8k_(SL_pTBg}oSXFA&wX&Wgm4OKF3}b7S>Sr>#?vuUJgpqM zN@hk4Qb${BKmmJw>}fEa35O@_bl7iav@coe>NnocQzNj)@#f<#V2L0u00~fAQEnql zoMId)$K-NYpZUL-Qn+Gh4)(x-K832#k~+)gz>ApeCKEL8Wfvw0g)1R>kAb|#f;)Kt z*Q1jUDK8EpncE=`)wzHVAJWg^`a>>w8;eh^8}7SN%tuL22_J_h%R?B*ceQ6Y`3!?kjJJ8w0%>cSnbVReZ)ugWhjN2lYbl}>EgRpZk zNRi1?0bYVr&efo5{abEFb~MQ~;**;| zdaJq>u|n@hs%s5fsNzX!9xl6$N!?$k8OCAvq7RM5qV~et&IR$jRo#m}Znbt=Zx6mK zm*ctj!E@fA?4yB-N=8O1v`XN2_IJpbbp;pyw{|sr6uQ@6NltoovnoT^)P62X9%rF# z`LF}r)8hYX>g;+}h?=hc9GQsKX*o)vmu3h_6m)R$PD9#vi#nswUCQ4?|1LW*GSQ{R zVJa3uk~%CUuvt1fa3U0snnnfVmW=5x&V{1bAlq9eatA!_AA!8@58gmIrtJCua3C{y zLB(_{LwQzunNxZG(Iv|eOXU_bm;L%f?|DFdWXJ`i#*C~DG(bnR>P=QkM7PbA@`olYHw)uuM%T) z3YX>yYr$M=;Q{3-*y(4vAXB9gy;i_~+bVqFmL`c{RzYt|*s2(l3Yx8o}PQ?!|yovyR}kyDvj zbDwdlwN;!rlCQ`W)y{^KGA(SqLzB+lzvG>TUPao=UFX4=pMW4l8bpCil_l-IP9ifH z^h1rtCYL8(sQGsq_~z`vYp`SWdzt9Cwu9i`0%3i73ZvI@9>kE1o4 zvN2F2?x&~~FSC*yv7G~EaBFPa0+;?p0yN8C>@i@eIQsqu4i&<5->>g^8fEN>J#8F| z2F};S#~@_3WwMfu?UyFWkU;~22;t`IZ=}$lh9C9Ek-aW;fIT6S`A%y7*n;Z~vU8AU zK>Dl0Q;B_h|8VRgMI8)Ju8O9w4ebOdUWyPQFTSZN^YCg`vc2F7|6_rvL9ehims-fYl^{PiN9(lub zI>j`v74RV8XI}JG<%`xyyzd7C;+vcw22p6Y$Py1Tm@Noj`e?ifNCx{>adkdj6~0coVWzQ=RU``vlv#lxPx z|FPC@JyA!CdIbp?pf1LHM;u!#ch0#lD=70X96e3H8ck@^71X2NZ1%Q!Vp5D2G1> zO!ok?fd+=V6*MLG8FRi}jgZvuxa7*0a7*PO;9rZj{-^&lnn(qP05hD-OSd+QL%&&# zvh&2!NT~2jNAf993&Sk?z?Hk-tOZCIP^@oQ!3se&xv3n>Mlt6Z4Scl53^4pFQ19?} z48IEvmr(A*ZK#PAdw`r8A$_4J;LVGB|Qi6Pq#6(yx z!*%ok#&eVRPNHwhVKa!8ATcuC8HV3SRNL?hAZr3xeyN=Bzlo9x%z zqfGX29FlnlZUZE2S;QU{{QnX~7fqmDd%J*r%G1>wb}e57*G*~^>OonGuF(XHw73&LN^g^9IhpQ16bRskn@l&eMYf|6kyH^BlITPavclq@!9K>;V5qgp{RD z6{jaUvdhT4UejF*aHrI*h1Vf?Z6Nwq#@eT?-eczYV8FADXS1~)VjU-*Tu8ORq4v?Vi7JZq1)bjX6Oo!r1rQOU<^rFwNMtg){2F(Bw?e}~kHiY#M$~@Ljs5wZVy*qoa z8QJQn5GY;e?|&kD0@?>N2(t>Rn^ZpWPd)cx!CQ7j@Q-E&_!3Qy8iEB5EEwHGE&S31 z5}M896rY`+>xy?)Z-rRuI##4$T)%ypDEqHmX?!Aa^Ln)m2)(+fN4e0~P5FZ-2=jKW zy4{R5u!{=}Svna2JmaVTsXE@<9u=>30%6FDh|BPUa!?{>(<5TwSX<)J!>Nf5C#fE*5UfjHW&#p{Kba&>tJu^M zknB-SX7^`lCAd6qv=>lSx0}W<&|y8emv1V^Nc0gab#6V9-t&d#Om3}wpM5H(OV;m} zas&1xK&{-&>!yn~We3~mH)17Xet71f3^f4?uIA1&p8qgNfnphL^p4QYAh0ltl5 zgTvGR_-imFJbV^aH0$seYB5Obpq_m*u%} zy8G=hbv6PR@);N~T_Xz|hVq<;p%G(!al}ZwX32nbvAwZ;8qP5Ffp-YS0V5H0wl%2Z zR?X&_J8TBqs*$HppG`!X8X1FN-Zy7Hjl@2d^Uf|yW9Xzd^dLtck^N||p0K(-AJG$6 zMbpLF`e@%$n__Kj$CJ<}hR!g#p=pNJOozWgYsIVveV_YEqPmom{FWUC#ADS1Q!Y*^ zsv_!fcMd0?9pYQ}Jefx2r6Dne<4~zv7DEcdtu^%>tj95U&Qv-sF6LYeg8KSz#<;eT z&}~k#)O2Ly?N*d^DK&)V0Vq4*YcjA|RWT`y(wkb1F8_vSu|7`9)o4MqFc-BW1+>=d zY)=6IV(&lg$A7;l#WIKZs6vghh?mDEJ(UI6gB5DsHNFbdqP8rcQ|&HUVQ1lH_p8X_ zfSt6Ogw(OO9t#DfZE%hID+5c)XVRlrM^l*BnK}IN}+|JifukFy{Eocr%Nz_B4u|x~t!lR|IN`(!O#iEy|s-ag`6c zNn;&Gh0(~tW;EhC^Y{4k$#iV#vSL7AfqFc4F>UVMQFf5>u_k&YMN1(W(rsS`*kP?^Zu{(=?sy{6vHYryo;6Ua1X z_nB0Xf2{k@58Hk|wNgv*i2Sm&T+PkZP8aHQyiH=dmPdjCpa6kG_&FGY1?xE#&{_BI zNB!t)Dr2`j8t;{uSH=RFT3iubLZ3OfsNvri6zln5Di%#CLWa|X`-SX{2Hb8oURutN zbzh%Q%vrw+Ezhd2eN>_}X)BPSxD-;=3fw73KE(; zSBUiC{3)o-Vzm0+CXiq@7lpu3yskRDPu)@7k0S@h8Iq_UmKW}J@}eZO=n*_(lzh{n z8B`Op);3wyzY;jS!ur{xe@fAhC&T6%Vu>SxW|$ncF8J~L9uKq78xXneZIiKauwh+; zbm8o6k?DzCmh@rzvkyL_WQ-#zn5mE>I_1Qvn1Yg!9%24o^eO0he2? zf8~}5-~(pZR!XfY(q1K@^LtH==dKb(@J7_wJ7kJ7;|PW8>R7cL%RxF`@Zl1%`fNu! zIwuC!v9MstwP@xklv=iowHLong`Y5vtAg_a|4mJgZS)|M#cQD(8)aT<=C#|kgQRAlDgw5HyUe|rS`SMozX zawnPJ-G?7eC=N`2`SzXcHn~t96$5;ABcztc+S!%Da($RZ6dRf_k<@H}ORm`04z{wf zJC>0r4_-k}D7!AXhr+7Z@_gRMEKdk{JN^!A!gz@i&ENl63$XfE7%g#m!W9L`Fh^ZXrMRm(F{ertgRgk)qZ9H!^FIZ!m0lj6a;!EKu@9jJHE$-@e+zT>2 z*fN#@Y<6f2Pilk5m5%Bwy*i%j`F=K8ITt;m*ON~X-S2aFZ;^jbLvhPCA%Yusc+2B~ zrb6#HuIU^pn8W+(6qxr9J#N>5gbn~eUFOE$sx{zw=)qhTAox)Ur^zD;%V}YT1?9 z!eYGj@?-eqB?Jp&BffpazR9F?wtV;D6tu_SKlb8@y@t5`Z!ZJgAm8CAk^4enjzQh; zPoHJ6ONooL2Mn`9R-Q}1Jhxx@yPa(<{gKxT_OfcYl+ZUAlA7e!l+z`&wO&$W5Wto-XY8VklTPEwM>msa3L(ae6EfH6r)gMjWEq_ zgGSGzbNr<;kdtiEB6zctan92#EA zJEDV1z+^txZZVc1E)d+$uj(aLNM8OfwJfq;leHT@hxWdOFH|U# z?e0#qTE_d0KPkudM;;?Z4n2h+=5I<}-JREe7Oai(f3!#P-$SJ8E|`sweGV%w6(0bX zpXmPcc}1VHVDs$gLU=HJ!Q#dyg4f}!T+l`O+7^?+6n)MvclK zRo3yZD&HO(%JIh4Hd~jK>6d#u#ef%vl@n)Sb4}YZ)S!VN1DV1-z_frjRn3~{(~~3L z^>v_dUJAf!|Bf)aOpa000Gd!(_2}^MXR_~dcU|Shs%>7R*QxPb=K;+RB~XE|N$C-i zVLpgG=Cua?-RBU!xo$120r0n<+ij{EU$j6h{J7c86quGwn@dTbi z0!Em7m^KxYRa39tTm*zCiEE65+)k`(~OO5V?0!W-rkLOR#WVGhucoHY)Q>(`^S)zayU z=dv+!6rIr|JssfyU;M`niUQcOfwxNiZvx7&C=s1r;w~UVA`Sh)m%VY@A zC+ss9i>|5;Y{qNj`hK6oW4QG|3KR}&()!a((6|(MjFno69veRdFfSee(I_5XKM8+V z+5(DCVySWKILMU0z0!)xTmmh(=K)zOR|k@^u}O{KzNNPL}`VdoszqFNe8w9$i(``ru9qguvQt#ABx1 z;1{$43+~Y-5+t9$*V_xO+nYjpA1&ku15aOjPG74Qn0vaBnMmU41l<;MNvSqXkgIjG z;+-0Q>tWC?#tsd!3MjGwEMiFAVxnaKwuu1y&t!E`h%Q3VtV#onj5 z!huDr6@&7(=*a0Q+0j6G@VFAHiR87ep1l!*!(?M;jr>IN7j{m;fcm-8ZPfI%O1rz;)BYo2~v1*w%W#$mGV0!A9 zyd7P{g*(K?viU4 z$9(BGlIqwHJ&AmR3%H)SCiyqCcbnheUQ*grbd7)erPROuZW|y7ww*l(jxAF(Hh8+N zl)h)wpqC?ic8*!D@$cP@%ER;G3YEAdOQwR$qveGm)xQmU_W^|r#m|58>8rUmF?}zT zsPN*WNW_w1&Gg<&?t?(+36Er=57y4U=Mb0c{=Pn5`5-oG?eniXG7#?t4~VYrb}#aM zP>rzYTcJCKXVleHQ95vceuNk9)xWOFYO&!>u}aBzs2QtKAk+~u=u6WJTW>OOsYu@Y z8@TX)J)ai=RJ7z>U}-+rvH^GeZOjmIJPwA?av9jfEO!#kGD>k#!h<1XB(qA3)U$~c z;_<7Cgv&npkOl!sE8G_H!p|9)0L)CsA|OssO(Ml21F5NB*9MHK+vzfGE$$cIr|f_s z@P>tIZRPz6#@yftC<7+?MIT~GTvei-rcYbhX3i%ykE=HKL+dxqTX|66eAo>%@t0U} zU~90qy}?H@Q45UR=;iaBiC!c)Ndv@CB;~pq#kt}NO;91I6lCRaFjobj%guT@4fc}G zKYhDdHe=P*F;2gVs~`#`);KK<8zgg0rN-14s)6J44A-#O6X@y5R{I1CW9nlW1wsIn zt}3h;^0K^-%(X7~>a`vfZ-p z93aw%&()wD=ZU&ad3Pncnm!T`Jd9V)Wt^KGe?70C(7%y+m!LIk<gV7f_s7g9i6ctWXEzFUUy z8$V!Tw2*gLs)}T5{cwm&=F90v;fVna++pFxNb^|v5Q$= zh)6-kP{VD4pOKJ^n-0h+&_P9?M^ts7!qNN`i|vq9LXeN`NE>OO8VT$Dj#2z+F|TVp zIPalnY5Xk;vbgQYO;wH46w=8w!4-UdBP`ECN~P~QkLtDyr!*|!5?{L7+kLb2lMk5Z zj#+It)+6*ZPun(|C@gU6X;8NpoNQ0h>Qw)k2R%vxW17$n3JTccCz$!)@c?=>+^O5m zyvp}V0lN2)7iJ(k1oM4k1-;mWUg zrmibep)AHy3t9pHUng zf+2xZGWNI{dUub2j#cT8PO@OrG>GE)#L=x#F(o7&mP*Sm72AB-FszpRSnCFJWfMk3 zvo?s&pm7T1;eQ~m2}91~!xFAUp4qJLfB&L-*5*hobPIbd9MhUGUWZ>;0)q2gRByoZ zQm#nl@a^Kb8A&se??2Qh-A4@)>V7-lXId+Q8CcDe?k9@i*#&PqIOS@Z$e|e6mUoh; zj*=lg>WBBl)G(CaKKR^nC51UJ*DN#IO4!#Pjb7P27uPV@yOthwP%IrVI2Vp!R1f#_ zS`%!7;K5H8`k3tWg;C+#go8MF;0kb??>wKgneHj7A`$gXy=lFUi7qlpw&^g+G-c%^ z5+&1~)EF`|E1fdKDOIK^A$pW2rg2-+W-dtXj(a$_f^$cJ(+8=WR4r_OgaPDu7L&5k zj%RO+9}cdT;DnPBlRqJ^ut`_&g0#ML0Ahf{S?z28%!R-4$>*p9tsaBA^iA_#KLw^j zgROht-J%e15W*dOuq8c?!Om=%UjuA z*#A4CZsz+sf=kT+=Zlem43n4C=Kj3CF*D(ApS`eY%Q!9->xdTyZJ#9#N?)!64CQ@J!4sOenfrh`i_ zYLEDDoGxZaU=g^J&%#Ql_zVs1cV`^{$h}%bf-{%B!Oz|gg!`2cyzzm*g$al#QITo~ zl)LySHLi^`a02ipsHnIRl#>eG+C%(-k$`VhgZzCX%-Z>HU}3_hvQp|$PMZ3t^S`E& z?%(bb6Ocf?cRs?x`@)fKUh0om3|;?*J>~{)&gnj;A9PH`YiIfaRh4&qoCgwR*8LoQOahmeDt=`J@NZP`wm}oY{0; z9)@(0dLhk=9Uih-bdoUbns9t|&5Ak9ZU)mY4!I`}HzPMiT>baVY!(VQe$2A^(^wgZ#!6SgkQe|yM(4NmNupoO_D&M=fJ5==zAjKUPUlC^+q4daRb zvk0RzL^9~$i&-7Rxy5{orZFz{yC@bW_(n?%RyRj7YcRp2##f8#P{FPMI7fu8QKt!} zk2MTDB;WIp>xv^^KADb|B@!d_YUQRe{b|W{;I%VD+h^FN{w;9%yR1P6>q1>k(3COpbh;oH46dcrFg%LwkKpUJ+#J z-Rh;l%3cx4F8&U4Cc#vuG#3bz1vRt&eu`eS5yzy6MGXpHU8BV}hFHIG7r6G)6aCUW$z-ADCzFgL8j4Ty(bE6enj(GYox!RKIS^6{B?&Wnbm6q0r_Icq){*NzgaHNg(=2J^3yOdA z+|B|9bo8cs@&9*Pv+<;ZZfkRQ@Obw^q;&ZrhgDj16oHMD2bJHonmx(1vSE=TesL+|8zw{9No+uo;s_$J<*7NN=H zMw6zmic}x^oupD(nz8WRvVelQhq3)O%rlBU^?p~5K6R(u%I1 zI|7cQt;zZlZ&?E|nbVzc(LiDH-WWZ#YzXeqE+*o56*b5&-0Y;BLW3n4_9=xA$I^QZ0G!qEw#rhAM331H!+YMoSuUNP+j z{W@jEe<3fGN1Sw~Qw8`Y!hLXkJ9PTvx~9L_V!y@`3pJky)(72O0}iKP_qCzBy&$7p zb+7D)-iptdaGY9C?%Kzh=F;EBr_5HgG7TH_IGcRt{cMb4vn0LXm*J*15=-#+pm0{{ z@SV>3U~maCR92SREM^$&ATRca^!0QEMkvgBn%LniyUq*_Ir8Pyee+R&3ae+sHRX3B z=cnRPZu)LH*gSwsv+E@91i6-Xj1rR?;Fg*UC${2abq&?QWrxbTYUnNX0p-r zR#S;Wy<$O_o>x)Sa#Ev3&O0s-JH=gs4WXIVX6UxZh~m0?{OB-OxKCVM47vSl;)}S@1~>7&0B^Wff@z3tsv$GSn@tRHPhj3ShsBA*({DchN_Vx_Wv^ z1XK&Bnb{ONR^6!V$5{e;c**Vog!8lE^JPFmtAOMs3i@BE0I@t~s4D$p#kYJxZ@*t< z#pqR#${?NyHB-X5%wbm{i#3-2kSn^)N8wpjyp-UU= zr2mabX!Jzg)DEa|9Yey)uKcZHchr9m8=yYr{WSR{1HeL!ggQF7=_zKJ!s~HI*IH0- zuq(*n*_t^GimuQ^du6!BMe(^ag8P-^@E}eGB}{R0Rm`LVJrP{JT5I z@cff$l8lIFrEj-)1t+W<9hrIm2^$JpeOb?e&+sp)SM=!y1S3FV&By|Bi`n^=;tH^O9D!8Z(azZ1>j0qT;?Sil$S9)A2J zntgL)>cyY+xlD`gv)@=s7=Azw$F>rl?w z1f}4(ylUQ}G&WFRd){;yZWK(iang0S=w;EtA3*DS0<=8D|15I&I(tcZWNFR0e~oul>)rJgY)I}e03kJCV`=_b zRzu7}!V>y5XCu zw1JEi$;qMA@jMguuv|>Xb!~sBSo1B-WV<<^Om%}flX_|5ixvZpA_ z%a3q&hIgmdHzjboNy3kUMKXK*N5{Y}cf6qsUb&yrrSG!_-A?pllut}K?W$$JPQgCn zS1{&G_x6v3j!APp8L`u@E;(XT$EQlpD)W(V!ogOrWY$O3chBDnr=F>sJOjnb(W`)% zwIU6r1as@eTn%%-!1;R=+<_qRcIZn-_6Qk`1veufhK+8f^@p|re zH-f~T0b0YCr4{!Pn4!pgZVI1&X^k_o;Q(==Mnbtp~!liMC?k( zndO|4O8w`3A|});q)GukAQ26sh2Jk7dVT+FF`{+CpNdn;Pwp!h5&Wkbg;VQ-e!gcg zh;(V6kH=Rau5NaOSv{lKdb-*GdYlU|#A8K#)aI+tSi$W!urHKJ#p_u#f3t<%7pi;K z$ITk%1BeiDGoX}vA)eoL-3OSA1qGJ!(Z7&SMDb8uM{-&#!6i|BIG~IroqDA>M;_aoUPG?hp!27^z~Sj;Q$k2MAdA)^&Z&0>s9*+iL#z z7AoC>EX1)eAN170WQE1ou=RuDHVA5FiFriW7KB`u&$&B2r1Yd&)W;vFmOXiCaS2(9 z#$724KvL){U`3r3*v-EQZ^Li)-@I9d=uD@uCJ6AAJT1W%qu77R;^>nfhLc~ z6e1*4R%ushQF@PJsWjwfbLYTXBzPG=>jL01@Bajo7Q*pAw1qSQ{H3Lk(QubX=vERM zW_G{XX@dpgxF9d)vN7)3BK1jl_n=IoJ5v4Ss?jp6(7_+P(Q@x|UVk!j@n z`Skd|matl=*s6Thk+KM}C5Ge~akMsERr3Z@Ms9zQV2Y(7222xk7An~S?y(H1LoK_O zaLQOT-eVE3kuNpi{tpQ4@I!O;yRgPamcEQ2GYsLTP#xss@@SrLuqLttLyF@ZRPE)6 z>BGutjZmnb(Q~?!^Jnt7*jbKX>`8-2>N#3Qr+UvoN5(Y;Jrv)BJ@tlK1ckrgsp897 zAK_1u?FHXN4|}ngNrz)J6|x0#2-DVGJJyMXv5pHvCyw+RhC)74$%Ra2eaWHv5fNaq z7bpBCdJuVcTzz+y#N0}bs}GHF+qAbweGEb;P)XeQAYT(cYCu<}A4~Q)IX@djDx@S% z9uK6d7+WQX{EiPMhfMk`()yKLKVPQkrjp`vITweB4a~h2xhYpG&^avl;V3!^MmLf1 zqxb?oOL4F{Ap2@!tGh&J>{$kaZ17_LTz8=J0i?4u|4CRR-oK0EeSd%1Q&)q?D}jLU zRl6vS#7M7C+gQDm|e+>&vKYn6<|c%z3$V1qR&-wadGP67Nj)6ilj8d5meG(E8QwM_hlEE z6_Hr)bMNT038C;*z{IcIp5&9G*g%O3$H|zX^q|FF01M$KYY2Dl#FF+C=-jKPjvV$)egy&euRHM$53nfG5?}Lj6nob}~D33UO<=oej={ zS9zJ82}Tx-_)fa@i`ajrcG`xfXvmo(=puSi-iD}eF{!$7Kiwt8`W#rQ8Ho28o-p&j z*rOeH)dG32009GtNu-4%FPPn>4ccgJhE_2s&fAHrCPFBywpcpZGeUvTSEfXg{Xd2@ z2l9-a&GdiTqHl?E6MU6CvtjAoxVh)Q*^z3*$yS*1jnW^J8S(rib9OGQD1=c%w6YE= zS)CbT=@M=F^jcDjzHA4|LB|gHWpXrPLthND3(@#)xx(?jdNb<*6+5ls$98@K87jrjGA=(NXG5Ho7|niC(HZ6z;tAHj>vTz;qh(~<|Y|&9MiqY zu|0)8<%(~#l+m~16-KyCfJ>$Gog3JVP|}#7v%-x4+*JAvJ&N)fpJ%2@`-9AL$=r*k zMRX5%sKJDiz2|!AfksCR8sK)o+>KXO{xM$bDr49W2JpF20cTe!j=uE492=e z*nZw&T%1UcI8Kssi{Y3D$4jG;03JjN2Aga`$<}gP&_Kh(9()#|dA+G^B&KBoh~adg z1M3=|uOuh_I)v%jq-aW?Eh>+}y!ksfX-%?2KTNiwooc?*bruIYH!td;w0lI)A3k|t zDsX7*bW8J20HvYd@o{+3B?Dy=r+*q$!@$#eWAB(KA0)&SZ?>y9sE@jsUN3pRTJ*Qf z;+0OM^qxqC>IW>Cse!7)DN%mU-R9GLZ57G9L1h6}?6)$(!c-+=Phb{fP!c&gmwahy zMwjSww&RSxktuN3W5N)CepN;xT}iay7d-^j}xBS+VFnTf_CG zdDB$fW()#tH4$WgYOAMTlGyn^vHA&nruFGu*sE{q=#~gr#ORyu0T59IL_yKw?+Pzn zbuJT=ca}5DOxVb_HD7oZuUiQxmnbtr>>cx*f0kW|UP>@DZ?2ji#`*d&GD{K*L9t8m zA(M3`YOubIldg;mu3dt4>VqE$l93VaJ%zlb`VzEjFo;sSPWtGP0mnper zD;E;jhIZ=Au=%{lJ?!n?P^Yvwo+&(fikh<+@OZVKtVAbA+C6?l-<~qWTo8EOwik5) zB-P{A45~X)8Nf4`yOce2H0*Q5k*Ct;;Ku@y=FRLHGTj~}SGoyXW*>rA8u@DUUI{G3 zSpX<0abIFb?Uo!#TzhF@Tu5%><6nsTdltwmnTC>;4(g%fil{#;`mamnA8S+yX(ffi z`Je)cH#+n9dB}{~CEp%1sERt~tK~n&06RsSOtE3!B{hL1f*FCGnCGIbtHa-g2g&9c zEP)14Zp+r?`wK>4BH-Wx;_ZKaN6W%(uGOxcX}>@NksP9y6iiUjc?>_7V@P$IP9ZiH zs-$x|tpH8t){ZVf!1FUAb}DR`<3GfJUgXP))51#SKV?cTH@BIE!khV%jTXWMTz|-> zx@l{GWm0$gmMjka(q+x^0OUD5JHr%a#9$-WfSA+H#pz^z!-%@#H>Zvq?1+C)3D}`) z9}D3m2$55j+(NKcf5#1Ge@LZJ-yPhoBZqQcO6e9!zv07zXdxD}{kG}V&vU<>Wo&`1 zh{Y0A{bNt}Dp>WCkpPrRut=gBK1RZ*E0}Q_n(zqO&k{;%x$;g-#;!0YC{=lkdpBDi z%VrcdV{67ExPhkI&hatO@cfZuyVoz{ohWKX^ys>9Fm zI?Dq5J2?(Yltbqr`kQS%1t}&Ns5(+O;t$99aurtVgaHsa;-}7?aX7(5#C;FqJ!2xF z*t>&z4xuWZ7srTW+W=rPXca(Se+gTD*%R@q>_@Tw??%2{6XeO#y0=8b5J$i%!Y`EH zQc~%}y0F{|f~ml%+G7qdx_pnme*EP7m2O<}hDO z=I7%+xOl?m!GS>^Fo2Sx$0z$|rDm#u6_-cN$dp-2KNDU3aZ!_4+FvL&Uq)VaN=&o< z)aRy=*>X z`)hB8yXP!FF)H!t4ccv1W`!1cJBB)=b70d#dguU#h=B!NT<%Vr|5wbp8*DTUO%w}T zMCvS|$?mrwLYf56Mx3*Q$>F?@Hv#Pw&YyqJtwM9U1_GsrOUAPI3_moAa=NK282v1| zajpgwtemfGlpA6gI?t^|Zw!6#$OB|_MGDn9PtvS~YT`~$RUq2UWzwJhu<8o?mnZF zr)&GjK-f^Kbs5dsFs#f=zt`GRF`edy@IoZ2QuIE@h7mDS5tIq)s-IRHZNnN-K0#3idKkXm7 z>YfwE>-vpo$G>}guNlf&bwa7b_J9E*nw*u>o>~~IORGtR+)A|Etqi7cOyk%^%Qk%L z3w~)6>%y`C`8j#Y2TfDLymo5m|6F#L_=8FkWK5&airRaF67 zeV<8dyA&I$_}(i9Fx-MpI|kNZu{FO`C<(B@XdE2xS7FF`MgE3PN6H|6=B~YW-P8z58@u#Qa+vutSJNfe zaTusPT5@qYg%ea=LCmk(kRdA2LfG7EM?R@(iVs$bN|!5dR7yHJN|aq=qy4-hPW{c9?ccjLE6L<^Z5JA&`MFUVjaL^Ff+8^(>n z*mLtYTy=#Bsl{X}`JO2pW5|OKXlG&O>|Q-G&4{T&wc(7LP|Zx|biM;o;cO#CbEqOD z|D~x>;)3pPm+tXB6AOgv7Bk4Y&76AA5u^_T%d}YR)VY%+NOV~1K546}iX=ilK_v8Z z^B%rN00aAyJ|iTk%3Jn?{ODNX<=K0`G1`wy)|lKqa>W5j>1(ndkDNB$;rYQY3Gy$L zisQDAC2Q<$4AL8j{7k7VxliLD3Z7@)cJ2CxO`wP0Flla}0AhxRWNd)u!1vGbFj@O8 zIm;h^e*86Qmt&iyA59*|TC?Id9Qr^%dGp}cH^nRtS>k!(ISKH{&3y7P@sTayUj0p% zML6kJIjpV41@Ku01BZp_e{NK_2{07m>3Pw*Wk*C9IT^jBcEg~^m3goBByA)mGCCLY zj`$}$6JHafKM=xBA1p&(N20D?HaI}nkO%EgV z>@M>9upMY&XEi8)mb`D>XNULm41Z|$Yc(~URzB5X$@%qI;O2gHYUu5s{u^(SfQh~N zb1Msg4y95VUCf4BZ-YFVz+B&C5|-}%NxB{jZS$=Tm(-0TLdpHu2n0lh& zM_NA5ZSDJ2cS^kc2Kq&FK=TVb4W%6wdg39xu7o^Q0QwdR;&~n_KuQyjQ(18w9c_mF zz3u(_U=M3fAy%sR-oib7B=vnf-DmOL)6x7-*N^?{S1y+s<6DXSsm)(kL2=&ZF& zHm*s@_>3*EFetjcaqqewTQ=pAY+PY?(eG1K%PC!(>q@HnQOF%0aCx^b0jhapl*7Eu zv!(xgG{7yErs}9FZtiUSEX)}^#6o+Yvd+%hO!KMKcMQ@j;k%bxN8a4vZ%e<0Lmyix z7kfkTen?JMJ(qO_wn14M7Jvtp>hsfHUMGoUT}dDxOZ!K+S?6 z0&&)Oi9Evws>6_R? zaZ4K+&bBFaeXB%h7UWdJW=GfF?2N^L5*sc9)s#FmInOytln|Yo)nUFoQywNrOmsZ_ z6bsj+7I$(KDw8R8+#EFKt=sX{y_dbo#z+8B#6jOuq`@@Qy)FTGR{R10M5oJ3!Rzbm zV`;$S+0;Fk;A5iWWs2D8;~Eu5C8RVeZEK`N`}%d~iK7F?9<01(xavyrJkjHWGE%Nq zR_UjbM6Qz&0)>;>m`U&^*K9ppg_&g0Bev*r6=NZ7$$t+c_K>yKcupwR%T`C}LDum_JOr^b!lGUR7IQum+JRo~men18bh z)WfB?DHiR;ry3WXqgY(OV;Sq4OV^|SEk~cAVZxmGy^~;%Gu3Ewdaqfxx=QhRBTFyX z9FD1|I+7J!eA*_EA5iS}qG{{lM9}0K&{1G9x;Io_$1Fl&4_BE*V3Vv^o)+*uT_QW6 z-z8b&`SzDm(lFN^M{een%U;7}{nDijcrHC&*{}OTmynk@%npt{6diRZ3C=v^H1@t5 zzdYEm?Dx*hF!=t*=?U})u-z6k(gH!MN51LdQKQ|&UeVKaGTn7g-<`hOzRkx-rMN}F z0_4LL3D7}~eJJ~7Srp7=gX5*=1g)9-ZO6l%>)#`NVT+?TeD}+)UfvHgRmP`Bl3@3w7?rbff__BDuh8lk)`oZ76W?1lS9lgQ*(t+X{RR5cRHwazDLMIT#FKnUlp zX}*;$0IX08HCUvowEbsXUwln%*52^{qC+;Vm@c2KzG7T;L70oa^73<#d2CpnED?5m z=qnT7E3d1<4uQZmU+@3cO0iySHt!|8jN<$y4T8MFPpaLJgbFxc8>Q4P@25SxbIKT! zllQprzvUgJo2c~oUGpe5b}j=0zqy2%`r~mY&mu{qmm0La3>5v7<|y>_ggv!PLNX#A z{)hkWtd|B-n$1%#ne6ujvG*ms=ba9D{(p$(Rv~0d_cp*val^O(u=z^_{yFN_ESYxx z@9X~f(z2}EQnVA2N4+?icDp{#S?v>(RmMQ)iw=^yz-3Xw_&vxJ21-F$Ys*OVRU+mZ zt>q6}2IKpU&*d=f0uTbJ9iuHYofAb^il&;mO7+T`7MT=HkN__l6E{KZL4{4VL7wo* zO9KAkedaUd)@AEWas|~cIGq1tg=()T^hGr5wolQetkuqrHFhWA<9Px0#2s9~yq z^0DP@lvFVbbzOBEWs|elN#rK2ErNRU>uJHXx&s-OX0(eDi&4`Qn()}91#um1bNJk+ z%+S%lW;OpLzb^T39)H83Ub;=**8CK4hBeO^c&nI^OXL&2N|VswaxiRtHH?P2v^ai5 z3!OoQU86h<+&o2PE(=^=8j1S$N$%B(c(di88bD-fzv}q)h9#CQAK6`I`#Y9f_qsu2 zeioI%=5G*$YH~IH>hOx_vuwlE$_^(~7r>(CyWF|`-}&#Y(I$0c)ODAhLYYW82Yv~9 zF4esWyA8!vLVaH0kPqu%QQ*~Txm&g?-AhcypwQTn`+={=$-fu2X2ey1rPRAhbXl3N z3wkS7G53gA*mBVU=~6cx-9bw4)fso&s2eWH7>RL1C*w--gjS8vjM@B4e<~|L$R<1E z4byMPrc8YBTdU8ibVe<7XUJpoHGN+xHCGeEqB-w2uzeEWmvGVq zz5vLnTJ1991V8kX%F7IB&!Zc2*^Y-D0|EfZ9&9NMrEm`GG34kK7$GsRb^m27EZz{z z(d^36Nq}ExSZ=bX7(=VrQ<3P<+YdI{?V;6PL4eS3OK#oXL^KwQwoXctIdd}LDZK^E&WCff)QJ1#SBLeS z_0<13tYpB3p$Ti6i6zo}k13An#iQ(ODVQ9g*?zlU2g4sTZBD$%{5!zTB#KQhzmm0m zLpk%K&Xf-U36v$*23sUY_Zubj`jZ{BdZs#+DOrYvS)t-@c=Ya%+GX_Y)L%D99Jra= zCfnoLCqMoe(I3SFL=Oynu)Lrei!R!`S_BOmvht{(#=5DIfz^kudFJehBnau47oH?Z zqU+sQ&1dI>4*=#(*Jqk{dR}euP(Q1|UDYmQIO9!wg!cKyoaS=G+ylRo-adw-;grtx zkAP7iq-kZR5m1-Hdgk-``~0sM-dxO(`gjDQC1g3WM?sbzPDguA>sV42w^2`m6&|Pr zn)vcW1`A(q0mMJLdAMfzxGJ3S*-aR^S~327f}Y?6AyEfA%1T7RuEUvbPSeQVf)To+ zfk`wFVVgwsH|q``$4r|Qp|8}cG7kiZUcsJC#7D%{?`hYk^t5|r-l9y+AE5$R6p z?hfgY&Y>IWRyu}mq-BVqrRz7I^F8nT2XI}$v)A6wy4SrvH{rZk@&`N#zDg42zNtE7 z*`FFs+_?sf+H@J}_w)TA9pSNo>zM|e(_6}4KAHpxcM;V{c8){SYIDX{LxS$C0ciEb z1(bDewwpNU*!1Kz_>r23^M}mb*EEfA*GW05E4WW&Sd;U^weu0wOjQVDT{(?mAEMtF zE`~gh1*jiIyRwN0z6z+q^0p$o0ojyurY%|U*#Q>MNTrLPiGap^++%xAlkf-C=m&VM zvM4ENm4R6yz%hw6L<_M;H*$5i=&1jEA5;c+OTn{hrQocEUgiF5#{%#R5CgpmhNL~j z!#c=;D!?B58ztCgJ8XIjIgI~e_`l~K9w-K8_KnJXhFfOWgCkz8E?;v^`rd8UheaWM zv!7Xp^&neZTia403rL-T2 z@T+38b`Asl`r@f00Al&+MO+1YyuSm+LFS`j4PFsJblX7c`nkPxa zqzxUE(kmX%Fi=HL1icz_nOjMcN2dXJca2y6SpjRZ4PfWdU!V;83atJ27L>QSIFzHH zZNl#Xy2XCim7mj$Eu(IL1Ho?mP!$AndA&g;VsU_B@n*S6bVv*DOl_{D)n;*ta! zbi$Pi@Gy3I-3N=d56!h+YnbXPG|elG*Wz^YQgo1^ZUUdfv&?vJy(%1jm8vyOnw3hR z9t3?o@{b}=6RUtL)J*!y!&?pNU%a1~9R;cPDL70_kHP>SBm>^qTc0MA$ZnWOu}G$2 zIGvK4nP3r=3v4yjbRzVp9`pf5`a6i+$)*{!0Imie^(_(-54J>kRDx2swUGJ6mFv zkr<^7Jmy($Zh$i5g<^M7LoviX2Cga#mf-lSF>U(hm^x_sxk%C0D0?(t_$qnw?D$;# zBW(6JYgh9yuUBcf;d4m5cv@`MI@?oSL`;1BnxpBOYyBCRT#qX!))R-cLELanAb6vxmiVVPSMowKduQ%dATSGpt z&LIu0O{ntY`lN*E5-kot?JSLGfvDCeJM_@ixO_RAQm##;$oiAnxJ8_IDkoCLa$5)r z{0wt(qM8kRpTmqkShq{EMp%gK0bHQA7K zGuFFZ)7PI}CJdgz1px~?Hj;mK^cR|M9z@)t-eMVcR%PctP!SP9JkfJ^UwnXR?f;f8yHoQyWso9=> z|4eV1g?mnY(bURKK!{=(-nC~L%r0t7#ATJzqBe1B7HF)xKNBk88*i_@vd~@c{@3HR z&7(I-62&9k;9;!Ltf}ik_n8_7WNX_w6$5ch5J)0$qAyM?Nu(rb0gYM0j3;5KF28MU zQ2WJ5{1yA{(&#GdmL20&aP~eucke{&ykn1aLO-DQy4pm5{rhjp-|d5$y#wi&>xcyS zVnwG2)G2x*8vFLA5X=I)!~Q~KtNoe9*wZTbPLm1qd%D=eGSQbpo_})eM>cv=B*AmLJ|!sk zLhQkkVh?G@4>Uhntf! zb9qA2`k0+&8>QG`JnP$G^TZ;WzRd$V0 z?`l6WnP2ueR5M>!<@aS`_V!cCrz6k46Zd+xx?=PV-{_i~KX6ZHHg^)=I+8ER4=&8~ z8CwO-;l2*o9bc&42RLtCs1L}LxzQ%be&-RW)86dBV)X>53>rhbZlBq(mz^(Rafk+n zpJ611aR-L-Iheg6X;fV-Sr?)BRL+JGmKPKU!Yv-9FU8|vBa(aaWOtyO>YLM24X`)3 zsB4Z7e4HCbNyJ$R3m88wA8~n-HnlNMJICV!GCzVL4P)% zy+;Vn)j&gEJ1cz6)(N<>{A@vbfLPi;JaK$1XxC3CKEJ98iKxI?#XHF@MJDG6J@?xD zY>N-}O4XuO%Y2{W_#>VAdEw(JC?+=sSmQ@K#nsrv@Z z?;@>ER^LPt48ySa!H;%EB8!ikNI4-$=JOxaOmEXF(=r9Wn9ixnC(jpHicq5?3oq4u zchZ$rDPg92l)t@)z4ZL<>mCUuE^wPH7`zh8U6dLhp37U2ddV_aJ8@k1HceCgdW|mH zWF;o`2Of)V9h3q*zj4;8D95$3(w~#%{W+-m5rLhR6FcwEZ@#W0|EutV^m znf-Q3tDc^mTHHJXt1qPgL|mm8k$P9yHamH`z;H%vyKg;EYJib+k1Z7IYEECQR$_A5iZZ zU++MQw66AAE^@D*Yva{2nh9Sh7(5QYci`>yVPpl$u<-^eqyy3p`}yfsE65FrS^;mX zTl%pF##nOZIqbIjQy%StElSeO`_T?XtP)-p8R!s{EkCp^ZoF& ziJq&~fe@*#t-%x-75qW^_d&t%?NKXK4@X9JiTJ~-7D78l1zX?J9jl<+Ea~!Y9YVe%=WSQ1 zhleR0rp747XdmHK46*FQ;}DA$XiI`$O~@1E4^kOGL1FNsK!op)PIU!g(q&{PznVRm zo?Sg9HC~iWlFPISQ|Fg8ucp)hpQx!49v4i<@%uQm!G>>>AH%f$WzuPY`&d{~QwVN9 zda&v>rl<9#i;MB~+v_SEX#1q}zSn$Fmv%8|^87uAlmIAbX}iWsIkpM6mkh0NRSutq zQ$Deq-^zOQBP<^3o1luO7{??!=lCkqmowZA#G>}_a$m}abZsavK>)xB!o;3J=VXe_ z@vCtSW0D|5b5ugKWy-Mzq8*s@=YzAu(0JS=mDX2D{5VUkzsIqwWQGT4R4- z{2}sD((-(toWbYPiA&q43LrRE@F)AW)29b^7j7Zw#j5`=*Mha~ACvNq{E#Gsa?D5g zEgeQPEah>KnoD2^EF>*Nn*5!*&P8PzM)z_vt*{KKWZ)D2nTA}zbp)MFeJf>Xf)N~d zop#8;UtU!tbgxsYtYW1hX|cBPC5-27tj?-GVpDZQYaYg>J&Yu0#=YB2XkCm8$G=F5 zsH$DZia?r`PkzgmCB6k8W6G!?!C9O8jyon+9L^+3_d|}Xs5yLRHOaI|^gM6JlL=c; z{FDD{+s+FNDWq$0Aq3S?K%DJA=&)42AHBwk3xgc_0)YNSr?m|}7(mO203<1~nBbHh z;$yfVqM1s?sy5~QQr?o0x@bIAIjWLO3UQu-cW19{wLJ!K!MMwNw`#q`1`noUtZyH~ zgK7m$7vmi>H~E;7URc=UR@$jX({If{n60Sy5tgI*UZ-BqUj2KLrjF!9JAO|sPyJU?kB(-0D5v_GXOdOt%dE%rUK6KuZNZ*>0a-;Zm~}JIB}N&!AT}Yog5A!-JeJ! zZuA};$CPDvvdyEJDTFSRKP)8Zy?F8!E{g9r?!PT?)pZNe&-qcwg%cpkyhGGzPfh3R zE4;b5PV+^u+~2o*o9{Vs_lX7f&^S={wIHj1aSA1O>u8NWJMI3>^RkmFfrCo^TBG9n zM#M&0xHIch!^CFk->5V9lZKy>psj%XNh%rK+Ebdt6WBrY`mA47K0fEo!Q5`CTG#5~ z6*sk;duerVr(R61XZ2PRj81`FShC`Swe$rR%v=Z-oKkjjdtvy%>seQo zKnCfl9lRQ9Kl6EJ`hX>a_59xux6daqj@Tqi_t$3beTnH~e3cV2*R*>(W)lmtS1|UaqT_RxdRO8YnIvUp3goc$w2)BYVDiv3A)m zT*JhxMW1;Vm|N}a0ukE$9WwpeQoat5>h=V1I%o&T1iBau&8vyYXy3b-*vuz0VWPa( z>#@%iz3W9?Qj>=_*d%8m4Uw9bdqSiW2c^0F_JmCI_|g0pt9+t z1~qrAG~}aE82Vv?^7--!h>DL60050XevDL_K~&ofdzwv zOJ2nMDqkI2X07pWkB0SrCpcV(bUb@9KShJPV6;g3k0xKxqbcLFpbR2&biI)^!UrGY z{Wijq?F1o}x6!gCRPQJbu7j{E$ZpqD63o2jZ)?a4gxCtc8hsr(N}3bTmg!_*U#FUU zZ_-)2n6If<;%E1kVR22r%Yb2#r~nMh=-jmR1OW3_(#yuR`R+!O;~I|u)_NQ?y#DKD zQF;?88m1b<9mh7rQeo;$rfWW#aUZT1FMIfD?V^Of=9mr$U5NSWB!dhN-DGvT6i!6Y z6;sZJOKbD45QF+hnD@IjBDbnLT#UQT@DHyCNI z_ePy^4=)M5a3})qov4u^tFF8xcv6f|scW_*PiZxch`J8U74|K~ci#IG!B<)Ta;OmS zbf*rmJTv*(8kMD>IYyo6YoU5WL=fJabS;d5v>Ha{X0sMOyU8G>AaPRUH#;6~WNmeo zHNl9wAFg&Xs%%zMzK278mJU7AeCC||RqS=dpJHxt4#leCaK)QeW7iO7>*8C!nPfv* zp7~;;oevQDrdOLO0Mmylymgt>nLm=NXRiJRv}e;ZF}*b|J`>=v_qCHi#m9^4pXQDK z4TI$BhmPiI@q$LR(f8>Sf4vAS07xtjV8jr0aNwRS4os)uPPZlt8GXgXQvO`_HNxlPRBlZk&G3Q|5RrC#NUv}lMD zFWc0>nd%#Ona>>vJ$+SiR)>>{_uKyhyX<&Mz@CBr!DLoW$Tf`#;1C0dTwl*{YL&dn7C@hNSgZ=&BGZk1eNW zAS^1byW`$GuFQd?4hOy;8&YHp)=#7}Jp4`8$R$IoFgzQbIV3uMr*Vp_ULBPdbhn=X zTgferJ96@0B>J-+mG4pu7*Kee^NWo2y zxFpl%f(?PeEMh~r&X&+0$YDhE96>Rh)LB=DL?BR5c*wOS9&1Vsrn%+QSwMdv7VEQo zT;eTdx4L$Ma$n^b=I=*_H~TBO?XbLH&$3_(v#`Dm=kWeq?4Vzj5gY!k_luog?zCThMnKSJb`7?w4rZC5|v*~~5}L)3t=??xV}l+>EcYzgq#^|<~z???^s zI>hlEB83r?oop^(vycDA-3C_uhSkl{f>JQS=?7&Fi_{31m*YZAIJQ+ey?n<__nSx6 z`>(lQ??-irabjxNI z#~eVg#2i)InHtzS|H<(+(|j}|IYNrIeZ7%9mnz*Ll2`)mnVfIoEf@Np)K5G#21_$O zHz$;9>$VM%!Eto_X%p?YO?H}&*KMAq!=@2W2j2yTgTS;sdRQwG`J0 zk8>Cs=lA9t%w=>j2YNtO{hT_Q?xN{1OJ5_SQwUVY=Nb6;SC}gxWA-b&Wv7;i)y{Pu z!jzUImv&~>2bdM2u~J3}I% z%GfzOP{rD5YG#ggx~{Og@S;kHIEG&(Ul%WY1n^t+a{f<$*o*kTG~EAOq!w6~rS95C zb=MRvl_HYg%*yj)2h-nDGacp8qW}HsyH>(fn)PNY6fn`3ifmyuzc%ix3=oS!mmR`9 zApDNY8>--MV4S`y&5uW&3Oz%7Q|AN_Leo-GWMGq-WpW|}8Dc0>|x-Z%5E`GMfa;9_qCnh}Pej|&sT zTp6|#NDgHcdW}6`2jdsxcnY8rcMwU10VAeD`p6~b{`Wwz$nNcK$m=N#;m6)a^`ahG zY@TJ21`BtI(qKQ8M%$@)kT5QTQ?+b?2MU%pq4#Bln~?lqV?|}E&X0#*XD^O?+rPwP zDu>UYWtP97uf`5;f-W?4#GtK%f>S-XcxkN!i@h@HClB6N#HUl%FRz0w(4Nd`jDBJr{FsDu4m_f zwx3hCe9&n9k=%_>1s>YfHHC23e!BYdvc^P==U% z7E)x;Gvgo4*8GgNNP~|$kO=}#DG@C68UAUg9%dUVUt!aWGj0kN__r_(Tg2V2k4aFr zf}We{J$-6aw0_*Qjak>D#xxKX6gAA*dI1<)J>EZ%4G#z4ri>d;@$D^B)t4!jBOli%UB%iRLiBaqkpSl8M@az z;_mvV$M+k)#?BeF1zCB&|8y<;n;JY}OJDlnS4YFQVK6jK#!-%$E6W-pQ<~OqvSnbT zUeMro!Iu{k7}@Uo?-W>iNI)L78AJZo(G4c7tsfun;roXhfII`wZ&ykBz%oB9(1@Gu zrX`KX+5-8IZ0>lLRRceE~vChHTF-NvISR z>y=y+rqlz?Do+cof7-hBv(0YHb)?xt4Uu91$RYC1Tnvtr%hs!;#&3!mE2_Jm#9+)< zt)<<5HJi7QG;1YufsnnO9F~q3Ysbp13rFRVcGE1>D?Bwb#K9n4etZ_1bc;dUOkX^WB-ViI_~6>x3Dk;2oIhF`;dV zjp=qx#y4(`d^nTrCK~~GGx8=sch@1-fGk*jFk6APQvkX1Q!UL6{paDFa(vSwW*0J< zsU;~otiSd4S-LyWV8^hRGi;~^{r3kF`C+05Cowu<7jWCA>8SKx_y?7|Nv7e`+{k}p zwa4fBt(T;P$6t&#P6Q?!6Hx#0X`7FUR|{lEZ(v-2=j)V!4(&aRJ=v&sbd55YLxbVM zi&cFP&-X_s3W z#fevwdbjGMS)(WfCC}u|3%7En8d;K~*Q$>8-E(&HAC#77rSsjOTvwxfg~5PPHsBynT+z^M#6RCudf*zxb2Vu*!s)LmQ%^-{^C4*#+rcX;c(xf zArqMKiK7=A$&-5pQor}jGl~<3`|wCSE_sEk%$`ti?9QbA0I$HzH;#{t4faLzc0NWo z*9=>DF6BH_ir(|Yr* zS3bP8f<$FwaQuo6%(Y-K{lpj)e<%HWTsV~P=a`!0UMQaoxFzOP{0+$zacGT^CuF9(<_mP;bSyQhJ9{8Nj zvOSjfKE=F;*G?k+;o#i6^evZJt*=nX>yz*9AncG{pGD#Rm-l=7e}|;=!k?8U(f{Fr zV9a(}Rw0)e_ESdCHOpB8MLzwSP-r7zBU+QUy;S3KN1woQ`5WeJdQN^+I9^@8%{;mXjHm85*DGv~Pq&b0 zJrx_%&72AUU^6L(@`(ascLFRz>Yf^0mx|>{v_v_HnCIKrok@g4Bf!*<&#=r>nEJF_ zf@pS|HeI*PB;b-qVZJel9eF>h;bOb*vSK)t6PMs-&fB2Ds1Z)^5oD~#d4s~Mt&OV- z3k=ycyZHy_Qn47-hL3Ol#H+g*@Pt8+t(3z$uy$)!Fh8(`)iNwyCG1_zQ=s&=qM<9Z z(oo}GXx5FYz=66t!*IRqCpdOB>wcBn_?Nz5HL*?rTh;!9lf}t$WxImE2D^-ZMO-13 z*>_u)RtPvc9|`KV_$@=--HFcs6pw!n(ZIL1uKUM}=lr!cYDAem!J*%5G3&BB@`v*_ zyo*0Lyak+wSkiuO=%i9v?z&Z8zj@->reK5YdE__RNb%~}ut87Ot;nmzCf0w9GY-IW zo_{JE)#|03Zo6hC-W8*_B(lC+)yA5(o0FS#%^v3dd8N)_u33dFLnAZoJM}+W4}{N0 zLf=<~kl~8K%s6ZW06HQfUK^3iaxo+3244l=H_z^3Vsg3O>4Rb|^T_Dvp}@bV1UL zK#;_Uy=_J;@<_4BBff54@K29dt{1CQa<%*&9D=uct@JtRoCN(`dwc9M=zO;?-u%5P zukp4zWp9w+Mr(C=&j4Ni&%&ZyAYm8&1I-0b^NWJ0y2VePCe^c4PDv7Y&PcPlW-kJ@ zJxnMRD|P0^1-pJlo(MOC_-64ZNHdiSuxpB zx68}J{lexLh!gr0hhPCmlesJqi47(x3m;sg5G%#Y+QMZnxwWmP=Q%>Z^@uwlVA%>#z2;@#)LIf_|#ekjt}-42n;m?U@hq)_?gC>ghUn zTQ1uG6-taCO(%@tG#$e~guRhG_x|r)sW&cGbT+~H>Ti55&KT@iR zzSOPhaoAnxjlNx(^IesR(d{L^3ZEo!B}&mMqDXp)Ehk3~DOApEY5ve9kFrnd1*Y4h zJhBRFYCjO8I*qV{4Ng2AWY#d0=pxzPJK^dXxSbEl zl!{M)`dwS)pKrcN^X?(eR$el(^?+O%u)DUXoUR5wzVqFNEcJnvx^p%io{h_xdi8

+u(R(}k1ZDuG*sGnd>||4*mFlkid$bOF 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

Dw!HAvvnq;Ik5JUy;x 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 z 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 literal 0 HcmV?d00001 diff --git a/Tiku.Api/wwwroot/platform-admin/index.html b/Tiku.Api/wwwroot/platform-admin/index.html new file mode 100644 index 0000000..fe50e8c --- /dev/null +++ b/Tiku.Api/wwwroot/platform-admin/index.html @@ -0,0 +1,35 @@ + + + + + + + 恭学题库 SaaS · 平台管理 + + + + + + + + + + +

+
+ +
+
PLATFORM / PROD平台经营工作台
所有系统正常
+
+
+
+ +
+
+ + diff --git a/Tiku.Api/wwwroot/platform-admin/platform-api.js b/Tiku.Api/wwwroot/platform-admin/platform-api.js new file mode 100644 index 0000000..7387306 --- /dev/null +++ b/Tiku.Api/wwwroot/platform-admin/platform-api.js @@ -0,0 +1,886 @@ +(() => { + const defaults = { + mode: 'mock', + apiBaseUrl: '', + fallbackToMock: true, + timeoutMs: 10000, + getAccessToken: null, + }; + const runtime = { ...defaults, ...(window.GONGXUE_PLATFORM_RUNTIME_CONFIG || {}) }; + const operationCatalog = Object.fromEntries( + (window.GONGXUE_PLATFORM_SPEC?.operations || []).map(operation => [operation.operationId, operation]), + ); + const trace = []; + + class PlatformApiError extends Error { + constructor(message, options = {}) { + super(message); + this.name = 'PlatformApiError'; + this.status = Number(options.status || 0); + this.code = options.code || 'PLATFORM_API_ERROR'; + this.requestId = options.requestId || ''; + this.operationId = options.operationId || ''; + this.details = options.details; + } + } + + function normalizeBaseUrl(value) { + return String(value || '').trim().replace(/\/+$/, ''); + } + + function isEnabled() { + return runtime.mode === 'api' || (runtime.mode === 'auto' && Boolean(normalizeBaseUrl(runtime.apiBaseUrl))); + } + + function publicConfig() { + return { + mode: runtime.mode, + apiBaseUrl: normalizeBaseUrl(runtime.apiBaseUrl), + fallbackToMock: runtime.fallbackToMock !== false, + timeoutMs: Number(runtime.timeoutMs) || defaults.timeoutMs, + configured: isEnabled(), + }; + } + + async function accessToken() { + const provider = typeof runtime.getAccessToken === 'function' + ? runtime.getAccessToken + : typeof window.GONGXUE_PLATFORM_ACCESS_TOKEN_PROVIDER === 'function' + ? window.GONGXUE_PLATFORM_ACCESS_TOKEN_PROVIDER + : null; + const token = provider ? await provider() : ''; + return String(token || '').trim(); + } + + function buildUrl(path, query = {}) { + const baseUrl = normalizeBaseUrl(runtime.apiBaseUrl); + const url = new URL(`${baseUrl}${path.startsWith('/') ? path : `/${path}`}`, window.location.href); + Object.entries(query).forEach(([key, value]) => { + if (value === undefined || value === null || value === '') return; + url.searchParams.set(key, Array.isArray(value) ? value.join(',') : String(value)); + }); + return url.toString(); + } + + function responseRequestId(payload, response) { + return String( + payload?.meta?.requestId + || payload?.requestId + || response?.headers?.get?.('x-request-id') + || '', + ); + } + + async function request(operationId, options = {}) { + const operation = operationCatalog[operationId]; + if (!operation) { + throw new PlatformApiError(`未知平台接口:${operationId}`, { + code: 'PLATFORM_OPERATION_UNKNOWN', + operationId, + }); + } + if (!isEnabled()) { + throw new PlatformApiError('平台 API 未启用', { + code: 'PLATFORM_API_DISABLED', + operationId, + }); + } + const token = await accessToken(); + if (!token) { + throw new PlatformApiError('缺少平台管理员 access token', { + status: 401, + code: 'PLATFORM_ACCESS_TOKEN_MISSING', + operationId, + }); + } + const extraHeaders = Object.fromEntries(Object.entries(options.headers || {}).map(([key, value]) => [key, String(value)])); + const forbiddenHeader = Object.keys(extraHeaders).find((key) => { + const normalized = key.toLowerCase(); + return ['authorization', 'x-user-id', 'x-platform-admin-key'].includes(normalized) + || normalized.startsWith('x-tenant-'); + }); + if (forbiddenHeader) { + throw new PlatformApiError(`禁止页面覆盖身份头:${forbiddenHeader}`, { + code: 'PLATFORM_IDENTITY_HEADER_FORBIDDEN', + operationId, + }); + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), Number(runtime.timeoutMs) || defaults.timeoutMs); + const startedAt = Date.now(); + if (options.signal) { + if (options.signal.aborted) controller.abort(); + else options.signal.addEventListener('abort', () => controller.abort(), { once: true }); + } + try { + const response = await fetch(buildUrl(operation.path, options.query), { + method: operation.method, + headers: { + ...extraHeaders, + Accept: 'application/json', + Authorization: `Bearer ${token}`, + ...(operation.method === 'GET' ? {} : { 'Content-Type': 'application/json' }), + }, + body: operation.method === 'GET' || options.body === undefined + ? undefined + : JSON.stringify(options.body), + credentials: 'omit', + signal: controller.signal, + }); + const text = await response.text(); + let payload = {}; + if (text) { + try { + payload = JSON.parse(text); + } catch { + payload = { error: text.slice(0, 500) }; + } + } + const requestId = responseRequestId(payload, response); + trace.push({ + operationId, + method: operation.method, + path: operation.path, + status: response.status, + requestId, + durationMs: Date.now() - startedAt, + at: new Date().toISOString(), + }); + if (trace.length > 120) trace.splice(0, trace.length - 120); + if (!response.ok) { + throw new PlatformApiError( + String(payload.message || payload.error || `请求失败(HTTP ${response.status})`), + { + status: response.status, + code: String(payload.code || 'PLATFORM_API_REQUEST_FAILED'), + requestId, + operationId, + details: payload, + }, + ); + } + return payload; + } catch (error) { + if (error instanceof PlatformApiError) throw error; + const aborted = error?.name === 'AbortError'; + throw new PlatformApiError(aborted ? '平台接口请求超时' : (error?.message || '平台接口请求失败'), { + code: aborted ? 'PLATFORM_API_TIMEOUT' : 'PLATFORM_API_NETWORK_ERROR', + operationId, + details: error, + }); + } finally { + clearTimeout(timeout); + } + } + + const dictionaries = { + tenantStatusToApi: { 待配置: 'draft', 试用: 'active', 正常: 'active', 暂停: 'suspended', 已归档: 'archived' }, + tenantStatusFromApi: { draft: '待配置', active: '正常', suspended: '暂停', archived: '已归档' }, + billingStatusToApi: { 试用: 'trial', 正常: 'active', 待付款: 'active', 逾期: 'past_due', 暂停: 'suspended', 已取消: 'cancelled' }, + billingStatusFromApi: { trial: '试用', active: '正常', past_due: '逾期', suspended: '暂停', cancelled: '已取消' }, + subscriptionStatusToApi: { 试用中: 'trial', 活跃: 'active', 暂停: 'suspended', 已取消: 'cancelled', 已到期: 'expired' }, + subscriptionStatusFromApi: { trial: '试用中', active: '活跃', suspended: '暂停', cancelled: '已取消', expired: '已到期' }, + invoiceStatusFromApi: { draft: '草稿', issued: '待付款', paid: '已支付', void: '已作废', overdue: '已逾期' }, + invoiceTypeToApi: { 订阅服务费: 'subscription', 用量超额费: 'usage', 人工调整: 'adjustment', 首期服务费: 'manual' }, + invoiceTypeFromApi: { subscription: '订阅服务费', usage: '用量超额费', usage_overage: '用量超额费', manual: '人工账单', adjustment: '人工调整' }, + severityToApi: { 低: 'low', 中: 'medium', 高: 'high', 严重: 'critical' }, + severityFromApi: { low: '低', medium: '中', high: '高', critical: '严重' }, + grantScopeToApi: { 全部活跃租户: 'all_active_tenants', 指定套餐: 'plans', 指定租户: 'tenants', '套餐 + 租户': 'mixed' }, + grantScopeFromApi: { all_active_tenants: '全部活跃租户', plans: '指定套餐', tenants: '指定租户', mixed: '套餐 + 租户' }, + grantStatusFromApi: { active: '启用', disabled: '已禁用', expired: '已过期' }, + providerToApi: { Webhook: 'generic', SMTP: 'generic', 邮件: 'generic', 短信: 'generic', 钉钉: 'dingtalk', 飞书: 'feishu', 企微: 'wecom' }, + providerFromApi: { generic: 'Webhook', dingtalk: '钉钉', feishu: '飞书', wecom: '企微' }, + eventStatusFromApi: { pending: '待处理', processing: '发送中', sent: '成功', retrying: '重试中', failed: '失败', discarded: '已丢弃' }, + }; + + function mapValue(map, value, fallback = value) { + return Object.prototype.hasOwnProperty.call(map, value) ? map[value] : fallback; + } + + function centsToYuan(value) { + const number = Number(value || 0); + return Number.isFinite(number) ? number / 100 : 0; + } + + function yuanToCents(value) { + const number = Number(value || 0); + return Number.isFinite(number) ? Math.round(number * 100) : 0; + } + + function dateOnly(value) { + return value ? String(value).slice(0, 10) : ''; + } + + function monthOnly(value) { + return value ? String(value).slice(0, 7) : ''; + } + + function dateTime(value) { + if (!value) return '-'; + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) return String(value); + return new Intl.DateTimeFormat('zh-CN', { + timeZone: 'Asia/Shanghai', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }).format(parsed).replaceAll('/', '-'); + } + + function isoStartOfDay(value) { + if (!value) return undefined; + return `${String(value).slice(0, 10)}T00:00:00+08:00`; + } + + function splitList(value) { + if (Array.isArray(value)) return value.map(String).map(item => item.trim()).filter(Boolean); + return String(value || '').split(/[、,,]/).map(item => item.trim()).filter(Boolean); + } + + function periodBounds(period) { + const match = String(period || '').match(/^(\d{4})-(\d{2})$/); + if (!match) return { periodStart: '', periodEnd: '' }; + const year = Number(match[1]); + const month = Number(match[2]); + const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); + return { + periodStart: `${match[1]}-${match[2]}-01`, + periodEnd: `${match[1]}-${match[2]}-${String(lastDay).padStart(2, '0')}`, + }; + } + + function invoicePeriodBounds(label) { + const match = String(label || '').match(/^(\d{4})年(\d{1,2})月$/); + return match ? periodBounds(`${match[1]}-${String(match[2]).padStart(2, '0')}`) : {}; + } + + const bodyBuilders = { + PlatformAdminTenantsController_create(input) { + return { + slug: input.slug, + name: input.name, + legalName: input.legalName || undefined, + brandName: input.brandName || undefined, + shortName: input.shortName || undefined, + primaryHost: input.primaryHost || undefined, + planCode: input.planCode || undefined, + status: mapValue(dictionaries.tenantStatusToApi, input.status, input.status), + billingStatus: mapValue(dictionaries.billingStatusToApi, input.billingStatus, input.billingStatus), + amountCents: yuanToCents(input.amount), + }; + }, + PlatformAdminTenantsController_status(input) { + return { + tenantId: input.tenantId, + status: mapValue(dictionaries.tenantStatusToApi, input.status, input.status), + billingStatus: mapValue(dictionaries.billingStatusToApi, input.billingStatus, input.billingStatus), + reason: input.reason, + }; + }, + PlatformAdminTenantsController_billingProfile(input) { + return { + tenantId: input.tenantId, + billingName: input.billingName || undefined, + taxId: input.taxId || undefined, + contactName: input.contactName || undefined, + contactPhone: input.phone || input.contactPhone || undefined, + contactEmail: input.email || input.contactEmail || undefined, + billingAddress: input.billingAddress || undefined, + invoiceTitle: input.invoiceTitle || undefined, + invoiceType: ({ 增值税专用发票: 'special_vat', 增值税普通发票: 'normal_vat', 不开票: 'none' })[input.type || input.invoiceType] || input.invoiceType || 'none', + bankName: input.bankName || undefined, + bankAccountMasked: input.bankAccountMasked || undefined, + }; + }, + PlatformAdminBillingController_createSubscription(input) { + return { + tenantId: input.tenantId, + planCode: input.planCode, + status: mapValue(dictionaries.subscriptionStatusToApi, input.status, input.status || 'active'), + billingCycle: ({ 年度: 'yearly', 月度: 'monthly', 试用: 'trial' })[input.cycle] || input.billingCycle, + amountCents: yuanToCents(input.amount), + startsAt: isoStartOfDay(input.start || input.startsAt), + expiresAt: isoStartOfDay(input.expires || input.expiresAt), + metadata: { autoRenew: Boolean(input.autoRenew), source: 'static-platform-console' }, + }; + }, + PlatformAdminBillingController_createInvoice(input) { + const bounds = invoicePeriodBounds(input.period); + return { + tenantId: input.tenantId, + invoiceType: mapValue(dictionaries.invoiceTypeToApi, input.type, input.invoiceType || 'manual'), + status: input.status || 'draft', + dueDate: input.due || input.dueDate, + billingPeriodStart: input.billingPeriodStart || bounds.periodStart, + billingPeriodEnd: input.billingPeriodEnd || bounds.periodEnd, + items: input.items || [{ + itemType: mapValue(dictionaries.invoiceTypeToApi, input.type, 'manual'), + description: input.note || input.type || '平台手工账单', + quantity: 1, + unitAmountCents: yuanToCents(input.amount), + metadata: { periodLabel: input.period || '' }, + }], + note: input.note || undefined, + }; + }, + PlatformAdminBillingController_fromSubscription(input) { + return { + tenantId: input.tenantId, + subscriptionId: input.subscriptionId || undefined, + status: input.status || 'issued', + dueDate: input.dueDate || undefined, + note: input.note || undefined, + }; + }, + PlatformAdminBillingController_fromSubscriptionsBatch(input) { + return { + tenantIds: input.tenantIds || undefined, + subscriptionIds: input.subscriptionIds || undefined, + daysAhead: input.daysAhead ?? 45, + status: input.status || 'issued', + dueDate: input.dueDate || undefined, + note: input.note || undefined, + dryRun: Boolean(input.dryRun), + }; + }, + PlatformAdminBillingController_fromUsage(input) { + const bounds = input.periodStart && input.periodEnd ? input : { ...input, ...periodBounds(input.period) }; + return { + tenantIds: bounds.tenantIds || undefined, + periodStart: bounds.periodStart, + periodEnd: bounds.periodEnd, + dueDate: bounds.dueDate || undefined, + note: bounds.note || undefined, + status: bounds.status || 'issued', + dryRun: Boolean(bounds.dryRun), + }; + }, + PlatformAdminBillingController_processOverdue(input) { + return { dryRun: Boolean(input.dryRun), channel: input.channel || 'internal', limit: Number(input.limit || 100) }; + }, + PlatformAdminBillingController_confirmPayment(input) { + return { + tenantId: input.tenantId, + invoiceId: input.invoiceId, + amountCents: yuanToCents(input.amount), + paymentNo: input.paymentNo || undefined, + provider: input.provider || 'manual', + method: input.method || 'manual', + paidAt: input.paidAt ? new Date(input.paidAt).toISOString() : undefined, + providerTradeNo: input.tradeNo || input.providerTradeNo || undefined, + }; + }, + PlatformAdminBillingController_recordUsage(input) { + const bounds = input.periodStart && input.periodEnd ? input : { ...input, ...periodBounds(input.period) }; + return { + tenantId: bounds.tenantId, + metricKey: bounds.metricKey, + metricValue: Number(bounds.metricValue || 0), + periodStart: bounds.periodStart, + periodEnd: bounds.periodEnd, + metadata: { source: 'platform_console_manual' }, + }; + }, + PlatformAdminQuestionBanksController_upsertGrant(input) { + return { + id: input.id || undefined, + sourceQuestionBankId: input.bankId || input.sourceQuestionBankId, + grantScope: mapValue(dictionaries.grantScopeToApi, input.scope, input.grantScope), + allowedPlanCodes: input.allowedPlanCodes || (input.planCode ? [input.planCode] : []), + allowedTenantIds: input.allowedTenantIds || (input.tenantId ? [input.tenantId] : []), + allowedRegionIds: input.allowedRegionIds || splitList(input.regionIds), + allowedSubjectIds: input.allowedSubjectIds || splitList(input.subjectIds), + status: input.status === '已禁用' || input.status === 'disabled' + ? 'disabled' + : input.status === '已过期' || input.status === 'expired' + ? 'expired' + : 'active', + startsAt: isoStartOfDay(input.startsAt), + expiresAt: isoStartOfDay(input.expiresAt), + metadata: { localEdit: Boolean(input.localEdit) }, + }; + }, + PlatformAdminOverviewController_upsertStaff(input) { + const permissionList = input.permissions || []; + return { + id: input.id || undefined, + authUserId: input.authUserId, + username: input.username, + email: input.email || undefined, + phone: input.phone || undefined, + name: input.name, + status: input.status === '已停用' ? 'disabled' : 'active', + platformPermissions: Object.fromEntries(permissionList.map(permission => [permission, true])), + metadata: { displayRole: input.role || '平台管理员' }, + }; + }, + PlatformAdminOverviewController_updateStaffStatus(input) { + return { + staffId: input.staffId, + status: input.status === '正常' || input.status === 'active' ? 'active' : 'disabled', + reason: input.reason || undefined, + revokeSessions: input.revokeSessions !== false, + }; + }, + PlatformAdminAuditController_updateAlert(input) { + return { + alertId: input.alertId, + status: input.status, + note: input.note || undefined, + }; + }, + }; + + function notificationBody(input) { + const maxAttempts = Number(input.maxAttempts || input.retry || 3); + return { + id: input.id || undefined, + channelCode: input.code || input.channelCode, + name: input.name, + enabled: input.enabled !== false, + provider: mapValue(dictionaries.providerToApi, input.provider, input.provider || 'generic'), + webhookUrl: input.endpoint || input.webhookUrl, + secret: input.secret || undefined, + secretRef: input.secretRef || undefined, + minSeverity: mapValue(dictionaries.severityToApi, input.minSeverity, input.minSeverity || 'medium'), + statusFilter: splitList(input.statusFilter || 'open,acknowledged'), + timeoutSec: Number(input.timeout || input.timeoutSec || 10), + maxAttempts, + metadata: { ...(input.metadata || {}), maxAttempts }, + }; + } + bodyBuilders.PlatformAdminAuditController_upsertChannel = notificationBody; + bodyBuilders.PlatformAdminDunningChannelsController_upsertChannel = input => ({ + ...notificationBody(input), + minReminderLevel: Number(input.minReminderLevel || 1), + reminderTypes: input.reminderTypes || ['overdue', 'final_notice'], + reminderChannels: input.reminderChannels || ['internal'], + }); + + function buildBody(operationId, input = {}) { + const builder = bodyBuilders[operationId]; + return builder ? builder(input) : input; + } + + function jsonSummary(value) { + if (!value) return ''; + if (typeof value === 'string') return value; + try { + return JSON.stringify(value); + } catch { + return String(value); + } + } + + function safeEndpoint(item) { + if (!item?.webhook) return ''; + const protocol = item.webhook.protocol ? `${item.webhook.protocol}://` : ''; + return `${protocol}${item.webhook.host || ''}${item.webhook.pathname || ''}`; + } + + function currentById(items, id) { + return (items || []).find(item => item.id === id) || {}; + } + + const adapters = { + plan(item, current = {}) { + const quotas = item.includedQuotas && typeof item.includedQuotas === 'object' + ? Object.entries(item.includedQuotas).map(([key, value]) => `${key}: ${value}`) + : current.quotas || []; + return { + ...current, + code: item.code, + name: item.name, + description: item.description || '', + cycle: ({ yearly: '年度', monthly: '月度' })[item.billingCycle] || item.billingCycle || '-', + amount: centsToYuan(item.baseAmountCents), + quotas, + tenantCount: Number(current.tenantCount || 0), + status: item.status, + }; + }, + tenant(item, current = {}, planMap = {}) { + const plan = planMap[item.planCode]; + return { + students: 0, + questions: 0, + storageGB: 0, + trafficTB: 0, + balance: 0, + host: '-', + owner: '待配置', + ownerPhone: '-', + ...current, + id: item.id, + slug: item.slug, + name: item.name, + legal: item.legalName || current.legal || item.name, + brand: item.brandName || current.brand || item.name, + planCode: item.planCode || '', + plan: plan?.name || current.plan || item.planCode || '未订阅', + status: mapValue(dictionaries.tenantStatusFromApi, item.status, item.status || '-'), + billing: mapValue(dictionaries.billingStatusFromApi, item.billingStatus, item.billingStatus || '-'), + mode: item.mode === 'saas' ? '正式' : item.mode || current.mode || '-', + expires: dateOnly(item.subscriptionExpiresAt), + balance: centsToYuan(item.openBalanceCents), + createdAt: dateTime(item.createdAt), + }; + }, + subscription(item) { + return { + id: item.subscriptionId || item.id, + tenantId: item.tenantId, + planCode: item.planCode || '', + status: mapValue(dictionaries.subscriptionStatusFromApi, item.status, item.status || '-'), + cycle: ({ yearly: '年度', monthly: '月度', trial: '试用' })[item.billingCycle] || item.billingCycle || '-', + amount: centsToYuan(item.amountCents), + start: dateOnly(item.startsAt), + expires: dateOnly(item.expiresAt), + autoRenew: Boolean(item.metadata?.autoRenew), + }; + }, + invoice(item) { + return { + id: item.invoiceNo || item.id, + backendId: item.id, + tenantId: item.tenantId, + type: mapValue(dictionaries.invoiceTypeFromApi, item.invoiceType, item.invoiceType || '-'), + period: item.billingPeriodStart ? monthOnly(item.billingPeriodStart) : monthOnly(item.issuedAt || item.dueDate), + total: centsToYuan(item.totalCents), + paid: centsToYuan(item.paidCents), + balance: centsToYuan(item.balanceCents), + due: dateOnly(item.dueDate), + status: mapValue(dictionaries.invoiceStatusFromApi, item.status, item.status || '-'), + note: item.note || '', + }; + }, + reminder(item) { + return { + id: item.id, + invoiceId: item.invoiceNo || item.invoiceId, + backendInvoiceId: item.invoiceId, + tenantId: item.tenantId, + level: Number(item.reminderLevel || 1), + channel: item.channel || '-', + status: mapValue(dictionaries.eventStatusFromApi, item.status, item.status || '-'), + date: dateTime(item.reminderDate || item.createdAt), + message: item.message || '', + }; + }, + bank(item, current = {}) { + return { + ...current, + id: item.id, + name: item.name, + catalog: item.regionName || item.tenantName || '公共题库', + source: item.sourceScope === 'platform' ? '平台公共题库' : '租户自建', + questions: Number(item.questionCount || 0), + version: item.metadata?.version || item.metadata?.contentVersion || '-', + status: item.status === 'active' ? '稳定版' : item.status || '-', + }; + }, + grant(item) { + return { + id: item.id, + bankId: item.sourceQuestionBankId, + scope: mapValue(dictionaries.grantScopeFromApi, item.grantScope, item.grantScope || '-'), + allowedPlanCodes: item.allowedPlanCodes || [], + allowedTenantIds: item.allowedTenantIds || [], + allowedRegionIds: item.allowedRegionIds || [], + allowedSubjectIds: item.allowedSubjectIds || [], + status: mapValue(dictionaries.grantStatusFromApi, item.status, item.status || '-'), + startsAt: dateOnly(item.startsAt), + expiresAt: dateOnly(item.expiresAt), + localEdit: Boolean(item.metadata?.localEdit), + }; + }, + sync(item) { + const progress = item.syncStatus === 'synced' + ? 100 + : Number(item.metadata?.lastSync?.progress || item.lastSyncCounts?.progress || 0); + return { + tenantId: item.tenantId, + bankId: item.sourceQuestionBankId, + current: item.metadata?.lastSync?.sourceVersion || '-', + target: item.metadata?.lastSync?.targetVersion || '-', + progress, + conflicts: Number(item.conflictCount || 0), + status: ({ pending: '同步中', synced: '已完成', failed: '同步失败' })[item.syncStatus] || item.syncStatus || item.status || '-', + }; + }, + staff(item) { + const permissions = item.roleCodes?.includes('platform_super_admin') ? ['*'] : []; + return { + id: item.userId || item.id, + authUserId: item.userId || item.authUserId || '', + username: item.email || '', + name: item.name || item.email || '平台员工', + email: item.email || '', + phone: item.phoneMasked || item.phone || '-', + role: permissions.includes('*') ? '超级管理员' : item.rawProfile?.displayRole || '平台管理员', + permissions, + status: item.status === 'active' ? '正常' : '已停用', + lastActive: dateTime(item.updatedAt), + }; + }, + audit(item) { + const details = jsonSummary(item.details); + return { + id: item.id, + time: `${dateTime(item.createdAt)} CST`, + actor: item.actorName || item.actorUsername || item.actorUserId || 'system', + role: item.actorUserId ? '平台管理员' : '系统任务', + action: item.action || '-', + targetType: item.targetType || '-', + targetId: item.targetId || '', + target: item.tenantName || item.targetId || item.targetType || '-', + severity: mapValue(dictionaries.severityFromApi, item.details?.severity, item.details?.severity || '中'), + result: item.details?.result === 'failed' ? '失败' : '成功', + ip: item.ipAddress || '-', + details, + }; + }, + alert(item) { + const evidence = Array.isArray(item.details?.evidence) + ? item.details.evidence.map(String) + : Object.entries(item.details || {}).slice(0, 8).map(([key, value]) => `${key}: ${jsonSummary(value)}`); + return { + id: item.id, + title: item.title || item.ruleName || '平台审计告警', + summary: item.summary || item.action || '', + severity: mapValue(dictionaries.severityFromApi, item.severity, item.severity || '-'), + status: item.status || 'open', + owner: item.acknowledgedByName || item.resolvedByName || item.tenantName || item.ruleName || '待认领', + targetType: item.targetType || '-', + targetId: item.targetId || '', + evidence, + updatedAt: dateTime(item.updatedAt || item.createdAt), + note: item.resolutionNote || '', + }; + }, + rule(item) { + return { + id: item.id, + code: item.code, + name: item.name, + severity: mapValue(dictionaries.severityFromApi, item.severity, item.severity || '-'), + enabled: item.enabled !== false, + window: item.conditions?.window || item.metadata?.window || '-', + threshold: item.conditions?.threshold || item.metadata?.threshold || '-', + target: item.description || (item.targetTypes || []).join('、') || '-', + }; + }, + channel(item) { + return { + id: item.id, + code: item.channelCode, + name: item.name, + provider: mapValue(dictionaries.providerFromApi, item.provider, item.provider || 'Webhook'), + endpoint: safeEndpoint(item), + enabled: item.enabled !== false, + retry: Number(item.maxAttempts || item.metadata?.maxAttempts || 3), + minSeverity: mapValue(dictionaries.severityFromApi, item.minSeverity, item.minSeverity || '中'), + statusFilter: (item.statusFilter || []).join(','), + timeout: Number(item.timeoutSec || 10), + maxAttempts: Number(item.maxAttempts || item.metadata?.maxAttempts || 3), + lastStatus: '正常', + secretRef: item.secretRef || '', + endpointRedacted: true, + reminderTypes: item.reminderTypes || [], + reminderChannels: item.reminderChannels || [], + minReminderLevel: Number(item.minReminderLevel || 1), + }; + }, + event(item, type = 'audit') { + return { + id: item.id, + channelId: item.channelId, + subject: type === 'dunning' + ? item.reminderMessage || item.invoiceNo || '催缴通知' + : item.alertTitle || item.alertAction || '审计告警通知', + status: mapValue(dictionaries.eventStatusFromApi, item.status, item.status || '-'), + attempts: Number(item.attempts || 0), + updatedAt: dateTime(item.updatedAt || item.sentAt || item.createdAt), + error: item.lastError || '', + }; + }, + }; + + function aggregateUsage(items) { + const records = new Map(); + items.forEach(item => { + const period = monthOnly(item.periodStart || item.periodEnd); + const key = `${item.tenantId}:${period}`; + const record = records.get(key) || { + tenantId: item.tenantId, + period, + source: item.metadata?.source === 'platform_usage_worker' ? 'Worker 自动采集' : '人工补录', + students: 0, + questions: 0, + storageGB: 0, + trafficTB: 0, + overage: [], + estimated: 0, + }; + const metricMap = { + students: 'students', + questions: 'questions', + storage_gb: 'storageGB', + traffic_tb: 'trafficTB', + }; + const field = metricMap[item.metricKey]; + if (field) record[field] = Number(item.metricValue || 0); + records.set(key, record); + }); + return Array.from(records.values()); + } + + function currentPeriod() { + const parts = new Intl.DateTimeFormat('en-US', { + timeZone: 'Asia/Shanghai', + year: 'numeric', + month: '2-digit', + }).formatToParts(new Date()); + const values = Object.fromEntries(parts.map(part => [part.type, part.value])); + return `${values.year}-${values.month}`; + } + + const bootstrapDescriptors = [ + ['overview', 'PlatformAdminOverviewController_overview', {}], + ['plans', 'PlatformAdminOverviewController_plans', { query: { includeArchived: false } }], + ['tenants', 'PlatformAdminTenantsController_list', { query: { limit: 200 } }], + ['staff', 'PlatformAdminOverviewController_staff', { query: { limit: 200 } }], + ['dunningChannels', 'PlatformAdminDunningChannelsController_channels', { query: { limit: 100 } }], + ['dunningEvents', 'PlatformAdminDunningEventsController_events', { query: { limit: 100 } }], + ['auditLogs', 'PlatformAdminAuditController_logs', { query: { limit: 200 } }], + ['alerts', 'PlatformAdminAuditController_alerts', { query: { limit: 100 } }], + ]; + + async function bootstrap(options = {}) { + const descriptors = bootstrapDescriptors.map(([key, operationId, requestOptions]) => { + const nextOptions = { ...requestOptions, query: { ...(requestOptions.query || {}) } }; + if (key === 'banks') nextOptions.query.includeTenantBanks = Boolean(options.includeTenantBanks); + return [key, operationId, nextOptions]; + }); + const results = await Promise.allSettled( + descriptors.map(([, operationId, requestOptions]) => request(operationId, requestOptions)), + ); + const payloads = {}; + const errors = {}; + results.forEach((result, index) => { + const [key] = descriptors[index]; + if (result.status === 'fulfilled') payloads[key] = result.value; + else errors[key] = result.reason; + }); + return { payloads, errors, requested: descriptors.length }; + } + + function adaptBootstrap(payloads, currentData = {}) { + const patch = {}; + if (payloads.overview) patch.apiOverview = payloads.overview.item || payloads.overview; + if (payloads.permissions?.item) patch.apiPermissions = payloads.permissions.item; + if (Array.isArray(payloads.plans?.items)) { + patch.plans = payloads.plans.items.map(item => adapters.plan(item, currentById(currentData.plans, item.code))); + } + const planItems = patch.plans || currentData.plans || []; + const planMap = Object.fromEntries(planItems.map(item => [item.code, item])); + if (Array.isArray(payloads.tenants?.items)) { + patch.tenants = payloads.tenants.items.map(item => adapters.tenant(item, currentById(currentData.tenants, item.id), planMap)); + } + if (Array.isArray(payloads.staff?.items)) patch.staff = payloads.staff.items.map(adapters.staff); + if (Array.isArray(payloads.invoices?.items)) patch.invoices = payloads.invoices.items.map(adapters.invoice); + if (Array.isArray(payloads.subscriptions?.items)) { + patch.subscriptions = payloads.subscriptions.items + .filter(item => item.subscriptionId) + .map(adapters.subscription); + } + if (Array.isArray(payloads.usage?.items)) patch.usage = aggregateUsage(payloads.usage.items); + if (Array.isArray(payloads.usageCandidates?.items) && patch.usage) { + const usageByKey = new Map(patch.usage.map(item => [`${item.tenantId}:${item.period}`, item])); + payloads.usageCandidates.items.forEach(candidate => { + const record = usageByKey.get(`${candidate.tenantId}:${monthOnly(candidate.periodStart)}`); + if (!record) return; + record.estimated = centsToYuan(candidate.totalCents); + record.overage = (candidate.items || []).map(item => item.description || item.itemType).filter(Boolean); + }); + } + if (Array.isArray(payloads.reminders?.items)) patch.reminders = payloads.reminders.items.map(adapters.reminder); + if (Array.isArray(payloads.dunningChannels?.items)) patch.dunningChannels = payloads.dunningChannels.items.map(adapters.channel); + if (Array.isArray(payloads.dunningEvents?.items)) patch.dunningEvents = payloads.dunningEvents.items.map(item => adapters.event(item, 'dunning')); + if (Array.isArray(payloads.banks?.items)) { + patch.banks = payloads.banks.items.map(item => adapters.bank(item, currentById(currentData.banks, item.id))); + } + if (Array.isArray(payloads.grants?.items)) patch.grants = payloads.grants.items.map(adapters.grant); + if (Array.isArray(payloads.bankSync?.items)) patch.bankSync = payloads.bankSync.items.map(adapters.sync); + if (Array.isArray(payloads.auditLogs?.items)) patch.auditLogs = payloads.auditLogs.items.map(adapters.audit); + if (Array.isArray(payloads.alertRules?.items)) patch.alertRules = payloads.alertRules.items.map(adapters.rule); + if (Array.isArray(payloads.alerts?.items)) patch.alerts = payloads.alerts.items.map(adapters.alert); + if (Array.isArray(payloads.auditChannels?.items)) patch.auditChannels = payloads.auditChannels.items.map(adapters.channel); + if (Array.isArray(payloads.notificationEvents?.items)) { + patch.notificationEvents = payloads.notificationEvents.items.map(item => adapters.event(item, 'audit')); + } + return patch; + } + + async function tenantDetail(tenantId) { + const payload = await request('PlatformAdminTenantsController_detail', { query: { tenantId } }); + const item = payload.item || {}; + return { + tenant: item.tenant || null, + domains: item.domains || [], + subscriptions: (item.subscriptions || []).map(adapters.subscription), + invoices: (item.invoices || []).map(adapters.invoice), + usage: aggregateUsage(item.usage || []), + billingProfile: item.tenant ? { + billingName: item.tenant.billingName || '', + taxId: item.tenant.taxId || '', + contactName: item.tenant.contactName || '', + phone: item.tenant.contactPhone || '', + email: item.tenant.contactEmail || '', + billingAddress: item.tenant.billingAddress || '', + invoiceTitle: item.tenant.invoiceTitle || '', + type: ({ special_vat: '增值税专用发票', normal_vat: '增值税普通发票', none: '不开票' })[item.tenant.invoiceType] || item.tenant.invoiceType || '', + bankName: item.tenant.bankName || '', + bankAccountMasked: item.tenant.bankAccountMasked || '', + } : null, + }; + } + + function downloadExport(payload) { + const item = payload?.item; + if (!item?.contentBase64) { + throw new PlatformApiError('审计导出响应缺少文件内容', { code: 'AUDIT_EXPORT_INVALID' }); + } + const bytes = Uint8Array.from(atob(item.contentBase64), character => character.charCodeAt(0)); + const blob = new Blob([bytes], { type: item.mimeType || 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = item.filename || `platform-audit.${item.format || 'csv'}`; + anchor.click(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + return item; + } + + window.GongxuePlatformApi = Object.freeze({ + PlatformApiError, + adapters, + adaptBootstrap, + bootstrap, + buildBody, + dictionaries, + downloadExport, + isEnabled, + operationCatalog, + periodBounds, + publicConfig, + request, + tenantDetail, + trace, + }); +})(); diff --git a/Tiku.Api/wwwroot/platform-admin/platform-auth.js b/Tiku.Api/wwwroot/platform-admin/platform-auth.js new file mode 100644 index 0000000..b61c8c4 --- /dev/null +++ b/Tiku.Api/wwwroot/platform-admin/platform-auth.js @@ -0,0 +1,172 @@ +(() => { + const ACCESS_TOKEN_KEY = 'tiku_platform_access_token'; + const REFRESH_TOKEN_KEY = 'tiku_platform_refresh_token'; + const ACCESS_EXPIRES_KEY = 'tiku_platform_access_expires_at'; + const USER_KEY = 'tiku_platform_user'; + const runtime = window.GONGXUE_PLATFORM_RUNTIME_CONFIG || {}; + let challengeToken = ''; + let pendingResolve; + + function apiUrl(path) { + return `${String(runtime.apiBaseUrl || '').replace(/\/+$/, '')}${path}`; + } + + async function post(path, body) { + const response = await fetch(apiUrl(path), { + method: 'POST', + headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, + credentials: 'omit', + body: JSON.stringify(body), + }); + const text = await response.text(); + let payload = {}; + if (text) { + try { payload = JSON.parse(text); } catch { payload = { detail: text.slice(0, 500) }; } + } + if (!response.ok) throw new Error(payload.detail || payload.title || payload.message || `请求失败(HTTP ${response.status})`); + return payload; + } + + function gate() { return document.querySelector('#platformAuthGate'); } + + function escapeHtml(value) { + return String(value ?? '').replace(/[&<>'"]/g, character => ({ + '&': '&', '<': '<', '>': '>', "'": ''', '"': '"', + })[character]); + } + + function shell(title, description, content) { + gate().innerHTML = ``; + gate().hidden = false; + document.querySelector('.platform-app')?.setAttribute('inert', ''); + } + + function showError(error) { + const target = document.querySelector('#platformAuthError'); + if (target) target.textContent = error?.message || '操作失败,请重试'; + } + + function setBusy(form, busy) { + form.querySelectorAll('button,input').forEach(control => { control.disabled = busy; }); + } + + function storeAuthenticated(result) { + const user = result?.user; + const tokens = user?.tokens; + if (!tokens?.accessToken || !tokens?.refreshToken) throw new Error('登录响应缺少令牌'); + sessionStorage.setItem(ACCESS_TOKEN_KEY, tokens.accessToken); + sessionStorage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken); + sessionStorage.setItem(ACCESS_EXPIRES_KEY, tokens.accessTokenExpiresAt || ''); + sessionStorage.setItem(USER_KEY, JSON.stringify({ userId: user.userId, email: user.email, name: user.name })); + } + + function completeGate() { + gate().hidden = true; + gate().innerHTML = ''; + document.querySelector('.platform-app')?.removeAttribute('inert'); + pendingResolve?.(true); + pendingResolve = null; + } + + function finishAuthentication(result, recoveryCodes = []) { + storeAuthenticated(result); + if (recoveryCodes.length) { + shell('保存恢复代码', '这些代码只显示一次,请保存到安全位置。', `
${recoveryCodes.map(code => `
${escapeHtml(code)}
`).join('')}

保存后再进入平台控制台。

`); + document.querySelector('#platformAuthContinue').addEventListener('click', completeGate); + return; + } + completeGate(); + } + + async function handleAuthenticationResult(result) { + challengeToken = result.challengeToken || ''; + if (result.status === 'authenticated') { finishAuthentication(result); return; } + if (result.status === 'password_change_required') { renderPasswordChange(); return; } + if (result.status === 'mfa_enrollment_required') { await renderMfaEnrollment(); return; } + if (result.status === 'mfa_required') { renderMfaVerification(); return; } + throw new Error(`不支持的认证状态:${result.status || 'unknown'}`); + } + + function renderLogin() { + shell('平台管理员登录', '连接真实 PostgreSQL 与 ASP.NET Core API。', `
`); + document.querySelector('#platformLoginForm').addEventListener('submit', async event => { + event.preventDefault(); + const form = event.currentTarget; + setBusy(form, true); + try { + const result = await post('/api/auth/login/password', { realm: 'platform', identifier: form.elements.identifier.value.trim(), password: form.elements.password.value }); + await handleAuthenticationResult(result); + } catch (error) { showError(error); setBusy(form, false); } + }); + } + + function renderPasswordChange() { + shell('设置正式密码', '首次登录必须先替换临时密码。', `
`); + document.querySelector('#platformPasswordForm').addEventListener('submit', async event => { + event.preventDefault(); + const form = event.currentTarget; + if (form.elements.newPassword.value !== form.elements.confirmPassword.value) { showError(new Error('两次输入的密码不一致')); return; } + setBusy(form, true); + try { await handleAuthenticationResult(await post('/api/auth/password/change-required', { challengeToken, newPassword: form.elements.newPassword.value })); } + catch (error) { showError(error); setBusy(form, false); } + }); + } + + async function renderMfaEnrollment() { + const setup = await post('/api/auth/mfa/totp/setup', { challengeToken }); + shell('绑定双重验证', '在认证器中添加密钥,然后输入当前 6 位验证码。', `${escapeHtml(setup.sharedKey)}

也可在支持的认证器中导入:${escapeHtml(setup.authenticatorUri)}

`); + document.querySelector('#platformMfaForm').addEventListener('submit', async event => { + event.preventDefault(); + const form = event.currentTarget; + setBusy(form, true); + try { + const result = await post('/api/auth/mfa/totp/confirm', { challengeToken, code: form.elements.code.value.trim() }); + finishAuthentication(result.authentication, result.recoveryCodes || []); + } catch (error) { showError(error); setBusy(form, false); } + }); + } + + function renderMfaVerification() { + shell('双重验证', '输入认证器中的当前 6 位验证码。', `
`); + document.querySelector('#platformMfaForm').addEventListener('submit', async event => { + event.preventDefault(); + const form = event.currentTarget; + setBusy(form, true); + try { await handleAuthenticationResult(await post('/api/auth/mfa/totp/verify', { challengeToken, code: form.elements.code.value.trim() })); } + catch (error) { showError(error); setBusy(form, false); } + }); + } + + function clearSession() { + [ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, ACCESS_EXPIRES_KEY, USER_KEY].forEach(key => sessionStorage.removeItem(key)); + } + + function hasSession() { + const token = sessionStorage.getItem(ACCESS_TOKEN_KEY); + const expiresAt = Date.parse(sessionStorage.getItem(ACCESS_EXPIRES_KEY) || ''); + return Boolean(token) && Number.isFinite(expiresAt) && expiresAt > Date.now() + 15_000; + } + + async function requireSession() { + if (hasSession()) return true; + clearSession(); + renderLogin(); + return new Promise(resolve => { pendingResolve = resolve; }); + } + + async function logout() { + const refreshToken = sessionStorage.getItem(REFRESH_TOKEN_KEY); + try { if (refreshToken) await post('/api/auth/logout', { refreshToken }); } catch { /* Clear the browser session even when revocation cannot be reached. */ } + clearSession(); + location.reload(); + } + + window.GongxuePlatformAuth = Object.freeze({ + getAccessToken: () => sessionStorage.getItem(ACCESS_TOKEN_KEY) || '', + getUser: () => { + try { return JSON.parse(sessionStorage.getItem(USER_KEY) || 'null'); } catch { return null; } + }, + logout, + requireSession, + }); +})(); diff --git a/Tiku.Api/wwwroot/platform-admin/runtime-config.js b/Tiku.Api/wwwroot/platform-admin/runtime-config.js new file mode 100644 index 0000000..cc4d436 --- /dev/null +++ b/Tiku.Api/wwwroot/platform-admin/runtime-config.js @@ -0,0 +1,24 @@ +/* + * 平台端公开运行时配置。 + * + * 默认连接同源后端。其他部署环境可在本文件之前注入同名对象, + * 或在部署时替换本文件。不要把 access token、service role key 或任何服务端密钥 + * 写入静态文件;getAccessToken 应从当前平台登录会话中按需读取短期 JWT。 + * + * window.GONGXUE_PLATFORM_RUNTIME_CONFIG = { + * mode: 'api', + * apiBaseUrl: 'https://api.example.com', + * fallbackToMock: true, + * timeoutMs: 10000, + * getAccessToken: async () => { + * return sessionStorage.getItem('tiku_platform_access_token') || ''; + * }, + * }; + */ +window.GONGXUE_PLATFORM_RUNTIME_CONFIG = window.GONGXUE_PLATFORM_RUNTIME_CONFIG || { + mode: 'api', + apiBaseUrl: '', + fallbackToMock: false, + timeoutMs: 10000, + getAccessToken: async () => window.GongxuePlatformAuth?.getAccessToken() || '', +}; diff --git a/Tiku.Api/wwwroot/platform-admin/spec-contract.js b/Tiku.Api/wwwroot/platform-admin/spec-contract.js new file mode 100644 index 0000000..319f650 --- /dev/null +++ b/Tiku.Api/wwwroot/platform-admin/spec-contract.js @@ -0,0 +1,246 @@ +(() => { + const actions = [ + ['P-01', 'act.p-01.01', '进入租户/账务/题库/审计', 'command', 'native', 'nav', '[data-nav="tenants"],[data-nav="billing"],[data-nav="bank"],[data-nav="audit"]', 'PlatformAdminOverviewController_overview', 'allow'], + ['P-01', 'act.p-01.02', '确认/解决告警', 'command', 'native', 'mutation', '[data-alert-detail],[data-action="transition-alert"]', 'PlatformAdminAuditController_alerts', 'allow'], + ['P-01', 'act.p-01.03', '导出审计', 'async', 'native', 'task', '[data-action="export-audit"]', 'PlatformAdminAuditController_logs', 'allow'], + ['P-01', 'act.p-01.04', '刷新', 'command', 'native', 'read', '[data-action="refresh"]', 'PlatformAdminOverviewController_overview', 'allow'], + ['P-02', 'act.p-02.01', '搜索', 'command', 'native', 'read', '#tenantSearch,[data-action="apply-tenant-query"]', 'PlatformAdminTenantsController_list', 'allow'], + ['P-02', 'act.p-02.02', '创建租户', 'command', 'native', 'mutation', '[data-action="create-tenant"],[form="createTenantForm"]', 'PlatformAdminTenantsController_create', 'block'], + ['P-02', 'act.p-02.03', '打开详情', 'command', 'native', 'nav', '[data-tenant-detail]', 'PlatformAdminTenantsController_list', 'allow'], + ['P-02', 'act.p-02.04', '准备暂停/恢复', 'destructive', 'native', 'mutation', '[data-action="prepare-tenant-status"],[form="tenantStatusForm"]', 'PlatformAdminTenantsController_status', 'block'], + ['P-02', 'act.p-02.05', '去账务', 'command', 'native', 'nav', '[data-action="tenant-to-billing"]', 'PlatformAdminBillingController_invoices', 'allow'], + ['P-03', 'act.p-03.01', '修改状态', 'command', 'native', 'mutation', '[data-action="prepare-tenant-status"],[form="tenantStatusForm"]', 'PlatformAdminTenantsController_status', 'block'], + ['P-03', 'act.p-03.02', '编辑账务资料', 'command', 'native', 'nav', '[data-action="edit-billing-profile"]', 'PlatformAdminTenantsController_billingProfile', 'block'], + ['P-03', 'act.p-03.03', '创建订阅', 'command', 'native', 'mutation', '[data-action="create-subscription-for-tenant"],[form="subscriptionForm"]', 'PlatformAdminBillingController_createSubscription', 'block'], + ['P-03', 'act.p-03.04', '查看账单/用量', 'command', 'native', 'nav', '[data-action="tenant-to-billing"],[data-action="tenant-to-usage"]', 'PlatformAdminTenantsController_detail', 'allow'], + ['P-03', 'act.p-03.05', '跳转租户门户(只做链接,不冒充登录)', 'command', 'native', 'nav', '[data-action="open-tenant-portal"]', 'PlatformAdminTenantsController_detail', 'allow'], + ['P-04', 'act.p-04.01', '编辑', 'command', 'native', 'mutation', '#billingProfileForm', 'PlatformAdminTenantsController_billingProfile', 'block'], + ['P-04', 'act.p-04.02', '保存', 'command', 'native', 'mutation', '[form="billingProfileForm"]', 'PlatformAdminTenantsController_billingProfile', 'block'], + ['P-04', 'act.p-04.03', '查看关联账单', 'command', 'native', 'nav', '[data-action="billing-profile-to-invoices"]', 'PlatformAdminTenantsController_billingProfile', 'allow'], + ['P-05', 'act.p-05.01', '选择套餐', 'command', 'native', 'mutation', '[data-action="select-plan"]', 'PlatformAdminOverviewController_plans', 'block'], + ['P-05', 'act.p-05.02', '创建订阅', 'command', 'native', 'mutation', '[data-action="create-subscription"],[form="subscriptionForm"]', 'PlatformAdminBillingController_createSubscription', 'block'], + ['P-05', 'act.p-05.03', '进入账单候选', 'command', 'native', 'nav', '[data-action="subscription-to-billing"]', 'PlatformAdminBillingController_createSubscription', 'allow'], + ['P-05', 'act.p-05.04', '查看租户详情', 'command', 'native', 'nav', '[data-tenant-detail]', 'PlatformAdminBillingController_createSubscription', 'allow'], + ['P-06', 'act.p-06.01', '查询', 'command', 'contract', 'read', '[data-action="apply-invoice-query"]', 'PlatformAdminBillingController_invoices', 'allow'], + ['P-06', 'act.p-06.02', '手工创建', 'command', 'native', 'mutation', '[data-action="create-invoice"],[form="invoiceForm"]', 'PlatformAdminBillingController_createInvoice', 'block'], + ['P-06', 'act.p-06.03', '预览候选', 'command', 'native', 'read', '[data-action="preview-invoice-candidates"]', 'PlatformAdminBillingController_subscriptionCandidates', 'block'], + ['P-06', 'act.p-06.04', '单个生成', 'async', 'contract', 'task', '[data-action="generate-subscription-invoice"]', 'PlatformAdminBillingController_fromSubscription', 'block'], + ['P-06', 'act.p-06.05', '批量 dry-run', 'command', 'native', 'read', '[data-action="run-invoice-dry-run"]', 'PlatformAdminBillingController_fromSubscriptionsBatch', 'block'], + ['P-06', 'act.p-06.06', '确认生成', 'async', 'native', 'task', '[data-action="confirm-generate-candidates"]', 'PlatformAdminBillingController_fromSubscriptionsBatch', 'block'], + ['P-06', 'act.p-06.07', '打开租户', 'command', 'native', 'nav', '[data-tenant-detail]', 'PlatformAdminBillingController_invoices', 'allow'], + ['P-07', 'act.p-07.01', '记录用量', 'command', 'native', 'mutation', '[data-action="record-usage"],[form="usageForm"]', 'PlatformAdminBillingController_recordUsage', 'block'], + ['P-07', 'act.p-07.02', '查询', 'command', 'native', 'read', '#usagePeriodFilter', 'PlatformAdminBillingController_usage', 'allow'], + ['P-07', 'act.p-07.03', '预览超额候选', 'command', 'native', 'read', '[data-action="preview-overage"]', 'PlatformAdminBillingController_usageCandidates', 'block'], + ['P-07', 'act.p-07.04', 'dry-run', 'command', 'native', 'read', '[data-action="run-overage-dry-run"]', 'PlatformAdminBillingController_fromUsage', 'block'], + ['P-07', 'act.p-07.05', '生成超额账单', 'async', 'native', 'task', '[data-action="confirm-overage-invoices"]', 'PlatformAdminBillingController_fromUsage', 'block'], + ['P-07', 'act.p-07.06', '跳租户详情', 'command', 'native', 'nav', '[data-tenant-detail]', 'PlatformAdminBillingController_usage', 'allow'], + ['P-08', 'act.p-08.01', '确认收款', 'command', 'native', 'mutation', '[data-action="confirm-payment"],[form="paymentForm"]', 'PlatformAdminBillingController_confirmPayment', 'block'], + ['P-08', 'act.p-08.02', '预览逾期', 'command', 'native', 'read', '[data-action="preview-overdue"]', 'PlatformAdminBillingController_processOverdue', 'block'], + ['P-08', 'act.p-08.03', '生成催缴', 'async', 'native', 'task', '[data-action="confirm-overdue-process"],[data-action="generate-reminder"]', 'PlatformAdminBillingController_processOverdue', 'block'], + ['P-08', 'act.p-08.04', '查询提醒', 'command', 'native', 'read', '[data-billing-tab="reminders"]', 'PlatformAdminBillingController_reminders', 'allow'], + ['P-08', 'act.p-08.05', '新增/编辑/启停渠道', 'command', 'native', 'mutation', '[data-action="new-dunning-channel"],[data-action="edit-dunning-channel"],[data-action="toggle-dunning-channel"]', 'PlatformAdminDunningChannelsController_upsertChannel', 'block'], + ['P-08', 'act.p-08.06', '查看通知事件', 'command', 'contract', 'read', '[data-notification-detail]', 'PlatformAdminDunningEventsController_events', 'allow'], + ['P-09', 'act.p-09.01', '搜索', 'command', 'contract', 'read', '[data-action="apply-bank-search"]', 'PlatformAdminQuestionBanksController_banks', 'allow'], + ['P-09', 'act.p-09.02', '含租户库切换', 'command', 'native', 'read', '#includeTenantBanks', 'PlatformAdminQuestionBanksController_banks', 'allow'], + ['P-09', 'act.p-09.03', '新建/编辑/禁用授权', 'destructive', 'native', 'mutation', '[data-action="new-grant"],[data-action="edit-grant"],[data-action="toggle-grant"]', 'PlatformAdminQuestionBanksController_upsertGrant', 'block'], + ['P-09', 'act.p-09.04', '选择题库', 'command', 'native', 'read', '[data-bank-select]', 'PlatformAdminQuestionBanksController_banks', 'allow'], + ['P-09', 'act.p-09.05', '查看租户采用/同步', 'async', 'contract', 'task', '[data-action="preview-bank-sync"]', 'PlatformAdminQuestionBanksController_syncStatus', 'allow'], + ['P-10', 'act.p-10.01', '新建/编辑', 'command', 'native', 'mutation', '[data-action="new-staff"],[data-action="edit-staff"]', 'PlatformAdminOverviewController_upsertStaff', 'block'], + ['P-10', 'act.p-10.02', '全选/分组授权', 'command', 'native', 'mutation', '[data-action="select-permission-group"]', 'PlatformAdminOverviewController_permissions', 'block'], + ['P-10', 'act.p-10.03', '启用/禁用', 'destructive', 'native', 'mutation', '[data-action="toggle-staff"]', 'PlatformAdminOverviewController_updateStaffStatus', 'block'], + ['P-10', 'act.p-10.04', '禁用时撤销会话', 'destructive', 'native', 'audit', '[data-action="execute-staff-disable"]', 'PlatformAdminOverviewController_updateStaffStatus', 'block'], + ['P-10', 'act.p-10.05', '清空表单', 'command', 'contract', 'mutation', '[data-action="clear-staff-form"]', 'PlatformAdminOverviewController_upsertStaff', 'block'], + ['P-11', 'act.p-11.01', '筛选', 'command', 'native', 'read', '#auditSearch,#auditRangeFilter,[data-audit-severity]', 'PlatformAdminAuditController_logs', 'allow'], + ['P-11', 'act.p-11.02', '查看详情', 'command', 'native', 'read', '[data-audit-detail]', 'PlatformAdminAuditController_logs', 'allow'], + ['P-11', 'act.p-11.03', '导出 CSV/JSON', 'async', 'native', 'task', '[data-action="export-audit"]', 'PlatformAdminAuditController_exportLogs', 'allow'], + ['P-11', 'act.p-11.04', '复制 ID', 'command', 'native', 'read', '[data-copy]', 'PlatformAdminAuditController_logs', 'allow'], + ['P-11', 'act.p-11.05', '跳目标对象', 'command', 'native', 'nav', '[data-action="jump-audit-target"],[data-action="jump-related-alert"]', 'PlatformAdminAuditController_logs', 'allow'], + ['P-12', 'act.p-12.01', '确认/解决告警', 'command', 'native', 'mutation', '[data-action="transition-alert"]', 'PlatformAdminAuditController_updateAlert', 'allow'], + ['P-12', 'act.p-12.02', '查看证据', 'command', 'native', 'read', '[data-alert-detail]', 'PlatformAdminAuditController_alerts', 'allow'], + ['P-12', 'act.p-12.03', '新建/编辑/启停通知渠道', 'command', 'native', 'mutation', '[data-action="new-audit-channel"],[data-action="edit-audit-channel"],[data-action="toggle-audit-channel"]', 'PlatformAdminAuditController_upsertChannel', 'block'], + ['P-12', 'act.p-12.04', '查看失败事件', 'command', 'contract', 'read', '[data-notification-detail]', 'PlatformAdminAuditController_events', 'allow'], + ].map(([pageId, id, label, kind, mode, effect, selector, operationId, mobile]) => ({ pageId, id, label, kind, mode, effect, selector, operationIds: [operationId], mobile, destructive: kind === 'destructive', implemented: true })); + + const operationRows = [ + ['P-01', 'PlatformAdminOverviewController_overview', 'GET', '/api/platform-admin/overview'], + ['P-01', 'PlatformAdminAuditController_alerts', 'GET', '/api/platform-admin/audit-alerts'], + ['P-01', 'PlatformAdminAuditController_logs', 'GET', '/api/platform-admin/audit-logs'], + ['P-02', 'PlatformAdminTenantsController_list', 'GET', '/api/platform-admin/tenants'], + ['P-02', 'PlatformAdminTenantsController_create', 'POST', '/api/platform-admin/tenants'], + ['P-03', 'PlatformAdminTenantsController_detail', 'GET', '/api/platform-admin/tenants/detail'], + ['P-03', 'PlatformAdminTenantsController_status', 'PATCH', '/api/platform-admin/tenants/status'], + ['P-03', 'PlatformAdminTenantsController_billingProfile', 'PUT', '/api/platform-admin/tenants/billing-profile'], + ['P-03', 'PlatformAdminBillingController_createSubscription', 'POST', '/api/platform-admin/subscriptions'], + ['P-04', 'PlatformAdminTenantsController_billingProfile', 'PUT', '/api/platform-admin/tenants/billing-profile'], + ['P-05', 'PlatformAdminOverviewController_plans', 'GET', '/api/platform-admin/plans'], + ['P-05', 'PlatformAdminBillingController_createSubscription', 'POST', '/api/platform-admin/subscriptions'], + ['P-06', 'PlatformAdminBillingController_invoices', 'GET', '/api/platform-admin/invoices'], + ['P-06', 'PlatformAdminBillingController_createInvoice', 'POST', '/api/platform-admin/invoices'], + ['P-06', 'PlatformAdminBillingController_subscriptionCandidates', 'GET', '/api/platform-admin/invoices/subscription-candidates'], + ['P-06', 'PlatformAdminBillingController_fromSubscription', 'POST', '/api/platform-admin/invoices/from-subscription'], + ['P-06', 'PlatformAdminBillingController_fromSubscriptionsBatch', 'POST', '/api/platform-admin/invoices/from-subscriptions-batch'], + ['P-07', 'PlatformAdminBillingController_usage', 'GET', '/api/platform-admin/usage'], + ['P-07', 'PlatformAdminBillingController_recordUsage', 'POST', '/api/platform-admin/usage'], + ['P-07', 'PlatformAdminBillingController_usageCandidates', 'GET', '/api/platform-admin/invoices/usage-overage-candidates'], + ['P-07', 'PlatformAdminBillingController_fromUsage', 'POST', '/api/platform-admin/invoices/from-usage-overage'], + ['P-08', 'PlatformAdminBillingController_confirmPayment', 'POST', '/api/platform-admin/invoices/payments/manual-confirm'], + ['P-08', 'PlatformAdminBillingController_processOverdue', 'POST', '/api/platform-admin/invoices/process-overdue'], + ['P-08', 'PlatformAdminBillingController_reminders', 'GET', '/api/platform-admin/invoices/reminders'], + ['P-08', 'PlatformAdminDunningChannelsController_channels', 'GET', '/api/platform-admin/dunning-notification-channels'], + ['P-08', 'PlatformAdminDunningChannelsController_upsertChannel', 'PUT', '/api/platform-admin/dunning-notification-channels'], + ['P-08', 'PlatformAdminDunningEventsController_events', 'GET', '/api/platform-admin/dunning-notification-events'], + ['P-09', 'PlatformAdminQuestionBanksController_banks', 'GET', '/api/platform-admin/question-banks'], + ['P-09', 'PlatformAdminQuestionBanksController_grants', 'GET', '/api/platform-admin/question-bank-grants'], + ['P-09', 'PlatformAdminQuestionBanksController_upsertGrant', 'PUT', '/api/platform-admin/question-bank-grants'], + ['P-09', 'PlatformAdminQuestionBanksController_syncStatus', 'GET', '/api/platform-admin/question-bank-sync-status'], + ['P-10', 'PlatformAdminOverviewController_permissions', 'GET', '/api/platform-admin/permissions'], + ['P-10', 'PlatformAdminOverviewController_staff', 'GET', '/api/platform-admin/staff'], + ['P-10', 'PlatformAdminOverviewController_upsertStaff', 'PUT', '/api/platform-admin/staff'], + ['P-10', 'PlatformAdminOverviewController_updateStaffStatus', 'PATCH', '/api/platform-admin/staff/status'], + ['P-11', 'PlatformAdminAuditController_logs', 'GET', '/api/platform-admin/audit-logs'], + ['P-11', 'PlatformAdminAuditController_exportLogs', 'GET', '/api/platform-admin/audit-logs/export'], + ['P-12', 'PlatformAdminAuditController_rules', 'GET', '/api/platform-admin/audit-alert-rules'], + ['P-12', 'PlatformAdminAuditController_alerts', 'GET', '/api/platform-admin/audit-alerts'], + ['P-12', 'PlatformAdminAuditController_updateAlert', 'POST', '/api/platform-admin/audit-alerts/status'], + ['P-12', 'PlatformAdminAuditController_channels', 'GET', '/api/platform-admin/audit-notification-channels'], + ['P-12', 'PlatformAdminAuditController_upsertChannel', 'PUT', '/api/platform-admin/audit-notification-channels'], + ['P-12', 'PlatformAdminAuditController_events', 'GET', '/api/platform-admin/audit-notification-events'], + ]; + const operations = operationRows.map(([pageId, operationId, method, path]) => ({ pageId, operationId, method, path })); + const operationById = Object.fromEntries(operations.map(operation => [operation.operationId, operation])); + + const navEdges = [ + ['nav.p-01.p-02', 'P-01', 'P-02', '[data-nav="tenants"]'], + ['nav.p-01.p-06', 'P-01', 'P-06', '[data-nav="billing"]'], + ['nav.p-01.p-12', 'P-01', 'P-12', '[data-nav="alerts"],[data-alert-detail]'], + ['nav.p-02.p-03', 'P-02', 'P-03', '[data-tenant-detail]'], + ['nav.p-03.p-04', 'P-03', 'P-04', '[data-action="edit-billing-profile"]'], + ['nav.p-03.p-05', 'P-03', 'P-05', '[data-action="create-subscription-for-tenant"]'], + ['nav.p-03.p-06', 'P-03', 'P-06', '[data-action="tenant-to-billing"]'], + ['nav.p-05.p-06', 'P-05', 'P-06', '[data-action="subscription-to-billing"]'], + ['nav.p-05.p-07', 'P-05', 'P-07', '[data-action="subscriptions-to-usage"]'], + ['nav.p-06.p-08', 'P-06', 'P-08', '[data-nav="dunning"]'], + ['nav.p-11.p-12', 'P-11', 'P-12', '[data-action="jump-related-alert"]'], + ].map(([id, fromPageId, toPageId, selector]) => ({ id, fromPageId, toPageId, selector, implemented: true })); + + const supportingReads = [ + { pageId: 'P-09', selector: '[data-action="sync-detail"],[data-action="refresh-bank-sync"]', operationId: 'PlatformAdminQuestionBanksController_syncStatus' }, + ]; + + const trace = []; + const decoratedActionIds = new Set(); + const selectorCollisions = new Set(); + const explicitResultActionIds = new Set(['act.p-06.01', 'act.p-06.04', 'act.p-09.01', 'act.p-09.05']); + function pushTrace(entry) { + trace.push({ at: new Date().toISOString(), ...entry }); + if (trace.length > 120) trace.splice(0, trace.length - 120); + document.documentElement.dataset.platformContractEvents = String(trace.length); + } + + function decorate(root = document, pageId = '') { + const shell = root.querySelector?.('.page-shell'); + if (shell && pageId) { + const pageOperations = operations.filter(operation => operation.pageId === pageId); + shell.dataset.specPageId = pageId; + shell.dataset.operationIds = pageOperations.map(operation => operation.operationId).join(' '); + shell.dataset.operationContracts = pageOperations.map(operation => `${operation.method} ${operation.path}`).join(' | '); + } + actions.filter(action => action.pageId === pageId).forEach(action => { + root.querySelectorAll?.(action.selector).forEach(element => { + const existingActionId = element.dataset.specActionId; + if (existingActionId && existingActionId !== action.id) selectorCollisions.add(`${existingActionId} -> ${action.id}`); + element.dataset.specActionId = action.id; + element.dataset.specActionMode = action.mode; + element.dataset.specEffect = action.effect; + element.dataset.mobileBoundary = action.mobile; + const operation = operationById[action.operationIds[0]]; + if (operation) { + element.dataset.operationId = operation.operationId; + element.dataset.operationMethod = operation.method; + element.dataset.operationPath = operation.path; + } + decoratedActionIds.add(action.id); + }); + }); + supportingReads.filter(read => read.pageId === pageId).forEach(read => { + const operation = operationById[read.operationId]; + root.querySelectorAll?.(read.selector).forEach(element => { + element.dataset.specSupportingRead = 'true'; + element.dataset.specEffect = 'read'; + if (operation) { + element.dataset.operationId = operation.operationId; + element.dataset.operationMethod = operation.method; + element.dataset.operationPath = operation.path; + } + }); + }); + navEdges.filter(edge => edge.fromPageId === pageId).forEach(edge => { + root.querySelectorAll?.(edge.selector).forEach(element => { + element.dataset.navEdgeId = edge.id; + element.dataset.navTargetPage = edge.toPageId; + }); + }); + window.__PLATFORM_SPEC_COVERAGE__ = coverage(); + } + + function eventContract(target) { + const element = target?.closest?.('[data-spec-action-id]'); + if (!element) return null; + return { element, actionId: element.dataset.specActionId, operationId: element.dataset.operationId || '', requestId: `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}` }; + } + + document.addEventListener('click', event => { + const contract = eventContract(event.target); + if (!contract) return; + contract.element.dataset.contractRequestId = contract.requestId; + pushTrace({ phase: 'before', actionId: contract.actionId, operationId: contract.operationId, requestId: contract.requestId }); + setTimeout(() => pushTrace({ phase: 'after', actionId: contract.actionId, operationId: contract.operationId, requestId: contract.requestId, result: 'handled', pageId: document.querySelector('.page-shell')?.dataset.specPageId || '' }), 0); + }, true); + + document.addEventListener('change', event => { + const contract = eventContract(event.target); + if (!contract) return; + pushTrace({ phase: 'after', actionId: contract.actionId, operationId: contract.operationId, requestId: contract.requestId, result: 'changed' }); + }, true); + + const coverage = () => ({ + expected: 59, + total: actions.length, + unique: new Set(actions.map(action => action.id)).size, + catalogued: actions.filter(action => action.label && action.kind).length, + selectorDeclared: actions.filter(action => action.selector).length, + nativeMetadata: actions.filter(action => action.mode === 'native').length, + contractMetadata: actions.filter(action => action.mode === 'contract').length, + nativeMapped: actions.filter(action => action.mode === 'native' && decoratedActionIds.has(action.id)).length, + dedicatedExecutor: explicitResultActionIds.size, + dedicatedExecutorDefinition: 'actions with explicit success/failure result recording', + reachable: actions.filter(action => action.implemented && action.selector).length, + mountedThisSession: decoratedActionIds.size, + decoratedInSession: decoratedActionIds.size, + notYetDecorated: actions.filter(action => !decoratedActionIds.has(action.id)).map(action => action.id), + implemented: actions.filter(action => action.implemented && action.selector).length, + unimplemented: actions.filter(action => !action.implemented || !action.selector).map(action => action.id), + selectorCollisions: Array.from(selectorCollisions), + apiExact: operations.filter(operation => operation.operationId && operation.method && operation.path).length, + uiOnlyExplained: actions.filter(action => action.operationIds.length === 0).length, + effectDeclared: actions.filter(action => action.effect).length, + mobileDeclared: actions.filter(action => action.mobile).length, + actuallyExecuted: new Set(trace.filter(entry => entry.phase === 'result' && entry.result === 'success').map(entry => entry.actionId)).size, + executionNote: 'catalogue and selector metadata are not counted as successful business execution', + navigation: { expected: 11, total: navEdges.length, unique: new Set(navEdges.map(edge => edge.id)).size }, + pageBindings: { expected: 43, total: operations.length, unique: new Set(operations.map(operation => `${operation.pageId}:${operation.operationId}`)).size }, + }); + const assertions = { + pages: new Set(actions.map(action => action.pageId)).size === 12, + actionIds: actions.every(action => action.id.startsWith(`act.${action.pageId.toLowerCase()}.`)), + duplicateActions: new Set(actions.map(action => action.id)).size === actions.length, + operationContracts: operations.every(operation => /^(GET|POST|PUT|PATCH|DELETE)$/.test(operation.method) && operation.path.startsWith('/api/')), + navTargets: navEdges.every(edge => actions.some(action => action.pageId === edge.fromPageId) && actions.some(action => action.pageId === edge.toPageId)), + }; + window.GONGXUE_PLATFORM_SPEC = { actions, operations, navEdges, supportingReads }; + window.__GONGXUE_ACTION_CONTRACT__ = { coverage, assertions, trace }; + window.decoratePlatformSpec = decorate; + window.recordPlatformSpecResult = (actionId, result = {}) => pushTrace({ phase: 'result', actionId, ...result }); + window.__PLATFORM_CONTRACT_EVENTS__ = trace; + window.__PLATFORM_SPEC_COVERAGE__ = coverage(); +})(); diff --git a/Tiku.Api/wwwroot/platform-admin/styles.css b/Tiku.Api/wwwroot/platform-admin/styles.css new file mode 100644 index 0000000..87a5542 --- /dev/null +++ b/Tiku.Api/wwwroot/platform-admin/styles.css @@ -0,0 +1,50 @@ +:root{--navy:#111827;--navy-2:#1d2939;--blue:#1455ff;--blue-50:#f3f6ff;--blue-100:#e7edff;--ink:#101828;--ink-2:#344054;--muted:#667085;--line:#e4e7ec;--canvas:#f5f6f8;--green:#159455;--orange:#ed8b00;--red:#d92d20;--purple:#6941c6;--cyan:#088ab2;--sidebar:230px;font-family:Inter,"PingFang SC","Microsoft YaHei",system-ui,sans-serif;color:var(--ink);background:var(--canvas);font-synthesis:none}*{box-sizing:border-box}html,body{margin:0;min-height:100%;background:var(--canvas)}body{min-width:320px;letter-spacing:0}button,input,select,textarea{font:inherit;letter-spacing:0}button{cursor:pointer;color:inherit}button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid rgba(20,85,255,.2);outline-offset:2px}svg{width:19px;height:19px}.platform-app{min-height:100vh;display:grid;grid-template-columns:var(--sidebar) minmax(0,1fr)}.platform-sidebar{position:fixed;inset:0 auto 0 0;z-index:30;width:var(--sidebar);display:flex;flex-direction:column;padding:17px 14px;background:var(--navy);color:#d0d5dd;overflow:auto}.platform-brand{height:53px;display:flex;align-items:center;gap:10px;padding:4px 7px;border:0;background:transparent;color:#fff;text-align:left}.platform-brand img{width:37px;height:37px;border-radius:7px}.platform-brand span{display:grid;gap:2px}.platform-brand strong{font-size:15px}.platform-brand small{color:#98a2b3;font-size:8px}.environment{height:34px;display:grid;grid-template-columns:8px 1fr auto;align-items:center;gap:7px;margin:13px 6px 21px;padding:0 9px;border:1px solid #344054;background:#182230;font-size:8px}.environment i{width:7px;height:7px;border-radius:50%;background:#32d583;box-shadow:0 0 0 3px rgba(50,213,131,.1)}.environment b{color:#98a2b3;font-size:7px}#sideNav{display:grid;gap:19px}#sideNav section{display:grid;gap:3px}#sideNav h2{margin:0 10px 5px;color:#667085;font-size:7.5px;text-transform:uppercase}#sideNav button,.platform-foot>button{position:relative;width:100%;height:40px;display:grid;grid-template-columns:20px 1fr auto;align-items:center;gap:8px;padding:0 10px;border:0;border-radius:5px;background:transparent;color:#98a2b3;text-align:left;font-size:10px;font-weight:650}#sideNav button:hover,.platform-foot>button:hover{background:#1d2939;color:#fff}#sideNav button.is-active{background:#23335d;color:#fff}#sideNav button.is-active:before{content:"";position:absolute;left:-14px;width:3px;height:25px;background:#6b8cff}#sideNav svg,.platform-foot svg{width:16px}#sideNav b{min-width:18px;height:18px;display:grid;place-items:center;border-radius:9px;background:#d92d20;color:#fff;font-size:7px}.platform-foot{margin-top:auto;padding-top:14px;border-top:1px solid #344054}.platform-foot>div{height:54px;display:grid;grid-template-columns:31px 1fr 24px;align-items:center;gap:8px;margin:8px 4px 0}.platform-foot>div>span{width:30px;height:30px;display:grid;place-items:center;border-radius:50%;background:#344054;color:#fff;font-size:9px}.platform-foot p{display:grid;gap:3px;margin:0}.platform-foot strong{color:#fff;font-size:9px}.platform-foot small{color:#667085;font-size:7.5px}.platform-foot>div button{border:0;background:transparent;color:#667085}.platform-foot>div svg{width:14px}.platform-workspace{grid-column:2;min-width:0}.platform-topbar{position:sticky;top:0;z-index:20;height:66px;display:flex;align-items:center;gap:22px;padding:0 30px;border-bottom:1px solid var(--line);background:rgba(255,255,255,.96);backdrop-filter:blur(12px)}.page-context{display:grid;gap:3px;min-width:190px}.page-context small{color:#98a2b3;font-size:7px}.page-context strong{font-size:11px}.platform-search{width:min(470px,37vw);height:37px;display:flex;align-items:center;gap:8px;padding:0 10px;border:1px solid transparent;background:#f3f5f7}.platform-search:focus-within{border-color:#a9c0ff;background:#fff}.platform-search svg{width:15px;color:var(--muted)}.platform-search input{min-width:0;width:100%;border:0;outline:0;background:transparent;font-size:9px}.platform-search kbd{padding:2px 5px;border:1px solid var(--line);background:#fff;color:var(--muted);font-size:7px}.top-status{margin-left:auto;display:flex;align-items:center;gap:13px}.top-status>span{display:flex;align-items:center;gap:6px;color:var(--green);font-size:8px}.top-status>span i{width:7px;height:7px;border-radius:50%;background:#32d583}.icon-button{position:relative;width:36px;height:36px;display:grid;place-items:center;border:1px solid var(--line);background:#fff}.icon-button svg{width:16px}.icon-button b{position:absolute;right:-3px;top:-4px;min-width:17px;height:17px;display:grid;place-items:center;border:2px solid #fff;border-radius:9px;background:var(--red);color:#fff;font-size:7px}.mobile-menu{display:none}#mainView{min-height:calc(100vh - 66px);padding:26px 30px 48px}.page-shell{width:min(1540px,100%);margin:0 auto}.page-head{min-height:52px;display:flex;align-items:flex-start;justify-content:space-between;gap:20px;margin-bottom:18px}.page-head h1{margin:0;font-size:21px}.page-head p{margin:5px 0 0;color:var(--muted);font-size:9.5px}.page-head>div:last-child{display:flex;gap:8px}.button{min-height:36px;display:inline-flex;align-items:center;justify-content:center;gap:7px;padding:0 12px;border:1px solid #d0d5dd;background:#fff;font-size:9px;font-weight:650}.button:hover{border-color:#9db9ff;color:var(--blue)}.button.primary{border-color:var(--blue);background:var(--blue);color:#fff}.button.danger{border-color:#ffb4ae;color:var(--red)}.button svg{width:14px}.control{height:36px;border:1px solid var(--line);background:#fff;padding:0 9px;font-size:8px}.link{padding:0;border:0;background:transparent;color:var(--blue);font-size:8px;font-weight:700}.metric-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:11px}.metric{min-height:112px;display:grid;grid-template-columns:40px 1fr;align-items:center;gap:12px;padding:17px;border:1px solid var(--line);background:#fff}.metric-icon{width:38px;height:38px;display:grid;place-items:center;border-radius:6px;background:var(--blue-100);color:var(--blue)}.metric-icon.green{background:#edf9f2;color:var(--green)}.metric-icon.orange{background:#fff5e8;color:var(--orange)}.metric-icon.red{background:#fff0ef;color:var(--red)}.metric-icon.purple{background:#f3f0ff;color:var(--purple)}.metric-icon.cyan{background:#edfafd;color:var(--cyan)}.metric-icon svg{width:17px}.metric div{display:grid;gap:5px}.metric small,.metric p{margin:0;color:var(--muted);font-size:7.5px}.metric strong{font-size:20px}.panel,.table-panel{border:1px solid var(--line);background:#fff}.panel-head{min-height:60px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 17px;border-bottom:1px solid var(--line)}.panel-head h2{margin:0;font-size:11px}.panel-head span{display:block;margin-top:4px;color:var(--muted);font-size:7.5px}.overview-grid{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(310px,.72fr);gap:11px;margin-top:11px}.revenue-panel{min-width:0}.legend{display:flex;gap:12px;color:var(--muted);font-size:7.5px}.legend span{display:flex!important;align-items:center;gap:5px}.legend i{width:7px;height:7px;border-radius:50%}.legend .blue{background:var(--blue)}.legend .green{background:var(--green)}.revenue-chart{height:278px;display:grid;grid-template-columns:38px 1fr;padding:24px 18px 18px}.chart-axis{display:flex;flex-direction:column;justify-content:space-between;color:#98a2b3;font-size:7px}.chart-plot{position:relative}.chart-plot .grid{position:absolute;inset:0 0 20px;background:repeating-linear-gradient(to bottom,var(--line) 0,var(--line) 1px,transparent 1px,transparent 33.333%)}.bars{position:absolute;inset:0;display:flex;align-items:end;gap:8px}.bars>span{height:100%;flex:1;display:flex;align-items:end;justify-content:center;gap:3px;position:relative;padding-bottom:20px}.bars i{width:45%;background:var(--blue);opacity:.78}.bars b{width:18%;background:var(--green)}.bars em{position:absolute;bottom:0;color:#98a2b3;font-size:6.5px;font-style:normal}.alert-row{width:100%;min-height:62px;display:grid;grid-template-columns:34px minmax(0,1fr) 14px;align-items:center;gap:9px;padding:9px 14px;border:0;border-bottom:1px solid var(--line);background:#fff;text-align:left}.alert-row:last-child{border-bottom:0}.alert-row:hover{background:#fafbfc}.alert-row>span{width:32px;height:32px;display:grid;place-items:center;border-radius:6px;background:#fff5e8;color:var(--orange)}.alert-row svg{width:15px}.alert-row div{display:grid;gap:4px}.alert-row strong{font-size:8.5px}.alert-row small{color:var(--muted);font-size:7px}.alert-row>svg{color:#98a2b3}.overview-lower{display:grid;grid-template-columns:.8fr 1fr 1fr;gap:11px;margin-top:11px}.tenant-health{min-height:192px;display:grid;grid-template-columns:140px 1fr;align-items:center;gap:10px;padding:17px}.health-donut{width:118px;height:118px;display:grid;place-items:center;align-content:center;gap:2px;border-radius:50%;background:conic-gradient(var(--blue) 79%,#6b8cff 0 89%,#a9bbff 0 95%,#e4e7ec 0);position:relative}.health-donut:before{content:"";position:absolute;width:82px;height:82px;border-radius:50%;background:#fff}.health-donut strong,.health-donut span{position:relative}.health-donut strong{font-size:19px}.health-donut span{color:var(--muted);font-size:7px}.tenant-health>div:last-child{display:grid;gap:10px}.tenant-health p{display:grid;grid-template-columns:55px 1fr 20px;align-items:center;gap:7px;margin:0;font-size:7.5px}.tenant-health p i{height:4px;background:#edf0f4}.tenant-health p i b{display:block;height:100%;background:var(--blue)}.usage-summary{display:grid;grid-template-columns:repeat(3,1fr);padding:18px}.usage-summary>div{display:grid;gap:6px;padding:0 13px;border-right:1px solid var(--line)}.usage-summary>div:first-child{padding-left:0}.usage-summary>div:last-child{border-right:0}.usage-summary span,.usage-summary small{color:var(--muted);font-size:7px}.usage-summary strong{font-size:15px}.usage-summary i,.usage-kpis i,.usage-cell i,.drawer-usage i{height:4px;background:#edf0f4}.usage-summary i b,.usage-kpis i b,.usage-cell i b,.drawer-usage i b{display:block;height:100%;background:var(--blue)}.platform-activity{display:grid;padding:7px 14px}.platform-activity>div{min-height:51px;display:grid;grid-template-columns:27px 1fr;align-items:center;gap:8px;border-bottom:1px solid var(--line)}.platform-activity>div:last-child{border-bottom:0}.platform-activity>div>span{width:26px;height:26px;display:grid;place-items:center;border-radius:50%;background:#f2f4f7;font-size:7px}.platform-activity p{display:grid;gap:4px;margin:0;font-size:7.5px}.platform-activity small{color:var(--muted);font-size:6.5px}.filter-bar{min-height:59px;display:flex;align-items:center;gap:8px;padding:10px 12px;border:1px solid var(--line);background:#fff}.filter-bar label{min-width:300px;height:36px;display:flex;align-items:center;gap:7px;padding:0 9px;border:1px solid var(--line);background:#f8fafc}.filter-bar label svg{width:14px;color:var(--muted)}.filter-bar input{min-width:0;width:100%;border:0;outline:0;background:transparent;font-size:8px}.filter-bar select{height:36px;border:1px solid var(--line);background:#fff;padding:0 28px 0 9px;font-size:8px}.filter-segment{display:flex;gap:3px;margin-left:auto;padding:3px;background:#f2f4f7}.filter-segment button{height:28px;padding:0 9px;border:0;background:transparent;color:var(--muted);font-size:7px;white-space:nowrap}.filter-segment button.is-active{background:#fff;color:var(--blue);box-shadow:0 1px 4px rgba(16,24,40,.08);font-weight:700}.table-panel{overflow:auto;border-top:0}.platform-table{width:100%;min-width:1040px;border-collapse:collapse}.platform-table th{height:42px;padding:0 12px;background:#f8fafc;color:var(--muted);font-size:7px;font-weight:650;text-align:left}.platform-table td{height:65px;padding:8px 12px;border-top:1px solid var(--line);font-size:8px;vertical-align:middle}.platform-table tr:hover td{background:#fcfcfd}.platform-table td>strong,.platform-table td>small{display:block}.platform-table td>small{margin-top:4px;color:var(--muted);font-size:6.5px}.tenant-cell{display:flex;align-items:center;gap:9px}.tenant-cell>span{width:31px;height:31px;display:grid;place-items:center;border-radius:6px;background:var(--blue-100);color:var(--blue);font-size:8px;font-weight:800}.tenant-cell>div{display:grid;gap:4px}.tenant-cell strong{font-size:8.5px}.tenant-cell small{color:var(--muted);font-size:6.5px}.status{display:inline-flex;align-items:center;justify-content:center;min-height:21px;padding:0 7px;border-radius:4px;font-size:6.5px!important;font-style:normal;font-weight:700;white-space:nowrap}.status.success{background:#edf9f2;color:var(--green)}.status.warning{background:#fff5e8;color:#b96700}.status.danger{background:#fff0ef;color:var(--red)}.status.neutral{background:#f2f4f7;color:var(--muted)}.money-due{color:var(--red)}.row-action{width:28px;height:28px;display:grid;place-items:center;border:0;background:transparent;color:var(--muted)}.row-action:hover{background:var(--blue-100);color:var(--blue)}.row-action svg{width:13px}.table-panel>footer{height:48px;display:flex;align-items:center;justify-content:space-between;padding:0 13px;border-top:1px solid var(--line);color:var(--muted);font-size:7px}.table-panel>footer button{display:flex;align-items:center;gap:5px;border:0;background:transparent;color:var(--blue);font-size:7px}.table-panel>footer svg{width:12px}.drawer-backdrop{position:fixed;z-index:60;inset:0;background:rgba(16,24,40,.28)}.tenant-drawer{position:absolute;right:0;top:0;bottom:0;width:min(620px,100%);display:flex;flex-direction:column;background:#fff;box-shadow:-18px 0 50px rgba(16,24,40,.18);overflow:auto}.tenant-drawer>header{min-height:92px;display:flex;align-items:center;justify-content:space-between;padding:16px 21px;border-bottom:1px solid var(--line)}.tenant-drawer>header>div{display:flex;align-items:center;gap:12px}.tenant-drawer>header>div>span{width:45px;height:45px;display:grid;place-items:center;border-radius:8px;background:var(--blue-100);color:var(--blue);font-size:12px;font-weight:800}.tenant-drawer header small{color:var(--blue);font-size:7px}.tenant-drawer h2{margin:4px 0;font-size:15px}.tenant-drawer header p{margin:0;color:var(--muted);font-size:7px}.tenant-drawer header>button{border:0;background:transparent}.tenant-drawer header svg{width:17px}.drawer-status{display:grid!important;grid-template-columns:repeat(4,1fr);padding:0!important;border-bottom:1px solid var(--line)}.drawer-status>div{min-height:73px;display:grid;align-content:center;gap:6px;padding:13px;border-right:1px solid var(--line)}.drawer-status>div:last-child{border-right:0}.drawer-status span{color:var(--muted);font-size:7px}.drawer-status strong{font-size:8px}.drawer-tabs{height:43px;display:flex;gap:16px;padding:0 20px;border-bottom:1px solid var(--line)}.drawer-tabs button{border:0;background:transparent;color:var(--muted);font-size:7.5px}.drawer-tabs button.is-active{color:var(--blue);border-bottom:2px solid var(--blue);font-weight:700}.tenant-drawer>section{padding:19px 21px;border-bottom:1px solid var(--line)}.tenant-drawer>section h3{margin:0 0 12px;font-size:10px}.tenant-drawer dl{display:grid;grid-template-columns:1fr 1fr;gap:13px;margin:0}.tenant-drawer dl div{display:grid;gap:4px}.tenant-drawer dt{color:var(--muted);font-size:7px}.tenant-drawer dd{margin:0;font-size:8px}.domain-item{min-height:49px;display:grid;grid-template-columns:24px 1fr auto;align-items:center;gap:8px;border-bottom:1px solid var(--line)}.domain-item>svg{width:15px;color:var(--blue)}.domain-item span{display:grid;gap:4px}.domain-item strong{font-size:8px}.domain-item small{color:var(--muted);font-size:6.5px}.drawer-usage{display:grid;gap:13px}.drawer-usage>div{display:grid;grid-template-columns:80px 1fr;align-items:center;gap:6px 12px}.drawer-usage span{color:var(--muted);font-size:7px}.drawer-usage strong{font-size:8px}.drawer-usage i{grid-column:2}.tenant-drawer>footer{position:sticky;bottom:0;display:flex;justify-content:flex-end;gap:7px;margin-top:auto;padding:12px 20px;border-top:1px solid var(--line);background:#fff}.lifecycle-summary{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}.lifecycle-summary article{min-height:92px;display:grid;grid-template-columns:42px 1fr 20px;align-items:center;gap:10px;padding:15px;border:1px solid var(--line);background:#fff}.stage-number{width:40px;height:40px;display:grid;place-items:center;border-radius:50%;background:var(--blue-100);color:var(--blue);font-size:15px;font-weight:800}.stage-number.risk{background:#fff0ef;color:var(--red)}.lifecycle-summary article>div{display:grid;gap:5px}.lifecycle-summary strong{font-size:9px}.lifecycle-summary small{color:var(--muted);font-size:7px}.lifecycle-summary button{border:0;background:transparent;color:#98a2b3}.lifecycle-summary svg{width:13px}.lifecycle-grid{display:grid;grid-template-columns:1.2fr .8fr;gap:11px;margin-top:11px}.lifecycle-row{min-height:64px;display:grid;grid-template-columns:31px minmax(0,1fr) 42px auto;align-items:center;gap:9px;padding:8px 15px;border-bottom:1px solid var(--line)}.lifecycle-row>span{width:29px;height:29px;display:grid;place-items:center;border-radius:6px;background:var(--blue-100);color:var(--blue);font-size:8px}.lifecycle-row>div{display:grid;gap:4px}.lifecycle-row strong{font-size:8px}.lifecycle-row small{color:var(--muted);font-size:6.5px}.lifecycle-row>b{color:var(--orange);font-size:8px}.setup-card{padding:17px}.setup-card header{display:flex;justify-content:space-between;margin-bottom:12px}.setup-card header span{font-size:9px;font-weight:700}.setup-card header b{color:var(--blue);font-size:8px}.setup-card p{display:flex;align-items:center;gap:7px;margin:0;min-height:35px;color:var(--muted);font-size:7.5px}.setup-card p.done{color:var(--green)}.setup-card p svg{width:14px}.setup-card .button{width:100%;margin-top:12px}.plan-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:11px}.plan-card{min-height:330px;padding:20px;border:1px solid var(--line);background:#fff}.plan-card.is-featured{border:2px solid var(--blue);padding:19px}.plan-card header{display:flex;justify-content:space-between;gap:10px}.plan-card header span{font-size:13px;font-weight:800}.plan-card header p{margin:5px 0 0;color:var(--muted);font-size:7.5px}.plan-card header b{height:21px;padding:0 7px;display:flex;align-items:center;background:var(--blue-100);color:var(--blue);font-size:6.5px;white-space:nowrap}.plan-card>strong{display:block;margin:26px 0 22px;color:var(--blue);font-size:31px}.plan-card>strong small{color:var(--muted);font-size:8px}.plan-card ul{display:grid;gap:12px;margin:0 0 25px;padding:18px 0;border-block:1px solid var(--line);list-style:none}.plan-card li{display:flex;align-items:center;gap:7px;font-size:8px}.plan-card li svg{width:13px;color:var(--green)}.plan-card>.button{width:100%}.subscription-table{margin-top:11px;border-top:1px solid var(--line)}.table-head{height:58px;display:flex;align-items:center;justify-content:space-between;padding:0 15px;border-bottom:1px solid var(--line)}.table-head h2{margin:0;font-size:10px}.table-head span{color:var(--muted);font-size:7px}.table-head label{width:220px;height:32px;display:flex;align-items:center;gap:6px;padding:0 8px;border:1px solid var(--line);background:#f8fafc}.table-head svg{width:13px;color:var(--muted)}.table-head input{min-width:0;width:100%;border:0;outline:0;background:transparent;font-size:7.5px}.switch{width:32px;height:18px;padding:2px;border:0;border-radius:9px;background:#d0d5dd}.switch i{display:block;width:14px;height:14px;border-radius:50%;background:#fff}.switch.is-on{background:var(--blue)}.switch.is-on i{transform:translateX(14px)}.billing-metrics{grid-template-columns:repeat(4,1fr)}.billing-tabs{height:47px;display:flex;gap:22px;margin-top:11px;padding:0 15px;border:1px solid var(--line);border-bottom:0;background:#fff}.billing-tabs button{border:0;background:transparent;color:var(--muted);font-size:8px}.billing-tabs button.is-active{color:var(--blue);border-bottom:2px solid var(--blue);font-weight:700}.collection-console{display:grid;grid-template-columns:1.2fr .8fr;gap:11px}.collection-console>.panel{border-top:0}.collection-console article{min-height:70px;display:grid;grid-template-columns:34px minmax(0,1fr) auto auto;align-items:center;gap:9px;padding:9px 15px;border-bottom:1px solid var(--line)}.collection-console article>span{width:32px;height:32px;display:grid;place-items:center;background:#fff0ef;color:var(--red)}.collection-console article svg{width:15px}.collection-console article>div{display:grid;gap:4px}.collection-console article strong{font-size:8.5px}.collection-console article small{color:var(--muted);font-size:7px}.collection-console article>b{color:var(--red);font-size:8px}.policy-timeline{padding:15px}.policy-timeline p{position:relative;display:grid;gap:4px;margin:0;padding:0 0 20px 20px;border-left:1px solid var(--line)}.policy-timeline p:last-child{padding-bottom:0}.policy-timeline p:before{content:"";position:absolute;left:-4px;top:2px;width:7px;height:7px;border-radius:50%;background:var(--blue)}.policy-timeline b{font-size:8px}.policy-timeline span{color:var(--muted);font-size:7px}.usage-kpis{display:grid;grid-template-columns:repeat(4,1fr);margin-bottom:11px;border:1px solid var(--line);background:#fff}.usage-kpis>div{min-height:110px;display:grid;gap:6px;align-content:center;padding:16px 18px;border-right:1px solid var(--line)}.usage-kpis>div:last-child{border-right:0}.usage-kpis span,.usage-kpis small{color:var(--muted);font-size:7px}.usage-kpis strong{font-size:20px}.usage-kpis i.orange b{background:var(--orange)}.usage-cell{min-width:110px;display:grid;gap:6px}.usage-cell span{font-size:7px}.bank-layout{min-height:600px;display:grid;grid-template-columns:280px minmax(0,1fr);border:1px solid var(--line);background:#fff}.bank-tree{border-right:1px solid var(--line)}.bank-tree header{height:51px;display:flex;align-items:center;justify-content:space-between;padding:0 13px;border-bottom:1px solid var(--line)}.bank-tree header strong{font-size:9px}.bank-tree header button{border:0;background:transparent;color:var(--blue)}.bank-tree header svg{width:14px}.bank-tree>button{width:100%;min-height:57px;display:grid;grid-template-columns:28px 1fr 14px;align-items:center;gap:8px;padding:8px 12px;border:0;border-bottom:1px solid var(--line);background:#fff;text-align:left}.bank-tree>button.is-active{background:var(--blue-50);box-shadow:inset 3px 0 var(--blue)}.bank-tree>button>svg:first-child{width:16px;color:var(--blue)}.bank-tree>button>svg:last-child{width:13px;color:#98a2b3}.bank-tree>button span{display:grid;gap:4px}.bank-tree strong{font-size:8px}.bank-tree small{color:var(--muted);font-size:6.5px}.bank-content{min-width:0;padding:22px}.bank-content>header{display:flex;align-items:flex-start;justify-content:space-between}.bank-content header span{color:var(--blue);font-size:7px}.bank-content h2{margin:7px 0 5px;font-size:16px}.bank-content header p{margin:0;color:var(--muted);font-size:7.5px}.bank-stats{display:grid;grid-template-columns:repeat(4,1fr);margin:20px 0;border:1px solid var(--line)}.bank-stats>div{min-height:75px;display:grid;align-content:center;gap:5px;padding:13px;border-right:1px solid var(--line)}.bank-stats>div:last-child{border-right:0}.bank-stats span{color:var(--muted);font-size:7px}.bank-stats strong{font-size:17px}.version-card{min-height:115px;display:flex;align-items:center;justify-content:space-between;gap:15px;padding:17px;border:1px solid var(--line);margin-top:10px}.version-card h3{margin:8px 0 6px;font-size:12px}.version-card p{margin:0;color:var(--ink-2);font-size:8px}.version-card small{display:block;margin-top:8px;color:var(--muted);font-size:6.5px}.version-card>div:last-child{display:flex;gap:7px}.audit-filter label{min-width:320px}.audit-table code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:6.5px}.severity{display:inline-flex;align-items:center;justify-content:center;min-width:34px;height:20px;border-radius:4px;font-size:6.5px;font-weight:800}.severity.success{background:#edf9f2;color:var(--green)}.severity.warning{background:#fff5e8;color:#b96700}.severity.danger{background:#fff0ef;color:var(--red)}.severity.neutral{background:#f2f4f7;color:var(--muted)}.alert-layout{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(320px,.65fr);gap:11px}.security-alert{min-height:94px;display:grid;grid-template-columns:40px minmax(0,1fr) auto auto;align-items:center;gap:11px;padding:12px 15px;border-bottom:1px solid var(--line)}.alert-level{width:38px;height:38px;display:grid;place-items:center;border-radius:50%;font-size:7px;font-weight:800}.alert-level.danger{background:#fff0ef;color:var(--red)}.alert-level.warning{background:#fff5e8;color:#b96700}.security-alert>div{display:grid;gap:4px}.security-alert small,.security-alert em{color:var(--muted);font-size:6.5px;font-style:normal}.security-alert strong{font-size:8.5px}.security-alert p{margin:0;color:var(--ink-2);font-size:7px}.channel-panel{display:grid;align-content:start;gap:11px}.channel-row{min-height:65px;display:grid;grid-template-columns:32px minmax(0,1fr) auto 22px;align-items:center;gap:8px;padding:9px 13px;border-bottom:1px solid var(--line)}.channel-row>span{width:30px;height:30px;display:grid;place-items:center;background:var(--blue-100);color:var(--blue)}.channel-row svg{width:14px}.channel-row>div{display:grid;gap:4px}.channel-row strong{font-size:8px}.channel-row small{color:var(--muted);font-size:6.5px}.channel-row>button{border:0;background:transparent}.failure-panel>div:last-child{padding:15px}.failure-panel strong{font-size:8px}.failure-panel p{color:var(--muted);font-size:7px;line-height:1.6}.staff-layout{display:grid;grid-template-columns:1.1fr .9fr;gap:11px}.staff-row{min-height:67px;display:grid;grid-template-columns:32px minmax(0,1fr) 82px 90px auto 22px;align-items:center;gap:9px;padding:9px 14px;border-bottom:1px solid var(--line)}.staff-row>span{width:30px;height:30px;display:grid;place-items:center;border-radius:50%;background:var(--blue-100);color:var(--blue);font-size:8px}.staff-row>div{display:grid;gap:4px}.staff-row strong{font-size:8px}.staff-row small{color:var(--muted);font-size:6.5px}.staff-row>b,.staff-row>em{font-size:7px;font-style:normal;font-weight:500}.staff-row>button{border:0;background:transparent}.role-row{min-height:68px;display:grid;grid-template-columns:32px minmax(0,1fr) 30px auto;align-items:center;gap:9px;padding:9px 14px;border-bottom:1px solid var(--line)}.role-row>span{width:30px;height:30px;display:grid;place-items:center;background:#edf9f2;color:var(--green)}.role-row svg{width:14px}.role-row>div{display:grid;gap:4px}.role-row strong{font-size:8px}.role-row small{color:var(--muted);font-size:6.5px}.role-row>b{font-size:7px}.toast{position:fixed;z-index:100;left:50%;bottom:24px;min-height:40px;display:flex;align-items:center;gap:8px;padding:0 13px;background:#101828;color:#fff;box-shadow:0 12px 30px rgba(16,24,40,.24);transform:translate(-50%,16px);opacity:0;pointer-events:none;transition:.2s;font-size:8px}.toast.is-visible{transform:translate(-50%,0);opacity:1}.toast svg{width:14px;color:#78e2a8} +@media(max-width:1240px){:root{--sidebar:210px}.metric-grid{grid-template-columns:repeat(3,1fr)}.overview-grid{grid-template-columns:1fr}.overview-lower{grid-template-columns:1fr 1fr}.activity-panel{grid-column:1/-1}.lifecycle-grid,.collection-console,.alert-layout{grid-template-columns:1fr}.staff-layout{grid-template-columns:1fr}} +@media(max-width:900px){.platform-app{display:block}.platform-sidebar{transform:translateX(-100%);transition:.2s;box-shadow:18px 0 48px rgba(16,24,40,.28)}.platform-app.menu-open .platform-sidebar{transform:translateX(0)}.platform-workspace{min-width:0}.platform-topbar{height:58px;padding:0 12px;gap:9px}.mobile-menu{display:grid}.page-context small,.platform-search kbd,.top-status>span{display:none}.page-context{min-width:120px}.platform-search{width:auto;flex:1}#mainView{padding:18px 12px 32px}.page-head{align-items:flex-start;flex-direction:column}.page-head>div:last-child{width:100%;overflow:auto}.metric-grid{grid-template-columns:1fr 1fr}.overview-lower{grid-template-columns:1fr}.activity-panel{grid-column:auto}.filter-bar{align-items:stretch;flex-wrap:wrap}.filter-bar label{min-width:100%}.filter-bar select{flex:1}.filter-segment{width:100%;margin-left:0;overflow:auto}.filter-segment button{flex:1}.table-panel{border-top:1px solid var(--line)}.lifecycle-summary{grid-template-columns:1fr 1fr}.plan-grid{grid-template-columns:1fr}.collection-console{grid-template-columns:1fr}.usage-kpis{grid-template-columns:1fr 1fr}.bank-layout{display:block}.bank-tree{border-right:0;border-bottom:1px solid var(--line)}.bank-tree>button{display:inline-grid;width:260px;border-right:1px solid var(--line)}.bank-tree{overflow:auto;white-space:nowrap}.bank-tree header{position:sticky;left:0;background:#fff}.bank-content{padding:16px}.staff-layout{grid-template-columns:1fr}} +@media(max-width:560px){.page-context{display:none}.top-status .icon-button{display:none}.metric-grid{grid-template-columns:1fr}.metric{min-height:94px}.revenue-chart{height:230px;padding-inline:8px}.overview-lower{grid-template-columns:1fr}.tenant-health{grid-template-columns:1fr;justify-items:center}.usage-summary{grid-template-columns:1fr}.usage-summary>div{padding:12px 0;border-right:0;border-bottom:1px solid var(--line)}.usage-summary>div:last-child{border-bottom:0}.lifecycle-summary{grid-template-columns:1fr}.drawer-status{grid-template-columns:1fr 1fr}.drawer-status>div:nth-child(2){border-right:0}.drawer-status>div:nth-child(-n+2){border-bottom:1px solid var(--line)}.drawer-tabs{overflow:auto}.drawer-tabs button{min-width:max-content}.tenant-drawer dl{grid-template-columns:1fr}.billing-metrics{grid-template-columns:1fr}.billing-tabs{overflow:auto}.billing-tabs button{min-width:max-content}.usage-kpis{grid-template-columns:1fr}.usage-kpis>div{border-right:0;border-bottom:1px solid var(--line)}.bank-stats{grid-template-columns:1fr 1fr}.bank-stats>div:nth-child(2){border-right:0}.bank-stats>div:nth-child(-n+2){border-bottom:1px solid var(--line)}.version-card{align-items:flex-start;flex-direction:column}.security-alert{grid-template-columns:38px minmax(0,1fr)}.security-alert>b,.security-alert>.button{grid-column:2;width:max-content}.staff-row{grid-template-columns:31px minmax(0,1fr) auto}.staff-row>em,.staff-row>i{display:none}.role-row{grid-template-columns:30px minmax(0,1fr) auto}.role-row>.button{grid-column:2/-1}.tenant-drawer{width:100%}} +@media(prefers-reduced-motion:reduce){*,*:before,*:after{transition:none!important}} + +/* Global productive typography floor: dense, but never miniature. */ +:root{--text-caption:12px;--text-label:13px;--text-body:14px;--text-heading:18px;--text-title:24px;--line-caption:18px;--line-body:22px} +.platform-brand small,.environment,.environment b,.environment-switch,.environment-switch small,#sideNav section h2,.platform-foot small,.platform-foot>div>span,.platform-foot strong,.page-context small,.platform-search input,.platform-search kbd,.top-status,.top-status>span,.icon-button b,.page-head p,.metric small,.metric p,.panel-head span,.legend,.chart-axis,.bars em,.alert-row small,.health-donut span,.tenant-health p,.usage-summary span,.usage-summary small,.platform-activity>span,.platform-activity small,.filter-bar input,.filter-bar select,.platform-table th,.platform-table td>small,.tenant-cell small,.table-panel>footer,.tenant-drawer header small,.tenant-drawer header p,.drawer-status span,.drawer-tabs button,.tenant-drawer dt,.domain-item small,.drawer-usage span,.setup-card p,.plan-card header p,.plan-card header b,.plan-card>strong small,.table-head span,.table-head input,.billing-tabs button,.collection-console article small,.policy-timeline span,.usage-kpis span,.usage-kpis small,.usage-cell span,.bank-tree small,.bank-content header span,.bank-content header p,.bank-stats span,.version-card small,.audit-table code,.severity,.security-alert small,.security-alert em,.security-alert p,.alert-level,.channel-row small,.failure-panel p,.staff-row small,.staff-row>b,.staff-row>em,.role-row small,.role-row>b{font-size:var(--text-caption)!important;line-height:var(--line-caption)} +#sideNav button,.platform-foot>button,.page-context strong,.button,.control,.link,.filter-segment button,.platform-table td,.platform-table footer button,.tenant-drawer h3,.tenant-drawer dd,.domain-item strong,.drawer-usage strong,.setup-card header span,.setup-card header b,.plan-card li,.table-head h2,.collection-console article strong,.collection-console article>b,.policy-timeline b,.bank-tree header strong,.bank-tree strong,.version-card p,.security-alert strong,.channel-row strong,.failure-panel strong,.staff-row strong,.role-row strong,.status,.toast{font-size:var(--text-label)!important;line-height:20px} +.page-head h1{font-size:var(--text-title);line-height:32px}.panel-head h2,.tenant-drawer h2,.bank-content h2{font-size:var(--text-heading);line-height:26px}.metric strong{font-size:28px;line-height:36px}.usage-kpis strong{font-size:24px;line-height:32px}.plan-card>strong{font-size:36px;line-height:44px}.platform-table th{height:46px}.platform-table td{height:68px}.button,.control,.platform-search,.filter-bar label,.filter-bar select{min-height:40px}.icon-button{width:40px;height:40px}.status,.severity{min-height:24px;padding-inline:8px}.tenant-drawer{width:min(640px,100%)}.tenant-drawer>header>button{width:40px;height:40px;display:grid;place-items:center}.drawer-tabs{height:48px}.drawer-tabs button{padding-inline:12px}.tenant-drawer>footer{gap:8px} +@media(max-width:900px){:root{--text-body:15px;--text-label:14px}.button,.control,.icon-button{min-height:44px}.platform-table td{font-size:14px!important}.platform-table th{font-size:12px!important}} + +/* v1.1 specification interaction layer */ +.prototype-controls{display:flex;align-items:center;gap:7px}.prototype-controls label{height:40px;display:flex;align-items:center;gap:6px;padding:0 8px;border:1px solid var(--line);background:#fff}.prototype-controls span{color:var(--muted);font-size:12px;white-space:nowrap}.prototype-controls select{max-width:118px;border:0;outline:0;background:transparent;font-size:12px;color:var(--ink)} +.page-id{display:block;margin:0 0 5px;color:var(--blue);font-size:12px;font-weight:750}.page-head.is-subpage{padding-bottom:14px;border-bottom:1px solid var(--line)}.subpage-nav{display:flex!important;align-items:center;gap:8px!important;margin-bottom:14px}.subpage-nav .icon-button{min-height:40px}.cell-link{padding:0;border:0;background:transparent;color:inherit;text-align:left;font:inherit}.tenant-cell.cell-link{display:flex}.row-actions,.inline-actions{display:flex;align-items:center;gap:6px}.platform-table tr.is-highlighted td{background:var(--blue-50)} +.state-panel{min-height:300px;display:grid;place-items:center;align-content:center;gap:12px;padding:36px;border:1px solid var(--line);background:#fff;text-align:center}.state-panel>span{width:52px;height:52px;display:grid;place-items:center;border-radius:50%;background:var(--blue-100);color:var(--blue)}.state-panel>span svg{width:24px}.state-panel h2,.state-panel p{margin:0}.state-panel h2{font-size:20px}.state-panel p{max-width:650px;color:var(--muted);font-size:14px;line-height:22px}.state-panel>div{display:flex;gap:8px}.state-panel.is-error>span{background:#fff0ef;color:var(--red)}.state-panel.is-forbidden>span{background:#fff5e8;color:var(--orange)}.state-panel.is-partial{min-height:180px;margin-top:12px;background:#fffdf7}.skeleton-stack{display:grid;gap:13px;padding:28px;border:1px solid var(--line);background:#fff}.skeleton-stack i{width:var(--w);height:42px;background:linear-gradient(90deg,#f2f4f7,#fafafa,#f2f4f7);background-size:200% 100%;animation:skeleton 1.2s linear infinite}.state-hint{color:var(--muted);font-size:13px}@keyframes skeleton{to{background-position:-200% 0}} +.detail-summary{display:grid;grid-template-columns:repeat(5,1fr);margin-bottom:12px;border:1px solid var(--line);background:#fff}.detail-summary>div{min-height:84px;display:grid;align-content:center;gap:7px;padding:14px 17px;border-right:1px solid var(--line)}.detail-summary>div:last-child{border-right:0}.detail-summary span{color:var(--muted);font-size:12px}.detail-summary strong{font-size:16px}.detail-grid{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(340px,.75fr);gap:12px}.detail-list{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin:0;padding:22px}.detail-list div{display:grid;gap:6px}.detail-list dt{color:var(--muted);font-size:12px}.detail-list dd{margin:0;font-size:14px;line-height:22px}.action-panel>button{width:100%;min-height:72px;display:grid;grid-template-columns:32px 1fr 20px;align-items:center;gap:10px;padding:12px 16px;border:0;border-bottom:1px solid var(--line);background:#fff;text-align:left}.action-panel>button:hover{background:var(--blue-50)}.action-panel>button>svg:first-child{color:var(--blue)}.action-panel>button>span{display:grid;gap:5px}.action-panel>button strong{font-size:14px}.action-panel>button small{color:var(--muted);font-size:12px}.timeline-panel{margin-top:12px}.timeline{display:grid;grid-template-columns:repeat(4,1fr);padding:22px}.timeline p{position:relative;display:grid;gap:6px;margin:0;padding:0 20px 0 25px;border-top:2px solid var(--line)}.timeline p:before{content:"";position:absolute;left:0;top:-7px;width:12px;height:12px;border-radius:50%;background:#d0d5dd}.timeline p.done{border-color:#98b1ff}.timeline p.done:before{background:var(--blue)}.timeline p.risk{border-color:#ffb4ae}.timeline p.risk:before{background:var(--red)}.timeline b{padding-top:16px;font-size:14px}.timeline span{color:var(--muted);font-size:12px;line-height:18px} +.form-workspace{display:grid;gap:12px}.form-section{overflow:hidden}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px;padding:22px}.form-grid label,.modal-form>label,.alert-evidence>label{display:grid;align-content:start;gap:7px}.form-grid label.wide,.modal-detail .wide{grid-column:1/-1}.form-grid label>span,.modal-form label>span,.alert-evidence label>span{font-size:13px;font-weight:700}.form-grid input,.form-grid select,.form-grid textarea,.modal-form input,.modal-form select,.modal-form textarea,.alert-evidence textarea{width:100%;min-height:42px;padding:9px 11px;border:1px solid #d0d5dd;background:#fff;color:var(--ink);font-size:14px}.form-grid textarea,.modal-form textarea{resize:vertical}.form-grid input:disabled,.form-grid select:disabled{background:#f2f4f7;color:var(--muted)}.form-actions{position:sticky;bottom:0;z-index:4;display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:14px 18px;border:1px solid var(--line);background:#fff}.form-actions>span{margin-right:auto;color:var(--muted);font-size:12px}.form-error-summary{margin:0;padding:14px 17px;border:1px solid #ffb4ae;background:#fff0ef;color:#b42318;font-size:13px}.form-error-summary strong{font-size:14px}.form-error-summary ul{margin:8px 0 0;padding-left:20px}.field-error{color:#b42318;font-size:12px;line-height:18px}.is-invalid{border-color:var(--red)!important;box-shadow:0 0 0 3px rgba(217,45,32,.08)} +.toggle-label{display:flex;align-items:center;gap:7px;padding:0 10px;border:1px solid var(--line);background:#fff;font-size:13px}.modal-backdrop{position:fixed;z-index:80;inset:0;display:grid;place-items:center;padding:24px;background:rgba(16,24,40,.42)}.modal{width:min(620px,100%);max-height:min(820px,calc(100vh - 48px));display:flex;flex-direction:column;border:1px solid var(--line);background:#fff;box-shadow:0 24px 80px rgba(16,24,40,.25)}.modal.is-wide{width:min(880px,100%)}.modal>header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:20px 22px;border-bottom:1px solid var(--line)}.modal>header small{color:var(--blue);font-size:12px;font-weight:750}.modal>header h2{margin:5px 0;font-size:20px}.modal>header p{margin:0;color:var(--muted);font-size:13px;line-height:20px}.modal>header .icon-button{flex:0 0 auto}.modal>footer{display:flex;justify-content:flex-end;gap:8px;padding:14px 20px;border-top:1px solid var(--line);background:#fff}.modal-form{min-height:0;display:grid;gap:16px;padding:20px 22px;overflow:auto}.modal-form .form-grid{padding:0}.check-row{display:flex!important;align-items:center!important;grid-template-columns:20px 1fr;gap:8px!important;min-height:42px}.check-row input{width:18px!important;min-height:18px!important}.danger-notice{display:flex;gap:8px;padding:12px;border:1px solid #ffb4ae;background:#fff0ef;color:#b42318;font-size:13px;line-height:20px}.modal-form fieldset{margin:0;padding:16px;border:1px solid var(--line)}.modal-form legend{padding:0 6px;font-size:14px;font-weight:700}.permission-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px}.permission-grid label{display:flex;align-items:center;gap:7px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}.permission-grid input{width:16px;min-height:16px}.modal-detail{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin:0;padding:22px;overflow:auto}.modal-detail div{display:grid;gap:6px}.modal-detail dt{color:var(--muted);font-size:12px}.modal-detail dd{margin:0;font-size:14px;line-height:22px}.candidate-list,.task-list{display:grid;gap:0;min-height:0;overflow:auto}.candidate-list label{min-height:68px;display:grid;grid-template-columns:20px 1fr;align-items:center;gap:10px;padding:12px 22px;border-bottom:1px solid var(--line)}.candidate-list label>span{display:grid;gap:5px}.candidate-list strong{font-size:14px}.candidate-list small{color:var(--muted);font-size:12px}.candidate-list>p{padding:30px;text-align:center;color:var(--muted)}.alert-evidence{display:grid;gap:18px;padding:22px;overflow:auto}.evidence-head{display:grid;grid-template-columns:42px 1fr auto;align-items:center;gap:12px}.evidence-head>div{display:grid;gap:5px}.evidence-head strong{font-size:15px}.evidence-head small{color:var(--muted);font-size:12px}.alert-evidence ul{display:grid;gap:8px;margin:0;padding:0;list-style:none}.alert-evidence li{display:flex;align-items:center;gap:8px;padding:10px 12px;background:#f8fafc;font-size:13px}.alert-evidence li svg{width:15px;color:var(--blue)}.task-list article{min-height:80px;display:grid;grid-template-columns:38px 1fr auto;align-items:center;gap:12px;padding:12px 20px;border-bottom:1px solid var(--line)}.task-state{width:36px;height:36px;display:grid;place-items:center;border-radius:50%;background:#f2f4f7;color:var(--muted)}.task-state.success{background:#edf9f2;color:var(--green)}.task-state.danger{background:#fff0ef;color:var(--red)}.task-state.warning svg{animation:spin 1s linear infinite}.task-list article>div{display:grid;gap:4px}.task-list strong{font-size:14px}.task-list small,.task-list p{margin:0;color:var(--muted);font-size:12px;line-height:18px}@keyframes spin{to{transform:rotate(360deg)}} +.toast.is-error svg{color:#ffb4ae}.failure-panel article{padding:0}.channel-row .row-actions{justify-content:flex-end}.collection-console article .inline-actions{justify-content:flex-end}.collection-console article .inline-actions .button{white-space:nowrap} +.confirm-impact{display:grid;grid-template-columns:42px 1fr;align-items:center;gap:12px;padding:22px}.confirm-impact>span{width:40px;height:40px;display:grid;place-items:center;border-radius:50%;background:#fff0ef;color:var(--red)}.confirm-impact p{margin:0;font-size:14px;line-height:22px}.icons-unavailable [data-lucide]{display:none}.icons-unavailable .button:empty,.icons-unavailable .icon-button:empty{display:none} +.unsaved-leave-guard{position:fixed;z-index:140;inset:0;display:grid;place-items:center;padding:24px;background:rgba(16,24,40,.5)}.leave-guard-card{width:min(520px,100%);box-shadow:0 28px 90px rgba(16,24,40,.28)} +.alert-row strong,.platform-activity p{font-size:13px;line-height:20px}.platform-activity p strong{font-size:13px}.tenant-health p strong{font-size:13px}.failure-panel article strong{font-size:13px}.failure-panel article p{font-size:12px;line-height:18px} +.platform-activity>div>span,#sideNav b{font-size:12px!important}.platform-foot>div>span{font-size:13px!important}.mobile-boundary{display:grid;grid-template-columns:48px 1fr;align-items:start;gap:14px;padding:22px}.mobile-boundary>span{width:46px;height:46px;display:grid;place-items:center;background:var(--blue-100);color:var(--blue)}.mobile-boundary strong{font-size:15px}.mobile-boundary p{margin:6px 0 0;color:var(--muted);font-size:13px;line-height:20px} +.tenant-cell>span,.staff-row>span{font-size:12px!important}.tenant-cell strong{font-size:13px!important;line-height:20px} +.sidebar-backdrop{display:none;position:fixed;z-index:29;inset:0;border:0;background:rgba(16,24,40,.42)} +@media(max-width:1320px){.prototype-controls label span{display:none}.prototype-controls select{max-width:105px}.detail-summary{grid-template-columns:repeat(3,1fr)}.detail-summary>div:nth-child(3){border-right:0}.detail-summary>div:nth-child(-n+3){border-bottom:1px solid var(--line)}} +@media(max-width:1050px){.prototype-controls{display:none}.detail-grid{grid-template-columns:1fr}.timeline{grid-template-columns:1fr}.timeline p{padding-bottom:16px;border-top:0;border-left:2px solid var(--line)}.timeline p:before{top:0;left:-7px}.timeline b{padding-top:0}.detail-summary{grid-template-columns:1fr 1fr}.detail-summary>div:nth-child(3){border-right:1px solid var(--line)}.detail-summary>div:nth-child(even){border-right:0}.detail-summary>div{border-bottom:1px solid var(--line)}} +@media(max-width:900px){body.menu-locked{overflow:hidden}.platform-app.menu-open+.sidebar-backdrop{display:block}.modal-backdrop{padding:0}.modal,.modal.is-wide{width:100%;height:100%;max-height:none}.modal>header{padding:16px}.modal>footer{padding:12px 14px}.form-grid,.modal-form .form-grid,.modal-detail,.permission-grid{grid-template-columns:1fr}.form-grid label.wide,.modal-detail .wide{grid-column:auto}.form-actions{position:static;bottom:auto;z-index:auto;flex-wrap:wrap}.form-actions>span{width:100%;margin:0}.row-action,.switch{position:relative;width:44px;height:44px;flex:0 0 44px}.row-action{background:transparent}.row-action:before{content:"";position:absolute;inset:8px;background:transparent}.row-action:hover{background:transparent}.row-action:hover:before{background:var(--blue-100)}.row-action svg{position:relative;z-index:1}.switch{display:grid;place-items:center;padding:0;border-radius:0;background:transparent}.switch:before{content:"";position:absolute;left:6px;top:13px;width:32px;height:18px;border-radius:9px;background:#d0d5dd}.switch i{position:relative;z-index:1;transform:translateX(-7px)}.switch.is-on{background:transparent}.switch.is-on:before{background:var(--blue)}.switch.is-on i{transform:translateX(7px)}.detail-summary{grid-template-columns:1fr 1fr}.collection-console article{grid-template-columns:34px minmax(0,1fr)}.collection-console article>b,.collection-console article>.inline-actions{grid-column:2}.platform-topbar{overflow:hidden}.platform-search{min-width:0}} +@media(max-width:900px){.unsaved-leave-guard{padding:0;align-items:end}.unsaved-leave-guard .modal{width:100%;height:auto;max-height:100vh}.leave-guard-card footer{flex-wrap:wrap}.leave-guard-card footer .button{min-height:44px;flex:1}} +@media(max-width:560px){.detail-summary{grid-template-columns:1fr}.detail-summary>div{border-right:0!important}.page-head.is-subpage>div:last-child{display:none}.modal>footer{overflow:auto;justify-content:flex-start}.modal>footer .button{white-space:nowrap}.form-actions .button{flex:1;min-width:0;white-space:nowrap}.alert-layout,.channel-panel,.panel{min-width:0}.panel-head{align-items:flex-start;flex-wrap:wrap;padding-block:12px}.panel-head>.filter-segment{max-width:100%;overflow:auto}.security-alert{grid-template-columns:38px minmax(0,1fr)}.security-alert .inline-actions{grid-column:2;flex-wrap:wrap;min-width:0}.security-alert .inline-actions .button{min-width:0;white-space:normal}.task-list article{grid-template-columns:34px 1fr}.task-list article>.button,.task-list article>.status{grid-column:2;width:max-content}.evidence-head{grid-template-columns:40px 1fr}.evidence-head>.status{grid-column:2;width:max-content}.top-status .icon-button:first-of-type{display:grid}.staff-row{grid-template-columns:32px minmax(0,1fr) auto}.staff-row>b,.staff-row>em,.staff-row>i{display:none}.mobile-boundary{grid-template-columns:1fr}.mobile-boundary>span{width:44px;height:44px}} +.contract-result{display:grid;grid-template-columns:24px minmax(0,1fr);align-items:center;gap:10px;margin:0 0 12px;padding:10px 12px;border:1px solid #b7e4c7;background:#f2fbf5;color:#17663a}.contract-result>svg{width:18px}.contract-result>span{display:grid;gap:3px}.contract-result strong{font-size:13px}.contract-result small{color:#52705f;font-size:12px;line-height:18px}.contract-result.is-loading{border-color:#bfd0ff;background:var(--blue-50);color:var(--blue)}.contract-result.is-loading>svg{animation:spin 1s linear infinite}.contract-result.is-error{border-color:#ffb4ae;background:#fff0ef;color:#b42318}.contract-result.is-error small{color:#8f2d25}.candidate-list label:has(>.button){grid-template-columns:20px minmax(0,1fr) auto}.alert-rules{margin-bottom:12px}.alert-rules .panel-head code{max-width:46%;overflow:hidden;color:var(--muted);font-size:12px;text-overflow:ellipsis;white-space:nowrap}.rule-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr))}.rule-grid article{min-height:106px;display:grid;grid-template-columns:42px minmax(0,1fr) auto;align-items:start;gap:10px;padding:16px;border-right:1px solid var(--line);border-top:1px solid var(--line)}.rule-grid article:last-child{border-right:0}.rule-grid article>div{display:grid;gap:5px}.rule-grid strong{font-size:13px}.rule-grid small,.rule-grid p{margin:0;color:var(--muted);font-size:12px;line-height:18px}.tree-empty{padding:18px;color:var(--muted);font-size:13px;text-align:center} +@media(max-width:1050px){.rule-grid{grid-template-columns:1fr}.rule-grid article{border-right:0}.candidate-list label:has(>.button){grid-template-columns:20px minmax(0,1fr)}.candidate-list label>.button{grid-column:2;width:max-content}} +/* Real API authentication gate. Tokens are held in sessionStorage by platform-auth.js. */ +.platform-auth-gate { position: fixed; inset: 0; z-index: 10000; display: grid; place-items: center; padding: 24px; background: radial-gradient(circle at 20% 10%, #17345f 0, #0b1220 42%, #060a12 100%); color: #e5e7eb; } +.platform-auth-gate[hidden] { display: none; } +.platform-auth-card { width: min(460px, 100%); padding: 32px; border: 1px solid rgba(148,163,184,.22); border-radius: 18px; background: rgba(15,23,42,.96); box-shadow: 0 24px 80px rgba(0,0,0,.45); } +.platform-auth-card header { display: flex; gap: 14px; align-items: center; margin-bottom: 26px; } +.platform-auth-card header img { width: 44px; height: 44px; border-radius: 12px; } +.platform-auth-card h1 { margin: 0; font-size: 22px; } +.platform-auth-card header p, .platform-auth-card .auth-help { margin: 5px 0 0; color: #94a3b8; font-size: 13px; line-height: 1.6; } +.platform-auth-card form { display: grid; gap: 16px; } +.platform-auth-card label { display: grid; gap: 7px; color: #cbd5e1; font-size: 13px; } +.platform-auth-card input { width: 100%; box-sizing: border-box; padding: 11px 12px; border: 1px solid #334155; border-radius: 9px; background: #0b1220; color: #f8fafc; font: inherit; } +.platform-auth-card input:focus { outline: 2px solid #3b82f6; outline-offset: 1px; } +.platform-auth-card button { width: 100%; min-height: 42px; margin-top: 14px; border: 0; border-radius: 9px; background: #2563eb; color: white; font: inherit; font-weight: 700; cursor: pointer; } +.platform-auth-card button:disabled { opacity: .55; cursor: wait; } +.platform-auth-card code, .platform-auth-recovery { display: block; margin-top: 8px; padding: 12px; border-radius: 8px; background: #020617; color: #93c5fd; overflow-wrap: anywhere; } +.platform-auth-error { min-height: 20px; margin: 14px 0 0; color: #fca5a5; font-size: 13px; } diff --git a/Tiku.DbMigrator/Program.cs b/Tiku.DbMigrator/Program.cs index 127a3dd..46a002b 100644 --- a/Tiku.DbMigrator/Program.cs +++ b/Tiku.DbMigrator/Program.cs @@ -3,12 +3,19 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Tiku.Infrastructure; using Tiku.Infrastructure.Persistence; using Tiku.Infrastructure.Bootstrap; using Tiku.Application; var builder = Host.CreateApplicationBuilder(args); +builder.Logging.AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning); +var isDevelopment = builder.Environment.IsDevelopment() || + string.Equals( + Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"), + Environments.Development, + StringComparison.OrdinalIgnoreCase); var bootstrapPlatformAdmin = args.Contains("--bootstrap-platform-admin", StringComparer.Ordinal); PlatformAdminBootstrapOptions? bootstrapOptions = null; if (bootstrapPlatformAdmin) @@ -22,11 +29,17 @@ if (bootstrapPlatformAdmin) var connectionString = builder.Configuration.GetConnectionString("Database") ?? Environment.GetEnvironmentVariable("DATABASE_URL") ?? - throw new InvalidOperationException( - "Database connection is required. Configure ConnectionStrings:Database or DATABASE_URL."); + (isDevelopment + ? $"Host=localhost;Database=tiku;Username={Environment.UserName}" + : throw new InvalidOperationException( + "Database connection is required outside Development. Configure ConnectionStrings:Database or DATABASE_URL.")); builder.Services.AddApplication(); -builder.Services.AddInfrastructure(connectionString); +builder.Services.AddInfrastructure( + connectionString, + isDevelopment && !bootstrapPlatformAdmin + ? DevelopmentPlatformAdminSeeder.Configure + : null); // Resolving UserManager also activates Identity's default token providers. // Bootstrap never issues a reset token, so the migrator uses a process-local provider; // the API remains the sole owner of the persisted, certificate-protected key ring. diff --git a/Tiku.Infrastructure/Bootstrap/DevelopmentPlatformAdminSeeder.cs b/Tiku.Infrastructure/Bootstrap/DevelopmentPlatformAdminSeeder.cs new file mode 100644 index 0000000..b59af7d --- /dev/null +++ b/Tiku.Infrastructure/Bootstrap/DevelopmentPlatformAdminSeeder.cs @@ -0,0 +1,192 @@ +using System.Security.Cryptography; +using System.Text.Json; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Npgsql; +using Tiku.Application.Security; +using Tiku.Domain.Identity; +using Tiku.Domain.Operations; +using Tiku.Infrastructure.Persistence; + +namespace Tiku.Infrastructure.Bootstrap; + +public static class DevelopmentPlatformAdminSeeder +{ + public const string Email = "admin@tiku.local"; + public const string RoleCode = PlatformAdminBootstrapper.SuperAdminRoleCode; + + public static void Configure(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseSeeding((context, _) => Seed((TikuDbContext)context)) + .UseAsyncSeeding((context, _, cancellationToken) => + SeedAsync((TikuDbContext)context, cancellationToken)); + } + + public static bool Seed(TikuDbContext dbContext) + { + ReloadPostgresTypes(dbContext); + + if (dbContext.PlatformBackendUserRoles.Any()) + { + return false; + } + + var temporaryPassword = GenerateTemporaryPassword(); + EnsureEmailIsAvailable(dbContext.Users.Any(user => + user.NormalizedEmail == Email.ToUpperInvariant() || + user.NormalizedUserName == Email.ToUpperInvariant())); + var permissionCodes = BackendPermissions.Platform.ToArray(); + var existingPermissionCodes = dbContext.BackendPermissions + .Where(permission => permissionCodes.Contains(permission.Code)) + .Select(permission => permission.Code) + .ToHashSet(StringComparer.Ordinal); + AddSeedGraph(dbContext, temporaryPassword, permissionCodes, existingPermissionCodes); + dbContext.SaveChanges(); + WriteFirstLoginInstructions(temporaryPassword); + return true; + } + + public static async Task SeedAsync( + TikuDbContext dbContext, + CancellationToken cancellationToken = default) + { + await ReloadPostgresTypesAsync(dbContext, cancellationToken); + + if (await dbContext.PlatformBackendUserRoles.AnyAsync(cancellationToken)) + { + return false; + } + + var temporaryPassword = GenerateTemporaryPassword(); + var normalizedEmail = Email.ToUpperInvariant(); + EnsureEmailIsAvailable(await dbContext.Users.AnyAsync(user => + user.NormalizedEmail == normalizedEmail || + user.NormalizedUserName == normalizedEmail, cancellationToken)); + var permissionCodes = BackendPermissions.Platform.ToArray(); + var existingPermissionCodes = (await dbContext.BackendPermissions + .Where(permission => permissionCodes.Contains(permission.Code)) + .Select(permission => permission.Code) + .ToArrayAsync(cancellationToken)) + .ToHashSet(StringComparer.Ordinal); + AddSeedGraph(dbContext, temporaryPassword, permissionCodes, existingPermissionCodes); + await dbContext.SaveChangesAsync(cancellationToken); + WriteFirstLoginInstructions(temporaryPassword); + return true; + } + + private static void AddSeedGraph( + TikuDbContext dbContext, + string temporaryPassword, + IReadOnlyCollection permissionCodes, + IReadOnlySet existingPermissionCodes) + { + var normalizedEmail = Email.ToUpperInvariant(); + var user = new User + { + Email = Email, + NormalizedEmail = normalizedEmail, + UserName = Email, + NormalizedUserName = normalizedEmail, + Name = "Local Platform Administrator", + EmailConfirmed = true, + Status = UserStatus.Active, + ForcePasswordChange = true, + TwoFactorEnabled = false + }; + var passwordHasher = new PasswordHasher(Options.Create(new PasswordHasherOptions + { + IterationCount = 210_000 + })); + user.PasswordHash = passwordHasher.HashPassword(user, temporaryPassword); + + var role = new PlatformBackendRole + { + Code = RoleCode, + Name = "Platform Super Administrator", + Description = "Built-in Development administrator created by EF Core data seeding.", + Status = BackendRoleStatus.Active, + IsSystem = true + }; + dbContext.Users.Add(user); + dbContext.PlatformBackendRoles.Add(role); + dbContext.BackendPermissions.AddRange( + permissionCodes + .Where(code => !existingPermissionCodes.Contains(code)) + .Select(code => new BackendPermission + { + Code = code, + Name = code, + Area = BackendPermissionArea.Platform, + Module = "platform", + Description = "Built-in platform permission.", + IsSystem = true + })); + dbContext.PlatformBackendRolePermissions.AddRange( + permissionCodes.Select(code => new PlatformBackendRolePermission + { + RoleId = role.Id, + PermissionCode = code + })); + dbContext.PlatformBackendUserRoles.Add(new PlatformBackendUserRole + { + UserId = user.Id, + RoleId = role.Id + }); + dbContext.AuditLogs.Add(new AuditLog + { + ActorUserId = user.Id, + Action = "platform.bootstrap_admin.created", + TargetType = "users", + TargetId = user.Id.ToString(), + Details = JsonSerializer.SerializeToElement(new + { + user.Email, + RoleCode, + ForcePasswordChange = true, + MfaEnrollmentRequired = true, + Source = "ef_core_use_seeding" + }) + }); + } + + private static string GenerateTemporaryPassword() => + $"Tiku!{Convert.ToHexString(RandomNumberGenerator.GetBytes(16))}9a"; + + private static void ReloadPostgresTypes(TikuDbContext dbContext) + { + if (dbContext.Database.IsNpgsql()) + { + ((NpgsqlConnection)dbContext.Database.GetDbConnection()).ReloadTypes(); + } + } + + private static async Task ReloadPostgresTypesAsync( + TikuDbContext dbContext, + CancellationToken cancellationToken) + { + if (dbContext.Database.IsNpgsql()) + { + await ((NpgsqlConnection)dbContext.Database.GetDbConnection()) + .ReloadTypesAsync(cancellationToken); + } + } + + private static void EnsureEmailIsAvailable(bool isAssigned) + { + if (isAssigned) + { + throw new InvalidOperationException( + $"Cannot create the Development platform administrator because '{Email}' is already assigned."); + } + } + + private static void WriteFirstLoginInstructions(string temporaryPassword) + { + Console.WriteLine("Development platform administrator created by EF Core data seeding."); + Console.WriteLine($" Account: {Email}"); + Console.WriteLine($" Temporary password: {temporaryPassword}"); + Console.WriteLine(" Change the password and enroll TOTP MFA at first sign-in. This password is shown only once."); + } +} diff --git a/Tiku.Infrastructure/DependencyInjection.cs b/Tiku.Infrastructure/DependencyInjection.cs index 336e9b4..ed2e177 100644 --- a/Tiku.Infrastructure/DependencyInjection.cs +++ b/Tiku.Infrastructure/DependencyInjection.cs @@ -51,7 +51,8 @@ public static class DependencyInjection { public static IServiceCollection AddInfrastructure( this IServiceCollection services, - string connectionString) + string connectionString, + Action? configureDatabase = null) { ArgumentException.ThrowIfNullOrWhiteSpace(connectionString); @@ -62,6 +63,7 @@ public static class DependencyInjection var dataSource = serviceProvider.GetRequiredService(); options.UseNpgsql(dataSource, npgsql => npgsql.MigrationsAssembly(typeof(TikuDbContext).Assembly.FullName)); + configureDatabase?.Invoke(options); options.AddInterceptors(serviceProvider.GetRequiredService()); }); services.AddIdentityCore(options => diff --git a/Tiku.IntegrationTests/Api/PlatformAdminStaticEndpointTests.cs b/Tiku.IntegrationTests/Api/PlatformAdminStaticEndpointTests.cs new file mode 100644 index 0000000..8a29e76 --- /dev/null +++ b/Tiku.IntegrationTests/Api/PlatformAdminStaticEndpointTests.cs @@ -0,0 +1,53 @@ +using System.Net; + +namespace Tiku.IntegrationTests.Api; + +public sealed class PlatformAdminStaticEndpointTests +{ + [Fact] + public async Task Platform_admin_static_shell_is_served_without_api_authentication() + { + await using var factory = new ApiTestFactory(); + using var client = factory.CreateClient(); + + var response = await client.GetAsync("/platform-admin/"); + var body = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("恭学题库 SaaS · 平台管理", body, StringComparison.Ordinal); + Assert.Contains("./platform-api.js", body, StringComparison.Ordinal); + Assert.Contains("./platform-auth.js", body, StringComparison.Ordinal); + Assert.Contains("platformAuthGate", body, StringComparison.Ordinal); + } + + [Fact] + public async Task Platform_admin_short_path_redirects_to_static_shell_directory() + { + await using var factory = new ApiTestFactory(); + using var client = factory.CreateClient(new() + { + AllowAutoRedirect = false + }); + + var response = await client.GetAsync("/platform-admin"); + + Assert.Equal(HttpStatusCode.MovedPermanently, response.StatusCode); + Assert.Equal("/platform-admin/", response.Headers.Location?.AbsolutePath); + } + + [Fact] + public async Task Platform_admin_runtime_uses_real_api_without_mock_fallback() + { + await using var factory = new ApiTestFactory(); + using var client = factory.CreateClient(); + + var runtime = await client.GetStringAsync("/platform-admin/runtime-config.js"); + var authentication = await client.GetStringAsync("/platform-admin/platform-auth.js"); + + Assert.Contains("mode: 'api'", runtime, StringComparison.Ordinal); + Assert.Contains("fallbackToMock: false", runtime, StringComparison.Ordinal); + Assert.Contains("/api/auth/login/password", authentication, StringComparison.Ordinal); + Assert.Contains("/api/auth/password/change-required", authentication, StringComparison.Ordinal); + Assert.Contains("/api/auth/mfa/totp/confirm", authentication, StringComparison.Ordinal); + } +} diff --git a/Tiku.UnitTests/Bootstrap/PlatformAdminBootstrapperTests.cs b/Tiku.UnitTests/Bootstrap/PlatformAdminBootstrapperTests.cs index 03e0560..2e530c6 100644 --- a/Tiku.UnitTests/Bootstrap/PlatformAdminBootstrapperTests.cs +++ b/Tiku.UnitTests/Bootstrap/PlatformAdminBootstrapperTests.cs @@ -82,6 +82,43 @@ public sealed class PlatformAdminBootstrapperTests Assert.Equal("bootstrap_user_already_exists", exception.Code); } + [Fact] + public async Task Development_async_seed_creates_one_forced_enrollment_administrator() + { + await using var provider = CreateProvider(); + await using var scope = provider.CreateAsyncScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + var created = await DevelopmentPlatformAdminSeeder.SeedAsync(context); + var createdAgain = await DevelopmentPlatformAdminSeeder.SeedAsync(context); + + Assert.True(created); + Assert.False(createdAgain); + var user = await context.Users.SingleAsync(); + Assert.Equal(DevelopmentPlatformAdminSeeder.Email, user.Email); + Assert.True(user.ForcePasswordChange); + Assert.False(user.TwoFactorEnabled); + Assert.Equal(BackendPermissions.Platform.Count, await context.PlatformBackendRolePermissions.CountAsync()); + Assert.Single(context.PlatformBackendUserRoles); + Assert.Single(context.AuditLogs); + } + + [Fact] + public async Task Development_sync_seed_is_idempotent() + { + await using var provider = CreateProvider(); + await using var scope = provider.CreateAsyncScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + var created = DevelopmentPlatformAdminSeeder.Seed(context); + var createdAgain = DevelopmentPlatformAdminSeeder.Seed(context); + + Assert.True(created); + Assert.False(createdAgain); + Assert.Single(context.Users); + Assert.Single(context.PlatformBackendUserRoles); + } + private static ServiceProvider CreateProvider() { var services = new ServiceCollection(); diff --git a/Tiku.Worker/appsettings.Development.json b/Tiku.Worker/appsettings.Development.json index b2dcdb6..fc7057b 100644 --- a/Tiku.Worker/appsettings.Development.json +++ b/Tiku.Worker/appsettings.Development.json @@ -2,6 +2,7 @@ "Logging": { "LogLevel": { "Default": "Information", + "Microsoft.EntityFrameworkCore": "Warning", "Microsoft.Hosting.Lifetime": "Information" } } diff --git a/Tiku.Worker/appsettings.json b/Tiku.Worker/appsettings.json index f6742fb..1cbdfef 100644 --- a/Tiku.Worker/appsettings.json +++ b/Tiku.Worker/appsettings.json @@ -12,6 +12,7 @@ "Logging": { "LogLevel": { "Default": "Information", + "Microsoft.EntityFrameworkCore": "Warning", "Microsoft.Hosting.Lifetime": "Information" } } diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..a3e7bb2 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,138 @@ +# 本地开发快速开始 + +这份文档用于从全新开发环境启动 TIKU Backend、初始化 PostgreSQL,并完成平台管理员的首次登录。 + +## 1. 准备环境 + +需要安装: + +- .NET 10 SDK; +- PostgreSQL(当前本地开发已验证 PostgreSQL 18); +- `psql`、`createdb` 等 PostgreSQL 命令行工具。 + +确认工具可用: + +```bash +dotnet --version +pg_isready -h 127.0.0.1 -p 5432 +psql --version +``` + +## 2. 获取并还原项目 + +```bash +git clone TIKU-BACKEND +cd TIKU-BACKEND +dotnet restore TIKU-BACKEND.slnx +dotnet build TIKU-BACKEND.slnx --no-restore +``` + +## 3. 创建本地数据库 + +如果本机 PostgreSQL 允许当前系统用户无密码登录,可以直接执行: + +```bash +createdb -h 127.0.0.1 -U "$(whoami)" tiku +``` + +Development 环境未显式配置连接串时,API 和 DbMigrator 默认使用: + +```text +Host=localhost;Database=tiku;Username=<当前系统用户> +``` + +如果数据库用户名、端口或认证方式不同,通过环境变量传入连接串: + +```bash +export DATABASE_URL='Host=127.0.0.1;Port=5432;Database=tiku;Username=<数据库用户>;Password=<本地密码>' +``` + +不要把包含密码的连接串写进 README、`appsettings*.json` 或提交到 Git。团队成员应各自使用环境变量、.NET Secret Manager 或受控密钥存储。 + +## 4. 执行迁移并初始化管理员 + +```bash +ASPNETCORE_ENVIRONMENT=Development dotnet run --project Tiku.DbMigrator +``` + +DbMigrator 会执行全部 EF Core Migration,并在全新 Development 数据库中自动创建平台超级管理员: + +```text +账号:admin@tiku.local +密码:首次初始化时安全随机生成,只在当前终端输出一次 +``` + +请立即保存终端显示的临时密码。重复执行 DbMigrator 是幂等的,不会重复创建管理员、重置密码或再次显示密码。 + +管理员首次登录后必须: + +1. 修改临时密码; +2. 绑定 TOTP MFA; +3. 保存一次性恢复码。 + +如果数据库已经包含平台管理员,自动初始化会跳过。不要为了重新获取密码删除包含业务数据的数据库。 + +## 5. 启动 API 和平台后台 + +```bash +dotnet run --project Tiku.Api +``` + +默认开发地址: + +- 平台后台: +- Scalar API 文档: +- OpenAPI JSON: +- 健康检查: + +平台后台默认连接同源真实 API,不会回退到 Mock 数据。当前开放的是已有后端契约的概览、租户、员工、审计和告警等页面;尚未接入真实接口的模块暂不开放。 + +## 6. 可选:启动 Worker + +需要调试后台任务时,另开终端并使用相同数据库连接: + +```bash +dotnet run --project Tiku.Worker +``` + +普通 API 开发不要求同时启动 Worker。 + +## 7. 开发前验证 + +```bash +curl --fail http://localhost:5090/api/health +dotnet test TIKU-BACKEND.slnx --no-build +dotnet format TIKU-BACKEND.slnx --verify-no-changes --no-restore +dotnet ef migrations has-pending-model-changes \ + --project Tiku.Infrastructure \ + --startup-project Tiku.DbMigrator \ + --no-build +git diff --check +``` + +PostgreSQL 特有的 Migration、约束、事务和租户隔离行为必须使用真实 PostgreSQL 验证,不能只依赖 EF InMemory 测试。 + +## 常见问题 + +### 连接 PostgreSQL 失败 + +先检查服务和实际登录信息: + +```bash +pg_isready -h 127.0.0.1 -p 5432 +psql -h 127.0.0.1 -U <数据库用户> -d postgres -c 'select current_user;' +``` + +然后确认当前终端中的 `DATABASE_URL` 指向正确的主机、端口、数据库和用户。 + +### 首次迁移无法创建扩展 + +Migration 会创建 `citext` 和 `ltree` 扩展。初始化数据库的 PostgreSQL 用户必须有安装这些扩展所需的权限;请让本地数据库管理员预先安装扩展或授予对应权限。 + +### 没看到管理员临时密码 + +临时密码只在全新 Development 数据库首次创建管理员时显示。如果管理员绑定已经存在,迁移会安全跳过。请使用已有管理员账号的密码恢复流程,不要在源码或文档中添加固定密码。 + +### API 启动后出现 HTTPS 重定向警告 + +本地仅使用 HTTP profile 时可能看到无法确定 HTTPS 端口的警告,不影响 `http://localhost:5090` 的开发访问。需要验证 HTTPS 时使用项目的 `https` launch profile。